From 20e099ce76c0feab88b3101536a9772779cef643 Mon Sep 17 00:00:00 2001 From: Dawid Dworak Date: Wed, 27 May 2026 17:53:13 +0200 Subject: [PATCH 1/3] Introduce scalafmt (config + sbt-scalafmt plugin) --- .scalafmt.conf | 102 ++++++++++++++++++++++++++++++++++++++++++++ project/plugins.sbt | 1 + 2 files changed, 103 insertions(+) create mode 100644 .scalafmt.conf diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 000000000..914b28f90 --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,102 @@ +//description of properties: https://scalameta.org/scalafmt/docs/configuration.html +version = "3.11.1" +runner.dialect = Scala213Source3 +maxColumn = 120 + +continuationIndent { + defnSite = 2 + ctorSite = 2 + extendSite = 2 + withSiteRelativeToExtends = 2 +} + +align.preset = none + +binPack { + parentConstructors = OnelineIfPrimaryOneline + literalsSingleLine = false +} + +newlines { + alwaysBeforeMultilineDef = false + afterCurlyLambda = preserve + implicitParamListModifierPrefer = after + penalizeSingleSelectMultiArgList = false + avoidForSimpleOverflow = [punct, tooLong] + afterInfix = some +} + +rewrite.rules = [AvoidInfix, SortModifiers, SortImports, PreferCurlyFors, RedundantParens, RedundantBraces] +rewrite.sortModifiers.order = ["override", "private", "protected", "implicit", "final", "sealed", "abstract", "lazy"] +rewrite.redundantBraces { + stringInterpolation = true + generalExpressions = false + methodBodies = true + includeUnitMethods = false + parensForOneLineApply = true +} + +trailingCommas = multiple +importSelectors = singleLine + +optIn { + breakChainOnFirstMethodDot = false + # - if newlines.source is missing or keep: + # - if true, will keep existing line breaks around annotations + # - if newlines.source is fold: + # - if true, will break before the entity being annotatated + # - will not force break between consecutive annotations + # - if newlines.source is unfold: + # - if true, will break between consecutive annotations + # - will always break before the entity being annotatated + annotationNewlines = false +} + +rewrite.neverInfix.excludeFilters = [ + until + to + by + eq + ne + "should.*" + "contain.*" + "must.*" + in + ignore + be + taggedAs + thrownBy + synchronized + have + when + size + only + noneOf + oneElementOf + noElementsOf + atLeastOneElementOf + atMostOneElementOf + allElementsOf + inOrderElementsOf + theSameElementsAs + //below extends default + theSameElementsInOrderAs + like + zip + orElse + getOrElse + matchOpt + map + flatMap +] + +xmlLiterals.assumeFormatted = true +project.git = true +danglingParentheses.exclude = [ + // "class", //with that, trailling comma don't work in class definition + "trait" +] +verticalMultiline { + atDefnSite = true + newlineAfterOpenParen = true +} diff --git a/project/plugins.sbt b/project/plugins.sbt index b422aa9ea..dbcf6656c 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -13,3 +13,4 @@ addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.2") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.8") addSbtPlugin("com.github.sbt" % "sbt-github-actions" % "0.30.0") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.5") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.1") From 5988da35cc34302c23e39527c8569322308535a9 Mon Sep 17 00:00:00 2001 From: Dawid Dworak Date: Wed, 27 May 2026 17:53:34 +0200 Subject: [PATCH 2/3] Reformat with scalafmt 3.11.1 --- .../scala/io/udash/auth/AuthApplication.scala | 10 +- .../scala/io/udash/auth/AuthPresenter.scala | 18 +- .../main/scala/io/udash/auth/AuthView.scala | 20 +- .../io/udash/auth/AuthApplicationTest.scala | 35 +- .../io/udash/auth/AuthPresenterTest.scala | 6 +- .../scala/io/udash/auth/AuthViewTest.scala | 37 +- .../scala/io/udash/auth/AuthRequires.scala | 4 +- .../main/scala/io/udash/auth/Permission.scala | 6 +- .../io/udash/auth/PermissionCombinator.scala | 18 +- .../scala/io/udash/auth/PermissionId.scala | 2 +- .../main/scala/io/udash/auth/UserCtx.scala | 2 +- .../io/udash/auth/AuthRequiresTest.scala | 12 +- .../scala/io/udash/auth/PermissionTest.scala | 23 +- .../alert/DismissibleUdashAlert.scala | 38 +- .../io/udash/bootstrap/alert/UdashAlert.scala | 17 +- .../bootstrap/alert/UdashAlertBase.scala | 44 +- .../io/udash/bootstrap/badge/UdashBadge.scala | 83 +- .../breadcrumb/UdashBreadcrumbs.scala | 101 +- .../udash/bootstrap/button/UdashButton.scala | 139 ++- .../bootstrap/button/UdashButtonGroup.scala | 229 ++-- .../bootstrap/button/UdashButtonToolbar.scala | 71 +- .../io/udash/bootstrap/card/UdashCard.scala | 148 ++- .../bootstrap/carousel/UdashCarousel.scala | 210 ++-- .../bootstrap/collapse/UdashAccordion.scala | 83 +- .../bootstrap/collapse/UdashCollapse.scala | 100 +- .../datepicker/UdashDatePicker.scala | 271 +++-- .../bootstrap/dropdown/UdashDropdown.scala | 233 ++-- .../io/udash/bootstrap/form/UdashForm.scala | 1030 ++++++++++------- .../bootstrap/form/UdashInputGroup.scala | 38 +- .../io/udash/bootstrap/form/Validator.scala | 7 +- .../bootstrap/jumbotron/UdashJumbotron.scala | 39 +- .../udash/bootstrap/list/UdashListGroup.scala | 50 +- .../io/udash/bootstrap/modal/UdashModal.scala | 144 ++- .../io/udash/bootstrap/nav/UdashNav.scala | 162 ++- .../io/udash/bootstrap/nav/UdashNavbar.scala | 163 +-- .../pagination/UdashPagination.scala | 158 +-- .../progressbar/UdashProgressBar.scala | 108 +- .../io/udash/bootstrap/table/UdashTable.scala | 91 +- .../bootstrap/tooltip/TooltipEvent.scala | 21 +- .../bootstrap/tooltip/TooltipUtils.scala | 58 +- .../bootstrap/tooltip/UdashPopover.scala | 23 +- .../bootstrap/tooltip/UdashTooltip.scala | 27 +- .../bootstrap/utils/BootstrapImplicits.scala | 5 +- .../bootstrap/utils/BootstrapStyles.scala | 39 +- .../bootstrap/BootstrapImplicitsTest.scala | 6 +- .../breadcrumb/UdashBreadcrumbsTest.scala | 15 +- .../button/UdashButtonGroupTest.scala | 2 +- .../bootstrap/button/UdashButtonTest.scala | 9 +- .../udash/bootstrap/card/UdashCardTest.scala | 19 +- .../carousel/UdashCarouselTest.scala | 84 +- .../collapse/UdashAccordionTest.scala | 24 +- .../collapse/UdashCollapseTest.scala | 6 +- .../datepicker/UdashDatePickerTest.scala | 188 +-- .../dropdown/UdashDropdownTest.scala | 23 +- .../bootstrap/form/UdashInputGroupTest.scala | 2 +- .../bootstrap/list/UdashListGroupTest.scala | 4 +- .../bootstrap/modal/UdashModalTest.scala | 10 +- .../io/udash/bootstrap/nav/UdashNavTest.scala | 9 +- .../udash/bootstrap/nav/UdashNavbarTest.scala | 11 +- .../pagination/UdashPaginationTest.scala | 75 +- .../progressbar/UdashProgressBarTest.scala | 9 +- .../bootstrap/table/UdashTableTest.scala | 27 +- .../bootstrap/tooltip/TooltipTestUtils.scala | 4 +- .../bootstrap/utils/UdashIconsTest.scala | 59 +- build.sbt | 312 ++--- .../src/main/scala/io/udash/Application.scala | 112 +- .../scala/io/udash/bindings/Bindings.scala | 643 +++++----- .../udash/bindings/inputs/CheckButtons.scala | 36 +- .../io/udash/bindings/inputs/Checkbox.scala | 17 +- .../io/udash/bindings/inputs/FileInput.scala | 47 +- .../inputs/GroupedButtonsBinding.scala | 10 +- .../io/udash/bindings/inputs/Input.scala | 33 +- .../udash/bindings/inputs/RadioButtons.scala | 32 +- .../io/udash/bindings/inputs/RangeInput.scala | 63 +- .../io/udash/bindings/inputs/Select.scala | 145 ++- .../udash/bindings/inputs/SelectBinding.scala | 21 +- .../io/udash/bindings/inputs/TextArea.scala | 37 +- .../bindings/inputs/TextInputsModifier.scala | 15 +- .../bindings/modifiers/DOMManipulator.scala | 19 +- .../bindings/modifiers/EmptyModifier.scala | 2 +- .../bindings/modifiers/PropertyModifier.scala | 10 +- .../modifiers/SeqAsValueModifier.scala | 15 +- .../modifiers/SeqPropertyModifier.scala | 14 +- .../modifiers/SeqPropertyModifierUtils.scala | 18 +- .../SeqPropertyWithIndexModifier.scala | 2 +- .../modifiers/SimplePropertyModifier.scala | 13 +- .../bindings/modifiers/ValueModifier.scala | 2 +- .../io/udash/bindings/modifiers/package.scala | 4 +- .../scala/io/udash/component/Component.scala | 3 +- .../scala/io/udash/component/Listenable.scala | 49 +- .../main/scala/io/udash/core/Defaults.scala | 13 +- .../scala/io/udash/core/Definitions.scala | 90 +- .../.js/src/main/scala/io/udash/package.scala | 6 +- .../main/scala/io/udash/routing/Routing.scala | 1 - .../io/udash/routing/RoutingEngine.scala | 95 +- .../io/udash/routing/RoutingRegistry.scala | 12 +- .../io/udash/routing/UrlChangeProvider.scala | 64 +- .../scala/io/udash/routing/UrlLogging.scala | 10 +- .../scala/io/udash/utils/FileService.scala | 107 +- .../scala/io/udash/utils/FileUploader.scala | 65 +- .../scala/io/udash/view/ViewRenderer.scala | 42 +- .../bindings/QueuedNodeModifierTest.scala | 8 +- .../io/udash/bindings/TagsBindingTest.scala | 312 ++--- .../bindings/inputs/CheckButtonsTest.scala | 4 +- .../io/udash/bindings/inputs/InputTest.scala | 51 +- .../bindings/inputs/RadioButtonsTest.scala | 4 +- .../io/udash/bindings/inputs/SelectTest.scala | 13 +- .../udash/bindings/inputs/TextAreaTest.scala | 48 +- .../io/udash/component/ComponentIdTest.scala | 2 +- .../io/udash/component/ListenableTest.scala | 24 +- .../io/udash/routing/RoutingEngineTest.scala | 20 +- .../io/udash/routing/UrlLoggingTest.scala | 3 +- .../WindowUrlFragmentChangeProviderTest.scala | 2 +- .../WindowUrlPathChangeProviderTest.scala | 2 +- .../scala/io/udash/testing/TestRouting.scala | 13 +- .../udash/testing/TestRoutingRegistry.scala | 2 +- .../io/udash/testing/TestViewFactory.scala | 4 +- .../testing/TestViewFactoryRegistry.scala | 6 +- .../udash/testing/UdashCoreFrontendTest.scala | 2 +- .../io/udash/view/ViewRendererTest.scala | 13 +- .../manual/PropertyErrorManualTest.scala | 81 +- .../scala/io/udash/properties/Blank.scala | 6 +- .../udash/properties/CallbackSequencer.scala | 19 +- .../HasGenCodecAndModelPropertyCreator.scala | 4 +- .../properties/HasModelPropertyCreator.scala | 2 +- .../udash/properties/ImmutableProperty.scala | 15 +- .../properties/IsModelPropertyTemplate.scala | 24 +- .../properties/MutableSetRegistration.scala | 5 +- .../io/udash/properties/Properties.scala | 27 +- .../io/udash/properties/PropertyCreator.scala | 7 +- .../properties/PropertyCreatorImplicits.scala | 2 +- .../properties/model/ModelProperty.scala | 25 +- .../properties/model/ModelPropertyImpl.scala | 1 + .../model/ReadableModelProperty.scala | 6 +- .../seq/CombinedReadableSeqProperty.scala | 9 +- .../properties/seq/DirectSeqProperty.scala | 10 +- .../properties/seq/FilteredSeqProperty.scala | 26 +- .../seq/ForwarderReadableSeqProperty.scala | 11 +- .../scala/io/udash/properties/seq/Patch.scala | 19 +- .../properties/seq/ReadableSeqProperty.scala | 42 +- .../io/udash/properties/seq/SeqProperty.scala | 12 +- .../seq/SeqPropertyFromSingleValue.scala | 46 +- .../seq/TransformedSeqProperty.scala | 18 +- .../properties/seq/ZippedSeqProperty.scala | 33 +- .../properties/single/CastableProperty.scala | 40 +- .../properties/single/CombinedProperty.scala | 17 +- .../io/udash/properties/single/Property.scala | 70 +- .../properties/single/ReadableProperty.scala | 102 +- .../single/TransformedProperty.scala | 13 +- .../scala/io/udash/properties/BlankTest.scala | 2 +- .../properties/CallbackSequencerTest.scala | 4 +- .../properties/ImmutablePropertyTest.scala | 28 +- .../udash/properties/ModelPropertyTest.scala | 33 +- .../properties/PropertyCreatorTest.scala | 2 +- .../io/udash/properties/PropertyTest.scala | 52 +- .../udash/properties/PropertyUsageTest.scala | 2 +- .../io/udash/properties/SeqPropertyTest.scala | 41 +- .../io/udash/testing/AsyncUdashCoreTest.scala | 2 +- .../io/udash/testing/UdashCoreTest.scala | 2 +- .../src/main/scala/io/udash/css/CssView.scala | 39 +- .../test/scala/io/udash/css/CssViewTest.scala | 2 +- .../scala/io/udash/css/CssFileRenderer.scala | 14 +- .../io/udash/css/CssFileRendererTest.scala | 12 +- .../io/udash/css/CssStringRendererTest.scala | 3 +- css/src/main/scala/io/udash/css/CssBase.scala | 180 ++- .../main/scala/io/udash/css/CssStyle.scala | 3 +- css/src/main/scala/io/udash/css/CssText.scala | 2 +- .../udash/css/SecondStylesheetExample.scala | 11 +- .../io/udash/css/StylesheetExample.scala | 59 +- .../main/scala/io/udash/web/Launcher.scala | 3 +- .../web/guide/demos/activity/CallLogger.scala | 4 +- .../guide/demos/i18n/TranslationServer.scala | 20 +- .../web/guide/demos/rpc/ClientIdServer.scala | 3 +- .../guide/demos/rpc/NotificationsServer.scala | 12 +- .../web/guide/demos/rpc/PingServer.scala | 3 +- .../markdown/MarkdownPagesEndpoint.scala | 20 +- .../guide/rest/ExposedRestInterfaces.scala | 3 +- .../io/udash/web/guide/rpc/ClientRPC.scala | 3 +- .../web/guide/rpc/ExposedRpcInterfaces.scala | 5 +- .../udash/web/server/ApplicationServer.scala | 31 +- .../io/udash/web/styles/CssRenderer.scala | 20 +- .../web/commons/components/CodeBlock.scala | 27 +- .../udash/web/commons/components/Footer.scala | 13 +- .../commons/components/ForceBootstrap.scala | 4 +- .../commons/components/HeaderButtons.scala | 9 +- .../web/commons/config/ExternalUrls.scala | 11 +- .../udash/web/commons/views/Component.scala | 2 +- .../web/commons/views/ImageFactory.scala | 15 +- .../web/commons/views/MarkdownView.scala | 33 +- .../udash/web/guide/RoutingRegistryDef.scala | 2 +- .../io/udash/web/guide/RoutingStatesDef.scala | 7 +- .../web/guide/StatesToViewFactoryDef.scala | 2 +- .../web/guide/components/BootstrapUtils.scala | 5 +- .../web/guide/components/GuideMenu.scala | 32 +- .../udash/web/guide/components/Header.scala | 4 +- .../io/udash/web/guide/demos/AutoDemo.scala | 5 +- .../guide/demos/rpc/NotificationsClient.scala | 2 +- .../main/scala/io/udash/web/guide/init.scala | 122 +- .../udash/web/guide/views/ContentView.scala | 4 +- .../io/udash/web/guide/views/ErrorView.scala | 17 +- .../io/udash/web/guide/views/FaqView.scala | 4 +- .../io/udash/web/guide/views/RootView.scala | 4 +- .../udash/web/guide/views/ViewContainer.scala | 34 +- .../AdvancedBootstrappingSbtView.scala | 27 +- .../BootstrappingBackendView.scala | 89 +- .../BootstrappingFrontendView.scala | 110 +- .../BootstrappingIntroView.scala | 51 +- .../bootstrapping/BootstrappingRpcView.scala | 62 +- .../bootstrapping/BootstrappingSbtView.scala | 24 +- .../bootstrapping/BootstrappingView.scala | 11 +- .../views/ext/AuthorizationExtView.scala | 53 +- .../guide/views/ext/BootstrapExtView.scala | 125 +- .../web/guide/views/ext/I18NExtView.scala | 114 +- .../web/guide/views/ext/JQueryExtView.scala | 23 +- .../guide/views/ext/UserActivityExtView.scala | 45 +- .../demo/DynamicRemoteTranslationsDemo.scala | 76 +- .../ext/demo/FrontendTranslationsDemo.scala | 39 +- .../views/ext/demo/JQueryCallbacksDemo.scala | 46 +- .../views/ext/demo/JQueryEventsDemo.scala | 27 +- .../ext/demo/RemoteTranslationsDemo.scala | 22 +- .../guide/views/ext/demo/RpcLoggingDemo.scala | 8 +- .../guide/views/ext/demo/UrlLoggingDemo.scala | 13 +- .../ext/demo/bootstrap/AccordionDemo.scala | 16 +- .../views/ext/demo/bootstrap/AlertsDemo.scala | 28 +- .../views/ext/demo/bootstrap/BadgesDemo.scala | 14 +- .../ext/demo/bootstrap/BreadcrumbsDemo.scala | 14 +- .../demo/bootstrap/ButtonDropdownDemo.scala | 5 +- .../demo/bootstrap/ButtonToolbarDemo.scala | 16 +- .../ext/demo/bootstrap/ButtonsDemo.scala | 48 +- .../views/ext/demo/bootstrap/CardsDemo.scala | 31 +- .../ext/demo/bootstrap/CarouselDemo.scala | 39 +- .../demo/bootstrap/CheckboxButtonsDemo.scala | 18 +- .../ext/demo/bootstrap/DatePickerDemo.scala | 46 +- .../demo/bootstrap/DateRangePickerDemo.scala | 25 +- .../ext/demo/bootstrap/DropdownsDemo.scala | 46 +- .../views/ext/demo/bootstrap/IconsDemo.scala | 11 +- .../ext/demo/bootstrap/InlineFormDemo.scala | 31 +- .../ext/demo/bootstrap/InputGroupDemo.scala | 9 +- .../ext/demo/bootstrap/JumbotronDemo.scala | 28 +- .../views/ext/demo/bootstrap/LabelsDemo.scala | 25 +- .../ext/demo/bootstrap/ListGroupDemo.scala | 14 +- .../views/ext/demo/bootstrap/NavbarDemo.scala | 29 +- .../ext/demo/bootstrap/NavigationDemo.scala | 54 +- .../views/ext/demo/bootstrap/NavsDemo.scala | 22 +- .../ext/demo/bootstrap/PaginationDemo.scala | 31 +- .../ext/demo/bootstrap/PopoversDemo.scala | 46 +- .../ext/demo/bootstrap/ProgressBarDemo.scala | 47 +- .../ext/demo/bootstrap/RadioButtonsDemo.scala | 10 +- .../demo/bootstrap/ResponsiveEmbedDemo.scala | 11 +- .../demo/bootstrap/SimpleCollapseDemo.scala | 5 +- .../ext/demo/bootstrap/SimpleFormDemo.scala | 48 +- .../ext/demo/bootstrap/SimpleModalDemo.scala | 38 +- .../bootstrap/StaticButtonsGroupDemo.scala | 3 +- .../ext/demo/bootstrap/StaticsDemo.scala | 25 +- .../views/ext/demo/bootstrap/TableDemo.scala | 12 +- .../demo/bootstrap/ToggleButtonsDemo.scala | 21 +- .../ext/demo/bootstrap/TooltipsDemo.scala | 42 +- .../views/frontend/FrontendBindingsView.scala | 89 +- .../views/frontend/FrontendFilesView.scala | 81 +- .../views/frontend/FrontendFormsView.scala | 68 +- .../views/frontend/FrontendIntroView.scala | 40 +- .../views/frontend/FrontendMVPView.scala | 77 +- .../frontend/FrontendPropertiesView.scala | 250 ++-- .../views/frontend/FrontendRoutingView.scala | 180 ++- .../frontend/FrontendTemplatesView.scala | 140 ++- .../guide/views/frontend/FrontendView.scala | 7 +- .../frontend/demos/BindAttributeDemo.scala | 6 +- .../guide/views/frontend/demos/BindDemo.scala | 4 +- .../frontend/demos/CheckButtonsDemo.scala | 17 +- .../views/frontend/demos/CheckboxDemo.scala | 18 +- .../guide/views/frontend/demos/DateDemo.scala | 21 +- .../frontend/demos/DateTimeLocalDemo.scala | 22 +- .../views/frontend/demos/FileInputDemo.scala | 36 +- .../views/frontend/demos/IntroFormDemo.scala | 86 +- .../frontend/demos/MultiSelectDemo.scala | 9 +- .../views/frontend/demos/ProduceDemo.scala | 30 +- .../frontend/demos/RadioButtonsDemo.scala | 28 +- .../views/frontend/demos/RepeatDemo.scala | 30 +- .../views/frontend/demos/SelectDemo.scala | 9 +- .../views/frontend/demos/ShowIfDemo.scala | 2 +- .../views/frontend/demos/TextAreaDemo.scala | 2 +- .../views/frontend/demos/TextInputDemo.scala | 14 +- .../guide/views/frontend/demos/TimeDemo.scala | 21 +- .../guide/views/rpc/RpcClientServerView.scala | 186 ++- .../guide/views/rpc/RpcInterfacesView.scala | 68 +- .../web/guide/views/rpc/RpcIntroView.scala | 37 +- .../views/rpc/RpcSerializationView.scala | 117 +- .../guide/views/rpc/RpcServerClientView.scala | 34 +- .../udash/web/guide/views/rpc/RpcView.scala | 4 +- .../rpc/demos/ClientIdDemoComponent.scala | 18 +- .../rpc/demos/ExceptionsDemoComponent.scala | 97 +- .../rpc/demos/GenCodecsDemoComponent.scala | 96 +- .../demos/NotificationsDemoComponent.scala | 23 +- .../rpc/demos/PingPongCallDemoComponent.scala | 9 +- .../rpc/demos/PingPongPushDemoComponent.scala | 8 +- .../udash/web/guide/demos/AutoDemoTest.scala | 1 - .../web/homepage/RoutingRegistryDef.scala | 2 +- .../web/homepage/StatesToViewFactoryDef.scala | 2 +- .../web/homepage/components/Buttons.scala | 3 +- .../web/homepage/components/Header.scala | 26 +- .../components/demo/DemoComponent.scala | 7 +- .../scala/io/udash/web/homepage/init.scala | 2 +- .../scala/io/udash/web/homepage/states.scala | 2 +- .../udash/web/homepage/views/ErrorView.scala | 14 +- .../udash/web/homepage/views/IndexView.scala | 66 +- .../udash/web/homepage/views/RootView.scala | 4 +- .../scala/io/udash/web/SeleniumTest.scala | 13 +- .../web/guide/demos/ext/I18nDemosTest.scala | 37 +- .../web/guide/demos/ext/JQueryDemosTest.scala | 12 +- .../demos/frontend/FrontendBindingsTest.scala | 18 +- .../demos/frontend/FrontendFormsTest.scala | 38 +- .../demos/frontend/FrontendIntroTest.scala | 5 +- .../demos/frontend/FrontendRoutingTest.scala | 2 +- .../web/commons/styles/DefaultStyles.scala | 103 +- .../web/commons/styles/GlobalStyles.scala | 41 +- .../styles/attributes/Attributes.scala | 13 +- .../styles/components/FooterStyles.scala | 52 +- .../components/HeaderButtonsStyles.scala | 72 +- .../styles/components/HeaderNavStyles.scala | 40 +- .../styles/components/MobileMenuStyles.scala | 32 +- .../styles/utils/CommonStyleUtils.scala | 22 +- .../commons/styles/utils/MediaQueries.scala | 28 +- .../commons/styles/utils/StyleConstants.scala | 9 +- .../web/commons/styles/utils/UdashFonts.scala | 27 +- .../io/udash/web/guide/MainClientRPC.scala | 2 +- .../io/udash/web/guide/MainServerRPC.scala | 2 +- .../web/guide/demos/DemosClientRPC.scala | 2 +- .../udash/web/guide/demos/activity/Call.scala | 2 +- .../web/guide/demos/i18n/Translations.scala | 1 - .../web/guide/demos/rest/MainServerREST.scala | 2 +- .../guide/demos/rest/RestExampleClass.scala | 2 +- .../guide/demos/rpc/ClientIdServerRPC.scala | 2 +- .../web/guide/demos/rpc/ExceptionsRPC.scala | 2 +- .../demos/rpc/NotificationsClientRPC.scala | 2 +- .../web/guide/markdown/MarkdownPageRPC.scala | 2 +- .../web/guide/styles/GuideDefaultStyles.scala | 42 +- .../web/guide/styles/MarkdownStyles.scala | 24 +- .../guide/styles/demo/ExampleKeyframes.scala | 8 +- .../web/guide/styles/demo/ExampleMixins.scala | 11 +- .../web/guide/styles/demo/ExampleStyles.scala | 74 +- .../guide/styles/partials/GuideStyles.scala | 216 ++-- .../guide/styles/partials/HeaderStyles.scala | 20 +- .../guide/styles/partials/MenuStyles.scala | 95 +- .../guide/styles/utils/GuideStyleUtils.scala | 14 +- .../web/guide/styles/utils/MediaQueries.scala | 26 +- .../styles/HomepageDefaultStyles.scala | 18 +- .../styles/partials/ButtonsStyle.scala | 68 +- .../homepage/styles/partials/DemoStyles.scala | 58 +- .../styles/partials/HeaderStyles.scala | 40 +- .../styles/partials/HomepageStyles.scala | 302 ++--- .../udash/i18n/LocalTranslationProvider.scala | 10 +- .../i18n/RemoteTranslationProvider.scala | 54 +- .../scala/io/udash/i18n/Translations.scala | 146 ++- .../bindings/AttrTranslationModifier.scala | 2 +- .../DynamicAttrTranslationBinding.scala | 10 +- .../bindings/DynamicTranslationBinding.scala | 8 +- .../i18n/bindings/TranslationModifier.scala | 10 +- .../io/udash/i18n/bindings/package.scala | 3 +- .../scala/io/udash/i18n/BindingsTest.scala | 98 +- .../i18n/LocalTranslationProviderTest.scala | 67 +- .../i18n/RemoteTranslationProviderTest.scala | 158 ++- ...eBundlesTranslationTemplatesProvider.scala | 13 +- .../udash/i18n/TranslationRPCEndpoint.scala | 3 +- .../i18n/TranslationTemplatesProvider.scala | 24 +- ...dlesTranslationTemplatesProviderTest.scala | 9 +- .../i18n/TranslationRPCEndpointTest.scala | 2 +- .../io/udash/i18n/RemoteTranslationRPC.scala | 1 + .../scala/io/udash/i18n/TranslationKey.scala | 2 +- .../io/udash/i18n/TranslationProvider.scala | 37 +- i18n/src/main/scala/io/udash/i18n/types.scala | 4 +- .../io/udash/i18n/TranslationKeyTest.scala | 22 +- .../io/udash/css/macros/StyleMacros.scala | 18 +- .../io/udash/macros/AllValuesMacro.scala | 26 +- .../io/udash/macros/ComponentIdMacro.scala | 2 +- .../io/udash/macros/PropertyMacros.scala | 182 +-- .../scala/io/udash/macros/TestMacros.scala | 2 +- project/Dependencies.scala | 253 ++-- .../scala/io/udash/rest/RestServlet.scala | 135 +-- .../udash/rest/openapi/OpenApiServlet.scala | 5 +- .../udash/rest/EndpointsIntegrationTest.scala | 48 +- .../scala/io/udash/rest/RestServletTest.scala | 33 +- .../udash/rest/ServletBasedRestApiTest.scala | 3 +- .../io/udash/rest/SttpRestCallTest.scala | 24 +- .../scala/io/udash/rest/UsesHttpServer.scala | 8 +- .../io/udash/rest/examples/UserApi.scala | 2 +- .../rest/openapi/OpenApiGenerationTest.scala | 7 +- .../io/udash/rest/jetty/JettyRestClient.scala | 105 +- .../udash/rest/jetty/JettyRestCallTest.scala | 11 +- .../udash/rest/DefaultRestApiCompanion.scala | 41 +- .../io/udash/rest/RestDataCompanion.scala | 143 ++- .../scala/io/udash/rest/SttpRestClient.scala | 79 +- .../scala/io/udash/rest/annotations.scala | 299 +++-- .../main/scala/io/udash/rest/companions.scala | 98 +- .../main/scala/io/udash/rest/implicits.scala | 72 +- .../rest/openapi/GeneratedSchemaName.scala | 92 +- .../scala/io/udash/rest/openapi/OpenApi.scala | 232 ++-- .../udash/rest/openapi/OpenApiMetadata.scala | 162 +-- .../io/udash/rest/openapi/RestSchema.scala | 284 +++-- .../io/udash/rest/openapi/RestStructure.scala | 120 +- .../udash/rest/openapi/WhenAbsentInfo.scala | 4 +- .../rest/openapi/adjusters/Adjuster.scala | 184 ++- .../io/udash/rest/raw/AbstractMapping.scala | 22 +- .../scala/io/udash/rest/raw/HttpBody.scala | 55 +- .../scala/io/udash/rest/raw/JsonValue.scala | 7 +- .../scala/io/udash/rest/raw/PlainValue.scala | 37 +- .../scala/io/udash/rest/raw/RawRest.scala | 156 +-- .../io/udash/rest/raw/RestMetadata.scala | 136 ++- .../scala/io/udash/rest/raw/RestRequest.scala | 13 +- .../io/udash/rest/raw/RestResponse.scala | 73 +- .../io/udash/rest/raw/StreamedBody.scala | 27 +- .../main/scala/io/udash/rest/util/Utils.scala | 10 +- .../io/udash/rest/util/WithHeaders.scala | 16 +- .../io/udash/rest/CompilationErrorsTest.scala | 33 +- .../io/udash/rest/PolyRestDataCompanion.scala | 4 +- .../scala/io/udash/rest/RestApiTest.scala | 21 +- .../scala/io/udash/rest/RestTestApi.scala | 60 +- .../io/udash/rest/RestValidationTest.scala | 18 +- .../io/udash/rest/StreamingRestTestApi.scala | 18 +- .../scala/io/udash/rest/TestRESTRecord.scala | 2 +- .../scala/io/udash/rest/WriteOpenApi.scala | 2 +- .../udash/rest/openapi/RestSchemaTest.scala | 40 +- .../rest/openapi/openapiDependencies.scala | 7 +- .../scala/io/udash/rest/raw/RawRestTest.scala | 174 +-- .../io/udash/rest/raw/ServerImplApiTest.scala | 48 +- .../scala/io/udash/rpc/ConnectionStatus.scala | 1 - .../scala/io/udash/rpc/DefaultServerRPC.scala | 14 +- .../rpc/internals/ExposesClientRPC.scala | 9 +- .../udash/rpc/internals/ServerConnector.scala | 53 +- .../udash/rpc/internals/UsesServerRPC.scala | 48 +- .../DefaultExceptionCodecRegistry.scala | 2 +- .../wrappers/atmosphere/Atmosphere.scala | 156 ++- .../scala/io/udash/rpc/ServerRPCTest.scala | 4 +- .../rpc/internals/ExposesClientRPCTest.scala | 17 +- .../io/udash/rpc/AtmosphereService.scala | 87 +- .../rpc/DefaultAtmosphereServiceConfig.scala | 5 +- .../scala/io/udash/rpc/DefaultClientRPC.scala | 10 +- .../scala/io/udash/rpc/ExposesServerRPC.scala | 9 +- .../main/scala/io/udash/rpc/RPCBackend.scala | 2 +- .../main/scala/io/udash/rpc/RpcServlet.scala | 6 +- .../udash/rpc/internals/UsesClientRPC.scala | 12 +- .../DefaultExceptionCodecRegistry.scala | 11 +- .../io/udash/rpc/utils/CallLogging.scala | 7 +- .../utils/DefaultAtmosphereFramework.scala | 7 +- .../udash/rpc/utils/FileDownloadServlet.scala | 5 +- .../udash/rpc/utils/FileUploadServlet.scala | 22 +- .../DefaultAtmosphereServiceConfigTest.scala | 12 +- .../DefaultExceptionCodecRegistryTest.scala | 7 +- .../rpc/JVMSerializationIntegrationTest.scala | 2 +- .../scala/io/udash/rpc/RpcMessagesTest.scala | 2 +- .../rpc/internals/AtmosphereServiceTest.scala | 132 ++- .../rpc/internals/ExposesServerRPCTest.scala | 57 +- .../udash/testing/UdashRpcBackendTest.scala | 25 +- .../scala/io/udash/rpc/ExposesLocalRPC.scala | 9 +- .../scala/io/udash/rpc/UsesRemoteRPC.scala | 7 +- .../main/scala/io/udash/rpc/companions.scala | 22 +- rpc/src/main/scala/io/udash/rpc/rawrpc.scala | 40 +- .../udash/rpc/serialization/EscapeUtils.scala | 6 +- .../ExceptionCodecRegistry.scala | 10 +- .../scala/io/udash/rpc/utils/Logged.scala | 3 +- .../scala/io/udash/rpc/RpcMessagesTest.scala | 23 +- .../SerializationIntegrationTestBase.scala | 15 +- rpc/src/test/scala/io/udash/rpc/TestRPC.scala | 2 + rpc/src/test/scala/io/udash/rpc/Utils.scala | 11 +- rpc/src/test/scala/io/udash/rpc/types.scala | 21 +- .../scala/io/udash/logging/UdashLogger.scala | 2 +- .../scala/io/udash/utils/URLEncoder.scala | 3 +- .../udash/testing/AsyncUdashSharedTest.scala | 29 +- .../io/udash/testing/UdashFrontendTest.scala | 2 +- .../udash/testing/AsyncUdashSharedTest.scala | 3 +- .../io/udash/utils/CallbacksHandler.scala | 38 +- .../scala/io/udash/utils/FilteringUtils.scala | 11 +- .../scala/io/udash/utils/Registration.scala | 9 +- .../testing/AsyncUdashSharedTestBase.scala | 3 +- .../io/udash/testing/UdashSharedTest.scala | 3 +- .../io/udash/utils/CallbacksHandlerTest.scala | 16 +- 475 files changed, 11449 insertions(+), 8577 deletions(-) diff --git a/auth/.js/src/main/scala/io/udash/auth/AuthApplication.scala b/auth/.js/src/main/scala/io/udash/auth/AuthApplication.scala index 2e68ad25a..76dd7c930 100644 --- a/auth/.js/src/main/scala/io/udash/auth/AuthApplication.scala +++ b/auth/.js/src/main/scala/io/udash/auth/AuthApplication.scala @@ -6,14 +6,16 @@ object AuthApplication { implicit final class ApplicationAuthExt[HierarchyRoot >: Null <: GState[HierarchyRoot]]( private val application: Application[HierarchyRoot] ) extends AnyVal { - /** - * Adds the default listener of authorization failure in routing (redirects to provided state). + + /** Adds the default listener of authorization failure in routing (redirects to provided state). * - * @param authFailedRedirectState application will redirect user to this state after auth fail + * @param authFailedRedirectState + * application will redirect user to this state after auth fail */ def withDefaultRoutingFailureListener(authFailedRedirectState: HierarchyRoot): Application[HierarchyRoot] = { application.onRoutingFailure { - case _: UnauthorizedException | _: UnauthenticatedException if application.currentState != authFailedRedirectState => + case _: UnauthorizedException | _: UnauthenticatedException + if application.currentState != authFailedRedirectState => application.goTo(authFailedRedirectState) } application diff --git a/auth/.js/src/main/scala/io/udash/auth/AuthPresenter.scala b/auth/.js/src/main/scala/io/udash/auth/AuthPresenter.scala index 682779db2..d05f0077a 100644 --- a/auth/.js/src/main/scala/io/udash/auth/AuthPresenter.scala +++ b/auth/.js/src/main/scala/io/udash/auth/AuthPresenter.scala @@ -2,15 +2,19 @@ package io.udash.auth import io.udash._ -/** - * Presenter which check user access in `handleState` method. +/** Presenter which check user access in `handleState` method. * - * @param permission PermissionCombinator verified against provided `userCtx`. - * @param requireAuthenticated If `true`, the presenter requires `userCtx` to don't be Unauthenticated subclass. + * @param permission + * PermissionCombinator verified against provided `userCtx`. + * @param requireAuthenticated + * If `true`, the presenter requires `userCtx` to don't be Unauthenticated subclass. */ -abstract class AuthPresenter[S <: State](permission: PermissionCombinator, requireAuthenticated: Boolean = false) - (implicit userCtx: UserCtx) - extends Presenter[S] with AuthRequires { +abstract class AuthPresenter[S <: State]( + permission: PermissionCombinator, + requireAuthenticated: Boolean = false, +)(implicit userCtx: UserCtx +) extends Presenter[S] + with AuthRequires { override def handleState(state: S): Unit = { require(permission, requireAuthenticated) diff --git a/auth/.js/src/main/scala/io/udash/auth/AuthView.scala b/auth/.js/src/main/scala/io/udash/auth/AuthView.scala index 8e23d0a70..ebe57fde9 100644 --- a/auth/.js/src/main/scala/io/udash/auth/AuthView.scala +++ b/auth/.js/src/main/scala/io/udash/auth/AuthView.scala @@ -5,8 +5,15 @@ import io.udash.bindings.modifiers.EmptyModifier import scalatags.JsDom.all._ trait AuthView { + /** Renders provided `view` only if user context has required permissions. */ - def require(permission: PermissionCombinator, requireAuthenticated: Boolean = false)(view: => Modifier)(implicit userCtx: UserCtx): Modifier = + def require( + permission: PermissionCombinator, + requireAuthenticated: Boolean = false, + )( + view: => Modifier + )(implicit userCtx: UserCtx + ): Modifier = requireWithFallback(permission, requireAuthenticated)(view)() /** Renders provided `view` only if user is authenticated. */ @@ -14,8 +21,15 @@ trait AuthView { require(AllowAll, requireAuthenticated = true)(view) /** Renders provided `view` if user context has required permissions or `fallback` view otherwise. */ - def requireWithFallback(permission: PermissionCombinator, requireAuthenticated: Boolean = false) - (view: => Modifier)(fallback: => Modifier = new EmptyModifier())(implicit userCtx: UserCtx): Modifier = + def requireWithFallback( + permission: PermissionCombinator, + requireAuthenticated: Boolean = false, + )( + view: => Modifier + )( + fallback: => Modifier = new EmptyModifier() + )(implicit userCtx: UserCtx + ): Modifier = if ((!requireAuthenticated || userCtx.isAuthenticated) && permission.check(userCtx)) view else fallback /** Renders provided `view` if user is authenticated or `fallback` view otherwise. */ diff --git a/auth/.js/src/test/scala/io/udash/auth/AuthApplicationTest.scala b/auth/.js/src/test/scala/io/udash/auth/AuthApplicationTest.scala index 364703093..6cc6bf8e4 100644 --- a/auth/.js/src/test/scala/io/udash/auth/AuthApplicationTest.scala +++ b/auth/.js/src/test/scala/io/udash/auth/AuthApplicationTest.scala @@ -5,7 +5,6 @@ import io.udash.core._ import io.udash.routing.RoutingRegistry import io.udash.testing.AsyncUdashFrontendTest - class AuthApplicationTest extends AsyncUdashFrontendTest with AuthTestUtils with AuthFrontendTestUtils { import AuthApplication.ApplicationAuthExt @@ -13,12 +12,15 @@ class AuthApplicationTest extends AsyncUdashFrontendTest with AuthTestUtils with class TestVF(p: Presenter[TestStates]) extends ViewFactory[TestStates] { override def create(): (View, Presenter[TestStates]) = - (new View { + ( + new View { - import scalatags.JsDom.all._ + import scalatags.JsDom.all._ - override def getTemplate: Modifier = div().render - }, p) + override def getTemplate: Modifier = div().render + }, + p, + ) } val rr: RoutingRegistry[TestStates] = new RoutingRegistry[TestStates] { @@ -40,9 +42,10 @@ class AuthApplicationTest extends AsyncUdashFrontendTest with AuthTestUtils with state match { case SomeState => new TestVF(new AuthPresenter[TestStates](P1) {}) case SecondState => new TestVF(new AuthPresenter[TestStates](AllowAll, requireAuthenticated = true) {}) - case ThirdState => new TestVF(new Presenter[TestStates] { - override def handleState(state: TestStates): Unit = {} - }) + case ThirdState => + new TestVF(new Presenter[TestStates] { + override def handleState(state: TestStates): Unit = {} + }) } } @@ -50,13 +53,13 @@ class AuthApplicationTest extends AsyncUdashFrontendTest with AuthTestUtils with val app = new Application[TestStates](rr, vfr).withDefaultRoutingFailureListener(ThirdState) app.run(root) for { - _ <- retrying { app.currentState should be(ThirdState) } + _ <- retrying(app.currentState should be(ThirdState)) _ = app.goTo(SecondState) - _ <- retrying { app.currentState should be(ThirdState) } + _ <- retrying(app.currentState should be(ThirdState)) _ = app.goTo(SomeState) - _ <- retrying { app.currentState should be(ThirdState) } + _ <- retrying(app.currentState should be(ThirdState)) _ = app.goTo(ThirdState) - r <- retrying { app.currentState should be(ThirdState) } + r <- retrying(app.currentState should be(ThirdState)) } yield r } @@ -75,13 +78,13 @@ class AuthApplicationTest extends AsyncUdashFrontendTest with AuthTestUtils with val app = new Application[TestStates](rr, vfr).withDefaultRoutingFailureListener(ThirdState) app.run(root) for { - _ <- retrying { app.currentState should be(ThirdState) } + _ <- retrying(app.currentState should be(ThirdState)) _ = app.goTo(SecondState) - _ <- retrying { app.currentState should be(ThirdState) } + _ <- retrying(app.currentState should be(ThirdState)) _ = app.goTo(SomeState) - _ <- retrying { app.currentState should be(ThirdState) } + _ <- retrying(app.currentState should be(ThirdState)) _ = app.goTo(ThirdState) - r <- retrying { app.currentState should be(ThirdState) } + r <- retrying(app.currentState should be(ThirdState)) } yield r } } diff --git a/auth/.js/src/test/scala/io/udash/auth/AuthPresenterTest.scala b/auth/.js/src/test/scala/io/udash/auth/AuthPresenterTest.scala index 203e796b0..299916ba4 100644 --- a/auth/.js/src/test/scala/io/udash/auth/AuthPresenterTest.scala +++ b/auth/.js/src/test/scala/io/udash/auth/AuthPresenterTest.scala @@ -7,12 +7,14 @@ class AuthPresenterTest extends UdashFrontendTest with AuthTestUtils with AuthFr "AuthPresenter" should { "throw an exception if user is not authenticated" in { - class SomePresenter extends AuthPresenter[SomeState.type](AllowAll, requireAuthenticated = false)(UnauthenticatedUser) + class SomePresenter + extends AuthPresenter[SomeState.type](AllowAll, requireAuthenticated = false)(UnauthenticatedUser) val p = new SomePresenter p.handleState(SomeState) - class SomePresenter2 extends AuthPresenter[SomeState.type](AllowAll, requireAuthenticated = true)(UnauthenticatedUser) + class SomePresenter2 + extends AuthPresenter[SomeState.type](AllowAll, requireAuthenticated = true)(UnauthenticatedUser) val p2 = new SomePresenter2 intercept[UnauthenticatedException] { diff --git a/auth/.js/src/test/scala/io/udash/auth/AuthViewTest.scala b/auth/.js/src/test/scala/io/udash/auth/AuthViewTest.scala index 91ee110d9..68c51260e 100644 --- a/auth/.js/src/test/scala/io/udash/auth/AuthViewTest.scala +++ b/auth/.js/src/test/scala/io/udash/auth/AuthViewTest.scala @@ -11,15 +11,15 @@ class AuthViewTest extends UdashFrontendTest with AuthTestUtils { implicit val user = User(Set(P1, P2)) val el = div( - require(P1.and(P2)) { "1" }, - require(P1.and(P3)) { "2" }, - require(P1.or(P3)) { "3" }, - require(P3.or(P1)) { "4" }, - require(P1) { "5" }, - require(P3) { "6" }, - require(P3.and(P1.or(P2))) { "7" }, - require(P1.and(P3.or(P2))) { "8" }, - require(P1.and(P2.or(P3))) { "9" } + require(P1.and(P2))("1"), + require(P1.and(P3))("2"), + require(P1.or(P3))("3"), + require(P3.or(P1))("4"), + require(P1)("5"), + require(P3)("6"), + require(P3.and(P1.or(P2)))("7"), + require(P1.and(P3.or(P2)))("8"), + require(P1.and(P2.or(P3)))("9"), ).render el.textContent should be("134589") @@ -33,15 +33,15 @@ class AuthViewTest extends UdashFrontendTest with AuthTestUtils { implicit val user = UnauthenticatedUser val el = div( - require(AllowAll, requireAuthenticated = true) { "1" }, - require(AllowAll) { "2" }, - require(AllowAll, requireAuthenticated = true) { "3" }, - require(AllowAll) { "4" }, - require(AllowAll, requireAuthenticated = true) { "5" }, - require(P1) { "6" }, - require(AllowAll, requireAuthenticated = true) { "7" }, - require(AllowAll) { "8" }, - require(AllowAll, requireAuthenticated = true) { "9" } + require(AllowAll, requireAuthenticated = true)("1"), + require(AllowAll)("2"), + require(AllowAll, requireAuthenticated = true)("3"), + require(AllowAll)("4"), + require(AllowAll, requireAuthenticated = true)("5"), + require(P1)("6"), + require(AllowAll, requireAuthenticated = true)("7"), + require(AllowAll)("8"), + require(AllowAll, requireAuthenticated = true)("9"), ).render el.textContent should be("248") @@ -52,7 +52,6 @@ class AuthViewTest extends UdashFrontendTest with AuthTestUtils { import PermissionCombinator.AllowAll import scalatags.JsDom.all._ - implicit val user = User(Set(P1, P2)) val el = div( diff --git a/auth/src/main/scala/io/udash/auth/AuthRequires.scala b/auth/src/main/scala/io/udash/auth/AuthRequires.scala index a4c6d0989..a6137c9f9 100644 --- a/auth/src/main/scala/io/udash/auth/AuthRequires.scala +++ b/auth/src/main/scala/io/udash/auth/AuthRequires.scala @@ -1,8 +1,10 @@ package io.udash.auth trait AuthRequires { + /** Checks if user context has required permissions. */ - def require(permission: PermissionCombinator, requireAuthenticated: Boolean = false)(implicit userCtx: UserCtx): Unit = { + def require(permission: PermissionCombinator, requireAuthenticated: Boolean = false)(implicit userCtx: UserCtx) + : Unit = { if (requireAuthenticated && !userCtx.isAuthenticated) throw new UnauthenticatedException() if (!permission.check(userCtx)) throw new UnauthorizedException() } diff --git a/auth/src/main/scala/io/udash/auth/Permission.scala b/auth/src/main/scala/io/udash/auth/Permission.scala index b768b4410..f976f73c1 100644 --- a/auth/src/main/scala/io/udash/auth/Permission.scala +++ b/auth/src/main/scala/io/udash/auth/Permission.scala @@ -1,7 +1,6 @@ package io.udash.auth -/** - * Base class for permissions used with PermissionControl. Permissions are compared by ID. +/** Base class for permissions used with PermissionControl. Permissions are compared by ID. */ trait Permission { def id: PermissionId @@ -19,6 +18,7 @@ trait Permission { } object Permission { + /** Single permission as a combinator resolved implicitly. */ implicit final class Single(private val permission: Permission) extends AnyVal with PermissionCombinator { override def check(ctx: UserCtx): Boolean = @@ -27,4 +27,4 @@ object Permission { override def toString: String = permission.toString } -} \ No newline at end of file +} diff --git a/auth/src/main/scala/io/udash/auth/PermissionCombinator.scala b/auth/src/main/scala/io/udash/auth/PermissionCombinator.scala index 60d53645b..9aa2bfa19 100644 --- a/auth/src/main/scala/io/udash/auth/PermissionCombinator.scala +++ b/auth/src/main/scala/io/udash/auth/PermissionCombinator.scala @@ -1,13 +1,14 @@ package io.udash.auth trait PermissionCombinator extends Any { - /** Should return `true` if provided user context passes this combinator test. - * It should not check `isAuthenticated` flag of user context. */ + + /** Should return `true` if provided user context passes this combinator test. It should not check `isAuthenticated` + * flag of user context. + */ def check(ctx: UserCtx): Boolean - /** - * Combines this person combinator with another, - * creating a new one granting permission only in case both combinators do so. + /** Combines this person combinator with another, creating a new one granting permission only in case both combinators + * do so. */ def and(other: PermissionCombinator): PermissionCombinator = new PermissionCombinator { override def check(ctx: UserCtx): Boolean = @@ -17,9 +18,8 @@ trait PermissionCombinator extends Any { s"($this && $other)" } - /** - * Combines this person combinator with another, - * creating a new one granting permission only in case any of combinators does so. + /** Combines this person combinator with another, creating a new one granting permission only in case any of + * combinators does so. */ def or(other: PermissionCombinator): PermissionCombinator = new PermissionCombinator { override def check(ctx: UserCtx): Boolean = @@ -31,6 +31,7 @@ trait PermissionCombinator extends Any { } object PermissionCombinator { + /** PermissionCombinator always granting permission. */ val AllowAll: PermissionCombinator = _ => true @@ -52,4 +53,3 @@ object PermissionCombinator { permissions.map(_.toString).mkString("(", " || ", ")") } } - diff --git a/auth/src/main/scala/io/udash/auth/PermissionId.scala b/auth/src/main/scala/io/udash/auth/PermissionId.scala index f495389a8..522f249fb 100644 --- a/auth/src/main/scala/io/udash/auth/PermissionId.scala +++ b/auth/src/main/scala/io/udash/auth/PermissionId.scala @@ -1,6 +1,6 @@ package io.udash.auth -import com.avsystem.commons.serialization.{HasGenCodec, transparent} +import com.avsystem.commons.serialization.{transparent, HasGenCodec} @transparent final case class PermissionId(value: String) extends AnyVal diff --git a/auth/src/main/scala/io/udash/auth/UserCtx.scala b/auth/src/main/scala/io/udash/auth/UserCtx.scala index 38b8642e9..1088c90fa 100644 --- a/auth/src/main/scala/io/udash/auth/UserCtx.scala +++ b/auth/src/main/scala/io/udash/auth/UserCtx.scala @@ -10,4 +10,4 @@ object UserCtx { override def has(permission: Permission): Boolean = false override def isAuthenticated: Boolean = false } -} \ No newline at end of file +} diff --git a/auth/src/test/scala/io/udash/auth/AuthRequiresTest.scala b/auth/src/test/scala/io/udash/auth/AuthRequiresTest.scala index 5e9d6cf77..3089e890b 100644 --- a/auth/src/test/scala/io/udash/auth/AuthRequiresTest.scala +++ b/auth/src/test/scala/io/udash/auth/AuthRequiresTest.scala @@ -10,8 +10,8 @@ class AuthRequiresTest extends UdashSharedTest with AuthTestUtils { implicit val user: UserCtx = User(Set(P1, P2)) AuthRequires.require(P1.and(P2)) - intercept[UnauthorizedException] { AuthRequires.require(P1.and(P3)) } - intercept[UnauthorizedException] { AuthRequires.require(P2.and(P3)) } + intercept[UnauthorizedException](AuthRequires.require(P1.and(P3))) + intercept[UnauthorizedException](AuthRequires.require(P2.and(P3))) AuthRequires.require(P1.or(P3)) AuthRequires.require(AllowAll) } @@ -20,10 +20,10 @@ class AuthRequiresTest extends UdashSharedTest with AuthTestUtils { implicit val user: UserCtx = UnauthenticatedUser AuthRequires.require(AllowAll) - intercept[UnauthenticatedException] { AuthRequires.require(AllowAll, requireAuthenticated = true) } - intercept[UnauthenticatedException] { AuthRequires.requireAuthenticated() } - intercept[UnauthenticatedException] { AuthRequires.require(P2.and(P3), requireAuthenticated = true) } - intercept[UnauthorizedException] { AuthRequires.require(P2.and(P3)) } + intercept[UnauthenticatedException](AuthRequires.require(AllowAll, requireAuthenticated = true)) + intercept[UnauthenticatedException](AuthRequires.requireAuthenticated()) + intercept[UnauthenticatedException](AuthRequires.require(P2.and(P3), requireAuthenticated = true)) + intercept[UnauthorizedException](AuthRequires.require(P2.and(P3))) } } } diff --git a/auth/src/test/scala/io/udash/auth/PermissionTest.scala b/auth/src/test/scala/io/udash/auth/PermissionTest.scala index 1cb866634..5f7bbe3e8 100644 --- a/auth/src/test/scala/io/udash/auth/PermissionTest.scala +++ b/auth/src/test/scala/io/udash/auth/PermissionTest.scala @@ -21,13 +21,22 @@ class PermissionTest extends UdashSharedTest { class PermAType(override val id: PermissionId) extends Permission class PermBType(override val id: PermissionId) extends Permission - Set(new PermAType(PermissionId("1")), new PermAType(PermissionId("1")), - new PermAType(PermissionId("1")), new PermAType(PermissionId("2")), - new PermAType(PermissionId("1")), new PermBType(PermissionId("1")), - new PermBType(PermissionId("1")), new PermAType(PermissionId("1")), - new PermBType(PermissionId("1")), new PermBType(PermissionId("1")), - new PermAType(PermissionId("1")), new PermBType(PermissionId("2")), - new PermBType(PermissionId("1")), new PermBType(PermissionId("2"))).size should be(2) + Set( + new PermAType(PermissionId("1")), + new PermAType(PermissionId("1")), + new PermAType(PermissionId("1")), + new PermAType(PermissionId("2")), + new PermAType(PermissionId("1")), + new PermBType(PermissionId("1")), + new PermBType(PermissionId("1")), + new PermAType(PermissionId("1")), + new PermBType(PermissionId("1")), + new PermBType(PermissionId("1")), + new PermAType(PermissionId("1")), + new PermBType(PermissionId("2")), + new PermBType(PermissionId("1")), + new PermBType(PermissionId("2")), + ).size should be(2) } } } diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/alert/DismissibleUdashAlert.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/alert/DismissibleUdashAlert.scala index 7055e8d95..6b1a1c3d6 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/alert/DismissibleUdashAlert.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/alert/DismissibleUdashAlert.scala @@ -9,21 +9,28 @@ import org.scalajs.dom.Element import scalatags.JsDom.all.* import com.avsystem.commons.SharedExtensions.universalOps -final class DismissibleUdashAlert private[alert]( - alertStyle: ReadableProperty[BootstrapStyles.Color], override val componentId: ComponentId -)(content: Binding.NestedInterceptor => Modifier) extends UdashAlertBase(alertStyle, componentId) { +final class DismissibleUdashAlert private[alert] ( + alertStyle: ReadableProperty[BootstrapStyles.Color], + override val componentId: ComponentId, +)( + content: Binding.NestedInterceptor => Modifier +) extends UdashAlertBase(alertStyle, componentId) { private val _dismissed = Property[Boolean](false) def dismissed: ReadableProperty[Boolean] = _dismissed - private val button = UdashButton(options = UdashButtonOptions(color = BootstrapStyles.Color.Link.opt)) { _ => Seq[Modifier]( - componentId.withSuffix("close"), - `type` := "button", BootstrapStyles.close, - BootstrapTags.dataDismiss := "alert", aria.label := "close", - span(aria.hidden := "true")("×") - )} + private val button = UdashButton(options = UdashButtonOptions(color = BootstrapStyles.Color.Link.opt)) { _ => + Seq[Modifier]( + componentId.withSuffix("close"), + `type` := "button", + BootstrapStyles.close, + BootstrapTags.dataDismiss := "alert", + aria.label := "close", + span(aria.hidden := "true")("×"), + ) + } button.listen { case UdashButton.ButtonClickEvent(_, _) => _dismissed.set(true) @@ -33,7 +40,8 @@ final class DismissibleUdashAlert private[alert]( override val render: Element = template( BootstrapStyles.Alert.dismissible, - content(nestedInterceptor), buttonRendered + content(nestedInterceptor), + buttonRendered, ).render def dismiss(): Unit = @@ -41,9 +49,11 @@ final class DismissibleUdashAlert private[alert]( } object DismissibleUdashAlert extends UdashAlertBaseCompanion[DismissibleUdashAlert] { - protected def create(alertStyle: ReadableProperty[BootstrapStyles.Color], componentId: ComponentId)( + protected def create( + alertStyle: ReadableProperty[BootstrapStyles.Color], + componentId: ComponentId, + )( content: Binding.NestedInterceptor => Modifier - ): DismissibleUdashAlert = { + ): DismissibleUdashAlert = new DismissibleUdashAlert(alertStyle, componentId)(content) - } -} \ No newline at end of file +} diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/alert/UdashAlert.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/alert/UdashAlert.scala index 336b981d6..c3ea251cc 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/alert/UdashAlert.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/alert/UdashAlert.scala @@ -7,17 +7,22 @@ import io.udash.bootstrap.utils.BootstrapStyles import org.scalajs.dom.Element import scalatags.JsDom.all._ -final class UdashAlert private[alert]( - alertStyle: ReadableProperty[BootstrapStyles.Color], override val componentId: ComponentId -)(content: Binding.NestedInterceptor => Modifier) extends UdashAlertBase(alertStyle, componentId) { +final class UdashAlert private[alert] ( + alertStyle: ReadableProperty[BootstrapStyles.Color], + override val componentId: ComponentId, +)( + content: Binding.NestedInterceptor => Modifier +) extends UdashAlertBase(alertStyle, componentId) { override val render: Element = template(content(nestedInterceptor)).render } object UdashAlert extends UdashAlertBaseCompanion[UdashAlert] { - protected def create(alertStyle: ReadableProperty[BootstrapStyles.Color], componentId: ComponentId)( + protected def create( + alertStyle: ReadableProperty[BootstrapStyles.Color], + componentId: ComponentId, + )( content: Binding.NestedInterceptor => Modifier - ): UdashAlert = { + ): UdashAlert = new UdashAlert(alertStyle, componentId)(content) - } } diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/alert/UdashAlertBase.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/alert/UdashAlertBase.scala index 56737a23f..a78e1acba 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/alert/UdashAlertBase.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/alert/UdashAlertBase.scala @@ -5,7 +5,7 @@ import io.udash._ import io.udash.bindings.modifiers.Binding import io.udash.bootstrap.utils.{BootstrapStyles, UdashBootstrapComponent} import io.udash.css.CssView._ -import io.udash.wrappers.jquery.{JQuery, jQ} +import io.udash.wrappers.jquery.{jQ, JQuery} import org.scalajs.dom.html.Div import scalatags.JsDom.TypedTag import scalatags.JsDom.all._ @@ -13,16 +13,17 @@ import scalatags.JsDom.all._ import scala.scalajs.js private[alert] abstract class UdashAlertBase( - alertStyle: ReadableProperty[BootstrapStyles.Color], override val componentId: ComponentId + alertStyle: ReadableProperty[BootstrapStyles.Color], + override val componentId: ComponentId, ) extends UdashBootstrapComponent { - protected final def template: TypedTag[Div] = { + protected final def template: TypedTag[Div] = div( - componentId, role := "alert", + componentId, + role := "alert", BootstrapStyles.Alert.alert, - nestedInterceptor((BootstrapStyles.Alert.color _).reactiveApply(alertStyle)) + nestedInterceptor((BootstrapStyles.Alert.color _).reactiveApply(alertStyle)), ) - } override def kill(): Unit = { super.kill() @@ -34,25 +35,32 @@ private[alert] abstract class UdashAlertBase( } private[alert] trait UdashAlertBaseCompanion[T <: UdashAlertBase] { - protected def create(alertStyle: ReadableProperty[BootstrapStyles.Color], componentId: ComponentId)( + protected def create( + alertStyle: ReadableProperty[BootstrapStyles.Color], + componentId: ComponentId, + )( content: Binding.NestedInterceptor => Modifier ): T - /** - * Creates an alert with provided style. - * More: Bootstrap Docs. + /** Creates an alert with provided style. More: Bootstrap Docs. * - * @param alertStyle A color of the alert. - * @param componentId An id of the root DOM node. - * @param content A content of the alert. Use the provided interceptor to properly clean up bindings inside the content. - * @return A `UdashAlert` component, call `render` to create a DOM element. + * @param alertStyle + * A color of the alert. + * @param componentId + * An id of the root DOM node. + * @param content + * A content of the alert. Use the provided interceptor to properly clean up bindings inside the content. + * @return + * A `UdashAlert` component, call `render` to create a DOM element. */ def apply( alertStyle: ReadableProperty[BootstrapStyles.Color] = UdashBootstrap.ColorSecondary, - componentId: ComponentId = ComponentId.generate() - )(content: Binding.NestedInterceptor => Modifier): T = { + componentId: ComponentId = ComponentId.generate(), + )( + content: Binding.NestedInterceptor => Modifier + ): T = create(alertStyle, componentId)(content) - } def link(link: Url)(content: Modifier*): Modifier = a(href := link, BootstrapStyles.Alert.link)(content) @@ -61,4 +69,4 @@ private[alert] trait UdashAlertBaseCompanion[T <: UdashAlertBase] { @js.native private trait UdashAlertJQuery extends JQuery { def alert(cmd: String): UdashAlertJQuery = js.native -} \ No newline at end of file +} diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/badge/UdashBadge.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/badge/UdashBadge.scala index 7ba2ae670..f58a3af88 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/badge/UdashBadge.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/badge/UdashBadge.scala @@ -8,74 +8,87 @@ import org.scalajs.dom.Element import scalatags.JsDom.TypedTag import scalatags.JsDom.all._ -class UdashBadge private[badge]( +class UdashBadge private[badge] ( badgeStyle: ReadableProperty[BootstrapStyles.Color], pillStyle: ReadableProperty[Boolean], - override val componentId: ComponentId -)(content: Binding.NestedInterceptor => Modifier) - extends UdashBootstrapComponent { + override val componentId: ComponentId, +)( + content: Binding.NestedInterceptor => Modifier +) extends UdashBootstrapComponent { import io.udash.css.CssView._ protected def baseTag: TypedTag[Element] = span - override val render: Element = { + override val render: Element = baseTag( - componentId, BootstrapStyles.Badge.badge, + componentId, + BootstrapStyles.Badge.badge, nestedInterceptor((BootstrapStyles.Badge.color _).reactiveApply(badgeStyle)), - nestedInterceptor(BootstrapStyles.Badge.pill.styleIf(pillStyle)) + nestedInterceptor(BootstrapStyles.Badge.pill.styleIf(pillStyle)), )(content(nestedInterceptor)).render - } } private[badge] class UdashBadgeLink( link: ReadableProperty[String], badgeStyle: ReadableProperty[BootstrapStyles.Color], pillStyle: ReadableProperty[Boolean], - override val componentId: ComponentId -)(content: Binding.NestedInterceptor => Modifier) - extends UdashBadge(badgeStyle, pillStyle, componentId)(content) { + override val componentId: ComponentId, +)( + content: Binding.NestedInterceptor => Modifier +) extends UdashBadge(badgeStyle, pillStyle, componentId)(content) { - protected override def baseTag: TypedTag[Element] = + override protected def baseTag: TypedTag[Element] = a(nestedInterceptor(href.bind(link))) } object UdashBadge { - /** - * Creates a badge component. - * More: Bootstrap Docs. + + /** Creates a badge component. More: Bootstrap Docs. * - * @param badgeStyle A color of the badge. - * @param pillStyle If true, the `badge-pill` style will be applied. - * @param componentId An id of the root DOM node. - * @param content A badge content. Use the provided interceptor to properly clean up bindings inside the content. - * @return A `UdashBadge` component, call `render` to create a DOM element. + * @param badgeStyle + * A color of the badge. + * @param pillStyle + * If true, the `badge-pill` style will be applied. + * @param componentId + * An id of the root DOM node. + * @param content + * A badge content. Use the provided interceptor to properly clean up bindings inside the content. + * @return + * A `UdashBadge` component, call `render` to create a DOM element. */ def apply( badgeStyle: ReadableProperty[BootstrapStyles.Color] = UdashBootstrap.ColorSecondary, pillStyle: ReadableProperty[Boolean] = UdashBootstrap.False, - componentId: ComponentId = ComponentId.generate() - )(content: Binding.NestedInterceptor => Modifier): UdashBadge = { + componentId: ComponentId = ComponentId.generate(), + )( + content: Binding.NestedInterceptor => Modifier + ): UdashBadge = new UdashBadge(badgeStyle, pillStyle, componentId)(content) - } - /** - * Creates a badge link component. - * More: Bootstrap Docs. + /** Creates a badge link component. More: Bootstrap + * Docs. * - * @param link A content of the `href` parameter in the returned component. - * @param badgeStyle A color of the badge. - * @param pillStyle If true, the `badge-pill` style will be applied. - * @param componentId An id of the root DOM node. - * @param content A badge content. Use the provided interceptor to properly clean up bindings inside the content. - * @return A `UdashBadge` component, call `render` to create a DOM element. + * @param link + * A content of the `href` parameter in the returned component. + * @param badgeStyle + * A color of the badge. + * @param pillStyle + * If true, the `badge-pill` style will be applied. + * @param componentId + * An id of the root DOM node. + * @param content + * A badge content. Use the provided interceptor to properly clean up bindings inside the content. + * @return + * A `UdashBadge` component, call `render` to create a DOM element. */ def link( link: ReadableProperty[String], badgeStyle: ReadableProperty[BootstrapStyles.Color] = UdashBootstrap.ColorSecondary, pillStyle: ReadableProperty[Boolean] = UdashBootstrap.False, - componentId: ComponentId = ComponentId.generate() - )(content: Binding.NestedInterceptor => Modifier): UdashBadge = { + componentId: ComponentId = ComponentId.generate(), + )( + content: Binding.NestedInterceptor => Modifier + ): UdashBadge = new UdashBadgeLink(link, badgeStyle, pillStyle, componentId)(content) - } } diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/breadcrumb/UdashBreadcrumbs.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/breadcrumb/UdashBreadcrumbs.scala index 952e1dd7e..1cef0eb8a 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/breadcrumb/UdashBreadcrumbs.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/breadcrumb/UdashBreadcrumbs.scala @@ -8,12 +8,12 @@ import io.udash.properties.seq import org.scalajs.dom.Element import scalatags.JsDom.all.Modifier -final class UdashBreadcrumbs[ItemType, ElemType <: ReadableProperty[ItemType]] private( +final class UdashBreadcrumbs[ItemType, ElemType <: ReadableProperty[ItemType]] private ( pages: seq.ReadableSeqProperty[ItemType, ElemType], - override val componentId: ComponentId + override val componentId: ComponentId, )( itemFactory: (ElemType, Binding.NestedInterceptor) => Modifier, - isActive: ItemType => Boolean + isActive: ItemType => Boolean, ) extends UdashBootstrapComponent { import io.udash.css.CssView._ @@ -28,7 +28,7 @@ final class UdashBreadcrumbs[ItemType, ElemType <: ReadableProperty[ItemType]] p repeatWithNested(pages) { case (page, nested) => li( BootstrapStyles.Navigation.breadcrumbItem, - nested(BootstrapStyles.active.styleIf(page.transform(isActive))) + nested(BootstrapStyles.active.styleIf(page.transform(isActive))), )(itemFactory(page, nested)).render } ) @@ -44,72 +44,83 @@ object UdashBreadcrumbs { class Breadcrumb(val name: String, val link: Url) /** A default breadcrumb component factory. */ - val defaultPageFactory: (ReadableProperty[Breadcrumb], Binding.NestedInterceptor) => Modifier = { - (page, nested) => nested(produce(page) { page => + val defaultPageFactory: (ReadableProperty[Breadcrumb], Binding.NestedInterceptor) => Modifier = { (page, nested) => + nested(produce(page) { page => a(href := page.link)(page.name).render }) } - /** - * Creates a breadcrumbs component, synchronised with a provided SeqProperty. - * More: Bootstrap Docs. + /** Creates a breadcrumbs component, synchronised with a provided SeqProperty. More: Bootstrap Docs. * - * @param items The SeqProperty containing breadcrumbs data. - * @param componentId An id of the root DOM node. - * @param itemFactory Creates a DOM representation for an item from the `items` property. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param isActive Decides whether an element has an additional `active` style class. - * @tparam ItemType A single element's type in the `items` sequence. - * @tparam ElemType A type of a property containing an element in the `items` sequence. - * @return A `UdashBreadcrumbs` component, call `render` to create a DOM element. + * @param items + * The SeqProperty containing breadcrumbs data. + * @param componentId + * An id of the root DOM node. + * @param itemFactory + * Creates a DOM representation for an item from the `items` property. Use the provided interceptor to properly + * clean up bindings inside the content. + * @param isActive + * Decides whether an element has an additional `active` style class. + * @tparam ItemType + * A single element's type in the `items` sequence. + * @tparam ElemType + * A type of a property containing an element in the `items` sequence. + * @return + * A `UdashBreadcrumbs` component, call `render` to create a DOM element. */ def apply[ItemType, ElemType <: ReadableProperty[ItemType]]( items: seq.ReadableSeqProperty[ItemType, ElemType], - componentId: ComponentId = ComponentId.generate() + componentId: ComponentId = ComponentId.generate(), )( itemFactory: (ElemType, Binding.NestedInterceptor) => Modifier, - isActive: ItemType => Boolean = (_: ItemType) => false - ): UdashBreadcrumbs[ItemType, ElemType] = { + isActive: ItemType => Boolean = (_: ItemType) => false, + ): UdashBreadcrumbs[ItemType, ElemType] = new UdashBreadcrumbs(items, componentId)(itemFactory, isActive) - } - /** - * Creates breadcrumbs component, synchronised with a provided SeqProperty. Based on the default model with a name and a link. - * More: Bootstrap Docs. + /** Creates breadcrumbs component, synchronised with a provided SeqProperty. Based on the default model with a name + * and a link. More: Bootstrap Docs. * - * @param items The SeqProperty containing breadcrumbs data. - * @param componentId An id of the root DOM node. - * @param itemFactory Creates a DOM representation for an item from the `items` property. - * @param isActive Decides whether an element has an additional `active` style class. - * @return A `UdashBreadcrumbs` component, call `render` to create a DOM element. + * @param items + * The SeqProperty containing breadcrumbs data. + * @param componentId + * An id of the root DOM node. + * @param itemFactory + * Creates a DOM representation for an item from the `items` property. + * @param isActive + * Decides whether an element has an additional `active` style class. + * @return + * A `UdashBreadcrumbs` component, call `render` to create a DOM element. */ def default( items: ReadableSeqProperty[Breadcrumb], - componentId: ComponentId = ComponentId.generate() + componentId: ComponentId = ComponentId.generate(), )( itemFactory: (ReadableProperty[Breadcrumb], Binding.NestedInterceptor) => Modifier = defaultPageFactory, - isActive: Breadcrumb => Boolean = (_: Breadcrumb) => false - ): UdashBreadcrumbs[Breadcrumb, ReadableProperty[Breadcrumb]] = { + isActive: Breadcrumb => Boolean = (_: Breadcrumb) => false, + ): UdashBreadcrumbs[Breadcrumb, ReadableProperty[Breadcrumb]] = new UdashBreadcrumbs(items, componentId)(itemFactory, isActive) - } - /** - * Creates breadcrumbs component, synchronised with provided SeqProperty. Based on a simple string elements. - * More: Bootstrap Docs. + /** Creates breadcrumbs component, synchronised with provided SeqProperty. Based on a simple string elements. More: Bootstrap Docs. * - * @param items The SeqProperty containing breadcrumbs data. - * @param componentId An id of the root DOM node. - * @param itemFactory Creates a DOM representation for an item from the `items` property. - * @param isActive Decides whether an element has an additional `active` style class. - * @return A `UdashBreadcrumbs` component, call `render` to create a DOM element. + * @param items + * The SeqProperty containing breadcrumbs data. + * @param componentId + * An id of the root DOM node. + * @param itemFactory + * Creates a DOM representation for an item from the `items` property. + * @param isActive + * Decides whether an element has an additional `active` style class. + * @return + * A `UdashBreadcrumbs` component, call `render` to create a DOM element. */ def text( items: ReadableSeqProperty[String], - componentId: ComponentId = ComponentId.generate() + componentId: ComponentId = ComponentId.generate(), )( itemFactory: (ReadableProperty[String], Binding.NestedInterceptor) => Modifier = (p, nested) => nested(bind(p)), - isActive: String => Boolean = (_: String) => false - ): UdashBreadcrumbs[String, ReadableProperty[String]] = { + isActive: String => Boolean = (_: String) => false, + ): UdashBreadcrumbs[String, ReadableProperty[String]] = new UdashBreadcrumbs(items, componentId)(itemFactory, isActive) - } } diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/button/UdashButton.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/button/UdashButton.scala index 192e54083..3a4b41559 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/button/UdashButton.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/button/UdashButton.scala @@ -7,7 +7,7 @@ import com.avsystem.commons.misc.AbstractCase import io.udash.bindings.modifiers.Binding import io.udash.bootstrap.button.UdashButton.{ButtonClickEvent, ButtonTag, UdashButtonJQuery} import io.udash.bootstrap.utils.* -import io.udash.wrappers.jquery.{JQuery, jQ} +import io.udash.wrappers.jquery.{jQ, JQuery} import org.scalajs.dom import org.scalajs.dom.* import scalatags.JsDom @@ -16,47 +16,56 @@ import com.avsystem.commons.SharedExtensions.universalOps import scala.scalajs.js -/** - * Options for `UdashButton` component - * - * @param color A button style, one of the standard bootstrap colors `BootstrapStyles.Color`. - * @param size A button size, one of the standard bootstrap sizes `BootstrapStyles.Size`. - * @param outline If true, selects the outline style for the button. More: Bootstrap Docs. - * @param block If true, rendered button will be a full-width block. - * @param tag HTML tag used in button, one of tags defined in `ButtonTag` - * @param customModifiers Sequence of custom modifiers. - */ +/** Options for `UdashButton` component + * + * @param color + * A button style, one of the standard bootstrap colors `BootstrapStyles.Color`. + * @param size + * A button size, one of the standard bootstrap sizes `BootstrapStyles.Size`. + * @param outline + * If true, selects the outline style for the button. More: Bootstrap Docs. + * @param block + * If true, rendered button will be a full-width block. + * @param tag + * HTML tag used in button, one of tags defined in `ButtonTag` + * @param customModifiers + * Sequence of custom modifiers. + */ final case class UdashButtonOptions( color: Opt[BootstrapStyles.Color] = BootstrapStyles.Color.Secondary.opt, size: Opt[BootstrapStyles.Size] = Opt.empty, outline: Boolean = false, block: Boolean = false, tag: ButtonTag = ButtonTag.Button, - customModifiers: Seq[Modifier] = Seq.empty + customModifiers: Seq[Modifier] = Seq.empty, ) extends AbstractCase -final class UdashButton private( +final class UdashButton private ( override val componentId: ComponentId, active: ReadableProperty[Boolean], disabled: ReadableProperty[Boolean], - options: UdashButtonOptions -)(content: Binding.NestedInterceptor => Modifier) extends UdashBootstrapComponent with Listenable { + options: UdashButtonOptions, +)( + content: Binding.NestedInterceptor => Modifier +) extends UdashBootstrapComponent + with Listenable { override type EventType = ButtonClickEvent private val classes: Seq[Modifier] = Seq( BootstrapStyles.Button.btn: Modifier, - options.color.map(if (options.outline) BootstrapStyles.Button.outline _ else BootstrapStyles.Button.color _): Modifier, + options.color + .map(if (options.outline) BootstrapStyles.Button.outline _ else BootstrapStyles.Button.color _): Modifier, BootstrapStyles.Button.block.styleIf(options.block), nestedInterceptor(BootstrapStyles.active.styleIf(active)), nestedInterceptor(BootstrapStyles.disabled.styleIf(disabled)), nestedInterceptor(JsDom.all.disabled.attrIf(disabled)), - seqNodeFromOpt(options.size.map(size => BootstrapStyles.Button.size(size): Modifier)) + seqNodeFromOpt(options.size.map(size => BootstrapStyles.Button.size(size): Modifier)), ) ++ options.customModifiers - - override val render: dom.html.Element = { + override val render: dom.html.Element = (options.tag match { case ButtonTag.Button => button(componentId, tpe := "button")(classes)( @@ -68,8 +77,7 @@ final class UdashButton private( div(componentId)(classes)( onclick :+= ((me: MouseEvent) => if (!disabled.get) fire(ButtonClickEvent(this, me))) ) - }) (content(nestedInterceptor)).render - } + })(content(nestedInterceptor)).render override def kill(): Unit = { super.kill() @@ -81,17 +89,17 @@ final class UdashButton private( } object UdashButton { - final case class ButtonClickEvent(source: UdashButton, mouseEvent: MouseEvent) extends AbstractCase with ListenableEvent - - /** - * Holds button enclosing tag options. Since buttons have their own click listeners implemented they can be enclosed - * in various tags, e.g.: - * - * - Button - encloses the button in tags - * - Anchor - encloses the button in tags. For this type of tags link options can be used to indicate the href - * to redirect user on click - * - Div - encloses the button in
tags - */ + final case class ButtonClickEvent(source: UdashButton, mouseEvent: MouseEvent) + extends AbstractCase with ListenableEvent + + /** Holds button enclosing tag options. Since buttons have their own click listeners implemented they can be enclosed + * in various tags, e.g.: + * + * - Button - encloses the button in tags + * - Anchor - encloses the button in tags. For this type of tags link options can be used to indicate the + * href to redirect user on click + * - Div - encloses the button in
tags + */ sealed trait ButtonTag object ButtonTag { @@ -101,43 +109,56 @@ object UdashButton { } - /** - * Creates a button component. - * More: Bootstrap Docs. - * - * @param componentId An id of the root DOM node. - * @param active A property indicating if the button is in the `active` state. - * @param disabled A property indicating if the button is disabled. - * @param options `UdashButton` options object - * @param content A content of the button. - * Use the provided interceptor to properly clean up bindings inside the content. - * @return A `UdashButton` component, call `render` to create a DOM element representing this button. - */ + /** Creates a button component. More: Bootstrap + * Docs. + * + * @param componentId + * An id of the root DOM node. + * @param active + * A property indicating if the button is in the `active` state. + * @param disabled + * A property indicating if the button is disabled. + * @param options + * `UdashButton` options object + * @param content + * A content of the button. Use the provided interceptor to properly clean up bindings inside the content. + * @return + * A `UdashButton` component, call `render` to create a DOM element representing this button. + */ def apply( componentId: ComponentId = ComponentId.generate(), active: ReadableProperty[Boolean] = UdashBootstrap.False, disabled: ReadableProperty[Boolean] = UdashBootstrap.False, - options: UdashButtonOptions = UdashButtonOptions() - )(content: Binding.NestedInterceptor => Modifier): UdashButton = + options: UdashButtonOptions = UdashButtonOptions(), + )( + content: Binding.NestedInterceptor => Modifier + ): UdashButton = new UdashButton(componentId, active, disabled, options)(content) - /** - * Creates a toggle button component. - * More: Bootstrap Docs. - * - * @param componentId An id of the root DOM node. - * @param active A property indicating if the button is in the `active` state. - * @param disabled A property indicating if the button is disabled. - * @param options `UdashButton` options object - * @param content A content of the button. Use the provided interceptor to properly clean up bindings inside the content. - * @return A `UdashButton` component, call `render` to create a DOM element representing this button. - */ + /** Creates a toggle button component. More: Bootstrap + * Docs. + * + * @param componentId + * An id of the root DOM node. + * @param active + * A property indicating if the button is in the `active` state. + * @param disabled + * A property indicating if the button is disabled. + * @param options + * `UdashButton` options object + * @param content + * A content of the button. Use the provided interceptor to properly clean up bindings inside the content. + * @return + * A `UdashButton` component, call `render` to create a DOM element representing this button. + */ def toggle( componentId: ComponentId = ComponentId.generate(), active: Property[Boolean], disabled: ReadableProperty[Boolean] = UdashBootstrap.False, - options: UdashButtonOptions = UdashButtonOptions() - )(content: Binding.NestedInterceptor => Modifier): UdashButton = { + options: UdashButtonOptions = UdashButtonOptions(), + )( + content: Binding.NestedInterceptor => Modifier + ): UdashButton = { val button = new UdashButton(componentId, active, disabled, options)(content) button.listen { case _ => active.set(!active.get) } button diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/button/UdashButtonGroup.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/button/UdashButtonGroup.scala index 5d9c2266f..7000fbe71 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/button/UdashButtonGroup.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/button/UdashButtonGroup.scala @@ -9,25 +9,26 @@ import io.udash.properties.seq import org.scalajs.dom.Element import scalatags.JsDom.all._ -final class UdashButtonGroup[ItemType, ElemType <: ReadableProperty[ItemType]] private( +final class UdashButtonGroup[ItemType, ElemType <: ReadableProperty[ItemType]] private ( items: seq.ReadableSeqProperty[ItemType, ElemType], size: ReadableProperty[Option[BootstrapStyles.Size]], vertical: ReadableProperty[Boolean], justified: ReadableProperty[Boolean], - override val componentId: ComponentId -)(itemFactory: (ElemType, Binding.NestedInterceptor) => Seq[Element]) extends UdashBootstrapComponent { + override val componentId: ComponentId, +)( + itemFactory: (ElemType, Binding.NestedInterceptor) => Seq[Element] +) extends UdashBootstrapComponent { import io.udash.css.CssView._ private def buttonFullWidth: Binding = BootstrapStyles.Sizing.width100.styleIf(justified) - private val classes: List[Modifier] = { + private val classes: List[Modifier] = (BootstrapStyles.Button.group: Modifier) :: nestedInterceptor(BootstrapStyles.Button.groupVertical.styleIf(vertical)) :: nestedInterceptor(BootstrapStyles.Display.flex().styleIf(justified)) :: nestedInterceptor((BootstrapStyles.Button.groupSize _).reactiveOptionApply(size)) :: Nil - } override val render: Element = div(componentId, role := "group", classes)( @@ -42,137 +43,181 @@ final class UdashButtonGroup[ItemType, ElemType <: ReadableProperty[ItemType]] p } object UdashButtonGroup { - /** - * Creates a static buttons group. - * More: Bootstrap Docs. - * - * @param size A buttons group size. It shadows the size of each button. - * @param vertical If true, the buttons will be rendered vertically. - * @param justified If true, the buttons will be justified. - * @param componentId An id of the root DOM node. - * @param buttons Rendered buttons belonging to this group. - * @return A `UdashButtonGroup` component, call `render` to create a DOM element representing this group. - */ + + /** Creates a static buttons group. More: Bootstrap Docs. + * + * @param size + * A buttons group size. It shadows the size of each button. + * @param vertical + * If true, the buttons will be rendered vertically. + * @param justified + * If true, the buttons will be justified. + * @param componentId + * An id of the root DOM node. + * @param buttons + * Rendered buttons belonging to this group. + * @return + * A `UdashButtonGroup` component, call `render` to create a DOM element representing this group. + */ def apply( size: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None, vertical: ReadableProperty[Boolean] = UdashBootstrap.False, justified: ReadableProperty[Boolean] = UdashBootstrap.False, - componentId: ComponentId = ComponentId.generate() - )(buttons: Element*): UdashButtonGroup[Element, Property[Element]] = { + componentId: ComponentId = ComponentId.generate(), + )( + buttons: Element* + ): UdashButtonGroup[Element, Property[Element]] = reactive[Element, Property[Element]]( SeqProperty[Element](buttons), - size, vertical, justified, componentId + size, + vertical, + justified, + componentId, )((p, _) => p.get) - } - - /** - * Creates a dynamic button group. `items` sequence changes will be synchronized with the rendered button group. - * More: Bootstrap Docs. - * - * @param items Data items which will be represented as the buttons in this group. - * @param size A buttons group size. It shadows the size of each button. - * @param vertical If true, the buttons will be rendered vertically. - * @param justified If true, the buttons will be justified. - * @param componentId An id of the root DOM node. - * @param itemFactory Creates a button based on an item from the `items` sequence. - * Use the provided interceptor to properly clean up bindings inside the content. - * @tparam ItemType A single element's type in the `items` sequence. - * @tparam ElemType A type of a property containing an element in the `items` sequence. - * @return A `UdashButtonGroup` component, call `render` to create a DOM element representing this group. - */ + /** Creates a dynamic button group. `items` sequence changes will be synchronized with the rendered button group. + * More: Bootstrap Docs. + * + * @param items + * Data items which will be represented as the buttons in this group. + * @param size + * A buttons group size. It shadows the size of each button. + * @param vertical + * If true, the buttons will be rendered vertically. + * @param justified + * If true, the buttons will be justified. + * @param componentId + * An id of the root DOM node. + * @param itemFactory + * Creates a button based on an item from the `items` sequence. Use the provided interceptor to properly clean up + * bindings inside the content. + * @tparam ItemType + * A single element's type in the `items` sequence. + * @tparam ElemType + * A type of a property containing an element in the `items` sequence. + * @return + * A `UdashButtonGroup` component, call `render` to create a DOM element representing this group. + */ def reactive[ItemType, ElemType <: ReadableProperty[ItemType]]( items: seq.ReadableSeqProperty[ItemType, ElemType], size: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None, vertical: ReadableProperty[Boolean] = UdashBootstrap.False, justified: ReadableProperty[Boolean] = UdashBootstrap.False, - componentId: ComponentId = ComponentId.generate() - )(itemFactory: (ElemType, Binding.NestedInterceptor) => Seq[Element]): UdashButtonGroup[ItemType, ElemType] = { + componentId: ComponentId = ComponentId.generate(), + )( + itemFactory: (ElemType, Binding.NestedInterceptor) => Seq[Element] + ): UdashButtonGroup[ItemType, ElemType] = new UdashButtonGroup[ItemType, ElemType]( - items, size, vertical, justified, componentId + items, + size, + vertical, + justified, + componentId, )(itemFactory) - } - /** - * Creates a dynamic toggle buttons group. `items` sequence changes will be synchronized with the rendered button group. - * More: Bootstrap Docs. - * - * @param selectedItems Elements represented by the active buttons. - * @param options Data items which will be represented as buttons in this group. - * @param size A buttons group size. It shadows the size of each button. - * @param vertical If true, the buttons will be rendered vertically. - * @param justified If true, the buttons will be justified. - * @param componentId An id of the root DOM node. - * @param btnFactory It should create UdashButton instance based on provided item and active property. - * Don't forget to pass the second argument to created button as `active` arguemnt. - * The default implementation uses `toString` in order to create a button's content. - * @return A `UdashButtonGroup` component, call `render` to create a DOM element representing this group. - */ + /** Creates a dynamic toggle buttons group. `items` sequence changes will be synchronized with the rendered button + * group. More: Bootstrap Docs. + * + * @param selectedItems + * Elements represented by the active buttons. + * @param options + * Data items which will be represented as buttons in this group. + * @param size + * A buttons group size. It shadows the size of each button. + * @param vertical + * If true, the buttons will be rendered vertically. + * @param justified + * If true, the buttons will be justified. + * @param componentId + * An id of the root DOM node. + * @param btnFactory + * It should create UdashButton instance based on provided item and active property. Don't forget to pass the + * second argument to created button as `active` arguemnt. The default implementation uses `toString` in order to + * create a button's content. + * @return + * A `UdashButtonGroup` component, call `render` to create a DOM element representing this group. + */ def checkboxes[ItemType, ElemType <: ReadableProperty[ItemType]]( selectedItems: SeqProperty[ItemType], options: seq.ReadableSeqProperty[ItemType, ElemType], size: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None, vertical: ReadableProperty[Boolean] = UdashBootstrap.False, justified: ReadableProperty[Boolean] = UdashBootstrap.False, - componentId: ComponentId = ComponentId.generate() + componentId: ComponentId = ComponentId.generate(), )( - btnFactory: (ElemType, ReadableProperty[Boolean], Binding.NestedInterceptor) => UdashButton = - (item: ElemType, active: ReadableProperty[Boolean], nested: Binding.NestedInterceptor) => - nested(UdashButton(active = active)(nested => Seq(nested(bind(item))))) - ): UdashButtonGroup[ItemType, ElemType] = { + btnFactory: (ElemType, ReadableProperty[Boolean], Binding.NestedInterceptor) => UdashButton = ( + item: ElemType, + active: ReadableProperty[Boolean], + nested: Binding.NestedInterceptor, + ) => nested(UdashButton(active = active)(nested => Seq(nested(bind(item))))) + ): UdashButtonGroup[ItemType, ElemType] = new UdashButtonGroup[ItemType, ElemType]( - options, size, vertical, justified, componentId + options, + size, + vertical, + justified, + componentId, )((item, nested) => { val active: ReadableProperty[Boolean] = selectedItems.transform(_.contains(item.get)) val btn: UdashButton = btnFactory(item, active, nested) - btn.listen { - case UdashButton.ButtonClickEvent(_, _) => - if (active.get) selectedItems.remove(item.get) - else selectedItems.append(item.get) + btn.listen { case UdashButton.ButtonClickEvent(_, _) => + if (active.get) selectedItems.remove(item.get) + else selectedItems.append(item.get) } nested(btn) btn.render }) - } - /** - * Creates a dynamic radio buttons group. `items` sequence changes will be synchronized with the rendered button group. - * More: Bootstrap Docs. - * - * @param selectedItem An element represented by the active buttons. - * @param options Data items which will be represented as buttons in this group. - * @param size A buttons group size. It shadows the size of each button. - * @param vertical If true, the buttons will be rendered vertically. - * @param justified If true, the buttons will be justified. - * @param componentId An id of the root DOM node. - * @param btnFactory It should create UdashButton instance based on provided item and active property. - * Don't forget to pass the second argument to created button as `active` arguemnt. - * The default implementation uses `toString` in order to create a button's content. - * @return A `UdashButtonGroup` component, call `render` to create a DOM element representing this group. - */ + /** Creates a dynamic radio buttons group. `items` sequence changes will be synchronized with the rendered button + * group. More: Bootstrap Docs. + * + * @param selectedItem + * An element represented by the active buttons. + * @param options + * Data items which will be represented as buttons in this group. + * @param size + * A buttons group size. It shadows the size of each button. + * @param vertical + * If true, the buttons will be rendered vertically. + * @param justified + * If true, the buttons will be justified. + * @param componentId + * An id of the root DOM node. + * @param btnFactory + * It should create UdashButton instance based on provided item and active property. Don't forget to pass the + * second argument to created button as `active` arguemnt. The default implementation uses `toString` in order to + * create a button's content. + * @return + * A `UdashButtonGroup` component, call `render` to create a DOM element representing this group. + */ def radio[ItemType, ElemType <: ReadableProperty[ItemType]]( selectedItem: Property[ItemType], options: seq.ReadableSeqProperty[ItemType, ElemType], size: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None, vertical: ReadableProperty[Boolean] = UdashBootstrap.False, justified: ReadableProperty[Boolean] = UdashBootstrap.False, - componentId: ComponentId = ComponentId.generate() + componentId: ComponentId = ComponentId.generate(), )( - btnFactory: (ElemType, ReadableProperty[Boolean], Binding.NestedInterceptor) => UdashButton = - (item: ElemType, active: ReadableProperty[Boolean], nested: Binding.NestedInterceptor) => - nested(UdashButton(active = active)(nested => Seq(nested(bind(item))))) - ): UdashButtonGroup[ItemType, ElemType] = { + btnFactory: (ElemType, ReadableProperty[Boolean], Binding.NestedInterceptor) => UdashButton = ( + item: ElemType, + active: ReadableProperty[Boolean], + nested: Binding.NestedInterceptor, + ) => nested(UdashButton(active = active)(nested => Seq(nested(bind(item))))) + ): UdashButtonGroup[ItemType, ElemType] = new UdashButtonGroup[ItemType, ElemType]( - options, size, vertical, justified, componentId + options, + size, + vertical, + justified, + componentId, )((item, nested) => { val active: ReadableProperty[Boolean] = selectedItem.transform(_ == item.get) val btn: UdashButton = btnFactory(item, active, nested) - btn.listen { - case UdashButton.ButtonClickEvent(_, _) => - selectedItem.set(item.get) + btn.listen { case UdashButton.ButtonClickEvent(_, _) => + selectedItem.set(item.get) } nested(btn) btn.render }) - } -} \ No newline at end of file +} diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/button/UdashButtonToolbar.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/button/UdashButtonToolbar.scala index d26a21fdd..5f3e1e181 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/button/UdashButtonToolbar.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/button/UdashButtonToolbar.scala @@ -8,10 +8,12 @@ import io.udash.properties.seq import org.scalajs.dom.Element import scalatags.JsDom.all._ -final class UdashButtonToolbar[ItemType, ElemType <: ReadableProperty[ItemType]] private( +final class UdashButtonToolbar[ItemType, ElemType <: ReadableProperty[ItemType]] private ( val items: seq.ReadableSeqProperty[ItemType, ElemType], - override val componentId: ComponentId -)(itemFactory: (ElemType, Binding.NestedInterceptor) => Seq[Element]) extends UdashBootstrapComponent { + override val componentId: ComponentId, +)( + itemFactory: (ElemType, Binding.NestedInterceptor) => Seq[Element] +) extends UdashBootstrapComponent { import io.udash.css.CssView._ @@ -22,34 +24,43 @@ final class UdashButtonToolbar[ItemType, ElemType <: ReadableProperty[ItemType]] } object UdashButtonToolbar { - /** - * Creates a static buttons toolbar. - * More: Bootstrap Docs. - * - * @param componentId An id of the root DOM node. - * @param groups Rendered button groups belonging to the toolbar. - * @return A `UdashButtonToolbar` component, call `render` to create a DOM element representing this toolbar. - */ - def apply(componentId: ComponentId = ComponentId.generate())(groups: Element*): UdashButtonToolbar[Element, Property[Element]] = { - reactive[Element, Property[Element]](SeqProperty[Element](groups), componentId)((item, _) => item.get) - } + /** Creates a static buttons toolbar. More: Bootstrap Docs. + * + * @param componentId + * An id of the root DOM node. + * @param groups + * Rendered button groups belonging to the toolbar. + * @return + * A `UdashButtonToolbar` component, call `render` to create a DOM element representing this toolbar. + */ + def apply(componentId: ComponentId = ComponentId.generate())(groups: Element*) + : UdashButtonToolbar[Element, Property[Element]] = + reactive[Element, Property[Element]](SeqProperty[Element](groups), componentId)((item, _) => item.get) - /** - * Creates a dynamic buttons toolbar. - * More: Bootstrap Docs. - * - * @param items Data items which will be represented as the button groups in this toolbar. - * @param componentId An id of the root DOM node. - * @param itemFactory Creates a buttons group based on an item from the `items` sequence. - * Use the provided interceptor to properly clean up bindings inside the content. - * @tparam ItemType A single element's type in the `items` sequence. - * @tparam ElemType A type of a property containing an element in the `items` sequence. - * @return A `UdashButtonToolbar` component, call `render` to create a DOM element representing this toolbar. - */ + /** Creates a dynamic buttons toolbar. More: Bootstrap Docs. + * + * @param items + * Data items which will be represented as the button groups in this toolbar. + * @param componentId + * An id of the root DOM node. + * @param itemFactory + * Creates a buttons group based on an item from the `items` sequence. Use the provided interceptor to properly + * clean up bindings inside the content. + * @tparam ItemType + * A single element's type in the `items` sequence. + * @tparam ElemType + * A type of a property containing an element in the `items` sequence. + * @return + * A `UdashButtonToolbar` component, call `render` to create a DOM element representing this toolbar. + */ def reactive[ItemType, ElemType <: ReadableProperty[ItemType]]( - items: seq.ReadableSeqProperty[ItemType, ElemType], componentId: ComponentId = ComponentId.generate() - )(itemFactory: (ElemType, Binding.NestedInterceptor) => Seq[Element]): UdashButtonToolbar[ItemType, ElemType] = { + items: seq.ReadableSeqProperty[ItemType, ElemType], + componentId: ComponentId = ComponentId.generate(), + )( + itemFactory: (ElemType, Binding.NestedInterceptor) => Seq[Element] + ): UdashButtonToolbar[ItemType, ElemType] = new UdashButtonToolbar[ItemType, ElemType](items, componentId)(itemFactory) - } -} \ No newline at end of file +} diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/card/UdashCard.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/card/UdashCard.scala index 490408561..bc50015a5 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/card/UdashCard.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/card/UdashCard.scala @@ -10,138 +10,174 @@ import io.udash.css.CssView._ import org.scalajs.dom.Element import scalatags.JsDom.all._ -final class UdashCard private( +final class UdashCard private ( backgroundColor: ReadableProperty[Option[BootstrapStyles.Color]], borderColor: ReadableProperty[Option[BootstrapStyles.Color]], textAlignment: ReadableProperty[Option[BootstrapStyles.Align]], textColor: ReadableProperty[Option[BootstrapStyles.Color]], - override val componentId: ComponentId -)(content: UdashCard#CardElementsFactory => Modifier) extends UdashBootstrapComponent { + override val componentId: ComponentId, +)( + content: UdashCard#CardElementsFactory => Modifier +) extends UdashBootstrapComponent { class CardElementsFactory { - /** Creates header of a card with a provided content. - * More: Bootstrap Docs. */ + /** Creates header of a card with a provided content. More: Bootstrap Docs. + */ def header(content: Binding.NestedInterceptor => Modifier): Modifier = div(BootstrapStyles.Card.header)(content(nestedInterceptor)) - /** Creates footer of a card with a provided content. - * More: Bootstrap Docs. */ + /** Creates footer of a card with a provided content. More: Bootstrap Docs. + */ def footer(content: Binding.NestedInterceptor => Modifier): Modifier = div(BootstrapStyles.Card.footer)(content(nestedInterceptor)) - /** Creates body of a card with a provided content. - * More: Bootstrap Docs. */ + /** Creates body of a card with a provided content. More: Bootstrap Docs. + */ def body(content: Binding.NestedInterceptor => Modifier): Modifier = div(BootstrapStyles.Card.body)(content(nestedInterceptor)) - /** Creates title of a card with a provided content. - * More: Bootstrap Docs. */ + /** Creates title of a card with a provided content. More: Bootstrap Docs. + */ def title(content: Binding.NestedInterceptor => Modifier): Modifier = div(BootstrapStyles.Card.title)(content(nestedInterceptor)) - /** Creates subtitle of a card with a provided content. - * More: Bootstrap Docs. */ + /** Creates subtitle of a card with a provided content. More: Bootstrap Docs. + */ def subtitle(content: Binding.NestedInterceptor => Modifier): Modifier = div(BootstrapStyles.Card.subtitle)(content(nestedInterceptor)) - /** Creates text paragraph for a card with a provided content. - * More: Bootstrap Docs. */ + /** Creates text paragraph for a card with a provided content. More: Bootstrap Docs. + */ def text(content: Binding.NestedInterceptor => Modifier): Modifier = p(BootstrapStyles.Card.text)(content(nestedInterceptor)) - /** Creates link with a provided content. - * More: Bootstrap Docs. */ + /** Creates link with a provided content. More: Bootstrap Docs. + */ def link(link: ReadableProperty[String])(content: Binding.NestedInterceptor => Modifier): Modifier = a(nestedInterceptor(href.bind(link)), BootstrapStyles.Card.link)(content(nestedInterceptor)) - /** Creates top image for a card with a provided content. - * More: Bootstrap Docs. */ - def imgTop(imageSrc: ReadableProperty[String], alternativeText: ReadableProperty[String])( + /** Creates top image for a card with a provided content. More: Bootstrap Docs. + */ + def imgTop( + imageSrc: ReadableProperty[String], + alternativeText: ReadableProperty[String], + )( additionalModifiers: Binding.NestedInterceptor => Modifier ): Modifier = img( nestedInterceptor(src.bind(imageSrc)), nestedInterceptor(alt.bind(alternativeText)), - BootstrapStyles.Card.imageTop + BootstrapStyles.Card.imageTop, )(additionalModifiers(nestedInterceptor)) - /** Creates bottom image for a card with a provided content. - * More: Bootstrap Docs. */ - def imgBottom(imageSrc: ReadableProperty[String], alternativeText: ReadableProperty[String])( + /** Creates bottom image for a card with a provided content. More: Bootstrap Docs. + */ + def imgBottom( + imageSrc: ReadableProperty[String], + alternativeText: ReadableProperty[String], + )( additionalModifiers: Binding.NestedInterceptor => Modifier ): Modifier = img( nestedInterceptor(src.bind(imageSrc)), nestedInterceptor(alt.bind(alternativeText)), - BootstrapStyles.Card.imageBottom + BootstrapStyles.Card.imageBottom, )(additionalModifiers(nestedInterceptor)) - /** Wraps provided content into an element with `card-img-overlay` style. - * More: Bootstrap Docs. */ + /** Wraps provided content into an element with `card-img-overlay` style. More: Bootstrap Docs. + */ def imgOverlay(content: Binding.NestedInterceptor => Modifier): Modifier = div(BootstrapStyles.Card.imageOverlay)(content(nestedInterceptor)) - /** Puts the provided list group into the card with additional `list-group-flush` style. - * More: Bootstrap Docs. */ + /** Puts the provided list group into the card with additional `list-group-flush` style. More: Bootstrap Docs. + */ def listGroup(list: Binding.NestedInterceptor => UdashListGroup[_, _]): Modifier = list(nestedInterceptor).render.styles(BootstrapStyles.ListGroup.flush) - /** Puts the provided navigation tabs into the card with additional `card-header-tabs` style. - * More: Bootstrap Docs. */ + /** Puts the provided navigation tabs into the card with additional `card-header-tabs` style. More: Bootstrap Docs. + */ def navigationTabs(navigation: Binding.NestedInterceptor => UdashNav[_, _]): Modifier = navigation(nestedInterceptor).render.styles(BootstrapStyles.Card.navTabs) - /** Puts the provided navigation tabs into the card with additional `card-header-pills` style. - * More: Bootstrap Docs. */ + /** Puts the provided navigation tabs into the card with additional `card-header-pills` style. More: Bootstrap Docs. + */ def navigationPills(navigation: Binding.NestedInterceptor => UdashNav[_, _]): Modifier = navigation(nestedInterceptor).render.styles(BootstrapStyles.Card.navPills) } override val render: Element = div( - BootstrapStyles.Card.card, componentId, - nestedInterceptor((BootstrapStyles.Text.align(_: BootstrapStyles.Align, BootstrapStyles.ResponsiveBreakpoint.All)).reactiveOptionApply(textAlignment)), + BootstrapStyles.Card.card, + componentId, + nestedInterceptor( + (BootstrapStyles.Text + .align(_: BootstrapStyles.Align, BootstrapStyles.ResponsiveBreakpoint.All)) + .reactiveOptionApply(textAlignment) + ), nestedInterceptor((BootstrapStyles.Text.color _).reactiveOptionApply(textColor)), nestedInterceptor((BootstrapStyles.Background.color _).reactiveOptionApply(backgroundColor)), nestedInterceptor((BootstrapStyles.Border.color _).reactiveOptionApply(borderColor)), - content(new CardElementsFactory) + content(new CardElementsFactory), ).render } object UdashCard { - /** - * Creates a card component. - * More: Bootstrap Docs. + + /** Creates a card component. More: Bootstrap Docs. * - * @param backgroundColor A card style, one of the standard bootstrap colors `BootstrapStyles.Color`. - * @param borderColor A color of the borders. One of the standard bootstrap colors `BootstrapStyles.Color`. - * @param textAlignment Alignment of a content of tthe card. One of the `BootstrapStyles.Align` values. - * @param textColor A color of the texts inside the card. One of the standard bootstrap colors `BootstrapStyles.Color`. - * @param componentId An id of the root DOM node. - * @param content A factory of the card elements. All elements created with the factory will be cleaned up on the card cleanup. - * @return A `UdashCard` component, call `render` to create a DOM element representing this button. + * @param backgroundColor + * A card style, one of the standard bootstrap colors `BootstrapStyles.Color`. + * @param borderColor + * A color of the borders. One of the standard bootstrap colors `BootstrapStyles.Color`. + * @param textAlignment + * Alignment of a content of tthe card. One of the `BootstrapStyles.Align` values. + * @param textColor + * A color of the texts inside the card. One of the standard bootstrap colors `BootstrapStyles.Color`. + * @param componentId + * An id of the root DOM node. + * @param content + * A factory of the card elements. All elements created with the factory will be cleaned up on the card cleanup. + * @return + * A `UdashCard` component, call `render` to create a DOM element representing this button. */ def apply( backgroundColor: ReadableProperty[Option[BootstrapStyles.Color]] = UdashBootstrap.None, borderColor: ReadableProperty[Option[BootstrapStyles.Color]] = UdashBootstrap.None, textAlignment: ReadableProperty[Option[BootstrapStyles.Align]] = UdashBootstrap.None, textColor: ReadableProperty[Option[BootstrapStyles.Color]] = UdashBootstrap.None, - componentId: ComponentId = ComponentId.generate() - )(content: UdashCard#CardElementsFactory => Modifier): UdashCard = { + componentId: ComponentId = ComponentId.generate(), + )( + content: UdashCard#CardElementsFactory => Modifier + ): UdashCard = new UdashCard(backgroundColor, borderColor, textAlignment, textColor, componentId)(content) - } - /** Puts the provided cards into a group. - * More: Bootstrap Docs. */ + /** Puts the provided cards into a group. More: Bootstrap Docs. + */ def group(cards: Modifier*): Element = div(BootstrapStyles.Card.group)(cards).render - /** Puts the provided cards into a deck. - * More: Bootstrap Docs. */ + /** Puts the provided cards into a deck. More: Bootstrap Docs. + */ def deck(cards: Modifier*): Element = div(BootstrapStyles.Card.deck)(cards).render - /** Puts the provided cards into a columns layout. - * More: Bootstrap Docs. */ + /** Puts the provided cards into a columns layout. More: Bootstrap Docs. + */ def columns(cards: Modifier*): Element = div(BootstrapStyles.Card.columns)(cards).render -} \ No newline at end of file +} diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/carousel/UdashCarousel.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/carousel/UdashCarousel.scala index 2d0e8f702..8b900e983 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/carousel/UdashCarousel.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/carousel/UdashCarousel.scala @@ -17,16 +17,17 @@ import scala.scalajs.js import scala.scalajs.js.Dictionary import scala.util.Try -final class UdashCarousel[ItemType, ElemType <: ReadableProperty[ItemType]] private( +final class UdashCarousel[ItemType, ElemType <: ReadableProperty[ItemType]] private ( slides: seq.ReadableSeqProperty[ItemType, ElemType], showIndicators: ReadableProperty[Boolean], animationOptions: ReadableProperty[AnimationOptions], srTexts: Option[(ReadableProperty[String], ReadableProperty[String])], val activeSlide: Property[Int], - override val componentId: ComponentId + override val componentId: ComponentId, )( slideContentFactory: (ElemType, Binding.NestedInterceptor) => Modifier -) extends UdashBootstrapComponent with Listenable { +) extends UdashBootstrapComponent + with Listenable { import UdashCarousel._ import io.udash.bootstrap.utils.BootstrapStyles.Carousel @@ -53,8 +54,9 @@ final class UdashCarousel[ItemType, ElemType <: ReadableProperty[ItemType]] priv override val render: Element = { def indicators(): Binding = { def indicator(index: Int) = li( - dataTarget := s"#$componentId", dataSlideTo := index, - nestedInterceptor(BootstrapStyles.active.styleIf(activeSlide.transform(_ == index))) + dataTarget := s"#$componentId", + dataSlideTo := index, + nestedInterceptor(BootstrapStyles.active.styleIf(activeSlide.transform(_ == index))), ) val indices = slides.transform(_.length) @@ -69,7 +71,7 @@ final class UdashCarousel[ItemType, ElemType <: ReadableProperty[ItemType]] priv nestedInterceptor(repeatWithIndex(slides) { (slide, idx, nested) => div( BootstrapStyles.Carousel.item, - nested(BootstrapStyles.active.styleIf(idx.combine(activeSlide)(_ == _))) + nested(BootstrapStyles.active.styleIf(idx.combine(activeSlide)(_ == _))), )( slideContentFactory(slide, nested) ).render @@ -99,19 +101,31 @@ final class UdashCarousel[ItemType, ElemType <: ReadableProperty[ItemType]] priv ), a(Carousel.controlNext, Carousel.control, href := s"#$componentId", role := "button", dataSlide := "next")( span(aria.label.bind(srTexts.map(_._2).getOrElse("Next".toProperty)))(Carousel.controlNextIcon) - ) + ), ).render val jqCarousel = jQ(res).asInstanceOf[UdashCarouselJQuery] - nestedInterceptor(new JQueryOnBinding(jqCarousel, "slide.bs.carousel", (_: Element, ev: JQueryEvent) => { - val (idx, dir) = extractEventData(ev) - fire(CarouselEvent(this, idx, dir, changed = false)) - })) - nestedInterceptor(new JQueryOnBinding(jqCarousel, "slid.bs.carousel", (_: Element, ev: JQueryEvent) => { - val (idx, dir) = extractEventData(ev) - activeSlide.set(idx) - fire(CarouselEvent(this, idx, dir, changed = true)) - })) + nestedInterceptor( + new JQueryOnBinding( + jqCarousel, + "slide.bs.carousel", + (_: Element, ev: JQueryEvent) => { + val (idx, dir) = extractEventData(ev) + fire(CarouselEvent(this, idx, dir, changed = false)) + }, + ) + ) + nestedInterceptor( + new JQueryOnBinding( + jqCarousel, + "slid.bs.carousel", + (_: Element, ev: JQueryEvent) => { + val (idx, dir) = extractEventData(ev) + activeSlide.set(idx) + fire(CarouselEvent(this, idx, dir, changed = true)) + }, + ) + ) propertyListeners += animationOptions.listen { animationOptions => jqCarousel.carousel(animationOptions.native) @@ -127,10 +141,10 @@ final class UdashCarousel[ItemType, ElemType <: ReadableProperty[ItemType]] priv /** Pause slide transition. */ def pause(): Unit = jQSelector().carousel("pause") - /** - * Change active slide. + /** Change active slide. * - * @param slideNumber new active slide index + * @param slideNumber + * new active slide index */ def goTo(slideNumber: Int): Unit = jQSelector().carousel(slideNumber) @@ -150,79 +164,102 @@ final class UdashCarousel[ItemType, ElemType <: ReadableProperty[ItemType]] priv } object UdashCarousel { - /** - * Creates a carousel component. - * More: Bootstrap Docs. - * - * @param slides A SeqProperty of carousel slides. - * @param showIndicators If true, the component shows carousel slide indicators. - * @param animationOptions A carousel animation options. - * @param srTexts Optional properties for previous and next arrows aria.label texts. - * @param activeSlide An active carousel slide index. - * @param componentId The arousel DOM element id. - * @param slideContentFactory Creates content of a slide. - * Use the provided interceptor to properly clean up bindings inside the content. - * @tparam ItemType A single element's type in the `items` sequence. - * @tparam ElemType A type of a property containing an element in the `items` sequence. - * @return A `UdashCarousel` component, call `render` to create a DOM element representing this button. - */ + + /** Creates a carousel component. More: Bootstrap + * Docs. + * + * @param slides + * A SeqProperty of carousel slides. + * @param showIndicators + * If true, the component shows carousel slide indicators. + * @param animationOptions + * A carousel animation options. + * @param srTexts + * Optional properties for previous and next arrows aria.label texts. + * @param activeSlide + * An active carousel slide index. + * @param componentId + * The arousel DOM element id. + * @param slideContentFactory + * Creates content of a slide. Use the provided interceptor to properly clean up bindings inside the content. + * @tparam ItemType + * A single element's type in the `items` sequence. + * @tparam ElemType + * A type of a property containing an element in the `items` sequence. + * @return + * A `UdashCarousel` component, call `render` to create a DOM element representing this button. + */ def apply[ItemType, ElemType <: ReadableProperty[ItemType]]( slides: seq.ReadableSeqProperty[ItemType, ElemType], showIndicators: ReadableProperty[Boolean] = UdashBootstrap.True, animationOptions: ReadableProperty[AnimationOptions] = AnimationOptions().toProperty, srTexts: Option[(ReadableProperty[String], ReadableProperty[String])] = None, activeSlide: Property[Int] = Property(0), - componentId: ComponentId = ComponentId.generate() + componentId: ComponentId = ComponentId.generate(), )( slideContentFactory: (ElemType, Binding.NestedInterceptor) => Modifier - ): UdashCarousel[ItemType, ElemType] = { + ): UdashCarousel[ItemType, ElemType] = new UdashCarousel(slides, showIndicators, animationOptions, srTexts, activeSlide, componentId)(slideContentFactory) - } - /** - * Creates the UdashCarousel component consisting of `UdashCarouselSlide`. - * More: Bootstrap Docs. - * - * @param slides A SeqProperty of carousel slides. - * @param showIndicators If true, the component shows carousel slide indicators. - * @param animationOptions A carousel animation options. - * @param srTexts Optional properties for previous and next arrows aria.label texts. - * @param activeSlide An active carousel slide index. - * @param componentId The arousel DOM element id. - * @param slideContentFactory Creates content of a slide. - * Use the provided interceptor to properly clean up bindings inside the content. - * @return A `UdashCarousel` component, call `render` to create a DOM element representing this button. - */ + /** Creates the UdashCarousel component consisting of `UdashCarouselSlide`. More: Bootstrap Docs. + * + * @param slides + * A SeqProperty of carousel slides. + * @param showIndicators + * If true, the component shows carousel slide indicators. + * @param animationOptions + * A carousel animation options. + * @param srTexts + * Optional properties for previous and next arrows aria.label texts. + * @param activeSlide + * An active carousel slide index. + * @param componentId + * The arousel DOM element id. + * @param slideContentFactory + * Creates content of a slide. Use the provided interceptor to properly clean up bindings inside the content. + * @return + * A `UdashCarousel` component, call `render` to create a DOM element representing this button. + */ def default( slides: ReadableSeqProperty[UdashCarouselSlide], showIndicators: ReadableProperty[Boolean] = UdashBootstrap.True, animationOptions: ReadableProperty[AnimationOptions] = AnimationOptions().toProperty, srTexts: Option[(ReadableProperty[String], ReadableProperty[String])] = None, activeSlide: Property[Int] = Property(0), - componentId: ComponentId = ComponentId.generate() + componentId: ComponentId = ComponentId.generate(), )( - slideContentFactory: (ReadableProperty[UdashCarouselSlide], Binding.NestedInterceptor) => Modifier = - (slide, nested) => nested(produce(slide)(_.render)) - ): UdashCarousel[UdashCarouselSlide, ReadableProperty[UdashCarouselSlide]] = { + slideContentFactory: (ReadableProperty[UdashCarouselSlide], Binding.NestedInterceptor) => Modifier = ( + slide, + nested, + ) => nested(produce(slide)(_.render)) + ): UdashCarousel[UdashCarouselSlide, ReadableProperty[UdashCarouselSlide]] = new UdashCarousel(slides, showIndicators, animationOptions, srTexts, activeSlide, componentId)(slideContentFactory) - } - /** - * Event emitted by [[UdashCarousel]] on slide change transition start - * - * @param source The [[UdashCarousel]] emitting the event. - * @param targetIndex The index of the slide source transitioned to. - * @param direction The animation direction. Either `CarouselEvent.Direction.Left` or `CarouselEvent.Direction.Right`. - */ + /** Event emitted by [[UdashCarousel]] on slide change transition start + * + * @param source + * The [[UdashCarousel]] emitting the event. + * @param targetIndex + * The index of the slide source transitioned to. + * @param direction + * The animation direction. Either `CarouselEvent.Direction.Left` or `CarouselEvent.Direction.Right`. + */ final case class CarouselEvent[ItemType, ElemType <: ReadableProperty[ItemType]]( - source: UdashCarousel[ItemType, ElemType], targetIndex: Int, direction: Direction, changed: Boolean - ) extends AbstractCase with ListenableEvent + source: UdashCarousel[ItemType, ElemType], + targetIndex: Int, + direction: Direction, + changed: Boolean, + ) extends AbstractCase + with ListenableEvent object CarouselEvent { + /** Carousel animation direction. */ final class Direction(implicit enumCtx: EnumCtx) extends AbstractValueEnum object Direction extends ValueEnumCompanion[Direction] { final val Left, Right: Value = new Direction + /** Animation direction from carousel.js that neither left nor right. */ final val Unknown: Value = new Direction } @@ -245,18 +282,26 @@ object UdashCarousel { var keyboard: Boolean = js.native } - /** - * [[UdashCarousel]] animation options. + /** [[UdashCarousel]] animation options. * - * @param interval The amount of time to delay between automatically cycling an item. - * @param pause Indicated whether the carousel should pass on some specific event. See [[UdashCarousel.AnimationOptions.PauseOption]]. - * @param wrap Should the carousel cycle continuously or have hard stops. - * @param keyboard Should the carousel react to keyboard events. - * @param active Should the animation be active. + * @param interval + * The amount of time to delay between automatically cycling an item. + * @param pause + * Indicated whether the carousel should pass on some specific event. See + * [[UdashCarousel.AnimationOptions.PauseOption]]. + * @param wrap + * Should the carousel cycle continuously or have hard stops. + * @param keyboard + * Should the carousel react to keyboard events. + * @param active + * Should the animation be active. */ final case class AnimationOptions( - interval: Duration = 5 seconds, pause: PauseOption = PauseOption.Hover, wrap: Boolean = true, - keyboard: Boolean = true, active: Boolean = true + interval: Duration = 5.seconds, + pause: PauseOption = PauseOption.Hover, + wrap: Boolean = true, + keyboard: Boolean = true, + active: Boolean = true, ) { private[UdashCarousel] def native: CarouselOptionsJS = { val options = js.Object().asInstanceOf[CarouselOptionsJS] @@ -272,6 +317,7 @@ object UdashCarousel { final class PauseOption(name: String)(implicit enumCtx: EnumCtx) extends AbstractValueEnum object PauseOption extends AbstractValueEnumCompanion[PauseOption] { + /** Pauses the cycling of the carousel on mouseenter and resumes the cycling of the carousel on mouseleave. */ final val Hover: Value = new PauseOption("hover") final val False: Value = new PauseOption("false") @@ -279,21 +325,21 @@ object UdashCarousel { } } -/** - * [[UdashCarousel]] slide. +/** [[UdashCarousel]] slide. * - * @param imgSrc Slide image source url. - * @param caption Slide caption content. + * @param imgSrc + * Slide image source url. + * @param caption + * Slide caption content. */ final case class UdashCarouselSlide(imgSrc: Url)(caption: Modifier*) { import io.udash.css.CssView._ - lazy val render: Node = { + lazy val render: Node = Seq( img(src := imgSrc.value, BootstrapStyles.Sizing.width100), div(BootstrapStyles.Carousel.caption)( caption - ) + ), ).render - } } diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/collapse/UdashAccordion.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/collapse/UdashAccordion.scala index e1def88c4..f9e27e598 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/collapse/UdashAccordion.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/collapse/UdashAccordion.scala @@ -11,13 +11,14 @@ import org.scalajs.dom._ import scala.collection.mutable -final class UdashAccordion[ItemType, ElemType <: ReadableProperty[ItemType]] private( +final class UdashAccordion[ItemType, ElemType <: ReadableProperty[ItemType]] private ( elements: seq.ReadableSeqProperty[ItemType, ElemType], - override val componentId: ComponentId + override val componentId: ComponentId, )( heading: (ElemType, Binding.NestedInterceptor) => Seq[Element], - body: (ElemType, Binding.NestedInterceptor) => Seq[Element] -) extends UdashBootstrapComponent with Listenable { + body: (ElemType, Binding.NestedInterceptor) => Seq[Element], +) extends UdashBootstrapComponent + with Listenable { import io.udash.bootstrap.utils.BootstrapTags._ import io.udash.css.CssView._ @@ -45,26 +46,35 @@ final class UdashAccordion[ItemType, ElemType <: ReadableProperty[ItemType]] pri repeatWithIndex(elements) { case (item, idx, nested) => val headingId = ComponentId.generate() val card = UdashCard() { factory => - val collapse = UdashCollapse()(_ => Seq( - aria.labelledby := headingId, dataParent := s"#$componentId", - factory.body(nested => body(item, nested)) - )) + val collapse = UdashCollapse()(_ => + Seq( + aria.labelledby := headingId, + dataParent := s"#$componentId", + factory.body(nested => body(item, nested)), + ) + ) collapses(item) = collapse - val header = factory.header { nested => Seq( - headingId, - h5(BootstrapStyles.Spacing.margin(BootstrapStyles.Side.Bottom, size = BootstrapStyles.SpacingSize.None))( - button( - BootstrapStyles.Button.btn, BootstrapStyles.Button.color(BootstrapStyles.Color.Link), - tpe := "button", dataToggle:= "collapse", href := s"#${collapse.componentId}", - heading(item, nested) - ) + val header = factory.header { nested => + Seq( + headingId, + h5(BootstrapStyles.Spacing.margin(BootstrapStyles.Side.Bottom, size = BootstrapStyles.SpacingSize.None))( + button( + BootstrapStyles.Button.btn, + BootstrapStyles.Button.color(BootstrapStyles.Color.Link), + tpe := "button", + dataToggle := "collapse", + href := s"#${collapse.componentId}", + heading(item, nested), + ) + ), ) - )} + } nested(collapse) Seq[Modifier]( - header, collapse.render, + header, + collapse.render, nested( new Binding { override def applyTo(t: Element): Unit = { @@ -73,7 +83,7 @@ final class UdashAccordion[ItemType, ElemType <: ReadableProperty[ItemType]] pri } } } - ) + ), ) } nested(card) @@ -88,29 +98,34 @@ object UdashAccordion { override val source: UdashAccordion[ItemType, ElemType], item: ItemType, idx: Int, - collapseEvent: UdashCollapse.CollapseEvent - ) extends AbstractCase with ListenableEvent + collapseEvent: UdashCollapse.CollapseEvent, + ) extends AbstractCase + with ListenableEvent - /** - * Creates a dynamic accordion component. `items` sequence changes will be synchronised with the rendered elements. + /** Creates a dynamic accordion component. `items` sequence changes will be synchronised with the rendered elements. * More: Bootstrap Docs. * - * @param elements Data items which will be represented as cards in the accordion. - * @param componentId An id of the root DOM node. - * @param heading Creates panel header. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param body Creates panel body. - * Use the provided interceptor to properly clean up bindings inside the content. - * @tparam ItemType A single element's type in the `items` sequence. - * @tparam ElemType A type of a property containing an element in the `items` sequence. - * @return A `UdashAccordion` component, call `render` to create a DOM element. + * @param elements + * Data items which will be represented as cards in the accordion. + * @param componentId + * An id of the root DOM node. + * @param heading + * Creates panel header. Use the provided interceptor to properly clean up bindings inside the content. + * @param body + * Creates panel body. Use the provided interceptor to properly clean up bindings inside the content. + * @tparam ItemType + * A single element's type in the `items` sequence. + * @tparam ElemType + * A type of a property containing an element in the `items` sequence. + * @return + * A `UdashAccordion` component, call `render` to create a DOM element. */ def apply[ItemType, ElemType <: ReadableProperty[ItemType]]( elements: seq.ReadableSeqProperty[ItemType, ElemType], - componentId: ComponentId = ComponentId.generate() + componentId: ComponentId = ComponentId.generate(), )( heading: (ElemType, Binding.NestedInterceptor) => Seq[Element], - body: (ElemType, Binding.NestedInterceptor) => Seq[Element] + body: (ElemType, Binding.NestedInterceptor) => Seq[Element], ): UdashAccordion[ItemType, ElemType] = new UdashAccordion(elements, componentId)(heading, body) } diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/collapse/UdashCollapse.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/collapse/UdashCollapse.scala index e12881841..6e471c6b0 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/collapse/UdashCollapse.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/collapse/UdashCollapse.scala @@ -12,13 +12,14 @@ import scalatags.generic.AttrPair import scala.scalajs.js -final class UdashCollapse private( +final class UdashCollapse private ( parentSelector: Option[String], toggleOnInit: Boolean, - override val componentId: ComponentId + override val componentId: ComponentId, )( content: Binding.NestedInterceptor => Modifier -) extends UdashBootstrapComponent with Listenable { +) extends UdashBootstrapComponent + with Listenable { import UdashCollapse._ import io.udash.bootstrap.utils.BootstrapTags._ @@ -36,27 +37,54 @@ final class UdashCollapse private( /** Hides this collapse. */ def hide(): Unit = jQSelector().collapse("hide") - /** Attributes which should be added to the button toggling this collapse component. - * Example: `UdashButton()(_ => Seq[Modifier](collapse.toggleButtonAttrs(), "Toggle..."))`*/ + /** Attributes which should be added to the button toggling this collapse component. Example: + * `UdashButton()(_ => Seq[Modifier](collapse.toggleButtonAttrs(), "Toggle..."))` + */ def toggleButtonAttrs(): Seq[AttrPair[Element, String]] = { import scalatags.JsDom.all._ Seq( dataToggle := "collapse", - dataTarget := s"#$componentId" + dataTarget := s"#$componentId", ) } override val render: Element = { val el = div( - parentSelector.map(dataParent := _), dataToggle := toggleOnInit, - BootstrapStyles.Collapse.collapse, componentId + parentSelector.map(dataParent := _), + dataToggle := toggleOnInit, + BootstrapStyles.Collapse.collapse, + componentId, )(content(nestedInterceptor)).render val jQEl = jQ(el) - nestedInterceptor(new JQueryOnBinding(jQEl, "show.bs.collapse", (_: Element, _: JQueryEvent) => fire(CollapseEvent(this, CollapseEvent.EventType.Show)))) - nestedInterceptor(new JQueryOnBinding(jQEl, "shown.bs.collapse", (_: Element, _: JQueryEvent) => fire(CollapseEvent(this, CollapseEvent.EventType.Shown)))) - nestedInterceptor(new JQueryOnBinding(jQEl, "hide.bs.collapse", (_: Element, _: JQueryEvent) => fire(CollapseEvent(this, CollapseEvent.EventType.Hide)))) - nestedInterceptor(new JQueryOnBinding(jQEl, "hidden.bs.collapse", (_: Element, _: JQueryEvent) => fire(CollapseEvent(this, CollapseEvent.EventType.Hidden)))) + nestedInterceptor( + new JQueryOnBinding( + jQEl, + "show.bs.collapse", + (_: Element, _: JQueryEvent) => fire(CollapseEvent(this, CollapseEvent.EventType.Show)), + ) + ) + nestedInterceptor( + new JQueryOnBinding( + jQEl, + "shown.bs.collapse", + (_: Element, _: JQueryEvent) => fire(CollapseEvent(this, CollapseEvent.EventType.Shown)), + ) + ) + nestedInterceptor( + new JQueryOnBinding( + jQEl, + "hide.bs.collapse", + (_: Element, _: JQueryEvent) => fire(CollapseEvent(this, CollapseEvent.EventType.Hide)), + ) + ) + nestedInterceptor( + new JQueryOnBinding( + jQEl, + "hidden.bs.collapse", + (_: Element, _: JQueryEvent) => fire(CollapseEvent(this, CollapseEvent.EventType.Hidden)), + ) + ) el } @@ -71,45 +99,59 @@ final class UdashCollapse private( } object UdashCollapse { + /** More: Bootstrap Docs */ final case class CollapseEvent( override val source: UdashCollapse, - tpe: CollapseEvent.EventType - ) extends AbstractCase with ListenableEvent + tpe: CollapseEvent.EventType, + ) extends AbstractCase + with ListenableEvent object CollapseEvent { final class EventType(implicit enumCtx: EnumCtx) extends AbstractValueEnum object EventType extends AbstractValueEnumCompanion[EventType] { + /** This event fires immediately when the show instance method is called. */ final val Show: Value = new EventType - /** This event is fired when a collapse element has been made visible to the user (will wait for CSS transitions to complete). */ + + /** This event is fired when a collapse element has been made visible to the user (will wait for CSS transitions + * to complete). + */ final val Shown: Value = new EventType + /** This event is fired immediately when the hide method has been called. */ final val Hide: Value = new EventType - /** This event is fired when a collapse element has been hidden from the user (will wait for CSS transitions to complete). */ + + /** This event is fired when a collapse element has been hidden from the user (will wait for CSS transitions to + * complete). + */ final val Hidden: Value = new EventType } } - /** - * Creates a collapsible component. - * More: Bootstrap Docs. + /** Creates a collapsible component. More: Bootstrap + * Docs. * - * @param parentSelector If a selector is provided, all collapsible elements under the specified parent will be - * closed when this collapsible item is shown. - * @param toggleOnInit Toggles the collapsible element on invocation. - * @param componentId An id of the root DOM node. - * @param content A content of the component. - * Use the provided interceptor to properly clean up bindings inside the content. - * @return A `UdashCollapse` component, call `render` to create a DOM element. + * @param parentSelector + * If a selector is provided, all collapsible elements under the specified parent will be closed when this + * collapsible item is shown. + * @param toggleOnInit + * Toggles the collapsible element on invocation. + * @param componentId + * An id of the root DOM node. + * @param content + * A content of the component. Use the provided interceptor to properly clean up bindings inside the content. + * @return + * A `UdashCollapse` component, call `render` to create a DOM element. */ def apply( parentSelector: Option[String] = None, toggleOnInit: Boolean = true, - componentId: ComponentId = ComponentId.generate() - )(content: Binding.NestedInterceptor => Modifier): UdashCollapse = { + componentId: ComponentId = ComponentId.generate(), + )( + content: Binding.NestedInterceptor => Modifier + ): UdashCollapse = new UdashCollapse(parentSelector, toggleOnInit, componentId)(content) - } @js.native private trait UdashCollapseJQuery extends JQuery { diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/datepicker/UdashDatePicker.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/datepicker/UdashDatePicker.scala index c41ee0ae4..1274a8b7c 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/datepicker/UdashDatePicker.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/datepicker/UdashDatePicker.scala @@ -10,18 +10,20 @@ import io.udash.bootstrap.utils.{BootstrapStyles, BootstrapTags, UdashBootstrapC import io.udash.css.{CssStyle, CssStyleName} import io.udash.logging.CrossLogging import io.udash.wrappers.jquery.* -import org.scalajs.dom.{Element, document} +import org.scalajs.dom.{document, Element} import scala.scalajs.js import scala.scalajs.js.| import scala.util.Try /** Wrapper for the Bootstrap 4 Datepicker. */ -final class UdashDatePicker private[datepicker]( +final class UdashDatePicker private[datepicker] ( val date: Property[Option[ju.Date]], options: ReadableProperty[UdashDatePicker.DatePickerOptions], - override val componentId: ComponentId -) extends UdashBootstrapComponent with Listenable with CrossLogging { + override val componentId: ComponentId, +) extends UdashBootstrapComponent + with Listenable + with CrossLogging { import UdashDatePicker.* import scalatags.JsDom.all.* @@ -29,16 +31,18 @@ final class UdashDatePicker private[datepicker]( override type EventType = UdashDatePicker.DatePickerEvent private val inp = input( - componentId, tpe := "text", - BootstrapStyles.Form.control, CssStyleName("datetimepicker-input"), - BootstrapTags.dataToggle := "datetimepicker", BootstrapTags.dataTarget := s"#$componentId" + componentId, + tpe := "text", + BootstrapStyles.Form.control, + CssStyleName("datetimepicker-input"), + BootstrapTags.dataToggle := "datetimepicker", + BootstrapTags.dataTarget := s"#$componentId", ).render private val jQInput = jQ(inp).asInstanceOf[UdashDatePickerJQuery] private val changeCallback = (_: Element, event: JQueryEvent) => { - val dateOption = event.asInstanceOf[DatePickerChangeJQEvent].option - .flatMap(ev => sanitizeDate(ev.date)) - .map(momentToDate) + val dateOption = + event.asInstanceOf[DatePickerChangeJQEvent].option.flatMap(ev => sanitizeDate(ev.date)).map(momentToDate) val oldDateOption = date.get if (dateOption != oldDateOption) { dateOption match { @@ -82,7 +86,8 @@ final class UdashDatePicker private[datepicker]( propertyListeners += options.listen(opts => optionsToJsDict(opts).foreach { case (optionKey, optionValue) => jQInput.datetimepicker(optionKey, optionValue) } ) - propertyListeners += date.listen(optionalDate => optionalDate.foreach(date => jQInput.datetimepicker("date", dateToMoment(date)))) + propertyListeners += + date.listen(optionalDate => optionalDate.foreach(date => jQInput.datetimepicker("date", dateToMoment(date)))) inp } @@ -116,7 +121,7 @@ final class UdashDatePicker private[datepicker]( jQInput.off(HideEvent, hideCallback) jQInput.off(ShowEvent, showCallback) jQInput.off(ErrorEvent, errorCallback) - } + }, ) } @@ -133,12 +138,14 @@ final class UdashDatePicker private[datepicker]( "stepping" -> options.stepping, "useCurrent" -> options.useCurrent, "collapse" -> options.collapse, - "disabledDates" -> (if (options.disabledDates.nonEmpty) options.disabledDates.map(dateToMoment).toJSArray else false), + "disabledDates" -> + (if (options.disabledDates.nonEmpty) options.disabledDates.map(dateToMoment).toJSArray else false), "enabledDates" -> (if (options.enabledDates.nonEmpty) options.enabledDates.map(dateToMoment).toJSArray else false), "icons" -> options.icons.jsDictionary, "useStrict" -> options.useStrict, "sideBySide" -> options.sideBySide, - "daysOfWeekDisabled" -> (if (options.daysOfWeekDisabled.nonEmpty) options.daysOfWeekDisabled.map(_.id).toJSArray else false), + "daysOfWeekDisabled" -> + (if (options.daysOfWeekDisabled.nonEmpty) options.daysOfWeekDisabled.map(_.id).toJSArray else false), "calendarWeeks" -> options.calendarWeeks, "viewMode" -> options.viewMode.id, "keepOpen" -> options.keepOpen, @@ -156,19 +163,25 @@ final class UdashDatePicker private[datepicker]( "minDate" -> (if (options.minDate.nonEmpty) dateToMoment(options.minDate.get) else false), "maxDate" -> (if (options.maxDate.nonEmpty) dateToMoment(options.maxDate.get) else false), "defaultDate" -> (if (options.defaultDate.nonEmpty) dateToMoment(options.defaultDate.get) else false), - "toolbarPlacement" -> (if (options.toolbarPlacement.nonEmpty) options.toolbarPlacement.get.name else Placement.DefaultPlacement.name), - "widgetPositioning" -> options.widgetPositioning.map { - case (horizontal, vertical) => js.Dictionary("horizontal" -> horizontal.name, "vertical" -> vertical.name) - }.getOrElse(js.Dictionary("horizontal" -> Placement.AutoPlacement.name, "vertical" -> Placement.AutoPlacement.name)), + "toolbarPlacement" -> + (if (options.toolbarPlacement.nonEmpty) options.toolbarPlacement.get.name else Placement.DefaultPlacement.name), + "widgetPositioning" -> + options.widgetPositioning + .map { case (horizontal, vertical) => + js.Dictionary("horizontal" -> horizontal.name, "vertical" -> vertical.name) + } + .getOrElse( + js.Dictionary("horizontal" -> Placement.AutoPlacement.name, "vertical" -> Placement.AutoPlacement.name) + ), "buttons" -> js.Dictionary( "showToday" -> options.showToday, "showClear" -> options.showClear, - "showClose" -> options.showClose - ) + "showClose" -> options.showClose, + ), ) } - private def tooltipsOptionToJSDict(tooltips: DatePickerTooltips): js.Dictionary[js.Any] = { + private def tooltipsOptionToJSDict(tooltips: DatePickerTooltips): js.Dictionary[js.Any] = js.Dictionary[js.Any]( "today" -> tooltips.today, "clear" -> tooltips.clear, @@ -183,20 +196,18 @@ final class UdashDatePicker private[datepicker]( "prevDecade" -> tooltips.prevDecade, "nextDecade" -> tooltips.nextDecade, "prevCentury" -> tooltips.prevCentury, - "nextCentury" -> tooltips.nextCentury + "nextCentury" -> tooltips.nextCentury, ) - } private def internalFormat = options.get.format private def internalLocale = options.get.locale.getOrElse("en") - private def dateToMoment(date: ju.Date): MomentFormatWrapper = { + private def dateToMoment(date: ju.Date): MomentFormatWrapper = Try { val fullDate = moment(internalLocale, date.getTime.toDouble, "x") // removes date part which is not present in format string; it prevents multiple updates of date from one user interaction moment(internalLocale, fullDate.format(internalFormat), internalFormat) }.getOrElse(null) - } private def momentToDate(date: MomentFormatWrapper): ju.Date = Try { @@ -215,31 +226,41 @@ object UdashDatePicker { private val ShowEvent = "show.datetimepicker" private val ErrorEvent = "error.datetimepicker" - /** Creates a date picker component. - * More: Bootstrap 4 Datepicker Docs. - * - * @param date A date selected in the input. - * @param options A date picker's behaviour options. - * @param componentId The DOM element id. - * @return A `UdashDatePicker` component, call `render` to create a DOM element representing this button. - */ + /** Creates a date picker component. More: Bootstrap 4 + * Datepicker Docs. + * + * @param date + * A date selected in the input. + * @param options + * A date picker's behaviour options. + * @param componentId + * The DOM element id. + * @return + * A `UdashDatePicker` component, call `render` to create a DOM element representing this button. + */ def apply( date: Property[Option[ju.Date]], options: ReadableProperty[DatePickerOptions], - componentId: ComponentId = ComponentId.generate() + componentId: ComponentId = ComponentId.generate(), ): UdashDatePicker = new UdashDatePicker(date, options, componentId) - /** Combines two date pickers into a date range selector. - * More: Bootstrap 4 Datepicker Docs. + /** Combines two date pickers into a date range selector. More: Bootstrap 4 Datepicker Docs. * - * @param fromOptions Options of the `from` picker. - * @param toOptions Options of the `to` picker. - * @return Registration cancelling the range selector. + * @param fromOptions + * Options of the `from` picker. + * @param toOptions + * Options of the `to` picker. + * @return + * Registration cancelling the range selector. */ - def dateRange(from: UdashDatePicker, to: UdashDatePicker)( + def dateRange( + from: UdashDatePicker, + to: UdashDatePicker, + )( fromOptions: Property[DatePickerOptions], - toOptions: Property[DatePickerOptions] + toOptions: Property[DatePickerOptions], ): Registration = { val r1 = from.date.streamTo(toOptions)(d => toOptions.get.copy(minDate = d)) val r2 = to.date.streamTo(fromOptions)(d => fromOptions.get.copy(maxDate = d)) @@ -261,56 +282,95 @@ object UdashDatePicker { /** Loads Bootstrap Date Picker styles. */ def loadBootstrapDatePickerStyles(): Element = - link(rel := "stylesheet", href := "https://cdnjs.cloudflare.com/ajax/libs/tempusdominus-bootstrap-4/5.1.2/css/tempusdominus-bootstrap-4.min.css").render + link( + rel := "stylesheet", + href := + "https://cdnjs.cloudflare.com/ajax/libs/tempusdominus-bootstrap-4/5.1.2/css/tempusdominus-bootstrap-4.min.css", + ).render sealed trait DatePickerEvent extends AbstractCase with ListenableEvent object DatePickerEvent { final case class Show(source: UdashDatePicker) extends DatePickerEvent final case class Hide(source: UdashDatePicker, date: Option[ju.Date]) extends DatePickerEvent - final case class Change(source: UdashDatePicker, date: Option[ju.Date], oldDate: Option[ju.Date]) extends DatePickerEvent + final case class Change(source: UdashDatePicker, date: Option[ju.Date], oldDate: Option[ju.Date]) + extends DatePickerEvent final case class Error(source: UdashDatePicker, date: Option[ju.Date]) extends DatePickerEvent } - /** - * Full docs: here. + /** Full docs: here. * - * @param format See momentjs' docs for valid formats. - * Format also dictates what components are shown, e.g. MM/dd/YYYY will not display the time picker. - * @param dayViewHeaderFormat Changes the heading of the datepicker when in "days" view. - * @param extraFormats Allows for several input formats to be valid. - * @param stepping Number of minutes the up/down arrow's will move the minutes value in the time picker. - * @param minDate Prevents date/time selections before this date. - * @param maxDate Prevents date/time selections after this date. - * @param useCurrent On show, will set the picker to the current date/time. - * @param collapse Using a Bootstraps collapse to switch between date/time pickers. - * @param locale See momentjs' docs for valid locales. - * @param defaultDate Sets the picker default date/time. Overrides `useCurrent`. - * @param disabledDates Disables selection of dates in the array, e.g. holidays. - * @param enabledDates Disables selection of dates NOT in the array, e.g. holidays. - * @param icons Change the default icons for the pickers functions. - * @param useStrict Defines if moment should use strict date parsing when considering a date to be valid. - * @param sideBySide Shows the picker side by side when using the time and date together. - * @param daysOfWeekDisabled Disables the section of days of the week, e.g. weekends. - * @param calendarWeeks Shows the week of the year to the left of first day of the week. - * @param viewMode The default view to display when the picker is shown. - * Note: To limit the picker to selecting, for instance the year and month, use format: `MM/YYYY` - * @param toolbarPlacement Changes the placement of the icon toolbar. - * @param showToday Show the "Today" button in the icon toolbar. - * @param showClear Show the "Clear" button in the icon toolbar. - * @param showClose Show the "Close" button in the icon toolbar. - * @param widgetPositioning Position of datepicker widget. - * @param widgetParent On picker show, places the widget at the identifier object if the element has css position: 'relative'. - * @param keepOpen Will cause the date picker to stay open after selecting a date if no time components are being used. - * @param inline Will display the picker inline without the need of a input field. This will also hide borders and shadows. - * @param keepInvalid Will cause the date picker to not revert or overwrite invalid dates. - * @param ignoreReadonly Allow date picker show event to fire even when the associated input element has the `readonly="readonly"` property. - * @param allowInputToggle If `true`, the picker will show on textbox focus and icon click when used in a button group. - * @param focusOnShow If `false`, the textbox will not be given focus when the picker is shown - * @param enabledHours Will allow or disallow hour selections. - * @param disabledHours Will allow or disallow hour selections. - * @param viewDate This will change the viewDate without changing or setting the selected date. - * @param tooltips This will change the tooltips over each icon to a custom string. - */ + * @param format + * See momentjs' docs for valid formats. Format also + * dictates what components are shown, e.g. MM/dd/YYYY will not display the time picker. + * @param dayViewHeaderFormat + * Changes the heading of the datepicker when in "days" view. + * @param extraFormats + * Allows for several input formats to be valid. + * @param stepping + * Number of minutes the up/down arrow's will move the minutes value in the time picker. + * @param minDate + * Prevents date/time selections before this date. + * @param maxDate + * Prevents date/time selections after this date. + * @param useCurrent + * On show, will set the picker to the current date/time. + * @param collapse + * Using a Bootstraps collapse to switch between date/time pickers. + * @param locale + * See momentjs' docs for valid locales. + * @param defaultDate + * Sets the picker default date/time. Overrides `useCurrent`. + * @param disabledDates + * Disables selection of dates in the array, e.g. holidays. + * @param enabledDates + * Disables selection of dates NOT in the array, e.g. holidays. + * @param icons + * Change the default icons for the pickers functions. + * @param useStrict + * Defines if moment should use strict date parsing when considering a date to be valid. + * @param sideBySide + * Shows the picker side by side when using the time and date together. + * @param daysOfWeekDisabled + * Disables the section of days of the week, e.g. weekends. + * @param calendarWeeks + * Shows the week of the year to the left of first day of the week. + * @param viewMode + * The default view to display when the picker is shown. Note: To limit the picker to selecting, for instance the + * year and month, use format: `MM/YYYY` + * @param toolbarPlacement + * Changes the placement of the icon toolbar. + * @param showToday + * Show the "Today" button in the icon toolbar. + * @param showClear + * Show the "Clear" button in the icon toolbar. + * @param showClose + * Show the "Close" button in the icon toolbar. + * @param widgetPositioning + * Position of datepicker widget. + * @param widgetParent + * On picker show, places the widget at the identifier object if the element has css position: 'relative'. + * @param keepOpen + * Will cause the date picker to stay open after selecting a date if no time components are being used. + * @param inline + * Will display the picker inline without the need of a input field. This will also hide borders and shadows. + * @param keepInvalid + * Will cause the date picker to not revert or overwrite invalid dates. + * @param ignoreReadonly + * Allow date picker show event to fire even when the associated input element has the `readonly="readonly"` + * property. + * @param allowInputToggle + * If `true`, the picker will show on textbox focus and icon click when used in a button group. + * @param focusOnShow + * If `false`, the textbox will not be given focus when the picker is shown + * @param enabledHours + * Will allow or disallow hour selections. + * @param disabledHours + * Will allow or disallow hour selections. + * @param viewDate + * This will change the viewDate without changing or setting the selected date. + * @param tooltips + * This will change the tooltips over each icon to a custom string. + */ final case class DatePickerOptions( format: String, dayViewHeaderFormat: String = "MMMM YYYY", @@ -334,7 +394,9 @@ object UdashDatePicker { showToday: Boolean = false, showClear: Boolean = false, showClose: Boolean = false, - widgetPositioning: Option[(UdashDatePicker.Placement.HorizontalPlacement, UdashDatePicker.Placement.VerticalPlacement)] = None, + widgetPositioning: Option[ + (UdashDatePicker.Placement.HorizontalPlacement, UdashDatePicker.Placement.VerticalPlacement) + ] = None, widgetParent: Option[String] = None, keepOpen: Boolean = false, inline: Boolean = false, @@ -359,8 +421,8 @@ object UdashDatePicker { prevDecade = "Previous decade", nextDecade = "Next decade", prevCentury = "Previous century", - nextCentury = "Next century" - ) + nextCentury = "Next century", + ), ) extends AbstractCase object DatePickerOptions extends HasModelPropertyCreator[DatePickerOptions] @@ -370,20 +432,21 @@ object UdashDatePicker { } final class CustomDatePickerIcons( - val time: Option[CssStyle] = Option.empty, - val date: Option[CssStyle] = Option.empty, - val up: Option[CssStyle] = Option.empty, - val down: Option[CssStyle] = Option.empty, - val previous: Option[CssStyle] = Option.empty, - val next: Option[CssStyle] = Option.empty, - val today: Option[CssStyle] = Option.empty, - val clear: Option[CssStyle] = Option.empty, - val close: Option[CssStyle] = Option.empty + val time: Option[CssStyle] = Option.empty, + val date: Option[CssStyle] = Option.empty, + val up: Option[CssStyle] = Option.empty, + val down: Option[CssStyle] = Option.empty, + val previous: Option[CssStyle] = Option.empty, + val next: Option[CssStyle] = Option.empty, + val today: Option[CssStyle] = Option.empty, + val clear: Option[CssStyle] = Option.empty, + val close: Option[CssStyle] = Option.empty, ) extends DatePickerIcons { import scala.scalajs.js.JSConverters._ override val jsDictionary: js.Dictionary[js.Any] = js.Dictionary( - DefaultDatePickerIcon.values.iterator.map(_.name.toLowerCase()) + DefaultDatePickerIcon.values.iterator + .map(_.name.toLowerCase()) .zip(Iterator(time, date, up, down, previous, next, today, clear, close)) .flatMap { case (key, valueOpt) => valueOpt.map(key -> _.classNames.toJSArray) } .toSeq: _* @@ -391,7 +454,8 @@ object UdashDatePicker { } object DefaultDatePickerIcons extends DatePickerIcons { - override val jsDictionary: js.Dictionary[js.Any] = js.Dictionary(DefaultDatePickerIcon.values.map(_.jsDictionaryItem): _*) + override val jsDictionary: js.Dictionary[js.Any] = + js.Dictionary(DefaultDatePickerIcon.values.map(_.jsDictionaryItem): _*) } final class DefaultDatePickerIcon(style: CssStyle)(implicit enumCtx: EnumCtx) extends AbstractValueEnum { @@ -425,7 +489,7 @@ object UdashDatePicker { prevDecade: String, nextDecade: String, prevCentury: String, - nextCentury: String + nextCentury: String, ) extends AbstractCase final class DayOfWeek(val id: Int) @@ -511,26 +575,25 @@ object UdashDatePicker { // // This mutation observer is turned on just before the first callback registration and off when there are no components // registered to be watched. - private val datePickerMutationObserver = { + private val datePickerMutationObserver = new MutationObserver((records: js.Array[MutationRecord], _: MutationObserver) => { def mutationHandler(nodesExtractor: MutationRecord => NodeList[Node], callbacks: Map[Node, () => Unit]): Unit = { records - .flatMap(nodesExtractor(_) |> (recordNodes => for {i <- 0 until recordNodes.length} yield recordNodes(i))) + .flatMap(nodesExtractor(_) |> (recordNodes => for { i <- 0 until recordNodes.length } yield recordNodes(i))) .foreach(node => - callbacks.iterator - .filter { case (pickerNode, _) => node.contains(pickerNode) } - .foreach { case (_, callback) => callback() } + callbacks.iterator.filter { case (pickerNode, _) => node.contains(pickerNode) }.foreach { + case (_, callback) => callback() + } ) } mutationHandler(_.removedNodes, datePickerDetachCallbacks) mutationHandler(_.addedNodes, datePickerSetupCallbacks) }) - } def registerMutationCallbacks(pickerNode: Node, setupCallback: () => Unit, detachCallback: () => Unit): Unit = { if (datePickerSetupCallbacks.isEmpty && datePickerDetachCallbacks.isEmpty) - datePickerMutationObserver.observe(document.body, new MutationObserverInit { childList = true; subtree = true } ) + datePickerMutationObserver.observe(document.body, new MutationObserverInit { childList = true; subtree = true }) datePickerSetupCallbacks += (pickerNode -> setupCallback) datePickerDetachCallbacks += (pickerNode -> detachCallback) } diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/dropdown/UdashDropdown.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/dropdown/UdashDropdown.scala index cd38843b3..56312e86c 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/dropdown/UdashDropdown.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/dropdown/UdashDropdown.scala @@ -15,17 +15,18 @@ import scalatags.JsDom.all._ import scala.scalajs.js -final class UdashDropdown[ItemType, ElemType <: ReadableProperty[ItemType]] private( +final class UdashDropdown[ItemType, ElemType <: ReadableProperty[ItemType]] private ( items: seq.ReadableSeqProperty[ItemType, ElemType], dropDirection: ReadableProperty[UdashDropdown.Direction], rightAlignMenu: ReadableProperty[Boolean], buttonToggle: ReadableProperty[Boolean], - override val componentId: ComponentId + override val componentId: ComponentId, )( itemBindingFactory: UdashDropdown[ItemType, ElemType] => Binding, buttonContent: Binding.NestedInterceptor => Modifier, - buttonFactory: (NestedInterceptor => Modifier) => UdashButton -) extends UdashBootstrapComponent with Listenable { + buttonFactory: (NestedInterceptor => Modifier) => UdashButton, +) extends UdashBootstrapComponent + with Listenable { import UdashDropdown._ import io.udash.bootstrap.dropdown.UdashDropdown.DropdownEvent._ @@ -35,6 +36,7 @@ final class UdashDropdown[ItemType, ElemType <: ReadableProperty[ItemType]] priv /** Dropdown menu list ID. */ val menuId: ComponentId = componentId.withSuffix("menu") + /** Dropdown button ID. */ val buttonId: ComponentId = componentId.withSuffix("button") @@ -53,33 +55,65 @@ final class UdashDropdown[ItemType, ElemType <: ReadableProperty[ItemType]] priv val el = div( componentId, nestedInterceptor( - ((direction: Direction) => direction match { - case Direction.Up => BootstrapStyles.Dropdown.dropup - case Direction.Down => BootstrapStyles.Dropdown.dropdown - case Direction.Left => BootstrapStyles.Dropdown.dropleft - case Direction.Right => BootstrapStyles.Dropdown.dropright - }).reactiveApply(dropDirection) - ) + ( + (direction: Direction) => + direction match { + case Direction.Up => BootstrapStyles.Dropdown.dropup + case Direction.Down => BootstrapStyles.Dropdown.dropdown + case Direction.Left => BootstrapStyles.Dropdown.dropleft + case Direction.Right => BootstrapStyles.Dropdown.dropright + } + ).reactiveApply(dropDirection) + ), )( nestedInterceptor(buttonFactory { nested => Seq[Modifier]( - nested(BootstrapStyles.Dropdown.toggle.styleIf(buttonToggle)), buttonId, dataToggle := "dropdown", - aria.haspopup := true, aria.expanded := false, - buttonContent(nested), span(BootstrapStyles.Dropdown.caret) + nested(BootstrapStyles.Dropdown.toggle.styleIf(buttonToggle)), + buttonId, + dataToggle := "dropdown", + aria.haspopup := true, + aria.expanded := false, + buttonContent(nested), + span(BootstrapStyles.Dropdown.caret), ) }), div( BootstrapStyles.Dropdown.menu, nestedInterceptor(BootstrapStyles.Dropdown.menuRight.styleIf(rightAlignMenu)), - aria.labelledby := buttonId, menuId - )(nestedInterceptor(itemBindingFactory(this))) + aria.labelledby := buttonId, + menuId, + )(nestedInterceptor(itemBindingFactory(this))), ).render val jQEl = jQ(el) - nestedInterceptor(new JQueryOnBinding(jQEl, "show.bs.dropdown", (_: Element, _: JQueryEvent) => fire(VisibilityChangeEvent(this, EventType.Show)))) - nestedInterceptor(new JQueryOnBinding(jQEl, "shown.bs.dropdown", (_: Element, _: JQueryEvent) => fire(VisibilityChangeEvent(this, EventType.Shown)))) - nestedInterceptor(new JQueryOnBinding(jQEl, "hide.bs.dropdown", (_: Element, _: JQueryEvent) => fire(VisibilityChangeEvent(this, EventType.Hide)))) - nestedInterceptor(new JQueryOnBinding(jQEl, "hidden.bs.dropdown", (_: Element, _: JQueryEvent) => fire(VisibilityChangeEvent(this, EventType.Hidden)))) + nestedInterceptor( + new JQueryOnBinding( + jQEl, + "show.bs.dropdown", + (_: Element, _: JQueryEvent) => fire(VisibilityChangeEvent(this, EventType.Show)), + ) + ) + nestedInterceptor( + new JQueryOnBinding( + jQEl, + "shown.bs.dropdown", + (_: Element, _: JQueryEvent) => fire(VisibilityChangeEvent(this, EventType.Shown)), + ) + ) + nestedInterceptor( + new JQueryOnBinding( + jQEl, + "hide.bs.dropdown", + (_: Element, _: JQueryEvent) => fire(VisibilityChangeEvent(this, EventType.Hide)), + ) + ) + nestedInterceptor( + new JQueryOnBinding( + jQEl, + "hidden.bs.dropdown", + (_: Element, _: JQueryEvent) => fire(VisibilityChangeEvent(this, EventType.Hidden)), + ) + ) el } @@ -93,6 +127,7 @@ final class UdashDropdown[ItemType, ElemType <: ReadableProperty[ItemType]] priv } object UdashDropdown { + /** More: Bootstrap Docs */ sealed trait DropdownEvent[ItemType, ElemType <: ReadableProperty[ItemType]] extends AbstractCase with ListenableEvent { def tpe: DropdownEvent.EventType @@ -101,25 +136,35 @@ object UdashDropdown { object DropdownEvent { final class EventType(implicit enumCtx: EnumCtx) extends AbstractValueEnum object EventType extends AbstractValueEnumCompanion[EventType] { + /** This event fires immediately when the show instance method is called. */ final val Show: Value = new EventType - /** This event is fired when the dropdown has been made visible to the user (will wait for CSS transitions, to complete). */ + + /** This event is fired when the dropdown has been made visible to the user (will wait for CSS transitions, to + * complete). + */ final val Shown: Value = new EventType + /** This event is fired immediately when the hide instance method has been called. */ final val Hide: Value = new EventType - /** This event is fired when the dropdown has finished being hidden from the user (will wait for CSS transitions, to complete). */ + + /** This event is fired when the dropdown has finished being hidden from the user (will wait for CSS transitions, + * to complete). + */ final val Hidden: Value = new EventType + /** This event is fired on selection of any (except disabled ones) element from the dropdown. */ final val Selection: Value = new EventType } final case class VisibilityChangeEvent[ItemType, ElemType <: ReadableProperty[ItemType]]( override val source: UdashDropdown[ItemType, ElemType], - override val tpe: DropdownEvent.EventType + override val tpe: DropdownEvent.EventType, ) extends DropdownEvent[ItemType, ElemType] final case class SelectionEvent[ItemType, ElemType <: ReadableProperty[ItemType]]( - override val source: UdashDropdown[ItemType, ElemType], item: ItemType + override val source: UdashDropdown[ItemType, ElemType], + item: ItemType, ) extends DropdownEvent[ItemType, ElemType] { override def tpe: EventType = EventType.Selection } @@ -153,17 +198,24 @@ object UdashDropdown { case Link(title, url) => a(BootstrapStyles.Dropdown.item, href := url.value)(title).render case Button(title, callback) => - button(BootstrapStyles.Dropdown.item, onclick :+= ((_: Event) => { - callback() - }))(title).render + button( + BootstrapStyles.Dropdown.item, + onclick :+= + ((_: Event) => { + callback() + }), + )(title).render case Header(title) => h6(BootstrapStyles.Dropdown.header)(title).render case Disabled(item) => val res = defaultItemFactory(item, nested).styles(BootstrapStyles.disabled) - res.addEventListener("click", (ev: Event) => { - ev.preventDefault() - ev.stopPropagation() - }) + res.addEventListener( + "click", + (ev: Event) => { + ev.preventDefault() + ev.stopPropagation() + }, + ) res case Raw(element) => element case Dynamic(produce) => produce(nested) @@ -172,71 +224,106 @@ object UdashDropdown { } } - /** - * Creates a dropdown component. - * More: Bootstrap Docs. - * - * @param items Data items which will be represented as the elements in this dropdown. - * @param dropDirection A direction of the menu expansion. - * @param rightAlignMenu If true, the menu will be aligned to the right side of button. - * @param buttonToggle If true, the toggle arrow will be displayed. - * @param itemFactory Creates DOM element for each item which is inserted into the dropdown menu. - * Use the provided interceptor to properly clean up bindings inside the content. - * Usually you should add the `BootstrapStyles.Dropdown.item` style to your element. - * @param buttonContent Content of the element opening the dropdown. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param buttonFactory Allows to customize button options. - * @tparam ItemType A single element's type in the `items` sequence. - * @tparam ElemType A type of a property containing an element in the `items` sequence. - * @return A `UdashDropdown` component, call `render` to create a DOM element. - */ + /** Creates a dropdown component. More: Bootstrap + * Docs. + * + * @param items + * Data items which will be represented as the elements in this dropdown. + * @param dropDirection + * A direction of the menu expansion. + * @param rightAlignMenu + * If true, the menu will be aligned to the right side of button. + * @param buttonToggle + * If true, the toggle arrow will be displayed. + * @param itemFactory + * Creates DOM element for each item which is inserted into the dropdown menu. Use the provided interceptor to + * properly clean up bindings inside the content. Usually you should add the `BootstrapStyles.Dropdown.item` style + * to your element. + * @param buttonContent + * Content of the element opening the dropdown. Use the provided interceptor to properly clean up bindings inside + * the content. + * @param buttonFactory + * Allows to customize button options. + * @tparam ItemType + * A single element's type in the `items` sequence. + * @tparam ElemType + * A type of a property containing an element in the `items` sequence. + * @return + * A `UdashDropdown` component, call `render` to create a DOM element. + */ def apply[ItemType, ElemType <: ReadableProperty[ItemType]]( items: seq.ReadableSeqProperty[ItemType, ElemType], dropDirection: ReadableProperty[Direction] = Direction.Down.toProperty, rightAlignMenu: ReadableProperty[Boolean] = UdashBootstrap.False, buttonToggle: ReadableProperty[Boolean] = UdashBootstrap.True, - componentId: ComponentId = ComponentId.generate() + componentId: ComponentId = ComponentId.generate(), )( itemFactory: (ElemType, Binding.NestedInterceptor) => Element, buttonContent: Binding.NestedInterceptor => Modifier, - buttonFactory: (NestedInterceptor => Modifier) => UdashButton = UdashButton() + buttonFactory: (NestedInterceptor => Modifier) => UdashButton = UdashButton(), ): UdashDropdown[ItemType, ElemType] = { - val itemBindingFactory = (dropdown: UdashDropdown[ItemType, ElemType]) => repeatWithNested(items) { case (item, nested) => - withSelectionListener(itemFactory(item, nested), item.get, dropdown) - } - new UdashDropdown(items, dropDirection, rightAlignMenu, buttonToggle, componentId)(itemBindingFactory, buttonContent, buttonFactory) + val itemBindingFactory = (dropdown: UdashDropdown[ItemType, ElemType]) => + repeatWithNested(items) { case (item, nested) => + withSelectionListener(itemFactory(item, nested), item.get, dropdown) + } + new UdashDropdown(items, dropDirection, rightAlignMenu, buttonToggle, componentId)( + itemBindingFactory, + buttonContent, + buttonFactory, + ) } - /** - * Creates a dropdown component with [[DefaultDropdownItem]] as items. - * More: Bootstrap Docs. - * - * @param items Data items which will be represented as the elements in this dropdown. - * @param dropDirection A direction of the menu expansion. - * @param rightAlignMenu If true, the menu will be aligned to the right side of button. - * @param buttonToggle If true, the toggle arrow will be displayed. - * @param buttonContent Content of the element opening the dropdown. - * Use the provided interceptor to properly clean up bindings inside the content. - * @return A `UdashDropdown` component, call `render` to create a DOM element. - */ + /** Creates a dropdown component with [[DefaultDropdownItem]] as items. More: Bootstrap Docs. + * + * @param items + * Data items which will be represented as the elements in this dropdown. + * @param dropDirection + * A direction of the menu expansion. + * @param rightAlignMenu + * If true, the menu will be aligned to the right side of button. + * @param buttonToggle + * If true, the toggle arrow will be displayed. + * @param buttonContent + * Content of the element opening the dropdown. Use the provided interceptor to properly clean up bindings inside + * the content. + * @return + * A `UdashDropdown` component, call `render` to create a DOM element. + */ def default[ElemType <: ReadableProperty[DefaultDropdownItem]]( items: seq.ReadableSeqProperty[DefaultDropdownItem, ElemType], dropDirection: ReadableProperty[Direction] = Direction.Down.toProperty, rightAlignMenu: ReadableProperty[Boolean] = UdashBootstrap.False, buttonToggle: ReadableProperty[Boolean] = UdashBootstrap.True, - componentId: ComponentId = ComponentId.generate() + componentId: ComponentId = ComponentId.generate(), )( buttonContent: Binding.NestedInterceptor => Modifier ): UdashDropdown[DefaultDropdownItem, ElemType] = { val itemBindingFactory: UdashDropdown[DefaultDropdownItem, ElemType] => Binding = dropdown => - produceWithNested(items)((items, nested) => items.map(item => withSelectionListener[DefaultDropdownItem, ElemType](defaultItemFactory(item, nested), item, dropdown))) + produceWithNested(items)((items, nested) => + items.map(item => + withSelectionListener[DefaultDropdownItem, ElemType](defaultItemFactory(item, nested), item, dropdown) + ) + ) new UdashDropdown(items, dropDirection, rightAlignMenu, buttonToggle, componentId)( - itemBindingFactory, buttonContent, UdashButton() + itemBindingFactory, + buttonContent, + UdashButton(), ) } - private def withSelectionListener[ItemType, ElemType <: ReadableProperty[ItemType]](elem: Element, item: => ItemType, source: UdashDropdown[ItemType, ElemType]): Element = { - source.nestedInterceptor(new source.JQueryOnBinding(jQ(elem), EventName.click, (_: Element, _: JQueryEvent) => source.fire(DropdownEvent.SelectionEvent(source, item)))) + private def withSelectionListener[ItemType, ElemType <: ReadableProperty[ItemType]]( + elem: Element, + item: => ItemType, + source: UdashDropdown[ItemType, ElemType], + ): Element = { + source.nestedInterceptor( + new source.JQueryOnBinding( + jQ(elem), + EventName.click, + (_: Element, _: JQueryEvent) => source.fire(DropdownEvent.SelectionEvent(source, item)), + ) + ) elem } @@ -244,4 +331,4 @@ object UdashDropdown { private trait UdashDropdownJQuery extends JQuery { def dropdown(arg: String): UdashDropdownJQuery = js.native } -} \ No newline at end of file +} diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/form/UdashForm.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/form/UdashForm.scala index f10fe6c7b..06a8e3dae 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/form/UdashForm.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/form/UdashForm.scala @@ -20,13 +20,16 @@ import scalatags.JsDom.all._ import scala.concurrent.duration.{Duration, DurationLong} import scala.util.{Failure, Success} -final class UdashForm private( +final class UdashForm private ( formStyle: Option[CssStyle], inputValidationTrigger: ValidationTrigger, selectValidationTrigger: ValidationTrigger, - override val componentId: ComponentId -)(content: FormElementsFactory => Modifier) - extends UdashBootstrapComponent with Listenable with CrossLogging { + override val componentId: ComponentId, +)( + content: FormElementsFactory => Modifier +) extends UdashBootstrapComponent + with Listenable + with CrossLogging { import io.udash.css.CssView._ @@ -41,7 +44,7 @@ final class UdashForm private( override val render: Form = form(formStyle)( onsubmit :+= { (_: Event) => fire(new UdashForm.FormEvent(this, UdashForm.FormEvent.EventType.Submit)); true }, - content(new FormElementsFactory(nestedInterceptor, inputValidationTrigger, selectValidationTrigger, this)) + content(new FormElementsFactory(nestedInterceptor, inputValidationTrigger, selectValidationTrigger, this)), ).render } @@ -51,6 +54,7 @@ object UdashForm { object FormEvent { final class EventType(implicit enumCtx: EnumCtx) extends AbstractValueEnum object EventType extends AbstractValueEnumCompanion[EventType] { + /** Fired on the form submit. */ final val Submit: Value = new EventType } @@ -59,137 +63,150 @@ object UdashForm { /** Decides when the provided property will be validated in order to highlight the input. */ final class ValidationTrigger(implicit enumCtx: EnumCtx) extends AbstractValueEnum object ValidationTrigger extends AbstractValueEnumCompanion[ValidationTrigger] { + /** The validation is disabled. */ final val None: Value = new ValidationTrigger + /** The validation will be triggered on initialization and will be retriggered on each property value change. */ final val Instant: Value = new ValidationTrigger + /** The validation will be triggered on each value change. */ final val OnChange: Value = new ValidationTrigger + /** The validation will be triggered on `blur` event on the input. */ final val OnBlur: Value = new ValidationTrigger - /** The validation will be triggered on the form `submit` event. - * Notice that the validation won't block the form submit. */ + + /** The validation will be triggered on the form `submit` event. Notice that the validation won't block the form + * submit. + */ final val OnSubmit: Value = new ValidationTrigger } - /** Settings for the horizontal form layout. - * More: Bootstrap Docs. - * - * @param labelWidth Width of the label column. - * @param inputWidth Width of the input column. - * @param breakpoint Breakpoint of the form's grid. - * @param labelSize Size of the label text. - */ + /** Settings for the horizontal form layout. More: Bootstrap Docs. + * + * @param labelWidth + * Width of the label column. + * @param inputWidth + * Width of the input column. + * @param breakpoint + * Breakpoint of the form's grid. + * @param labelSize + * Size of the label text. + */ final case class HorizontalLayoutSettings( labelWidth: Int = 4, inputWidth: Int = 8, breakpoint: ResponsiveBreakpoint = ResponsiveBreakpoint.Medium, - labelSize: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None + labelSize: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None, ) extends AbstractCase - /** - * Creates a standard form with a provided content.
- * Example:
- *
-   * UdashForm() { factory => Seq(
-   *   factory.input.formGroup()(
-   * nested => factory.input.textInput(name, validationTrigger = ValidationTrigger.OnBlur)().render,
-   * labelContent = nested => Some(span("Name: ", nested(bind(name)))),
-   * validFeedback = _ => Some(span("Looks good.")),
-   * invalidFeedback = _ => Some(span("Name is too short."))
-   * ),
-   *   factory.input.formGroup()(
-   * nested => factory.input.passwordInput(name, validationTrigger = ValidationTrigger.Instant)().render,
-   * labelContent = nested => Some(span("Password: ", nested(bind(name)))),
-   * validFeedback = _ => Some(span("Looks good.")),
-   * invalidFeedback = _ => Some(span("Name is too short."))
-   * )
-   * )}.render
-   * 
- * More: Bootstrap Docs. - * - * @param inline If true, creates an inline form. - * @param inputValidationTrigger Default validation trigger for text inputs in this form. - * @param selectValidationTrigger Default validation trigger for selectors like checkboxes or select inputs. - * @param componentId An id of the root DOM node. - * @param content A factory of the form elements. All elements created with the factory will be cleaned up on the form cleanup. - * @return A `UdashForm` component, call `render` to create a DOM element. - */ + /** Creates a standard form with a provided content.
Example:
 UdashForm() { factory => Seq(
+    * factory.input.formGroup()( nested => factory.input.textInput(name, validationTrigger =
+    * ValidationTrigger.OnBlur)().render, labelContent = nested => Some(span("Name: ", nested(bind(name)))),
+    * validFeedback = _ => Some(span("Looks good.")), invalidFeedback = _ => Some(span("Name is too short.")) ),
+    * factory.input.formGroup()( nested => factory.input.passwordInput(name, validationTrigger =
+    * ValidationTrigger.Instant)().render, labelContent = nested => Some(span("Password: ", nested(bind(name)))),
+    * validFeedback = _ => Some(span("Looks good.")), invalidFeedback = _ => Some(span("Name is too short.")) )
+    * )}.render 
More: Bootstrap Docs. + * + * @param inline + * If true, creates an inline form. + * @param inputValidationTrigger + * Default validation trigger for text inputs in this form. + * @param selectValidationTrigger + * Default validation trigger for selectors like checkboxes or select inputs. + * @param componentId + * An id of the root DOM node. + * @param content + * A factory of the form elements. All elements created with the factory will be cleaned up on the form cleanup. + * @return + * A `UdashForm` component, call `render` to create a DOM element. + */ def apply( inline: Boolean = false, inputValidationTrigger: ValidationTrigger = ValidationTrigger.OnBlur, selectValidationTrigger: ValidationTrigger = ValidationTrigger.OnChange, - componentId: ComponentId = ComponentId.generate() - )(content: FormElementsFactory => Modifier): UdashForm = { + componentId: ComponentId = ComponentId.generate(), + )( + content: FormElementsFactory => Modifier + ): UdashForm = new UdashForm( Some(BootstrapStyles.Form.inline).filter(_ => inline), - inputValidationTrigger, selectValidationTrigger, componentId + inputValidationTrigger, + selectValidationTrigger, + componentId, )(content) - } } final class FormElementsFactory( factoryNestedInterceptor: Binding.NestedInterceptor, inputValidationTrigger: ValidationTrigger, selectValidationTrigger: ValidationTrigger, - form: OptArg[UdashForm] = OptArg.Empty + form: OptArg[UdashForm] = OptArg.Empty, ) extends CrossLogging { import io.udash.css.CssView._ - /** Use this method to bond the external binding's lifecycle with the lifecycle of the elements created via this factory. */ + /** Use this method to bond the external binding's lifecycle with the lifecycle of the elements created via this + * factory. + */ def externalBinding[T <: Binding](binding: T): T = { factoryNestedInterceptor(binding) binding } - /** - * Wrapper for disabled elements. - * - * @param disabled Property indicating if elements are disabled. You can change it anytime. - */ - def disabled(disabled: ReadableProperty[Boolean] = UdashBootstrap.True)( + /** Wrapper for disabled elements. + * + * @param disabled + * Property indicating if elements are disabled. You can change it anytime. + */ + def disabled( + disabled: ReadableProperty[Boolean] = UdashBootstrap.True + )( content: Binding.NestedInterceptor => Modifier - ): Modifier = { + ): Modifier = fieldset( factoryNestedInterceptor((scalatags.JsDom.attrs.disabled := "disabled").attrIf(disabled)) )(content(factoryNestedInterceptor)) - } /** Provides input elements factory methods. */ object input { - /** - * Creates a standard form group for the provided input. It contains a label and validation feedback elements. - * The group layout can be organized vertically or horizontally based on the provided options. - * You can wrap the result into grid elements in order to create more complex form layout. - * - * @param horizontal Optional settings for a horizontal layout. If `None`, the layout will be organized vertically. - * @param groupId Id of the root element. - * @param input The input element. It can be wrapped into `UdashInputGroup` or any other decoration. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param labelContent Optional label content. - * It will be wrapped into `label` element with properly set `for` attribute. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param validFeedback Optional content of positive validation feedback. - * It will be wrapped into `div` element with `valid-feedback` style. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param invalidFeedback Optional content of negative validation feedback. - * It will be wrapped into `div` element with `invalid-feedback` style. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param helpText Optional content of help text block. - * It will be wrapped into `div` element with `form-text text-muted` style. - * Use the provided interceptor to properly clean up bindings inside the content. - */ + + /** Creates a standard form group for the provided input. It contains a label and validation feedback elements. The + * group layout can be organized vertically or horizontally based on the provided options. You can wrap the result + * into grid elements in order to create more complex form layout. + * + * @param horizontal + * Optional settings for a horizontal layout. If `None`, the layout will be organized vertically. + * @param groupId + * Id of the root element. + * @param input + * The input element. It can be wrapped into `UdashInputGroup` or any other decoration. Use the provided + * interceptor to properly clean up bindings inside the content. + * @param labelContent + * Optional label content. It will be wrapped into `label` element with properly set `for` attribute. Use the + * provided interceptor to properly clean up bindings inside the content. + * @param validFeedback + * Optional content of positive validation feedback. It will be wrapped into `div` element with `valid-feedback` + * style. Use the provided interceptor to properly clean up bindings inside the content. + * @param invalidFeedback + * Optional content of negative validation feedback. It will be wrapped into `div` element with + * `invalid-feedback` style. Use the provided interceptor to properly clean up bindings inside the content. + * @param helpText + * Optional content of help text block. It will be wrapped into `div` element with `form-text text-muted` style. + * Use the provided interceptor to properly clean up bindings inside the content. + */ def formGroup( horizontal: Option[HorizontalLayoutSettings] = None, - groupId: ComponentId = ComponentId.generate() + groupId: ComponentId = ComponentId.generate(), )( input: Binding.NestedInterceptor => Element, labelContent: Option[Binding.NestedInterceptor => Modifier] = None, validFeedback: Option[Binding.NestedInterceptor => Modifier] = None, invalidFeedback: Option[Binding.NestedInterceptor => Modifier] = None, - helpText: Option[Binding.NestedInterceptor => Modifier] = None - ): UdashBootstrapComponent = { + helpText: Option[Binding.NestedInterceptor => Modifier] = None, + ): UdashBootstrapComponent = externalBinding(new UdashBootstrapComponent { override val render: Element = horizontal match { case None => @@ -199,7 +216,9 @@ final class FormElementsFactory( inputEl, validFeedback.map(content => div(BootstrapStyles.Form.validFeedback)(content(nestedInterceptor))), invalidFeedback.map(content => div(BootstrapStyles.Form.invalidFeedback)(content(nestedInterceptor))), - helpText.map(content => div(BootstrapStyles.Form.text, BootstrapStyles.Text.muted)(content(nestedInterceptor))) + helpText.map(content => + div(BootstrapStyles.Form.text, BootstrapStyles.Text.muted)(content(nestedInterceptor)) + ), ).render case Some(HorizontalLayoutSettings(labelWidth, inputWidth, breakpoint, labelSize)) => val inputEl = input(nestedInterceptor) @@ -214,231 +233,278 @@ final class FormElementsFactory( div(BootstrapStyles.Grid.col(inputWidth, breakpoint))( inputEl, validFeedback.map(content => div(BootstrapStyles.Form.validFeedback)(content(nestedInterceptor))), - invalidFeedback.map(content => div(BootstrapStyles.Form.invalidFeedback)(content(nestedInterceptor))) - ) + invalidFeedback.map(content => div(BootstrapStyles.Form.invalidFeedback)(content(nestedInterceptor))), + ), ).render } override val componentId: ComponentId = groupId }) - } - /** - * Creates a text input with a default bootstrap styling and an optional validation callback which sets - * proper bootstrap classes: `is-valid` and `is-invalid`. - * Use `formGroup` if you want to create an input with a label and validation feedback elements. - * - * @param property Property which will be synchronised with the input content and validated. - * @param debounce Property update timeout after input changes. - * @param size Size of the input. - * @param validationTrigger Selects the event updating validation state of the input. - * @param inputId Id of the input DOM element. - * @param inputModifier Modifiers applied directly to the `input` element. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param validator Validator for provided text property - */ + /** Creates a text input with a default bootstrap styling and an optional validation callback which sets proper + * bootstrap classes: `is-valid` and `is-invalid`. Use `formGroup` if you want to create an input with a label and + * validation feedback elements. + * + * @param property + * Property which will be synchronised with the input content and validated. + * @param debounce + * Property update timeout after input changes. + * @param size + * Size of the input. + * @param validationTrigger + * Selects the event updating validation state of the input. + * @param inputId + * Id of the input DOM element. + * @param inputModifier + * Modifiers applied directly to the `input` element. Use the provided interceptor to properly clean up bindings + * inside the content. + * @param validator + * Validator for provided text property + */ def textInput( property: Property[String], - debounce: Duration = 20 millis, + debounce: Duration = 20.millis, size: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None, validationTrigger: ValidationTrigger = inputValidationTrigger, - inputId: ComponentId = ComponentId.generate() + inputId: ComponentId = ComponentId.generate(), )( inputModifier: Option[Binding.NestedInterceptor => Modifier] = None, - validator: Validator[String] = Validator.Default - ): UdashBootstrapComponent = { - externalBinding(new InputComponent( - TextInput(property, debounce)( + validator: Validator[String] = Validator.Default, + ): UdashBootstrapComponent = + externalBinding( + new InputComponent( + TextInput(property, debounce)( + inputId, + BootstrapStyles.Form.control, + inputModifier.map(_.apply(factoryNestedInterceptor)), + validationModifier(property, validationTrigger, factoryNestedInterceptor)(validator), + (BootstrapStyles.Form.size _).reactiveOptionApply(size), + ), inputId, - BootstrapStyles.Form.control, - inputModifier.map(_.apply(factoryNestedInterceptor)), - validationModifier(property, validationTrigger, factoryNestedInterceptor)(validator), - (BootstrapStyles.Form.size _).reactiveOptionApply(size) - ), inputId - )) - } + ) + ) - /** - * Creates a password input with a default bootstrap styling and an optional validation callback which sets - * proper bootstrap classes: `is-valid` and `is-invalid`. - * Use `formGroup` if you want to create an input with a label and validation feedback elements. - * - * @param property Property which will be synchronised with the input content and validated. - * @param debounce Property update timeout after input changes. - * @param size Size of the input. - * @param validationTrigger Selects the event updating validation state of the input. - * @param inputId Id of the input DOM element. - * @param inputModifier Modifiers applied directly to the `input` element. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param validator Validator for provided password property - */ + /** Creates a password input with a default bootstrap styling and an optional validation callback which sets proper + * bootstrap classes: `is-valid` and `is-invalid`. Use `formGroup` if you want to create an input with a label and + * validation feedback elements. + * + * @param property + * Property which will be synchronised with the input content and validated. + * @param debounce + * Property update timeout after input changes. + * @param size + * Size of the input. + * @param validationTrigger + * Selects the event updating validation state of the input. + * @param inputId + * Id of the input DOM element. + * @param inputModifier + * Modifiers applied directly to the `input` element. Use the provided interceptor to properly clean up bindings + * inside the content. + * @param validator + * Validator for provided password property + */ def passwordInput( property: Property[String], - debounce: Duration = 20 millis, + debounce: Duration = 20.millis, size: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None, validationTrigger: ValidationTrigger = inputValidationTrigger, - inputId: ComponentId = ComponentId.generate() + inputId: ComponentId = ComponentId.generate(), )( inputModifier: Option[Binding.NestedInterceptor => Modifier] = None, - validator: Validator[String] = Validator.Default - ): UdashBootstrapComponent = { - externalBinding(new InputComponent( - PasswordInput(property, debounce)( + validator: Validator[String] = Validator.Default, + ): UdashBootstrapComponent = + externalBinding( + new InputComponent( + PasswordInput(property, debounce)( + inputId, + BootstrapStyles.Form.control, + inputModifier.map(_.apply(factoryNestedInterceptor)), + validationModifier(property, validationTrigger, factoryNestedInterceptor)(validator), + (BootstrapStyles.Form.size _).reactiveOptionApply(size), + ), inputId, - BootstrapStyles.Form.control, - inputModifier.map(_.apply(factoryNestedInterceptor)), - validationModifier(property, validationTrigger, factoryNestedInterceptor)(validator), - (BootstrapStyles.Form.size _).reactiveOptionApply(size) - ), inputId - )) - } + ) + ) - /** - * Creates a number input with a default bootstrap styling and an optional validation callback which sets - * proper bootstrap classes: `is-valid` and `is-invalid`. - * Use `formGroup` if you want to create an input with a label and validation feedback elements. - * - * @param property Property which will be synchronised with the input content and validated. - * @param debounce Property update timeout after input changes. - * @param size Size of the input. - * @param validationTrigger Selects the event updating validation state of the input. - * @param inputId Id of the input DOM element. - * @param inputModifier Modifiers applied directly to the `input` element. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param validator Validator for provided number property - */ + /** Creates a number input with a default bootstrap styling and an optional validation callback which sets proper + * bootstrap classes: `is-valid` and `is-invalid`. Use `formGroup` if you want to create an input with a label and + * validation feedback elements. + * + * @param property + * Property which will be synchronised with the input content and validated. + * @param debounce + * Property update timeout after input changes. + * @param size + * Size of the input. + * @param validationTrigger + * Selects the event updating validation state of the input. + * @param inputId + * Id of the input DOM element. + * @param inputModifier + * Modifiers applied directly to the `input` element. Use the provided interceptor to properly clean up bindings + * inside the content. + * @param validator + * Validator for provided number property + */ def numberInput( property: Property[Double], - debounce: Duration = 20 millis, + debounce: Duration = 20.millis, size: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None, validationTrigger: ValidationTrigger = inputValidationTrigger, - inputId: ComponentId = ComponentId.generate() + inputId: ComponentId = ComponentId.generate(), )( inputModifier: Option[Binding.NestedInterceptor => Modifier] = None, - validator: Validator[Double] = Validator.Default - ): UdashBootstrapComponent = { - externalBinding(new InputComponent( - NumberInput(property.bitransform(_.toString)(_.toDouble), debounce)( + validator: Validator[Double] = Validator.Default, + ): UdashBootstrapComponent = + externalBinding( + new InputComponent( + NumberInput(property.bitransform(_.toString)(_.toDouble), debounce)( + inputId, + BootstrapStyles.Form.control, + inputModifier.map(_.apply(factoryNestedInterceptor)), + validationModifier(property, validationTrigger, factoryNestedInterceptor)(validator), + (BootstrapStyles.Form.size _).reactiveOptionApply(size), + ), inputId, - BootstrapStyles.Form.control, - inputModifier.map(_.apply(factoryNestedInterceptor)), - validationModifier(property, validationTrigger, factoryNestedInterceptor)(validator), - (BootstrapStyles.Form.size _).reactiveOptionApply(size) - ), inputId - )) - } + ) + ) - /** - * Creates a range input with a custom bootstrap styling and an optional validation callback which sets - * proper bootstrap classes: `is-valid` and `is-invalid`. - * Use `formGroup` if you want to create an input with a label and validation feedback elements. - * - * @param value Current value synchronised with the input. - * The value should be between `minValue` and `maxValue`. It should be also divisible by `valueStep`. - * @param minValue The minimum value for this input, which must not be greater than its maximum (`maxValue` attribute) value. - * @param maxValue The maximum value for the input. Must not be less than its minimum (`minValue` attribute) value. - * @param valueStep Limit the increments at which a numeric value can be set. - * @param validationTrigger Selects the event updating validation state of the input. - * @param inputId Id of the input DOM element. - * @param inputModifier Modifiers applied directly to the `input` element. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param validator Validator for provided value property - */ + /** Creates a range input with a custom bootstrap styling and an optional validation callback which sets proper + * bootstrap classes: `is-valid` and `is-invalid`. Use `formGroup` if you want to create an input with a label and + * validation feedback elements. + * + * @param value + * Current value synchronised with the input. The value should be between `minValue` and `maxValue`. It should be + * also divisible by `valueStep`. + * @param minValue + * The minimum value for this input, which must not be greater than its maximum (`maxValue` attribute) value. + * @param maxValue + * The maximum value for the input. Must not be less than its minimum (`minValue` attribute) value. + * @param valueStep + * Limit the increments at which a numeric value can be set. + * @param validationTrigger + * Selects the event updating validation state of the input. + * @param inputId + * Id of the input DOM element. + * @param inputModifier + * Modifiers applied directly to the `input` element. Use the provided interceptor to properly clean up bindings + * inside the content. + * @param validator + * Validator for provided value property + */ def rangeInput( value: Property[Double], minValue: ReadableProperty[Double] = 0d.toProperty, maxValue: ReadableProperty[Double] = 100d.toProperty, valueStep: ReadableProperty[Double] = 1d.toProperty, validationTrigger: ValidationTrigger = selectValidationTrigger, - inputId: ComponentId = ComponentId.generate() + inputId: ComponentId = ComponentId.generate(), )( inputModifier: Option[Binding.NestedInterceptor => Modifier] = None, - validator: Validator[Double] = Validator.Default - ): UdashBootstrapComponent = { - externalBinding(new InputComponent( - RangeInput(value, minValue, maxValue, valueStep)( + validator: Validator[Double] = Validator.Default, + ): UdashBootstrapComponent = + externalBinding( + new InputComponent( + RangeInput(value, minValue, maxValue, valueStep)( + inputId, + BootstrapStyles.Form.controlRange, + BootstrapStyles.Form.customRange, + inputModifier.map(_.apply(factoryNestedInterceptor)), + validationModifier(value, validationTrigger, factoryNestedInterceptor)(validator), + ), inputId, - BootstrapStyles.Form.controlRange, - BootstrapStyles.Form.customRange, - inputModifier.map(_.apply(factoryNestedInterceptor)), - validationModifier(value, validationTrigger, factoryNestedInterceptor)(validator) - ), inputId - )) - } + ) + ) - /** - * Creates a text area with a default bootstrap styling and an optional validation callback which sets - * proper bootstrap classes: `is-valid` and `is-invalid`. - * Use `formGroup` if you want to create an input with a label and validation feedback elements. - * - * @param property Property which will be synchronised with the input content and validated. - * @param debounce Property update timeout after input changes. - * @param size Size of the input. - * @param validationTrigger Selects the event updating validation state of the input. - * @param inputId Id of the input DOM element. - * @param inputModifier Modifiers applied directly to the `input` element. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param validator Validator for provided text property - */ + /** Creates a text area with a default bootstrap styling and an optional validation callback which sets proper + * bootstrap classes: `is-valid` and `is-invalid`. Use `formGroup` if you want to create an input with a label and + * validation feedback elements. + * + * @param property + * Property which will be synchronised with the input content and validated. + * @param debounce + * Property update timeout after input changes. + * @param size + * Size of the input. + * @param validationTrigger + * Selects the event updating validation state of the input. + * @param inputId + * Id of the input DOM element. + * @param inputModifier + * Modifiers applied directly to the `input` element. Use the provided interceptor to properly clean up bindings + * inside the content. + * @param validator + * Validator for provided text property + */ def textArea( property: Property[String], - debounce: Duration = 20 millis, + debounce: Duration = 20.millis, size: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None, validationTrigger: ValidationTrigger = inputValidationTrigger, - inputId: ComponentId = ComponentId.generate() + inputId: ComponentId = ComponentId.generate(), )( inputModifier: Option[Binding.NestedInterceptor => Modifier] = None, - validator: Validator[String] = Validator.Default - ): UdashBootstrapComponent = { - externalBinding(new InputComponent( - TextArea(property, debounce)( + validator: Validator[String] = Validator.Default, + ): UdashBootstrapComponent = + externalBinding( + new InputComponent( + TextArea(property, debounce)( + inputId, + BootstrapStyles.Form.control, + inputModifier.map(_.apply(factoryNestedInterceptor)), + validationModifier(property, validationTrigger, factoryNestedInterceptor)(validator), + (BootstrapStyles.Form.size _).reactiveOptionApply(size), + ), inputId, - BootstrapStyles.Form.control, - inputModifier.map(_.apply(factoryNestedInterceptor)), - validationModifier(property, validationTrigger, factoryNestedInterceptor)(validator), - (BootstrapStyles.Form.size _).reactiveOptionApply(size) - ), inputId - )) - } + ) + ) - /** - * Creates a file input with a default bootstrap styling and an optional validation callback which sets - * proper bootstrap classes: `is-valid` and `is-invalid`. - * It creates the whole input compoennt including a label and validation feedback. - * - * @param selectedFiles This property contains information about files selected by a user. - * @param acceptMultipleFiles Accepts more than one file if true. - * @param size Size of the input. - * @param validationTrigger Selects the event updating validation state of the input. - * @param inputId Id of the input DOM element. - * @param inputName Input element name. - * @param inputModifier Modifiers applied directly to the `input` element. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param labelContent Required label content. - * It will be wrapped into `label` element with properly set `for` attribute. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param validFeedback Optional content of positive validation feedback. - * It will be wrapped into `div` element with `valid-feedback` style. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param invalidFeedback Optional content of negative validation feedback. - * It will be wrapped into `div` element with `invalid-feedback` style. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param validator Validator for provided files property - */ + /** Creates a file input with a default bootstrap styling and an optional validation callback which sets proper + * bootstrap classes: `is-valid` and `is-invalid`. It creates the whole input compoennt including a label and + * validation feedback. + * + * @param selectedFiles + * This property contains information about files selected by a user. + * @param acceptMultipleFiles + * Accepts more than one file if true. + * @param size + * Size of the input. + * @param validationTrigger + * Selects the event updating validation state of the input. + * @param inputId + * Id of the input DOM element. + * @param inputName + * Input element name. + * @param inputModifier + * Modifiers applied directly to the `input` element. Use the provided interceptor to properly clean up bindings + * inside the content. + * @param labelContent + * Required label content. It will be wrapped into `label` element with properly set `for` attribute. Use the + * provided interceptor to properly clean up bindings inside the content. + * @param validFeedback + * Optional content of positive validation feedback. It will be wrapped into `div` element with `valid-feedback` + * style. Use the provided interceptor to properly clean up bindings inside the content. + * @param invalidFeedback + * Optional content of negative validation feedback. It will be wrapped into `div` element with + * `invalid-feedback` style. Use the provided interceptor to properly clean up bindings inside the content. + * @param validator + * Validator for provided files property + */ def fileInput( selectedFiles: SeqProperty[File], acceptMultipleFiles: ReadableProperty[Boolean] = UdashBootstrap.False, size: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None, validationTrigger: ValidationTrigger = inputValidationTrigger, - inputId: ComponentId = ComponentId.generate() + inputId: ComponentId = ComponentId.generate(), )( inputName: String, inputModifier: Option[Binding.NestedInterceptor => Modifier] = None, labelContent: Binding.NestedInterceptor => Modifier = _ => "", validFeedback: Option[Binding.NestedInterceptor => Modifier] = None, invalidFeedback: Option[Binding.NestedInterceptor => Modifier] = None, - validator: Validator[BSeq[File]] = Validator.Default - ): UdashBootstrapComponent = { + validator: Validator[BSeq[File]] = Validator.Default, + ): UdashBootstrapComponent = externalBinding(new UdashBootstrapComponent { private val input = FileInput(selectedFiles, acceptMultipleFiles)( inputName, @@ -446,7 +512,7 @@ final class FormElementsFactory( BootstrapStyles.Form.customFileInput, inputModifier.map(_.apply(nestedInterceptor)), validationModifier(selectedFiles, validationTrigger, nestedInterceptor)(validator), - (BootstrapStyles.Form.size _).reactiveOptionApply(size) + (BootstrapStyles.Form.size _).reactiveOptionApply(size), ) override val componentId: ComponentId = inputId @@ -455,234 +521,309 @@ final class FormElementsFactory( input.render, label(`for` := inputId, BootstrapStyles.Form.customFileLabel)(labelContent(nestedInterceptor)), validFeedback.map(content => div(BootstrapStyles.Form.validFeedback)(content(nestedInterceptor))), - invalidFeedback.map(content => div(BootstrapStyles.Form.invalidFeedback)(content(nestedInterceptor))) + invalidFeedback.map(content => div(BootstrapStyles.Form.invalidFeedback)(content(nestedInterceptor))), ).render }) - } - /** - * Creates a select menu with a default bootstrap styling and an optional validation callback which sets - * proper bootstrap classes: `is-valid` and `is-invalid`. - * Use `formGroup` if you want to create an input with a label and validation feedback elements. - * - * @param selectedItem Property containing selected element. - * @param options SeqProperty of available options. - * @param size Size of the input. - * @param validationTrigger Selects the event updating validation state of the input. - * @param inputId Id of the input DOM element. - * @param itemLabel Provides options' labels. - * @param inputModifier Modifiers applied directly to the `input` element. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param validator Validator for provided item property - */ + /** Creates a select menu with a default bootstrap styling and an optional validation callback which sets proper + * bootstrap classes: `is-valid` and `is-invalid`. Use `formGroup` if you want to create an input with a label and + * validation feedback elements. + * + * @param selectedItem + * Property containing selected element. + * @param options + * SeqProperty of available options. + * @param size + * Size of the input. + * @param validationTrigger + * Selects the event updating validation state of the input. + * @param inputId + * Id of the input DOM element. + * @param itemLabel + * Provides options' labels. + * @param inputModifier + * Modifiers applied directly to the `input` element. Use the provided interceptor to properly clean up bindings + * inside the content. + * @param validator + * Validator for provided item property + */ def select[T]( selectedItem: Property[T], options: ReadableSeqProperty[T], size: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None, validationTrigger: ValidationTrigger = selectValidationTrigger, - inputId: ComponentId = ComponentId.generate() + inputId: ComponentId = ComponentId.generate(), )( itemLabel: T => Modifier, inputModifier: Option[Binding.NestedInterceptor => Modifier] = None, - validator: Validator[T] = Validator.Default - ): UdashBootstrapComponent = { - externalBinding(new InputComponent( - Select(selectedItem, options)( - itemLabel, + validator: Validator[T] = Validator.Default, + ): UdashBootstrapComponent = + externalBinding( + new InputComponent( + Select(selectedItem, options)( + itemLabel, + inputId, + BootstrapStyles.Form.customSelect, + inputModifier.map(_.apply(factoryNestedInterceptor)), + validationModifier(selectedItem, validationTrigger, factoryNestedInterceptor)(validator), + factoryNestedInterceptor((BootstrapStyles.Form.size _).reactiveOptionApply(size)), + ), inputId, - BootstrapStyles.Form.customSelect, - inputModifier.map(_.apply(factoryNestedInterceptor)), - validationModifier(selectedItem, validationTrigger, factoryNestedInterceptor)(validator), - factoryNestedInterceptor((BootstrapStyles.Form.size _).reactiveOptionApply(size)) - ), inputId - )) - } + ) + ) - /** - * Creates a multiple select menu with a default bootstrap styling and an optional validation callback which sets - * proper bootstrap classes: `is-valid` and `is-invalid`. - * Use `formGroup` if you want to create an input with a label and validation feedback elements. - * - * @param selectedItems Property containing selected elements. - * @param options SeqProperty of available options. - * @param size Size of the input. - * @param validationTrigger Selects the event updating validation state of the input. - * @param inputId Id of the input DOM element. - * @param itemLabel Provides options' labels. - * @param inputModifier Modifiers applied directly to the `input` element. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param validator Validator for provided items property - */ + /** Creates a multiple select menu with a default bootstrap styling and an optional validation callback which sets + * proper bootstrap classes: `is-valid` and `is-invalid`. Use `formGroup` if you want to create an input with a + * label and validation feedback elements. + * + * @param selectedItems + * Property containing selected elements. + * @param options + * SeqProperty of available options. + * @param size + * Size of the input. + * @param validationTrigger + * Selects the event updating validation state of the input. + * @param inputId + * Id of the input DOM element. + * @param itemLabel + * Provides options' labels. + * @param inputModifier + * Modifiers applied directly to the `input` element. Use the provided interceptor to properly clean up bindings + * inside the content. + * @param validator + * Validator for provided items property + */ def multiSelect[T, ElemType <: Property[T]]( selectedItems: seq.SeqProperty[T, ElemType], options: ReadableSeqProperty[T], size: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None, validationTrigger: ValidationTrigger = selectValidationTrigger, - inputId: ComponentId = ComponentId.generate() + inputId: ComponentId = ComponentId.generate(), )( itemLabel: T => Modifier, inputModifier: Option[Binding.NestedInterceptor => Modifier] = None, - validator: Validator[BSeq[T]] = Validator.Default - ): UdashBootstrapComponent = { - externalBinding(new InputComponent( - Select(selectedItems, options)( - itemLabel, + validator: Validator[BSeq[T]] = Validator.Default, + ): UdashBootstrapComponent = + externalBinding( + new InputComponent( + Select(selectedItems, options)( + itemLabel, + inputId, + BootstrapStyles.Form.customSelect, + inputModifier.map(_.apply(factoryNestedInterceptor)), + validationModifier(selectedItems, validationTrigger, factoryNestedInterceptor)(validator), + factoryNestedInterceptor((BootstrapStyles.Form.size _).reactiveOptionApply(size)), + ), inputId, - BootstrapStyles.Form.customSelect, - inputModifier.map(_.apply(factoryNestedInterceptor)), - validationModifier(selectedItems, validationTrigger, factoryNestedInterceptor)(validator), - factoryNestedInterceptor((BootstrapStyles.Form.size _).reactiveOptionApply(size)) - ), inputId - )) - } + ) + ) - /** - * Creates a checkbox with a custom bootstrap styling and an optional validation callback which sets - * proper bootstrap classes: `is-valid` and `is-invalid`. - * It also creates a label and validation feedback elements for the checkbox. - * - * @param property Property which will be synchronised with the checkbox state and validated. - * @param validationTrigger Selects the event updating validation state of the input. - * @param inputId Id of the input DOM element. - * @param groupId Id of the root element. - * @param inputModifier Modifiers applied directly to the `input` element. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param labelContent Optional label content. - * It will be wrapped into `label` element with properly set `for` attribute. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param validFeedback Optional content of positive validation feedback. - * It will be wrapped into `div` element with `valid-feedback` style. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param invalidFeedback Optional content of negative validation feedback. - * It will be wrapped into `div` element with `invalid-feedback` style. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param validator Validator for provided checkbox state property - */ + /** Creates a checkbox with a custom bootstrap styling and an optional validation callback which sets proper + * bootstrap classes: `is-valid` and `is-invalid`. It also creates a label and validation feedback elements for the + * checkbox. + * + * @param property + * Property which will be synchronised with the checkbox state and validated. + * @param validationTrigger + * Selects the event updating validation state of the input. + * @param inputId + * Id of the input DOM element. + * @param groupId + * Id of the root element. + * @param inputModifier + * Modifiers applied directly to the `input` element. Use the provided interceptor to properly clean up bindings + * inside the content. + * @param labelContent + * Optional label content. It will be wrapped into `label` element with properly set `for` attribute. Use the + * provided interceptor to properly clean up bindings inside the content. + * @param validFeedback + * Optional content of positive validation feedback. It will be wrapped into `div` element with `valid-feedback` + * style. Use the provided interceptor to properly clean up bindings inside the content. + * @param invalidFeedback + * Optional content of negative validation feedback. It will be wrapped into `div` element with + * `invalid-feedback` style. Use the provided interceptor to properly clean up bindings inside the content. + * @param validator + * Validator for provided checkbox state property + */ def checkbox( property: Property[Boolean], validationTrigger: ValidationTrigger = selectValidationTrigger, inline: ReadableProperty[Boolean] = UdashBootstrap.False, inputId: ComponentId = ComponentId.generate(), - groupId: ComponentId = ComponentId.generate() + groupId: ComponentId = ComponentId.generate(), )( inputModifier: Option[Binding.NestedInterceptor => Modifier] = None, labelContent: Option[Binding.NestedInterceptor => Modifier] = None, validFeedback: Option[Binding.NestedInterceptor => Modifier] = None, invalidFeedback: Option[Binding.NestedInterceptor => Modifier] = None, - validator: Validator[Boolean] = Validator.Default - ): UdashBootstrapComponent = { + validator: Validator[Boolean] = Validator.Default, + ): UdashBootstrapComponent = externalBinding(new UdashBootstrapComponent { - private val input = nestedInterceptor(Checkbox(property)( - inputId, - BootstrapStyles.Form.control, - inputModifier.map(_.apply(nestedInterceptor)), - validationModifier(property, validationTrigger, nestedInterceptor)(validator) - )) + private val input = nestedInterceptor( + Checkbox(property)( + inputId, + BootstrapStyles.Form.control, + inputModifier.map(_.apply(nestedInterceptor)), + validationModifier(property, validationTrigger, nestedInterceptor)(validator), + ) + ) override val componentId: ComponentId = groupId override val render: Element = div( - groupId, BootstrapStyles.Form.customControl, BootstrapStyles.Form.customCheckbox, + groupId, + BootstrapStyles.Form.customControl, + BootstrapStyles.Form.customCheckbox, input.render.styles(BootstrapStyles.Form.customControlInput), nestedInterceptor(BootstrapStyles.Form.customControlInline.styleIf(inline)), label(`for` := inputId, BootstrapStyles.Form.customControlLabel)( labelContent.map(_.apply(nestedInterceptor)).getOrElse(span("\u00a0")) ), validFeedback.map(content => div(BootstrapStyles.Form.validFeedback)(content(nestedInterceptor))), - invalidFeedback.map(content => div(BootstrapStyles.Form.invalidFeedback)(content(nestedInterceptor))) + invalidFeedback.map(content => div(BootstrapStyles.Form.invalidFeedback)(content(nestedInterceptor))), ).render }) - } - /** - * Creates a checkboxes with a custom bootstrap styling and an optional validation callback which sets - * proper bootstrap classes: `is-valid` and `is-invalid`. - * It also creates a label and validation feedback elements for each checkbox. - * - * @param selectedItems Property which will be synchronised with the checkbox state and validated. - * @param options Seq of available options, one checkbox will be created for each option. - * @param validationTrigger Selects the event updating validation state of the input. - * @param groupId Id of the root element. - * @param inputModifier Modifiers applied directly to the `input` element. - * The factory takes item value and index as the arguments. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param labelContent Optional label content. - * It will be wrapped into `label` element with properly set `for` attribute. - * The factory takes item value and index as the arguments. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param validFeedback Optional content of positive validation feedback. - * It will be wrapped into `div` element with `valid-feedback` style. - * The factory takes item value and index as the arguments. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param invalidFeedback Optional content of negative validation feedback. - * It will be wrapped into `div` element with `invalid-feedback` style. - * The factory takes item value and index as the arguments. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param validator Validator for provided checkbox states property - */ + /** Creates a checkboxes with a custom bootstrap styling and an optional validation callback which sets proper + * bootstrap classes: `is-valid` and `is-invalid`. It also creates a label and validation feedback elements for + * each checkbox. + * + * @param selectedItems + * Property which will be synchronised with the checkbox state and validated. + * @param options + * Seq of available options, one checkbox will be created for each option. + * @param validationTrigger + * Selects the event updating validation state of the input. + * @param groupId + * Id of the root element. + * @param inputModifier + * Modifiers applied directly to the `input` element. The factory takes item value and index as the arguments. + * Use the provided interceptor to properly clean up bindings inside the content. + * @param labelContent + * Optional label content. It will be wrapped into `label` element with properly set `for` attribute. The factory + * takes item value and index as the arguments. Use the provided interceptor to properly clean up bindings inside + * the content. + * @param validFeedback + * Optional content of positive validation feedback. It will be wrapped into `div` element with `valid-feedback` + * style. The factory takes item value and index as the arguments. Use the provided interceptor to properly clean + * up bindings inside the content. + * @param invalidFeedback + * Optional content of negative validation feedback. It will be wrapped into `div` element with + * `invalid-feedback` style. The factory takes item value and index as the arguments. Use the provided + * interceptor to properly clean up bindings inside the content. + * @param validator + * Validator for provided checkbox states property + */ def checkButtons[T]( selectedItems: seq.SeqProperty[T, _ <: ReadableProperty[T]], options: ReadableSeqProperty[T], inline: ReadableProperty[Boolean] = UdashBootstrap.False, validationTrigger: ValidationTrigger = selectValidationTrigger, - groupId: ComponentId = ComponentId.generate() + groupId: ComponentId = ComponentId.generate(), )( - inputModifier: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = (_: T, _: Int, _: Binding.NestedInterceptor) => None, - labelContent: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = (_: T, _: Int, _: Binding.NestedInterceptor) => None, - validFeedback: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = (_: T, _: Int, _: Binding.NestedInterceptor) => None, - invalidFeedback: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = (_: T, _: Int, _: Binding.NestedInterceptor) => None, - validator: Validator[BSeq[T]] = Validator.Default - ): UdashBootstrapComponent = { - externalBinding(new ButtonsComponent( - selectedItems, CheckButtons(selectedItems, options)(_: Seq[(JSInput, T)] => Seq[Node]), - BootstrapStyles.Form.customCheckbox, inline, validationTrigger, groupId - )(inputModifier, labelContent, validFeedback, invalidFeedback, validator)) - } + inputModifier: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = ( + _: T, + _: Int, + _: Binding.NestedInterceptor, + ) => None, + labelContent: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = ( + _: T, + _: Int, + _: Binding.NestedInterceptor, + ) => None, + validFeedback: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = ( + _: T, + _: Int, + _: Binding.NestedInterceptor, + ) => None, + invalidFeedback: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = ( + _: T, + _: Int, + _: Binding.NestedInterceptor, + ) => None, + validator: Validator[BSeq[T]] = Validator.Default, + ): UdashBootstrapComponent = + externalBinding( + new ButtonsComponent( + selectedItems, + CheckButtons(selectedItems, options)(_: Seq[(JSInput, T)] => Seq[Node]), + BootstrapStyles.Form.customCheckbox, + inline, + validationTrigger, + groupId, + )(inputModifier, labelContent, validFeedback, invalidFeedback, validator) + ) - /** - * Creates radio buttons with a custom bootstrap styling and an optional validation callback which sets - * proper bootstrap classes: `is-valid` and `is-invalid`. - * It also creates a label and validation feedback elements for each radio button. - * - * @param selectedItem Property which will be synchronised with the radio state and validated. - * @param options Seq of available options, one button will be created for each option. - * @param validationTrigger Selects the event updating validation state of the input. - * @param groupId Id of the root element. - * @param inputModifier Modifiers applied directly to the `input` element. - * The factory takes item value and index as the arguments. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param labelContent Optional label content. - * It will be wrapped into `label` element with properly set `for` attribute. - * The factory takes item value and index as the arguments. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param validFeedback Optional content of positive validation feedback. - * It will be wrapped into `div` element with `valid-feedback` style. - * The factory takes item value and index as the arguments. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param invalidFeedback Optional content of negative validation feedback. - * It will be wrapped into `div` element with `invalid-feedback` style. - * The factory takes item value and index as the arguments. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param validator Validator for provided item selection property - */ + /** Creates radio buttons with a custom bootstrap styling and an optional validation callback which sets proper + * bootstrap classes: `is-valid` and `is-invalid`. It also creates a label and validation feedback elements for + * each radio button. + * + * @param selectedItem + * Property which will be synchronised with the radio state and validated. + * @param options + * Seq of available options, one button will be created for each option. + * @param validationTrigger + * Selects the event updating validation state of the input. + * @param groupId + * Id of the root element. + * @param inputModifier + * Modifiers applied directly to the `input` element. The factory takes item value and index as the arguments. + * Use the provided interceptor to properly clean up bindings inside the content. + * @param labelContent + * Optional label content. It will be wrapped into `label` element with properly set `for` attribute. The factory + * takes item value and index as the arguments. Use the provided interceptor to properly clean up bindings inside + * the content. + * @param validFeedback + * Optional content of positive validation feedback. It will be wrapped into `div` element with `valid-feedback` + * style. The factory takes item value and index as the arguments. Use the provided interceptor to properly clean + * up bindings inside the content. + * @param invalidFeedback + * Optional content of negative validation feedback. It will be wrapped into `div` element with + * `invalid-feedback` style. The factory takes item value and index as the arguments. Use the provided + * interceptor to properly clean up bindings inside the content. + * @param validator + * Validator for provided item selection property + */ def radioButtons[T]( selectedItem: Property[T], options: ReadableSeqProperty[T], inline: ReadableProperty[Boolean] = UdashBootstrap.False, validationTrigger: ValidationTrigger = selectValidationTrigger, - groupId: ComponentId = ComponentId.generate() + groupId: ComponentId = ComponentId.generate(), )( - inputModifier: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = (_: T, _: Int, _: Binding.NestedInterceptor) => None, - labelContent: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = (_: T, _: Int, _: Binding.NestedInterceptor) => None, - validFeedback: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = (_: T, _: Int, _: Binding.NestedInterceptor) => None, - invalidFeedback: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = (_: T, _: Int, _: Binding.NestedInterceptor) => None, - validator: Validator[T] = Validator.Default - ): UdashBootstrapComponent = { - externalBinding(new ButtonsComponent( - selectedItem, RadioButtons(selectedItem, options)(_: Seq[(JSInput, T)] => Seq[Node]), - BootstrapStyles.Form.customRadio, inline, validationTrigger, groupId - )(inputModifier, labelContent, validFeedback, invalidFeedback, validator)) - } + inputModifier: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = ( + _: T, + _: Int, + _: Binding.NestedInterceptor, + ) => None, + labelContent: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = ( + _: T, + _: Int, + _: Binding.NestedInterceptor, + ) => None, + validFeedback: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = ( + _: T, + _: Int, + _: Binding.NestedInterceptor, + ) => None, + invalidFeedback: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = ( + _: T, + _: Int, + _: Binding.NestedInterceptor, + ) => None, + validator: Validator[T] = Validator.Default, + ): UdashBootstrapComponent = + externalBinding( + new ButtonsComponent( + selectedItem, + RadioButtons(selectedItem, options)(_: Seq[(JSInput, T)] => Seq[Node]), + BootstrapStyles.Form.customRadio, + inline, + validationTrigger, + groupId, + )(inputModifier, labelContent, validFeedback, invalidFeedback, validator) + ) private class InputComponent(in: InputBinding[_ <: Element], inputId: ComponentId) extends UdashBootstrapComponent { private val input: InputBinding[_ <: Element] = nestedInterceptor(in) @@ -696,13 +837,13 @@ final class FormElementsFactory( inputDecorationClass: CssStyle, inline: ReadableProperty[Boolean], validationTrigger: ValidationTrigger, - groupId: ComponentId + groupId: ComponentId, )( inputModifier: (T, Int, Binding.NestedInterceptor) => Option[Modifier], labelContent: (T, Int, Binding.NestedInterceptor) => Option[Modifier], validFeedback: (T, Int, Binding.NestedInterceptor) => Option[Modifier], invalidFeedback: (T, Int, Binding.NestedInterceptor) => Option[Modifier], - validator: Validator[SelectedType] + validator: Validator[SelectedType], ) extends UdashBootstrapComponent { private val inputs = nestedInterceptor(input { inputs => val groupValidationTrigger = Some(Property(0)) @@ -710,15 +851,18 @@ final class FormElementsFactory( Seq[Modifier]( BootstrapStyles.Form.customControlInput, inputModifier(item, idx, nestedInterceptor), - validationModifier(selected, validationTrigger, nestedInterceptor, groupValidationTrigger)(validator) + validationModifier(selected, validationTrigger, nestedInterceptor, groupValidationTrigger)(validator), ).applyTo(singleInput) div( singleInput, - BootstrapStyles.Form.customControl, inputDecorationClass, + BootstrapStyles.Form.customControl, + inputDecorationClass, nestedInterceptor(BootstrapStyles.Form.customControlInline.styleIf(inline)), - labelContent(item, idx, nestedInterceptor).map(label(`for` := singleInput.id, BootstrapStyles.Form.customControlLabel)(_)), + labelContent(item, idx, nestedInterceptor).map( + label(`for` := singleInput.id, BootstrapStyles.Form.customControlLabel)(_) + ), validFeedback(item, idx, nestedInterceptor).map(div(BootstrapStyles.Form.validFeedback)(_)), - invalidFeedback(item, idx, nestedInterceptor).map(div(BootstrapStyles.Form.invalidFeedback)(_)) + invalidFeedback(item, idx, nestedInterceptor).map(div(BootstrapStyles.Form.invalidFeedback)(_)), ).render } }) @@ -729,9 +873,13 @@ final class FormElementsFactory( } private def validationModifier[ArgumentType]( - property: ReadableProperty[ArgumentType], validationTrigger: ValidationTrigger, nested: Binding.NestedInterceptor, - groupValidationTrigger: Option[Property[Int]] = None // value change on this property should trigger validation - )(validator: Validator[ArgumentType]): Modifier = { + property: ReadableProperty[ArgumentType], + validationTrigger: ValidationTrigger, + nested: Binding.NestedInterceptor, + groupValidationTrigger: Option[Property[Int]] = None, // value change on this property should trigger validation + )( + validator: Validator[ArgumentType] + ): Modifier = { def groupTrigger(startValidation: () => Any) = new Binding { override def applyTo(t: Element): Unit = { groupValidationTrigger.foreach { p => @@ -743,7 +891,7 @@ final class FormElementsFactory( } def startValidation(validationResult: Property[Option[ValidationResult]], triggerGroup: Boolean): Unit = { - if (triggerGroup) groupValidationTrigger.foreach { p => p.set(p.get + 1) } + if (triggerGroup) groupValidationTrigger.foreach(p => p.set(p.get + 1)) validationResult.set(None) validator(property.get).onComplete { case Success(r) => validationResult.set(Some(r)) @@ -765,7 +913,7 @@ final class FormElementsFactory( super.kill() form.foreach(_.validationProperties -= validationResult) } - }) + }), ) validationTrigger match { @@ -775,9 +923,8 @@ final class FormElementsFactory( Seq( nested(new Binding { override def applyTo(t: Element): Unit = { - propertyListeners += property.listen({ _ => - startValidation(validationResult, triggerGroup = true) - }, initUpdate = true) + propertyListeners += + property.listen(_ => startValidation(validationResult, triggerGroup = true), initUpdate = true) form.foreach(_.validationProperties += validationResult) } override def kill(): Unit = { @@ -786,7 +933,9 @@ final class FormElementsFactory( } }), nested(BootstrapStyles.Form.isValid.styleIf(validationResult.transform(_.contains(Valid)))), - nested(BootstrapStyles.Form.isInvalid.styleIf(validationResult.transform(v => v.isDefined && !v.contains(Valid)))) + nested( + BootstrapStyles.Form.isInvalid.styleIf(validationResult.transform(v => v.isDefined && !v.contains(Valid))) + ), ) case ValidationTrigger.OnBlur => val validationResult = Property[Option[ValidationResult]](None) @@ -795,7 +944,7 @@ final class FormElementsFactory( onblur :+= { (_: Event) => startValidation(validationResult, triggerGroup = true) false - } + }, ) case ValidationTrigger.OnChange => val validationResult = Property[Option[ValidationResult]](None) @@ -807,7 +956,7 @@ final class FormElementsFactory( startValidation(validationResult, triggerGroup = true) } } - }) + }), ) case ValidationTrigger.OnSubmit => val validationResult = Property[Option[ValidationResult]](None) @@ -820,7 +969,7 @@ final class FormElementsFactory( startValidation(validationResult, triggerGroup = true) }) } - }) + }), ) } } @@ -847,6 +996,7 @@ object FormElementsFactory { nestedInterceptor: NestedInterceptor = NestedInterceptor.Identity, inputValidationTrigger: ValidationTrigger = ValidationTrigger.None, selectValidationTrigger: ValidationTrigger = ValidationTrigger.None, - form: OptArg[UdashForm] = OptArg.Empty - ): FormElementsFactory = new FormElementsFactory(nestedInterceptor, inputValidationTrigger, selectValidationTrigger, form) -} \ No newline at end of file + form: OptArg[UdashForm] = OptArg.Empty, + ): FormElementsFactory = + new FormElementsFactory(nestedInterceptor, inputValidationTrigger, selectValidationTrigger, form) +} diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/form/UdashInputGroup.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/form/UdashInputGroup.scala index a71de4d4d..6b11829be 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/form/UdashInputGroup.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/form/UdashInputGroup.scala @@ -6,39 +6,45 @@ import io.udash.bootstrap.utils.{BootstrapStyles, UdashBootstrapComponent} import org.scalajs.dom.Element import scalatags.JsDom.all._ -final class UdashInputGroup private( +final class UdashInputGroup private ( groupSize: ReadableProperty[Option[BootstrapStyles.Size]], - override val componentId: ComponentId -)(content: Modifier*) extends UdashBootstrapComponent { + override val componentId: ComponentId, +)( + content: Modifier* +) extends UdashBootstrapComponent { import io.udash.css.CssView._ override val render: Element = div( BootstrapStyles.InputGroup.inputGroup, - nestedInterceptor((BootstrapStyles.InputGroup.size _).reactiveOptionApply(groupSize)) + nestedInterceptor((BootstrapStyles.InputGroup.size _).reactiveOptionApply(groupSize)), )(content).render } object UdashInputGroup { import io.udash.css.CssView._ - /** - * Creates an inputs group. - * More: Bootstrap Docs. + /** Creates an inputs group. More: Bootstrap + * Docs. * - * @param groupSize A size of the inputs in group. One of standard bootstrap values: `BootstrapStyles.Size`. - * @param componentId An id of the root DOM node. - * @param content The group content. The elements usually need to be wrapped with methods like: - * `UdashInputGroup.input`, `UdashInputGroup.select`, `UdashInputGroup.prepend`, `UdashInputGroup.append`, etc. - * @return A `UdashInputGroup` component, call `render` to create a DOM element. + * @param groupSize + * A size of the inputs in group. One of standard bootstrap values: `BootstrapStyles.Size`. + * @param componentId + * An id of the root DOM node. + * @param content + * The group content. The elements usually need to be wrapped with methods like: `UdashInputGroup.input`, + * `UdashInputGroup.select`, `UdashInputGroup.prepend`, `UdashInputGroup.append`, etc. + * @return + * A `UdashInputGroup` component, call `render` to create a DOM element. */ def apply( groupSize: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None, - componentId: ComponentId = ComponentId.generate() - )(content: Modifier*): UdashInputGroup = { + componentId: ComponentId = ComponentId.generate(), + )( + content: Modifier* + ): UdashInputGroup = new UdashInputGroup(groupSize, componentId)(content) - } /** Adds `form-control` style to provided element. It's required to properly display input as part of group. */ def input(el: Element): Element = @@ -92,4 +98,4 @@ object UdashInputGroup { @inline def appendButton(content: Modifier*): Modifier = append(content) -} \ No newline at end of file +} diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/form/Validator.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/form/Validator.scala index 6c8c70e8f..21d3ada67 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/form/Validator.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/form/Validator.scala @@ -30,7 +30,7 @@ object Invalid { Invalid(error +: errors) def apply(error: String, errors: String*): Invalid[ValidationError] = - this ((error +: errors).map(DefaultValidationError.apply)) + this((error +: errors).map(DefaultValidationError.apply)) } trait Validator[-ArgumentType] { @@ -41,11 +41,10 @@ object Validator { final val Default: Validator[Any] = _ => Valid implicit final class FutureValidationOps[T](private val future: Future[Seq[ValidationResult]]) extends AnyVal { - def foldValidationResult: Future[ValidationResult] = { + def foldValidationResult: Future[ValidationResult] = future.mapNow { s => val errors = s.iterator.flatCollect { case Invalid(results) => results } if (errors.isEmpty) Valid else Invalid(errors.toSeq) } - } } -} \ No newline at end of file +} diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/jumbotron/UdashJumbotron.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/jumbotron/UdashJumbotron.scala index ea00067c8..c8c9b9d7d 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/jumbotron/UdashJumbotron.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/jumbotron/UdashJumbotron.scala @@ -7,37 +7,42 @@ import io.udash.bootstrap.utils.{BootstrapStyles, UdashBootstrapComponent} import org.scalajs.dom.Element import scalatags.JsDom.all._ -final class UdashJumbotron private( +final class UdashJumbotron private ( fluid: ReadableProperty[Boolean], - override val componentId: ComponentId -)(content: Binding.NestedInterceptor => Modifier) extends UdashBootstrapComponent { + override val componentId: ComponentId, +)( + content: Binding.NestedInterceptor => Modifier +) extends UdashBootstrapComponent { import io.udash.css.CssView._ - override val render: Element = { + override val render: Element = div( componentId, BootstrapStyles.Jumbotron.jumbotron, - nestedInterceptor(BootstrapStyles.Jumbotron.fluid.styleIf(fluid)) + nestedInterceptor(BootstrapStyles.Jumbotron.fluid.styleIf(fluid)), )(content(nestedInterceptor)).render - } } object UdashJumbotron { - /** - * Creates a jumbotron component. - * More: Bootstrap Docs. + + /** Creates a jumbotron component. More: Bootstrap + * Docs. * - * @param fluid If true, applies `jumbotron-fluid` style. - * @param componentId An id of the root DOM node. - * @param content A jumbotron content. - * Use the provided interceptor to properly clean up bindings inside the content. - * @return A `UdashJumbotron` component, call `render` to create a DOM element. + * @param fluid + * If true, applies `jumbotron-fluid` style. + * @param componentId + * An id of the root DOM node. + * @param content + * A jumbotron content. Use the provided interceptor to properly clean up bindings inside the content. + * @return + * A `UdashJumbotron` component, call `render` to create a DOM element. */ def apply( fluid: ReadableProperty[Boolean] = UdashBootstrap.False, - componentId: ComponentId = ComponentId.generate() - )(content: Binding.NestedInterceptor => Modifier): UdashJumbotron = { + componentId: ComponentId = ComponentId.generate(), + )( + content: Binding.NestedInterceptor => Modifier + ): UdashJumbotron = new UdashJumbotron(fluid, componentId)(content) - } } diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/list/UdashListGroup.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/list/UdashListGroup.scala index 6e9ce3e45..6a750fb12 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/list/UdashListGroup.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/list/UdashListGroup.scala @@ -7,18 +7,21 @@ import io.udash.bootstrap.utils.{BootstrapStyles, UdashBootstrapComponent} import io.udash.properties.seq import org.scalajs.dom.Element -final class UdashListGroup[ItemType, ElemType <: ReadableProperty[ItemType]] private( +final class UdashListGroup[ItemType, ElemType <: ReadableProperty[ItemType]] private ( items: seq.ReadableSeqProperty[ItemType, ElemType], flush: ReadableProperty[Boolean], - override val componentId: ComponentId -)(itemFactory: (ElemType, Binding.NestedInterceptor) => Element) extends UdashBootstrapComponent { + override val componentId: ComponentId, +)( + itemFactory: (ElemType, Binding.NestedInterceptor) => Element +) extends UdashBootstrapComponent { import io.udash.css.CssView._ import scalatags.JsDom.all._ override val render: Element = div( - componentId, BootstrapStyles.ListGroup.listGroup, - nestedInterceptor(BootstrapStyles.ListGroup.flush.styleIf(flush)) + componentId, + BootstrapStyles.ListGroup.listGroup, + nestedInterceptor(BootstrapStyles.ListGroup.flush.styleIf(flush)), )( nestedInterceptor( repeatWithNested(items) { (item, nested) => @@ -29,23 +32,32 @@ final class UdashListGroup[ItemType, ElemType <: ReadableProperty[ItemType]] pri } object UdashListGroup { - /** - * Creates a list group component, synchronised with a provided items sequence. - * More: Bootstrap Docs. + + /** Creates a list group component, synchronised with a provided items sequence. More: Bootstrap Docs. * - * @param items Data items which will be represented as DOM elements in this group. - * @param flush If true, applies `list-group-flush` style. - * @param componentId An id of the root DOM node. - * @param itemFactory Creates DOM representation of the provided element. - * Use the provided interceptor to properly clean up bindings inside the content. - * @tparam ItemType A single element's type in the `items` sequence. - * @tparam ElemType A type of a property containing an element in the `items` sequence. - * @return A `UdashBreadcrumbs` component, call `render` to create a DOM element. + * @param items + * Data items which will be represented as DOM elements in this group. + * @param flush + * If true, applies `list-group-flush` style. + * @param componentId + * An id of the root DOM node. + * @param itemFactory + * Creates DOM representation of the provided element. Use the provided interceptor to properly clean up bindings + * inside the content. + * @tparam ItemType + * A single element's type in the `items` sequence. + * @tparam ElemType + * A type of a property containing an element in the `items` sequence. + * @return + * A `UdashBreadcrumbs` component, call `render` to create a DOM element. */ def apply[ItemType, ElemType <: ReadableProperty[ItemType]]( items: seq.ReadableSeqProperty[ItemType, ElemType], flush: ReadableProperty[Boolean] = UdashBootstrap.False, - componentId: ComponentId = ComponentId.generate() - )(itemFactory: (ElemType, Binding.NestedInterceptor) => Element): UdashListGroup[ItemType, ElemType] = + componentId: ComponentId = ComponentId.generate(), + )( + itemFactory: (ElemType, Binding.NestedInterceptor) => Element + ): UdashListGroup[ItemType, ElemType] = new UdashListGroup(items, flush, componentId)(itemFactory) -} \ No newline at end of file +} diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/modal/UdashModal.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/modal/UdashModal.scala index 8bbdb9fa4..189386689 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/modal/UdashModal.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/modal/UdashModal.scala @@ -10,18 +10,19 @@ import org.scalajs.dom.Element import scala.scalajs.js -final class UdashModal private( +final class UdashModal private ( modalSize: ReadableProperty[Option[BootstrapStyles.Size]], fade: ReadableProperty[Boolean], labelId: ReadableProperty[Option[String]], backdrop: ReadableProperty[UdashModal.BackdropType], keyboard: ReadableProperty[Boolean], - override val componentId: ComponentId + override val componentId: ComponentId, )( headerFactory: Option[Binding.NestedInterceptor => Element], bodyFactory: Option[Binding.NestedInterceptor => Element], - footerFactory: Option[Binding.NestedInterceptor => Element] -) extends UdashBootstrapComponent with Listenable { + footerFactory: Option[Binding.NestedInterceptor => Element], +) extends UdashBootstrapComponent + with Listenable { import UdashModal._ import io.udash.css.CssView._ @@ -32,34 +33,41 @@ final class UdashModal private( /** Toggles modal visibility. */ def toggle(): Unit = jQSelector().modal("toggle") + /** Shows modal window. */ def show(): Unit = jQSelector().modal("show") + /** Hides modal window. */ def hide(): Unit = jQSelector().modal("hide") - /** Readjusts the modal's positioning to counter a scrollbar in case one should - * appear, which would make the modal jump to the left. */ + + /** Readjusts the modal's positioning to counter a scrollbar in case one should appear, which would make the modal + * jump to the left. + */ def handleUpdate(): Unit = jQSelector().modal("handleUpdate") override val render: Element = { val content = Seq( (headerFactory, BootstrapStyles.Modal.header), (bodyFactory, BootstrapStyles.Modal.body), - (footerFactory, BootstrapStyles.Modal.footer) + (footerFactory, BootstrapStyles.Modal.footer), ).collect { case (Some(factory), styleName) => factory(nestedInterceptor).styles(styleName) } val el = div( BootstrapStyles.Modal.modal, - tabindex := "-1", role := "dialog", componentId, + tabindex := "-1", + role := "dialog", + componentId, nestedInterceptor(aria.labelledby.bindIf(labelId.transform(_.getOrElse("")), labelId.transform(_.isDefined))), nestedInterceptor(BootstrapTags.dataBackdrop.bind(backdrop.transform(_.jsValue))), nestedInterceptor(BootstrapTags.dataKeyboard.bind(keyboard.transform(_.toString))), - nestedInterceptor(BootstrapStyles.fade.styleIf(fade)) + nestedInterceptor(BootstrapStyles.fade.styleIf(fade)), )( div( - BootstrapStyles.Modal.dialog, role := "document", - nestedInterceptor((BootstrapStyles.Modal.size _).reactiveOptionApply(modalSize)) + BootstrapStyles.Modal.dialog, + role := "document", + nestedInterceptor((BootstrapStyles.Modal.size _).reactiveOptionApply(modalSize)), )( div(BootstrapStyles.Modal.content)( content @@ -68,10 +76,34 @@ final class UdashModal private( ).render val jQEl = jQ(el) - nestedInterceptor(new JQueryOnBinding(jQEl, "show.bs.modal", (_: Element, _: JQueryEvent) => fire(ModalEvent(this, ModalEvent.EventType.Show)))) - nestedInterceptor(new JQueryOnBinding(jQEl, "shown.bs.modal", (_: Element, _: JQueryEvent) => fire(ModalEvent(this, ModalEvent.EventType.Shown)))) - nestedInterceptor(new JQueryOnBinding(jQEl, "hide.bs.modal", (_: Element, _: JQueryEvent) => fire(ModalEvent(this, ModalEvent.EventType.Hide)))) - nestedInterceptor(new JQueryOnBinding(jQEl, "hidden.bs.modal", (_: Element, _: JQueryEvent) => fire(ModalEvent(this, ModalEvent.EventType.Hidden)))) + nestedInterceptor( + new JQueryOnBinding( + jQEl, + "show.bs.modal", + (_: Element, _: JQueryEvent) => fire(ModalEvent(this, ModalEvent.EventType.Show)), + ) + ) + nestedInterceptor( + new JQueryOnBinding( + jQEl, + "shown.bs.modal", + (_: Element, _: JQueryEvent) => fire(ModalEvent(this, ModalEvent.EventType.Shown)), + ) + ) + nestedInterceptor( + new JQueryOnBinding( + jQEl, + "hide.bs.modal", + (_: Element, _: JQueryEvent) => fire(ModalEvent(this, ModalEvent.EventType.Hide)), + ) + ) + nestedInterceptor( + new JQueryOnBinding( + jQEl, + "hidden.bs.modal", + (_: Element, _: JQueryEvent) => fire(ModalEvent(this, ModalEvent.EventType.Hidden)), + ) + ) el } @@ -95,42 +127,59 @@ object UdashModal { /** More: Bootstrap Docs */ final case class ModalEvent( override val source: UdashModal, - tpe: ModalEvent.EventType - ) extends AbstractCase with ListenableEvent + tpe: ModalEvent.EventType, + ) extends AbstractCase + with ListenableEvent object ModalEvent { final class EventType(implicit enumCtx: EnumCtx) extends AbstractValueEnum object EventType extends AbstractValueEnumCompanion[EventType] { - /** This event fires immediately when the show instance method is called. - * If caused by a click, the clicked element is available as the relatedTarget property of the event. */ + + /** This event fires immediately when the show instance method is called. If caused by a click, the clicked + * element is available as the relatedTarget property of the event. + */ final val Show: Value = new EventType - /** This event is fired when the modal has been made visible to the user (will wait for CSS transitions to complete). - * If caused by a click, the clicked element is available as the relatedTarget property of the event. */ + + /** This event is fired when the modal has been made visible to the user (will wait for CSS transitions to + * complete). If caused by a click, the clicked element is available as the relatedTarget property of the event. + */ final val Shown: Value = new EventType + /** This event is fired immediately when the hide instance method has been called. */ final val Hide: Value = new EventType - /** This event is fired when the modal has finished being hidden from the user (will wait for CSS transitions to complete). */ + + /** This event is fired when the modal has finished being hidden from the user (will wait for CSS transitions to + * complete). + */ final val Hidden: Value = new EventType } } - /** - * Creates a modal window. - * More: Bootstrap Docs. + /** Creates a modal window. More: Bootstrap Docs. * - * @param modalSize A window size. One of the standard bootstrap sizes `BootstrapStyles.Size`. - * @param fade If true, show and hide will be animated. - * @param labelId An id of the label describing this modal. - * @param backdrop Modal backdrop type. - * @param keyboard If true, allows user to close modal with keyboard (Esc button). - * @param componentId An id of the root DOM node. - * @param headerFactory Creates content of modal header. Modal will be rendered without the header if `None`. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param bodyFactory Creates content of modal body. Modal will be rendered without body if `None`. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param footerFactory Creates content of modal footer. Modal without footer will be rendered if `None`. - * Use the provided interceptor to properly clean up bindings inside the content. - * @return A `UdashModal` component, call `render` to create a DOM element. + * @param modalSize + * A window size. One of the standard bootstrap sizes `BootstrapStyles.Size`. + * @param fade + * If true, show and hide will be animated. + * @param labelId + * An id of the label describing this modal. + * @param backdrop + * Modal backdrop type. + * @param keyboard + * If true, allows user to close modal with keyboard (Esc button). + * @param componentId + * An id of the root DOM node. + * @param headerFactory + * Creates content of modal header. Modal will be rendered without the header if `None`. Use the provided + * interceptor to properly clean up bindings inside the content. + * @param bodyFactory + * Creates content of modal body. Modal will be rendered without body if `None`. Use the provided interceptor to + * properly clean up bindings inside the content. + * @param footerFactory + * Creates content of modal footer. Modal without footer will be rendered if `None`. Use the provided interceptor + * to properly clean up bindings inside the content. + * @return + * A `UdashModal` component, call `render` to create a DOM element. */ def apply( modalSize: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None, @@ -138,19 +187,24 @@ object UdashModal { labelId: ReadableProperty[Option[String]] = UdashBootstrap.None, backdrop: ReadableProperty[UdashModal.BackdropType] = BackdropType.Active.toProperty, keyboard: ReadableProperty[Boolean] = UdashBootstrap.True, - componentId: ComponentId = ComponentId.generate() + componentId: ComponentId = ComponentId.generate(), )( headerFactory: Option[Binding.NestedInterceptor => Element], bodyFactory: Option[Binding.NestedInterceptor => Element], - footerFactory: Option[Binding.NestedInterceptor => Element] - ): UdashModal = { + footerFactory: Option[Binding.NestedInterceptor => Element], + ): UdashModal = new UdashModal( - modalSize, fade, labelId, backdrop, keyboard, componentId + modalSize, + fade, + labelId, + backdrop, + keyboard, + componentId, )(headerFactory, bodyFactory, footerFactory) - } - /** Attributes which should be added to button closing the modal window. - * Example: `UdashButton()(_ => Seq[Modifier](UdashModal.CloseButtonAttr, "Close..."))` */ + /** Attributes which should be added to button closing the modal window. Example: + * `UdashButton()(_ => Seq[Modifier](UdashModal.CloseButtonAttr, "Close..."))` + */ lazy val CloseButtonAttr: scalatags.generic.AttrPair[Element, String] = { import scalatags.JsDom.all._ BootstrapTags.dataDismiss := "modal" diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/nav/UdashNav.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/nav/UdashNav.scala index e9cceb2a0..9adc83005 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/nav/UdashNav.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/nav/UdashNav.scala @@ -6,13 +6,13 @@ import io.udash.bindings.modifiers.Binding import io.udash.bootstrap.nav.UdashNav.UdashTabJQuery import io.udash.bootstrap.utils.{BootstrapStyles, UdashBootstrapComponent} import io.udash.properties.seq -import io.udash.wrappers.jquery.{JQuery, jQ} +import io.udash.wrappers.jquery.{jQ, JQuery} import org.scalajs.dom.Element import scalatags.JsDom.all._ import scala.scalajs.js -final class UdashNav[ItemType, ElemType <: ReadableProperty[ItemType]] private( +final class UdashNav[ItemType, ElemType <: ReadableProperty[ItemType]] private ( panels: seq.ReadableSeqProperty[ItemType, ElemType], align: ReadableProperty[BootstrapStyles.Align], vertical: ReadableProperty[Boolean], @@ -20,32 +20,34 @@ final class UdashNav[ItemType, ElemType <: ReadableProperty[ItemType]] private( justified: ReadableProperty[Boolean], tabs: ReadableProperty[Boolean], pills: ReadableProperty[Boolean], - override val componentId: ComponentId + override val componentId: ComponentId, )( elemFactory: (ElemType, Binding.NestedInterceptor) => Element, isActive: ElemType => ReadableProperty[Boolean], isDisabled: ElemType => ReadableProperty[Boolean], - isDropdown: ElemType => ReadableProperty[Boolean] + isDropdown: ElemType => ReadableProperty[Boolean], ) extends UdashBootstrapComponent { import io.udash.css.CssView._ - override val render: Element = { + override val render: Element = ul( componentId, BootstrapStyles.Navigation.nav, - nestedInterceptor(BootstrapStyles.Navigation.justifyCenter.styleIf(align.transform(_ == BootstrapStyles.Align.Center))), + nestedInterceptor( + BootstrapStyles.Navigation.justifyCenter.styleIf(align.transform(_ == BootstrapStyles.Align.Center)) + ), nestedInterceptor(BootstrapStyles.Navigation.justifyRight.styleIf(align.transform(_ == BootstrapStyles.Align.Right))), nestedInterceptor(BootstrapStyles.Flex.column().styleIf(vertical)), nestedInterceptor(BootstrapStyles.Navigation.tabs.styleIf(tabs)), nestedInterceptor(BootstrapStyles.Navigation.pills.styleIf(pills)), nestedInterceptor(BootstrapStyles.Navigation.fill.styleIf(fill)), - nestedInterceptor(BootstrapStyles.Navigation.justified.styleIf(justified)) + nestedInterceptor(BootstrapStyles.Navigation.justified.styleIf(justified)), )( nestedInterceptor( repeatWithNested(panels) { case (panel, nested) => li(role := "presentation")( BootstrapStyles.Navigation.item, - nested(BootstrapStyles.Dropdown.dropdown.styleIf(isDropdown(panel))) + nested(BootstrapStyles.Dropdown.dropdown.styleIf(isDropdown(panel))), ) { val el = elemFactory(panel, nested) nested(BootstrapStyles.active.styleIf(isActive(panel))).applyTo(el) @@ -55,7 +57,6 @@ final class UdashNav[ItemType, ElemType <: ReadableProperty[ItemType]] private( } ) ).render - } override def kill(): Unit = { super.kill() @@ -73,35 +74,49 @@ object UdashNav { class NavItem(val name: String, val link: Url) /** Default breadcrumb model factory. */ - val defaultItemFactory: (ReadableProperty[NavItem], Binding.NestedInterceptor) => Element = { - (item, nested) => a( + val defaultItemFactory: (ReadableProperty[NavItem], Binding.NestedInterceptor) => Element = { (item, nested) => + a( nested(href.bind(item.transform(_.link.value))), nested(bind(item.transform(_.name))), - BootstrapStyles.Navigation.link + BootstrapStyles.Navigation.link, ).render } - /** - * Creates a navigation component. - * More: Bootstrap Docs. + /** Creates a navigation component. More: Bootstrap + * Docs. * - * @param panels Sequence of elements to be converted into navigation. - * @param align Alignment of elements in navigation component. - * @param vertical If true, shows component as column. - * @param fill If true, adjusts items width to fill the whole component space. - * @param justified If true, adjusts items width (all elements with equal width) to fill the whole component space. - * @param tabs If true, applies `nav-tabs` style. - * @param pills If true, applies `nav-pills` style. - * @param componentId An id of the root DOM node. - * @param elemFactory Creates DOM hierarchy representing an element in the navigation. - * Use the provided interceptor to properly clean up bindings inside the content. - * Usually you should add the `BootstrapStyles.Navigation.link` style to your links. - * @param isActive Creates property indicating if an element is active. - * @param isDisabled Creates property indicating if an element is disabled. - * @param isDropdown Creates property indicating if an element has a dropdown menu. - * @tparam ItemType A single element's type in the `items` sequence. - * @tparam ElemType A type of a property containing an element in the `items` sequence. - * @return A `UdashNav` component, call `render` to create a DOM element. + * @param panels + * Sequence of elements to be converted into navigation. + * @param align + * Alignment of elements in navigation component. + * @param vertical + * If true, shows component as column. + * @param fill + * If true, adjusts items width to fill the whole component space. + * @param justified + * If true, adjusts items width (all elements with equal width) to fill the whole component space. + * @param tabs + * If true, applies `nav-tabs` style. + * @param pills + * If true, applies `nav-pills` style. + * @param componentId + * An id of the root DOM node. + * @param elemFactory + * Creates DOM hierarchy representing an element in the navigation. Use the provided interceptor to properly clean + * up bindings inside the content. Usually you should add the `BootstrapStyles.Navigation.link` style to your + * links. + * @param isActive + * Creates property indicating if an element is active. + * @param isDisabled + * Creates property indicating if an element is disabled. + * @param isDropdown + * Creates property indicating if an element has a dropdown menu. + * @tparam ItemType + * A single element's type in the `items` sequence. + * @tparam ElemType + * A type of a property containing an element in the `items` sequence. + * @return + * A `UdashNav` component, call `render` to create a DOM element. */ def apply[ItemType, ElemType <: ReadableProperty[ItemType]]( panels: seq.ReadableSeqProperty[ItemType, ElemType], @@ -111,37 +126,56 @@ object UdashNav { justified: ReadableProperty[Boolean] = UdashBootstrap.False, tabs: ReadableProperty[Boolean] = UdashBootstrap.False, pills: ReadableProperty[Boolean] = UdashBootstrap.False, - componentId: ComponentId = ComponentId.generate() + componentId: ComponentId = ComponentId.generate(), )( elemFactory: (ElemType, Binding.NestedInterceptor) => Element, isActive: ElemType => ReadableProperty[Boolean] = (_: ElemType) => UdashBootstrap.False, isDisabled: ElemType => ReadableProperty[Boolean] = (_: ElemType) => UdashBootstrap.False, - isDropdown: ElemType => ReadableProperty[Boolean] = (_: ElemType) => UdashBootstrap.False - ): UdashNav[ItemType, ElemType] = { + isDropdown: ElemType => ReadableProperty[Boolean] = (_: ElemType) => UdashBootstrap.False, + ): UdashNav[ItemType, ElemType] = new UdashNav( - panels, align, vertical, fill, justified, tabs, pills, componentId + panels, + align, + vertical, + fill, + justified, + tabs, + pills, + componentId, )(elemFactory, isActive, isDisabled, isDropdown) - } - /** - * Creates a navigation component based on the default `NavItem`. - * More: Bootstrap Docs. + /** Creates a navigation component based on the default `NavItem`. More: Bootstrap Docs. * - * @param panels Sequence of elements to be converted into navigation. - * @param align Alignment of elements in navigation component. - * @param vertical If true, shows component as column. - * @param fill If true, adjusts items width to fill the whole component space. - * @param justified If true, adjusts items width (all elements with equal width) to fill the whole component space. - * @param tabs If true, applies `nav-tabs` style. - * @param pills If true, applies `nav-pills` style. - * @param componentId An id of the root DOM node. - * @param elemFactory Creates DOM hierarchy representing an element in the navigation. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param isActive Creates property indicating if an element is active. - * @param isDisabled Creates property indicating if an element is disabled. - * @param isDropdown Creates property indicating if an element has a dropdown menu. - * @tparam ElemType A type of a property containing an element in the `items` sequence. - * @return A `UdashNav` component, call `render` to create a DOM element. + * @param panels + * Sequence of elements to be converted into navigation. + * @param align + * Alignment of elements in navigation component. + * @param vertical + * If true, shows component as column. + * @param fill + * If true, adjusts items width to fill the whole component space. + * @param justified + * If true, adjusts items width (all elements with equal width) to fill the whole component space. + * @param tabs + * If true, applies `nav-tabs` style. + * @param pills + * If true, applies `nav-pills` style. + * @param componentId + * An id of the root DOM node. + * @param elemFactory + * Creates DOM hierarchy representing an element in the navigation. Use the provided interceptor to properly clean + * up bindings inside the content. + * @param isActive + * Creates property indicating if an element is active. + * @param isDisabled + * Creates property indicating if an element is disabled. + * @param isDropdown + * Creates property indicating if an element has a dropdown menu. + * @tparam ElemType + * A type of a property containing an element in the `items` sequence. + * @return + * A `UdashNav` component, call `render` to create a DOM element. */ def default[ElemType <: ReadableProperty[NavItem]]( panels: seq.ReadableSeqProperty[NavItem, ElemType], @@ -151,20 +185,26 @@ object UdashNav { justified: ReadableProperty[Boolean] = UdashBootstrap.False, tabs: ReadableProperty[Boolean] = UdashBootstrap.False, pills: ReadableProperty[Boolean] = UdashBootstrap.False, - componentId: ComponentId = ComponentId.generate() + componentId: ComponentId = ComponentId.generate(), )( elemFactory: (ElemType, Binding.NestedInterceptor) => Element = defaultItemFactory, isActive: ElemType => ReadableProperty[Boolean] = (_: ElemType) => UdashBootstrap.False, isDisabled: ElemType => ReadableProperty[Boolean] = (_: ElemType) => UdashBootstrap.False, - isDropdown: ElemType => ReadableProperty[Boolean] = (_: ElemType) => UdashBootstrap.False - ): UdashNav[NavItem, ElemType] = { + isDropdown: ElemType => ReadableProperty[Boolean] = (_: ElemType) => UdashBootstrap.False, + ): UdashNav[NavItem, ElemType] = new UdashNav( - panels, align, vertical, fill, justified, tabs, pills, componentId + panels, + align, + vertical, + fill, + justified, + tabs, + pills, + componentId, )(elemFactory, isActive, isDisabled, isDropdown) - } @js.native private trait UdashTabJQuery extends JQuery { def tab(cmd: String): UdashTabJQuery = js.native } -} \ No newline at end of file +} diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/nav/UdashNavbar.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/nav/UdashNavbar.scala index 5ca1afee4..0d291a20e 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/nav/UdashNavbar.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/nav/UdashNavbar.scala @@ -10,17 +10,16 @@ import org.scalajs.dom.Element import scalatags.JsDom.all._ import scalatags.JsDom.tags2 -final class UdashNavbar[ItemType, ElemType <: ReadableProperty[ItemType]] private( +final class UdashNavbar[ItemType, ElemType <: ReadableProperty[ItemType]] private ( expandBreakpoint: ReadableProperty[BootstrapStyles.ResponsiveBreakpoint], darkStyle: ReadableProperty[Boolean], backgroundStyle: ReadableProperty[BootstrapStyles.Color], position: ReadableProperty[UdashNavbar.Position], - override val componentId: ComponentId + override val componentId: ComponentId, )( navigationFactory: Binding.NestedInterceptor => Modifier, - brand: Modifier -) - extends UdashBootstrapComponent { + brand: Modifier, +) extends UdashBootstrapComponent { import io.udash.bootstrap.utils.BootstrapTags._ import io.udash.css.CssView._ @@ -28,31 +27,41 @@ final class UdashNavbar[ItemType, ElemType <: ReadableProperty[ItemType]] privat private val collapseId = ComponentId.generate() override val render: Element = - tags2.nav( - componentId, BootstrapStyles.NavigationBar.navbar, - nestedInterceptor(BootstrapStyles.Position.fixedTop.styleIf(position.transform(_ == Position.FixedTop))), - nestedInterceptor(BootstrapStyles.Position.fixedBottom.styleIf(position.transform(_ == Position.FixedBottom))), - nestedInterceptor(BootstrapStyles.Position.stickyTop.styleIf(position.transform(_ == Position.StickyTop))), - nestedInterceptor(((dark: Boolean) => - if (dark) BootstrapStyles.NavigationBar.dark - else BootstrapStyles.NavigationBar.light - ).reactiveApply(darkStyle)), - nestedInterceptor((BootstrapStyles.Background.color _).reactiveApply(backgroundStyle)), - nestedInterceptor((BootstrapStyles.NavigationBar.expand _).reactiveApply(expandBreakpoint)) - )( - div(BootstrapStyles.NavigationBar.brand)(brand), - button( - tpe := "button", dataToggle := "collapse", dataTarget := s"#$collapseId", - aria.expanded := false, aria.label := "Toggle navigation", - BootstrapStyles.NavigationBar.toggler - )(span(BootstrapStyles.NavigationBar.togglerIcon)), - div( - collapseId, BootstrapStyles.Collapse.collapse, - BootstrapStyles.NavigationBar.collapse + tags2 + .nav( + componentId, + BootstrapStyles.NavigationBar.navbar, + nestedInterceptor(BootstrapStyles.Position.fixedTop.styleIf(position.transform(_ == Position.FixedTop))), + nestedInterceptor(BootstrapStyles.Position.fixedBottom.styleIf(position.transform(_ == Position.FixedBottom))), + nestedInterceptor(BootstrapStyles.Position.stickyTop.styleIf(position.transform(_ == Position.StickyTop))), + nestedInterceptor( + ( + (dark: Boolean) => + if (dark) BootstrapStyles.NavigationBar.dark + else BootstrapStyles.NavigationBar.light + ).reactiveApply(darkStyle) + ), + nestedInterceptor((BootstrapStyles.Background.color _).reactiveApply(backgroundStyle)), + nestedInterceptor((BootstrapStyles.NavigationBar.expand _).reactiveApply(expandBreakpoint)), )( - navigationFactory(nestedInterceptor) + div(BootstrapStyles.NavigationBar.brand)(brand), + button( + tpe := "button", + dataToggle := "collapse", + dataTarget := s"#$collapseId", + aria.expanded := false, + aria.label := "Toggle navigation", + BootstrapStyles.NavigationBar.toggler, + )(span(BootstrapStyles.NavigationBar.togglerIcon)), + div( + collapseId, + BootstrapStyles.Collapse.collapse, + BootstrapStyles.NavigationBar.collapse, + )( + navigationFactory(nestedInterceptor) + ), ) - ).render + .render } object UdashNavbar { @@ -61,67 +70,85 @@ object UdashNavbar { final val Auto, FixedTop, FixedBottom, StickyTop: Value = new Position } - /** - * Creates a default, responsive navigation bar. - * More: Bootstrap Docs. + /** Creates a default, responsive navigation bar. More: Bootstrap Docs. * - * @param expandBreakpoint Screen size breakpoint to switch between collapsed and expanded menu. - * @param darkStyle If true, enables dark navigation bar theme. - * @param backgroundStyle Selects navigation bar background style. - * @param position Sets bar position. - * @param componentId An id of the root DOM node. - * @param navigationFactory Navigation component factory - if you want to clean up the created navigation on - * the navigation bar cleanup pass it to the provided interceptor. - * @param brand A brand DOM element. - * @tparam ItemType A single element's type in the `items` sequence. - * @tparam ElemType A type of a property containing an element in the `items` sequence. - * @return A `UdashNavbar` component, call `render` to create a DOM element. + * @param expandBreakpoint + * Screen size breakpoint to switch between collapsed and expanded menu. + * @param darkStyle + * If true, enables dark navigation bar theme. + * @param backgroundStyle + * Selects navigation bar background style. + * @param position + * Sets bar position. + * @param componentId + * An id of the root DOM node. + * @param navigationFactory + * Navigation component factory - if you want to clean up the created navigation on the navigation bar cleanup pass + * it to the provided interceptor. + * @param brand + * A brand DOM element. + * @tparam ItemType + * A single element's type in the `items` sequence. + * @tparam ElemType + * A type of a property containing an element in the `items` sequence. + * @return + * A `UdashNavbar` component, call `render` to create a DOM element. */ def apply[ItemType, ElemType <: ReadableProperty[ItemType]]( - expandBreakpoint: ReadableProperty[BootstrapStyles.ResponsiveBreakpoint] = BootstrapStyles.ResponsiveBreakpoint.Large.toProperty, + expandBreakpoint: ReadableProperty[BootstrapStyles.ResponsiveBreakpoint] = + BootstrapStyles.ResponsiveBreakpoint.Large.toProperty, darkStyle: ReadableProperty[Boolean] = UdashBootstrap.False, backgroundStyle: ReadableProperty[BootstrapStyles.Color] = BootstrapStyles.Color.Light.toProperty, position: ReadableProperty[UdashNavbar.Position] = Position.Auto.toProperty, - componentId: ComponentId = ComponentId.generate() + componentId: ComponentId = ComponentId.generate(), )( navigationFactory: Binding.NestedInterceptor => UdashNav[ItemType, ElemType], - brand: Modifier = () + brand: Modifier = (), ): UdashNavbar[ItemType, ElemType] = { import io.udash.css.CssView._ new UdashNavbar(expandBreakpoint, darkStyle, backgroundStyle, position, componentId)( interceptor => navigationFactory(interceptor).render.styles(BootstrapStyles.NavigationBar.nav), - brand + brand, ) } - /** - * Creates a default, responsive navigation bar. - * More: Bootstrap Docs. + /** Creates a default, responsive navigation bar. More: Bootstrap Docs. * - * @param expandBreakpoint Screen size breakpoint to switch between collapsed and expanded menu. - * @param darkStyle If true, enables dark navigation bar theme. - * @param backgroundStyle Selects navigation bar background style. - * @param position Sets bar position. - * @param componentId An id of the root DOM node. - * @param navigationFactory Navigation content factory - if you want to clean up the created navigation on - * the navigation bar cleanup pass it to the provided interceptor. - * The returned modifier is applied to the collapsible navigation container. - * Usually the modifier should be an `Element` or a sequence of `Element`s. - * @param brand A brand DOM element. - * @tparam ItemType A single element's type in the `items` sequence. - * @tparam ElemType A type of a property containing an element in the `items` sequence. - * @return A `UdashNavbar` component, call `render` to create a DOM element. + * @param expandBreakpoint + * Screen size breakpoint to switch between collapsed and expanded menu. + * @param darkStyle + * If true, enables dark navigation bar theme. + * @param backgroundStyle + * Selects navigation bar background style. + * @param position + * Sets bar position. + * @param componentId + * An id of the root DOM node. + * @param navigationFactory + * Navigation content factory - if you want to clean up the created navigation on the navigation bar cleanup pass + * it to the provided interceptor. The returned modifier is applied to the collapsible navigation container. + * Usually the modifier should be an `Element` or a sequence of `Element`s. + * @param brand + * A brand DOM element. + * @tparam ItemType + * A single element's type in the `items` sequence. + * @tparam ElemType + * A type of a property containing an element in the `items` sequence. + * @return + * A `UdashNavbar` component, call `render` to create a DOM element. */ def customContent[ItemType, ElemType <: ReadableProperty[ItemType]]( - expandBreakpoint: ReadableProperty[BootstrapStyles.ResponsiveBreakpoint] = BootstrapStyles.ResponsiveBreakpoint.Large.toProperty, + expandBreakpoint: ReadableProperty[BootstrapStyles.ResponsiveBreakpoint] = + BootstrapStyles.ResponsiveBreakpoint.Large.toProperty, darkStyle: ReadableProperty[Boolean] = UdashBootstrap.False, backgroundStyle: ReadableProperty[BootstrapStyles.Color] = BootstrapStyles.Color.Light.toProperty, position: ReadableProperty[UdashNavbar.Position] = Position.Auto.toProperty, - componentId: ComponentId = ComponentId.generate() + componentId: ComponentId = ComponentId.generate(), )( navigationFactory: Binding.NestedInterceptor => Modifier, - brand: Modifier = () - ): UdashNavbar[ItemType, ElemType] = { + brand: Modifier = (), + ): UdashNavbar[ItemType, ElemType] = new UdashNavbar(expandBreakpoint, darkStyle, backgroundStyle, position, componentId)(navigationFactory, brand) - } -} \ No newline at end of file +} diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/pagination/UdashPagination.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/pagination/UdashPagination.scala index a9ad0ade2..7fcf05958 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/pagination/UdashPagination.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/pagination/UdashPagination.scala @@ -9,17 +9,17 @@ import io.udash.properties.seq import org.scalajs.dom.{Element, Event} import scalatags.JsDom.all._ -final class UdashPagination[PageType, ElemType <: ReadableProperty[PageType]] private( +final class UdashPagination[PageType, ElemType <: ReadableProperty[PageType]] private ( pages: seq.ReadableSeqProperty[PageType, ElemType], selectedPageIdx: Property[Int], paginationSize: ReadableProperty[Option[BootstrapStyles.Size]], showArrows: ReadableProperty[Boolean], highlightActive: ReadableProperty[Boolean], - override val componentId: ComponentId + override val componentId: ComponentId, )( itemFactory: (ElemType, ReadableProperty[Int], Binding.NestedInterceptor) => Modifier, arrowFactory: (ElemType, UdashPagination.ArrowType, Binding.NestedInterceptor) => Modifier, - additionalListModifiers: Binding.NestedInterceptor => Modifier + additionalListModifiers: Binding.NestedInterceptor => Modifier, ) extends UdashBootstrapComponent { import io.udash.css.CssView._ @@ -33,12 +33,12 @@ final class UdashPagination[PageType, ElemType <: ReadableProperty[PageType]] pr } } - val selectedPage: ReadableProperty[PageType] = { + val selectedPage: ReadableProperty[PageType] = selectedPageIdx.combine(pages)((idx, pages) => pages(idx)) - } - /** Safely set selected page to the provided index. - * It will select first/last index if the provided value is out of bounds. */ + /** Safely set selected page to the provided index. It will select first/last index if the provided value is out of + * bounds. + */ def changePage(pageIdx: Int): Unit = { selectedPageIdx.set(math.min(pages.get.size - 1, math.max(0, pageIdx))) } @@ -52,37 +52,43 @@ final class UdashPagination[PageType, ElemType <: ReadableProperty[PageType]] pr override val render: Element = { import scalatags.JsDom.tags2 - tags2.nav( - ul( - componentId, BootstrapStyles.Pagination.pagination, - nestedInterceptor((BootstrapStyles.Pagination.size _).reactiveOptionApply(paginationSize)), - additionalListModifiers(nestedInterceptor) - )( - nestedInterceptor( - arrow((idx, _) => idx <= 0, previous _, UdashPagination.ArrowType.PreviousPage) - ), - nestedInterceptor( - repeatWithIndex(pages) { (page, idx, nested) => - li( - BootstrapStyles.Pagination.item, - nested(BootstrapStyles.active.styleIf( - selectedPageIdx.combine(idx)(_ == _).combine(highlightActive)(_ && _) - )) - )( - span(BootstrapStyles.Pagination.link)( - itemFactory(page, idx, nested) - ) - )(onclick :+= ((_: Event) => changePage(idx.get))).render - } - ), - nestedInterceptor( - arrow((idx, size) => idx >= size - 1, next _, UdashPagination.ArrowType.NextPage) + tags2 + .nav( + ul( + componentId, + BootstrapStyles.Pagination.pagination, + nestedInterceptor((BootstrapStyles.Pagination.size _).reactiveOptionApply(paginationSize)), + additionalListModifiers(nestedInterceptor), + )( + nestedInterceptor( + arrow((idx, _) => idx <= 0, previous _, UdashPagination.ArrowType.PreviousPage) + ), + nestedInterceptor( + repeatWithIndex(pages) { (page, idx, nested) => + li( + BootstrapStyles.Pagination.item, + nested( + BootstrapStyles.active.styleIf( + selectedPageIdx.combine(idx)(_ == _).combine(highlightActive)(_ && _) + ) + ), + )( + span(BootstrapStyles.Pagination.link)( + itemFactory(page, idx, nested) + ) + )(onclick :+= ((_: Event) => changePage(idx.get))).render + } + ), + nestedInterceptor( + arrow((idx, size) => idx >= size - 1, next _, UdashPagination.ArrowType.NextPage) + ), ) ) - ).render + .render } - protected def arrow(highlightCond: (Int, Int) => Boolean, onClick: () => Any, buttonType: UdashPagination.ArrowType): Binding = { + protected def arrow(highlightCond: (Int, Int) => Boolean, onClick: () => Any, buttonType: UdashPagination.ArrowType) + : Binding = { import scalatags.JsDom.all._ produceWithNested(showArrows) { @@ -90,9 +96,11 @@ final class UdashPagination[PageType, ElemType <: ReadableProperty[PageType]] pr val elements = pages.elemProperties li( BootstrapStyles.Pagination.item, - nested(BootstrapStyles.disabled.styleIf( - selectedPageIdx.combine(pages)((selected, pages) => highlightCond(selected, pages.size)) - )) + nested( + BootstrapStyles.disabled.styleIf( + selectedPageIdx.combine(pages)((selected, pages) => highlightCond(selected, pages.size)) + ) + ), )( nested(produceWithNested(selectedPageIdx) { (idx, nested) => span(BootstrapStyles.Pagination.link)( @@ -118,11 +126,11 @@ object UdashPagination { def defaultPageFactory[ElemType]: (ElemType, ReadableProperty[Int], Binding.NestedInterceptor) => Modifier = (_, idx, nested) => span(nested(bind(idx.transform(_ + 1)))) - /** - * Creates standard arrows. - * - * @param srTexts Optional properties for previous and next arrows aria.label texts. - */ + /** Creates standard arrows. + * + * @param srTexts + * Optional properties for previous and next arrows aria.label texts. + */ def defaultArrowFactory[ElemType]( srTexts: Option[(ReadableProperty[String], ReadableProperty[String])] = None ): (ElemType, UdashPagination.ArrowType, Binding.NestedInterceptor) => Modifier = @@ -132,28 +140,36 @@ object UdashPagination { else span(aria.label.bind(srTexts.map(_._2).getOrElse("Next".toProperty)))(span(aria.hidden := true)("»")) - /** - * Creates pagination component. - * More: Bootstrap Docs. + /** Creates pagination component. More: Bootstrap + * Docs. * - * @param pages Sequence of available pages. - * @param selectedPageIdx A property containing selected page index. - * @param paginationSize A pagination component size. - * @param showArrows If property value is true, shows next/prev page arrows. - * @param highlightActive If property value is true, highlights selected page. - * @param componentId An id of the root DOM node. - * @param itemFactory Creates button for each element in `pages`. - * The factory gets an element property and index as arguments. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param arrowFactory Creates button for the pagination arrows. - * The factory gets an element property and arrow type as arguments. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param additionalListModifiers Additional modifiers of the `ul` element. - * You can pass `BootstrapStyles.Flex.justifyContentCenter()` - * here to center the pagination component. - * @tparam PageType A single element's type in the `items` sequence. - * @tparam ElemType A type of a property containing an element in the `items` sequence. - * @return A `UdashPagination` component, call `render` to create a DOM element. + * @param pages + * Sequence of available pages. + * @param selectedPageIdx + * A property containing selected page index. + * @param paginationSize + * A pagination component size. + * @param showArrows + * If property value is true, shows next/prev page arrows. + * @param highlightActive + * If property value is true, highlights selected page. + * @param componentId + * An id of the root DOM node. + * @param itemFactory + * Creates button for each element in `pages`. The factory gets an element property and index as arguments. Use the + * provided interceptor to properly clean up bindings inside the content. + * @param arrowFactory + * Creates button for the pagination arrows. The factory gets an element property and arrow type as arguments. Use + * the provided interceptor to properly clean up bindings inside the content. + * @param additionalListModifiers + * Additional modifiers of the `ul` element. You can pass `BootstrapStyles.Flex.justifyContentCenter()` here to + * center the pagination component. + * @tparam PageType + * A single element's type in the `items` sequence. + * @tparam ElemType + * A type of a property containing an element in the `items` sequence. + * @return + * A `UdashPagination` component, call `render` to create a DOM element. */ def apply[PageType, ElemType <: ReadableProperty[PageType]]( pages: seq.ReadableSeqProperty[PageType, ElemType], @@ -161,14 +177,18 @@ object UdashPagination { paginationSize: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None, showArrows: ReadableProperty[Boolean] = UdashBootstrap.True, highlightActive: ReadableProperty[Boolean] = UdashBootstrap.True, - componentId: ComponentId = ComponentId.generate() + componentId: ComponentId = ComponentId.generate(), )( itemFactory: (ElemType, ReadableProperty[Int], Binding.NestedInterceptor) => Modifier = defaultPageFactory, arrowFactory: (ElemType, UdashPagination.ArrowType, Binding.NestedInterceptor) => Modifier = defaultArrowFactory(), - additionalListModifiers: Binding.NestedInterceptor => Modifier = _ => () - ): UdashPagination[PageType, ElemType] = { + additionalListModifiers: Binding.NestedInterceptor => Modifier = _ => (), + ): UdashPagination[PageType, ElemType] = new UdashPagination( - pages, selectedPageIdx, paginationSize, showArrows, highlightActive, componentId + pages, + selectedPageIdx, + paginationSize, + showArrows, + highlightActive, + componentId, )(itemFactory, arrowFactory, additionalListModifiers) - } -} \ No newline at end of file +} diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/progressbar/UdashProgressBar.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/progressbar/UdashProgressBar.scala index fa733c1bb..f0b7cef30 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/progressbar/UdashProgressBar.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/progressbar/UdashProgressBar.scala @@ -7,7 +7,7 @@ import io.udash.bootstrap.utils.{BootstrapStyles, UdashBootstrapComponent} import org.scalajs.dom.Element import scalatags.JsDom.all._ -final class UdashProgressBar private[progressbar]( +final class UdashProgressBar private[progressbar] ( progress: ReadableProperty[Int], showPercentage: ReadableProperty[Boolean], barStyle: ReadableProperty[Option[BootstrapStyles.Color]], @@ -16,17 +16,25 @@ final class UdashProgressBar private[progressbar]( minValue: ReadableProperty[Int], maxValue: ReadableProperty[Int], minWidthEm: ReadableProperty[Int], - override val componentId: ComponentId -)(labelFactory: (ReadableProperty[Int], ReadableProperty[Int], ReadableProperty[Int], Binding.NestedInterceptor) => Modifier) extends UdashBootstrapComponent { + override val componentId: ComponentId, +)( + labelFactory: ( + ReadableProperty[Int], + ReadableProperty[Int], + ReadableProperty[Int], + Binding.NestedInterceptor, + ) => Modifier +) extends UdashBootstrapComponent { import io.udash.css.CssView._ protected def barModifiers: Seq[Modifier] = Seq[Modifier]( - role := "progressbar", BootstrapStyles.ProgressBar.progressBar, + role := "progressbar", + BootstrapStyles.ProgressBar.progressBar, nestedInterceptor( width.bind( - progress.combine(minValue.combine(maxValue)((_, _)))((_, _)).transform { - case (current, (min, max)) => s"${100 * (current - min) / (max - min)}%" + progress.combine(minValue.combine(maxValue)((_, _)))((_, _)).transform { case (current, (min, max)) => + s"${100 * (current - min) / (max - min)}%" } ) ), @@ -36,10 +44,10 @@ final class UdashProgressBar private[progressbar]( nestedInterceptor(minWidth.bind(minWidthEm.transform(v => s"${v}em"))), nestedInterceptor((BootstrapStyles.Background.color _).reactiveOptionApply(barStyle)), nestedInterceptor(BootstrapStyles.ProgressBar.animated.styleIf(animated)), - nestedInterceptor(BootstrapStyles.ProgressBar.striped.styleIf(stripped)) + nestedInterceptor(BootstrapStyles.ProgressBar.striped.styleIf(stripped)), ) - override val render: Element = { + override val render: Element = div(BootstrapStyles.ProgressBar.progress)( div(componentId, barModifiers)( nestedInterceptor( @@ -50,60 +58,82 @@ final class UdashProgressBar private[progressbar]( ) ) ).render - } } object UdashProgressBar { - val ToStringValueStringifier: (ReadableProperty[Int], ReadableProperty[Int], ReadableProperty[Int], Binding.NestedInterceptor) => Modifier = + val ToStringValueStringifier + : (ReadableProperty[Int], ReadableProperty[Int], ReadableProperty[Int], Binding.NestedInterceptor) => Modifier = (p, _, _, nested) => nested(bind(p.transform(_.toString))) /** Default method of converting progress to string. */ - val PercentValueStringifier: (ReadableProperty[Int], ReadableProperty[Int], ReadableProperty[Int], Binding.NestedInterceptor) => Modifier = { + val PercentValueStringifier + : (ReadableProperty[Int], ReadableProperty[Int], ReadableProperty[Int], Binding.NestedInterceptor) => Modifier = { (progress, min, max, nested) => - nested(bind( - progress.combine(min.combine(max)((_, _)))((_, _)).transform { - case (current, (min, max)) => + nested( + bind( + progress.combine(min.combine(max)((_, _)))((_, _)).transform { case (current, (min, max)) => val pct = 100 * (current - min) / (max - min) s"$pct%" - } - )) + } + ) + ) } - /** - * Creates a progress bar component. - * More: Bootstrap Docs. + /** Creates a progress bar component. More: Bootstrap + * Docs. * - * @param progress Property containing Integer in range from `minValue` to `maxValue`. - * @param showPercentage If true, display progress string. - * @param barStyle A bar color. One of the standard bootstrap colors `BootstrapStyles.Color`. - * @param stripped If true, applies striped style to the bar. - * @param animated If true, applies animation style to the bar. - * @param minValue A minimum progress value. - * @param maxValue A maximum progress value. - * @param minWidth A minimal width of the progress indicator. - * @param componentId An id of the root DOM node. - * @param labelFactory Creates a label content from current progress, min value, max value. - * Use the provided interceptor to properly clean up bindings inside the content. - * @return A `UdashProgressBar` component, call `render` to create a DOM element. + * @param progress + * Property containing Integer in range from `minValue` to `maxValue`. + * @param showPercentage + * If true, display progress string. + * @param barStyle + * A bar color. One of the standard bootstrap colors `BootstrapStyles.Color`. + * @param stripped + * If true, applies striped style to the bar. + * @param animated + * If true, applies animation style to the bar. + * @param minValue + * A minimum progress value. + * @param maxValue + * A maximum progress value. + * @param minWidth + * A minimal width of the progress indicator. + * @param componentId + * An id of the root DOM node. + * @param labelFactory + * Creates a label content from current progress, min value, max value. Use the provided interceptor to properly + * clean up bindings inside the content. + * @return + * A `UdashProgressBar` component, call `render` to create a DOM element. */ def apply( progress: ReadableProperty[Int], showPercentage: ReadableProperty[Boolean] = UdashBootstrap.False, - barStyle: ReadableProperty[Option[BootstrapStyles.Color]] = UdashBootstrap.None, + barStyle: ReadableProperty[Option[BootstrapStyles.Color]] = UdashBootstrap.None, stripped: ReadableProperty[Boolean] = UdashBootstrap.False, animated: ReadableProperty[Boolean] = UdashBootstrap.False, minValue: ReadableProperty[Int] = 0.toProperty, maxValue: ReadableProperty[Int] = 100.toProperty, minWidth: ReadableProperty[Int] = 1.toProperty, - componentId: ComponentId = ComponentId.generate() + componentId: ComponentId = ComponentId.generate(), )( - labelFactory: (ReadableProperty[Int], ReadableProperty[Int], ReadableProperty[Int], Binding.NestedInterceptor) => Modifier = - PercentValueStringifier - ): UdashProgressBar = { + labelFactory: ( + ReadableProperty[Int], + ReadableProperty[Int], + ReadableProperty[Int], + Binding.NestedInterceptor, + ) => Modifier = PercentValueStringifier + ): UdashProgressBar = new UdashProgressBar( - progress, showPercentage, barStyle, stripped, animated, - minValue, maxValue, minWidth, componentId + progress, + showPercentage, + barStyle, + stripped, + animated, + minValue, + maxValue, + minWidth, + componentId, )(labelFactory) - } } diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/table/UdashTable.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/table/UdashTable.scala index 99b9f20f7..436d0c124 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/table/UdashTable.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/table/UdashTable.scala @@ -8,7 +8,7 @@ import io.udash.properties.seq import org.scalajs.dom._ import scalatags.JsDom.all._ -final class UdashTable[ItemType, ElemType <: ReadableProperty[ItemType]] private( +final class UdashTable[ItemType, ElemType <: ReadableProperty[ItemType]] private ( items: seq.ReadableSeqProperty[ItemType, ElemType], responsive: ReadableProperty[Option[BootstrapStyles.ResponsiveBreakpoint]], dark: ReadableProperty[Boolean], @@ -17,16 +17,16 @@ final class UdashTable[ItemType, ElemType <: ReadableProperty[ItemType]] private borderless: ReadableProperty[Boolean], hover: ReadableProperty[Boolean], small: ReadableProperty[Boolean], - override val componentId: ComponentId + override val componentId: ComponentId, )( captionFactory: Option[Binding.NestedInterceptor => Modifier], headerFactory: Option[Binding.NestedInterceptor => Modifier], - rowFactory: (ElemType, Binding.NestedInterceptor) => Element + rowFactory: (ElemType, Binding.NestedInterceptor) => Element, ) extends UdashBootstrapComponent { import io.udash.css.CssView._ - override val render: Element = { + override val render: Element = div( nestedInterceptor((BootstrapStyles.Table.responsive _).reactiveOptionApply(responsive)), table( @@ -37,7 +37,7 @@ final class UdashTable[ItemType, ElemType <: ReadableProperty[ItemType]] private nestedInterceptor(BootstrapStyles.Table.bordered.styleIf(bordered)), nestedInterceptor(BootstrapStyles.Table.borderless.styleIf(borderless)), nestedInterceptor(BootstrapStyles.Table.hover.styleIf(hover)), - nestedInterceptor(BootstrapStyles.Table.small.styleIf(small)) + nestedInterceptor(BootstrapStyles.Table.small.styleIf(small)), )( captionFactory.map(content => caption(content(nestedInterceptor)).render), headerFactory.map(head => thead(head(nestedInterceptor)).render), @@ -47,56 +47,73 @@ final class UdashTable[ItemType, ElemType <: ReadableProperty[ItemType]] private rowFactory(item, nested) } ) - ) - ) + ), + ), ).render - } } object UdashTable { - /** - * Creates a table component. - * More: Bootstrap Docs. + /** Creates a table component. More: Bootstrap Docs. * - * @param items Elements which will be rendered as the table rows. - * @param responsive If defined, the table will be horizontally scrollable on selected screen size. - * @param dark Switch table to the dark theme. - * @param striped Turn on zebra-striping. - * @param bordered Add vertical borders. - * @param borderless Removes all borders. - * @param hover Highlight row on hover. - * @param small Makes table more compact. - * @param componentId An id of the root DOM node. - * @param rowFactory Creates row representation of the table element - it should create the `tr` tag. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param headerFactory Creates table header - it should create the `tr` tag. - * Table without header will be rendered if `None` passed. - * Use the provided interceptor to properly clean up bindings inside the content. - * @param captionFactory Creates table caption - the result will be wrapped into the `caption` tag. - * Table without caption will be rendered if `None` passed. - * Use the provided interceptor to properly clean up bindings inside the content. - * @tparam ItemType A single element's type in the `items` sequence. - * @tparam ElemType A type of a property containing an element in the `items` sequence. - * @return A `UdashTable` component, call `render` to create a DOM element. + * @param items + * Elements which will be rendered as the table rows. + * @param responsive + * If defined, the table will be horizontally scrollable on selected screen size. + * @param dark + * Switch table to the dark theme. + * @param striped + * Turn on zebra-striping. + * @param bordered + * Add vertical borders. + * @param borderless + * Removes all borders. + * @param hover + * Highlight row on hover. + * @param small + * Makes table more compact. + * @param componentId + * An id of the root DOM node. + * @param rowFactory + * Creates row representation of the table element - it should create the `tr` tag. Use the provided interceptor to + * properly clean up bindings inside the content. + * @param headerFactory + * Creates table header - it should create the `tr` tag. Table without header will be rendered if `None` passed. + * Use the provided interceptor to properly clean up bindings inside the content. + * @param captionFactory + * Creates table caption - the result will be wrapped into the `caption` tag. Table without caption will be + * rendered if `None` passed. Use the provided interceptor to properly clean up bindings inside the content. + * @tparam ItemType + * A single element's type in the `items` sequence. + * @tparam ElemType + * A type of a property containing an element in the `items` sequence. + * @return + * A `UdashTable` component, call `render` to create a DOM element. */ def apply[ItemType, ElemType <: ReadableProperty[ItemType]]( items: seq.ReadableSeqProperty[ItemType, ElemType], - responsive: ReadableProperty[Option[BootstrapStyles.ResponsiveBreakpoint]] = UdashBootstrap.None, + responsive: ReadableProperty[Option[BootstrapStyles.ResponsiveBreakpoint]] = UdashBootstrap.None, dark: ReadableProperty[Boolean] = UdashBootstrap.False, striped: ReadableProperty[Boolean] = UdashBootstrap.False, bordered: ReadableProperty[Boolean] = UdashBootstrap.False, borderless: ReadableProperty[Boolean] = UdashBootstrap.False, hover: ReadableProperty[Boolean] = UdashBootstrap.False, small: ReadableProperty[Boolean] = UdashBootstrap.False, - componentId: ComponentId = ComponentId.generate() + componentId: ComponentId = ComponentId.generate(), )( rowFactory: (ElemType, Binding.NestedInterceptor) => Element, headerFactory: Option[Binding.NestedInterceptor => Modifier] = None, - captionFactory: Option[Binding.NestedInterceptor => Modifier] = None - ): UdashTable[ItemType, ElemType] = { + captionFactory: Option[Binding.NestedInterceptor => Modifier] = None, + ): UdashTable[ItemType, ElemType] = new UdashTable( - items, responsive, dark, striped, bordered, borderless, hover, small, componentId + items, + responsive, + dark, + striped, + bordered, + borderless, + hover, + small, + componentId, )(captionFactory, headerFactory, rowFactory) - } } diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/tooltip/TooltipEvent.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/tooltip/TooltipEvent.scala index 73d7af0ce..bde2cf478 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/tooltip/TooltipEvent.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/tooltip/TooltipEvent.scala @@ -5,22 +5,33 @@ import com.avsystem.commons.misc.{AbstractCase, AbstractValueEnum, AbstractValue final case class TooltipEvent( override val source: Tooltip, - tpe: TooltipEvent.EventType -) extends AbstractCase with ListenableEvent + tpe: TooltipEvent.EventType, +) extends AbstractCase + with ListenableEvent object TooltipEvent { + /** More: Bootstrap Docs */ final class EventType(implicit enumCtx: EnumCtx) extends AbstractValueEnum object EventType extends AbstractValueEnumCompanion[EventType] { + /** This event fires immediately when the show instance method is called. */ final val Show: Value = new EventType - /** This event is fired when the tooltip has been made visible to the user (will wait for CSS transitions to complete). */ + + /** This event is fired when the tooltip has been made visible to the user (will wait for CSS transitions to + * complete). + */ final val Shown: Value = new EventType + /** This event is fired immediately when the hide instance method has been called. */ final val Hide: Value = new EventType - /** This event is fired when the tooltip has finished being hidden from the user (will wait for CSS transitions to complete). */ + + /** This event is fired when the tooltip has finished being hidden from the user (will wait for CSS transitions to + * complete). + */ final val Hidden: Value = new EventType + /** This event is fired after the show.bs.tooltip event when the tooltip template has been added to the DOM. */ final val Inserted: Value = new EventType } -} \ No newline at end of file +} diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/tooltip/TooltipUtils.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/tooltip/TooltipUtils.scala index ebca41ff3..41b2a02b9 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/tooltip/TooltipUtils.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/tooltip/TooltipUtils.scala @@ -23,8 +23,9 @@ trait Tooltip extends Listenable { /** Toggles tooltip visibility. */ def toggle(): Unit - /** Hides and destroys an element's popover. - * Check Bootstrap Docs for more details. */ + /** Hides and destroys an element's popover. Check Bootstrap Docs for more details. + */ def destroy(): Unit private[tooltip] def on(selector: JQuery, event: EventName, callback: JQueryCallback): Unit = { @@ -78,37 +79,50 @@ abstract class TooltipUtils[TooltipType <: Tooltip] { final val Manual: Value = new Trigger("manual") } - /** - * Adds tooltip/popover to provided element. - * More: Bootstrap Docs (Tooltip). - * More: Bootstrap Docs (Popover). + /** Adds tooltip/popover to provided element. More: Bootstrap Docs (Tooltip). More: Bootstrap Docs (Popover). * - * @param animation Apply a CSS fade transition to the popover. - * @param boundary Keeps the popover within the bounds of this element. - * @param container Appends the popover to a specific element. - * @param content Popover content. - * @param delay Show/hide delay. - * @param html Treat content and title as HTML. - * @param offset Offset of the popover relative to its target. - * @param placement Tooltip/popover placement. - * @param template Tooltip/popover template. - * @param title Component title. - * @param trigger Triggers to show/hide tooltip. - * @param el Node which will own the created tooltip/popover. + * @param animation + * Apply a CSS fade transition to the popover. + * @param boundary + * Keeps the popover within the bounds of this element. + * @param container + * Appends the popover to a specific element. + * @param content + * Popover content. + * @param delay + * Show/hide delay. + * @param html + * Treat content and title as HTML. + * @param offset + * Offset of the popover relative to its target. + * @param placement + * Tooltip/popover placement. + * @param template + * Tooltip/popover template. + * @param title + * Component title. + * @param trigger + * Triggers to show/hide tooltip. + * @param el + * Node which will own the created tooltip/popover. */ def apply( animation: Boolean = true, boundary: String | dom.Node = "scrollParent", container: Option[String | dom.Node] = None, content: js.Function1[dom.Node, String] | dom.Node = io.udash.emptyStringNode(), - delay: Delay | Long = Delay(0 millis, 0 millis), + delay: Delay | Long = Delay(0.millis, 0.millis), html: Boolean = false, offset: Int | String = "0", placement: Placement = defaultPlacement, template: Option[String] = None, title: String | js.Function1[dom.Node, String] | dom.Node = "", - trigger: Seq[Trigger] = defaultTrigger - )(el: dom.Node): TooltipType = + trigger: Seq[Trigger] = defaultTrigger, + )( + el: dom.Node + ): TooltipType = initTooltip( js.Dictionary( "animation" -> animation, @@ -121,7 +135,7 @@ abstract class TooltipUtils[TooltipType <: Tooltip] { "placement" -> placement.jsValue, "template" -> template.getOrElse(defaultTemplate), "title" -> title, - "trigger" -> trigger.map(_.jsValue).mkString(" ") + "trigger" -> trigger.map(_.jsValue).mkString(" "), ) )(el) diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/tooltip/UdashPopover.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/tooltip/UdashPopover.scala index 3d237a0d8..e21b1e58d 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/tooltip/UdashPopover.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/tooltip/UdashPopover.scala @@ -22,8 +22,9 @@ final class UdashPopover(selector: UdashPopover.UdashPopoverJQuery) extends Tool def toggle(): Unit = selector.popover("toggle") - /** Hides and destroys an element's popover. - * Check Bootstrap Docs for more details. */ + /** Hides and destroys an element's popover. Check Bootstrap Docs for more details. + */ def destroy(): Unit = { off(selector) selector.popover("dispose") @@ -33,10 +34,18 @@ final class UdashPopover(selector: UdashPopover.UdashPopoverJQuery) extends Tool selector.popover("setContent") on(selector, "show.bs.popover", (_: Element, _: JQueryEvent) => fire(TooltipEvent(this, TooltipEvent.EventType.Show))) - on(selector,"shown.bs.popover", (_: Element, _: JQueryEvent) => fire(TooltipEvent(this, TooltipEvent.EventType.Shown))) - on(selector,"hide.bs.popover", (_: Element, _: JQueryEvent) => fire(TooltipEvent(this, TooltipEvent.EventType.Hide))) - on(selector,"hidden.bs.popover", (_: Element, _: JQueryEvent) => fire(TooltipEvent(this, TooltipEvent.EventType.Hidden))) - on(selector,"inserted.bs.popover", (_: Element, _: JQueryEvent) => fire(TooltipEvent(this, TooltipEvent.EventType.Inserted))) + on(selector, "shown.bs.popover", (_: Element, _: JQueryEvent) => fire(TooltipEvent(this, TooltipEvent.EventType.Shown))) + on(selector, "hide.bs.popover", (_: Element, _: JQueryEvent) => fire(TooltipEvent(this, TooltipEvent.EventType.Hide))) + on( + selector, + "hidden.bs.popover", + (_: Element, _: JQueryEvent) => fire(TooltipEvent(this, TooltipEvent.EventType.Hidden)), + ) + on( + selector, + "inserted.bs.popover", + (_: Element, _: JQueryEvent) => fire(TooltipEvent(this, TooltipEvent.EventType.Inserted)), + ) } object UdashPopover extends TooltipUtils[UdashPopover] { @@ -53,7 +62,7 @@ object UdashPopover extends TooltipUtils[UdashPopover] { div(BootstrapStyles.Popover.popover, role := "tooltip")( div(BootstrapStyles.arrow), h3(BootstrapStyles.Popover.header), - div(BootstrapStyles.Popover.body) + div(BootstrapStyles.Popover.body), ).render } override protected val defaultTrigger: Seq[Trigger] = Seq(Trigger.Click) diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/tooltip/UdashTooltip.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/tooltip/UdashTooltip.scala index 999ed1d38..7d125799c 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/tooltip/UdashTooltip.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/tooltip/UdashTooltip.scala @@ -8,7 +8,7 @@ import org.scalajs.dom.Element import scala.scalajs.js -final class UdashTooltip private(selector: UdashTooltip.UdashTooltipJQuery) extends Tooltip { +final class UdashTooltip private (selector: UdashTooltip.UdashTooltipJQuery) extends Tooltip { /** Shows the tooltip. */ def show(): Unit = @@ -22,8 +22,9 @@ final class UdashTooltip private(selector: UdashTooltip.UdashTooltipJQuery) exte def toggle(): Unit = selector.tooltip("toggle") - /** Hides and destroys an element's popover. - * Check Bootstrap Docs for more details. */ + /** Hides and destroys an element's popover. Check Bootstrap Docs for more details. + */ def destroy(): Unit = { off(selector) selector.tooltip("dispose") @@ -32,11 +33,19 @@ final class UdashTooltip private(selector: UdashTooltip.UdashTooltipJQuery) exte private[tooltip] def reloadContent(): Unit = selector.tooltip("setContent") - on(selector,"show.bs.tooltip", (_: Element, _: JQueryEvent) => fire(TooltipEvent(this, TooltipEvent.EventType.Show))) - on(selector,"shown.bs.tooltip", (_: Element, _: JQueryEvent) => fire(TooltipEvent(this, TooltipEvent.EventType.Shown))) - on(selector,"hide.bs.tooltip", (_: Element, _: JQueryEvent) => fire(TooltipEvent(this, TooltipEvent.EventType.Hide))) - on(selector,"hidden.bs.tooltip", (_: Element, _: JQueryEvent) => fire(TooltipEvent(this, TooltipEvent.EventType.Hidden))) - on(selector,"inserted.bs.tooltip", (_: Element, _: JQueryEvent) => fire(TooltipEvent(this, TooltipEvent.EventType.Inserted))) + on(selector, "show.bs.tooltip", (_: Element, _: JQueryEvent) => fire(TooltipEvent(this, TooltipEvent.EventType.Show))) + on(selector, "shown.bs.tooltip", (_: Element, _: JQueryEvent) => fire(TooltipEvent(this, TooltipEvent.EventType.Shown))) + on(selector, "hide.bs.tooltip", (_: Element, _: JQueryEvent) => fire(TooltipEvent(this, TooltipEvent.EventType.Hide))) + on( + selector, + "hidden.bs.tooltip", + (_: Element, _: JQueryEvent) => fire(TooltipEvent(this, TooltipEvent.EventType.Hidden)), + ) + on( + selector, + "inserted.bs.tooltip", + (_: Element, _: JQueryEvent) => fire(TooltipEvent(this, TooltipEvent.EventType.Inserted)), + ) } object UdashTooltip extends TooltipUtils[UdashTooltip] { @@ -52,7 +61,7 @@ object UdashTooltip extends TooltipUtils[UdashTooltip] { import scalatags.Text.all._ div(BootstrapStyles.Tooltip.tooltip, role := "tooltip")( div(BootstrapStyles.Tooltip.arrow), - div(BootstrapStyles.Tooltip.inner) + div(BootstrapStyles.Tooltip.inner), ).render } override protected val defaultTrigger: Seq[Trigger] = Seq(Trigger.Hover, Trigger.Focus) diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/utils/BootstrapImplicits.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/utils/BootstrapImplicits.scala index d7acb659e..464432b7b 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/utils/BootstrapImplicits.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/utils/BootstrapImplicits.scala @@ -8,10 +8,11 @@ import scalatags.JsDom.GenericAttr import scalatags.JsDom.all._ trait BootstrapImplicits { - implicit val urlAttrValue: AttrValue[Url] = (t: Element, a: JsDom.Attr, v: Url) => new GenericAttr[String].apply(t, a, v.value) + implicit val urlAttrValue: AttrValue[Url] = (t: Element, a: JsDom.Attr, v: Url) => + new GenericAttr[String].apply(t, a, v.value) implicit def withoutNested(modifier: Modifier): Binding.NestedInterceptor => Modifier = _ => modifier implicit def stringWithoutNested(modifier: String): Binding.NestedInterceptor => Modifier = _ => modifier } -object BootstrapImplicits extends BootstrapImplicits \ No newline at end of file +object BootstrapImplicits extends BootstrapImplicits diff --git a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/utils/BootstrapStyles.scala b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/utils/BootstrapStyles.scala index 4bb852a9e..054d33432 100644 --- a/bootstrap4/.js/src/main/scala/io/udash/bootstrap/utils/BootstrapStyles.scala +++ b/bootstrap4/.js/src/main/scala/io/udash/bootstrap/utils/BootstrapStyles.scala @@ -231,7 +231,10 @@ trait BootstrapStyles { def wrapReverse(breakpoint: ResponsiveBreakpoint = ResponsiveBreakpoint.All) = CssStyleName(s"flex${breakpoint.classMarker}-wrap-reverse") - def justifyContent(justification: FlexContentJustification, breakpoint: ResponsiveBreakpoint = ResponsiveBreakpoint.All) = + def justifyContent( + justification: FlexContentJustification, + breakpoint: ResponsiveBreakpoint = ResponsiveBreakpoint.All, + ) = CssStyleName(s"justify-content${breakpoint.classMarker}${justification.classMarker}") def alignItems(align: FlexAlign, breakpoint: ResponsiveBreakpoint = ResponsiveBreakpoint.All) = @@ -500,10 +503,18 @@ trait BootstrapStyles { } object Spacing { - def margin(side: Side = Side.All, breakpoint: ResponsiveBreakpoint = ResponsiveBreakpoint.All, size: SpacingSize = SpacingSize.Normal) = + def margin( + side: Side = Side.All, + breakpoint: ResponsiveBreakpoint = ResponsiveBreakpoint.All, + size: SpacingSize = SpacingSize.Normal, + ) = CssStyleName(s"m${side.classMarker}${breakpoint.classMarker}${size.classMarker}") - def padding(side: Side = Side.All, breakpoint: ResponsiveBreakpoint = ResponsiveBreakpoint.All, size: SpacingSize = SpacingSize.Normal) = + def padding( + side: Side = Side.All, + breakpoint: ResponsiveBreakpoint = ResponsiveBreakpoint.All, + size: SpacingSize = SpacingSize.Normal, + ) = CssStyleName(s"p${side.classMarker}${breakpoint.classMarker}${size.classMarker}") } @@ -619,25 +630,24 @@ object BootstrapStyles extends BootstrapStyles { final val Print: Value = new ResponsiveBreakpoint("-print") } - final class Side(val classMarker: String, val longClassMarker: String)(implicit enumCtx: EnumCtx) extends AbstractValueEnum + final class Side(val classMarker: String, val longClassMarker: String)(implicit enumCtx: EnumCtx) + extends AbstractValueEnum object Side extends AbstractValueEnumCompanion[Side] { final val All: Value = new Side("", "") final val Top: Value = new Side("t", "-top") final val Bottom: Value = new Side("b", "-bottom") final val Left: Value = new Side("l", "-left") final val Right: Value = new Side("r", "-right") - /** - * This value is not always supported by Bootstrap. - * It works with spacing, but you cannot use it with borders methods.
- * More: Spacing Docs and - * Borders Docs + + /** This value is not always supported by Bootstrap. It works with spacing, but you cannot use it with borders + * methods.
More: Spacing Docs and Borders Docs */ final val X: Value = new Side("x", "") - /** - * This value is not always supported by Bootstrap. - * It works with spacing, but you cannot use it with borders methods.
- * More: Spacing Docs and - * Borders Docs + + /** This value is not always supported by Bootstrap. It works with spacing, but you cannot use it with borders + * methods.
More: Spacing Docs and Borders Docs */ final val Y: Value = new Side("y", "") } @@ -660,6 +670,7 @@ object BootstrapStyles extends BootstrapStyles { final val Light: Value = new Color("-light") final val Dark: Value = new Color("-dark") final val White: Value = new Color("-white") + /** This value is only supported in buttons. */ final val Link: Value = new Color("-link") } diff --git a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/BootstrapImplicitsTest.scala b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/BootstrapImplicitsTest.scala index bd27e41f6..0f7218601 100644 --- a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/BootstrapImplicitsTest.scala +++ b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/BootstrapImplicitsTest.scala @@ -37,8 +37,6 @@ class BootstrapImplicitsTest extends UdashCoreFrontendTest { implicit class ElemOps(elem: Element) { def hasStyles(styles: CssStyleName*): Boolean = - styles - .map(_.className) - .forall(elem.classList.contains) + styles.map(_.className).forall(elem.classList.contains) } -} \ No newline at end of file +} diff --git a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/breadcrumb/UdashBreadcrumbsTest.scala b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/breadcrumb/UdashBreadcrumbsTest.scala index d3323dca0..f5026bbd1 100644 --- a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/breadcrumb/UdashBreadcrumbsTest.scala +++ b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/breadcrumb/UdashBreadcrumbsTest.scala @@ -14,14 +14,15 @@ class UdashBreadcrumbsTest extends UdashCoreFrontendTest { val pages = SeqProperty( new CustomBreadcrumb("A", "id1"), new CustomBreadcrumb("B", "id2"), - new CustomBreadcrumb("C", "id3") + new CustomBreadcrumb("C", "id3"), ) val breadcrumbs = UdashBreadcrumbs(pages)( - itemFactory = (item, nested) => span( - nested(id.bind(item.transform(_.id))), - nested(bind(item.transform(_.name))) - ).render, - isActive = _.id.charAt(2).toInt % 2 == 0 + itemFactory = (item, nested) => + span( + nested(id.bind(item.transform(_.id))), + nested(bind(item.transform(_.name))), + ).render, + isActive = _.id.charAt(2).toInt % 2 == 0, ) val el = breadcrumbs.render @@ -55,7 +56,7 @@ class UdashBreadcrumbsTest extends UdashCoreFrontendTest { val pages = SeqProperty( new UdashBreadcrumbs.Breadcrumb("Home", Url("https://udash.io/")), new UdashBreadcrumbs.Breadcrumb("Guide", Url("https://guide.udash.io/")), - new UdashBreadcrumbs.Breadcrumb("RPC", Url("https://guide.udash.io/rpc")) + new UdashBreadcrumbs.Breadcrumb("RPC", Url("https://guide.udash.io/rpc")), ) val breadcrumbs = UdashBreadcrumbs.default(pages)() val el = breadcrumbs.render diff --git a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/button/UdashButtonGroupTest.scala b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/button/UdashButtonGroupTest.scala index 8bcf0f5ac..cb139667b 100644 --- a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/button/UdashButtonGroupTest.scala +++ b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/button/UdashButtonGroupTest.scala @@ -13,7 +13,7 @@ class UdashButtonGroupTest extends UdashCoreFrontendTest { val buttons = UdashButtonGroup()( UdashButton()("Button 1").render, UdashButton()("Button 2").render, - UdashButton()("Button 3").render + UdashButton()("Button 3").render, ) val el = buttons.render diff --git a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/button/UdashButtonTest.scala b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/button/UdashButtonTest.scala index 55116c0b3..d8a592770 100644 --- a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/button/UdashButtonTest.scala +++ b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/button/UdashButtonTest.scala @@ -84,7 +84,11 @@ class UdashButtonTest extends UdashCoreFrontendTest { val active: Property[Boolean] = Property(false) val disabled: Property[Boolean] = Property(false) - val btn = UdashButton(active = active, disabled = disabled, options = UdashButtonOptions(color = buttonStyle, size = size, outline = outline, block = block))("btn") + val btn = UdashButton( + active = active, + disabled = disabled, + options = UdashButtonOptions(color = buttonStyle, size = size, outline = outline, block = block), + )("btn") val el = btn.render el.classList.length should be(4) el.classList should contain(BootstrapStyles.Button.btn.className) @@ -111,7 +115,6 @@ class UdashButtonTest extends UdashCoreFrontendTest { el.classList should contain(BootstrapStyles.Button.size(BootstrapStyles.Size.Large).className) el.classList should contain(BootstrapStyles.Button.color(BootstrapStyles.Color.Secondary).className) - btn.kill() active.listenersCount() should be(0) disabled.listenersCount() should be(0) @@ -128,7 +131,7 @@ class UdashButtonTest extends UdashCoreFrontendTest { val btn = UdashButton( active = active, disabled = disabled, - options = UdashButtonOptions(color = buttonStyle, size = size, outline = outline, block = block) + options = UdashButtonOptions(color = buttonStyle, size = size, outline = outline, block = block), )("btn") val el = btn.render el.classList.length should be(4) diff --git a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/card/UdashCardTest.scala b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/card/UdashCardTest.scala index 05bd00f8e..cee22fe4b 100644 --- a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/card/UdashCardTest.scala +++ b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/card/UdashCardTest.scala @@ -16,13 +16,18 @@ class UdashCardTest extends UdashCoreFrontendTest { val textAlignment: Property[Option[BootstrapStyles.Align]] = Property(None) val textColor: Property[Option[BootstrapStyles.Color]] = Property(None) val card = UdashCard( - backgroundColor, borderColor, textAlignment, textColor - ) { factory => Seq( - factory.header(nested => nested(bind(headText))), - factory.body { nested => - factory.body(_ => nested(bind(bodyText))) - } - )} + backgroundColor, + borderColor, + textAlignment, + textColor, + ) { factory => + Seq( + factory.header(nested => nested(bind(headText))), + factory.body { nested => + factory.body(_ => nested(bind(bodyText))) + }, + ) + } val el = card.render el.childNodes.length should be(2) el.textContent should be("headbody") diff --git a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/carousel/UdashCarouselTest.scala b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/carousel/UdashCarouselTest.scala index 8de67118c..890145e63 100644 --- a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/carousel/UdashCarouselTest.scala +++ b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/carousel/UdashCarouselTest.scala @@ -49,7 +49,7 @@ class UdashCarouselTest extends AsyncUdashCoreFrontendTest { val carousel = UdashCarousel.default( sl, activeSlide = Property(3), - animationOptions = UdashCarousel.AnimationOptions(active = false).toProperty + animationOptions = UdashCarousel.AnimationOptions(active = false).toProperty, )() jQ("body").append(carousel.render) carousel.activeSlide.get shouldBe 3 @@ -64,7 +64,7 @@ class UdashCarouselTest extends AsyncUdashCoreFrontendTest { val carousel = UdashCarousel.default( sl, activeSlide = activeSlide, - animationOptions = options + animationOptions = options, )() jQ("body").append(carousel.render) @@ -74,15 +74,15 @@ class UdashCarouselTest extends AsyncUdashCoreFrontendTest { options.listenersCount() should be(0) } - //For some reason, the 3 tests below have to be independent (can't first goTo() then nextSlide() without timeout) - //This does not seem to cause any problems with carousels in the wild (outside tests). + // For some reason, the 3 tests below have to be independent (can't first goTo() then nextSlide() without timeout) + // This does not seem to cause any problems with carousels in the wild (outside tests). "go to slide in" in { val sl = slides() val carousel = UdashCarousel.default( sl, activeSlide = Property(1), - animationOptions = UdashCarousel.AnimationOptions(active = false).toProperty + animationOptions = UdashCarousel.AnimationOptions(active = false).toProperty, )() jQ("body").append(carousel.render) carousel.activeSlide.get shouldBe 1 @@ -97,7 +97,7 @@ class UdashCarouselTest extends AsyncUdashCoreFrontendTest { val carousel = UdashCarousel.default( sl, activeSlide = Property(1), - animationOptions = UdashCarousel.AnimationOptions(active = false).toProperty + animationOptions = UdashCarousel.AnimationOptions(active = false).toProperty, )() jQ("body").append(carousel.render) carousel.activeSlide.get shouldBe 1 @@ -112,7 +112,7 @@ class UdashCarouselTest extends AsyncUdashCoreFrontendTest { val carousel = UdashCarousel.default( sl, activeSlide = Property(1), - animationOptions = UdashCarousel.AnimationOptions(active = false).toProperty + animationOptions = UdashCarousel.AnimationOptions(active = false).toProperty, )() jQ("body").append(carousel.render) carousel.activeSlide.get shouldBe 1 @@ -126,16 +126,16 @@ class UdashCarouselTest extends AsyncUdashCoreFrontendTest { val carousel = UdashCarousel.default( slides(), activeSlide = Property(1), - animationOptions = UdashCarousel.AnimationOptions(active = false).toProperty + animationOptions = UdashCarousel.AnimationOptions(active = false).toProperty, )() jQ("body").append(carousel.render) carousel.activeSlide.get shouldBe 1 var changeEvent: CarouselEvent[UdashCarouselSlide, ReadableProperty[UdashCarouselSlide]] = null var changedEvent: CarouselEvent[UdashCarouselSlide, ReadableProperty[UdashCarouselSlide]] = null carousel.listen { - case ev@CarouselEvent(_, _, _, false) => + case ev @ CarouselEvent(_, _, _, false) => changeEvent = ev - case ev@CarouselEvent(_, _, _, true) => + case ev @ CarouselEvent(_, _, _, true) => changedEvent = ev } carousel.goTo(5) @@ -149,7 +149,7 @@ class UdashCarouselTest extends AsyncUdashCoreFrontendTest { val carousel = UdashCarousel.default( sl, activeSlide = Property(3), - animationOptions = UdashCarousel.AnimationOptions(active = false).toProperty + animationOptions = UdashCarousel.AnimationOptions(active = false).toProperty, )() jQ("body").append(carousel.render) carousel.activeSlide.get shouldBe 3 @@ -163,7 +163,7 @@ class UdashCarouselTest extends AsyncUdashCoreFrontendTest { implicit val tp: LocalTranslationProvider = new LocalTranslationProvider( Map( Lang("test") -> Bundle(BundleHash("h"), Map("prev" -> "Poprzedni", "next" -> "Następny")), - Lang("test2") -> Bundle(BundleHash("h"), Map("prev" -> "Prev", "next" -> "next")) + Lang("test2") -> Bundle(BundleHash("h"), Map("prev" -> "Prev", "next" -> "next")), ) ) val langProperty = Property(Lang("test")) @@ -172,52 +172,64 @@ class UdashCarouselTest extends AsyncUdashCoreFrontendTest { val next = Property.blank[String] val sl = slides() - val carousel = UdashCarousel.default( - sl, - srTexts = Some((previous, next)), - animationOptions = UdashCarousel.AnimationOptions(active = false).toProperty - )().setup(_.addRegistration( - langProperty.listen(implicit lang => - for { - p <- TranslationKey.key("prev")() - n <- TranslationKey.key("next")() - } yield CallbackSequencer().sequence { - previous.set(p.string) - next.set(n.string) - }, - initUpdate = true) - )) + val carousel = UdashCarousel + .default( + sl, + srTexts = Some((previous, next)), + animationOptions = UdashCarousel.AnimationOptions(active = false).toProperty, + )() + .setup( + _.addRegistration( + langProperty.listen( + implicit lang => + for { + p <- TranslationKey.key("prev")() + n <- TranslationKey.key("next")() + } yield CallbackSequencer().sequence { + previous.set(p.string) + next.set(n.string) + }, + initUpdate = true, + ) + ) + ) val el = carousel.render jQ("body").append(el) for { _ <- retrying { - el.getElementsByClassName(Carousel.controlPrevIcon.className)(0).getAttribute(aria.label.name) should be("Poprzedni") - el.getElementsByClassName(Carousel.controlNextIcon.className)(0).getAttribute(aria.label.name) should be("Następny") + el.getElementsByClassName(Carousel.controlPrevIcon.className)(0).getAttribute(aria.label.name) should + be("Poprzedni") + el.getElementsByClassName(Carousel.controlNextIcon.className)(0).getAttribute(aria.label.name) should + be("Następny") } _ <- Future(langProperty.set(Lang("test2"))) r <- retrying { - el.getElementsByClassName(Carousel.controlPrevIcon.className)(0).getAttribute(aria.label.name) should be("Prev") - el.getElementsByClassName(Carousel.controlNextIcon.className)(0).getAttribute(aria.label.name) should be("next") + el.getElementsByClassName(Carousel.controlPrevIcon.className)(0).getAttribute(aria.label.name) should + be("Prev") + el.getElementsByClassName(Carousel.controlNextIcon.className)(0).getAttribute(aria.label.name) should + be("next") } } yield r } } - private def activeIdx(carousel: UdashCarousel[_, _]): Int = { + private def activeIdx(carousel: UdashCarousel[_, _]): Int = jQ(carousel.render) - .find(s".${BootstrapStyles.Carousel.item.className}").get() + .find(s".${BootstrapStyles.Carousel.item.className}") + .get() .zipWithIndex .collectFirst { case (el, idx) if el.classList.contains(BootstrapStyles.active.className) => idx } .getOrElse(-1) - } private def activeIndicatorIdx(carousel: UdashCarousel[_, _]): Int = { val indicators = jQ(carousel.render).find(s".${BootstrapStyles.Carousel.indicators.className}").children() (0 until indicators.length) .map(indicators.at) - .find(_.hasClass(BootstrapStyles.active.className)).get - .attr(BootstrapTags.dataSlideTo.name).get + .find(_.hasClass(BootstrapStyles.active.className)) + .get + .attr(BootstrapTags.dataSlideTo.name) + .get .toInt } diff --git a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/collapse/UdashAccordionTest.scala b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/collapse/UdashAccordionTest.scala index 73c858800..a07851f37 100644 --- a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/collapse/UdashAccordionTest.scala +++ b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/collapse/UdashAccordionTest.scala @@ -12,13 +12,17 @@ class UdashAccordionTest extends AsyncUdashCoreFrontendTest { "UdashAccordion component" should { "grant access to created UdashCollapse instances" in { - val news = SeqProperty[String](Seq( - "Title 1", "Title 2", "Title 3" - )) + val news = SeqProperty[String]( + Seq( + "Title 1", + "Title 2", + "Title 3", + ) + ) val accordion = UdashAccordion(news)( (news, nested) => span(">>", nested(bind(news))).render, - (news, nested) => span(nested(bind(news))).render + (news, nested) => span(nested(bind(news))).render, ) val accordionElement = accordion.render @@ -42,13 +46,17 @@ class UdashAccordionTest extends AsyncUdashCoreFrontendTest { } "expose collapse events" in { - val news = SeqProperty[String](Seq( - "Title 1", "Title 2", "Title 3" - )) + val news = SeqProperty[String]( + Seq( + "Title 1", + "Title 2", + "Title 3", + ) + ) val accordion = UdashAccordion(news)( (news, nested) => i(">>", nested(bind(news))).render, - (news, nested) => span(nested(bind(news))).render + (news, nested) => span(nested(bind(news))).render, ) val events = mutable.ArrayBuffer.empty[UdashAccordion.AccordionEvent[String, _]] diff --git a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/collapse/UdashCollapseTest.scala b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/collapse/UdashCollapseTest.scala index 2a0e87ac9..3dde744fd 100644 --- a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/collapse/UdashCollapseTest.scala +++ b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/collapse/UdashCollapseTest.scala @@ -20,9 +20,8 @@ class UdashCollapseTest extends AsyncUdashCoreFrontendTest { jQ("body").append(element) val q: JQuery = jQ(s"#${collapse.componentId}") - def checkState(): (Boolean, Boolean) = { + def checkState(): (Boolean, Boolean) = (q.hasClass("show"), q.hasClass("collapsing")) - } var showCounter = 0 var shownCounter = 0 @@ -78,9 +77,8 @@ class UdashCollapseTest extends AsyncUdashCoreFrontendTest { jQ("body").append(element) val q: JQuery = jQ(s"#${collapse.componentId}") - def checkState(): (Boolean, Boolean) = { + def checkState(): (Boolean, Boolean) = (q.hasClass("show"), q.hasClass("collapsing")) - } var showCounter = 0 var shownCounter = 0 diff --git a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/datepicker/UdashDatePickerTest.scala b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/datepicker/UdashDatePickerTest.scala index c74a45881..db66c403a 100644 --- a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/datepicker/UdashDatePickerTest.scala +++ b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/datepicker/UdashDatePickerTest.scala @@ -23,17 +23,21 @@ class UdashDatePickerTest extends AsyncUdashCoreFrontendTest { "show/hide on method call and emit events" in { val contentId = "datepicker-test-content" val date = Property[Option[ju.Date]](None) - val options = Property(new UdashDatePicker.DatePickerOptions( - format = "YYYY MM DD a", - useCurrent = false - )) + val options = Property( + new UdashDatePicker.DatePickerOptions( + format = "YYYY MM DD a", + useCurrent = false, + ) + ) val picker = UdashDatePicker(date, options, ComponentId(contentId)) - jQ("body").append(UdashInputGroup()( - UdashInputGroup.input(picker.render), - UdashInputGroup.appendText("test"), - style := "position: relative;" - ).render) + jQ("body").append( + UdashInputGroup()( + UdashInputGroup.input(picker.render), + UdashInputGroup.appendText("test"), + style := "position: relative;", + ).render + ) var showCounter = 0 var hideCounter = 0 @@ -92,17 +96,19 @@ class UdashDatePickerTest extends AsyncUdashCoreFrontendTest { "not fail on null input value" in { val date = Property[Option[ju.Date]](Some(new ju.Date())) - val pickerOptions = Property(new UdashDatePicker.DatePickerOptions( - format = "MMMM Do YYYY, hh:mm a", - locale = Some("en_GB") - )) + val pickerOptions = Property( + new UdashDatePicker.DatePickerOptions( + format = "MMMM Do YYYY, hh:mm a", + locale = Some("en_GB"), + ) + ) val picker: UdashDatePicker = UdashDatePicker(date, pickerOptions) jQ("body").append( div( UdashDatePicker.loadBootstrapDatePickerStyles(), UdashInputGroup()( UdashInputGroup.input(picker.render) - ).render + ).render, ).render ) noException shouldBe thrownBy { @@ -112,16 +118,18 @@ class UdashDatePickerTest extends AsyncUdashCoreFrontendTest { "sync with property" in { val date = Property[Option[ju.Date]](Some(new ju.Date())) - val pickerOptions = Property(new UdashDatePicker.DatePickerOptions( - format = "MMMM Do YYYY, hh:mm a", - locale = Some("en_GB") - )) + val pickerOptions = Property( + new UdashDatePicker.DatePickerOptions( + format = "MMMM Do YYYY, hh:mm a", + locale = Some("en_GB"), + ) + ) val picker: UdashDatePicker = UdashDatePicker(date, pickerOptions) val r = div( UdashDatePicker.loadBootstrapDatePickerStyles(), UdashInputGroup()( UdashInputGroup.input(picker.render) - ).render + ).render, ).render jQ("body").append(r) @@ -148,18 +156,22 @@ class UdashDatePickerTest extends AsyncUdashCoreFrontendTest { "emit error events" in { val contentId = "datepicker-test-content" val date = Property[Option[ju.Date]](Some(new ju.Date())) - val options = Property(new UdashDatePicker.DatePickerOptions( - format = "YYYY MM DD", - minDate = Some(new ju.Date(1000000000)), - maxDate = Some(new ju.Date(5000000000L)) - )) + val options = Property( + new UdashDatePicker.DatePickerOptions( + format = "YYYY MM DD", + minDate = Some(new ju.Date(1000000000)), + maxDate = Some(new ju.Date(5000000000L)), + ) + ) val picker = UdashDatePicker(date, options, ComponentId(contentId)) - jQ("body").append(UdashInputGroup()( - UdashInputGroup.input(picker.render), - UdashInputGroup.appendText("test"), - style := "position: relative;" - ).render) + jQ("body").append( + UdashInputGroup()( + UdashInputGroup.input(picker.render), + UdashInputGroup.appendText("test"), + style := "position: relative;", + ).render + ) var errorCounter = 0 var changeCounter = 0 @@ -212,32 +224,55 @@ class UdashDatePickerTest extends AsyncUdashCoreFrontendTest { implicit val tp = new LocalTranslationProvider( Map( Lang("test") -> Bundle(BundleHash("h"), Map("today" -> "Dzisiaj", "clear" -> "Wyczyść")), - Lang("test2") -> Bundle(BundleHash("h"), Map("today" -> "Today", "clear" -> "Clear")) + Lang("test2") -> Bundle(BundleHash("h"), Map("today" -> "Today", "clear" -> "Clear")), ) ) val lang = Property(Lang("test")) val date = Property[Option[ju.Date]](Some(new ju.Date())) - val pickerOptions = ModelProperty(new UdashDatePicker.DatePickerOptions( - format = "MMMM Do YYYY, hh:mm a", - locale = Some("en_GB") - )) + val pickerOptions = ModelProperty( + new UdashDatePicker.DatePickerOptions( + format = "MMMM Do YYYY, hh:mm a", + locale = Some("en_GB"), + ) + ) val emptyTk = TranslationKey.untranslatable("") - val picker = UdashDatePicker(date, pickerOptions).setup(_.addRegistration(lang.listen(implicit lang => - for { - today <- TranslationKey.key("today")().mapNow(_.string) - clear <- TranslationKey.key("clear")().mapNow(_.string) - close <- TranslationKey.untranslatable("close 123")().mapNow(_.string) - other <- emptyTk().mapNow(_.string) - } yield { - pickerOptions.subProp(_.tooltips).set( - UdashDatePicker.DatePickerTooltips( - today, clear, close, other, other, other, other, other, other, other, other, other, other, other - ) + val picker = UdashDatePicker(date, pickerOptions).setup( + _.addRegistration( + lang.listen( + implicit lang => + for { + today <- TranslationKey.key("today")().mapNow(_.string) + clear <- TranslationKey.key("clear")().mapNow(_.string) + close <- TranslationKey.untranslatable("close 123")().mapNow(_.string) + other <- emptyTk().mapNow(_.string) + } yield { + pickerOptions + .subProp(_.tooltips) + .set( + UdashDatePicker.DatePickerTooltips( + today, + clear, + close, + other, + other, + other, + other, + other, + other, + other, + other, + other, + other, + other, + ) + ) + }, + initUpdate = true, ) - } - , initUpdate = true))) + ) + ) jQ("body").append(picker.render) @@ -272,28 +307,44 @@ class UdashDatePickerTest extends AsyncUdashCoreFrontendTest { "invoke setup/detach callbacks when appending to/removing from the DOM" in { def defaultPicker = UdashDatePicker( Property(Option.empty), - new UdashDatePicker.DatePickerOptions("YYYY MM DD").toProperty + new UdashDatePicker.DatePickerOptions("YYYY MM DD").toProperty, ).render var firstPickerSetupCount = 0 var firstPickerDetachCount = 0 - val firstPicker = defaultPicker.setup(UdashDatePicker.registerMutationCallbacks( - _, setupCallback = () => firstPickerSetupCount += 1, detachCallback = () => firstPickerDetachCount += 1)) + val firstPicker = defaultPicker.setup( + UdashDatePicker.registerMutationCallbacks( + _, + setupCallback = () => firstPickerSetupCount += 1, + detachCallback = () => firstPickerDetachCount += 1, + ) + ) var secondPickerSetupCount = 0 var secondPickerDetachCount = 0 - val secondPicker = defaultPicker.setup(UdashDatePicker.registerMutationCallbacks( - _, setupCallback = () => secondPickerSetupCount += 1, detachCallback = () => secondPickerDetachCount += 1)) + val secondPicker = defaultPicker.setup( + UdashDatePicker.registerMutationCallbacks( + _, + setupCallback = () => secondPickerSetupCount += 1, + detachCallback = () => secondPickerDetachCount += 1, + ) + ) def assertCounters( expectedFirstPickerSetupCount: Int, - expectedSecondPickerSetupCount: Int + expectedSecondPickerSetupCount: Int, )( expectedFirstPickerDetachCount: Int, - expectedSecondPickerDetachCount: Int + expectedSecondPickerDetachCount: Int, ) = (firstPickerSetupCount, secondPickerSetupCount, firstPickerDetachCount, secondPickerDetachCount) should be( - (expectedFirstPickerSetupCount, expectedSecondPickerSetupCount, expectedFirstPickerDetachCount, expectedSecondPickerDetachCount)) + ( + expectedFirstPickerSetupCount, + expectedSecondPickerSetupCount, + expectedFirstPickerDetachCount, + expectedSecondPickerDetachCount, + ) + ) for { _ <- { @@ -327,8 +378,8 @@ class UdashDatePickerTest extends AsyncUdashCoreFrontendTest { format = dateFormat, locale = Some(localeString), inline = inlinePicker, - showToday = todayButton - ).toProperty + showToday = todayButton, + ).toProperty, ).render def assertOptions() = { @@ -362,7 +413,7 @@ class UdashDatePickerTest extends AsyncUdashCoreFrontendTest { date = Property(Some(new ju.Date(0L))), options = new UdashDatePicker.DatePickerOptions( format = formatString - ).toProperty + ).toProperty, ).render val jqPicker = jQ(picker).asInstanceOf[JQueryDatePickerExt] @@ -385,11 +436,13 @@ class UdashDatePickerTest extends AsyncUdashCoreFrontendTest { "apply selected icons config" in { new UdashDatePicker.CustomDatePickerIcons( date = FontAwesome.Solid.calendarAlt.option, - next = FontAwesome.Regular.arrowAltCircleRight.option - ).jsDictionary.json should be(js.Dictionary[js.Any]( - "date" -> FontAwesome.Solid.calendarAlt.classNames.toJSArray, - "next" -> FontAwesome.Regular.arrowAltCircleRight.classNames.toJSArray - ).json) + next = FontAwesome.Regular.arrowAltCircleRight.option, + ).jsDictionary.json should be( + js.Dictionary[js.Any]( + "date" -> FontAwesome.Solid.calendarAlt.classNames.toJSArray, + "next" -> FontAwesome.Regular.arrowAltCircleRight.classNames.toJSArray, + ).json + ) } "apply full icons config" in { @@ -402,7 +455,7 @@ class UdashDatePickerTest extends AsyncUdashCoreFrontendTest { next = FontAwesome.Solid.angleRight.option, today = FontAwesome.Regular.calendarCheck.option, clear = FontAwesome.Regular.trashAlt.option, - close = FontAwesome.Solid.times.option + close = FontAwesome.Solid.times.option, ).jsDictionary.json should be(UdashDatePicker.DefaultDatePickerIcons.jsDictionary.json) } } @@ -421,10 +474,11 @@ object UdashDatePickerTest { @js.native private trait MomentDate extends js.Any { - /** - * https://momentjs.com/docs/#/parsing/creation-data/ + + /** https://momentjs.com/docs/#/parsing/creation-data/ * - * @return input data provided during the Moment object creation + * @return + * input data provided during the Moment object creation */ def creationData(): js.Dictionary[Any] = js.native } diff --git a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/dropdown/UdashDropdownTest.scala b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/dropdown/UdashDropdownTest.scala index 7abca8399..fa30cc5ab 100644 --- a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/dropdown/UdashDropdownTest.scala +++ b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/dropdown/UdashDropdownTest.scala @@ -11,12 +11,12 @@ import scalatags.JsDom.all._ class UdashDropdownTest extends UdashCoreFrontendTest { import UdashDropdown._ - private val elements: Seq[DefaultDropdownItem] =Seq( + private val elements: Seq[DefaultDropdownItem] = Seq( DefaultDropdownItem.Header("Header"), DefaultDropdownItem.Link("Link 1", Url("#")), DefaultDropdownItem.Link("Link 2", Url("#")), DefaultDropdownItem.Divider, - DefaultDropdownItem.Disabled(DefaultDropdownItem.Link("Link 3", Url("#"))) + DefaultDropdownItem.Disabled(DefaultDropdownItem.Link("Link 3", Url("#"))), ) "UdashDropdown component" should { @@ -42,8 +42,8 @@ class UdashDropdownTest extends UdashCoreFrontendTest { el.childNodes(0).asInstanceOf[Button].click() showCounter should be(i) shownCounter should be(i) - hideCounter should be(i-1) - hiddenCounter should be(i-1) + hideCounter should be(i - 1) + hiddenCounter should be(i - 1) el.childNodes(0).asInstanceOf[Button].click() showCounter should be(i) shownCounter should be(i) @@ -59,7 +59,6 @@ class UdashDropdownTest extends UdashCoreFrontendTest { val el = dropdown.render jQ("body").append(el) - var showCounter = 0 var shownCounter = 0 var hideCounter = 0 @@ -75,8 +74,8 @@ class UdashDropdownTest extends UdashCoreFrontendTest { dropdown.toggle() showCounter should be(i) shownCounter should be(i) - hideCounter should be(i-1) - hiddenCounter should be(i-1) + hideCounter should be(i - 1) + hiddenCounter should be(i - 1) dropdown.toggle() showCounter should be(i) shownCounter should be(i) @@ -91,7 +90,6 @@ class UdashDropdownTest extends UdashCoreFrontendTest { val el = dropdown.render jQ("body").append(el) - var elClickCounter = 0 var selectedItem: DefaultDropdownItem = DefaultDropdownItem.Divider dropdown.listen { case ev: SelectionEvent[_, _] => @@ -100,7 +98,7 @@ class UdashDropdownTest extends UdashCoreFrontendTest { } for (i <- 1 to 5) { - el.childNodes(1).childNodes(i-1).asInstanceOf[Button].click() + el.childNodes(1).childNodes(i - 1).asInstanceOf[Button].click() elClickCounter should be(i) els.get should contain(selectedItem) } @@ -108,7 +106,7 @@ class UdashDropdownTest extends UdashCoreFrontendTest { els.elemProperties(1).set(DefaultDropdownItem.Header("Test Header 123")) for (i <- 1 to 5) { - el.childNodes(1).childNodes(i-1).asInstanceOf[Button].click() + el.childNodes(1).childNodes(i - 1).asInstanceOf[Button].click() elClickCounter should be(i + 5) els.get should contain(selectedItem) } @@ -120,7 +118,6 @@ class UdashDropdownTest extends UdashCoreFrontendTest { val el = dropdown.render jQ("body").append(el) - el.childNodes(1).childNodes.length should be(els.get.length) val tmp: DefaultDropdownItem.Link = DefaultDropdownItem.Link("New", Url("#")) @@ -150,7 +147,9 @@ class UdashDropdownTest extends UdashCoreFrontendTest { val direction = Property[Direction](Direction.Up) val rightAlign = Property[Boolean](true) val dropdown = UdashDropdown.default( - els, direction, rightAlign + els, + direction, + rightAlign, )(nested => Seq[Modifier]("Test", nested(bind(direction)))) val el = dropdown.render jQ("body").append(el) diff --git a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/form/UdashInputGroupTest.scala b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/form/UdashInputGroupTest.scala index 5820cae52..bd68c3fcf 100644 --- a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/form/UdashInputGroupTest.scala +++ b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/form/UdashInputGroupTest.scala @@ -12,7 +12,7 @@ class UdashInputGroupTest extends UdashCoreFrontendTest { val size = Property[Option[BootstrapStyles.Size]](Some(BootstrapStyles.Size.Large)) val group = UdashInputGroup(size)( UdashInputGroup.prependText("Test"), - UdashInputGroup.input(TextInput(Property(""))().render) + UdashInputGroup.input(TextInput(Property(""))().render), ) val el = group.render el.childNodes.length should be(2) diff --git a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/list/UdashListGroupTest.scala b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/list/UdashListGroupTest.scala index d5a3e1066..3711512e8 100644 --- a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/list/UdashListGroupTest.scala +++ b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/list/UdashListGroupTest.scala @@ -11,9 +11,7 @@ class UdashListGroupTest extends UdashCoreFrontendTest { "update items list and clean up listeners properly" in { val items = SeqProperty("a", "b", "c") val flush = Property[Boolean](false) - val group = UdashListGroup(items, flush)( - (item, nested) => div(nested(bind(item))).render - ) + val group = UdashListGroup(items, flush)((item, nested) => div(nested(bind(item))).render) val el = group.render el.childNodes.length should be(3) el.textContent should be("abc") diff --git a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/modal/UdashModalTest.scala b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/modal/UdashModalTest.scala index 516104694..67ad10a95 100644 --- a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/modal/UdashModalTest.scala +++ b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/modal/UdashModalTest.scala @@ -31,7 +31,7 @@ class UdashModalTest extends UdashCoreFrontendTest { for (i <- 1 to 10) { modal.show() showCounter should be(i) - hideCounter should be(i-1) + hideCounter should be(i - 1) modal.hide() showCounter should be(i) hideCounter should be(i) @@ -42,22 +42,22 @@ class UdashModalTest extends UdashCoreFrontendTest { val modal = UdashModal()( Some(header), Some(body), - Some(footer) + Some(footer), ) val modal1 = UdashModal()( None, Some(body), - Some(footer) + Some(footer), ) val modal2 = UdashModal()( Some(header), None, - Some(footer) + Some(footer), ) val modal3 = UdashModal()( Some(header), Some(body), - None + None, ) modal.render.textContent should be("Header Body Footer ") diff --git a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/nav/UdashNavTest.scala b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/nav/UdashNavTest.scala index a0e5505f8..5476ad3a1 100644 --- a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/nav/UdashNavTest.scala +++ b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/nav/UdashNavTest.scala @@ -10,9 +10,7 @@ class UdashNavTest extends UdashCoreFrontendTest { "UdashNav component" should { "render provided elements" in { val items = SeqProperty[String]("a", "b", "c") - val nav = UdashNav(items)( - (item, nested) => span(nested(bind(item))).render - ) + val nav = UdashNav(items)((item, nested) => span(nested(bind(item))).render) val el = nav.render el.childNodes.length should be(3) el.textContent should be("abc") @@ -45,9 +43,8 @@ class UdashNavTest extends UdashCoreFrontendTest { val tabs: ReadableProperty[Boolean] = Property(true) val pills: ReadableProperty[Boolean] = Property(false) - val nav = UdashNav(items, align, vertical, fill, justified, tabs, pills)( - (item, nested) => span(nested(bind(item))).render - ) + val nav = + UdashNav(items, align, vertical, fill, justified, tabs, pills)((item, nested) => span(nested(bind(item))).render) val el = nav.render el.childNodes.length should be(3) el.textContent should be("abc") diff --git a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/nav/UdashNavbarTest.scala b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/nav/UdashNavbarTest.scala index 1d41c5e22..63a5fb75f 100644 --- a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/nav/UdashNavbarTest.scala +++ b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/nav/UdashNavbarTest.scala @@ -10,9 +10,7 @@ class UdashNavbarTest extends UdashCoreFrontendTest { "UdashNavbar component" should { "render provided elements and cleanup provided navigation only if it was wrapped with interceptor" in { val items = SeqProperty[String]("a", "b", "c") - val nav = UdashNav(items)( - (item, nested) => span(nested(bind(item))).render - ) + val nav = UdashNav(items)((item, nested) => span(nested(bind(item))).render) val navbar = UdashNavbar() { nested => nested(nav) nav @@ -36,9 +34,7 @@ class UdashNavbarTest extends UdashCoreFrontendTest { } "render provided elements and cleanup provided navigation only if it was wrapped with interceptor (without wrap)" in { val items = SeqProperty[String]("a", "b", "c") - val nav = UdashNav(items)( - (item, nested) => span(nested(bind(item))).render - ) + val nav = UdashNav(items)((item, nested) => span(nested(bind(item))).render) val navbar = UdashNavbar()(_ => nav) val el = navbar.render el.textContent should be("abc") @@ -66,7 +62,8 @@ class UdashNavbarTest extends UdashCoreFrontendTest { val items = SeqProperty[String]("a", "b", "c") val nav = UdashNav(items)((item, nested) => span(nested(bind(item))).render) - val expandBreakpoint: ReadableProperty[BootstrapStyles.ResponsiveBreakpoint] = Property(BootstrapStyles.ResponsiveBreakpoint.Small) + val expandBreakpoint: ReadableProperty[BootstrapStyles.ResponsiveBreakpoint] = + Property(BootstrapStyles.ResponsiveBreakpoint.Small) val darkStyle: ReadableProperty[Boolean] = Property(true) val backgroundStyle: ReadableProperty[BootstrapStyles.Color] = Property(BootstrapStyles.Color.Secondary) val position: ReadableProperty[UdashNavbar.Position] = Property(UdashNavbar.Position.StickyTop) diff --git a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/pagination/UdashPaginationTest.scala b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/pagination/UdashPaginationTest.scala index 225875a83..eed462917 100644 --- a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/pagination/UdashPaginationTest.scala +++ b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/pagination/UdashPaginationTest.scala @@ -18,7 +18,10 @@ class UdashPaginationTest extends AsyncUdashCoreFrontendTest { val pages = SeqProperty(Seq.tabulate[Int](7)(identity)) val pagination = UdashPagination( - pages, selected, showArrows = showArrows, highlightActive = highlightActive + pages, + selected, + showArrows = showArrows, + highlightActive = highlightActive, )() val paginationElement = pagination.render @@ -37,7 +40,10 @@ class UdashPaginationTest extends AsyncUdashCoreFrontendTest { val pages = SeqProperty(Seq.tabulate[Int](7)(identity)) val pagination = UdashPagination( - pages, selected, showArrows = showArrows, highlightActive = highlightActive + pages, + selected, + showArrows = showArrows, + highlightActive = highlightActive, )() val paginationElement = pagination.render @@ -109,7 +115,10 @@ class UdashPaginationTest extends AsyncUdashCoreFrontendTest { val pages = SeqProperty(Seq.tabulate[Int](7)(identity)) val pagination = UdashPagination( - pages, selected, showArrows = showArrows, highlightActive = highlightActive + pages, + selected, + showArrows = showArrows, + highlightActive = highlightActive, )() val paginationElement = pagination.render @@ -145,7 +154,10 @@ class UdashPaginationTest extends AsyncUdashCoreFrontendTest { val pages = SeqProperty(Seq.tabulate[Int](7)(identity)) val pagination = UdashPagination( - pages, selected, showArrows = showArrows, highlightActive = highlightActive + pages, + selected, + showArrows = showArrows, + highlightActive = highlightActive, )() val paginationElement = pagination.render @@ -165,7 +177,7 @@ class UdashPaginationTest extends AsyncUdashCoreFrontendTest { implicit val tp = new LocalTranslationProvider( Map( Lang("test") -> Bundle(BundleHash("h"), Map("prev" -> "Poprzedni", "next" -> "Następny")), - Lang("test2") -> Bundle(BundleHash("h"), Map("prev" -> "Prev", "next" -> "next")) + Lang("test2") -> Bundle(BundleHash("h"), Map("prev" -> "Prev", "next" -> "next")), ) ) val lang = Property(Lang("test")) @@ -178,31 +190,43 @@ class UdashPaginationTest extends AsyncUdashCoreFrontendTest { val pagination = UdashPagination(pages, selected)( arrowFactory = UdashPagination.defaultArrowFactory(Some((previous, next))) - ).setup(_.addRegistration( - lang.listen(implicit lang => - for { - p <- TranslationKey.key("prev")() - n <- TranslationKey.key("next")() - } yield CallbackSequencer().sequence { - previous.set(p.string) - next.set(n.string) - }, - initUpdate = true) - )) + ).setup( + _.addRegistration( + lang.listen( + implicit lang => + for { + p <- TranslationKey.key("prev")() + n <- TranslationKey.key("next")() + } yield CallbackSequencer().sequence { + previous.set(p.string) + next.set(n.string) + }, + initUpdate = true, + ) + ) + ) val el = pagination.render import scalatags.JsDom.all._ for { _ <- retrying { - el.getElementsByTagName("li")(0).firstElementChild.firstElementChild.getAttribute(aria.label.name) should be("Poprzedni") - el.getElementsByTagName("li")(pages.size + 1).firstElementChild.firstElementChild.getAttribute(aria.label.name) should be("Następny") + el.getElementsByTagName("li")(0).firstElementChild.firstElementChild.getAttribute(aria.label.name) should + be("Poprzedni") + el.getElementsByTagName("li")(pages.size + 1) + .firstElementChild + .firstElementChild + .getAttribute(aria.label.name) should be("Następny") } _ <- Future { lang.set(Lang("test2")) } _ <- retrying { - el.getElementsByTagName("li")(0).firstElementChild.firstElementChild.getAttribute(aria.label.name) should be("Prev") - el.getElementsByTagName("li")(pages.size + 1).firstElementChild.firstElementChild.getAttribute(aria.label.name) should be("next") + el.getElementsByTagName("li")(0).firstElementChild.firstElementChild.getAttribute(aria.label.name) should + be("Prev") + el.getElementsByTagName("li")(pages.size + 1) + .firstElementChild + .firstElementChild + .getAttribute(aria.label.name) should be("next") } _ <- Future { pagination.kill() @@ -217,10 +241,9 @@ class UdashPaginationTest extends AsyncUdashCoreFrontendTest { val selected = Property(0) val pages = SeqProperty(Seq.tabulate[Int](7)(identity)) - val pagination = UdashPagination(pages, selected, showArrows = false.toProperty) { - (v, _, nested) => - import scalatags.JsDom.all._ - span(nested(bind(v))) + val pagination = UdashPagination(pages, selected, showArrows = false.toProperty) { (v, _, nested) => + import scalatags.JsDom.all._ + span(nested(bind(v))) } val paginationElement = pagination.render @@ -242,9 +265,9 @@ class UdashPaginationTest extends AsyncUdashCoreFrontendTest { } def checkDisabled(els: Element*)(expectFirst: Boolean, expectLast: Boolean): Unit = { - els.foreach(el => { + els.foreach { el => jQ(el).find("li").first().hasClass("disabled") should be(expectFirst) jQ(el).find("li").last().hasClass("disabled") should be(expectLast) - }) + } } } diff --git a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/progressbar/UdashProgressBarTest.scala b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/progressbar/UdashProgressBarTest.scala index 325dd98d9..91e8fc145 100644 --- a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/progressbar/UdashProgressBarTest.scala +++ b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/progressbar/UdashProgressBarTest.scala @@ -17,7 +17,14 @@ class UdashProgressBarTest extends UdashCoreFrontendTest { val maxValue = Property(50) val minWidth = Property(1) val progressbar = UdashProgressBar( - progress, showPercentage, barStyle, stripped, animated, minValue, maxValue, minWidth + progress, + showPercentage, + barStyle, + stripped, + animated, + minValue, + maxValue, + minWidth, )() val el = progressbar.render el.textContent should be("50%") diff --git a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/table/UdashTableTest.scala b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/table/UdashTableTest.scala index 19c7ffe14..3f3792400 100644 --- a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/table/UdashTableTest.scala +++ b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/table/UdashTableTest.scala @@ -15,9 +15,10 @@ class UdashTableTest extends UdashCoreFrontendTest { val items = SeqProperty( TestEntity("Adam", 34, "EN"), TestEntity("John", 21, "GE"), - TestEntity("Alice", 24, "FR") + TestEntity("Alice", 24, "FR"), ) - val responsive = Property[Option[BootstrapStyles.ResponsiveBreakpoint]](Some(BootstrapStyles.ResponsiveBreakpoint.Small)) + val responsive = + Property[Option[BootstrapStyles.ResponsiveBreakpoint]](Some(BootstrapStyles.ResponsiveBreakpoint.Small)) val dark = Property(false) val striped = Property(false) val bordered = Property(false) @@ -25,14 +26,22 @@ class UdashTableTest extends UdashCoreFrontendTest { val hover = Property(false) val small = Property(false) val table = UdashTable( - items, responsive, dark, striped, bordered, borderless, hover, small + items, + responsive, + dark, + striped, + bordered, + borderless, + hover, + small, )( - (item, nested) => tr( - td(nested(bind(item.asModel.subProp(_.name)))), - td(nested(bind(item.asModel.subProp(_.age)))), - td(nested(bind(item.asModel.subProp(_.language)))) - ).render, - headerFactory = Some(_ => tr(th("Name"), th("Age"), th("Language"))) + (item, nested) => + tr( + td(nested(bind(item.asModel.subProp(_.name)))), + td(nested(bind(item.asModel.subProp(_.age)))), + td(nested(bind(item.asModel.subProp(_.language)))), + ).render, + headerFactory = Some(_ => tr(th("Name"), th("Age"), th("Language"))), ) val el = table.render el.textContent should be("NameAgeLanguageAdam34ENJohn21GEAlice24FR") diff --git a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/tooltip/TooltipTestUtils.scala b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/tooltip/TooltipTestUtils.scala index b82bc259a..6bb9ac896 100644 --- a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/tooltip/TooltipTestUtils.scala +++ b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/tooltip/TooltipTestUtils.scala @@ -22,13 +22,13 @@ class TooltipTestUtils extends AsyncUdashCoreFrontendTest { implicit val tp = new LocalTranslationProvider( Map( Lang("test") -> Bundle(BundleHash("h"), Map("a" -> s"$randMarker:AAA", "b" -> s"$randMarker:BBB")), - Lang("test2") -> Bundle(BundleHash("h"), Map("a" -> s"$randMarker:ccc", "b" -> s"$randMarker:ddd")) + Lang("test2") -> Bundle(BundleHash("h"), Map("a" -> s"$randMarker:ccc", "b" -> s"$randMarker:ddd")), ) ) val tooltip = companion.apply( title = span(translatedDynamic(TranslationKey.key("a"))(_.apply())).render, - content = span(translatedDynamic(TranslationKey.key("b"))(_.apply())).render + content = span(translatedDynamic(TranslationKey.key("b"))(_.apply())).render, )(item) def expectedText(): String = diff --git a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/utils/UdashIconsTest.scala b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/utils/UdashIconsTest.scala index 996ca7f93..308e9f7b9 100644 --- a/bootstrap4/.js/src/test/scala/io/udash/bootstrap/utils/UdashIconsTest.scala +++ b/bootstrap4/.js/src/test/scala/io/udash/bootstrap/utils/UdashIconsTest.scala @@ -24,38 +24,43 @@ class UdashIconsTest extends AsyncUdashCoreFrontendTest with CssView { val el = span( addressBook.styleIf(p), angry.styleIf(p.transform(!_)), - heart.styleIf(p) + heart.styleIf(p), ).render - Future.successful(assert( - (Iterator(el.classList.length should be(2)) ++ (0 to 10).iterator.map { i => - p.set(!p.get) - el.classList.length should be(if (i % 2 == 0) 3 else 2) - }).forall(_ == Succeeded) - )) + Future.successful( + assert( + (Iterator(el.classList.length should be(2)) ++ (0 to 10).iterator.map { i => + p.set(!p.get) + el.classList.length should be(if (i % 2 == 0) 3 else 2) + }).forall(_ == Succeeded) + ) + ) } "apply appropriate FontAwesome Free unicodes to the :before pseudoselector" in { - Future.sequence( - Iterator( - valuesOfType[CssStyle](Brands).iterator, - valuesOfType[CssStyle](Regular).iterator, - valuesOfType[CssStyle](Solid).iterator - ).flatten.map { iconStyle => - val icon = i(iconStyle).render - document.body.appendChild(icon) - - (for { - _ <- retrying { - window.getComputedStyle(icon).fontFamily should (equal("\"Font Awesome 5 Free\"") or equal("\"Font Awesome 5 Brands\"")) - } - r <- retrying { - window.getComputedStyle(icon, ":before").content.replaceAll("\"|\'", "").head.toHexString should - (startWith("f") and have length 4) - } - } yield r).andThen { case _ => document.body.removeChild(icon) }(RunNowEC) - } - ).map(assertions => assert(assertions.forall(_ == Succeeded))) + Future + .sequence( + Iterator( + valuesOfType[CssStyle](Brands).iterator, + valuesOfType[CssStyle](Regular).iterator, + valuesOfType[CssStyle](Solid).iterator, + ).flatten.map { iconStyle => + val icon = i(iconStyle).render + document.body.appendChild(icon) + + (for { + _ <- retrying { + window.getComputedStyle(icon).fontFamily should + (equal("\"Font Awesome 5 Free\"").or(equal("\"Font Awesome 5 Brands\""))) + } + r <- retrying { + window.getComputedStyle(icon, ":before").content.replaceAll("\"|\'", "").head.toHexString should + (startWith("f").and(have).length(4)) + } + } yield r).andThen { case _ => document.body.removeChild(icon) }(RunNowEC) + } + ) + .map(assertions => assert(assertions.forall(_ == Succeeded))) } } } diff --git a/build.sbt b/build.sbt index dd4e47f6a..8324ba682 100644 --- a/build.sbt +++ b/build.sbt @@ -9,64 +9,72 @@ name := "udash" Global / excludeLintKeys ++= Set(ideOutputDirectory, ideSkipProject) -inThisBuild(Seq( - organization := "io.udash", - resolvers += Resolver.defaultLocal, - homepage := Some(url("https://udash.io")), - licenses := Seq(License.Apache2), - scmInfo := Some(ScmInfo( - browseUrl = url("https://github.com/UdashFramework/udash-core"), - connection = "scm:git:git@github.com:UdashFramework/udash-core.git", - devConnection = Some("scm:git:git@github.com:UdashFramework/udash-core.git"), - )), - developers := List( - Developer("ddworak", "Dawid Dworak", "d.dworak@avsystem.com", url("https://github.com/ddworak")), - ), - scalaVersion := Dependencies.versionOfScala, - - githubWorkflowTargetTags ++= Seq("v*"), - githubWorkflowArtifactUpload := false, - githubWorkflowJavaVersions := Seq(JavaSpec.temurin("17"), JavaSpec.temurin("21"), JavaSpec.temurin("25")), - githubWorkflowBuildPreamble ++= Seq( - WorkflowStep.Use( - ref = UseRef.Public("actions", "setup-node", "v4"), - name = Some("Setup Node.js"), +inThisBuild( + Seq( + organization := "io.udash", + resolvers += Resolver.defaultLocal, + homepage := Some(url("https://udash.io")), + licenses := Seq(License.Apache2), + scmInfo := Some( + ScmInfo( + browseUrl = url("https://github.com/UdashFramework/udash-core"), + connection = "scm:git:git@github.com:UdashFramework/udash-core.git", + devConnection = Some("scm:git:git@github.com:UdashFramework/udash-core.git"), + ) ), - WorkflowStep.Run( - commands = List("npm install"), - name = Some("Install npm dependencies") - ) - ), - githubWorkflowPublishTargetBranches := Seq(RefPredicate.StartsWith(Ref.Tag("v"))), - githubWorkflowEnv += "JAVA_OPTS" -> "-Dfile.encoding=UTF-8 -Xmx4G", - githubWorkflowBuildMatrixFailFast := Some(false), - githubWorkflowBuildMatrixAdditions += "command" -> List("udash-jvm/test", "udash-js/test", "guide-selenium/test"), - githubWorkflowBuild := Seq(WorkflowStep.Sbt( - commands = List("${{ matrix.command }}"), - name = Some("Run tests"), - )), - // MiMa only compares bytecode — result is JDK-agnostic, so we run it in a dedicated - // single-JDK job instead of multiplying the work across the build matrix. - githubWorkflowAddedJobs += WorkflowJob( - id = "mima", - name = "Binary Compatibility Check", - scalas = List(Dependencies.versionOfScala), - javas = List(JavaSpec.temurin("21")), - steps = githubWorkflowJobSetup.value.toList :+ WorkflowStep.Sbt( - List("mimaReportBinaryIssues"), - name = Some("Check binary compatibility"), + developers := List( + Developer("ddworak", "Dawid Dworak", "d.dworak@avsystem.com", url("https://github.com/ddworak")) ), - ), - githubWorkflowPublish := Seq(WorkflowStep.Sbt( - List("ci-release"), - env = Map( - "PGP_PASSPHRASE" -> "${{ secrets.PGP_PASSPHRASE }}", - "PGP_SECRET" -> "${{ secrets.PGP_SECRET }}", - "SONATYPE_PASSWORD" -> "${{ secrets.SONATYPE_PASSWORD }}", - "SONATYPE_USERNAME" -> "${{ secrets.SONATYPE_USERNAME }}" - ) - )), -)) + scalaVersion := Dependencies.versionOfScala, + + githubWorkflowTargetTags ++= Seq("v*"), + githubWorkflowArtifactUpload := false, + githubWorkflowJavaVersions := Seq(JavaSpec.temurin("17"), JavaSpec.temurin("21"), JavaSpec.temurin("25")), + githubWorkflowBuildPreamble ++= Seq( + WorkflowStep.Use( + ref = UseRef.Public("actions", "setup-node", "v4"), + name = Some("Setup Node.js"), + ), + WorkflowStep.Run( + commands = List("npm install"), + name = Some("Install npm dependencies"), + ), + ), + githubWorkflowPublishTargetBranches := Seq(RefPredicate.StartsWith(Ref.Tag("v"))), + githubWorkflowEnv += "JAVA_OPTS" -> "-Dfile.encoding=UTF-8 -Xmx4G", + githubWorkflowBuildMatrixFailFast := Some(false), + githubWorkflowBuildMatrixAdditions += "command" -> List("udash-jvm/test", "udash-js/test", "guide-selenium/test"), + githubWorkflowBuild := Seq( + WorkflowStep.Sbt( + commands = List("${{ matrix.command }}"), + name = Some("Run tests"), + ) + ), + // MiMa only compares bytecode — result is JDK-agnostic, so we run it in a dedicated + // single-JDK job instead of multiplying the work across the build matrix. + githubWorkflowAddedJobs += WorkflowJob( + id = "mima", + name = "Binary Compatibility Check", + scalas = List(Dependencies.versionOfScala), + javas = List(JavaSpec.temurin("21")), + steps = githubWorkflowJobSetup.value.toList :+ WorkflowStep.Sbt( + List("mimaReportBinaryIssues"), + name = Some("Check binary compatibility"), + ), + ), + githubWorkflowPublish := Seq( + WorkflowStep.Sbt( + List("ci-release"), + env = Map( + "PGP_PASSPHRASE" -> "${{ secrets.PGP_PASSPHRASE }}", + "PGP_SECRET" -> "${{ secrets.PGP_SECRET }}", + "SONATYPE_PASSWORD" -> "${{ secrets.SONATYPE_PASSWORD }}", + "SONATYPE_USERNAME" -> "${{ secrets.SONATYPE_USERNAME }}", + ), + ) + ), + ) +) val forIdeaImport = System.getProperty("idea.managed", "false").toBoolean && System.getProperty("idea.runid") == null val CompileAndTest = "test->test;compile->compile" @@ -74,10 +82,9 @@ val CompileAndTest = "test->test;compile->compile" val previousCompatibleVersions: Set[String] = Set("0.23.0") // Settings for JS tests run in browser -val browserCapabilities: Capabilities = { +val browserCapabilities: Capabilities = // requires gecko driver, see https://github.com/mozilla/geckodriver new FirefoxOptions().setHeadless(true).setLogLevel(FirefoxDriverLogLevel.WARN) -} val commonSettings = Seq( scalacOptions ++= Seq( @@ -94,7 +101,8 @@ val commonSettings = Seq( "-Xlint:_,-missing-interpolator,-unused", "-Xsource:3", "-Yrangepos", - "-Ybackend-parallelism", "8", + "-Ybackend-parallelism", + "8", "-Ycache-plugin-class-loader:last-modified", "-Ycache-macro-class-loader:last-modified", "-Xnon-strict-patmat-analysis", @@ -145,7 +153,7 @@ def sourceDirsSettings(baseMapper: File => File) = { def mkSourceDirs(base: File, scalaBinary: String, conf: String): Seq[File] = Seq( base / "src" / conf / "scala", base / "src" / conf / s"scala-$scalaBinary", - base / "src" / conf / "java" + base / "src" / conf / "java", ) def mkResourceDirs(base: File, conf: String): Seq[File] = Seq( @@ -154,12 +162,9 @@ def sourceDirsSettings(baseMapper: File => File) = { Seq( Compile / unmanagedSourceDirectories ++= mkSourceDirs(baseMapper(baseDirectory.value), scalaBinaryVersion.value, "main"), - Compile / unmanagedResourceDirectories ++= - mkResourceDirs(baseMapper(baseDirectory.value), "main"), - Test / unmanagedSourceDirectories ++= - mkSourceDirs(baseMapper(baseDirectory.value), scalaBinaryVersion.value, "test"), - Test / unmanagedResourceDirectories ++= - mkResourceDirs(baseMapper(baseDirectory.value), "test"), + Compile / unmanagedResourceDirectories ++= mkResourceDirs(baseMapper(baseDirectory.value), "main"), + Test / unmanagedSourceDirectories ++= mkSourceDirs(baseMapper(baseDirectory.value), scalaBinaryVersion.value, "test"), + Test / unmanagedResourceDirectories ++= mkResourceDirs(baseMapper(baseDirectory.value), "test"), ) } @@ -170,12 +175,11 @@ def jvmProject(proj: Project): Project = ) def jsProject(proj: Project): Project = - proj.in(proj.base / ".js") - .enablePlugins(ScalaJSPlugin, JSDependenciesPlugin) - .settings(commonJsSettings) + proj.in(proj.base / ".js").enablePlugins(ScalaJSPlugin, JSDependenciesPlugin).settings(commonJsSettings) def jsProjectFor(jsProj: Project, jvmProj: Project): Project = - jsProj.in(jvmProj.base / ".js") + jsProj + .in(jvmProj.base / ".js") .enablePlugins(ScalaJSPlugin, JSDependenciesPlugin) .configure(p => if (forIdeaImport) p.dependsOn(jvmProj) else p) .settings( @@ -185,15 +189,17 @@ def jsProjectFor(jsProj: Project, jvmProj: Project): Project = sourceDirsSettings(_.getParentFile), // workaround for some cross-compilation problems in IntelliJ - libraryDependencies ++= (if (forIdeaImport) (jvmProj / libraryDependencies).value else Seq.empty) + libraryDependencies ++= (if (forIdeaImport) (jvmProj / libraryDependencies).value else Seq.empty), ) -def frontendExecutable(proj: Project)( +def frontendExecutable( + proj: Project +)( staticsRoot: String, jsDeps: Def.Initialize[Seq[JSModuleID]], cssRenderer: Option[(Project, String)] = None, additionalAssetsDirectory: Def.Initialize[Task[Option[File]]] = Def.task(None), -) = { +) = proj .enablePlugins(ScalaJSPlugin, JSDependenciesPlugin, SbtWeb) .settings(commonJsSettings) @@ -204,7 +210,7 @@ def frontendExecutable(proj: Project)( jsDependencies ++= jsDeps.value, Compile / scalaJSUseMainModuleInitializer := true, - //library CSS settings + // library CSS settings Assets / LessKeys.cleancss := true, Assets / LessKeys.compress := true, Assets / LessKeys.strictMath := true, @@ -212,7 +218,7 @@ def frontendExecutable(proj: Project)( Assets / LessKeys.less / includeFilter := "assets.less", Assets / LessKeys.less / resourceManaged := (Compile / target).value / staticsRoot / "assets" / "styles", - //fastOptJS invokes less, which would fail without additional assets (e.g. prism.css) + // fastOptJS invokes less, which would fail without additional assets (e.g. prism.css) Assets / LessKeys.less := (Assets / LessKeys.less).dependsOn(Compile / copyAssets).value, Compile / copyAssets := { @@ -226,7 +232,7 @@ def frontendExecutable(proj: Project)( // (appropriate CSS file is inlined by less within UdashStatics/WebContent/guide/assets/styles/assets.min.css) IO.copyDirectory( (Assets / WebKeys.webJarsDirectory).value / WebKeys.webModulesLib.value / "font-awesome/webfonts", - assets / "webfonts" + assets / "webfonts", ) }, // a font-awesome WebJar is required on the classpath to execute the copyAssets task @@ -234,12 +240,14 @@ def frontendExecutable(proj: Project)( // Compiles CSS files and put them in the target directory compileCss := Def.taskDyn { - cssRenderer.map { case (rendererProject, rendererClass) => - val dir = (Compile / target).value / staticsRoot / "styles" - val path = dir.absolutePath - dir.mkdirs() - (rendererProject / Compile / runMain).toTask(s" $rendererClass $path false") - }.getOrElse(Def.task(())) + cssRenderer + .map { case (rendererProject, rendererClass) => + val dir = (Compile / target).value / staticsRoot / "styles" + val path = dir.absolutePath + dir.mkdirs() + (rendererProject / Compile / runMain).toTask(s" $rendererClass $path false") + } + .getOrElse(Def.task(())) }.value, // Compiles JS files without full optimizations @@ -248,27 +256,33 @@ def frontendExecutable(proj: Project)( // Compiles JS files with full optimizations compileAndOptimizeStatics := (Compile / fullOptJS / target).value / "UdashStatics", - compileAndOptimizeStatics := compileAndOptimizeStatics.dependsOn( - Compile / fullOptJS, Compile / copyAssets, Compile / compileCss - ).value, + compileAndOptimizeStatics := + compileAndOptimizeStatics + .dependsOn( + Compile / fullOptJS, + Compile / copyAssets, + Compile / compileCss, + ) + .value, // force fullOpt dependencies generation after fastOpt deps generation - Compile / packageMinifiedJSDependencies := - (Compile / packageMinifiedJSDependencies).dependsOn(Compile / packageJSDependencies).value, + Compile / packageMinifiedJSDependencies := (Compile / packageMinifiedJSDependencies) + .dependsOn(Compile / packageJSDependencies) + .value, // Target files for Scala.js plugin - Compile / fastOptJS / artifactPath := - (Compile / fastOptJS / target).value / staticsRoot / "scripts" / "frontend.js", - Compile / fullOptJS / artifactPath := - (Compile / fullOptJS / target).value / staticsRoot / "scripts" / "frontend.js", - Compile / packageJSDependencies / artifactPath := - (Compile / packageJSDependencies / target).value / staticsRoot / "scripts" / "frontend-deps.js", + Compile / fastOptJS / artifactPath := (Compile / fastOptJS / target).value / staticsRoot / "scripts" / + "frontend.js", + Compile / fullOptJS / artifactPath := (Compile / fullOptJS / target).value / staticsRoot / "scripts" / + "frontend.js", + Compile / packageJSDependencies / artifactPath := (Compile / packageJSDependencies / target).value / staticsRoot / + "scripts" / "frontend-deps.js", Compile / packageMinifiedJSDependencies / artifactPath := - (Compile / packageMinifiedJSDependencies / target).value / staticsRoot / "scripts" / "frontend-deps.js" + (Compile / packageMinifiedJSDependencies / target).value / staticsRoot / "scripts" / "frontend-deps.js", ) -} -lazy val udash = project.in(file(".")) +lazy val udash = project + .in(file(".")) .aggregate(`udash-jvm`, `udash-js`, guide) .settings( aggregateProjectSettings, @@ -277,40 +291,43 @@ lazy val udash = project.in(file(".")) //for simplifying Travis build matrix and project dependencies lazy val jvmLibraries = Seq[ProjectReference](macros, utils, core, rpc, rest, `rest-jetty`, i18n, auth, css) -lazy val `udash-jvm` = project.in(file(".jvm")) - .aggregate(jvmLibraries: _*) - .settings(aggregateProjectSettings) +lazy val `udash-jvm` = project.in(file(".jvm")).aggregate(jvmLibraries: _*).settings(aggregateProjectSettings) lazy val jsLibraries = Seq[ProjectReference]( - macros, `utils-js`, `core-js`, `rpc-js`, `rest-js`, `i18n-js`, `auth-js`, `css-js`, bootstrap4 + macros, + `utils-js`, + `core-js`, + `rpc-js`, + `rest-js`, + `i18n-js`, + `auth-js`, + `css-js`, + bootstrap4, ) -lazy val `udash-js` = project.in(file(".js")) - .aggregate(jsLibraries: _*) - .settings(aggregateProjectSettings) +lazy val `udash-js` = project.in(file(".js")).aggregate(jsLibraries: _*).settings(aggregateProjectSettings) -lazy val macros = project - .settings( - commonSettings, - libraryDependencies ++= Dependencies.macroDeps.value, - mimaPreviousArtifacts := Set.empty, // no need for MiMa checks - ) +lazy val macros = project.settings( + commonSettings, + libraryDependencies ++= Dependencies.macroDeps.value, + mimaPreviousArtifacts := Set.empty, // no need for MiMa checks +) lazy val utils = jvmProject(project) .dependsOn(macros) .settings( - libraryDependencies ++= Dependencies.utilsJvmDeps.value, + libraryDependencies ++= Dependencies.utilsJvmDeps.value ) lazy val `utils-js` = jsProjectFor(project, utils) .dependsOn(macros) .settings( - libraryDependencies ++= Dependencies.utilsSjsDeps.value, + libraryDependencies ++= Dependencies.utilsSjsDeps.value ) lazy val core = jvmProject(project) .dependsOn(utils % CompileAndTest) .settings( - libraryDependencies ++= Dependencies.coreJvmDeps.value, + libraryDependencies ++= Dependencies.coreJvmDeps.value ) lazy val `core-js` = jsProjectFor(project, core) @@ -323,55 +340,51 @@ lazy val `core-js` = jsProjectFor(project, core) lazy val rpc = jvmProject(project) .dependsOn(utils % CompileAndTest) .settings( - libraryDependencies ++= Dependencies.rpcJvmDeps.value, + libraryDependencies ++= Dependencies.rpcJvmDeps.value ) lazy val `rpc-js` = jsProjectFor(project, rpc) .dependsOn(`utils-js` % CompileAndTest) .settings( - jsDependencies ++= Dependencies.rpcJsDeps.value, + jsDependencies ++= Dependencies.rpcJsDeps.value ) lazy val rest = jvmProject(project) .dependsOn(utils % CompileAndTest) .settings( - libraryDependencies ++= Dependencies.restJvmDeps.value, + libraryDependencies ++= Dependencies.restJvmDeps.value ) lazy val `rest-js` = jsProjectFor(project, rest) .dependsOn(`utils-js` % CompileAndTest) .settings( - libraryDependencies ++= Dependencies.restSjsDeps.value, + libraryDependencies ++= Dependencies.restSjsDeps.value ) lazy val `rest-jetty` = jvmProject(project.in(file("rest/jetty"))) .dependsOn(rest % CompileAndTest) .settings( - libraryDependencies ++= Dependencies.restJettyDeps.value, + libraryDependencies ++= Dependencies.restJettyDeps.value ) -lazy val i18n = jvmProject(project) - .dependsOn(core % CompileAndTest, rpc % CompileAndTest) +lazy val i18n = jvmProject(project).dependsOn(core % CompileAndTest, rpc % CompileAndTest) -lazy val `i18n-js` = jsProjectFor(project, i18n) - .dependsOn(`core-js` % CompileAndTest, `rpc-js` % CompileAndTest) +lazy val `i18n-js` = jsProjectFor(project, i18n).dependsOn(`core-js` % CompileAndTest, `rpc-js` % CompileAndTest) -lazy val auth = jvmProject(project) - .dependsOn(core % CompileAndTest, rpc) +lazy val auth = jvmProject(project).dependsOn(core % CompileAndTest, rpc) -lazy val `auth-js` = jsProjectFor(project, auth) - .dependsOn(`core-js` % CompileAndTest, `rpc-js`) +lazy val `auth-js` = jsProjectFor(project, auth).dependsOn(`core-js` % CompileAndTest, `rpc-js`) lazy val css = jvmProject(project) .dependsOn(core % CompileAndTest) .settings( - libraryDependencies ++= Dependencies.cssJvmDeps.value, + libraryDependencies ++= Dependencies.cssJvmDeps.value ) lazy val `css-js` = jsProjectFor(project, css) .dependsOn(`core-js` % CompileAndTest) .settings( - libraryDependencies ++= Dependencies.cssSjsDeps.value, + libraryDependencies ++= Dependencies.cssSjsDeps.value ) lazy val bootstrap4 = jsProject(project) @@ -379,7 +392,7 @@ lazy val bootstrap4 = jsProject(project) .settings( testInBrowser, libraryDependencies ++= Dependencies.bootstrap4SjsDeps.value, - jsDependencies ++= Dependencies.bootstrap4JsDeps.value + jsDependencies ++= Dependencies.bootstrap4JsDeps.value, ) lazy val benchmarks = jsProject(project) @@ -402,9 +415,18 @@ val compileAndOptimizeStatics = taskKey[File]( "Compiles and optimizes JavaScript files and copies all assets to the target directory." ) -lazy val guide = project.in(file("guide")) - .aggregate(`guide-shared`, `guide-shared-js`, `guide-backend`, `guide-commons`, `guide-homepage`, - `guide-guide`, `guide-packager`, `guide-selenium`) +lazy val guide = project + .in(file("guide")) + .aggregate( + `guide-shared`, + `guide-shared-js`, + `guide-backend`, + `guide-commons`, + `guide-homepage`, + `guide-guide`, + `guide-packager`, + `guide-selenium`, + ) .settings( aggregateProjectSettings, ideSkipProject := true, @@ -450,7 +472,7 @@ lazy val `guide-homepage` = "UdashStatics/WebContent/homepage", Dependencies.homepageJsDeps, Some((`guide-backend`, "io.udash.web.styles.HomepageCssRenderer")), - Def.task(Some((`guide-commons` / sourceDirectory).value / "main" / "assets")) + Def.task(Some((`guide-commons` / sourceDirectory).value / "main" / "assets")), ) lazy val `guide-guide` = @@ -458,11 +480,12 @@ lazy val `guide-guide` = "UdashStatics/WebContent/guide", Dependencies.guideJsDeps, Some((`guide-backend`, "io.udash.web.styles.GuideCssRenderer")), - Def.task(Some((`guide-commons` / sourceDirectory).value / "main" / "assets")) + Def.task(Some((`guide-commons` / sourceDirectory).value / "main" / "assets")), ) lazy val `guide-packager` = - project.in(file("guide/packager")) + project + .in(file("guide/packager")) .dependsOn(`guide-backend`) .enablePlugins(JavaServerAppPackaging) .settings( @@ -478,17 +501,17 @@ lazy val `guide-packager` = Universal / mappings ++= { import Path.relativeTo val frontendStatics = (`guide-homepage` / Compile / compileAndOptimizeStatics).value - (frontendStatics.allPaths --- frontendStatics) pair relativeTo(frontendStatics.getParentFile) + (frontendStatics.allPaths --- frontendStatics).pair(relativeTo(frontendStatics.getParentFile)) }, // add guide statics to the package Universal / mappings ++= { import Path.relativeTo val frontendStatics = (`guide-guide` / Compile / compileAndOptimizeStatics).value - (frontendStatics.allPaths --- frontendStatics) pair relativeTo(frontendStatics.getParentFile) + (frontendStatics.allPaths --- frontendStatics).pair(relativeTo(frontendStatics.getParentFile)) }, - dockerExposedPorts += 8080, //should match ui.server.port + dockerExposedPorts += 8080, // should match ui.server.port dockerEnvVars += "DISABLE_FILE_LOGGING" -> "true", dockerBaseImage := "eclipse-temurin:17-jdk-jammy", dockerUpdateLatest := true, @@ -505,16 +528,19 @@ lazy val `guide-selenium` = Test / parallelExecution := false, - Test / compile := (Test / compile).dependsOn( - `guide-homepage` / Compile / compileStatics, - `guide-guide` / Compile / compileStatics, - ).value + Test / compile := (Test / compile) + .dependsOn( + `guide-homepage` / Compile / compileStatics, + `guide-guide` / Compile / compileStatics, + ) + .value, ) -lazy val benchmarksJVM = project.in(file("benchmarks")) +lazy val benchmarksJVM = project + .in(file("benchmarks")) .enablePlugins(JmhPlugin) .dependsOn(jvmLibraries.map(p => p: ClasspathDep[ProjectReference]): _*) .settings( commonSettings, noPublishSettings, - ) \ No newline at end of file + ) diff --git a/core/.js/src/main/scala/io/udash/Application.scala b/core/.js/src/main/scala/io/udash/Application.scala index ee4eb452f..bfd398a9c 100644 --- a/core/.js/src/main/scala/io/udash/Application.scala +++ b/core/.js/src/main/scala/io/udash/Application.scala @@ -10,17 +10,21 @@ import org.scalajs.dom.{DocumentReadyState, Element, Event, EventListenerOptions import scala.util.Try -/** - * Root application which is used to start single instance of app. - * - * @param routingRegistry [[io.udash.routing.RoutingRegistry]] implementation, which will be used to match [[io.udash.core.Url]] to [[io.udash.core.State]] - * @param viewFactoryRegistry [[io.udash.core.ViewFactoryRegistry]] implementation, which will be used to match [[io.udash.core.State]] into [[io.udash.core.ViewFactory]] - * @tparam HierarchyRoot Should be a sealed trait which extends [[io.udash.core.State]]. - */ -class Application[HierarchyRoot >: Null <: GState[HierarchyRoot] : PropertyCreator]( +/** Root application which is used to start single instance of app. + * + * @param routingRegistry + * [[io.udash.routing.RoutingRegistry]] implementation, which will be used to match [[io.udash.core.Url]] to + * [[io.udash.core.State]] + * @param viewFactoryRegistry + * [[io.udash.core.ViewFactoryRegistry]] implementation, which will be used to match [[io.udash.core.State]] into + * [[io.udash.core.ViewFactory]] + * @tparam HierarchyRoot + * Should be a sealed trait which extends [[io.udash.core.State]]. + */ +class Application[HierarchyRoot >: Null <: GState[HierarchyRoot]: PropertyCreator]( routingRegistry: RoutingRegistry[HierarchyRoot], viewFactoryRegistry: ViewFactoryRegistry[HierarchyRoot], - urlChangeProvider: UrlChangeProvider = new WindowUrlFragmentChangeProvider + urlChangeProvider: UrlChangeProvider = new WindowUrlFragmentChangeProvider, ) extends CrossLogging { private var rootElement: Element = _ @@ -31,11 +35,11 @@ class Application[HierarchyRoot >: Null <: GState[HierarchyRoot] : PropertyCreat private def handleUrl(url: Url, fullReload: Boolean = false): Try[Unit] = routingEngine.handleUrl(url, fullReload).recover { case t => handleRoutingFailure(t) } - /** - * Starts the application using selected element as root. - * - * @param attachElement Root element of application. - */ + /** Starts the application using selected element as root. + * + * @param attachElement + * Root element of application. + */ final def run(attachElement: Element): Unit = { rootElement = attachElement @@ -44,16 +48,23 @@ class Application[HierarchyRoot >: Null <: GState[HierarchyRoot] : PropertyCreat handleUrl(urlChangeProvider.currentFragment) } - /** - * Starts the application using selectors to find root element. Handles waiting for document to be ready. - * - * @param selectors A DOMString containing one or more selectors to match. - * This string must be a valid CSS selector string; if it isn't, a native SyntaxError exception is thrown. - * See https://developer.mozilla.org/en-US/docs/Web/API/Document_object_model/Locating_DOM_elements_using_selectors. - * @param onDocumentReady Callback ran on the application root element before the application is started but once the DOM document is ready. - * @param onApplicationStarted Callback ran on the application root element after the application is started. - */ - final def run(selectors: String, onDocumentReady: Element => Unit = _ => (), onApplicationStarted: Element => Unit = _ => ()): Unit = { + /** Starts the application using selectors to find root element. Handles waiting for document to be ready. + * + * @param selectors + * A DOMString containing one or more selectors to match. This string must be a valid CSS selector string; if it + * isn't, a native SyntaxError exception is thrown. See + * https://developer.mozilla.org/en-US/docs/Web/API/Document_object_model/Locating_DOM_elements_using_selectors. + * @param onDocumentReady + * Callback ran on the application root element before the application is started but once the DOM document is + * ready. + * @param onApplicationStarted + * Callback ran on the application root element after the application is started. + */ + final def run( + selectors: String, + onDocumentReady: Element => Unit = _ => (), + onApplicationStarted: Element => Unit = _ => (), + ): Unit = { def onReady(): Unit = { val rootElement = dom.document.querySelector(selectors) onDocumentReady(rootElement) @@ -61,21 +72,25 @@ class Application[HierarchyRoot >: Null <: GState[HierarchyRoot] : PropertyCreat onApplicationStarted(rootElement) } if (dom.document.readyState != DocumentReadyState.loading) onReady() - else dom.document.addEventListener("DOMContentLoaded", { (_: Event) => onReady() }, new EventListenerOptions { - once = true - passive = true - }) + else + dom.document.addEventListener( + "DOMContentLoaded", + (_: Event) => onReady(), + new EventListenerOptions { + once = true + passive = true + }, + ) } def reload(): Unit = handleUrl(urlChangeProvider.currentFragment, fullReload = true) - /** - * Registers callback which will be called after routing failure. - * - * The callbacks are executed in order of registration. Registration operations don't preserve callbacks order. - * Each callback is executed once, exceptions thrown in callbacks are swallowed. - */ + /** Registers callback which will be called after routing failure. + * + * The callbacks are executed in order of registration. Registration operations don't preserve callbacks order. Each + * callback is executed once, exceptions thrown in callbacks are swallowed. + */ def onRoutingFailure(listener: routingFailureListeners.CallbackType): Registration = routingFailureListeners.register(listener) @@ -84,12 +99,13 @@ class Application[HierarchyRoot >: Null <: GState[HierarchyRoot] : PropertyCreat routingFailureListeners.fire(ex) } - /** - * Changes application routing state to the provided one. - * - * @param state New application routing state, - * @param replaceCurrent indicates whether new state should replace the current one in history - */ + /** Changes application routing state to the provided one. + * + * @param state + * New application routing state, + * @param replaceCurrent + * indicates whether new state should replace the current one in history + */ def goTo(state: HierarchyRoot, replaceCurrent: Boolean = false): Unit = { val url = routingRegistry.matchState(state) urlChangeProvider.changeFragment(url, replaceCurrent) @@ -99,17 +115,17 @@ class Application[HierarchyRoot >: Null <: GState[HierarchyRoot] : PropertyCreat def redirectTo(url: Url): Unit = urlChangeProvider.changeUrl(url) - /** - * Register callback for routing state change. - * - * @param callback Callback getting newState and oldState as arguments. - */ + /** Register callback for routing state change. + * + * @param callback + * Callback getting newState and oldState as arguments. + */ def onStateChange(callback: StateChangeEvent[HierarchyRoot] => Unit): Registration = routingEngine.onStateChange(callback) - /** - * @return URL matched to the provided state. - */ + /** @return + * URL matched to the provided state. + */ def matchState(state: HierarchyRoot): Url = routingRegistry.matchState(state) diff --git a/core/.js/src/main/scala/io/udash/bindings/Bindings.scala b/core/.js/src/main/scala/io/udash/bindings/Bindings.scala index 7a4ee5548..c56034442 100644 --- a/core/.js/src/main/scala/io/udash/bindings/Bindings.scala +++ b/core/.js/src/main/scala/io/udash/bindings/Bindings.scala @@ -37,290 +37,379 @@ trait Bindings { def emptyStringNode(): Node = document.createTextNode("") - /** - * Renders component with provided timeout. - * It's useful to render heavy components after displaying the main view. - */ + /** Renders component with provided timeout. It's useful to render heavy components after displaying the main view. + */ def queuedNode(component: => Seq[Node], timeout: Int = 0): Modifier[Element] = t => { val el = document.createElement("div") t.appendChild(el) window.setTimeout(() => t.replaceChildren(Seq(el), component), timeout) } - /** - * Use it to bind value of property into DOM structure. Value of the property will be rendered as text node. (Using .toString method.) - * If property value is null, empty text node will be added. - * - * @param property `Property` to bind. - * @return property binding. - */ + /** Use it to bind value of property into DOM structure. Value of the property will be rendered as text node. (Using + * .toString method.) If property value is null, empty text node will be added. + * + * @param property + * `Property` to bind. + * @return + * property binding. + */ def bind(property: ReadableProperty[_]): Binding = new SimplePropertyModifier(property) - /** - * Shows provided DOM elements only if property value is `true`. - * - * @param property `Property` to check. - * @param elements `Element`s to show if property value is `true`. - * @return property binding. - */ + /** Shows provided DOM elements only if property value is `true`. + * + * @param property + * `Property` to check. + * @param elements + * `Element`s to show if property value is `true`. + * @return + * property binding. + */ def showIf(property: ReadableProperty[Boolean])(elements: => Seq[Node]): Binding = showIfElse(property)(elements, Seq.empty) - /** - * Shows provided DOM elements only if property value is `true`. - * - * @param property `Property` to check. - * @param elements `Element`s to show if property value is `true`. - * @param customElementsReplace takes root element, old children and new children. It should return `true`, - * if it did not replace elements in DOM. Is such a case the default implementation - * will replace the elements. Otherwise you have to replace elements in DOM manually. - * @return property binding. - */ - def showIf(property: ReadableProperty[Boolean], customElementsReplace: DOMManipulator.ReplaceMethod) - (elements: => Seq[Node]): Binding = + /** Shows provided DOM elements only if property value is `true`. + * + * @param property + * `Property` to check. + * @param elements + * `Element`s to show if property value is `true`. + * @param customElementsReplace + * takes root element, old children and new children. It should return `true`, if it did not replace elements in + * DOM. Is such a case the default implementation will replace the elements. Otherwise you have to replace elements + * in DOM manually. + * @return + * property binding. + */ + def showIf( + property: ReadableProperty[Boolean], + customElementsReplace: DOMManipulator.ReplaceMethod, + )( + elements: => Seq[Node] + ): Binding = showIfElse(property, customElementsReplace)(elements, Seq.empty) - /** - * Switches provided DOM elements depending on property value. - * - * @param property `Property` to check. - * @param elements `Element`s to show if property value is `true`. - * @param elseElements `Element`s to show if property value is `false`. - * @return property binding. - */ + /** Switches provided DOM elements depending on property value. + * + * @param property + * `Property` to check. + * @param elements + * `Element`s to show if property value is `true`. + * @param elseElements + * `Element`s to show if property value is `false`. + * @return + * property binding. + */ def showIfElse(property: ReadableProperty[Boolean])(elements: => Seq[Node], elseElements: => Seq[Node]): Binding = produce(property)(if (_) elements else elseElements) - /** - * Switches provided DOM elements depending on property value. - * - * @param property `Property` to check. - * @param elements `Element`s to show if property value is `true`. - * @param elseElements `Element`s to show if property value is `false`. - * @param customElementsReplace takes root element, old children and new children. It should return `true`, - * if it did not replace elements in DOM. Is such a case the default implementation - * will replace the elements. Otherwise you have to replace elements in DOM manually. - * @return property binding. - */ - def showIfElse(property: ReadableProperty[Boolean], customElementsReplace: DOMManipulator.ReplaceMethod) - (elements: => Seq[Node], elseElements: => Seq[Node]): Binding = + /** Switches provided DOM elements depending on property value. + * + * @param property + * `Property` to check. + * @param elements + * `Element`s to show if property value is `true`. + * @param elseElements + * `Element`s to show if property value is `false`. + * @param customElementsReplace + * takes root element, old children and new children. It should return `true`, if it did not replace elements in + * DOM. Is such a case the default implementation will replace the elements. Otherwise you have to replace elements + * in DOM manually. + * @return + * property binding. + */ + def showIfElse( + property: ReadableProperty[Boolean], + customElementsReplace: DOMManipulator.ReplaceMethod, + )( + elements: => Seq[Node], + elseElements: => Seq[Node], + ): Binding = produce(property, customElementsReplace, checkNull = true)(if (_) elements else elseElements) - /** - * Use it to bind property into DOM structure, given `builder` will be used to generate DOM element on every value change. - * If property value is null, empty text node will be added as placeholder. - * - * @param property `Property` to bind. - * @param builder `Element` builder which will be used to create HTML element. - * @param checkNull if it is true, then null value of property will result in rendering empty text node. - * if it is false, then null value has to be handled by builder. - * @return property binding. - */ + /** Use it to bind property into DOM structure, given `builder` will be used to generate DOM element on every value + * change. If property value is null, empty text node will be added as placeholder. + * + * @param property + * `Property` to bind. + * @param builder + * `Element` builder which will be used to create HTML element. + * @param checkNull + * if it is true, then null value of property will result in rendering empty text node. if it is false, then null + * value has to be handled by builder. + * @return + * property binding. + */ def produce[T](property: ReadableProperty[T], checkNull: Boolean = true)(builder: T => Seq[Node]): Binding = produce(property, DOMManipulator.DefaultElementReplace, checkNull)(builder) - /** - * Use it to bind property into DOM structure, given `builder` will be used to generate DOM element on every value change. - * If property value is null, empty text node will be added as placeholder. - * - * @param property `Property` to bind. - * @param builder `Element` builder which will be used to create HTML element. - * @param checkNull if it is true, then null value of property will result in rendering empty text node. - * if it is false, then null value has to be handled by builder. - * @param customElementsReplace takes root element, old children and new children. It should return `true`, - * if it did not replace elements in DOM. Is such a case the default implementation - * will replace the elements. Otherwise you have to replace elements in DOM manually. - * @return property binding. - */ - def produce[T](property: ReadableProperty[T], customElementsReplace: DOMManipulator.ReplaceMethod, checkNull: Boolean) - (builder: T => Seq[Node]): Binding = + /** Use it to bind property into DOM structure, given `builder` will be used to generate DOM element on every value + * change. If property value is null, empty text node will be added as placeholder. + * + * @param property + * `Property` to bind. + * @param builder + * `Element` builder which will be used to create HTML element. + * @param checkNull + * if it is true, then null value of property will result in rendering empty text node. if it is false, then null + * value has to be handled by builder. + * @param customElementsReplace + * takes root element, old children and new children. It should return `true`, if it did not replace elements in + * DOM. Is such a case the default implementation will replace the elements. Otherwise you have to replace elements + * in DOM manually. + * @return + * property binding. + */ + def produce[T]( + property: ReadableProperty[T], + customElementsReplace: DOMManipulator.ReplaceMethod, + checkNull: Boolean, + )( + builder: T => Seq[Node] + ): Binding = new PropertyModifier[T](property, builder, checkNull, customElementsReplace) - /** - * Use it to bind property into DOM structure, given `builder` will be used to generate DOM element on every value change. - * If property value is null, empty text node will be added as placeholder.

- * - * The builder takes nested bindings interceptor - it should be used if you want to create another binding inside - * this builder. This prevents memory leaks by killing nested bindings on property change.

- * - * For example: - * {{{ - * produceWithNested(property) { case (data, nested) => - * div(data, nested(produce(anotherProperty) { innerData => span(innerData).render })).render - * } - * }}} - * - * @param property `Property` to bind. - * @param builder `Element` builder which will be used to create HTML element. - * @param checkNull if it is true, then null value of property will result in rendering empty text node. - * if it is false, then null value has to be handled by builder. - * @return property binding. - */ - def produceWithNested[T](property: ReadableProperty[T], checkNull: Boolean = true) - (builder: (T, Binding.NestedInterceptor) => Seq[Node]): Binding = + /** Use it to bind property into DOM structure, given `builder` will be used to generate DOM element on every value + * change. If property value is null, empty text node will be added as placeholder.

+ * + * The builder takes nested bindings interceptor - it should be used if you want to create another binding inside + * this builder. This prevents memory leaks by killing nested bindings on property change.

+ * + * For example: + * {{{ + * produceWithNested(property) { case (data, nested) => + * div(data, nested(produce(anotherProperty) { innerData => span(innerData).render })).render + * } + * }}} + * + * @param property + * `Property` to bind. + * @param builder + * `Element` builder which will be used to create HTML element. + * @param checkNull + * if it is true, then null value of property will result in rendering empty text node. if it is false, then null + * value has to be handled by builder. + * @return + * property binding. + */ + def produceWithNested[T]( + property: ReadableProperty[T], + checkNull: Boolean = true, + )( + builder: (T, Binding.NestedInterceptor) => Seq[Node] + ): Binding = produceWithNested(property, DOMManipulator.DefaultElementReplace, checkNull)(builder) - /** - * Use it to bind property into DOM structure, given `builder` will be used to generate DOM element on every value change. - * If property value is null, empty text node will be added as placeholder.

- * - * The builder takes nested bindings interceptor - it should be used if you want to create another binding inside - * this builder. This prevents memory leaks by killing nested bindings on property change.

- * - * For example: - * {{{ - * produceWithNested(property) { case (data, nested) => - * div(data, nested(produce(anotherProperty) { innerData => span(innerData).render })).render - * } - * }}} - * - * @param property `Property` to bind. - * @param builder `Element` builder which will be used to create HTML element. - * @param checkNull if it is true, then null value of property will result in rendering empty text node. - * if it is false, then null value has to be handled by builder. - * @param customElementsReplace takes root element, old children and new children. It should return `true`, - * if it did not replace elements in DOM. Is such a case the default implementation - * will replace the elements. Otherwise you have to replace elements in DOM manually. - * @return property binding. - */ - def produceWithNested[T](property: ReadableProperty[T], customElementsReplace: DOMManipulator.ReplaceMethod, checkNull: Boolean) - (builder: (T, Binding.NestedInterceptor) => Seq[Node]): Binding = + /** Use it to bind property into DOM structure, given `builder` will be used to generate DOM element on every value + * change. If property value is null, empty text node will be added as placeholder.

+ * + * The builder takes nested bindings interceptor - it should be used if you want to create another binding inside + * this builder. This prevents memory leaks by killing nested bindings on property change.

+ * + * For example: + * {{{ + * produceWithNested(property) { case (data, nested) => + * div(data, nested(produce(anotherProperty) { innerData => span(innerData).render })).render + * } + * }}} + * + * @param property + * `Property` to bind. + * @param builder + * `Element` builder which will be used to create HTML element. + * @param checkNull + * if it is true, then null value of property will result in rendering empty text node. if it is false, then null + * value has to be handled by builder. + * @param customElementsReplace + * takes root element, old children and new children. It should return `true`, if it did not replace elements in + * DOM. Is such a case the default implementation will replace the elements. Otherwise you have to replace elements + * in DOM manually. + * @return + * property binding. + */ + def produceWithNested[T]( + property: ReadableProperty[T], + customElementsReplace: DOMManipulator.ReplaceMethod, + checkNull: Boolean, + )( + builder: (T, Binding.NestedInterceptor) => Seq[Node] + ): Binding = new PropertyModifier[T](property, builder, checkNull, customElementsReplace) - /** - * Use it to bind sequence property into DOM structure, given `builder` will be used to generate DOM element on every value change. - * Notice that on every property change, whole element representing property will be rendered again. - * - * @param property `Property` to bind. - * @param builder `Element` builder which will be used to create HTML element. `Seq` passed to the builder cannot be null. - * @return property binding. - */ - def produce[T](property: ReadableSeqProperty[T, _ <: ReadableProperty[T]]) - (builder: Seq[T] => Seq[Node]): Binding = + /** Use it to bind sequence property into DOM structure, given `builder` will be used to generate DOM element on every + * value change. Notice that on every property change, whole element representing property will be rendered again. + * + * @param property + * `Property` to bind. + * @param builder + * `Element` builder which will be used to create HTML element. `Seq` passed to the builder cannot be null. + * @return + * property binding. + */ + def produce[T](property: ReadableSeqProperty[T, _ <: ReadableProperty[T]])(builder: Seq[T] => Seq[Node]): Binding = produce(property, DOMManipulator.DefaultElementReplace)(builder) - /** - * Use it to bind sequence property into DOM structure, given `builder` will be used to generate DOM element on every value change. - * Notice that on every property change, whole element representing property will be rendered again. - * - * @param property `Property` to bind. - * @param builder `Element` builder which will be used to create HTML element. Seq passed to the builder can not be null. - * @param customElementsReplace takes root element, old children and new children. It should return `true`, - * if it did not replace elements in DOM. Is such a case the default implementation - * will replace the elements. Otherwise you have to replace elements in DOM manually. - * @return property binding. - */ - def produce[T](property: ReadableSeqProperty[T, _ <: ReadableProperty[T]], customElementsReplace: DOMManipulator.ReplaceMethod) - (builder: Seq[T] => Seq[Node]): Binding = + /** Use it to bind sequence property into DOM structure, given `builder` will be used to generate DOM element on every + * value change. Notice that on every property change, whole element representing property will be rendered again. + * + * @param property + * `Property` to bind. + * @param builder + * `Element` builder which will be used to create HTML element. Seq passed to the builder can not be null. + * @param customElementsReplace + * takes root element, old children and new children. It should return `true`, if it did not replace elements in + * DOM. Is such a case the default implementation will replace the elements. Otherwise you have to replace elements + * in DOM manually. + * @return + * property binding. + */ + def produce[T]( + property: ReadableSeqProperty[T, _ <: ReadableProperty[T]], + customElementsReplace: DOMManipulator.ReplaceMethod, + )( + builder: Seq[T] => Seq[Node] + ): Binding = new SeqAsValueModifier[T](property, builder, customElementsReplace) - /** - * Use it to bind sequence property into DOM structure, given `builder` will be used to generate DOM element on every value change. - * Notice that on every property change, whole element representing property will be rendered again.

- * - * The builder takes nested bindings interceptor - it should be used if you want to create another binding inside - * this builder. This prevents memory leaks by killing nested bindings on property change.

- * - * @param property `Property` to bind. - * @param builder `Element` builder which will be used to create HTML element. Seq passed to the builder can not be null. - * @return property binding. - */ - def produceWithNested[T](property: ReadableSeqProperty[T, _ <: ReadableProperty[T]]) - (builder: (Seq[T], Binding.NestedInterceptor) => Seq[Node]): Binding = + /** Use it to bind sequence property into DOM structure, given `builder` will be used to generate DOM element on every + * value change. Notice that on every property change, whole element representing property will be rendered + * again.

+ * + * The builder takes nested bindings interceptor - it should be used if you want to create another binding inside + * this builder. This prevents memory leaks by killing nested bindings on property change.

+ * + * @param property + * `Property` to bind. + * @param builder + * `Element` builder which will be used to create HTML element. Seq passed to the builder can not be null. + * @return + * property binding. + */ + def produceWithNested[T]( + property: ReadableSeqProperty[T, _ <: ReadableProperty[T]] + )( + builder: (Seq[T], Binding.NestedInterceptor) => Seq[Node] + ): Binding = produceWithNested(property, DOMManipulator.DefaultElementReplace)(builder) - /** - * Use it to bind sequence property into DOM structure, given `builder` will be used to generate DOM element on every value change. - * Notice that on every property change, whole element representing property will be rendered again.

- * - * The builder takes nested bindings interceptor - it should be used if you want to create another binding inside - * this builder. This prevents memory leaks by killing nested bindings on property change.

- * - * @param property `Property` to bind. - * @param builder `Element` builder which will be used to create HTML element. Seq passed to the builder can not be null. - * @param customElementsReplace takes root element, old children and new children. It should return `true`, - * if it did not replace elements in DOM. Is such a case the default implementation - * will replace the elements. Otherwise you have to replace elements in DOM manually. - * @return property binding. - */ - def produceWithNested[T](property: ReadableSeqProperty[T, _ <: ReadableProperty[T]], customElementsReplace: DOMManipulator.ReplaceMethod) - (builder: (Seq[T], Binding.NestedInterceptor) => Seq[Node]): Binding = + /** Use it to bind sequence property into DOM structure, given `builder` will be used to generate DOM element on every + * value change. Notice that on every property change, whole element representing property will be rendered + * again.

+ * + * The builder takes nested bindings interceptor - it should be used if you want to create another binding inside + * this builder. This prevents memory leaks by killing nested bindings on property change.

+ * + * @param property + * `Property` to bind. + * @param builder + * `Element` builder which will be used to create HTML element. Seq passed to the builder can not be null. + * @param customElementsReplace + * takes root element, old children and new children. It should return `true`, if it did not replace elements in + * DOM. Is such a case the default implementation will replace the elements. Otherwise you have to replace elements + * in DOM manually. + * @return + * property binding. + */ + def produceWithNested[T]( + property: ReadableSeqProperty[T, _ <: ReadableProperty[T]], + customElementsReplace: DOMManipulator.ReplaceMethod, + )( + builder: (Seq[T], Binding.NestedInterceptor) => Seq[Node] + ): Binding = new SeqAsValueModifier[T](property, builder, customElementsReplace) - /** - * Use it to bind sequence property into DOM structure. This method cares about adding new elements which appears in - * sequence and also removes those which were removed. You only need to provide builder which is used to - * create HTML element for each sequence member.
- * Note: This will handle only structure changes, if you want to handle concrete subproperties changes, you should use - * another binding method inside `builder`. - * - * @param property `Property` to bind. - * @param builder `Builder` which is used for every element. - * @param customElementsInsert takes root element, ref node and new children. It should return `true`, - * if it does not insert elements in DOM. Is such a case the default implementation - * will insert the elements. Otherwise you have to insert elements in DOM manually. - * @param customElementsReplace takes root element, old children and new children. It should return `true`, - * if it did not replace elements in DOM. Is such a case the default implementation - * will replace the elements. Otherwise you have to replace elements in DOM manually. - * @return property binding. - */ - def repeat[T, E <: ReadableProperty[T]](property: ReadableSeqProperty[T, E], + /** Use it to bind sequence property into DOM structure. This method cares about adding new elements which appears in + * sequence and also removes those which were removed. You only need to provide builder which is used to create HTML + * element for each sequence member.
Note: This will handle only structure changes, if you want to + * handle concrete subproperties changes, you should use another binding method inside `builder`. + * + * @param property + * `Property` to bind. + * @param builder + * `Builder` which is used for every element. + * @param customElementsInsert + * takes root element, ref node and new children. It should return `true`, if it does not insert elements in DOM. + * Is such a case the default implementation will insert the elements. Otherwise you have to insert elements in DOM + * manually. + * @param customElementsReplace + * takes root element, old children and new children. It should return `true`, if it did not replace elements in + * DOM. Is such a case the default implementation will replace the elements. Otherwise you have to replace elements + * in DOM manually. + * @return + * property binding. + */ + def repeat[T, E <: ReadableProperty[T]]( + property: ReadableSeqProperty[T, E], customElementsReplace: DOMManipulator.ReplaceMethod = DOMManipulator.DefaultElementReplace, - customElementsInsert: DOMManipulator.InsertMethod = DOMManipulator.DefaultElementInsert) - (builder: E => Seq[Node]): Binding = + customElementsInsert: DOMManipulator.InsertMethod = DOMManipulator.DefaultElementInsert, + )( + builder: E => Seq[Node] + ): Binding = new SeqPropertyModifier[T, E](property, builder, customElementsReplace, customElementsInsert) - /** - * Use it to bind sequence property into DOM structure. This method cares about adding new elements which appears in - * sequence and also removes those which were removed. You only need to provide builder which is used to - * create HTML element for each sequence member.
- * Note: This will handle only structure changes, if you want to handle concrete subproperties changes, you should use - * another binding method inside `builder`.

- * - * The builder takes nested bindings interceptor - it should be used if you want to create another binding inside - * this builder. This prevents memory leaks by killing nested bindings on property change.

- * - * @param property `Property` to bind. - * @param builder `Builder` which is used for every element. - * @param customElementsInsert takes root element, ref node and new children. It should return `true`, - * if it does not insert elements in DOM. Is such a case the default implementation - * will insert the elements. Otherwise you have to insert elements in DOM manually. - * @param customElementsReplace takes root element, old children and new children. It should return `true`, - * if it did not replace elements in DOM. Is such a case the default implementation - * will replace the elements. Otherwise you have to replace elements in DOM manually. - * @return property binding. - */ - def repeatWithNested[T, E <: ReadableProperty[T]](property: ReadableSeqProperty[T, E], + /** Use it to bind sequence property into DOM structure. This method cares about adding new elements which appears in + * sequence and also removes those which were removed. You only need to provide builder which is used to create HTML + * element for each sequence member.
Note: This will handle only structure changes, if you want to + * handle concrete subproperties changes, you should use another binding method inside `builder`.

+ * + * The builder takes nested bindings interceptor - it should be used if you want to create another binding inside + * this builder. This prevents memory leaks by killing nested bindings on property change.

+ * + * @param property + * `Property` to bind. + * @param builder + * `Builder` which is used for every element. + * @param customElementsInsert + * takes root element, ref node and new children. It should return `true`, if it does not insert elements in DOM. + * Is such a case the default implementation will insert the elements. Otherwise you have to insert elements in DOM + * manually. + * @param customElementsReplace + * takes root element, old children and new children. It should return `true`, if it did not replace elements in + * DOM. Is such a case the default implementation will replace the elements. Otherwise you have to replace elements + * in DOM manually. + * @return + * property binding. + */ + def repeatWithNested[T, E <: ReadableProperty[T]]( + property: ReadableSeqProperty[T, E], customElementsReplace: DOMManipulator.ReplaceMethod = DOMManipulator.DefaultElementReplace, - customElementsInsert: DOMManipulator.InsertMethod = DOMManipulator.DefaultElementInsert) - (builder: (E, Binding.NestedInterceptor) => Seq[Node]): Binding = + customElementsInsert: DOMManipulator.InsertMethod = DOMManipulator.DefaultElementInsert, + )( + builder: (E, Binding.NestedInterceptor) => Seq[Node] + ): Binding = new SeqPropertyModifier[T, E](property, builder, customElementsReplace, customElementsInsert) - /** - * Use it to bind sequence property into DOM structure. This method cares about adding new elements which appears in - * sequence and also removes those which were removed. You only need to provide builder which is used to - * create HTML element for each sequence member. This modifier provides also property with element index in sequence.
- * Note: This will handle only structure changes, if you want to handle concrete subproperties changes, you should use - * another binding method inside `builder`.

- * - * The builder takes nested bindings interceptor - it should be used if you want to create another binding inside - * this builder. This prevents memory leaks by killing nested bindings on property change.

- * - * @param property `Property` to bind. - * @param builder `Builder` which is used for every element. - * @param customElementsInsert takes root element, ref node and new children. It should return `true`, - * if it does not insert elements in DOM. Is such a case the default implementation - * will insert the elements. Otherwise you have to insert elements in DOM manually. - * @param customElementsReplace takes root element, old children and new children. It should return `true`, - * if it did not replace elements in DOM. Is such a case the default implementation - * will replace the elements. Otherwise you have to replace elements in DOM manually. - * @return property binding. - */ - def repeatWithIndex[T, E <: ReadableProperty[T]](property: ReadableSeqProperty[T, E], + /** Use it to bind sequence property into DOM structure. This method cares about adding new elements which appears in + * sequence and also removes those which were removed. You only need to provide builder which is used to create HTML + * element for each sequence member. This modifier provides also property with element index in sequence.
+ * Note: This will handle only structure changes, if you want to handle concrete subproperties changes, you + * should use another binding method inside `builder`.

+ * + * The builder takes nested bindings interceptor - it should be used if you want to create another binding inside + * this builder. This prevents memory leaks by killing nested bindings on property change.

+ * + * @param property + * `Property` to bind. + * @param builder + * `Builder` which is used for every element. + * @param customElementsInsert + * takes root element, ref node and new children. It should return `true`, if it does not insert elements in DOM. + * Is such a case the default implementation will insert the elements. Otherwise you have to insert elements in DOM + * manually. + * @param customElementsReplace + * takes root element, old children and new children. It should return `true`, if it did not replace elements in + * DOM. Is such a case the default implementation will replace the elements. Otherwise you have to replace elements + * in DOM manually. + * @return + * property binding. + */ + def repeatWithIndex[T, E <: ReadableProperty[T]]( + property: ReadableSeqProperty[T, E], customElementsReplace: DOMManipulator.ReplaceMethod = DOMManipulator.DefaultElementReplace, - customElementsInsert: DOMManipulator.InsertMethod = DOMManipulator.DefaultElementInsert) - (builder: (E, ReadableProperty[Int], Binding.NestedInterceptor) => Seq[Node]): Binding = + customElementsInsert: DOMManipulator.InsertMethod = DOMManipulator.DefaultElementInsert, + )( + builder: (E, ReadableProperty[Int], Binding.NestedInterceptor) => Seq[Node] + ): Binding = new SeqPropertyWithIndexModifier[T, E](property, builder, customElementsReplace, customElementsInsert) implicit def toAttrOps(attr: Attr): AttrOps = new AttrOps(attr) @@ -328,6 +417,7 @@ trait Bindings { implicit def toPropertyOps[T](property: ReadableProperty[T]): PropertyOps[T] = new PropertyOps(property) implicit final class InlineStyleOps[T: HasCssName](style: T) { + /** Sets style on element. */ def applyTo(element: HTMLElement, value: String = ""): Unit = element.style.setProperty(HasCssName[T].cssName(style), value) @@ -344,10 +434,9 @@ trait Bindings { case _ => } - /** - * Synchronises style value with property content by adding it when property is not null and - * condition property is 'true' and removing otherwise. - */ + /** Synchronises style value with property content by adding it when property is not null and condition property is + * 'true' and removing otherwise. + */ def bindIf(property: ReadableProperty[String], conditionProperty: ReadableProperty[Boolean]): Binding = property.combine(conditionProperty)((_, _)).reactiveApply { case (elem: HTMLElement, (null, _) | (_, false)) => removeFrom(elem) @@ -361,30 +450,35 @@ trait Bindings { object Bindings extends Bindings { final class AttrOps(private val attr: Attr) extends AnyVal { + /** Use this to bind value which is nullable. If the value is null, attribute will be removed. */ def :?=[B, T](value: T)(implicit ev: AttrValue[B, T]): Modifier[B] = if (value == null) new EmptyModifier[B] else AttrPair(attr, value, ev) - /** - * Use this to add more events listeners to an attribute (:= always overrides previous binding). - * If callback returns true, other listeners which are queued will not be invoked. - * If callback returns false, next callback in the queue will be invoked. - * Results other than booleans are treated as false - they don't prevent event propagation. - */ + /** Use this to add more events listeners to an attribute (:= always overrides previous binding). If callback + * returns true, other listeners which are queued will not be invoked. If callback returns false, next callback in + * the queue will be invoked. Results other than booleans are treated as false - they don't prevent event + * propagation. + */ def :+=[T <: Event](callback: T => Any): Modifier[Element] = - AttrPair(attr, callback, (el: Element, attr: Attr, callback: T => Any) => { - val dyn: js.Dynamic = el.asInstanceOf[js.Dynamic] - val existingCallbacks: js.Function1[T, Boolean] = dyn.selectDynamic(attr.name).asInstanceOf[js.Function1[T, Boolean]] - if (existingCallbacks == null) - dyn.updateDynamic(attr.name) { (e: T) => if (callback(e) == true) e.preventDefault() } - else - dyn.updateDynamic(attr.name) { (e: T) => - val preventDefault = callback(e) - if (preventDefault == true) e.preventDefault() - else existingCallbacks(e) - } - }) + AttrPair( + attr, + callback, + (el: Element, attr: Attr, callback: T => Any) => { + val dyn: js.Dynamic = el.asInstanceOf[js.Dynamic] + val existingCallbacks: js.Function1[T, Boolean] = + dyn.selectDynamic(attr.name).asInstanceOf[js.Function1[T, Boolean]] + if (existingCallbacks == null) + dyn.updateDynamic(attr.name)((e: T) => if (callback(e) == true) e.preventDefault()) + else + dyn.updateDynamic(attr.name) { (e: T) => + val preventDefault = callback(e) + if (preventDefault == true) e.preventDefault() + else existingCallbacks(e) + } + }, + ) /** Sets attribute on element. */ def applyTo(element: Element, value: String = ""): Unit = @@ -401,10 +495,9 @@ object Bindings extends Bindings { case (elem, v) => applyTo(elem, v) } - /** - * Synchronises attribute value with property content by adding it when property is not null and - * condition property is 'true' and removing otherwise. - */ + /** Synchronises attribute value with property content by adding it when property is not null and condition property + * is 'true' and removing otherwise. + */ def bindIf(property: ReadableProperty[String], conditionProperty: ReadableProperty[Boolean]): Binding = property.combine(conditionProperty)((_, _)).reactiveApply { case (elem, (null, _) | (_, false)) => removeFrom(elem) @@ -413,6 +506,7 @@ object Bindings extends Bindings { } final class AttrPairOps(private val attr: scalatags.generic.AttrPair[Element, _]) extends AnyVal { + /** Sets attribute on element. */ def applyTo(element: Element): Unit = attr.applyTo(element) @@ -423,18 +517,16 @@ object Bindings extends Bindings { /** Synchronises attribute with property content by adding it if value is `false` and removing otherwise. */ def attrIfNot(property: ReadableProperty[Boolean]): Binding = - property.reactiveApply( - (elem, apply) => - if (apply) removeFrom(elem) - else applyTo(elem) + property.reactiveApply((elem, apply) => + if (apply) removeFrom(elem) + else applyTo(elem) ) /** Synchronises attribute with property content by adding it if value is `true` and removing otherwise. */ def attrIf(property: ReadableProperty[Boolean]): Binding = - property.reactiveApply( - (elem, apply) => - if (apply) applyTo(elem) - else removeFrom(elem) + property.reactiveApply((elem, apply) => + if (apply) applyTo(elem) + else removeFrom(elem) ) /** Adds attribute to element if `condition` is `true`. */ @@ -443,6 +535,7 @@ object Bindings extends Bindings { } final class PropertyOps[T](private val property: ReadableProperty[T]) extends AnyVal { + /** Calls provided callback on every property value change. */ def reactiveApply(callback: (Element, T) => Unit): Binding = new Binding { override def applyTo(t: Element): Unit = { diff --git a/core/.js/src/main/scala/io/udash/bindings/inputs/CheckButtons.scala b/core/.js/src/main/scala/io/udash/bindings/inputs/CheckButtons.scala index 234e47c69..7d583023a 100644 --- a/core/.js/src/main/scala/io/udash/bindings/inputs/CheckButtons.scala +++ b/core/.js/src/main/scala/io/udash/bindings/inputs/CheckButtons.scala @@ -15,24 +15,32 @@ object CheckButtons { def divWithLabelDecorator[T](labelFactory: T => Modifier): Seq[(JSInput, T)] => Seq[Node] = RadioButtons.divWithLabelDecorator(labelFactory) - /** - * @param selectedItems SeqProperty which is going to be bound to checkboxes - * @param options Seq of available options, one checkbox will be created for each option. - * @param decorator Function creating HTML element from checkboxes Seq. - * @param inputModifiers Modifiers to apply on each generated checkbox. - * @return HTML element created by decorator. - */ + /** @param selectedItems + * SeqProperty which is going to be bound to checkboxes + * @param options + * Seq of available options, one checkbox will be created for each option. + * @param decorator + * Function creating HTML element from checkboxes Seq. + * @param inputModifiers + * Modifiers to apply on each generated checkbox. + * @return + * HTML element created by decorator. + */ def apply[T]( - selectedItems: SeqProperty[T, _ <: ReadableProperty[T]], options: ReadableSeqProperty[T] - )(decorator: Seq[(JSInput, T)] => Seq[Node], inputModifiers: Modifier*): InputBinding[Div] = { + selectedItems: SeqProperty[T, _ <: ReadableProperty[T]], + options: ReadableSeqProperty[T], + )( + decorator: Seq[(JSInput, T)] => Seq[Node], + inputModifiers: Modifier* + ): InputBinding[Div] = new GroupedButtonsBinding(options, decorator, inputModifiers)( "checkbox", opt => selectedItems.transform(_.contains(opt)), opts => selectedItems.set(selectedItems.get.filter(opts.contains)), - (in: JSInput, opt: T) => (_: Event) => { - if (in.checked && !selectedItems.get.contains(opt)) selectedItems.append(opt) - else selectedItems.remove(opt) - } + (in: JSInput, opt: T) => + (_: Event) => { + if (in.checked && !selectedItems.get.contains(opt)) selectedItems.append(opt) + else selectedItems.remove(opt) + }, ) - } } diff --git a/core/.js/src/main/scala/io/udash/bindings/inputs/Checkbox.scala b/core/.js/src/main/scala/io/udash/bindings/inputs/Checkbox.scala index c348d5c6f..f78838511 100644 --- a/core/.js/src/main/scala/io/udash/bindings/inputs/Checkbox.scala +++ b/core/.js/src/main/scala/io/udash/bindings/inputs/Checkbox.scala @@ -5,18 +5,20 @@ import org.scalajs.dom.Event import org.scalajs.dom.html.{Input => JSInput} import scalatags.JsDom.all._ -/** - * Plain checkbox bidirectionally bound to Property. +/** Plain checkbox bidirectionally bound to Property. * * For SeqProperty take a look at [[io.udash.bindings.inputs.CheckButtons]] */ object Checkbox { - /** - * @param selected Property to bind. - * @param inputModifiers Additional Modifiers, don't use modifiers on type, checked and onchange attributes. - * @return HTML input (checkbox) tag with bound Property and applied modifiers. + + /** @param selected + * Property to bind. + * @param inputModifiers + * Additional Modifiers, don't use modifiers on type, checked and onchange attributes. + * @return + * HTML input (checkbox) tag with bound Property and applied modifiers. */ - def apply(selected: Property[Boolean])(inputModifiers: Modifier*): InputBinding[JSInput] = { + def apply(selected: Property[Boolean])(inputModifiers: Modifier*): InputBinding[JSInput] = new InputBinding[JSInput] { private val in = input(inputModifiers, tpe := "checkbox").render @@ -25,5 +27,4 @@ object Checkbox { override def render: JSInput = in } - } } diff --git a/core/.js/src/main/scala/io/udash/bindings/inputs/FileInput.scala b/core/.js/src/main/scala/io/udash/bindings/inputs/FileInput.scala index 27af10961..6fd467658 100644 --- a/core/.js/src/main/scala/io/udash/bindings/inputs/FileInput.scala +++ b/core/.js/src/main/scala/io/udash/bindings/inputs/FileInput.scala @@ -8,42 +8,51 @@ import scalatags.JsDom.all._ object FileInput { - /** - * Creates a file input providing information about the selected file. + /** Creates a file input providing information about the selected file. * - * @param selectedFile This property contains information about the file selected by user. - * @param inputName Input element name. - * @param inputModifiers Additional modifiers to apply on a generated input. + * @param selectedFile + * This property contains information about the file selected by user. + * @param inputName + * Input element name. + * @param inputModifiers + * Additional modifiers to apply on a generated input. * @return */ - def single(selectedFile: Property[File])(inputName: String, inputModifiers: Modifier*): InputBinding[JSInput] = { + def single(selectedFile: Property[File])(inputName: String, inputModifiers: Modifier*): InputBinding[JSInput] = apply(selectedFile.bitransformToSeq(Seq(_))(_.head), false.toProperty)(inputName, inputModifiers) - } - /** - * Creates file input providing information about selected files. + /** Creates file input providing information about selected files. * - * @param selectedFiles This property contains information about files selected by user. - * @param acceptMultipleFiles Accepts more than one file if true. - * @param inputName Input element name. - * @param inputModifiers Additional modifiers to apply on a generated input. + * @param selectedFiles + * This property contains information about files selected by user. + * @param acceptMultipleFiles + * Accepts more than one file if true. + * @param inputName + * Input element name. + * @param inputModifiers + * Additional modifiers to apply on a generated input. * @return */ def apply( - selectedFiles: SeqProperty[File], acceptMultipleFiles: ReadableProperty[Boolean] - )(inputName: String, inputModifiers: Modifier*): InputBinding[JSInput] = { + selectedFiles: SeqProperty[File], + acceptMultipleFiles: ReadableProperty[Boolean], + )( + inputName: String, + inputModifiers: Modifier* + ): InputBinding[JSInput] = new InputBinding[JSInput] { private val in = input( - inputModifiers, `type` := "file", name := inputName, - nestedInterceptor((multiple := "multiple").attrIf(acceptMultipleFiles)) + inputModifiers, + `type` := "file", + name := inputName, + nestedInterceptor((multiple := "multiple").attrIf(acceptMultipleFiles)), ).render in.onchange = (ev: Event) => { ev.preventDefault() - selectedFiles.replace(0, selectedFiles.size, (0 until in.files.length).map { idx => in.files(idx) }: _*) + selectedFiles.replace(0, selectedFiles.size, (0 until in.files.length).map(idx => in.files(idx)): _*) } override def render: JSInput = in } - } } diff --git a/core/.js/src/main/scala/io/udash/bindings/inputs/GroupedButtonsBinding.scala b/core/.js/src/main/scala/io/udash/bindings/inputs/GroupedButtonsBinding.scala index a755dc94e..1f841b196 100644 --- a/core/.js/src/main/scala/io/udash/bindings/inputs/GroupedButtonsBinding.scala +++ b/core/.js/src/main/scala/io/udash/bindings/inputs/GroupedButtonsBinding.scala @@ -8,12 +8,14 @@ import scalatags.JsDom.all._ import scala.util.Random private[inputs] class GroupedButtonsBinding[T]( - options: ReadableSeqProperty[T], decorator: Seq[(JSInput, T)] => Seq[Node], inputModifiers: Modifier* + options: ReadableSeqProperty[T], + decorator: Seq[(JSInput, T)] => Seq[Node], + inputModifiers: Modifier* )( inputTpe: String, checkedIf: T => ReadableProperty[Boolean], refreshSelection: Seq[T] => Unit, - onChange: (JSInput, T) => Event => Unit + onChange: (JSInput, T) => Event => Unit, ) extends InputBinding[Div] { private val groupIdPrefix: Long = Random.nextLong() @@ -26,7 +28,9 @@ private[inputs] class GroupedButtonsBinding[T]( opts.zipWithIndex.map { case (opt, idx) => val in = input( id := s"$groupIdPrefix-$idx", // default id, can be replaced by `inputModifiers` - inputModifiers, tpe := inputTpe, value := idx.toString + inputModifiers, + tpe := inputTpe, + value := idx.toString, ).render val selected = checkedIf(opt) diff --git a/core/.js/src/main/scala/io/udash/bindings/inputs/Input.scala b/core/.js/src/main/scala/io/udash/bindings/inputs/Input.scala index 8342adb8c..51484c522 100644 --- a/core/.js/src/main/scala/io/udash/bindings/inputs/Input.scala +++ b/core/.js/src/main/scala/io/udash/bindings/inputs/Input.scala @@ -9,27 +9,33 @@ import scala.concurrent.duration.{Duration, DurationInt} /** Abstraction for HTML input tags. */ private[bindings] abstract class Input(inputType: String) { - /** - * @param value Property to bind. - * @param debounce Property update timeout after input changes. - * @param onInputElementEvent Callback that's executed when `Input` element receives one of following events: - * `Input`, `Change`, `KeyUp`, `Paste` and element value is different than property value. - * Can be used to build unidirectional data flow component on top of `Input` component. - * @param inputModifiers Additional modifiers. Attributes: `tpe`, `value`, `onkeyup`, `onchange`, `onpaste`, `oninput` - * are ignored as they are overwritten internally. - * @return HTML input with bound Property, applied modifiers and nested options. - */ + + /** @param value + * Property to bind. + * @param debounce + * Property update timeout after input changes. + * @param onInputElementEvent + * Callback that's executed when `Input` element receives one of following events: `Input`, `Change`, `KeyUp`, + * `Paste` and element value is different than property value. Can be used to build unidirectional data flow + * component on top of `Input` component. + * @param inputModifiers + * Additional modifiers. Attributes: `tpe`, `value`, `onkeyup`, `onchange`, `onpaste`, `oninput` are ignored as + * they are overwritten internally. + * @return + * HTML input with bound Property, applied modifiers and nested options. + */ def apply( value: Property[String] = Property(""), - debounce: Duration = 20 millis, + debounce: Duration = 20.millis, onInputElementEvent: String => Unit = _ => (), )( inputModifiers: Modifier* ): InputBinding[JSInput] = new InputBinding[JSInput] { private val element = input( - inputModifiers, tpe := inputType, - nestedInterceptor(new InputModifier(value, debounce, onInputElementEvent)) + inputModifiers, + tpe := inputType, + nestedInterceptor(new InputModifier(value, debounce, onInputElementEvent)), ).render override def render: JSInput = element @@ -78,4 +84,3 @@ object DateInput extends Input("date") /** Time input. (HTML5) */ object TimeInput extends Input("time") - diff --git a/core/.js/src/main/scala/io/udash/bindings/inputs/RadioButtons.scala b/core/.js/src/main/scala/io/udash/bindings/inputs/RadioButtons.scala index c12a36f34..49e9ddea5 100644 --- a/core/.js/src/main/scala/io/udash/bindings/inputs/RadioButtons.scala +++ b/core/.js/src/main/scala/io/udash/bindings/inputs/RadioButtons.scala @@ -5,8 +5,7 @@ import org.scalajs.dom.html.{Div, Input => JSInput} import org.scalajs.dom.{Event, Node} import scalatags.JsDom.all._ -/** - * Radio buttons group for finite options with one element selection. +/** Radio buttons group for finite options with one element selection. */ object RadioButtons { def inputsOnlyDecorator[T]: Seq[(JSInput, T)] => Seq[Node] = @@ -16,22 +15,29 @@ object RadioButtons { def divWithLabelDecorator[T](labelFactory: T => Modifier): Seq[(JSInput, T)] => Seq[Node] = _.map { case (in, v) => div(in, label(labelFactory(v))).render } - /** - * @param selectedItem Property which is going to be bound to radio buttons group. - * @param options Seq of available options, one radio button will be created for each option. - * @param decorator Function creating HTML element from buttons Seq. - * (Check: `RadioButtons.inputsOnlyDecorator`, `RadioButtons.spanWithLabelDecorator` and `RadioButtons.divWithLabelDecorator`) - * @param inputModifiers Modifiers to apply on each generated checkbox. - * @return HTML element created by decorator. + /** @param selectedItem + * Property which is going to be bound to radio buttons group. + * @param options + * Seq of available options, one radio button will be created for each option. + * @param decorator + * Function creating HTML element from buttons Seq. (Check: `RadioButtons.inputsOnlyDecorator`, + * `RadioButtons.spanWithLabelDecorator` and `RadioButtons.divWithLabelDecorator`) + * @param inputModifiers + * Modifiers to apply on each generated checkbox. + * @return + * HTML element created by decorator. */ def apply[T]( - selectedItem: Property[T], options: ReadableSeqProperty[T] - )(decorator: Seq[(JSInput, T)] => Seq[Node], inputModifiers: Modifier*): InputBinding[Div] = { + selectedItem: Property[T], + options: ReadableSeqProperty[T], + )( + decorator: Seq[(JSInput, T)] => Seq[Node], + inputModifiers: Modifier* + ): InputBinding[Div] = new GroupedButtonsBinding(options, decorator, inputModifiers)( "radio", opt => selectedItem.transform(_ == opt), opts => if (opts.nonEmpty && !opts.contains(selectedItem.get)) selectedItem.set(opts.head), - (_: JSInput, opt: T) => (_: Event) => selectedItem.set(opt) + (_: JSInput, opt: T) => (_: Event) => selectedItem.set(opt), ) - } } diff --git a/core/.js/src/main/scala/io/udash/bindings/inputs/RangeInput.scala b/core/.js/src/main/scala/io/udash/bindings/inputs/RangeInput.scala index 1a22b1c49..6e6364834 100644 --- a/core/.js/src/main/scala/io/udash/bindings/inputs/RangeInput.scala +++ b/core/.js/src/main/scala/io/udash/bindings/inputs/RangeInput.scala @@ -5,40 +5,57 @@ import org.scalajs.dom.Event import org.scalajs.dom.html.{Input => JSInput} import scalatags.JsDom.all._ -/** The HTML range input.*/ +/** The HTML range input. */ object RangeInput { - /** - * @param property Current value synchronised with the input. - * The value should be between `minValue` and `maxValue`. It should be also divisible by `valueStep`. - * @param minValue The minimum value for this input, which must not be greater than its maximum (`maxValue` attribute) value. - * @param maxValue The maximum value for the input. Must not be less than its minimum (`minValue` attribute) value. - * @param valueStep Limit the increments at which a numeric value can be set. - * @param inputModifiers Additional Modifiers, don't use modifiers on value, onchange, min, max and step attributes. - * @return HTML range input with bound Property, applied modifiers and nested options. + + /** @param property + * Current value synchronised with the input. The value should be between `minValue` and `maxValue`. It should be + * also divisible by `valueStep`. + * @param minValue + * The minimum value for this input, which must not be greater than its maximum (`maxValue` attribute) value. + * @param maxValue + * The maximum value for the input. Must not be less than its minimum (`minValue` attribute) value. + * @param valueStep + * Limit the increments at which a numeric value can be set. + * @param inputModifiers + * Additional Modifiers, don't use modifiers on value, onchange, min, max and step attributes. + * @return + * HTML range input with bound Property, applied modifiers and nested options. */ def apply( property: Property[Double], minValue: ReadableProperty[Double] = 0d.toProperty, maxValue: ReadableProperty[Double] = 100d.toProperty, - valueStep: ReadableProperty[Double] = 1d.toProperty - )(inputModifiers: Modifier*): InputBinding[JSInput] = + valueStep: ReadableProperty[Double] = 1d.toProperty, + )( + inputModifiers: Modifier* + ): InputBinding[JSInput] = new InputBinding[JSInput] { private val element = input(inputModifiers, tpe := "range").render element.onchange = (_: Event) => property.set(element.valueAsNumber) propertyListeners += property.listen(element.valueAsNumber = _, initUpdate = true) - propertyListeners += minValue.listen({ v => - (min := v).applyTo(element) - property.set(element.valueAsNumber) - }, initUpdate = true) - propertyListeners += maxValue.listen({ v => - (max := v).applyTo(element) - property.set(element.valueAsNumber) - }, initUpdate = true) - propertyListeners += valueStep.listen({ v => - (step := v).applyTo(element) - property.set(element.valueAsNumber) - }, initUpdate = true) + propertyListeners += minValue.listen( + { v => + (min := v).applyTo(element) + property.set(element.valueAsNumber) + }, + initUpdate = true, + ) + propertyListeners += maxValue.listen( + { v => + (max := v).applyTo(element) + property.set(element.valueAsNumber) + }, + initUpdate = true, + ) + propertyListeners += valueStep.listen( + { v => + (step := v).applyTo(element) + property.set(element.valueAsNumber) + }, + initUpdate = true, + ) override def render: JSInput = element } diff --git a/core/.js/src/main/scala/io/udash/bindings/inputs/Select.scala b/core/.js/src/main/scala/io/udash/bindings/inputs/Select.scala index e3dfbf9ec..bd0245cfb 100644 --- a/core/.js/src/main/scala/io/udash/bindings/inputs/Select.scala +++ b/core/.js/src/main/scala/io/udash/bindings/inputs/Select.scala @@ -6,90 +6,113 @@ import org.scalajs.dom.{Event, HTMLOptionElement} import org.scalajs.dom.html.Select import scalatags.JsDom.all._ -/** - * Select of finite options for single and multi selection. +/** Select of finite options for single and multi selection. */ object Select { val defaultLabel: String => Modifier = s => StringFrag(s) - /** - * Single select for ValueProperty. + /** Single select for ValueProperty. * - * @param selectedItem Property to bind. - * @param options SeqProperty of available options. - * @param label Provides element's label. - * @param selectModifiers Additional Modifiers for the select tag, don't use modifiers on value, onchange and selected attributes. - * @return Binding with `select` element, which can be used as Scalatags modifier. + * @param selectedItem + * Property to bind. + * @param options + * SeqProperty of available options. + * @param label + * Provides element's label. + * @param selectModifiers + * Additional Modifiers for the select tag, don't use modifiers on value, onchange and selected attributes. + * @return + * Binding with `select` element, which can be used as Scalatags modifier. */ def apply[T]( - selectedItem: Property[T], options: ReadableSeqProperty[T] - )(label: T => Modifier, selectModifiers: Modifier*): InputBinding[Select] = { + selectedItem: Property[T], + options: ReadableSeqProperty[T], + )( + label: T => Modifier, + selectModifiers: Modifier* + ): InputBinding[Select] = new SelectBinding(options, label, None, selectModifiers)( opt => selectedItem.transform(_ == opt), opts => if (opts.nonEmpty && !opts.contains(selectedItem.get)) selectedItem.set(opts.head), - selector => (_: Event) => selectedItem.set(options.get.apply(selector.value.toInt)) + selector => (_: Event) => selectedItem.set(options.get.apply(selector.value.toInt)), ) - } - - /** - * Single select for optional ValueProperty. It differs from the `apply` method since the rendered output contains - * the empty string for the empty value. - * It might be useful when you need to use the `required` attribute - * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#attr-required - * - * Example rendered output: - * ``` - * - * ``` - * - * @param selectedItem Property to bind. - * @param options SeqProperty of available options. - * @param labelNoValue Label for option without value - * @param label Provides element's label. - * @param selectModifiers Additional Modifiers for the select tag, don't use modifiers on value, onchange and selected attributes. - * @return Binding with `select` element, which can be used as Scalatags modifier. - */ + /** Single select for optional ValueProperty. It differs from the `apply` method since the rendered output contains + * the empty string for the empty value. It might be useful when you need to use the `required` attribute + * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#attr-required + * + * Example rendered output: + * ``` + * + * ``` + * + * @param selectedItem + * Property to bind. + * @param options + * SeqProperty of available options. + * @param labelNoValue + * Label for option without value + * @param label + * Provides element's label. + * @param selectModifiers + * Additional Modifiers for the select tag, don't use modifiers on value, onchange and selected attributes. + * @return + * Binding with `select` element, which can be used as Scalatags modifier. + */ def optional[T]( - selectedItem: Property[Option[T]], options: ReadableSeqProperty[T], labelNoValue: Modifier - )(label: T => Modifier, selectModifiers: Modifier*): InputBinding[Select] = { + selectedItem: Property[Option[T]], + options: ReadableSeqProperty[T], + labelNoValue: Modifier, + )( + label: T => Modifier, + selectModifiers: Modifier* + ): InputBinding[Select] = new SelectBinding(options, label, Some(labelNoValue), selectModifiers)( opt => selectedItem.transform(_.contains(opt)), opts => if (selectedItem.get.isEmpty && !opts.exists(x => selectedItem.get.contains(x))) selectedItem.set(None), - selector => (_: Event) => selector.value match { - case "" => selectedItem.set(None) - case s:String => selectedItem.set(Some(options.get.apply(s.toInt))) - } + selector => + (_: Event) => + selector.value match { + case "" => selectedItem.set(None) + case s: String => selectedItem.set(Some(options.get.apply(s.toInt))) + }, ) - } - /** - * Multi selection for SeqProperty. Bound SeqProperty will contain selected options. + /** Multi selection for SeqProperty. Bound SeqProperty will contain selected options. * - * @param selectedItems Property to bind. - * @param options SeqProperty of available options. - * @param label Provides element label. - * @param selectModifiers Additional Modifiers, don't use modifiers on value, onchange and selected attributes. - * @return Binding with `select` element, which can be used as Scalatags modifier. + * @param selectedItems + * Property to bind. + * @param options + * SeqProperty of available options. + * @param label + * Provides element label. + * @param selectModifiers + * Additional Modifiers, don't use modifiers on value, onchange and selected attributes. + * @return + * Binding with `select` element, which can be used as Scalatags modifier. */ def apply[T, ElemType <: Property[T]]( - selectedItems: SeqProperty[T, ElemType], options: ReadableSeqProperty[T] - )(label: T => Modifier, selectModifiers: Modifier*): InputBinding[Select] = { + selectedItems: SeqProperty[T, ElemType], + options: ReadableSeqProperty[T], + )( + label: T => Modifier, + selectModifiers: Modifier* + ): InputBinding[Select] = new SelectBinding(options, label, None, selectModifiers :+ (multiple := true))( opt => selectedItems.transform(_.contains(opt)), opts => selectedItems.set(selectedItems.get.filter(opts.contains)), - selector => (_: Event) => { - val opts = options.get - val selectedNodes = selector.querySelectorAll("option:checked") - val selection = (0 until selectedNodes.length).map { idx => - opts(selectedNodes(idx).asInstanceOf[HTMLOptionElement].value.toInt) - } - selectedItems.set(selection) - } + selector => + (_: Event) => { + val opts = options.get + val selectedNodes = selector.querySelectorAll("option:checked") + val selection = (0 until selectedNodes.length).map { idx => + opts(selectedNodes(idx).asInstanceOf[HTMLOptionElement].value.toInt) + } + selectedItems.set(selection) + }, ) - } } diff --git a/core/.js/src/main/scala/io/udash/bindings/inputs/SelectBinding.scala b/core/.js/src/main/scala/io/udash/bindings/inputs/SelectBinding.scala index 9d4587d2d..cea791280 100644 --- a/core/.js/src/main/scala/io/udash/bindings/inputs/SelectBinding.scala +++ b/core/.js/src/main/scala/io/udash/bindings/inputs/SelectBinding.scala @@ -6,11 +6,14 @@ import org.scalajs.dom.html.Select import scalatags.JsDom.all._ private[inputs] class SelectBinding[T]( - options: ReadableSeqProperty[T], label: T => Modifier, labelNoValue: Option[Modifier], selectModifiers: Modifier* + options: ReadableSeqProperty[T], + label: T => Modifier, + labelNoValue: Option[Modifier], + selectModifiers: Modifier* )( checkedIf: T => ReadableProperty[Boolean], refreshSelection: Seq[T] => Unit, - onChange: Select => Event => Unit + onChange: Select => Event => Unit, ) extends InputBinding[Select] { private val selector = select(selectModifiers)( produce(options) { opts => @@ -22,13 +25,13 @@ private[inputs] class SelectBinding[T]( } { - empty.iterator ++ opts.iterator.zipWithIndex.map { case (opt, idx) => - val el = option(value := idx.toString, label(opt)).render + empty.iterator ++ opts.iterator.zipWithIndex.map { case (opt, idx) => + val el = option(value := idx.toString, label(opt)).render - val selected = checkedIf(opt) - propertyListeners += selected.listen(el.selected = _, initUpdate = true) - el - } + val selected = checkedIf(opt) + propertyListeners += selected.listen(el.selected = _, initUpdate = true) + el + } }.toSeq } ).render @@ -36,4 +39,4 @@ private[inputs] class SelectBinding[T]( selector.onchange = onChange(selector) override def render: Select = selector -} \ No newline at end of file +} diff --git a/core/.js/src/main/scala/io/udash/bindings/inputs/TextArea.scala b/core/.js/src/main/scala/io/udash/bindings/inputs/TextArea.scala index 5d97308d5..31e9867bd 100644 --- a/core/.js/src/main/scala/io/udash/bindings/inputs/TextArea.scala +++ b/core/.js/src/main/scala/io/udash/bindings/inputs/TextArea.scala @@ -9,33 +9,42 @@ import scala.concurrent.duration.{Duration, DurationInt} /** Simple HTML text area with bound Property. */ object TextArea { - /** - * @param value Property to bind. - * @param debounce Property update timeout after input changes. - * @param onInputElementEvent Callback that's executed when `Input` element receives one of following events: - * `Input`, `Change`, `KeyUp`, `Paste` and element value is different than property value. - * Can Can be used to build unidirectional data flow component on top of `TextArea` component. - * @param textareaModifiers Additional modifiers. Attributes: `tpe`, `value`, `onkeyup`, `onchange`, `onpaste`, `oninput` - * are ignored as they are overwritten internally. - * @return HTML textarea with bound Property, applied modifiers and nested options. - */ + + /** @param value + * Property to bind. + * @param debounce + * Property update timeout after input changes. + * @param onInputElementEvent + * Callback that's executed when `Input` element receives one of following events: `Input`, `Change`, `KeyUp`, + * `Paste` and element value is different than property value. Can Can be used to build unidirectional data flow + * component on top of `TextArea` component. + * @param textareaModifiers + * Additional modifiers. Attributes: `tpe`, `value`, `onkeyup`, `onchange`, `onpaste`, `oninput` are ignored as + * they are overwritten internally. + * @return + * HTML textarea with bound Property, applied modifiers and nested options. + */ def apply( value: Property[String] = Property(""), - debounce: Duration = 20 millis, + debounce: Duration = 20.millis, onInputElementEvent: String => Unit = _ => (), )( textareaModifiers: Modifier* ): InputBinding[TextArea] = new InputBinding[TextArea] { private val element = textarea( - textareaModifiers, nestedInterceptor(new TextAreaModifier(value, debounce, onInputElementEvent)) + textareaModifiers, + nestedInterceptor(new TextAreaModifier(value, debounce, onInputElementEvent)), ).render override def render: TextArea = element } - private class TextAreaModifier(property: Property[String], debounce: Duration, onInputElementEvent: String => Unit = _ => ()) - extends TextInputsModifier(property, debounce, onInputElementEvent) { + private class TextAreaModifier( + property: Property[String], + debounce: Duration, + onInputElementEvent: String => Unit = _ => (), + ) extends TextInputsModifier(property, debounce, onInputElementEvent) { override def elementValue(t: Element): String = t.asInstanceOf[TextArea].value diff --git a/core/.js/src/main/scala/io/udash/bindings/inputs/TextInputsModifier.scala b/core/.js/src/main/scala/io/udash/bindings/inputs/TextInputsModifier.scala index 1991a2e36..56aa973a7 100644 --- a/core/.js/src/main/scala/io/udash/bindings/inputs/TextInputsModifier.scala +++ b/core/.js/src/main/scala/io/udash/bindings/inputs/TextInputsModifier.scala @@ -8,7 +8,11 @@ import scala.concurrent.duration.Duration /** Template of binding for text inputs. */ -private[bindings] abstract class TextInputsModifier(property: Property[String], debounce: Duration, onInputElementEvent: String => Unit) extends Binding { +private[bindings] abstract class TextInputsModifier( + property: Property[String], + debounce: Duration, + onInputElementEvent: String => Unit, +) extends Binding { def elementValue(t: Element): String def setElementValue(t: Element, v: String): Unit def setElementKeyUp(t: Element, callback: KeyboardEvent => Unit): Unit @@ -36,9 +40,12 @@ private[bindings] abstract class TextInputsModifier(property: Property[String], if (debounce.toMillis > 0) (_: Event) => { if (propertyUpdateHandler != 0) window.clearTimeout(propertyUpdateHandler) - propertyUpdateHandler = window.setTimeout(() => { - updatePropertyValueForElement(t) - }, debounce.toMillis.toDouble) + propertyUpdateHandler = window.setTimeout( + () => { + updatePropertyValueForElement(t) + }, + debounce.toMillis.toDouble, + ) } else (_: Event) => updatePropertyValueForElement(t) diff --git a/core/.js/src/main/scala/io/udash/bindings/modifiers/DOMManipulator.scala b/core/.js/src/main/scala/io/udash/bindings/modifiers/DOMManipulator.scala index 1c6e18344..5eea9653f 100644 --- a/core/.js/src/main/scala/io/udash/bindings/modifiers/DOMManipulator.scala +++ b/core/.js/src/main/scala/io/udash/bindings/modifiers/DOMManipulator.scala @@ -2,26 +2,19 @@ package io.udash.bindings.modifiers import org.scalajs.dom.{DocumentFragment, Node} - private[bindings] trait DOMManipulator { import DOMManipulator._ - /** - * Provides custom child elements replace method. This method takes - * root element, old children and new children. - * It should return `true`, if it does not replace elements in DOM. - * Is such a case the default implementation will replace the elements. - * Otherwise you have to replace elements in DOM manually. + /** Provides custom child elements replace method. This method takes root element, old children and new children. It + * should return `true`, if it does not replace elements in DOM. Is such a case the default implementation will + * replace the elements. Otherwise you have to replace elements in DOM manually. */ def customElementsReplace: ReplaceMethod - /** - * Provides custom child elements insert method. This method takes - * root element, ref node and new children. - * It should return `true`, if it does not insert elements in DOM. - * Is such a case the default implementation will insert the elements. - * Otherwise you have to replace elements in DOM manually. + /** Provides custom child elements insert method. This method takes root element, ref node and new children. It should + * return `true`, if it does not insert elements in DOM. Is such a case the default implementation will insert the + * elements. Otherwise you have to replace elements in DOM manually. */ def customElementsInsert: InsertMethod = DefaultElementInsert diff --git a/core/.js/src/main/scala/io/udash/bindings/modifiers/EmptyModifier.scala b/core/.js/src/main/scala/io/udash/bindings/modifiers/EmptyModifier.scala index be9731c77..c9915fb83 100644 --- a/core/.js/src/main/scala/io/udash/bindings/modifiers/EmptyModifier.scala +++ b/core/.js/src/main/scala/io/udash/bindings/modifiers/EmptyModifier.scala @@ -4,4 +4,4 @@ import scalatags.generic.Modifier private[udash] final class EmptyModifier[T] extends Modifier[T] { override def applyTo(t: T): Unit = () -} \ No newline at end of file +} diff --git a/core/.js/src/main/scala/io/udash/bindings/modifiers/PropertyModifier.scala b/core/.js/src/main/scala/io/udash/bindings/modifiers/PropertyModifier.scala index 7905f9eaa..658fc0907 100644 --- a/core/.js/src/main/scala/io/udash/bindings/modifiers/PropertyModifier.scala +++ b/core/.js/src/main/scala/io/udash/bindings/modifiers/PropertyModifier.scala @@ -8,12 +8,16 @@ private[bindings] class PropertyModifier[T]( override val property: ReadableProperty[T], override val builder: (T, Binding.NestedInterceptor) => Seq[Node], override val checkNull: Boolean, - override val customElementsReplace: DOMManipulator.ReplaceMethod + override val customElementsReplace: DOMManipulator.ReplaceMethod, ) extends ValueModifier[T] { - def this(property: ReadableProperty[T], builder: T => Seq[Node], checkNull: Boolean, customElementsReplace: DOMManipulator.ReplaceMethod) = { + def this( + property: ReadableProperty[T], + builder: T => Seq[Node], + checkNull: Boolean, + customElementsReplace: DOMManipulator.ReplaceMethod, + ) = this(property, (data: T, _: Binding.NestedInterceptor) => builder(data), checkNull, customElementsReplace) - } def listen(callback: T => Unit): Registration = property.listen(callback) diff --git a/core/.js/src/main/scala/io/udash/bindings/modifiers/SeqAsValueModifier.scala b/core/.js/src/main/scala/io/udash/bindings/modifiers/SeqAsValueModifier.scala index 0b7e76bd0..3f4f1f48e 100644 --- a/core/.js/src/main/scala/io/udash/bindings/modifiers/SeqAsValueModifier.scala +++ b/core/.js/src/main/scala/io/udash/bindings/modifiers/SeqAsValueModifier.scala @@ -10,19 +10,22 @@ import org.scalajs.dom.Node private[bindings] final class SeqAsValueModifier[T]( override val property: ReadableSeqProperty[T, _ <: ReadableProperty[T]], build: (Seq[T], Binding.NestedInterceptor) => Seq[Node], - override val customElementsReplace: DOMManipulator.ReplaceMethod + override val customElementsReplace: DOMManipulator.ReplaceMethod, ) extends ValueModifier[BSeq[T]] { - override protected def builder: (BSeq[T], NestedInterceptor) => Seq[Node] = (data, interceptor) => build(data.toSeq, interceptor) + override protected def builder: (BSeq[T], NestedInterceptor) => Seq[Node] = (data, interceptor) => + build(data.toSeq, interceptor) - def this(property: ReadableSeqProperty[T, _ <: ReadableProperty[T]], builder: Seq[T] => Seq[Node], - customElementsReplace: DOMManipulator.ReplaceMethod) = { + def this( + property: ReadableSeqProperty[T, _ <: ReadableProperty[T]], + builder: Seq[T] => Seq[Node], + customElementsReplace: DOMManipulator.ReplaceMethod, + ) = this(property, (data: Seq[T], _: Binding.NestedInterceptor) => builder(data), customElementsReplace) - } override def listen(callback: BSeq[T] => Unit): Registration = property.listen(callback) override def checkNull: Boolean = false // SeqProperty can not return null from `get` method -} \ No newline at end of file +} diff --git a/core/.js/src/main/scala/io/udash/bindings/modifiers/SeqPropertyModifier.scala b/core/.js/src/main/scala/io/udash/bindings/modifiers/SeqPropertyModifier.scala index 3a76760e3..f6cd509c5 100644 --- a/core/.js/src/main/scala/io/udash/bindings/modifiers/SeqPropertyModifier.scala +++ b/core/.js/src/main/scala/io/udash/bindings/modifiers/SeqPropertyModifier.scala @@ -8,17 +8,17 @@ private[bindings] final class SeqPropertyModifier[T, E <: ReadableProperty[T]]( override val property: ReadableSeqProperty[T, E], builder: (E, Binding.NestedInterceptor) => Seq[Node], override val customElementsReplace: DOMManipulator.ReplaceMethod, - override val customElementsInsert: DOMManipulator.InsertMethod + override val customElementsInsert: DOMManipulator.InsertMethod, ) extends SeqPropertyModifierUtils[T, E] { - def this(property: ReadableSeqProperty[T, E], builder: E => Seq[Node], - customElementsReplace: DOMManipulator.ReplaceMethod, - customElementsInsert: DOMManipulator.InsertMethod) = { + def this( + property: ReadableSeqProperty[T, E], + builder: E => Seq[Node], + customElementsReplace: DOMManipulator.ReplaceMethod, + customElementsInsert: DOMManipulator.InsertMethod, + ) = this(property, (d, _) => builder(d), customElementsReplace, customElementsInsert) - } protected def build(item: E): Seq[Node] = builder(item, propertyAwareNestedInterceptor(item)) } - - diff --git a/core/.js/src/main/scala/io/udash/bindings/modifiers/SeqPropertyModifierUtils.scala b/core/.js/src/main/scala/io/udash/bindings/modifiers/SeqPropertyModifierUtils.scala index f7a49c616..f236ce17c 100644 --- a/core/.js/src/main/scala/io/udash/bindings/modifiers/SeqPropertyModifierUtils.scala +++ b/core/.js/src/main/scala/io/udash/bindings/modifiers/SeqPropertyModifierUtils.scala @@ -39,13 +39,13 @@ private[bindings] trait SeqPropertyModifierUtils[T, E <: ReadableProperty[T]] ex val childNodes: BSeq[Node] = root.childNodes - //index of the first element produced by the binding + // index of the first element produced by the binding val firstIndex = childNodes.indexOf(firstElement) - //number of nodes produced by properties before patch index + // number of nodes produced by properties before patch index val elementsBefore = producedElementsCount.jsSlice(0, patch.idx).sum - //total number of produced nodes + // total number of produced nodes val allElements = elementsBefore + producedElementsCount.iterator.drop(patch.idx).sum // Add new elements @@ -61,8 +61,9 @@ private[bindings] trait SeqPropertyModifierUtils[T, E <: ReadableProperty[T]] ex if (patch.removed.nonEmpty) { def childToRemoveIdx(elIdx: Int): Int = elIdx + firstIndex + newElementsFlatten.size + elementsBefore - val nodesToRemove = (0 until producedElementsCount.jsSlice(patch.idx, patch.idx + patch.removed.size).sum) - .map(idx => root.childNodes(childToRemoveIdx(idx))) + val nodesToRemove = (0 until producedElementsCount.jsSlice(patch.idx, patch.idx + patch.removed.size).sum).map( + idx => root.childNodes(childToRemoveIdx(idx)) + ) val replacement = { // if no new elements were added and all old ones are to be removed, add a placeholder @@ -78,9 +79,8 @@ private[bindings] trait SeqPropertyModifierUtils[T, E <: ReadableProperty[T]] ex val sizeChange = patch.added.size - patch.removed.size if (sizeChange > 0) producedElementsCount.insertAll(patch.idx, Seq.fill(sizeChange)(0)) else producedElementsCount.remove(patch.idx, -sizeChange) - newElements.zipWithIndex.foreach { - case (res, idx) => - producedElementsCount(patch.idx + idx) = res.size + newElements.zipWithIndex.foreach { case (res, idx) => + producedElementsCount(patch.idx + idx) = res.size } } @@ -102,5 +102,3 @@ private[bindings] trait SeqPropertyModifierUtils[T, E <: ReadableProperty[T]] ex } } } - - diff --git a/core/.js/src/main/scala/io/udash/bindings/modifiers/SeqPropertyWithIndexModifier.scala b/core/.js/src/main/scala/io/udash/bindings/modifiers/SeqPropertyWithIndexModifier.scala index 4c20d09f2..59363476d 100644 --- a/core/.js/src/main/scala/io/udash/bindings/modifiers/SeqPropertyWithIndexModifier.scala +++ b/core/.js/src/main/scala/io/udash/bindings/modifiers/SeqPropertyWithIndexModifier.scala @@ -10,7 +10,7 @@ private[bindings] final class SeqPropertyWithIndexModifier[T, E <: ReadablePrope override val property: ReadableSeqProperty[T, E], builder: (E, ReadableProperty[Int], Binding.NestedInterceptor) => Seq[Node], override val customElementsReplace: DOMManipulator.ReplaceMethod, - override val customElementsInsert: DOMManipulator.InsertMethod + override val customElementsInsert: DOMManipulator.InsertMethod, ) extends SeqPropertyModifierUtils[T, E] { private val indexes = CrossCollections.createMap[E, Property[Int]] diff --git a/core/.js/src/main/scala/io/udash/bindings/modifiers/SimplePropertyModifier.scala b/core/.js/src/main/scala/io/udash/bindings/modifiers/SimplePropertyModifier.scala index d01143694..d61f96ebc 100644 --- a/core/.js/src/main/scala/io/udash/bindings/modifiers/SimplePropertyModifier.scala +++ b/core/.js/src/main/scala/io/udash/bindings/modifiers/SimplePropertyModifier.scala @@ -4,9 +4,10 @@ import io.udash._ import io.udash.properties.single.ReadableProperty import org.scalajs.dom -private[bindings] final class SimplePropertyModifier(property: ReadableProperty[_]) extends PropertyModifier[Any]( - property, - t => dom.document.createTextNode(t.toString), - checkNull = true, - DOMManipulator.DefaultElementReplace -) +private[bindings] final class SimplePropertyModifier(property: ReadableProperty[_]) + extends PropertyModifier[Any]( + property, + t => dom.document.createTextNode(t.toString), + checkNull = true, + DOMManipulator.DefaultElementReplace, + ) diff --git a/core/.js/src/main/scala/io/udash/bindings/modifiers/ValueModifier.scala b/core/.js/src/main/scala/io/udash/bindings/modifiers/ValueModifier.scala index c1cfb029f..d645a4d02 100644 --- a/core/.js/src/main/scala/io/udash/bindings/modifiers/ValueModifier.scala +++ b/core/.js/src/main/scala/io/udash/bindings/modifiers/ValueModifier.scala @@ -36,4 +36,4 @@ private[bindings] trait ValueModifier[T] extends Binding with DOMManipulator { propertyListeners.push(listen(rebuild)) rebuild(property.get) } -} \ No newline at end of file +} diff --git a/core/.js/src/main/scala/io/udash/bindings/modifiers/package.scala b/core/.js/src/main/scala/io/udash/bindings/modifiers/package.scala index 98f36da0b..86ee99e84 100644 --- a/core/.js/src/main/scala/io/udash/bindings/modifiers/package.scala +++ b/core/.js/src/main/scala/io/udash/bindings/modifiers/package.scala @@ -11,7 +11,9 @@ package object modifiers { el.replaceChild(fresh, old) } oldChildren.iterator.drop(newChildren.size).foreach(el.removeChild) - newChildren.iterator.drop(oldChildren.size - 1).sliding(2) + newChildren.iterator + .drop(oldChildren.size - 1) + .sliding(2) .foreach(s => if (s.size == 2) el.insertBefore(s(1), s(0).nextSibling)) } } diff --git a/core/.js/src/main/scala/io/udash/component/Component.scala b/core/.js/src/main/scala/io/udash/component/Component.scala index 368ce19b6..c26d34846 100644 --- a/core/.js/src/main/scala/io/udash/component/Component.scala +++ b/core/.js/src/main/scala/io/udash/component/Component.scala @@ -5,6 +5,7 @@ import org.scalajs.dom.Element /** Base trait for Udash based components. */ trait Component extends Binding { + /** Component root DOM element ID. */ val componentId: ComponentId @@ -13,4 +14,4 @@ trait Component extends Binding { override def applyTo(t: Element): Unit = t.appendChild(render) -} \ No newline at end of file +} diff --git a/core/.js/src/main/scala/io/udash/component/Listenable.scala b/core/.js/src/main/scala/io/udash/component/Listenable.scala index 0c84511d1..915134601 100644 --- a/core/.js/src/main/scala/io/udash/component/Listenable.scala +++ b/core/.js/src/main/scala/io/udash/component/Listenable.scala @@ -3,36 +3,33 @@ package io.udash.component import com.avsystem.commons._ import io.udash.Registration -/** - * A class (typically a component) exposing callbacks events. - * - * Callbacks emitted by this utility interface are executed in the order of registration (restart counts as an registration). - * Events can be defined via implementing [[ListenableEvent]] and setting the `EventType` appropriately. - * - */ +/** A class (typically a component) exposing callbacks events. + * + * Callbacks emitted by this utility interface are executed in the order of registration (restart counts as an + * registration). Events can be defined via implementing [[ListenableEvent]] and setting the `EventType` appropriately. + */ trait Listenable { - /** - * The type of emitted events. Has to be set to the top-level type of events emitted by this component. - */ + /** The type of emitted events. Has to be set to the top-level type of events emitted by this component. + */ type EventType <: ListenableEvent final type EventHandler = PartialFunction[EventType, Unit] - //visible for testing + // visible for testing private[udash] val listeners = MLinkedHashSet.empty[EventHandler] - /** - * Registers an event handler in the component. - * - * @param onEvent Partial function which handles component events. - * @return [[Registration]] which allows you to manage the lifecycle of the listener. - */ + /** Registers an event handler in the component. + * + * @param onEvent + * Partial function which handles component events. + * @return + * [[Registration]] which allows you to manage the lifecycle of the listener. + */ final def listen(onEvent: EventHandler): Registration = new ListenableRegistration(onEvent) - /** - * Removes all registered listeners. - */ + /** Removes all registered listeners. + */ final def removeListeners(): Unit = listeners.clear() private class ListenableRegistration(onEvent: EventHandler) extends Registration { @@ -47,15 +44,13 @@ trait Listenable { override def isActive: Boolean = listeners.contains(onEvent) } - /** - * Fires an event on the Listenable. - */ + /** Fires an event on the Listenable. + */ protected final def fire(event: EventType): Unit = listeners.foreach(_.applyOrElse(event, (_: EventType) => ())) } -/** - * An event emitted by a [[Listenable]]. - */ +/** An event emitted by a [[Listenable]]. + */ trait ListenableEvent { self => - val source: Listenable {type EventType >: self.type <: ListenableEvent} + val source: Listenable { type EventType >: self.type <: ListenableEvent } } diff --git a/core/.js/src/main/scala/io/udash/core/Defaults.scala b/core/.js/src/main/scala/io/udash/core/Defaults.scala index 052c032b3..d7762be3b 100644 --- a/core/.js/src/main/scala/io/udash/core/Defaults.scala +++ b/core/.js/src/main/scala/io/udash/core/Defaults.scala @@ -1,11 +1,10 @@ package io.udash.core -/** - * Creates view with [[io.udash.core.EmptyPresenter]]. Used for static views. - * - * By default, instances of this class are compared by class name to prevent rerendering of static views. - * This behaviour can be opted out of by overriding equals/hashCode. - **/ +/** Creates view with [[io.udash.core.EmptyPresenter]]. Used for static views. + * + * By default, instances of this class are compared by class name to prevent rerendering of static views. This + * behaviour can be opted out of by overriding equals/hashCode. + */ abstract class StaticViewFactory[S <: State](viewCreator: () => View) extends ViewFactory[S] { override def create(): (View, EmptyPresenter.type) = (viewCreator(), EmptyPresenter) @@ -17,4 +16,4 @@ abstract class StaticViewFactory[S <: State](viewCreator: () => View) extends Vi /** Ignores state changes. Useful for static views. */ object EmptyPresenter extends Presenter[State] { override def handleState(state: State): Unit = () -} \ No newline at end of file +} diff --git a/core/.js/src/main/scala/io/udash/core/Definitions.scala b/core/.js/src/main/scala/io/udash/core/Definitions.scala index 84852577a..90ac4a8c1 100644 --- a/core/.js/src/main/scala/io/udash/core/Definitions.scala +++ b/core/.js/src/main/scala/io/udash/core/Definitions.scala @@ -4,63 +4,64 @@ import io.udash.properties.HasModelPropertyCreator import org.scalajs.dom._ import scalatags.generic.Modifier -/** - * Url wrapper - just for avoiding strings. +/** Url wrapper - just for avoiding strings. */ final case class Url(value: String) extends AnyVal object Url extends HasModelPropertyCreator[Url] -/** - * The Presenter should contain all business logic of a view: user interaction callbacks, server communication. - * It should not call any methods of a View class. The View and the Presenter should communicate via Model properties. - * When implementing Presenter, you should remember, that a handleState method can be called not only on view initialization. +/** The Presenter should contain all business logic of a view: user interaction callbacks, server communication. It + * should not call any methods of a View class. The View and the Presenter should communicate via Model properties. + * When implementing Presenter, you should remember, that a handleState method can be called not only on view + * initialization. * - * @tparam S State for which this presenter is defined. + * @tparam S + * State for which this presenter is defined. */ trait Presenter[-S <: State] { - /** - * This method will be called when relevant state needs to be resolved. - * It can be uses to get parameters from state and use it to call eg. external API. - * - * @param state the instance of resolved state - */ + + /** This method will be called when relevant state needs to be resolved. It can be uses to get parameters from state + * and use it to call eg. external API. + * + * @param state + * the instance of resolved state + */ def handleState(state: S): Unit - /** - * This method will be called when this presenter is replaced by another one. This is where you can do cleanup. - */ + /** This method will be called when this presenter is replaced by another one. This is where you can do cleanup. + */ def onClose(): Unit = () } -/** - * The ViewFactory has to prepare model, [[io.udash.core.View]], [[io.udash.core.Presenter]] and link them together. +/** The ViewFactory has to prepare model, [[io.udash.core.View]], [[io.udash.core.Presenter]] and link them together. * - * @tparam S State for which this pair is defined. + * @tparam S + * State for which this pair is defined. */ trait ViewFactory[S <: State] { - /** - * Factory method which should return ready to used instance of [[io.udash.core.Presenter]] and [[io.udash.core.View]]. + + /** Factory method which should return ready to used instance of [[io.udash.core.Presenter]] and + * [[io.udash.core.View]]. * - * @return pair of presenter and view for state S + * @return + * pair of presenter and view for state S */ def create(): (View, Presenter[S]) } -/** - * Abstract view which should be used in order to implement View for [[io.udash.core.ViewFactory]]. - * The View implementation usually gets the model and the [[io.udash.core.Presenter]] as constructor arguments. +/** Abstract view which should be used in order to implement View for [[io.udash.core.ViewFactory]]. The View + * implementation usually gets the model and the [[io.udash.core.Presenter]] as constructor arguments. */ trait View { - /** - * Implementation of this method should return DOM representation of view. + + /** Implementation of this method should return DOM representation of view. * - * @return DOM representation of view + * @return + * DOM representation of view */ def getTemplate: Modifier[Element] - /** - * This method will be called when this view is replaced by another one. This is where you can do cleanup. - */ + /** This method will be called when this view is replaced by another one. This is where you can do cleanup. + */ def onClose(): Unit = () } @@ -76,15 +77,15 @@ trait ContainerView extends View { while (childViewContainer.firstChild != null) childViewContainer.removeChild(childViewContainer.firstChild) - /** - * Will be invoked in order to render the child view inside the parent view.
- * - * This method can receive `None` as "view" argument, then previous child view should be removed. - * - * The default implementation removes everything from `childViewContainer` and renders new subview inside. - * - * @param view view which origins from child - */ + /** Will be invoked in order to render the child view inside the parent view.
+ * + * This method can receive `None` as "view" argument, then previous child view should be removed. + * + * The default implementation removes everything from `childViewContainer` and renders new subview inside. + * + * @param view + * view which origins from child + */ def renderChild(view: Option[View]): Unit = { clearChildViewContainer() view.foreach(_.getTemplate.applyTo(childViewContainer)) @@ -93,14 +94,13 @@ trait ContainerView extends View { /** The class which should be used to present the state for [[io.udash.routing.RoutingRegistry]]. */ trait State { - type HierarchyRoot <: State {type HierarchyRoot = State.this.HierarchyRoot} + type HierarchyRoot <: State { type HierarchyRoot = State.this.HierarchyRoot } def parentState: Option[HierarchyRoot] } -/** - * The implementation of this trait should be injected to [[io.udash.Application]]. - * It is used to map [[State]] to [[ViewFactory]]. - */ +/** The implementation of this trait should be injected to [[io.udash.Application]]. It is used to map [[State]] to + * [[ViewFactory]]. + */ trait ViewFactoryRegistry[HierarchyRoot <: State] { def matchStateToResolver(state: HierarchyRoot): ViewFactory[_ <: HierarchyRoot] } diff --git a/core/.js/src/main/scala/io/udash/package.scala b/core/.js/src/main/scala/io/udash/package.scala index f6115b9ab..b1fc195bb 100644 --- a/core/.js/src/main/scala/io/udash/package.scala +++ b/core/.js/src/main/scala/io/udash/package.scala @@ -1,9 +1,7 @@ package io package object udash - extends io.udash.bindings.Bindings - with io.udash.properties.Properties - with io.udash.routing.Routing { + extends io.udash.bindings.Bindings with io.udash.properties.Properties with io.udash.routing.Routing { // Defaults type StaticViewFactory[S <: State] = io.udash.core.StaticViewFactory[S] @@ -21,7 +19,7 @@ package object udash type ContainerView = io.udash.core.ContainerView type State = io.udash.core.State - type GState[HRoot <: State] = io.udash.core.State {type HierarchyRoot = HRoot} + type GState[HRoot <: State] = io.udash.core.State { type HierarchyRoot = HRoot } type RoutingRegistry[HierarchyRoot <: State] = io.udash.routing.RoutingRegistry[HierarchyRoot] diff --git a/core/.js/src/main/scala/io/udash/routing/Routing.scala b/core/.js/src/main/scala/io/udash/routing/Routing.scala index 45bf93774..0ce1cf16c 100644 --- a/core/.js/src/main/scala/io/udash/routing/Routing.scala +++ b/core/.js/src/main/scala/io/udash/routing/Routing.scala @@ -5,4 +5,3 @@ trait Routing { type WindowUrlFragmentChangeProvider = io.udash.routing.WindowUrlFragmentChangeProvider type WindowUrlPathChangeProvider = io.udash.routing.WindowUrlPathChangeProvider } - diff --git a/core/.js/src/main/scala/io/udash/routing/RoutingEngine.scala b/core/.js/src/main/scala/io/udash/routing/RoutingEngine.scala index e3b1130ae..4a0a957eb 100644 --- a/core/.js/src/main/scala/io/udash/routing/RoutingEngine.scala +++ b/core/.js/src/main/scala/io/udash/routing/RoutingEngine.scala @@ -13,25 +13,25 @@ import scala.annotation.{nowarn, tailrec} final case class StateChangeEvent[S <: State](currentState: S, oldState: S) extends AbstractCase -/** - * RoutingEngine handles URL changes by resolving application [[io.udash.core.State]] with - * matching [[io.udash.core.ViewFactory]]s and rendering views via passed [[io.udash.ViewRenderer]]. - */ -private[udash] final class RoutingEngine[HierarchyRoot >: Null <: GState[HierarchyRoot] : PropertyCreator]( +/** RoutingEngine handles URL changes by resolving application [[io.udash.core.State]] with matching + * [[io.udash.core.ViewFactory]]s and rendering views via passed [[io.udash.ViewRenderer]]. + */ +private[udash] final class RoutingEngine[HierarchyRoot >: Null <: GState[HierarchyRoot]: PropertyCreator]( routingRegistry: RoutingRegistry[HierarchyRoot], viewFactoryRegistry: ViewFactoryRegistry[HierarchyRoot], - viewRenderer: ViewRenderer + viewRenderer: ViewRenderer, ) extends CrossLogging { private val currentStateProp = Property(null: HierarchyRoot) private val callbacks = new CallbacksHandler[StateChangeEvent[HierarchyRoot]] private val statesMap = MLinkedHashMap.empty[HierarchyRoot, (View, Presenter[_ <: HierarchyRoot])] - /** - * Handles the URL change. Gets a routing states hierarchy for the provided URL and redraws only changed ViewFactories. - * - * @param url URL to be resolved - */ + /** Handles the URL change. Gets a routing states hierarchy for the provided URL and redraws only changed + * ViewFactories. + * + * @param url + * URL to be resolved + */ def handleUrl(url: Url, fullReload: Boolean = false): Try[Unit] = Try { if (fullReload) { cleanup(statesMap.valuesIterator) @@ -48,11 +48,16 @@ private[udash] final class RoutingEngine[HierarchyRoot >: Null <: GState[Hierarc val diffPath = findDiffSuffix(newStatePath.iterator, statesMap.keysIterator).toSeq val (viewsToLeave, viewsToAdd) = { - val toUpdateStatesSize = getUpdatablePathSize(diffPath.iterator, statesMap.slice(samePathSize, statesMap.size).keysIterator) - cleanup(statesMap.slice(samePathSize + toUpdateStatesSize, statesMap.size).valuesIterator) //cleanup removed states + val toUpdateStatesSize = + getUpdatablePathSize(diffPath.iterator, statesMap.slice(samePathSize, statesMap.size).keysIterator) + cleanup( + statesMap.slice(samePathSize + toUpdateStatesSize, statesMap.size).valuesIterator + ) // cleanup removed states val oldViewFactories = - newStatePath.view.slice(samePathSize, samePathSize + toUpdateStatesSize).iterator + newStatePath.view + .slice(samePathSize, samePathSize + toUpdateStatesSize) + .iterator .zip(statesMap.slice(samePathSize, samePathSize + toUpdateStatesSize).valuesIterator) var i = samePathSize statesMap.retain { (_, _) => @@ -62,11 +67,15 @@ private[udash] final class RoutingEngine[HierarchyRoot >: Null <: GState[Hierarc statesMap ++= oldViewFactories val viewsToLeave = statesMap.values.map(_._1).iterator - val views = diffPath.view.slice(toUpdateStatesSize, diffPath.size).iterator.map { state => - val (view, presenter) = viewFactoryRegistry.matchStateToResolver(state).create() - statesMap(state) = (view, presenter) - view - }.toList + val views = diffPath.view + .slice(toUpdateStatesSize, diffPath.size) + .iterator + .map { state => + val (view, presenter) = viewFactoryRegistry.matchStateToResolver(state).create() + statesMap(state) = (view, presenter) + view + } + .toList (viewsToLeave, views) } @@ -82,24 +91,24 @@ private[udash] final class RoutingEngine[HierarchyRoot >: Null <: GState[Hierarc if (fullReload || newState != oldState) callbacks.fire(StateChangeEvent(newState, oldState)) }.recover { case ex: Throwable => statesMap.clear(); throw ex } - /** - * Register a callback for the routing state change. - * - * @param callback Callback getting StateChangeEvent as arguments - */ + /** Register a callback for the routing state change. + * + * @param callback + * Callback getting StateChangeEvent as arguments + */ def onStateChange(callback: StateChangeEvent[HierarchyRoot] => Any): Registration = - onStateChange({ - case x => callback(x) + onStateChange({ case x => + callback(x) }: callbacks.CallbackType) - /** - * Register a callback for the routing state change. - * - * The callbacks are executed in order of registration. Registration operations don't preserve callbacks order. - * Each callback is executed once, exceptions thrown in callbacks are swallowed. - * - * @param callback Callback (PartialFunction) getting StateChangeEvent as arguments - */ + /** Register a callback for the routing state change. + * + * The callbacks are executed in order of registration. Registration operations don't preserve callbacks order. Each + * callback is executed once, exceptions thrown in callbacks are swallowed. + * + * @param callback + * Callback (PartialFunction) getting StateChangeEvent as arguments + */ def onStateChange(callback: callbacks.CallbackType): Registration = callbacks.register(callback) @@ -110,15 +119,19 @@ private[udash] final class RoutingEngine[HierarchyRoot >: Null <: GState[Hierarc def currentStateProperty: ReadableProperty[HierarchyRoot] = currentStateProp @tailrec - private def getStatePath(forState: Option[HierarchyRoot], acc: List[HierarchyRoot] = Nil): List[HierarchyRoot] = forState match { - case Some(state) => getStatePath(state.parentState, state :: acc) - case None => acc - } + private def getStatePath(forState: Option[HierarchyRoot], acc: List[HierarchyRoot] = Nil): List[HierarchyRoot] = + forState match { + case Some(state) => getStatePath(state.parentState, state :: acc) + case None => acc + } private def getUpdatablePathSize(path: Iterator[HierarchyRoot], oldPath: Iterator[HierarchyRoot]): Int = - path.zip(oldPath).takeWhile { - case (h1, h2) => viewFactoryRegistry.matchStateToResolver(h1) == viewFactoryRegistry.matchStateToResolver(h2) - }.length + path + .zip(oldPath) + .takeWhile { case (h1, h2) => + viewFactoryRegistry.matchStateToResolver(h1) == viewFactoryRegistry.matchStateToResolver(h2) + } + .length private def cleanup(state: Iterator[(View, Presenter[_])]): Unit = { state.foreach { case (view, presenter) => diff --git a/core/.js/src/main/scala/io/udash/routing/RoutingRegistry.scala b/core/.js/src/main/scala/io/udash/routing/RoutingRegistry.scala index 321486eee..d395c15c3 100644 --- a/core/.js/src/main/scala/io/udash/routing/RoutingRegistry.scala +++ b/core/.js/src/main/scala/io/udash/routing/RoutingRegistry.scala @@ -2,16 +2,16 @@ package io.udash.routing import io.udash.core.{State, Url} -/** - * The implementation of this trait should be injected to [[io.udash.Application]]. - * It should implement a bidirectional mapping between [[io.udash.core.Url]] and [[io.udash.core.State]]. - */ +/** The implementation of this trait should be injected to [[io.udash.Application]]. It should implement a bidirectional + * mapping between [[io.udash.core.Url]] and [[io.udash.core.State]]. + */ trait RoutingRegistry[HierarchyRoot <: State] { def matchUrl(url: Url): HierarchyRoot def matchState(state: HierarchyRoot): Url - protected def bidirectional(pf: PartialFunction[String, HierarchyRoot]): (PartialFunction[String, HierarchyRoot], PartialFunction[HierarchyRoot, String]) = - macro com.avsystem.commons.macros.misc.BidirectionalMacro.impl[String, HierarchyRoot] + protected def bidirectional(pf: PartialFunction[String, HierarchyRoot]) + : (PartialFunction[String, HierarchyRoot], PartialFunction[HierarchyRoot, String]) = + macro com.avsystem.commons.macros.misc.BidirectionalMacro.impl[String, HierarchyRoot] import RoutingRegistry._ diff --git a/core/.js/src/main/scala/io/udash/routing/UrlChangeProvider.scala b/core/.js/src/main/scala/io/udash/routing/UrlChangeProvider.scala index b1116d15d..a5cc7fb1c 100644 --- a/core/.js/src/main/scala/io/udash/routing/UrlChangeProvider.scala +++ b/core/.js/src/main/scala/io/udash/routing/UrlChangeProvider.scala @@ -11,8 +11,9 @@ import scala.scalajs.js /** Provides information about current URL. */ trait UrlChangeProvider { - /** Enables all required event listeners. - * [[io.udash.Application]] initializes the provider automatically. */ + + /** Enables all required event listeners. [[io.udash.Application]] initializes the provider automatically. + */ def initialize(): Unit /** Changes the URL part representing the frontend routing state. */ @@ -52,10 +53,9 @@ final class WindowUrlFragmentChangeProvider extends UrlChangeProvider { } } -/** - * Used for routing based on the URL path. - * Don't forget to configure your web server to handle frontend routes. You may find "rewrite rules" mechanism useful. - */ +/** Used for routing based on the URL path. Don't forget to configure your web server to handle frontend routes. You may + * find "rewrite rules" mechanism useful. + */ final class WindowUrlPathChangeProvider extends UrlChangeProvider { import dom.window @@ -78,9 +78,13 @@ final class WindowUrlPathChangeProvider extends UrlChangeProvider { @inline private def shouldIgnoreClick( - event: MouseEvent, target: Element, href: String, - samePath: Boolean, sameHash: Boolean, sameOrigin: Boolean - ): Boolean = { + event: MouseEvent, + target: Element, + href: String, + samePath: Boolean, + sameHash: Boolean, + sameOrigin: Boolean, + ): Boolean = // handle only links in the same browser card event.button != 0 || event.metaKey || event.ctrlKey || event.shiftKey || // ignore click if default already prevented @@ -91,26 +95,30 @@ final class WindowUrlPathChangeProvider extends UrlChangeProvider { !sameOrigin || // ignore if only the URL fragment changed, but path is the same (samePath && !sameHash) - } override def initialize(): Unit = { - window.document.addEventListener("click", (event: MouseEvent) => { - event.target.opt - .collect { case node: Node => node } - .flatMap(Iterator.iterate(_)(_.parentNode).takeWhile(_ != null).collectFirstOpt { case a: HTMLAnchorElement => a }) - .filter(_.getAttribute("href") != null) - .foreach { target => - val href = target.getAttribute("href") - val location = window.location - val newUrl = new JSUrl(href, location.toString) - val (samePath, sameHash, sameOrigin) = - (isSamePath(location, newUrl), isSameHash(location, newUrl), isSameOrigin(location, newUrl)) - if (!shouldIgnoreClick(event, target, href, samePath, sameHash, sameOrigin)) { - if (!samePath) changeFragment(Url(href)) - event.preventDefault() + window.document.addEventListener( + "click", + (event: MouseEvent) => { + event.target.opt + .collect { case node: Node => node } + .flatMap(Iterator.iterate(_)(_.parentNode).takeWhile(_ != null).collectFirstOpt { case a: HTMLAnchorElement => + a + }) + .filter(_.getAttribute("href") != null) + .foreach { target => + val href = target.getAttribute("href") + val location = window.location + val newUrl = new JSUrl(href, location.toString) + val (samePath, sameHash, sameOrigin) = + (isSamePath(location, newUrl), isSameHash(location, newUrl), isSameOrigin(location, newUrl)) + if (!shouldIgnoreClick(event, target, href, samePath, sameHash, sameOrigin)) { + if (!samePath) changeFragment(Url(href)) + event.preventDefault() + } } - } - }) + }, + ) window.addEventListener("popstate", (_: PopStateEvent) => callbacks.foreach(_.apply(currentFragment))) } @@ -124,11 +132,11 @@ final class WindowUrlPathChangeProvider extends UrlChangeProvider { (null, "", url.value) |> ( if (replaceCurrent) window.history.replaceState(_: js.Any, _: String, _: String) else window.history.pushState(_: js.Any, _: String, _: String) - ).tupled + ).tupled val withoutHash = Url(url.value.takeWhile(_ != '#')) callbacks.foreach(_.apply(withoutHash)) } override def currentFragment: Url = Url(window.history.state.opt.map(_.asInstanceOf[js.Dynamic].url.toString).getOrElse(window.location.pathname)) -} \ No newline at end of file +} diff --git a/core/.js/src/main/scala/io/udash/routing/UrlLogging.scala b/core/.js/src/main/scala/io/udash/routing/UrlLogging.scala index b4fe19c92..129ba685b 100644 --- a/core/.js/src/main/scala/io/udash/routing/UrlLogging.scala +++ b/core/.js/src/main/scala/io/udash/routing/UrlLogging.scala @@ -5,14 +5,14 @@ import io.udash.logging.CrossLogging import scala.util.Try -/** - * RoutingRegistry mixin simplifying logging app navigation. +/** RoutingRegistry mixin simplifying logging app navigation. */ trait UrlLogging[S >: Null <: GState[S]] extends CrossLogging { app: Application[S] => protected def log(url: String, referrer: Option[String]): Unit app.onStateChange(event => - Try(log(matchState(event.currentState).value, Try(matchState(event.oldState).value).toOption)) - .failed - .foreach(t => logger.warn("Logging url change failed: {}", t.getMessage))) + Try(log(matchState(event.currentState).value, Try(matchState(event.oldState).value).toOption)).failed.foreach(t => + logger.warn("Logging url change failed: {}", t.getMessage) + ) + ) } diff --git a/core/.js/src/main/scala/io/udash/utils/FileService.scala b/core/.js/src/main/scala/io/udash/utils/FileService.scala index aed64835a..bf9aa7a20 100644 --- a/core/.js/src/main/scala/io/udash/utils/FileService.scala +++ b/core/.js/src/main/scala/io/udash/utils/FileService.scala @@ -18,67 +18,65 @@ object FileService { final val OctetStreamType = "application/octet-stream" - /** - * Converts specified bytes arrays to string that contains URL - * that representing the array given in the parameter with specified mime-type. - * - * Keep in mind that returned URL should be closed. - */ + /** Converts specified bytes arrays to string that contains URL that representing the array given in the parameter + * with specified mime-type. + * + * Keep in mind that returned URL should be closed. + */ def createURL(bytesArrays: Seq[Array[Byte]], mimeType: String): CloseableUrl = { import js.typedarray._ import scalajs.js.JSConverters._ - val blob = new Blob(bytesArrays.iterator.map(_.toTypedArray: BlobPart).toJSArray, new BlobPropertyBag { - `type` = mimeType - }) + val blob = new Blob( + bytesArrays.iterator.map(_.toTypedArray: BlobPart).toJSArray, + new BlobPropertyBag { + `type` = mimeType + }, + ) CloseableUrl(URL.createObjectURL(blob)) } - /** - * Converts specified bytes arrays to string that contains URL - * that representing the array given in the parameter with `application/octet-stream` mime-type. - * - * Keep in mind that returned URL should be closed. - */ + /** Converts specified bytes arrays to string that contains URL that representing the array given in the parameter + * with `application/octet-stream` mime-type. + * + * Keep in mind that returned URL should be closed. + */ def createURL(bytesArrays: Seq[Array[Byte]]): CloseableUrl = createURL(bytesArrays, OctetStreamType) - /** - * Converts specified bytes array to string that contains URL - * that representing the array given in the parameter with specified mime-type. - * - * Keep in mind that returned URL should be closed. - */ + /** Converts specified bytes array to string that contains URL that representing the array given in the parameter with + * specified mime-type. + * + * Keep in mind that returned URL should be closed. + */ def createURL(byteArray: Array[Byte], mimeType: String): CloseableUrl = createURL(Seq(byteArray), mimeType) - /** - * Converts specified bytes array to string that contains URL - * that representing the array given in the parameter with `application/octet-stream` mime-type. - * - * Keep in mind that returned URL should be closed. - */ + /** Converts specified bytes array to string that contains URL that representing the array given in the parameter with + * `application/octet-stream` mime-type. + * + * Keep in mind that returned URL should be closed. + */ def createURL(byteArray: Array[Byte]): CloseableUrl = createURL(Seq(byteArray), OctetStreamType) - /** - * Asynchronously convert specified part of file to bytes array. - */ + /** Asynchronously convert specified part of file to bytes array. + */ def asBytesArray(file: File, start: Double, end: Double): Future[Array[Byte]] = { import js.typedarray._ val fileReader = new FileReader() val promise = Promise[Array[Byte]]() - fileReader.onerror = (e: Event) => - promise.failure(new IOException(e.toString)) + fileReader.onerror = (e: Event) => promise.failure(new IOException(e.toString)) - fileReader.onabort = (e: Event) => - promise.failure(new IOException(e.toString)) + fileReader.onabort = (e: Event) => promise.failure(new IOException(e.toString)) fileReader.onload = (_: ProgressEvent) => - promise.complete(Try( - new Int8Array(fileReader.result.asInstanceOf[ArrayBuffer]).toArray - )) + promise.complete( + Try( + new Int8Array(fileReader.result.asInstanceOf[ArrayBuffer]).toArray + ) + ) val slice = file.slice(start, end) fileReader.readAsArrayBuffer(slice) @@ -86,20 +84,18 @@ object FileService { promise.future } - /** - * Asynchronously convert specified file to bytes array. - */ + /** Asynchronously convert specified file to bytes array. + */ def asBytesArray(file: File): Future[Array[Byte]] = asBytesArray(file, 0, file.size) - /** - * Synchronously convert specified part of file to bytes array. - * - * Because it is using synchronous I/O this API can be used only inside worker. - * - * This method is using FileReaderSync that is part of Working Draft File API. - * Anyway it is supported for majority of modern browsers - */ + /** Synchronously convert specified part of file to bytes array. + * + * Because it is using synchronous I/O this API can be used only inside worker. + * + * This method is using FileReaderSync that is part of Working Draft File API. Anyway it is supported for majority of + * modern browsers + */ def asBytesArraySync(file: File, start: Double, end: Double): Array[Byte] = { import js.typedarray._ @@ -111,14 +107,13 @@ object FileService { int8Array.toArray } - /** - * Synchronously convert file to bytes array. - * - * Because it is using synchronous I/O this API can be used only inside worker. - * - * This method is using FileReaderSync that is part of Working Draft File API. - * Anyway it is supported for majority of modern browsers - */ + /** Synchronously convert file to bytes array. + * + * Because it is using synchronous I/O this API can be used only inside worker. + * + * This method is using FileReaderSync that is part of Working Draft File API. Anyway it is supported for majority of + * modern browsers + */ def asBytesArraySync(file: File): Array[Byte] = asBytesArraySync(file, 0, file.size) } diff --git a/core/.js/src/main/scala/io/udash/utils/FileUploader.scala b/core/.js/src/main/scala/io/udash/utils/FileUploader.scala index 945d12d4c..f9acafec5 100644 --- a/core/.js/src/main/scala/io/udash/utils/FileUploader.scala +++ b/core/.js/src/main/scala/io/udash/utils/FileUploader.scala @@ -15,56 +15,63 @@ class FileUploader(url: Url) { def upload(input: html.Input): ReadableModelProperty[FileUploadModel] = upload( input.name, - (0 until input.files.length).map(input.files.item) + (0 until input.files.length).map(input.files.item), ) /** Uploads provided `file` in a field named `fieldName` with optional additional request headers. */ def uploadFile( - fieldName: String, file: File, extraData: Map[String, String | Blob] = Map.empty, additionalRequestHeaders: Map[RequestName, RequestValue] = Map.empty + fieldName: String, + file: File, + extraData: Map[String, String | Blob] = Map.empty, + additionalRequestHeaders: Map[RequestName, RequestValue] = Map.empty, ): ReadableModelProperty[FileUploadModel] = upload(fieldName, Seq(file), extraData = extraData, additionalRequestHeaders = additionalRequestHeaders) /** Uploads provided `files` in a field named `fieldName` with optional additional request headers. */ def upload( - fieldName: String, files: Seq[File], extraData: Map[String, String | Blob] = Map.empty, additionalRequestHeaders: Map[RequestName, RequestValue] = Map.empty + fieldName: String, + files: Seq[File], + extraData: Map[String, String | Blob] = Map.empty, + additionalRequestHeaders: Map[RequestName, RequestValue] = Map.empty, ): ReadableModelProperty[FileUploadModel] = { val p = ModelProperty[FileUploadModel]( new FileUploadModel(Seq.empty, FileUploadState.InProgress, 0, 0, None) ) val data = new FormData() - extraData.foreach { - case (key, value) => - (value: Any) match { - case string: String => data.append(key, string) - case blob: Blob => data.append(key, blob) - } + extraData.foreach { case (key, value) => + (value: Any) match { + case string: String => data.append(key, string) + case blob: Blob => data.append(key, blob) + } } - files.foreach(file => { + files.foreach { file => data.append(fieldName, file) p.subSeq(_.files).append(file) - }) + } val xhr = new XMLHttpRequest - xhr.upload.addEventListener("progress", (ev: ProgressEvent) => - if (ev.lengthComputable) { - p.subProp(_.bytesSent).set(ev.loaded) - p.subProp(_.bytesTotal).set(ev.total) - } - ) - xhr.addEventListener("load", (_: Event) => { - p.subProp(_.response).set(Some(new HttpResponse(xhr))) - p.subProp(_.state).set(xhr.status / 100 match { - case 2 => FileUploadState.Completed - case _ => FileUploadState.Failed - }) - }) - xhr.addEventListener("error", (_: Event) => - p.subProp(_.state).set(FileUploadState.Failed) + xhr.upload.addEventListener( + "progress", + (ev: ProgressEvent) => + if (ev.lengthComputable) { + p.subProp(_.bytesSent).set(ev.loaded) + p.subProp(_.bytesTotal).set(ev.total) + }, ) - xhr.addEventListener("abort", (_: Event) => - p.subProp(_.state).set(FileUploadState.Cancelled) + xhr.addEventListener( + "load", + (_: Event) => { + p.subProp(_.response).set(Some(new HttpResponse(xhr))) + p.subProp(_.state) + .set(xhr.status / 100 match { + case 2 => FileUploadState.Completed + case _ => FileUploadState.Failed + }) + }, ) + xhr.addEventListener("error", (_: Event) => p.subProp(_.state).set(FileUploadState.Failed)) + xhr.addEventListener("abort", (_: Event) => p.subProp(_.state).set(FileUploadState.Cancelled)) xhr.open(method = "POST", url = url.value) additionalRequestHeaders.foreach { case (name, value) => xhr.setRequestHeader(name.name, value.value) } xhr.send(data) @@ -106,7 +113,7 @@ object FileUploader { val state: FileUploadState, val bytesSent: Double, val bytesTotal: Double, - val response: Option[HttpResponse] + val response: Option[HttpResponse], ) object FileUploadModel extends HasModelPropertyCreator[FileUploadModel] } diff --git a/core/.js/src/main/scala/io/udash/view/ViewRenderer.scala b/core/.js/src/main/scala/io/udash/view/ViewRenderer.scala index e000c7411..46a359f37 100644 --- a/core/.js/src/main/scala/io/udash/view/ViewRenderer.scala +++ b/core/.js/src/main/scala/io/udash/view/ViewRenderer.scala @@ -8,9 +8,8 @@ import org.scalajs.dom.Element import scala.annotation.nowarn import scala.scalajs.js -/** - * ViewRenderer is used to provide mechanism to render nested [[View]] within provided [[rootElement]]. - */ +/** ViewRenderer is used to provide mechanism to render nested [[View]] within provided [[rootElement]]. + */ private[udash] class ViewRenderer(rootElement: => Element) { private lazy val endpoint = rootElement private val views: MBuffer[View] = js.Array[View]() @@ -21,7 +20,9 @@ private[udash] class ViewRenderer(rootElement: => Element) { case p: ContainerView => p.renderChild(Some(child)) case rest => - throw new RuntimeException(s"Only instances of ContainerView can render a child view! Check the states hierarchy of view $rest.") + throw new RuntimeException( + s"Only instances of ContainerView can render a child view! Check the states hierarchy of view $rest." + ) } pathIterator.nextOpt.setup(_.foreach { top => @@ -34,31 +35,22 @@ private[udash] class ViewRenderer(rootElement: => Element) { }) } - /** - * Updates views hierarchy. - *

- * Example:
- * Current views: A -> B -> C -> D
- * subPathToLeave: A -> B
- * pathToAdd: E -> F
- *
- * Calls:
- * A - nothing
- * B - renderChild(None); renderChild(E)
- * C - renderChild(None)
- * D - renderChild(None) // if D is a ContainerView
- * E - getTemplate(); renderChild(F)
- * F - getTemplate()
- * - * @param subPathToLeave prefix of views hierarchy, which will not be removed - * @param pathToAdd views list, which will be added to hierarchy - */ + /** Updates views hierarchy.

Example:
Current views: A -> B -> C -> D
subPathToLeave: A -> B + *
pathToAdd: E -> F

Calls:
A - nothing
B - renderChild(None); renderChild(E)
C - + * renderChild(None)
D - renderChild(None) // if D is a ContainerView
E - getTemplate(); + * renderChild(F)
F - getTemplate()
+ * + * @param subPathToLeave + * prefix of views hierarchy, which will not be removed + * @param pathToAdd + * views list, which will be added to hierarchy + */ def renderView(subPathToLeave: Iterator[View], pathToAdd: Iterable[View]): Unit = { val viewsToLeaveSize = findEqPrefix(subPathToLeave, views.iterator).size val rootView = views.applyOpt(viewsToLeaveSize - 1) - //technically e.g. B from docs stays, but we run it through the algorithm anyway for proper children cleanup + // technically e.g. B from docs stays, but we run it through the algorithm anyway for proper children cleanup val unmodifiedViews = rootView.foldLeft(viewsToLeaveSize)((unmodified, _) => unmodified - 1) - views.drop(unmodifiedViews).foreach { //doesn't mutate views + views.drop(unmodifiedViews).foreach { // doesn't mutate views case c: ContainerView => c.renderChild(None) case _ => } diff --git a/core/.js/src/test/scala/io/udash/bindings/QueuedNodeModifierTest.scala b/core/.js/src/test/scala/io/udash/bindings/QueuedNodeModifierTest.scala index 18e03eab2..5c5fb9160 100644 --- a/core/.js/src/test/scala/io/udash/bindings/QueuedNodeModifierTest.scala +++ b/core/.js/src/test/scala/io/udash/bindings/QueuedNodeModifierTest.scala @@ -12,7 +12,7 @@ class QueuedNodeModifierTest extends AsyncUdashFrontendTest with Bindings { bind queuedNode { span("heavy thing").render }, - "end" + "end", ).render template.childNodes.length should be(3) @@ -30,10 +30,8 @@ class QueuedNodeModifierTest extends AsyncUdashFrontendTest with Bindings { bind "render node with timeout > 0" in { val template = div( "start", - queuedNode({ - span("heavy thing 2").render - }, (patienceConfig.interval.millisPart * 3).toInt), - "end" + queuedNode(span("heavy thing 2").render, (patienceConfig.interval.millisPart * 3).toInt), + "end", ).render template.childNodes.length should be(3) diff --git a/core/.js/src/test/scala/io/udash/bindings/TagsBindingTest.scala b/core/.js/src/test/scala/io/udash/bindings/TagsBindingTest.scala index fe421d020..1848ef519 100644 --- a/core/.js/src/test/scala/io/udash/bindings/TagsBindingTest.scala +++ b/core/.js/src/test/scala/io/udash/bindings/TagsBindingTest.scala @@ -3,7 +3,7 @@ package io.udash.bindings import com.avsystem.commons._ import io.udash._ import io.udash.bindings.modifiers.Binding -import io.udash.properties.{HasModelPropertyCreator, seq} +import io.udash.properties.{seq, HasModelPropertyCreator} import io.udash.testing.UdashFrontendTest import org.scalajs.dom.Node @@ -44,7 +44,7 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin val template = div( span(), bind(p), - span() + span(), ).render val template2 = div(bind(p)).render @@ -110,7 +110,7 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin bind(p), span("2"), bind(p2), - div("3") + div("3"), ).render template.textContent should be("1A2B3") @@ -180,7 +180,7 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin val template = div( span(), showIf(p)(element.render), - span() + span(), ).render val template2 = div(showIf(p)(element.render)).render @@ -217,7 +217,7 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin showIf(p)(element), span("2"), showIf(p2)(element2), - div("3") + div("3"), ).render template.textContent should be("1Test23") @@ -284,13 +284,13 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin val template = div( span(), showIf(p, customReplace(true))(element), - span() + span(), ).render val element2 = h1("Test").render val template2 = div( span(), showIf(p, customReplace(false))(element2), - span() + span(), ).render template.textContent should be("Test") @@ -333,7 +333,7 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin val template = div( span(), showIfElse(p)(element.render, elseElement.render), - span() + span(), ).render val template2 = div(showIfElse(p)(element.render, elseElement.render)).render @@ -371,7 +371,7 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin showIfElse(p)(element, elseElement), span("2"), showIf(p2)(element2), - div("3") + div("3"), ).render template.textContent should be("1Test23") @@ -440,14 +440,14 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin val template = div( span(), showIfElse(p, customReplace(true))(element, elementElse), - span() + span(), ).render val element2 = h1("Test").render val elementElse2 = h1("Else").render val template2 = div( span(), showIfElse(p, customReplace(false))(element2, elementElse2), - span() + span(), ).render template.textContent should be("Test") @@ -491,10 +491,8 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin val p = Property[String]("ABC") val template = div( span(), - produce(p)((s: String) => { - b(s).render - }), - span() + produce(p)((s: String) => b(s).render), + span(), ).render template.childNodes(0).textContent should be("") @@ -520,7 +518,7 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin produce(p1)(producer), produce(p2)(producer), produce(p3)(producer), - span() + span(), ).render template.textContent should be("122333") @@ -572,9 +570,7 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin "handle null value providing empty content" in { val p = Property[String]("ABC") val template = div( - produce(p)((s: String) => { - b(s).render - }) + produce(p)((s: String) => b(s).render) ).render template.childNodes.apply(0).nodeName should be("B") @@ -601,7 +597,7 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin }, produce(sub) { t => div(t.i).render - } + }, ).render template.textContent should be("") @@ -619,7 +615,7 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin }, produce(sub) { t => div(t.i).render - } + }, ).render template.textContent should be("") @@ -636,10 +632,10 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin "allow custom null handling" in { val p = Property[String]("ABC") val template = div( - produce(p, checkNull = false)((s: String) => { + produce(p, checkNull = false)((s: String) => if (s != null) b(s).render else i("empty").render - }) + ) ).render template.childNodes.apply(0).nodeName should be("B") @@ -663,14 +659,10 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin val p2 = Property[String]("B") val template = div( "1", - produce(p)((s: String) => { - b(s).render - }), + produce(p)((s: String) => b(s).render), span("2"), - produce(p2)((s: String) => { - i(s).render - }), - div("3") + produce(p2)((s: String) => i(s).render), + div("3"), ).render template.textContent should be("1A2B3") @@ -721,7 +713,7 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin "stop updates after `kill` call" in { val p = Property[String]("A") - val binding = produce(p) { v => span(v * 3).render } + val binding = produce(p)(v => span(v * 3).render) val template = div(binding).render template.textContent should be("AAA") @@ -745,10 +737,13 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin val template = div( produceWithNested(p) { case (v, nested) => externalCounter += 1 - div(v, nested(produce(p2) { v2 => - internalCounter += 1 - span(v2).render - })).render + div( + v, + nested(produce(p2) { v2 => + internalCounter += 1 + span(v2).render + }), + ).render } ).render @@ -777,18 +772,18 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin def testCustomReplacement( produceAndReplace: ReadableProperty[Boolean] => Binding, - produceManually: ReadableProperty[Boolean] => Binding + produceManually: ReadableProperty[Boolean] => Binding, ) = { val p = Property[Boolean](true) val template = div( span(), produceAndReplace(p), - span() + span(), ).render val template2 = div( span(), produceManually(p), - span() + span(), ).render template.textContent should be("true") @@ -813,14 +808,18 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin testCustomReplacement( produce(_, customElementsReplace = customReplace(true), checkNull = false)(v => div(v.toString).render), - produce(_, customElementsReplace = customReplace(false), checkNull = false)(v => div(v.toString).render) + produce(_, customElementsReplace = customReplace(false), checkNull = false)(v => div(v.toString).render), ) oldCounter = 0 testCustomReplacement( - produceWithNested(_, customElementsReplace = customReplace(true), checkNull = false)((v, _) => div(v.toString).render), - produceWithNested(_, customElementsReplace = customReplace(false), checkNull = false)((v, _) => div(v.toString).render) + produceWithNested(_, customElementsReplace = customReplace(true), checkNull = false)((v, _) => + div(v.toString).render + ), + produceWithNested(_, customElementsReplace = customReplace(false), checkNull = false)((v, _) => + div(v.toString).render + ), ) } @@ -837,10 +836,14 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin "handle SeqFrag update" in { val p = SeqProperty.blank[String] - val template = div(produce(p)(s => Seq( - "test".render, - s.render - ))).render + val template = div( + produce(p)(s => + Seq( + "test".render, + s.render, + ) + ) + ).render template.outerHTML shouldBe "
test
" p.set(Seq("A", "B", "C")) @@ -849,9 +852,13 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin "handle nested SeqFrag update" in { val p = SeqProperty.blank[String] - val template = div(produce(p)(s => Seq( - Seq(s.render).render - ).render)).render + val template = div( + produce(p)(s => + Seq( + Seq(s.render).render + ).render + ) + ).render template.outerHTML shouldBe "
" p.set(Seq("A", "B", "C")) @@ -876,12 +883,14 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin val template = div( span(), produce(p)((s: Seq[Int]) => - div(s.map(v => - if (v % 2 == 0) b(v.toString).render - else i(v.toString).render - )).render + div( + s.map(v => + if (v % 2 == 0) b(v.toString).render + else i(v.toString).render + ) + ).render ), - span() + span(), ).render template.childNodes(0).textContent should be("") @@ -918,10 +927,12 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin val p = seq.SeqProperty[Int](1, 2, 3) val template = div( produce(p)((s: Seq[Int]) => - div(s.map(v => - if (v % 2 == 0) b(v.toString).render - else i(v.toString).render - )).render + div( + s.map(v => + if (v % 2 == 0) b(v.toString).render + else i(v.toString).render + ) + ).render ) ).render @@ -967,19 +978,23 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin val template = div( "A", produce(p)((s: Seq[Int]) => - div(s.map(v => - if (v % 2 == 0) b(v.toString).render - else i(v.toString).render - )).render + div( + s.map(v => + if (v % 2 == 0) b(v.toString).render + else i(v.toString).render + ) + ).render ), span("B"), produce(p2)((s: Seq[Int]) => - div(s.map(v => - if (v % 2 == 0) b(v.toString).render - else i(v.toString).render - )).render + div( + s.map(v => + if (v % 2 == 0) b(v.toString).render + else i(v.toString).render + ) + ).render ), - div("C") + div("C"), ).render template.textContent should be("A123B321C") @@ -1027,7 +1042,7 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin "stop updates after `kill` call" in { val p = SeqProperty[Int](1, 2, 3, 4, 5) - val binding = produce(p) { v => span(v.mkString(",")).render } + val binding = produce(p)(v => span(v.mkString(",")).render) val template = div(binding).render template.textContent should be("1,2,3,4,5") @@ -1051,10 +1066,13 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin val template = div( produceWithNested(p) { case (v, nested) => externalCounter += 1 - div(v.mkString(","), nested(produce(p2) { v2 => - internalCounter += 1 - span(v2.mkString("|")).render - })).render + div( + v.mkString(","), + nested(produce(p2) { v2 => + internalCounter += 1 + span(v2.mkString("|")).render + }), + ).render } ).render @@ -1083,18 +1101,18 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin def testCustomReplacement( produceAndReplace: ReadableSeqProperty[String] => Binding, - produceManually: ReadableSeqProperty[String] => Binding + produceManually: ReadableSeqProperty[String] => Binding, ) = { val p = SeqProperty[String]("a", "b", "c") val template = div( span(), produceAndReplace(p), - span() + span(), ).render val template2 = div( span(), produceManually(p), - span() + span(), ).render template.textContent should be("a,b,c") @@ -1119,14 +1137,14 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin testCustomReplacement( produce(_, customElementsReplace = customReplace(true))(v => div(v.mkString(",")).render), - produce(_, customElementsReplace = customReplace(false))(v => div(v.mkString(",")).render) + produce(_, customElementsReplace = customReplace(false))(v => div(v.mkString(",")).render), ) oldCounter = 0 testCustomReplacement( produceWithNested(_, customElementsReplace = customReplace(true))((v, _) => div(v.mkString(",")).render), - produceWithNested(_, customElementsReplace = customReplace(false))((v, _) => div(v.mkString(",")).render) + produceWithNested(_, customElementsReplace = customReplace(false))((v, _) => div(v.mkString(",")).render), ) } } @@ -1136,12 +1154,12 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin val p = seq.SeqProperty[Int](1, 2, 3) val template = div( span(), - repeat(p)((p: Property[Int]) => { + repeat(p) { (p: Property[Int]) => val v = p.get if (v % 2 == 0) b(v.toString).render else i(v.toString).render - }), - span() + }, + span(), ).render template.childNodes.length should be(3 + 2) @@ -1257,16 +1275,16 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin val p = seq.SeqProperty.blank[Int] val template = div( span(), - repeat(p)((p: Property[Int]) => { + repeat(p) { (p: Property[Int]) => val v = p.get if (v % 2 == 0) b(v.toString).render else i(v.toString).render - }), - span() + }, + span(), ).render template.childNodes.length should be(3) // spans + placeholder - p.set(Seq(1,2,3)) + p.set(Seq(1, 2, 3)) template.childNodes.length should be(3 + 2) template.childNodes(0).textContent should be("") @@ -1300,7 +1318,7 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin val template = div( repeat(p1)(builder), repeat(p2)(builder), - repeat(p3)(builder) + repeat(p3)(builder), ).render def expectedChildrenCount: Int = @@ -1361,11 +1379,11 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin "handle null value providing empty text placeholder" in { val p = seq.SeqProperty[Int](1, 2, 3) val template = div( - repeat(p)((p: Property[Int]) => { + repeat(p) { (p: Property[Int]) => val v = p.get if (v % 2 == 0) b(v.toString).render else i(v.toString).render - }) + } ).render template.childNodes.length should be(3) @@ -1448,13 +1466,12 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin } "not swap position" in { - def rep(p: seq.SeqProperty[Int, Property[Int]]) = { - repeat(p)((p: Property[Int]) => { + def rep(p: seq.SeqProperty[Int, Property[Int]]) = + repeat(p) { (p: Property[Int]) => val v = p.get if (v % 2 == 0) b(v.toString).render else i(v.toString).render - }) - } + } val p = seq.SeqProperty[Int](1, 2, 3) val p2 = seq.SeqProperty[Int](3, 2, 1) @@ -1463,14 +1480,13 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin rep(p), span("B"), rep(p2), - div("C") + div("C"), ).render val template2 = div( rep(p), - rep(p2) + rep(p2), ).render - template.textContent should be("A123B321C") template2.textContent should be("123321") @@ -1512,13 +1528,12 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin } "not swap position with CallbackSequencer" in { - def rep(p: seq.SeqProperty[Int, Property[Int]]) = { - repeat(p)((p: Property[Int]) => { + def rep(p: seq.SeqProperty[Int, Property[Int]]) = + repeat(p) { (p: Property[Int]) => val v = p.get if (v % 2 == 0) b(v.toString).render else i(v.toString).render - }) - } + } val p = seq.SeqProperty[Int](1, 2, 3) val p2 = seq.SeqProperty[Int](3, 2, 1) @@ -1527,14 +1542,13 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin rep(p), span("B"), rep(p2), - div("C") + div("C"), ).render val template2 = div( rep(p), - rep(p2) + rep(p2), ).render - template.textContent should be("A123B321C") template2.textContent should be("123321") @@ -1560,9 +1574,11 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin val numbers = SeqProperty(1, 2, 3, 4, 5) val dom = div( - produce(filter)(f => ul( - repeat(numbers.filter(f.matcher))(i => li(i.get).render) - ).render) + produce(filter)(f => + ul( + repeat(numbers.filter(f.matcher))(i => li(i.get).render) + ).render + ) ).render dom.textContent should be("135") @@ -1598,9 +1614,14 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin val evens = ints.filter(_ % 2 == 0) val dom = div( - "Doubles: ", repeat(doubles)(p => span(p.get, ", ").render), ";", - "Ints: ", repeat(ints)(p => span(p.get, ", ").render), ";", - "Evens: ", repeat(evens)(p => span(p.get, ", ").render) + "Doubles: ", + repeat(doubles)(p => span(p.get, ", ").render), + ";", + "Ints: ", + repeat(ints)(p => span(p.get, ", ").render), + ";", + "Evens: ", + repeat(evens)(p => span(p.get, ", ").render), ).render dom.textContent should be("Doubles: 1.5, 2.3, 3.7, ;Ints: 1, 2, 3, ;Evens: 2, ") @@ -1636,9 +1657,11 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin val numbers = SeqProperty(1, 2, 3, 4, 5) val dom = div( - produce(filter)(f => ul( - repeat(numbers.filter(f.matcher))(i => li(i.get).render) - ).render) + produce(filter)(f => + ul( + repeat(numbers.filter(f.matcher))(i => li(i.get).render) + ).render + ) ).render dom.textContent should be("135") @@ -1675,8 +1698,7 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin case object ActiveTodosFilter extends TodosFilter(todo => !todo.completed) case object CompletedTodosFilter extends TodosFilter(todo => todo.completed) - case class Todo(override val name: String, - override val completed: Boolean) extends TodoElement + case class Todo(override val name: String, override val completed: Boolean) extends TodoElement val filter = Property[TodosFilter](AllTodosFilter) val todos = SeqProperty.blank[TodoElement] @@ -1685,13 +1707,15 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin val patches = MArrayBuffer.empty[Patch[_]] done.listenStructure(p => patches += p) - todos.set(Seq( - Todo("A", completed = false), - Todo("B", completed = false), - Todo("C", completed = false), - Todo("D", completed = false), - Todo("E", completed = false), - )) + todos.set( + Seq( + Todo("A", completed = false), + Todo("B", completed = false), + Todo("C", completed = false), + Todo("D", completed = false), + Todo("E", completed = false), + ) + ) patches.size should be(0) patches.clear() @@ -1699,16 +1723,16 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin val repeats = MArrayBuffer[AtomicInteger]() val dom = div( - produce(filter)(f => { + produce(filter) { f => val i = new AtomicInteger(0) repeats.append(i) ul( - repeat(todos.filter(f.matcher))(todo => { + repeat(todos.filter(f.matcher)) { todo => i.incrementAndGet() li(todo.get.name).render - }) + } ).render - }) + } ).render dom.textContent should be("ABCDE") @@ -1865,13 +1889,17 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin val p = SeqProperty[String]("a", "b", "c") val template = div( span(), - repeat(p, customElementsReplace = customReplace(true), customElementsInsert = customInsert(true))(v => div(v.get).render), - span() + repeat(p, customElementsReplace = customReplace(true), customElementsInsert = customInsert(true))(v => + div(v.get).render + ), + span(), ).render val template2 = div( span(), - repeat(p, customElementsReplace = customReplace(false), customElementsInsert = customInsert(false))(v => div(v.get).render), - span() + repeat(p, customElementsReplace = customReplace(false), customElementsInsert = customInsert(false))(v => + div(v.get).render + ), + span(), ).render template.textContent should be("abc") @@ -1895,7 +1923,7 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin "handle nested document frag" in { val sp = SeqProperty(1, 2, 3) val template = div( - repeat(sp)(p => Seq(p.get.toString).render), + repeat(sp)(p => Seq(p.get.toString).render) ).render template.textContent should be("123") @@ -1964,12 +1992,16 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin val el = div( repeatWithIndex(p)((item, idx, nested) => - span(nested(bind( - item.combine(idx) { (itemValue, indexValue) => - counter += 1 - s"$indexValue$itemValue" - } - ))).render + span( + nested( + bind( + item.combine(idx) { (itemValue, indexValue) => + counter += 1 + s"$indexValue$itemValue" + } + ) + ) + ).render ) ).render @@ -2156,11 +2188,11 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin fired.result() should contain theSameElementsInOrderAs Seq( (1, 2), - (3, 4) + (3, 4), ) } - //todo https://github.com/UdashFramework/udash-core/issues/290 + // todo https://github.com/UdashFramework/udash-core/issues/290 "avoid multiple updates in produceWithNested on transformed properties" ignore { val p = Property(2) val s = Property(3) @@ -2192,7 +2224,7 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin (3, 3), (21, 30), (21, 300), - (201, 300) + (201, 300), ) } @@ -2228,7 +2260,7 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin (3, 3), (21, 30), (21, 300), - (201, 300) + (201, 300), ) } @@ -2262,7 +2294,7 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin (1, 3), (2, 4), (2, 5), - (2, 6) + (2, 6), ) } @@ -2299,7 +2331,7 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin (1, 3), (2, 4), (2, 5), - (2, 6) + (2, 6), ) } @@ -2347,7 +2379,7 @@ class TagsBindingTest extends UdashFrontendTest with Bindings { bindings: Bindin (5, 6), (6, 4), (6, 5), - (6, 6) + (6, 6), ) } } diff --git a/core/.js/src/test/scala/io/udash/bindings/inputs/CheckButtonsTest.scala b/core/.js/src/test/scala/io/udash/bindings/inputs/CheckButtonsTest.scala index 623a54a34..d8559dafb 100644 --- a/core/.js/src/test/scala/io/udash/bindings/inputs/CheckButtonsTest.scala +++ b/core/.js/src/test/scala/io/udash/bindings/inputs/CheckButtonsTest.scala @@ -11,8 +11,8 @@ class CheckButtonsTest extends UdashFrontendTest { "CheckButtons" should { def checkSelected(select: Element, selected: Seq[Boolean])(implicit pos: Position): Unit = { - selected.zipWithIndex.foreach { - case (value, idx) => select.childNodes(idx).asInstanceOf[JSInput].checked should be(value) + selected.zipWithIndex.foreach { case (value, idx) => + select.childNodes(idx).asInstanceOf[JSInput].checked should be(value) } } diff --git a/core/.js/src/test/scala/io/udash/bindings/inputs/InputTest.scala b/core/.js/src/test/scala/io/udash/bindings/inputs/InputTest.scala index be9b39212..5735954b3 100644 --- a/core/.js/src/test/scala/io/udash/bindings/inputs/InputTest.scala +++ b/core/.js/src/test/scala/io/udash/bindings/inputs/InputTest.scala @@ -3,7 +3,7 @@ package io.udash.bindings.inputs import io.udash._ import io.udash.testing.AsyncUdashFrontendTest import org.scalactic.source.Position -import org.scalajs.dom.{ClipboardEvent, Event, KeyboardEvent, html} +import org.scalajs.dom.{html, ClipboardEvent, Event, KeyboardEvent} import org.scalatest.Assertion import org.scalatest.exceptions.TestFailedException import org.scalatest.time.{Millis, Span} @@ -26,15 +26,17 @@ class InputTest extends AsyncUdashFrontendTest { "should ignore Attributes: `tpe`, `value`, `onkeyup`, `onchange`, `onpaste`, `oninput` from inputModifiers" in { val p = Property[String]("") - val input = TextInput(p, 0 millis)(inputModifiers = Seq[Modifier]( - tpe :+= null, - scalatags.JsDom.all.value :+= null, - onkeyup :+= { (_: Event) => throw new TestFailedException(Option.empty, Option.empty, 0) }, - onchange :+= { (_: Event) => throw new TestFailedException(Option.empty, Option.empty, 0) }, - onpaste :+= { (_: Event) => throw new TestFailedException(Option.empty, Option.empty, 0) }, - oninput :+= { (_: Event) => throw new TestFailedException(Option.empty, Option.empty, 0) }, - attr("sth") := "sth2" - )) + val input = TextInput(p, 0.millis)(inputModifiers = + Seq[Modifier]( + tpe :+= null, + scalatags.JsDom.all.value :+= null, + onkeyup :+= { (_: Event) => throw new TestFailedException(Option.empty, Option.empty, 0) }, + onchange :+= { (_: Event) => throw new TestFailedException(Option.empty, Option.empty, 0) }, + onpaste :+= { (_: Event) => throw new TestFailedException(Option.empty, Option.empty, 0) }, + oninput :+= { (_: Event) => throw new TestFailedException(Option.empty, Option.empty, 0) }, + attr("sth") := "sth2", + ) + ) val inputEl = input.render inputEl.value = "ABCD" @@ -58,7 +60,7 @@ class InputTest extends AsyncUdashFrontendTest { "update state on KeyUp, Change, Paste and Input events" in { val p = Property[String]("") - val input = TextInput(p, 0 millis)() + val input = TextInput(p, 0.millis)() val inputEl = input.render inputEl.value = "ABCD" @@ -80,7 +82,7 @@ class InputTest extends AsyncUdashFrontendTest { "synchronise state with property changes" in { val p = Property[String]("ABC") - val input = TextInput(p, 0 millis)() + val input = TextInput(p, 0.millis)() val inputEl = input.render inputEl.value should be("ABC") @@ -110,7 +112,7 @@ class InputTest extends AsyncUdashFrontendTest { "synchronise property with state changes" in { val p = Property[String]("ABC") - val input = TextInput(p, 0 millis)() + val input = TextInput(p, 0.millis)() val inputEl = input.render inputEl.changeValue("ABCD") @@ -135,7 +137,7 @@ class InputTest extends AsyncUdashFrontendTest { "synchronise property with state changes with debouncing" in { val p = Property[String]("ABC") - val input = TextInput(p, 0 millis)() + val input = TextInput(p, 0.millis)() val inputEl = input.render inputEl.changeValue("ABCD") @@ -143,7 +145,6 @@ class InputTest extends AsyncUdashFrontendTest { inputEl.changeValue("54321") inputEl.changeValue("ABCD") - retrying { p.get should be("ABCD") } flatMap { _ => @@ -175,8 +176,8 @@ class InputTest extends AsyncUdashFrontendTest { "synchronise with two inputs bound to a single property" in { val p = Property[String]("ABC") - val input = TextInput(p, 0 millis)() - val input2 = TextInput(p, 0 millis)() + val input = TextInput(p, 0.millis)() + val input2 = TextInput(p, 0.millis)() val r = input.render val r2 = input2.render @@ -217,7 +218,7 @@ class InputTest extends AsyncUdashFrontendTest { "run callback on state changes" in { val p = Property[String]("ABC") var result = "" - val input = TextInput(p, 0 millis, result = _)() + val input = TextInput(p, 0.millis, result = _)() val inputEl = input.render inputEl.changeValue("ABCD") @@ -243,7 +244,7 @@ class InputTest extends AsyncUdashFrontendTest { "run callback on state changes with debouncing" in { val p = Property[String]("ABC") var result = "" - val input = TextInput(p, 20 millis, result = _)() + val input = TextInput(p, 20.millis, result = _)() val inputEl = input.render inputEl.changeValue("ABCD") @@ -276,7 +277,7 @@ class InputTest extends AsyncUdashFrontendTest { "not run callback when value is the same with debouncing" in { val p = Property[String]("ABC") var callbackValues = Seq.empty[String] - val input = TextInput(p, 1 seconds, newValue => callbackValues :+= newValue)() + val input = TextInput(p, 1.seconds, newValue => callbackValues :+= newValue)() val inputEl = input.render callbackValues should be(empty) @@ -289,9 +290,13 @@ class InputTest extends AsyncUdashFrontendTest { )(PatienceConfig(scaled(Span(2000, Millis)), scaled(Span(500, Millis))), Position.here).transform { case Failure(_: RetryingTimeout | _: TestFailedException) => Success(succeed) case fail: Failure[Assertion] => fail - case Success(_) => Failure(fail(s"Callback shouldn't be executed with this debounce setup but it was executed ${callbackValues.length}" + - s" times with following values: ${callbackValues.mkString(",")}" - )) + case Success(_) => + Failure( + fail( + s"Callback shouldn't be executed with this debounce setup but it was executed ${callbackValues.length}" + + s" times with following values: ${callbackValues.mkString(",")}" + ) + ) } } diff --git a/core/.js/src/test/scala/io/udash/bindings/inputs/RadioButtonsTest.scala b/core/.js/src/test/scala/io/udash/bindings/inputs/RadioButtonsTest.scala index 14057cf0b..71c74d14b 100644 --- a/core/.js/src/test/scala/io/udash/bindings/inputs/RadioButtonsTest.scala +++ b/core/.js/src/test/scala/io/udash/bindings/inputs/RadioButtonsTest.scala @@ -10,8 +10,8 @@ class RadioButtonsTest extends UdashFrontendTest { "RadioButtons" should { def checkSelected(select: Element, selected: Seq[Boolean])(implicit pos: Position): Unit = { - selected.zipWithIndex.foreach { - case (value, idx) => select.childNodes(idx).asInstanceOf[JSInput].checked should be(value) + selected.zipWithIndex.foreach { case (value, idx) => + select.childNodes(idx).asInstanceOf[JSInput].checked should be(value) } } diff --git a/core/.js/src/test/scala/io/udash/bindings/inputs/SelectTest.scala b/core/.js/src/test/scala/io/udash/bindings/inputs/SelectTest.scala index c0e8788d6..0cfd93dc5 100644 --- a/core/.js/src/test/scala/io/udash/bindings/inputs/SelectTest.scala +++ b/core/.js/src/test/scala/io/udash/bindings/inputs/SelectTest.scala @@ -130,12 +130,13 @@ class SelectTest extends UdashFrontendTest { val options = Seq(Some("A"), None, Some("B")) val p = Property[Option[Option[String]]](Some(None)) - val select = Select.optional(p, options.toSeqProperty,StringFrag("empty"))(x => StringFrag(x.getOrElse(""))).render + val select = + Select.optional(p, options.toSeqProperty, StringFrag("empty"))(x => StringFrag(x.getOrElse(""))).render select.childElementCount should be(4) // empty value should be included select.value should be("1") - def trimHtml(s:String):String = s.split("\n").map(_.trim).mkString + def trimHtml(s: String): String = s.split("\n").map(_.trim).mkString val expectedHtml = """