├── .gitignore ├── LICENSE ├── README.md ├── core └── src │ └── main │ └── scala │ └── com │ └── acework │ └── js │ ├── components │ └── bootstrap │ │ ├── Accordion.scala │ │ ├── Affix.scala │ │ ├── Alert.scala │ │ ├── Badge.scala │ │ ├── BootstrapComponent.scala │ │ ├── BootstrapJQuery.scala │ │ ├── BsProps.scala │ │ ├── Button.scala │ │ ├── ButtonGroup.scala │ │ ├── ButtonToolbar.scala │ │ ├── Carousel.scala │ │ ├── CarouselItem.scala │ │ ├── Classes.scala │ │ ├── Col.scala │ │ ├── CollapsableMixin.scala │ │ ├── CollapsableNav.scala │ │ ├── Directions.scala │ │ ├── DropdownButton.scala │ │ ├── DropdownMenu.scala │ │ ├── DropdownStateMixin.scala │ │ ├── FadeMixin.scala │ │ ├── Glyphicon.scala │ │ ├── Grid.scala │ │ ├── Icon.scala │ │ ├── Input.scala │ │ ├── Interpolate.scala │ │ ├── JQuery.scala │ │ ├── JsNumberOrString.scala │ │ ├── Jumbotron.scala │ │ ├── Label.scala │ │ ├── ListGroup.scala │ │ ├── ListGroupItem.scala │ │ ├── MenuItem.scala │ │ ├── MergeableProps.scala │ │ ├── Modal.scala │ │ ├── ModalTrigger.scala │ │ ├── Nav.scala │ │ ├── NavBar.scala │ │ ├── NavItem.scala │ │ ├── OverlayMixin.scala │ │ ├── OverlayTrigger.scala │ │ ├── PageHeader.scala │ │ ├── PageItem.scala │ │ ├── Pager.scala │ │ ├── Panel.scala │ │ ├── PanelGroup.scala │ │ ├── Placements.scala │ │ ├── Popover.scala │ │ ├── ProgressBar.scala │ │ ├── Row.scala │ │ ├── Sizes.scala │ │ ├── SplitButton.scala │ │ ├── Styles.scala │ │ ├── SubNav.scala │ │ ├── TabPane.scala │ │ ├── TabbedArea.scala │ │ ├── Table.scala │ │ ├── Tooltip.scala │ │ ├── Utils.scala │ │ ├── Well.scala │ │ └── package.scala │ ├── logger │ ├── Log4JavaScript.scala │ ├── LoggerFactory.scala │ └── package.scala │ └── utils │ ├── EventListener.scala │ └── TransitionEvent.scala ├── demo ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── index.html ├── res │ ├── app.css │ ├── bootstrap-docs.css │ ├── bootstrap.css │ ├── bootstrap.min.css │ ├── carousel.png │ ├── default.css │ ├── docs.css │ ├── highlight.pack.js │ └── logo.png └── src │ └── main │ └── scala │ └── demo │ ├── ReactApp.scala │ ├── examples │ ├── AlertAutoDismissable.scala │ ├── AlertDismissable.scala │ ├── ContainedModal.scala │ ├── ExamplePageFooter.scala │ ├── ExamplePageHeader.scala │ └── util │ │ └── CodeContent.scala │ └── pages │ ├── Alerts.scala │ ├── Badges.scala │ ├── ButtonGroups.scala │ ├── Buttons.scala │ ├── Carousels.scala │ ├── Components.scala │ ├── DropdownButtons.scala │ ├── Glyphicons.scala │ ├── Grids.scala │ ├── Inputs.scala │ ├── Jumbotrons.scala │ ├── Labels.scala │ ├── ListGroups.scala │ ├── Modals.scala │ ├── Navbars.scala │ ├── Navs.scala │ ├── PageHeaders.scala │ ├── Pagers.scala │ ├── Panels.scala │ ├── Popovers.scala │ ├── Progressbars.scala │ ├── Section.scala │ ├── SubSection.scala │ ├── Tables.scala │ ├── Tabs.scala │ ├── Tooltips.scala │ └── Wells.scala ├── macro ├── build.sbt └── src │ └── main │ └── scala │ └── com │ └── acework │ └── js │ └── utils │ ├── Mappable.scala │ └── Mergeable.scala ├── project ├── ScalajsReactBootstrap.scala └── plugins.sbt └── test └── src └── test └── scala └── test ├── CoreTest.scala └── TestUtil.scala /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | core/target 3 | demo/target 4 | project/project 5 | project/target 6 | target 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 wei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scala-react-bootstrap 2 | Bootstrap components built with scalajs-react 3 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/Accordion.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import japgolly.scalajs.react._ 4 | 5 | /** 6 | * Created by weiyin on 10/03/15. 7 | */ 8 | object Accordion { 9 | 10 | val Accordion = ReactComponentB[Unit]("Accordion") 11 | .stateless 12 | .render { (P, C, _) => 13 | // TODO spread props 14 | PanelGroup(PanelGroup.PanelGroup(accordion = true), C) 15 | }.buildU 16 | 17 | def apply(children: ReactNode*) = Accordion(children) 18 | } 19 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/Affix.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.utils.EventListener 4 | import japgolly.scalajs.react._ 5 | import japgolly.scalajs.react.vdom.prefix_<^._ 6 | import org.scalajs.dom.raw.HTMLElement 7 | import org.scalajs.dom.{Event, document, window} 8 | 9 | import scala.scalajs.js 10 | import scala.scalajs.js.{UndefOr, undefined} 11 | import Utils._ 12 | 13 | /** 14 | * Created by weiyin on 10/03/15. 15 | */ 16 | object Affix { 17 | 18 | case class State(affixClass: String = "affix-top", affixPositionTop: Double = 0) 19 | 20 | case class Affix(offset: UndefOr[Double] = undefined, 21 | offsetTop: UndefOr[Double] = undefined, 22 | offsetBottom: UndefOr[Double] = undefined, 23 | role: UndefOr[String] = undefined, 24 | affixed: UndefOr[String] = undefined, 25 | addClasses: String = "") { 26 | def apply(children: ReactNode*) = component(this, children) 27 | 28 | def apply() = component(this) 29 | } 30 | 31 | val affixRegexp = "(affix-top|affix-bottom|affix)".r 32 | 33 | class Backend(val scope: BackendScope[Affix, State]) { 34 | var _onWindowScrollListener: UndefOr[EventListener] = undefined 35 | var _onDocumentClickListener: UndefOr[EventListener] = undefined 36 | 37 | 38 | var pinnedOffset: UndefOr[Double] = undefined 39 | var unpin: UndefOr[Double] = undefined 40 | var affixed: UndefOr[String] = undefined 41 | 42 | 43 | def checkPositionWithEventLoop() = { 44 | js.timers.setTimeout(0)(checkPosition()) 45 | } 46 | 47 | def getPinnedOffset(node: TopNode) = { 48 | if (pinnedOffset.isDefined) 49 | pinnedOffset.get 50 | else { 51 | var className = affixRegexp.replaceAllIn(node.className, "") 52 | className = if (className.length > 0) className + " affix" else className + "affix" 53 | node.className = className 54 | 55 | pinnedOffset = getOffset(node).top - window.pageYOffset 56 | pinnedOffset.get 57 | } 58 | } 59 | 60 | def checkPosition() = { 61 | if (scope.isMounted()) { 62 | val domNode = scope.getDOMNode() 63 | // documentElement has no offsetHeight 64 | val scrollHeight = document.body.offsetHeight 65 | val scrollTop = window.pageYOffset 66 | var position = getOffset(domNode) 67 | 68 | if (scope.props.affixed.getOrElse("NA") == "top") 69 | position = position.copy(top = position.top + scrollTop) 70 | 71 | var offsetTop = if (scope.props.offsetTop.isDefined) 72 | scope.props.offsetTop 73 | else 74 | scope.props.offset 75 | 76 | var offsetBottom = if (scope.props.offsetBottom.isDefined) 77 | scope.props.offsetBottom 78 | else 79 | scope.props.offset 80 | 81 | if (offsetTop.isDefined || offsetBottom.isDefined) { 82 | if (!offsetTop.isDefined) 83 | offsetTop = 0.0 84 | if (!offsetBottom.isDefined) 85 | offsetBottom = 0.0 86 | } 87 | 88 | var affix: UndefOr[String] = undefined 89 | 90 | if (this.unpin.isDefined && (scrollTop + unpin.get < position.top)) { 91 | affix = undefined 92 | } else if (offsetBottom.isDefined && (position.top + domNode.offsetHeight >= scrollHeight - offsetBottom.get)) { 93 | affix = "bottom" 94 | } else if (offsetTop.isDefined && (scrollTop <= offsetTop.get)) { 95 | affix = "top" 96 | } 97 | 98 | if (affixed.getOrElse("NA1") != affix.getOrElse("NA2")) { 99 | if (unpin.isDefined) 100 | domNode.style.top = "" 101 | 102 | val affixType = if (affix.isDefined) s"affix-${affix.get}" else "affix" 103 | affixed = affix 104 | 105 | if (affix.getOrElse("NA") == "bottom") 106 | unpin = getPinnedOffset(domNode) 107 | else 108 | unpin = undefined 109 | 110 | var affixPositionTop = 0.0 111 | if (affix.getOrElse("NA") == "bottom") { 112 | domNode.className = affixRegexp.replaceAllIn(domNode.className, "affix-bottom") 113 | affixPositionTop = scrollHeight - offsetBottom.get - domNode.offsetHeight - getOffset(domNode).top 114 | } 115 | 116 | scope.modState(_.copy(affixClass = affixType, affixPositionTop = affixPositionTop)) 117 | } 118 | } 119 | } 120 | 121 | def onComponentDidMount() = { 122 | _onWindowScrollListener = EventListener.listen(window, "scroll", (_: Event) => checkPosition()) 123 | _onDocumentClickListener = EventListener.listen(document, "click", (_: Event) => checkPositionWithEventLoop()) 124 | } 125 | 126 | def onComponentWillUnmount() = { 127 | if (_onDocumentClickListener.isDefined) { 128 | _onDocumentClickListener.get.remove() 129 | _onDocumentClickListener = undefined 130 | } 131 | 132 | if (_onWindowScrollListener.isDefined) { 133 | _onWindowScrollListener.get.remove() 134 | _onWindowScrollListener = undefined 135 | } 136 | } 137 | 138 | def onComponentDidUpdate(prevProps: Affix, prevState: State) = { 139 | if (prevState.affixClass == scope.state.affixClass) 140 | checkPositionWithEventLoop() 141 | } 142 | } 143 | 144 | val component = ReactComponentB[Affix]("Affix") 145 | .initialState(State()) 146 | .backend(new Backend(_)) 147 | .render((P, C, S, B) => { 148 | <.div(^.classSet1(P.addClasses, S.affixClass -> true), ^.top := S.affixPositionTop, 149 | C 150 | ) 151 | 152 | }) 153 | .componentDidMount(scope => scope.backend.onComponentDidMount()) 154 | .componentDidUpdate((scope, prevProps, prevState) => scope.backend.onComponentDidUpdate(prevProps, prevState)) 155 | .componentWillUnmount(scope => scope.backend.onComponentWillUnmount()) 156 | .build 157 | 158 | def apply(props: Affix, children: ReactNode*) = component(props, children) 159 | 160 | } 161 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/Alert.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.utils.{Mappable, Mergeable} 4 | import japgolly.scalajs.react._ 5 | import japgolly.scalajs.react.vdom.prefix_<^._ 6 | 7 | import scala.scalajs.js 8 | import scala.scalajs.js.{UndefOr, undefined} 9 | 10 | /** 11 | * Created by weiyin on 10/03/15. 12 | */ 13 | object Alert extends BootstrapComponent { 14 | override type P = Alert 15 | override type S = Unit 16 | override type B = Backend 17 | override type N = TopNode 18 | 19 | override def defaultProps = Alert() 20 | 21 | case class Alert(dismissAfter: UndefOr[Int] = undefined, 22 | onDismiss: UndefOr[() => Unit] = undefined, 23 | bsClass: UndefOr[Classes.Value] = Classes.alert, 24 | bsStyle: UndefOr[Styles.Value] = Styles.info, 25 | bsSize: UndefOr[Sizes.Value] = undefined, 26 | addClasses: String = "") extends BsProps with MergeableProps[Alert] { 27 | 28 | def merge(t: Map[String, Any]): Alert = implicitly[Mergeable[Alert]].merge(this, t) 29 | 30 | def asMap: Map[String, Any] = implicitly[Mappable[Alert]].toMap(this) 31 | 32 | def apply(children: ReactNode*) = component(this, children) 33 | 34 | def apply() = component(this) 35 | } 36 | 37 | class Backend($: BackendScope[Alert, Unit]) { 38 | var dismissTimer: js.UndefOr[js.timers.SetTimeoutHandle] = 39 | js.undefined 40 | 41 | def clearTimer() = { 42 | if (dismissTimer.isDefined) { 43 | js.timers.clearTimeout(dismissTimer.get) 44 | dismissTimer = undefined 45 | } 46 | } 47 | 48 | def start(dismissAfter: Int, onDismiss: () => Unit) = 49 | dismissTimer = js.timers.setTimeout(dismissAfter)(onDismiss()) 50 | } 51 | 52 | override val component = ReactComponentB[Alert]("Alert") 53 | .stateless 54 | .backend(new Backend(_)) 55 | .render { (P, C, S, B) => 56 | var classes = P.bsClassSet 57 | val isDismissable = P.onDismiss.isDefined 58 | classes += ("alert-dismissable" -> isDismissable) 59 | 60 | def renderDismissButton() = { 61 | <.button(^.tpe := "button", ^.className := "close", 62 | ^.onClick --> P.onDismiss.get(), ^.aria.hidden := true, ^.dangerouslySetInnerHtml("×")) 63 | } 64 | 65 | // TODO spread props 66 | <.div(^.classSet1M(P.addClasses, classes))( 67 | if (isDismissable) renderDismissButton() else EmptyTag, 68 | C 69 | ) 70 | } 71 | .componentDidMount($ => 72 | if ($.props.dismissAfter.isDefined && $.props.onDismiss.isDefined) 73 | $.backend.start($.props.dismissAfter.get, $.props.onDismiss.get) 74 | ) 75 | .componentWillUnmount($ => $.backend.clearTimer()) 76 | .build 77 | 78 | } 79 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/Badge.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.components.bootstrap.Utils.ValidComponentChildren 4 | import japgolly.scalajs.react._ 5 | import japgolly.scalajs.react.vdom.prefix_<^._ 6 | 7 | import scala.scalajs.js 8 | import scala.scalajs.js._ 9 | 10 | /** 11 | * Created by weiyin on 10/03/15. 12 | */ 13 | object Badge extends BootstrapComponent { 14 | override type P = Badge 15 | override type S = Unit 16 | override type B = Unit 17 | override type N = TopNode 18 | 19 | def defaultProps = Badge() 20 | 21 | case class Badge(pullRight: UndefOr[Boolean] = undefined, addClasses: String = "") { 22 | def apply(children: ReactNode*) = component(this, children) 23 | 24 | def apply() = component(this) 25 | } 26 | 27 | override val component = ReactComponentB[Badge]("Badge") 28 | .render { (P, C) => 29 | 30 | def hasContent = { 31 | var res = ValidComponentChildren.hasValidComponents(C) 32 | if (!res) { 33 | C.forEach { c => 34 | if (js.typeOf(c) == "string" || js.typeOf(c) == "number") 35 | res = true 36 | } 37 | } 38 | res 39 | } 40 | 41 | // FIXME spread props 42 | <.span(^.className := P.addClasses, 43 | ^.classSet("pull-right" -> P.pullRight.getOrElse(false), "badge" -> hasContent))(C) 44 | 45 | }.build 46 | 47 | } 48 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/BootstrapComponent.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import japgolly.scalajs.react.ReactComponentC.ReqProps 4 | import japgolly.scalajs.react.{ReactNode, TopNode} 5 | 6 | import scala.scalajs.js 7 | 8 | /** 9 | * Created by weiyin on 18/03/15. 10 | */ 11 | trait BootstrapComponent { 12 | 13 | type P 14 | type S 15 | type B 16 | type N <: TopNode 17 | 18 | val component: ReqProps[P, S, B, N] 19 | 20 | def defaultProps: P 21 | 22 | def withKey(key: js.Any) = component.withKey(key) 23 | 24 | def withRef(ref: String) = component.withRef(ref) 25 | 26 | def apply(props: P, children: ReactNode*) = component(props, children) 27 | 28 | def apply(children: ReactNode*) = component(defaultProps, children) 29 | 30 | def apply() = component 31 | } 32 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/BootstrapJQuery.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import scala.scalajs.js 4 | 5 | /** 6 | * Created by weiyin on 11/03/15. 7 | */ 8 | trait BootstrapJQuery extends JQuery { 9 | def modal(action: String): BootstrapJQuery = js.native 10 | 11 | def modal(options: js.Any): BootstrapJQuery = js.native 12 | } 13 | 14 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/BsProps.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import scala.scalajs.js.UndefOr 4 | 5 | /** 6 | * Created by weiyin on 09/03/15. 7 | */ 8 | 9 | trait BsProps { 10 | val bsClass: UndefOr[Classes.Value] 11 | val bsStyle: UndefOr[Styles.Value] 12 | val bsSize: UndefOr[Sizes.Value] 13 | 14 | def bsClassSet: Map[String, Boolean] = { 15 | var classes = Map[String, Boolean]() 16 | 17 | if (bsClass.isDefined && Classes.values.contains(bsClass.get)) { 18 | val bsClassName = bsClass.get.toString.replaceAll("\\$minus", "-") 19 | classes += (bsClassName -> true) 20 | 21 | val prefix = bsClassName + "-" 22 | if (bsSize.isDefined && Sizes.values.contains(bsSize.get)) { 23 | val bsSizeName = bsSize.get.toString 24 | classes += (prefix + bsSizeName -> true) 25 | } 26 | if (bsStyle.isDefined && Styles.values.contains(bsStyle.get)) { 27 | val bsStyleName = bsStyle.get.toString 28 | classes += ((prefix + bsStyleName) -> true) 29 | } 30 | } 31 | classes 32 | } 33 | 34 | def prefixClass(subClass: String) = { 35 | val bsClassName = bsClass.get.toString.replaceAll("\\$minus", "-") 36 | s"$bsClassName-$subClass" 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/Button.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.utils.{Mappable, Mergeable} 4 | import japgolly.scalajs.react.ReactComponentC.ReqProps 5 | import japgolly.scalajs.react._ 6 | import japgolly.scalajs.react.vdom.prefix_<^._ 7 | 8 | import scala.scalajs.js.{UndefOr, undefined} 9 | 10 | /** 11 | * Created by weiyin on 09/03/15. 12 | */ 13 | 14 | object Button extends BootstrapComponent { 15 | override type P = Button 16 | override type S = Unit 17 | override type B = Unit 18 | override type N = TopNode 19 | 20 | override def defaultProps = Button() 21 | 22 | case class Button( 23 | /*== start react bootstraps ==*/ 24 | active: Boolean = false, 25 | disabled: Boolean = false, 26 | block: Boolean = false, 27 | navItem: Boolean = false, 28 | navDropdown: Boolean = false, 29 | componentClass: UndefOr[String] = undefined, 30 | href: UndefOr[String] = undefined, 31 | target: UndefOr[String] = undefined, 32 | `type`: String = "button", 33 | value: UndefOr[String] = undefined, 34 | /*== end react bootstraps ==*/ 35 | onClick: UndefOr[(ReactEvent) => Unit] = undefined, 36 | onMouseOver: UndefOr[(ReactEvent) => Unit] = undefined, 37 | onMouseOut: UndefOr[(ReactEvent) => Unit] = undefined, 38 | onBlur: UndefOr[(ReactEvent) => Unit] = undefined, 39 | onFocus: UndefOr[(ReactEvent) => Unit] = undefined, 40 | bsClass: UndefOr[Classes.Value] = Classes.btn, 41 | bsStyle: UndefOr[Styles.Value] = Styles.default, 42 | bsSize: UndefOr[Sizes.Value] = undefined, 43 | addClasses: String = "") extends BsProps with MergeableProps[Button] { 44 | 45 | def merge(t: Map[String, Any]): Button = implicitly[Mergeable[Button]].merge(this, t) 46 | 47 | def asMap: Map[String, Any] = implicitly[Mappable[Button]].toMap(this) 48 | 49 | def apply(children: ReactNode*) = component(this, children) 50 | 51 | def apply() = component(this) 52 | } 53 | 54 | override val component = ReactComponentB[Button]("Button") 55 | .render { (P, C) => 56 | 57 | def renderNavItem(classes: Map[String, Boolean]) = { 58 | var componentClass = <.li(^.classSet("active" -> P.active)) 59 | 60 | componentClass = spreadEventHandlers(componentClass) 61 | componentClass(renderAnchor(classes)) 62 | } 63 | 64 | def spreadEventHandlers(node: ReactTag): ReactTag = { 65 | var componentClass = node 66 | if (P.onClick.isDefined) 67 | componentClass = componentClass(^.onClick ==> P.onClick.get) 68 | 69 | if (P.onMouseOut.isDefined) 70 | componentClass = componentClass(^.onMouseOut ==> P.onMouseOut.get) 71 | 72 | if (P.onMouseOver.isDefined) 73 | componentClass = componentClass(^.onMouseOver ==> P.onMouseOver.get) 74 | 75 | if (P.onBlur.isDefined) 76 | componentClass = componentClass(^.onBlur ==> P.onBlur.get) 77 | 78 | if (P.onFocus.isDefined) 79 | componentClass = componentClass(^.onFocus ==> P.onFocus.get) 80 | 81 | componentClass 82 | } 83 | 84 | def renderAnchor(classes: Map[String, Boolean]) = { 85 | // TODO spread props 86 | var componentClass = P.componentClass.getOrElse("a").reactTag 87 | componentClass = componentClass(^.href := P.href.getOrElse("#"), 88 | ^.target := P.target, ^.tpe := P.`type`, 89 | ^.classSet1M(P.addClasses, classes + ("disabled" -> P.disabled)), 90 | ^.role := "button") 91 | 92 | componentClass = spreadEventHandlers(componentClass) 93 | componentClass(C) 94 | } 95 | 96 | def renderButton(classes: Map[String, Boolean]) = { 97 | var componentClass = P.componentClass.getOrElse("button").reactTag 98 | 99 | // TODO spread props 100 | componentClass = componentClass(^.classSet1M(P.addClasses, classes), ^.tpe := P.`type`, 101 | ^.value := P.value, ^.disabled := P.disabled) 102 | 103 | componentClass = spreadEventHandlers(componentClass) 104 | componentClass(C) 105 | } 106 | 107 | var classes = if (P.navDropdown) Map[String, Boolean]() else P.bsClassSet 108 | classes ++= Map("active" -> P.active, "btn-block" -> P.block) 109 | 110 | if (P.navItem) 111 | renderNavItem(classes) 112 | else if (P.href.isDefined || P.target.isDefined || P.navDropdown) 113 | renderAnchor(classes) 114 | else 115 | renderButton(classes) 116 | } 117 | .build 118 | 119 | } 120 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/ButtonGroup.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.utils.{Mappable, Mergeable} 4 | import japgolly.scalajs.react._ 5 | import japgolly.scalajs.react.vdom.prefix_<^._ 6 | 7 | import scala.scalajs.js.{Any => JAny, UndefOr, undefined} 8 | 9 | /** 10 | * Created by weiyin on 10/03/15. 11 | */ 12 | 13 | 14 | object ButtonGroup extends BootstrapComponent { 15 | override type P = ButtonGroup 16 | override type S = Unit 17 | override type B = Unit 18 | override type N = TopNode 19 | 20 | override def defaultProps: P = ButtonGroup() 21 | 22 | case class ButtonGroup(id: UndefOr[String] = undefined, 23 | vertical: UndefOr[Boolean] = undefined, 24 | justified: UndefOr[Boolean] = undefined, 25 | bsClass: UndefOr[Classes.Value] = Classes.`btn-group`, 26 | bsStyle: UndefOr[Styles.Value] = undefined, 27 | bsSize: UndefOr[Sizes.Value] = undefined, 28 | addClasses: String = "") extends BsProps with MergeableProps[ButtonGroup] { 29 | 30 | def merge(t: Map[String, Any]): ButtonGroup = implicitly[Mergeable[ButtonGroup]].merge(this, t) 31 | 32 | def asMap: Map[String, Any] = implicitly[Mappable[ButtonGroup]].toMap(this) 33 | 34 | def apply(children: ReactNode*) = component(this, children) 35 | 36 | def apply() = component(this) 37 | } 38 | 39 | override val component = ReactComponentB[ButtonGroup]("ButtonGroup") 40 | .render { (P, C) => 41 | val classes = P.bsClassSet ++ Map( 42 | "btn-group" -> !P.vertical.getOrElse(false), 43 | "btn-group-vertical" -> P.vertical.getOrElse(false), 44 | "btn-group-justifed" -> P.justified.getOrElse(false)) 45 | 46 | // TODO spread props 47 | <.div(^.classSet1M(P.addClasses, classes))(C) 48 | }.build 49 | 50 | } 51 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/ButtonToolbar.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.utils.{Mappable, Mergeable} 4 | import japgolly.scalajs.react._ 5 | import japgolly.scalajs.react.vdom.prefix_<^._ 6 | 7 | import scala.scalajs.js.{UndefOr, undefined} 8 | 9 | /** 10 | * Created by weiyin on 10/03/15. 11 | */ 12 | object ButtonToolbar extends BootstrapComponent { 13 | override type P = ButtonToolBar 14 | override type S = Unit 15 | override type B = Unit 16 | override type N = TopNode 17 | 18 | override def defaultProps = ButtonToolBar() 19 | 20 | case class ButtonToolBar(bsClass: UndefOr[Classes.Value] = Classes.`btn-toolbar`, 21 | bsStyle: UndefOr[Styles.Value] = undefined, 22 | bsSize: UndefOr[Sizes.Value] = undefined, 23 | addClasses: String = "") extends BsProps with MergeableProps[ButtonToolBar] { 24 | 25 | def merge(t: Map[String, Any]): ButtonToolBar = implicitly[Mergeable[ButtonToolBar]].merge(this, t) 26 | 27 | def asMap: Map[String, Any] = implicitly[Mappable[ButtonToolBar]].toMap(this) 28 | 29 | def apply(children: ReactNode*) = component(this, children) 30 | 31 | def apply() = component(this) 32 | } 33 | 34 | override val component = ReactComponentB[ButtonToolBar]("ButtonToolbar") 35 | .render { (P, C) => 36 | // TODO spread props 37 | <.div(^.role := "toolbar", ^.classSet1M(P.addClasses, P.bsClassSet))(C) 38 | }.build 39 | 40 | } 41 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/CarouselItem.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.utils.{Mappable, Mergeable, TransitionEvent} 4 | import japgolly.scalajs.react._ 5 | import japgolly.scalajs.react.vdom.prefix_<^._ 6 | import org.scalajs.dom.raw.Event 7 | 8 | import scala.scalajs.js 9 | import scala.scalajs.js.{UndefOr, undefined} 10 | 11 | /** 12 | * Created by weiyin on 10/03/15. 13 | */ 14 | object CarouselItem extends BootstrapComponent { 15 | override type P = CarouselItem 16 | override type S = State 17 | override type B = Backend 18 | override type N = TopNode 19 | 20 | override def defaultProps = CarouselItem() 21 | 22 | case class CarouselItem(index: Int = 0, 23 | active: UndefOr[Boolean] = undefined, 24 | animation: UndefOr[Boolean] = true, 25 | animateIn: UndefOr[Boolean] = false, 26 | animateOut: UndefOr[Boolean] = false, 27 | direction: UndefOr[Directions.Value] = Directions.next, 28 | onAnimateOutEnd: UndefOr[(Int) => Unit] = undefined, 29 | caption: UndefOr[String] = undefined, 30 | addClasses: String = "") extends MergeableProps[CarouselItem] { 31 | 32 | def merge(t: Map[String, Any]): CarouselItem = implicitly[Mergeable[CarouselItem]].merge(this, t) 33 | 34 | def asMap: Map[String, Any] = implicitly[Mappable[CarouselItem]].toMap(this) 35 | 36 | def apply(children: ReactNode*) = component(this, children) 37 | 38 | def apply() = component(this) 39 | } 40 | 41 | case class State(direction: UndefOr[Directions.Value] = undefined) 42 | 43 | class Backend($: BackendScope[CarouselItem, State]) { 44 | 45 | def startAnimation() = { 46 | if ($.isMounted()) 47 | $.modState(s => s.copy(direction = if ($.props.direction.getOrElse(Directions.next) == Directions.prev) Directions.right else Directions.left)) 48 | } 49 | 50 | def onAnimateOutEnd(e: Any) = { 51 | if ($.isMounted() && $.props.onAnimateOutEnd.isDefined) 52 | $.props.onAnimateOutEnd.get($.props.index) 53 | } 54 | } 55 | 56 | val component = ReactComponentB[CarouselItem]("CarouselItem") 57 | .initialState(State()) 58 | .backend(new Backend(_)) 59 | .render((P, C, S, B) => { 60 | 61 | def renderCaption() = { 62 | if (P.caption.isDefined) { 63 | <.div(^.className := "carousel-caption")(P.caption.get) 64 | } 65 | else 66 | EmptyTag 67 | } 68 | 69 | var classes = Map("item" -> true, 70 | "active" -> (P.active.getOrElse(false) && !P.animateIn.getOrElse(false) || P.animateOut.getOrElse(false)), 71 | "next" -> (P.active.getOrElse(false) && P.animateIn.getOrElse(false) && P.direction.getOrElse(Directions.next) == Directions.next), 72 | "prev" -> (P.active.getOrElse(false) && P.animateIn.getOrElse(false) && P.direction.getOrElse(Directions.next) == Directions.prev) 73 | ) 74 | 75 | if (S.direction.isDefined && (P.animateIn.getOrElse(false) || P.animateOut.getOrElse(false))) 76 | classes += (S.direction.get.toString -> true) 77 | 78 | // TODO spread props 79 | <.div(^.classSet1M(P.addClasses, classes))(C, renderCaption()) 80 | }) 81 | .componentWillReceiveProps(($, newProps) => 82 | if ($.props.active.getOrElse(false) != newProps.active.getOrElse(false)) 83 | $.modState(_.copy(direction = undefined)) 84 | ) 85 | .componentDidUpdate(($, previousProps, S) => { 86 | if (!$.props.active.getOrElse(false) && previousProps.active.getOrElse(false)) { 87 | TransitionEvent.addEndEventListener($.getDOMNode(), (e: Event) => $.backend.onAnimateOutEnd(e)) 88 | } 89 | 90 | if ($.props.active.getOrElse(false) != previousProps.active.getOrElse(false)) { 91 | js.timers.setTimeout(20)($.backend.startAnimation()) 92 | } 93 | }) 94 | .build 95 | } 96 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/Classes.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | /** 4 | * Created by weiyin on 09/03/15. 5 | */ 6 | object Classes extends Enumeration { 7 | val alert, btn, `btn-group`, `btn-toolbar`, column, `input-group`, 8 | form, plyphicon, label, `list-group-item`, panel, `panel-group`, `progress-bar`, 9 | nav, navbar, modal, row, well, glyphicon = Value 10 | } 11 | 12 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/Col.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import japgolly.scalajs.react._ 4 | import japgolly.scalajs.react.vdom.prefix_<^._ 5 | 6 | import scala.scalajs.js._ 7 | 8 | /** 9 | * Created by weiyin on 10/03/15. 10 | */ 11 | object Col { 12 | 13 | case class Col(xs: UndefOr[Int] = undefined, 14 | sm: UndefOr[Int] = undefined, 15 | md: UndefOr[Int] = undefined, 16 | lg: UndefOr[Int] = undefined, 17 | xsOffset: UndefOr[Int] = undefined, 18 | smOffset: UndefOr[Int] = undefined, 19 | mdOffset: UndefOr[Int] = undefined, 20 | lgOffset: UndefOr[Int] = undefined, 21 | xsPush: UndefOr[Int] = undefined, 22 | smPush: UndefOr[Int] = undefined, 23 | mdPush: UndefOr[Int] = undefined, 24 | lgPush: UndefOr[Int] = undefined, 25 | xsPull: UndefOr[Int] = undefined, 26 | smPull: UndefOr[Int] = undefined, 27 | mdPull: UndefOr[Int] = undefined, 28 | lgPull: UndefOr[Int] = undefined, 29 | componentClass: String = "div", addClasses: String = "") { 30 | 31 | def apply(children: ReactNode*) = component(this, children) 32 | 33 | def apply() = component(this) 34 | } 35 | 36 | val component = ReactComponentB[Col]("Col") 37 | .render { (P, C) => 38 | var classes = Map[String, Boolean]() 39 | 40 | def extractClasses(sizeName: String, size: UndefOr[Int], offset: UndefOr[Int], push: UndefOr[Int], pull: UndefOr[Int]) { 41 | 42 | if (size.isDefined) 43 | classes += (s"col-$sizeName-${size.get}" -> true) 44 | 45 | if (offset.isDefined && offset.get >= 0) 46 | classes += (s"col-$sizeName-offset-${offset.get}" -> true) 47 | 48 | if (push.isDefined && push.get >= 0) 49 | classes += (s"col-$sizeName-push-${push.get}" -> true) 50 | 51 | if (pull.isDefined && pull.get >= 0) 52 | classes += (s"col-$sizeName-pull-${pull.get}" -> true) 53 | } 54 | 55 | extractClasses("xs", P.xs, P.xsOffset, P.xsPush, P.xsPull) 56 | extractClasses("sm", P.sm, P.smOffset, P.smPush, P.smPull) 57 | extractClasses("md", P.md, P.mdOffset, P.mdPush, P.mdPull) 58 | extractClasses("lg", P.lg, P.lgOffset, P.lgPush, P.lgPull) 59 | 60 | val componentClass = P.componentClass.reactTag 61 | componentClass(^.classSet1M(P.addClasses, classes))(C) 62 | }.build 63 | 64 | } 65 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/CollapsableMixin.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.utils.TransitionEvent 4 | import japgolly.scalajs.react.{BackendScope, TopNode} 5 | import org.scalajs.dom.Event 6 | 7 | import scala.scalajs.js 8 | import scala.scalajs.js.UndefOr 9 | 10 | /** 11 | * Created by weiyin on 09/03/15. 12 | */ 13 | 14 | case class CollapsableState(collapsing: Boolean, expanded: Boolean) 15 | 16 | trait CollapsableProps { 17 | val collapsable: UndefOr[Boolean] 18 | val expanded: UndefOr[Boolean] 19 | } 20 | 21 | trait CollapsableMixin[P <: CollapsableProps] { 22 | 23 | val scope: BackendScope[P, CollapsableState] 24 | var _collapseEnd = false 25 | 26 | def isExpanded = { 27 | scope.props.expanded.getOrElse(scope.state.expanded) 28 | } 29 | 30 | def getCollapsableClassSet(classNames: String) = { 31 | var classes = Map[String, Boolean]() 32 | for (className <- classNames.split(" ")) 33 | classes += (className -> true) 34 | 35 | classes += ("collapsing" -> scope.state.collapsing) 36 | classes += ("collapse" -> !scope.state.collapsing) 37 | classes += ("in" -> (isExpanded && !scope.state.collapsing)) 38 | 39 | classes 40 | } 41 | 42 | def dimension: String = "height" 43 | 44 | def getCollapsableDimensionValue: Int 45 | 46 | def onComponentWillUpdate(nextProps: P, nextState: CollapsableState) = { 47 | val willExpanded = nextProps.expanded.getOrElse(nextState.expanded) 48 | if (willExpanded != isExpanded) { 49 | // if the expanded state is being toggled, ensure node has a dimension value 50 | // this is needed for the animation to work and needs to be set before 51 | // the collapsing class is applied (after collapsing is applied the in class 52 | // is removed and the node's dimension will be wrong) 53 | val dim = dimension 54 | val value = if (!willExpanded) getCollapsableDimensionValue else 0 55 | 56 | getCollapsableDOMNode.map(_.style.setProperty(dim, s"${value}px")) 57 | 58 | _afterWillUpdate() 59 | } 60 | } 61 | 62 | def onComponentDidUpdate(prevProps: P, prevState: CollapsableState) = { 63 | // check if expanded is being toggled, if so, set collapsing 64 | _checkToggleCollapsing(prevProps, prevState) 65 | 66 | // check if collapsing was turned on, if so, start animation 67 | _checkStartAnimation() 68 | } 69 | 70 | def _afterWillUpdate() = {} 71 | 72 | def _checkStartAnimation() = { 73 | if (scope.state.collapsing) { 74 | val dim = dimension 75 | val value = if (isExpanded) getCollapsableDimensionValue else 0 76 | getCollapsableDOMNode.map(_.style.setProperty(dim, s"${value}px")) 77 | } 78 | } 79 | 80 | def _checkToggleCollapsing(nextProps: P, nextState: CollapsableState) = { 81 | val wasExpanded = nextProps.expanded.getOrElse(nextState.expanded) 82 | if (wasExpanded != isExpanded) { 83 | if (wasExpanded) 84 | _handleCollapse() 85 | else 86 | _handleExpand() 87 | } 88 | } 89 | 90 | def _handleCollapse() = { 91 | getCollapsableDOMNode.map { node => 92 | var onHandleCollapseComplete: (Event) => Unit = null 93 | onHandleCollapseComplete = (e: Event) => { 94 | _removeEndEventListener(node, onHandleCollapseComplete) 95 | scope.modState(_.copy(collapsing = false)) 96 | } 97 | _addEndEventListener(node, onHandleCollapseComplete) 98 | scope.modState(_.copy(collapsing = true)) 99 | } 100 | } 101 | 102 | def _handleExpand() = { 103 | getCollapsableDOMNode.map { node => 104 | val dim = dimension 105 | var onHandleExpandComplete: (Event) => Unit = null 106 | onHandleExpandComplete = (e: Event) => { 107 | _removeEndEventListener(node, onHandleExpandComplete) 108 | // remove dimension value - this ensure the collapsable item can grow 109 | // in dimension after initial display (such as an image loading) 110 | node.style.setProperty(dim, "") 111 | scope.modState(_.copy(collapsing = false)) 112 | } 113 | _addEndEventListener(node, onHandleExpandComplete) 114 | scope.modState(_.copy(collapsing = true)) 115 | } 116 | } 117 | 118 | def _addEndEventListener(node: TopNode, complete: (Event) => Unit) = 119 | TransitionEvent.addEndEventListener(node, complete) 120 | 121 | def _removeEndEventListener(node: TopNode, complete: (Event) => Unit) = 122 | TransitionEvent.removeEndEventListener(node, complete) 123 | 124 | def getCollapsableDOMNode: Option[TopNode] 125 | } 126 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/CollapsableNav.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.components.bootstrap.Utils._ 4 | import com.acework.js.utils.{Mappable, Mergeable} 5 | import japgolly.scalajs.react._ 6 | import japgolly.scalajs.react.vdom.prefix_<^._ 7 | import org.scalajs.dom.raw.HTMLUListElement 8 | 9 | import scala.scalajs.js.{UndefOr, undefined} 10 | 11 | 12 | /** 13 | * Created by weiyin on 09/03/15. 14 | */ 15 | 16 | object CollapsableNav extends BootstrapComponent { 17 | override type P = CollapsableNav 18 | override type S = CollapsableState 19 | override type B = Backend 20 | override type N = TopNode 21 | 22 | override def defaultProps = CollapsableNav() 23 | 24 | 25 | case class CollapsableNav(navbar: Boolean = false, stacked: Boolean = false, 26 | justified: Boolean = false, pullRight: Boolean = false, 27 | right: Boolean = false, 28 | collapsable: UndefOr[Boolean] = undefined, 29 | expanded: UndefOr[Boolean] = undefined, 30 | activeHref: UndefOr[String] = undefined, 31 | activeKey: UndefOr[String] = undefined, 32 | role: UndefOr[String] = undefined, 33 | eventKey: UndefOr[String] = undefined, 34 | id: UndefOr[String] = undefined, 35 | bsClass: UndefOr[Classes.Value] = Classes.nav, 36 | bsStyle: UndefOr[Styles.Value] = undefined, 37 | bsSize: UndefOr[Sizes.Value] = undefined, 38 | onSelect: UndefOr[Seq[UndefOr[String]] => Unit] = undefined, 39 | addClasses: String = "") extends BsProps with CollapsableProps with MergeableProps[CollapsableNav] { 40 | 41 | def merge(t: Map[String, Any]): CollapsableNav = implicitly[Mergeable[CollapsableNav]].merge(this, t) 42 | 43 | def asMap: Map[String, Any] = implicitly[Mappable[CollapsableNav]].toMap(this) 44 | 45 | def apply(children: ReactNode*) = component(this, children) 46 | 47 | def apply() = component(this) 48 | } 49 | 50 | 51 | class Backend(val scope: BackendScope[CollapsableNav, CollapsableState]) extends CollapsableMixin[CollapsableNav] { 52 | 53 | def getCollapsableDOMNode: Option[TopNode] = Some(scope.getDOMNode()) 54 | 55 | def getCollapsableDimensionValue: Int = { 56 | 0 57 | } 58 | } 59 | 60 | val ulRef = Ref[HTMLUListElement]("ul") 61 | 62 | override val component = ReactComponentB[CollapsableNav]("Nav") 63 | .initialState(CollapsableState(false, false)) 64 | .backend(new Backend(_)) 65 | .render((P, C, S, B) => { 66 | 67 | def getChildActiveProp(child: ReactNode) = { 68 | val childPropsAny = getChildProps[Any](child) 69 | childPropsAny match { 70 | case props: NavItem.NavItem => 71 | props.active || 72 | props.eventKey.getOrElse("NA1") == P.activeKey.getOrElse("NA") || 73 | props.href.getOrElse("NA1") == P.activeHref.getOrElse("NA") 74 | case _ => false 75 | } 76 | } 77 | 78 | def getChildOnSelectProp(child: ReactNode) = { 79 | val childPropsAny = getChildProps[Any](child) 80 | childPropsAny match { 81 | case props: NavItem.NavItem => 82 | props.onSelect 83 | case _ => undefined 84 | } 85 | } 86 | 87 | def renderChildren(child: ReactNode, index: Int) = { 88 | val (key, _) = getChildKeyAndRef2(child, index) 89 | cloneWithProps(child, (key, s"nocollapse_$key"), Map( 90 | "activeKey" -> P.activeKey, 91 | "activeHref" -> P.activeHref, 92 | "navItem" -> true 93 | )) 94 | } 95 | 96 | def renderCollapsableNavChildren(child: ReactNode, index: Int) = { 97 | val (key, _) = getChildKeyAndRef2(child, index) 98 | 99 | cloneWithProps(child, (key, s"collapsable_$key"), Map( 100 | "active" -> getChildActiveProp(child), 101 | "activeKey" -> P.activeKey, 102 | "activeHref" -> P.activeHref, 103 | "onSelect" -> createChainedFunction1(getChildOnSelectProp(child), P.onSelect), 104 | "navItem" -> true 105 | )) 106 | } 107 | 108 | var classes = if (P.collapsable.getOrElse(false)) B.getCollapsableClassSet("") else Map[String, Boolean]() 109 | classes += ("navbar-collapse" -> P.collapsable.getOrElse(false)) 110 | 111 | <.div(^.classSet1M(P.addClasses, classes), 112 | ValidComponentChildren.map(C, 113 | if (P.collapsable.getOrElse(false)) 114 | renderCollapsableNavChildren 115 | else 116 | renderChildren 117 | ) 118 | ) 119 | 120 | } 121 | ) 122 | .componentWillUpdate((scope, nextProps, nextState) => scope.backend.onComponentWillUpdate(nextProps, nextState)) 123 | .componentDidUpdate((scope, prevProps, prevState) => scope.backend.onComponentDidUpdate(prevProps, prevState)) 124 | .build 125 | 126 | } 127 | 128 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/Directions.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | /** 4 | * Created by weiyin on 10/03/15. 5 | */ 6 | object Directions extends Enumeration { 7 | val prev, next, left, right = Value 8 | } 9 | 10 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/DropdownButton.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.components.bootstrap.Utils._ 4 | import com.acework.js.utils.{Mappable, Mergeable} 5 | import japgolly.scalajs.react.Addons.ReactCloneWithProps 6 | import japgolly.scalajs.react.Ref 7 | 8 | import scala.scalajs.js 9 | 10 | /** 11 | * Created by weiyin on 11/03/15. 12 | */ 13 | 14 | import japgolly.scalajs.react._ 15 | import japgolly.scalajs.react.vdom.prefix_<^._ 16 | 17 | import scala.scalajs.js.{UndefOr, undefined} 18 | 19 | object DropdownButton extends BootstrapComponent { 20 | override type P = DropdownButton 21 | override type S = DropdownState 22 | override type B = Backend 23 | override type N = TopNode 24 | 25 | override def defaultProps: P = DropdownButton() 26 | 27 | case class DropdownButton( 28 | /*== start react bootstraps ==*/ 29 | id: UndefOr[String] = undefined, 30 | pullRight: UndefOr[Boolean] = undefined, 31 | dropup: UndefOr[Boolean] = undefined, 32 | title: UndefOr[ReactNode] = undefined, 33 | href: UndefOr[String] = undefined, 34 | eventKey: UndefOr[String] = undefined, 35 | navItem: UndefOr[Boolean] = undefined, 36 | noCaret: UndefOr[Boolean] = undefined, 37 | onClick: () => Unit = () => (), 38 | onSelect: UndefOr[(String) => Unit] = undefined, 39 | /*== end react bootstraps ==*/ 40 | bsClass: UndefOr[Classes.Value] = Classes.btn, 41 | bsStyle: UndefOr[Styles.Value] = Styles.default, 42 | bsSize: UndefOr[Sizes.Value] = undefined, 43 | addClasses: String = "") 44 | extends BsProps with MergeableProps[DropdownButton] { 45 | 46 | def merge(t: Map[String, Any]): DropdownButton = implicitly[Mergeable[DropdownButton]].merge(this, t) 47 | 48 | def asMap: Map[String, Any] = implicitly[Mappable[DropdownButton]].toMap(this) 49 | 50 | def apply(children: ReactNode*) = component(this, children) 51 | 52 | def apply() = component(this) 53 | } 54 | 55 | class Backend(val scope: BackendScope[DropdownButton, DropdownState]) extends DropdownStateMixin[DropdownButton] { 56 | 57 | def handleOptionSelect(key: String): Unit = { 58 | if (scope.props.onSelect.isDefined) 59 | scope.props.onSelect.get(key) 60 | } 61 | 62 | def handleDropdownClick(e: ReactEvent) = { 63 | e.preventDefault() 64 | setDropdownState(!scope.state.open) 65 | } 66 | } 67 | 68 | override val component = ReactComponentB[DropdownButton]("DropdownButton") 69 | .initialState(DropdownState(open = false)) 70 | .backend(new Backend(_)) 71 | .render((P, C, S, B) => { 72 | def renderButtonGroup(children: ReactNode*) = { 73 | var addClasses = "dropdown" 74 | if (S.open) 75 | addClasses += " open" 76 | ButtonGroup(ButtonGroup.ButtonGroup(addClasses = addClasses), children) 77 | } 78 | def renderNavItem(children: ReactNode*) = { 79 | <.li(^.classSet1("dropdown", "open" -> S.open, "dropup" -> P.dropup.getOrElse(false)))(children) 80 | } 81 | 82 | def renderMenuItem(child: ReactNode, index: Int) = { 83 | // Only handle the option selection if an onSelect prop has been set on the 84 | // component or it's child, this allows a user not to pass an onSelect 85 | // handler and have the browser preform the default action. 86 | val handleOptionSelect: js.Any = if (P.onSelect.isDefined) // FIXME || child.props.onSelect) 87 | B.handleOptionSelect _ 88 | else () => () 89 | 90 | val keyAndRef = getChildKeyAndRef(child, index) 91 | ReactCloneWithProps(child, keyAndRef ++ 92 | Map[String, js.Any]( 93 | "onSelect" -> handleOptionSelect) // FIXME createChainedFunction0(childProps.onSelect, handleOptionSelect) 94 | ) 95 | } 96 | 97 | val buttonRef = Ref("dropdownButton") 98 | val buttonProps = Button.Button( 99 | addClasses = "dropdown-toggle", 100 | onClick = (e: ReactEvent) => B.handleDropdownClick(e), 101 | navDropdown = P.navItem.getOrElse(false), 102 | bsStyle = P.bsStyle, 103 | bsSize = P.bsSize, 104 | bsClass = P.bsClass 105 | ) 106 | 107 | val menuRef = Ref("menu") 108 | val dropdownMenu = DropdownMenu.withKey(1).withRef("menu")(DropdownMenu.DropdownMenu(ariaLabelledBy = P.id, 109 | pullRight = P.pullRight), 110 | ValidComponentChildren.map(C, renderMenuItem) 111 | ) 112 | 113 | val button = if (P.noCaret.getOrElse(false)) 114 | Button.withKey(0).withRef("dropdownButton")(buttonProps, P.title.get) 115 | else 116 | Button.withKey(0).withRef("dropdownButton")(buttonProps, P.title.get, " ", <.span(^.className := "caret")) 117 | 118 | if (P.navItem.getOrElse(false)) 119 | renderNavItem(button, dropdownMenu) 120 | else 121 | renderButtonGroup(button, dropdownMenu) 122 | }) 123 | .componentWillUnmount(_.backend.onComponentWillUnmount()) 124 | .build 125 | 126 | } -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/DropdownMenu.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.utils.{Mappable, Mergeable} 4 | import japgolly.scalajs.react.Addons.ReactCloneWithProps 5 | 6 | import scala.scalajs.js 7 | 8 | /** 9 | * Created by weiyin on 11/03/15. 10 | */ 11 | 12 | import com.acework.js.components.bootstrap.Utils._ 13 | import japgolly.scalajs.react._ 14 | import japgolly.scalajs.react.vdom.prefix_<^._ 15 | 16 | import scala.scalajs.js.{UndefOr, undefined} 17 | 18 | object DropdownMenu extends BootstrapComponent { 19 | override type P = DropdownMenu 20 | override type S = Unit 21 | override type B = Unit 22 | override type N = TopNode 23 | 24 | override def defaultProps = DropdownMenu() 25 | 26 | case class DropdownMenu( 27 | /*== start react bootstraps ==*/ 28 | ariaLabelledBy: UndefOr[String] = undefined, 29 | pullRight: UndefOr[Boolean] = undefined, 30 | onSelect: UndefOr[(String) => Unit] = undefined, 31 | /*== end react bootstraps ==*/ 32 | addClasses: String = "") extends MergeableProps[DropdownMenu] { 33 | 34 | def merge(t: Map[String, Any]): DropdownMenu = implicitly[Mergeable[DropdownMenu]].merge(this, t) 35 | 36 | def asMap: Map[String, Any] = implicitly[Mappable[DropdownMenu]].toMap(this) 37 | 38 | def apply(children: ReactNode*) = component(this, children) 39 | 40 | def apply() = component(this) 41 | } 42 | 43 | override val component = ReactComponentB[DropdownMenu]("DropdownMenu") 44 | .render((P, C) => { 45 | 46 | def renderMenuItem(child: ReactNode, index: Int) = { 47 | val keyAndRef = getChildKeyAndRef(child, index) 48 | if (P.onSelect.isDefined) 49 | ReactCloneWithProps(child, keyAndRef ++ 50 | Map[String, js.Any]( 51 | "onSelect" -> P.onSelect.get) // FIXME createChainedFunction0(childProps.onSelect, handleOptionSelect) 52 | ) 53 | else 54 | ReactCloneWithProps(child, keyAndRef) 55 | } 56 | 57 | <.ul(^.classSet1("dropdown-menu", "dropdown-menu-right" -> P.pullRight.getOrElse(false)), 58 | ^.role := "menu", ^.aria.labelledby := P.ariaLabelledBy)( 59 | ValidComponentChildren.map(C, renderMenuItem) 60 | ) 61 | }) 62 | .build 63 | 64 | } 65 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/DropdownStateMixin.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.utils.EventListener 4 | import japgolly.scalajs.react.{BackendScope, ReactEvent} 5 | import org.scalajs.dom.document 6 | import org.scalajs.dom.raw.{KeyboardEvent, Node} 7 | 8 | /** 9 | * Created by weiyin on 11/03/15. 10 | */ 11 | 12 | case class DropdownState(open: Boolean) 13 | 14 | trait DropdownStateMixin[P] { 15 | 16 | val scope: BackendScope[P, DropdownState] 17 | 18 | var _onDocumentClickListener: Option[EventListener] = None 19 | var _onDocumentKeyUpListener: Option[EventListener] = None 20 | 21 | /** 22 | * Checks whether a node is within 23 | * a root nodes tree 24 | * 25 | * @param node 26 | * @param root 27 | */ 28 | def isNodeInRoot(node: Node, root: Node): Boolean = { 29 | var aNode = node 30 | var done = false 31 | var result = false 32 | while (aNode != null && !done) { 33 | if (aNode == root) { 34 | done = true 35 | result = true 36 | } 37 | else 38 | aNode = aNode.parentNode 39 | } 40 | result 41 | } 42 | 43 | def setDropdownState(newState: Boolean, onStateChangeComplete: () => Unit = () => ()) = { 44 | if (newState) 45 | bindRootCloseHandlers() 46 | else 47 | unBindRootCloseHandlers() 48 | 49 | scope.modState(s => s.copy(open = newState), onStateChangeComplete) 50 | } 51 | 52 | def bindRootCloseHandlers(): Unit = { 53 | _onDocumentClickListener = Some(EventListener.listen(document, "click", handleDocumentClick(_: ReactEvent))) 54 | _onDocumentKeyUpListener = Some(EventListener.listen(document, "keyup", handleDocumentKeyUp(_: KeyboardEvent))) 55 | } 56 | 57 | def unBindRootCloseHandlers(): Unit = { 58 | _onDocumentClickListener.map(_.remove()) 59 | _onDocumentClickListener = None 60 | _onDocumentKeyUpListener.map(_.remove()) 61 | _onDocumentKeyUpListener = None 62 | } 63 | 64 | def handleDocumentKeyUp(e: KeyboardEvent) = { 65 | if (e.keyCode == 27) 66 | setDropdownState(false) 67 | } 68 | 69 | def handleDocumentClick(e: ReactEvent) = { 70 | if (!isNodeInRoot(e.target, scope.getDOMNode())) 71 | setDropdownState(false) 72 | } 73 | 74 | def onComponentWillUnmount() = unBindRootCloseHandlers() 75 | 76 | } 77 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/FadeMixin.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import japgolly.scalajs.react.{BackendScope, TopNode} 4 | import org.scalajs.dom.raw.HTMLElement 5 | import org.scalajs.dom.{document, raw} 6 | 7 | import scala.scalajs.js 8 | 9 | /** 10 | * Created by weiyin on 11/03/15. 11 | */ 12 | trait FadeMixin[P <: OverlayProps, S] { 13 | 14 | def scope: BackendScope[P, S] 15 | 16 | var fadeOutEl: Option[raw.Element] = None 17 | 18 | def getElementAndSelf(root: TopNode, classes: Array[String]): Array[TopNode] = { 19 | val elements = root.querySelectorAll("." + classes.mkString(".")) 20 | 21 | val els = (for (i <- 0 until elements.length) yield elements(i).asInstanceOf[TopNode]).toArray 22 | 23 | var matched = true 24 | for (i <- 0 until classes.length if matched) { 25 | val Pattern = "\\b${classes(i)}\\b".r 26 | root.className match { 27 | case Pattern() => 28 | case _ => 29 | matched = false 30 | } 31 | } 32 | if (matched) 33 | root +: els 34 | else 35 | els 36 | } 37 | 38 | def _fadeIn() = { 39 | if (scope.isMounted()) { 40 | val elements = getElementAndSelf(scope.getDOMNode(), Array("fade")) 41 | elements.foreach { el => 42 | val classes = el.className.split(" ").filter(_ != "in") ++ Seq("in") 43 | el.className = classes.mkString(" ") 44 | } 45 | } 46 | } 47 | 48 | def _fadeOut() = { 49 | val elements = getElementAndSelf(scope.getDOMNode(), Array("fade", "in")) 50 | elements.foreach { el => 51 | el.className = el.className.replace("\\bin\\b", "") 52 | } 53 | js.timers.setTimeout(300)(handleFadeOutEnd()) 54 | } 55 | 56 | def handleFadeOutEnd(): Unit = { 57 | fadeOutEl.map { fadeout => 58 | if (fadeout.parentNode != null) 59 | fadeout.parentNode.removeChild(fadeout) 60 | } 61 | } 62 | 63 | def onComponentDidMount() = { 64 | // FIXME what does this mean? -- if(document.querySelectorAll) 65 | //val nodes = document.querySelectorAll("") 66 | if (true) { 67 | // Firefox needs delay for transition to be triggered 68 | js.timers.setTimeout(20)(_fadeIn()) 69 | } 70 | } 71 | 72 | def onComponentWillUnmount(): Unit = { 73 | val elements = getElementAndSelf(scope.getDOMNode(), Array("fade")) 74 | // TODO container 75 | val container = scope.props.container.getDOMNode 76 | if (elements.length > 0) { 77 | val fadeOut = document.createElement("div") 78 | container.appendChild(fadeOut) 79 | fadeOut.appendChild(scope.getDOMNode().cloneNode(deep = true)) 80 | fadeOutEl = Some(fadeOut) 81 | js.timers.setTimeout(20)(_fadeOut()) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/Grid.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import japgolly.scalajs.react._ 4 | import japgolly.scalajs.react.vdom.prefix_<^._ 5 | 6 | import scala.scalajs.js._ 7 | 8 | /** 9 | * Created by weiyin on 10/03/15. 10 | */ 11 | object Grid { 12 | 13 | case class Grid(fluid: UndefOr[Boolean] = undefined, 14 | componentClass: String = "div", addClasses: String = "") { 15 | 16 | def apply(children: ReactNode*) = component(this, children) 17 | 18 | def apply() = component(this) 19 | } 20 | 21 | val component = ReactComponentB[Grid]("Grid") 22 | .render { (P, C) => 23 | 24 | val classes = if (P.fluid.getOrElse(false)) Map("container-fluid" -> true) else Map("container" -> true) 25 | val componentClass = P.componentClass.reactTag 26 | 27 | // FIXME spread props 28 | componentClass(^.classSet1M(P.addClasses, classes))(C) 29 | 30 | }.build 31 | 32 | def apply(props: Grid, children: ReactNode*) = component(props, children) 33 | 34 | def apply(children: ReactNode*) = component(Grid(), children) 35 | } 36 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/Interpolate.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import japgolly.scalajs.react._ 4 | import japgolly.scalajs.react.vdom.prefix_<^._ 5 | 6 | import scala.scalajs.js._ 7 | import Utils._ 8 | 9 | import scala.util.matching.Regex.Groups 10 | 11 | /** 12 | * Created by weiyin on 10/03/15. 13 | */ 14 | object Interpolate extends BootstrapComponent { 15 | override type P = Interpolate 16 | override type S = Unit 17 | override type B = Unit 18 | override type N = TopNode 19 | 20 | def defaultProps = Interpolate() 21 | 22 | case class Interpolate(now: UndefOr[Double] = undefined, 23 | min: UndefOr[Double] = undefined, 24 | max: UndefOr[Double] = undefined, 25 | format: UndefOr[String] = undefined, 26 | componentClass: UndefOr[String] = "span", 27 | unsafe: UndefOr[Boolean] = undefined, 28 | percent: UndefOr[Double] = undefined, 29 | bsClass: UndefOr[Classes.Value] = Classes.label, 30 | bsStyle: UndefOr[Styles.Value] = Styles.default, 31 | bsSize: UndefOr[Sizes.Value] = undefined, 32 | addClasses: String = "") extends BsProps { 33 | 34 | def apply(children: ReactNode*) = component(this, children) 35 | 36 | def apply() = component(this) 37 | } 38 | 39 | val REGEXP = "%\\((.*)\\)s".r 40 | 41 | override val component = ReactComponentB[Interpolate]("Interpolate") 42 | .render { (P, C) => 43 | 44 | def getFormat(): String = { 45 | // FIXME C is str 46 | //if (ValidComponentChildren.hasValidComponents(C)) 47 | if (React.Children.count(C) > 0) 48 | C.asInstanceOf[Array[String]](0) 49 | else 50 | P.format.getOrElse("") 51 | } 52 | 53 | val format = getFormat() 54 | val parent = P.componentClass.get.reactTag 55 | val unsafe = P.unsafe.getOrElse(false) 56 | 57 | val content = REGEXP.replaceAllIn(format, _ match { 58 | case Groups("now") => s"${P.min}" 59 | case Groups("min") => s"${P.min}" 60 | case Groups("max") => s"${P.min}" 61 | case Groups("percent") => s"${P.percent}" 62 | case Groups("bsStyle") => s"${P.bsStyle}" 63 | }) 64 | 65 | // TODO props spread 66 | if (unsafe) { 67 | parent(^.classSet1M(P.addClasses, P.bsClassSet), ^.dangerouslySetInnerHtml(content)) 68 | } 69 | else 70 | parent(^.classSet1M(P.addClasses, P.bsClassSet), content) 71 | }.build 72 | 73 | } 74 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/JQuery.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import org.scalajs.dom._ 4 | 5 | import scala.scalajs.js 6 | 7 | /** 8 | * Minimal facade for JQuery. Use https://github.com/scala-js/scala-js-jquery/blob/master/src/main/scala/org/scalajs/jquery/JQuery.scala 9 | * for more complete one. 10 | */ 11 | trait JQueryEventObject extends Event { 12 | var data: js.Any = js.native 13 | } 14 | 15 | trait JQueryStatic extends js.Object { 16 | def apply(element: Element): JQuery = js.native 17 | } 18 | 19 | trait JQuery extends js.Object { 20 | def on(events: String, selector: js.Any, data: js.Any, handler: js.Function1[JQueryEventObject, js.Any]): JQuery = js.native 21 | 22 | def off(events: String): JQuery = js.native 23 | 24 | def offset(): js.Any = js.native 25 | 26 | def position(): js.Any = js.native 27 | } -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/JsNumberOrString.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import scala.scalajs.js 4 | import scala.scalajs.js.UndefOr 5 | 6 | /** 7 | * Created by weiyin on 17/03/15. 8 | */ 9 | 10 | trait JsNumberOrString extends js.Object 11 | 12 | object JsNumberOrString { 13 | implicit def int2JsNumOrString(i: Int): UndefOr[JsNumberOrString] = i.asInstanceOf[JsNumberOrString] 14 | 15 | implicit def str2JsNumOrString(s: String): UndefOr[JsNumberOrString] = s.asInstanceOf[JsNumberOrString] 16 | } -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/Jumbotron.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import japgolly.scalajs.react._ 4 | import japgolly.scalajs.react.vdom.prefix_<^._ 5 | 6 | 7 | /** 8 | * Created by weiyin on 10/03/15. 9 | */ 10 | object Jumbotron extends BootstrapComponent { 11 | override type P = Jumbotron 12 | override type S = Unit 13 | override type B = Unit 14 | override type N = TopNode 15 | 16 | override def defaultProps = Jumbotron() 17 | 18 | 19 | case class Jumbotron(addClasses: String = ""){ 20 | 21 | def apply(children: ReactNode*) = component(this, children) 22 | 23 | def apply() = component(this) 24 | } 25 | 26 | override val component = ReactComponentB[Jumbotron]("Jumbotron") 27 | .render { (P, C) => 28 | <.div(^.classSet1(P.addClasses, "jumbotron" -> true))(C) 29 | }.build 30 | 31 | } 32 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/Label.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.utils.{Mappable, Mergeable} 4 | import japgolly.scalajs.react._ 5 | import japgolly.scalajs.react.vdom.prefix_<^._ 6 | 7 | import scala.scalajs.js.{UndefOr, undefined} 8 | 9 | /** 10 | * Created by weiyin on 10/03/15. 11 | */ 12 | object Label extends BootstrapComponent { 13 | override type P = Label 14 | override type S = Unit 15 | override type B = Unit 16 | override type N = TopNode 17 | 18 | override def defaultProps = Label() 19 | 20 | // defaults are based on getDefaultProps 21 | case class Label(bsClass: UndefOr[Classes.Value] = Classes.label, 22 | bsStyle: UndefOr[Styles.Value] = Styles.default, 23 | bsSize: UndefOr[Sizes.Value] = undefined, 24 | addClasses: String = "") extends BsProps with MergeableProps[Label] { 25 | 26 | def merge(t: Map[String, Any]): Label = implicitly[Mergeable[Label]].merge(this, t) 27 | 28 | def asMap: Map[String, Any] = implicitly[Mappable[Label]].toMap(this) 29 | 30 | def apply(children: ReactNode*) = component(this, children) 31 | 32 | def apply() = component(this) 33 | } 34 | 35 | override val component = ReactComponentB[Label]("Label") 36 | .render { (P, C) => 37 | // TODO props spread 38 | <.span(^.classSet1M(P.addClasses, P.bsClassSet))(C) 39 | }.build 40 | 41 | } 42 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/ListGroup.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.components.bootstrap.Utils._ 4 | import japgolly.scalajs.react.Addons.ReactCloneWithProps 5 | import japgolly.scalajs.react._ 6 | import japgolly.scalajs.react.vdom.prefix_<^._ 7 | 8 | import scala.scalajs.js._ 9 | 10 | /** 11 | * Created by weiyin on 10/03/15. 12 | */ 13 | object ListGroup extends BootstrapComponent { 14 | override type P = ListGroup 15 | override type S = Unit 16 | override type B = Unit 17 | override type N = TopNode 18 | 19 | override def defaultProps = ListGroup() 20 | 21 | case class ListGroup(fill: Boolean = false, onClick: UndefOr[() => Unit] = undefined) { 22 | def apply(children: ReactNode*) = component(this, children) 23 | 24 | def apply() = component(this) 25 | } 26 | 27 | override val component = ReactComponentB[ListGroup]("ListGroup") 28 | .render { (P, C) => 29 | 30 | def renderListItem(child: ReactNode, index: Int) = { 31 | ReactCloneWithProps(child, getChildKeyAndRef(child, index)) 32 | } 33 | 34 | <.div(^.className := "list-group")( 35 | ValidComponentChildren.map(C, renderListItem) 36 | ) 37 | }.build 38 | 39 | } 40 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/ListGroupItem.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.utils.{Mappable, Mergeable} 4 | import japgolly.scalajs.react.Addons.ReactCloneWithProps 5 | import japgolly.scalajs.react._ 6 | import japgolly.scalajs.react.vdom.prefix_<^._ 7 | 8 | import scala.scalajs.js.{UndefOr, undefined} 9 | 10 | /** 11 | * Created by weiyin on 10/03/15. 12 | */ 13 | object ListGroupItem extends BootstrapComponent { 14 | override type P = ListGroupItem 15 | override type S = Unit 16 | override type B = Unit 17 | override type N = TopNode 18 | 19 | override def defaultProps = ListGroupItem() 20 | 21 | case class ListGroupItem(active: UndefOr[Boolean] = undefined, 22 | disabled: UndefOr[Boolean] = undefined, 23 | header: UndefOr[ReactNode] = undefined, 24 | eventKey: UndefOr[String] = undefined, 25 | href: UndefOr[String] = undefined, 26 | target: UndefOr[String] = undefined, 27 | onSelect: UndefOr[(UndefOr[String]) => Unit] = undefined, 28 | onClick: UndefOr[(UndefOr[String]) => Unit] = undefined, 29 | bsClass: UndefOr[Classes.Value] = Classes.`list-group-item`, 30 | bsStyle: UndefOr[Styles.Value] = undefined, 31 | bsSize: UndefOr[Sizes.Value] = undefined, 32 | addClasses: String = "") extends BsProps with MergeableProps[ListGroupItem] { 33 | 34 | def merge(t: Map[String, Any]): ListGroupItem = implicitly[Mergeable[ListGroupItem]].merge(this, t) 35 | 36 | def asMap: Map[String, Any] = implicitly[Mappable[ListGroupItem]].toMap(this) 37 | 38 | def apply(children: ReactNode*) = component(this, children) 39 | 40 | def apply() = component(this) 41 | } 42 | 43 | override val component = ReactComponentB[ListGroupItem]("ListGroupItem") 44 | .render { (P, C) => 45 | 46 | def renderAnchor(classes: Map[String, Boolean]) = { 47 | // TODO spread props 48 | <.a(^.classSet1M(P.addClasses, classes))( 49 | if (P.header.isDefined) renderStructuredContent() else C 50 | ) 51 | } 52 | 53 | def renderSpan(classes: Map[String, Boolean]) = { 54 | // TODO spread props 55 | <.span(^.classSet1M(P.addClasses, classes))( 56 | if (P.header.isDefined) renderStructuredContent() else C 57 | ) 58 | } 59 | 60 | def renderStructuredContent(): Seq[ReactNode] = { 61 | val header: ReactNode = 62 | if (React.isValidElement(P.header.get)) { 63 | ReactCloneWithProps(P.header.get, Map("className" -> "list-group-item-heading")) 64 | } 65 | else { 66 | <.h4(^.className := "list-group-item-heading", P.header.get) 67 | } 68 | val content = <.p(^.className := "list-group-item-text")(C) 69 | // FIXME in js, it is {header: header, content: content} 70 | Seq(header, content) 71 | } 72 | 73 | var classes = P.bsClassSet 74 | classes += ("active" -> P.active.getOrElse(false)) 75 | classes += ("disabled" -> P.disabled.getOrElse(false)) 76 | 77 | if (P.href.isDefined || P.target.isDefined || P.onClick.isDefined) 78 | renderAnchor(classes) 79 | else 80 | renderSpan(classes) 81 | 82 | }.build 83 | 84 | } 85 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/MenuItem.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.utils.{Mappable, Mergeable} 4 | import japgolly.scalajs.react._ 5 | import japgolly.scalajs.react.vdom.prefix_<^._ 6 | 7 | import scala.scalajs.js.{UndefOr, undefined} 8 | 9 | /** 10 | * Created by weiyin on 10/03/15. 11 | */ 12 | object MenuItem extends BootstrapComponent { 13 | override type P = MenuItem 14 | override type S = Unit 15 | override type B = Backend 16 | override type N = TopNode 17 | 18 | override def defaultProps = MenuItem() 19 | 20 | case class MenuItem(header: UndefOr[Boolean] = undefined, 21 | divider: UndefOr[Boolean] = undefined, 22 | href: String = "#", 23 | title: UndefOr[String] = undefined, 24 | target: UndefOr[String] = undefined, 25 | eventKey: UndefOr[String] = undefined, 26 | onSelect: UndefOr[(String, String, String) => Unit] = undefined, 27 | addClasses: String = "") extends MergeableProps[MenuItem] { 28 | 29 | def merge(t: Map[String, Any]): MenuItem = implicitly[Mergeable[MenuItem]].merge(this, t) 30 | 31 | def asMap: Map[String, Any] = implicitly[Mappable[MenuItem]].toMap(this) 32 | 33 | def apply(children: ReactNode*) = component(this, children) 34 | 35 | def apply() = component(this) 36 | } 37 | 38 | class Backend($: BackendScope[MenuItem, Unit]) { 39 | def handleClick(e: ReactEvent): Unit = { 40 | if ($.props.onSelect.isDefined) { 41 | e.preventDefault() 42 | $.props.onSelect.get($.props.eventKey.get, $.props.href, $.props.target.get) 43 | } 44 | } 45 | } 46 | 47 | override val component = ReactComponentB[MenuItem]("MenuItem") 48 | .stateless 49 | .backend(new Backend(_)) 50 | .render { (P, C, S, B) => 51 | 52 | def renderAnchor(): ReactNode = { 53 | <.a(^.onClick --> B.handleClick _, ^.href := P.href, ^.target := P.target, ^.title := P.title, ^.tabIndex := -1)(C) 54 | } 55 | 56 | val classes = Map("dropdown-header" -> P.header.getOrElse(false), "divider" -> P.divider.getOrElse(false)) 57 | 58 | val children: TagMod = 59 | if (P.header.getOrElse(false)) 60 | C 61 | else if (!P.divider.getOrElse(false)) 62 | renderAnchor() 63 | else 64 | EmptyTag 65 | 66 | // TODO spread props 67 | <.li(^.classSet1M(P.addClasses, classes), ^.role := "presentation")(children) 68 | }.build 69 | 70 | } 71 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/MergeableProps.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | /** 4 | * Created by weiyin on 18/03/15. 5 | */ 6 | trait MergeableProps[T] { 7 | 8 | def merge(t: Map[String, Any]): MergeableProps[T] 9 | 10 | def asMap: Map[String, Any] 11 | } 12 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/ModalTrigger.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.components.bootstrap.Utils._ 4 | import japgolly.scalajs.react._ 5 | import japgolly.scalajs.react.vdom.prefix_<^._ 6 | 7 | import scala.scalajs.js 8 | import scala.scalajs.js.{UndefOr, undefined} 9 | 10 | /** 11 | * Created by weiyin on 11/03/15. 12 | */ 13 | object ModalTrigger { 14 | 15 | class Container extends OverlayContainer { 16 | override def getDOMNode = super.getDOMNode 17 | } 18 | 19 | case class ModalTrigger(modal: ReactElement, container: OverlayContainer = new OverlayContainer {}) extends OverlayProps { 20 | 21 | def apply(child: ReactNode) = component(this, child) 22 | 23 | def apply() = component(this) 24 | } 25 | 26 | case class State(isOverlayShown: Boolean = false) 27 | 28 | class Backend(val scope: BackendScope[ModalTrigger, State]) extends OverlayMixin[ModalTrigger, State] { 29 | 30 | def show() = scope.modState(_.copy(isOverlayShown = true)) 31 | 32 | def hide() = scope.modState(_.copy(isOverlayShown = false)) 33 | 34 | def toggle() = { 35 | scope.modState(s => s.copy(isOverlayShown = !s.isOverlayShown)) 36 | } 37 | 38 | override def renderOverlay(): Option[ReactElement] = { 39 | if (scope.state.isOverlayShown) 40 | Some(cloneWithProps(scope.props.modal, (undefined, undefined), 41 | Map("container" -> scope.props.container, 42 | "onRequestHide" -> (() => hide())))) 43 | else 44 | Some(<.span()) 45 | } 46 | } 47 | 48 | val component = ReactComponentB[ModalTrigger]("ModelTrigger") 49 | .initialState(State()) 50 | .backend(new Backend(_)) 51 | .render((P, C, S, B) => { 52 | 53 | val child = React.Children.only(C) 54 | val childProps = getChildProps[Any](child).asInstanceOf[js.Dynamic] 55 | val childOnClick: UndefOr[js.Any] = childProps.onClick 56 | val jsChildOnClick: UndefOr[(ReactEvent) => (js.Any)] = if (childOnClick == null || childOnClick == undefined) undefined else childOnClick.asInstanceOf[(ReactEvent) => (js.Any)] 57 | val toggle = ((e: ReactEvent) => B.toggle()).asInstanceOf[(ReactEvent) => (js.Any)] 58 | val onClick = createChainedFunction1(jsChildOnClick, toggle) 59 | 60 | cloneWithProps(child, (undefined, undefined), Map("onClick" -> onClick)) 61 | }) 62 | .componentDidMount(scope => scope.backend.onComponentDidMount()) 63 | .componentDidUpdate((scope, nextProps, state) => scope.backend.onComponentDidUpdate()) 64 | .componentWillUnmount(scope => scope.backend.onComponentWillUnmount()) 65 | .build 66 | 67 | def apply(props: ModalTrigger, child: ReactNode) = component(props, child) 68 | } 69 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/Nav.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.utils.{Mappable, Mergeable} 4 | import japgolly.scalajs.react._ 5 | import japgolly.scalajs.react.vdom.prefix_<^._ 6 | import org.scalajs.dom.raw.{HTMLDocument, HTMLUListElement} 7 | 8 | import scala.scalajs.js 9 | import scala.scalajs.js.{UndefOr, undefined} 10 | import com.acework.js.components.bootstrap.Utils._ 11 | 12 | 13 | /** 14 | * Created by weiyin on 09/03/15. 15 | */ 16 | 17 | object Nav extends BootstrapComponent { 18 | override type P = Nav 19 | override type S = CollapsableState 20 | override type B = Backend 21 | override type N = TopNode 22 | 23 | override def defaultProps = Nav() 24 | 25 | @inline implicit final class ReactExt2_PropsChildren(val _c: PropsChildren) extends AnyVal { 26 | @inline def map[U](f: ReactNode => U): UndefOr[Object] = 27 | React.Children.map(_c, (f: js.Function).asInstanceOf[js.Function1[ReactNode, js.Any]]) 28 | 29 | @inline def map[U](f: (ReactNode, Int) => U): UndefOr[Object] = 30 | React.Children.map(_c, (f: js.Function).asInstanceOf[js.Function2[ReactNode, Int, js.Any]]) 31 | } 32 | 33 | case class Nav(navbar: Boolean = false, stacked: Boolean = false, 34 | justified: Boolean = false, pullRight: Boolean = false, 35 | right: Boolean = false, 36 | collapsable: UndefOr[Boolean] = undefined, 37 | expanded: UndefOr[Boolean] = undefined, 38 | activeHref: UndefOr[String] = undefined, 39 | activeKey: UndefOr[String] = undefined, 40 | role: UndefOr[String] = undefined, 41 | eventKey: UndefOr[String] = undefined, 42 | id: UndefOr[String] = undefined, 43 | bsClass: UndefOr[Classes.Value] = Classes.nav, 44 | bsStyle: UndefOr[Styles.Value] = undefined, 45 | bsSize: UndefOr[Sizes.Value] = undefined, 46 | onSelect: UndefOr[Seq[UndefOr[String]] => Unit] = undefined, 47 | addClasses: String = "") extends BsProps with CollapsableProps with MergeableProps[Nav] { 48 | 49 | def merge(t: Map[String, Any]): Nav = implicitly[Mergeable[Nav]].merge(this, t) 50 | 51 | def asMap: Map[String, Any] = implicitly[Mappable[Nav]].toMap(this) 52 | 53 | def apply(children: ReactNode*) = component(this, children) 54 | 55 | def apply() = component(this) 56 | } 57 | 58 | 59 | class Backend(val scope: BackendScope[Nav, CollapsableState]) extends CollapsableMixin[Nav] { 60 | 61 | def getCollapsableDOMNode: Option[TopNode] = Some(scope.getDOMNode()) 62 | 63 | def getCollapsableDimensionValue: Int = { 64 | if (scope.isMounted() && scope.refs("ul").isDefined) { 65 | val node = scope.refs("ul").get.asInstanceOf[TopNode] 66 | val height = node.offsetHeight.toInt 67 | val computedStyles = node.ownerDocument.asInstanceOf[HTMLDocument].defaultView.getComputedStyle(node, "") 68 | height + computedStyles.marginTop.toInt + computedStyles.marginBottom.toInt 69 | } 70 | else 71 | 0 72 | } 73 | 74 | } 75 | 76 | val ulRef = Ref[HTMLUListElement]("ul") 77 | 78 | override val component = ReactComponentB[Nav]("Nav") 79 | .initialState(CollapsableState(false, false)) 80 | .backend(new Backend(_)) 81 | .render((P, C, S, B) => { 82 | 83 | def getChildActiveProp(child: ReactNode) = { 84 | val childPropsAny = getChildProps[Any](child) 85 | childPropsAny match { 86 | case props: NavItem.NavItem => 87 | props.active || 88 | props.eventKey.getOrElse("NA1") == P.activeKey.getOrElse("NA") || 89 | props.href.getOrElse("NA1") == P.activeHref.getOrElse("NA") 90 | case _ => false 91 | } 92 | } 93 | 94 | def getOnSelectProps(child: ReactNode): UndefOr[Seq[UndefOr[String]] => Unit] = { 95 | val childPropsAny = getChildProps[Any](child) 96 | childPropsAny match { 97 | case props: NavItem.NavItem => 98 | if (props.onSelect.isDefined) 99 | props.onSelect 100 | else 101 | P.onSelect 102 | case _ => undefined 103 | } 104 | } 105 | 106 | def renderNavItem(child: ReactNode, index: Int): ReactNode = { 107 | val keyAndRef = getChildKeyAndRef2(child, index) 108 | 109 | val propsMap = Map[String, Any]( 110 | "active" -> getChildActiveProp(child), 111 | "activeKey" -> P.activeKey, 112 | "activeHref" -> P.activeHref, 113 | "onSelect" -> getOnSelectProps(child), 114 | "navItem" -> true 115 | ) 116 | cloneWithProps(child, keyAndRef, propsMap) 117 | } 118 | 119 | def renderUI() = { 120 | val classes = P.bsClassSet ++ Map( 121 | "nav-stacked" -> P.stacked, 122 | "nav-justified" -> P.justified, 123 | "navbar-nav" -> P.navbar, 124 | "pull-right" -> P.pullRight, 125 | "navbar-right" -> P.right) 126 | 127 | /* 128 | val navItems = new ListBuffer[ReactNode] 129 | C.forEach { (n, idx) => 130 | if (React.isValidElement(n)) 131 | navItems.append(renderNavItem(n, idx)) 132 | } */ 133 | 134 | <.ul(^.classSet1M(P.addClasses, classes), ^.ref := ulRef, 135 | ValidComponentChildren.map(C, renderNavItem) 136 | ) 137 | } 138 | 139 | var classes = if (P.collapsable.getOrElse(false)) B.getCollapsableClassSet("") else Map[String, Boolean]() 140 | classes += ("navbar-collapse" -> P.collapsable.getOrElse(false)) 141 | 142 | if (P.navbar && !P.collapsable.getOrElse(false)) 143 | renderUI() 144 | else { 145 | val ui = renderUI() 146 | <.nav(^.classSetM(classes), ^.role := P.role, ui) 147 | } 148 | } 149 | ).build 150 | 151 | } 152 | 153 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/NavBar.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.components.bootstrap.Utils._ 4 | import com.acework.js.utils.{Mappable, Mergeable} 5 | import japgolly.scalajs.react.Addons.ReactCloneWithProps 6 | import japgolly.scalajs.react._ 7 | import japgolly.scalajs.react.vdom.prefix_<^._ 8 | 9 | import scala.scalajs.js 10 | import scala.scalajs.js.{UndefOr, undefined} 11 | 12 | 13 | /** 14 | * Created by weiyin on 09/03/15. 15 | */ 16 | 17 | object NavBar extends BootstrapComponent { 18 | override type P = NavBar 19 | override type S = State 20 | override type B = Backend 21 | override type N = TopNode 22 | 23 | override def defaultProps = NavBar() 24 | 25 | case class NavBar( 26 | /*== start react bootstraps ==*/ 27 | fixedTop: UndefOr[Boolean] = undefined, 28 | fixedBottom: UndefOr[Boolean] = undefined, 29 | staticTop: UndefOr[Boolean] = undefined, 30 | inverse: UndefOr[Boolean] = undefined, 31 | fluid: UndefOr[Boolean] = undefined, 32 | role: UndefOr[String] = "navigation", 33 | componentClass: UndefOr[String] = "Nav", 34 | brand: UndefOr[ReactNode] = undefined, 35 | toggleButton: UndefOr[ReactNode] = undefined, 36 | toggleNavKey: UndefOr[String] = undefined, // FIXME String or Number 37 | onToggle: UndefOr[() => Unit] = undefined, 38 | navExpanded: UndefOr[Boolean] = undefined, 39 | defaultNavExpanded: UndefOr[Boolean] = undefined, 40 | bsClass: UndefOr[Classes.Value] = Classes.navbar, 41 | bsStyle: UndefOr[Styles.Value] = Styles.default, 42 | bsSize: UndefOr[Sizes.Value] = undefined, 43 | addClasses: String = "") extends BsProps with MergeableProps[NavBar] { 44 | 45 | def merge(t: Map[String, Any]): NavBar = implicitly[Mergeable[NavBar]].merge(this, t) 46 | 47 | def asMap: Map[String, Any] = implicitly[Mappable[NavBar]].toMap(this) 48 | 49 | def apply(children: ReactNode*) = component(this, children) 50 | 51 | def apply() = component(this) 52 | } 53 | 54 | case class State(navExpanded: Boolean) 55 | 56 | case class Backend(scope: BackendScope[NavBar, State]) { 57 | var _isChanging = false 58 | 59 | def handleToggle() = { 60 | if (scope.props.onToggle.isDefined) { 61 | _isChanging = true 62 | scope.props.onToggle.get() 63 | _isChanging = false 64 | } 65 | 66 | scope.modState(s => s.copy(navExpanded = !s.navExpanded)) 67 | } 68 | 69 | def isNavExpanded = { 70 | if (scope.props.navExpanded.isDefined) 71 | scope.props.navExpanded.get 72 | else 73 | scope.state.navExpanded 74 | } 75 | } 76 | 77 | 78 | override val component = ReactComponentB[NavBar]("NavBar") 79 | .initialStateP(P => State(navExpanded = P.defaultNavExpanded.getOrElse(false))) 80 | .backend(new Backend(_)) 81 | .render((P, C, S, B) => { 82 | 83 | def renderHeader(): TagMod = { 84 | val brand: TagMod = 85 | if (P.brand.isDefined) { 86 | if (React.isValidElement(P.brand.get)) { 87 | cloneWithProps(P.brand.get, (undefined, undefined), Map("addClasses" -> "navbar-brand")) 88 | } 89 | else 90 | <.span(^.className := "navbar-brand", P.brand.get) 91 | } 92 | else 93 | EmptyTag 94 | 95 | val toggleButton = if (P.toggleButton.isDefined || P.toggleNavKey.isDefined) 96 | renderToggleButton() 97 | else 98 | EmptyTag 99 | 100 | <.div(^.className := "navbar-header", 101 | brand, 102 | toggleButton 103 | ) 104 | } 105 | 106 | def renderChild(child: ReactNode, index: Int) = { 107 | val dynChild = child.asInstanceOf[js.Dynamic] 108 | val childProps = dynChild.props 109 | 110 | val keyAndRef = getChildKeyAndRef2(child, index) 111 | cloneWithProps(child, keyAndRef, Map( 112 | "navbar" -> true, 113 | "collapsable" -> (P.toggleNavKey.isDefined && P.toggleNavKey.get == childProps.eventKey), 114 | "expanded" -> (P.toggleNavKey.isDefined && P.toggleNavKey.get == childProps.eventKey && B.isNavExpanded) 115 | )) 116 | } 117 | 118 | def renderToggleButton(): TagMod = { 119 | if (P.toggleButton.isDefined) { 120 | if (React.isValidElement(P.toggleButton)) 121 | ReactCloneWithProps(P.toggleButton.get, Map[String, js.Any]("className" -> "navbar-toggle", 122 | "onClick" -> (() => B.handleToggle()) // FIXME createChainedFunction0(B.handleToggle(_), P.toggleButton.props.onClick)) 123 | )) 124 | else { 125 | val children: TagMod = if (P.toggleButton.isDefined) 126 | P.toggleButton.get 127 | else 128 | Seq( 129 | <.span(^.className := "sr-only", ^.key := 0, "Toggle navigation"), 130 | <.span(^.className := "icon-bar", ^.key := 1), 131 | <.span(^.className := "icon-bar", ^.key := 2), 132 | <.span(^.className := "icon-bar", ^.key := 3) 133 | ) 134 | 135 | <.button(^.className := "navbar-toggle", ^.tpe := "botton", ^.onClick --> B.handleToggle)(children) 136 | } 137 | } 138 | else 139 | EmptyTag 140 | } 141 | 142 | 143 | 144 | val classes = P.bsClassSet ++ Map( 145 | "navbar-fixed-top" -> P.fixedTop.getOrElse(false), 146 | "navbar-fixed-bottom" -> P.fixedBottom.getOrElse(false), 147 | "navbar-static-top" -> P.staticTop.getOrElse(false), 148 | "navbar-inverse" -> P.inverse.getOrElse(false)) 149 | 150 | val header: TagMod = 151 | if (P.brand.isDefined || P.toggleButton.isDefined || P.toggleNavKey.isDefined) 152 | renderHeader() 153 | else 154 | EmptyTag 155 | 156 | val className = if (P.fluid.getOrElse(false)) "container-fluid" else "container" 157 | if (P.componentClass.isDefined && P.componentClass.get != "Nav") { 158 | val componentClass = P.componentClass.get.reactTag 159 | componentClass(^.classSet1M(P.addClasses, classes), ^.role := P.role, 160 | <.div(^.className := className, 161 | header, 162 | ValidComponentChildren.map(C, renderChild) 163 | ) 164 | ) 165 | } 166 | else { 167 | val addClasses = classes.filter(_._2).map(_._1).mkString(" ") 168 | Nav.Nav(addClasses = s"$addClasses ${P.addClasses}".trim, role = P.role)( 169 | <.div(^.className := className, 170 | header, 171 | ValidComponentChildren.map(C, renderChild) 172 | ) 173 | ) 174 | } 175 | 176 | } 177 | ).build 178 | 179 | } 180 | 181 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/NavItem.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.logger._ 4 | import com.acework.js.utils.{Mappable, Mergeable} 5 | import japgolly.scalajs.react._ 6 | import japgolly.scalajs.react.vdom.prefix_<^._ 7 | 8 | import scala.scalajs.js.{UndefOr, undefined} 9 | 10 | /** 11 | * Created by weiyin on 09/03/15. 12 | */ 13 | object NavItem extends BootstrapComponent { 14 | override type P = NavItem 15 | override type S = Unit 16 | override type B = Unit 17 | override type N = TopNode 18 | 19 | override def defaultProps = NavItem() 20 | 21 | case class NavItem(active: Boolean = false, disabled: Boolean = false, 22 | href: UndefOr[String] = "#", 23 | eventKey: UndefOr[String] = undefined, 24 | title: UndefOr[String] = undefined, 25 | target: UndefOr[String] = undefined, 26 | onSelect: UndefOr[Seq[UndefOr[String]] => Unit] = undefined, 27 | addClasses: String = "") extends MergeableProps[NavItem] { 28 | 29 | def merge(t: Map[String, Any]): NavItem = implicitly[Mergeable[NavItem]].merge(this, t) 30 | 31 | def asMap: Map[String, Any] = implicitly[Mappable[NavItem]].toMap(this) 32 | 33 | def apply(children: ReactNode*) = component(this, children) 34 | 35 | def apply() = component(this) 36 | } 37 | 38 | override val component = ReactComponentB[NavItem]("NavItem") 39 | .render((P, C) => { 40 | 41 | def handleClick(e: ReactEvent): Unit = { 42 | P.onSelect.map { onSelect => 43 | e.preventDefault() 44 | if (!P.disabled) 45 | onSelect(Seq(P.eventKey, P.href, P.target)) 46 | } 47 | } 48 | 49 | val classes = Map("active" -> P.active, "disabled" -> P.disabled) 50 | 51 | var link = <.a(^.href := P.href, ^.title := P.title, ^.target := P.target, 52 | ^.ref := "anchor", ^.onClick ==> handleClick) 53 | 54 | if (P.href.getOrElse("NA") == "#") 55 | link = link(^.role := "button") 56 | 57 | <.li(^.classSet1M(P.addClasses, classes), 58 | link(C) 59 | ) 60 | } 61 | 62 | ).build 63 | 64 | } -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/OverlayMixin.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import japgolly.scalajs.react._ 4 | import org.scalajs.dom.raw.HTMLElement 5 | import org.scalajs.dom.{Node, document} 6 | 7 | 8 | /** 9 | * Created by weiyin on 17/03/15. 10 | */ 11 | 12 | trait OverlayContainer { 13 | // Provide `getDOMNode` fn mocking a React component API. The `document.body` 14 | // reference needs to be contained within this function so that it is not accessed 15 | // in environments where it would not be defined, e.g. nodejs. Equally this is needed 16 | // before the body is defined where `document.body === null`, this ensures 17 | // `document.body` is only accessed after componentDidMount. 18 | def getDOMNode: HTMLElement = document.body 19 | } 20 | 21 | class ReferencedContainer[P, S](ref: RefSimple[TopNode], scope: BackendScope[P, S]) extends OverlayContainer { 22 | override def getDOMNode = { 23 | val refNode = ref(scope) 24 | if (refNode != null) 25 | refNode.get.getDOMNode() 26 | else 27 | super.getDOMNode 28 | } 29 | } 30 | 31 | trait OverlayProps { 32 | val container: OverlayContainer 33 | } 34 | 35 | trait OverlayMixin[P <: OverlayProps, S] { 36 | 37 | val scope: BackendScope[P, S] 38 | 39 | var _overlayTarget: Option[Node] = None 40 | var _overlayInstance: Option[ReactComponentM_[TopNode]] = None 41 | 42 | def onComponentWillUnmount() = { 43 | _unrenderOverlay() 44 | _overlayTarget match { 45 | case Some(target) => 46 | getContainerDOMNode.removeChild(target) 47 | _overlayTarget = None 48 | case None => 49 | } 50 | } 51 | 52 | def onComponentDidUpdate() = { 53 | _renderOverlay() 54 | } 55 | 56 | def onComponentDidMount() = { 57 | _renderOverlay() 58 | } 59 | 60 | def _renderOverlay() = { 61 | _overlayTarget match { 62 | case None => 63 | _mountOverlayTarget() 64 | case Some(_) => 65 | } 66 | 67 | renderOverlay() match { 68 | case Some(overlay) => 69 | _overlayInstance = Some(React.render(overlay, _overlayTarget.get)) 70 | case None => 71 | // unreder if the componet is null or transitions to null 72 | _unrenderOverlay() 73 | } 74 | } 75 | 76 | def _unrenderOverlay() = { 77 | _overlayTarget map { target => 78 | React.unmountComponentAtNode(target) 79 | _overlayTarget = None 80 | } 81 | } 82 | 83 | def _mountOverlayTarget() = { 84 | _overlayTarget = Some(document.createElement("div")) 85 | getContainerDOMNode.appendChild(_overlayTarget.get) 86 | } 87 | 88 | def getOverlayDOMNode: Option[TopNode] = { 89 | var node: Option[TopNode] = None 90 | if (scope.isMounted()) { 91 | if (_overlayInstance.isDefined) 92 | node = Some(_overlayInstance.get.getDOMNode()) 93 | } 94 | node 95 | } 96 | 97 | def getContainerDOMNode: HTMLElement = { 98 | scope.props.container.getDOMNode 99 | } 100 | 101 | def renderOverlay(): Option[ReactElement] 102 | 103 | } 104 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/PageHeader.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import japgolly.scalajs.react._ 4 | import japgolly.scalajs.react.vdom.prefix_<^._ 5 | 6 | 7 | /** 8 | * Created by weiyin on 10/03/15. 9 | */ 10 | object PageHeader extends BootstrapComponent { 11 | override type P = PageHeader 12 | override type S = Unit 13 | override type B = Unit 14 | override type N = TopNode 15 | 16 | override def defaultProps = PageHeader() 17 | 18 | case class PageHeader(addClasses: String = "") { 19 | 20 | def apply(children: ReactNode*) = component(this, children) 21 | 22 | def apply() = component(this) 23 | } 24 | 25 | override val component = ReactComponentB[PageHeader]("PageHeader") 26 | .render { (P, C) => 27 | <.div(^.classSet1(P.addClasses, "page-header" -> true), 28 | <.h1(C)) 29 | }.build 30 | 31 | } 32 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/PageItem.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.utils.{Mappable, Mergeable} 4 | import japgolly.scalajs.react._ 5 | import japgolly.scalajs.react.vdom.prefix_<^._ 6 | 7 | import scala.scalajs.js 8 | import scala.scalajs.js.{UndefOr, undefined} 9 | 10 | /** 11 | * Created by weiyin on 10/03/15. 12 | */ 13 | object PageItem extends BootstrapComponent { 14 | override type P = PageItem 15 | override type S = Unit 16 | override type B = Unit 17 | override type N = TopNode 18 | 19 | override def defaultProps = PageItem() 20 | 21 | case class PageItem(href: UndefOr[String] = "#", 22 | title: UndefOr[String] = undefined, 23 | target: UndefOr[String] = undefined, 24 | disabled: UndefOr[Boolean] = undefined, 25 | previous: UndefOr[Boolean] = undefined, 26 | next: UndefOr[Boolean] = undefined, 27 | eventKey: UndefOr[js.Any] = undefined, 28 | onSelect: UndefOr[(js.Any, String, String) => Unit] = undefined, addClasses: String = "") 29 | extends MergeableProps[PageItem] { 30 | 31 | def merge(t: Map[String, Any]): PageItem = implicitly[Mergeable[PageItem]].merge(this, t) 32 | 33 | def asMap: Map[String, Any] = implicitly[Mappable[PageItem]].toMap(this) 34 | 35 | def apply(children: ReactNode*) = component(this, children) 36 | 37 | def apply() = component(this) 38 | } 39 | 40 | override val component = ReactComponentB[PageItem]("PageItem") 41 | .render { (P, C) => 42 | 43 | def handleSelect(e: ReactEvent) = { 44 | if (P.onSelect.isDefined) { 45 | e.preventDefault() 46 | 47 | if (!P.disabled.getOrElse(false)) { 48 | P.onSelect.get(P.eventKey.get, P.href.get, P.target.get) 49 | } 50 | } 51 | } 52 | 53 | val classes = Map( 54 | "disabled" -> P.disabled.getOrElse(false), 55 | "previous" -> P.previous.getOrElse(false), 56 | "next" -> P.next.getOrElse(false) 57 | ) 58 | 59 | <.li(^.classSet1M(P.addClasses, classes), 60 | <.a(^.href := P.href, ^.title := P.title, ^.target := P.target, ^.onClick ==> handleSelect, 61 | C) 62 | ) 63 | }.build 64 | } 65 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/Pager.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.components.bootstrap.Utils._ 4 | import com.acework.js.utils.{Mappable, Mergeable} 5 | import japgolly.scalajs.react._ 6 | import japgolly.scalajs.react.vdom.prefix_<^._ 7 | 8 | import scala.scalajs.js.{UndefOr, undefined} 9 | 10 | /** 11 | * Created by weiyin on 10/03/15. 12 | */ 13 | object Pager extends BootstrapComponent { 14 | override type P = Pager 15 | override type S = Unit 16 | override type B = Unit 17 | override type N = TopNode 18 | 19 | override def defaultProps = Pager() 20 | 21 | case class Pager(onSelect: UndefOr[Seq[UndefOr[String]] => Unit] = undefined, addClasses: String = "") 22 | extends MergeableProps[Pager] { 23 | 24 | def merge(t: Map[String, Any]): Pager = implicitly[Mergeable[Pager]].merge(this, t) 25 | 26 | def asMap: Map[String, Any] = implicitly[Mappable[Pager]].toMap(this) 27 | 28 | def apply(children: ReactNode*) = component(this, children) 29 | 30 | def apply() = component(this) 31 | } 32 | 33 | override val component = ReactComponentB[Pager]("Pager") 34 | .render { (P, C) => 35 | 36 | def getOnSelectProps(child: ReactNode): UndefOr[Seq[UndefOr[String]] => Unit] = { 37 | val childPropsAny = getChildProps[Any](child) 38 | childPropsAny match { 39 | case props: NavItem.NavItem => 40 | if (props.onSelect.isDefined) 41 | props.onSelect 42 | else 43 | P.onSelect 44 | case _ => undefined 45 | } 46 | } 47 | 48 | def renderPageItem(child: ReactNode, index: Int) = { 49 | val keyAndRef = getChildKeyAndRef2(child, index) 50 | val propsMap = Map("onSelect" -> getOnSelectProps(child)) 51 | cloneWithProps(child, keyAndRef, propsMap) 52 | } 53 | 54 | <.ul(^.classSet1(P.addClasses, "pager" -> true))( 55 | ValidComponentChildren.map(C, renderPageItem)) 56 | }.build 57 | } 58 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/PanelGroup.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.components.bootstrap.Utils._ 4 | import com.acework.js.utils.{Mappable, Mergeable} 5 | import japgolly.scalajs.react._ 6 | import japgolly.scalajs.react.vdom.prefix_<^._ 7 | 8 | import scala.scalajs.js 9 | import scala.scalajs.js.{UndefOr, undefined} 10 | 11 | /** 12 | * Created by weiyin on 10/03/15. 13 | */ 14 | object PanelGroup extends BootstrapComponent { 15 | override type P = PanelGroup 16 | override type S = State 17 | override type B = Backend 18 | override type N = TopNode 19 | 20 | override def defaultProps = PanelGroup() 21 | 22 | case class State(activeKey: UndefOr[String]) 23 | 24 | case class PanelGroup(activeKey: UndefOr[String] = undefined, 25 | accordion: UndefOr[Boolean] = undefined, 26 | collapsable: UndefOr[Boolean] = undefined, 27 | onSelect: UndefOr[(UndefOr[String]) => Unit] = undefined, 28 | defaultActiveKey: UndefOr[String] = undefined, 29 | bsClass: UndefOr[Classes.Value] = Classes.`panel-group`, 30 | bsStyle: UndefOr[Styles.Value] = undefined, 31 | bsSize: UndefOr[Sizes.Value] = undefined, 32 | addClasses: String = "") extends BsProps with MergeableProps[PanelGroup] { 33 | 34 | def merge(t: Map[String, Any]): PanelGroup = implicitly[Mergeable[PanelGroup]].merge(this, t) 35 | 36 | def asMap: Map[String, Any] = implicitly[Mappable[PanelGroup]].toMap(this) 37 | 38 | def apply(children: ReactNode*) = component(this, children) 39 | 40 | def apply() = component(this) 41 | } 42 | 43 | 44 | class Backend($: BackendScope[PanelGroup, State]) { 45 | def children: js.Any = $._props.asInstanceOf[js.Dynamic].children 46 | 47 | var isChanging: Boolean = false 48 | 49 | def handleSelect(e: ReactEvent, key: UndefOr[String]): Unit = { 50 | e.preventDefault() 51 | 52 | if ($.props.onSelect.isDefined) { 53 | isChanging = true 54 | $.props.onSelect.get(key) 55 | isChanging = false 56 | } 57 | var newKey: UndefOr[String] = key 58 | if ($.state.activeKey == key) 59 | newKey = undefined 60 | 61 | $.modState(s => s.copy(activeKey = newKey)) 62 | } 63 | 64 | def shouldComponentUpdate = { 65 | // defer any updates to this component during the onselect handler 66 | !isChanging 67 | } 68 | } 69 | 70 | override val component = ReactComponentB[PanelGroup]("PanelGroup") 71 | .initialStateP(P => State(P.defaultActiveKey)) 72 | .backend(new Backend(_)) 73 | .render { 74 | (P, C, S, B) => 75 | 76 | def renderPanel(child: ReactNode, index: Int) = { 77 | val activeKey = if (P.activeKey.isDefined) P.activeKey else S.activeKey 78 | 79 | val childPropsAny = getChildProps[Any](child) 80 | 81 | val (eventKey, bsStyle) = 82 | childPropsAny match { 83 | case props: Panel.Panel => 84 | (props.eventKey, props.bsStyle) 85 | 86 | case _ => (undefined, undefined) 87 | } 88 | 89 | var propsMap = Map[String, Any]("bsStyle" -> (if (bsStyle.isDefined) bsStyle else P.bsStyle)) 90 | 91 | val keyAndRef = getChildKeyAndRef2(child, index) 92 | if (P.accordion.getOrElse(false)) { 93 | propsMap ++= Map( 94 | "collapsable" -> true, 95 | "expanded" -> (eventKey.getOrElse("NA1") == activeKey.getOrElse("NA2")), 96 | "onSelect" -> B.handleSelect _) 97 | } 98 | 99 | cloneWithProps(child, keyAndRef, propsMap) 100 | } 101 | 102 | <.div(^.classSet1M(P.addClasses, P.bsClassSet))( 103 | ValidComponentChildren.map(C, renderPanel) 104 | ) 105 | } 106 | .shouldComponentUpdate((scope, _, _) => scope.backend.shouldComponentUpdate) 107 | .build 108 | 109 | } 110 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/Placements.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | /** 4 | * Created by weiyin on 17/03/15. 5 | */ 6 | object Placements extends Enumeration { 7 | val top, right, bottom, left = Value 8 | } 9 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/Popover.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.utils.{Mappable, Mergeable} 4 | import japgolly.scalajs.react._ 5 | import japgolly.scalajs.react.vdom.prefix_<^._ 6 | 7 | import scala.scalajs.js.{UndefOr, undefined} 8 | 9 | /** 10 | * Created by weiyin on 10/03/15. 11 | */ 12 | object Popover extends BootstrapComponent { 13 | override type P = Popover 14 | override type S = Unit 15 | override type B = Unit 16 | override type N = TopNode 17 | 18 | override def defaultProps = Popover() 19 | 20 | case class Popover(title: UndefOr[ReactNode] = undefined, 21 | placement: Placements.Value = Placements.right, 22 | positionLeft: UndefOr[Int] = undefined, 23 | positionTop: UndefOr[Int] = undefined, 24 | arrowOffsetLeft: UndefOr[Int] = undefined, 25 | arrowOffsetTop: UndefOr[Int] = undefined, 26 | bsClass: UndefOr[Classes.Value] = Classes.btn, 27 | bsStyle: UndefOr[Styles.Value] = Styles.default, 28 | bsSize: UndefOr[Sizes.Value] = undefined, 29 | addClasses: String = "") extends BsProps with MergeableProps[Popover] { 30 | 31 | def merge(t: Map[String, Any]): Popover = implicitly[Mergeable[Popover]].merge(this, t) 32 | 33 | def asMap: Map[String, Any] = implicitly[Mappable[Popover]].toMap(this) 34 | 35 | def apply(children: ReactNode*) = component(this, children) 36 | 37 | def apply() = component(this) 38 | } 39 | 40 | override val component = ReactComponentB[Popover]("Popover") 41 | .render { (P, C) => 42 | def renderTitle() = { 43 | <.h3(^.className := "popover-title", P.title) 44 | } 45 | 46 | val classes = Map("popover" -> true, 47 | P.placement.toString -> true, 48 | "in" -> (P.positionLeft.isDefined || P.positionTop.isDefined) 49 | ) 50 | 51 | <.div(^.classSet1M(P.addClasses, classes), ^.left := P.positionLeft, ^.top := P.positionTop, 52 | ^.display := "block", 53 | <.div(^.className := "arrow", ^.left := P.arrowOffsetLeft, ^.top := P.arrowOffsetTop), 54 | if (P.title.isDefined) renderTitle() else EmptyTag, 55 | <.div(^.className := "popover-content")(C) 56 | ) 57 | }.build 58 | 59 | } 60 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/ProgressBar.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.components.bootstrap.Utils._ 4 | import com.acework.js.utils.{Mappable, Mergeable} 5 | import japgolly.scalajs.react._ 6 | import japgolly.scalajs.react.vdom.prefix_<^._ 7 | 8 | import scala.scalajs.js.{UndefOr, undefined} 9 | 10 | /** 11 | * Created by weiyin on 10/03/15. 12 | */ 13 | object ProgressBar extends BootstrapComponent { 14 | override type P = ProgressBar 15 | override type S = Unit 16 | override type B = Unit 17 | override type N = TopNode 18 | 19 | override def defaultProps = ProgressBar() 20 | 21 | 22 | case class ProgressBar( 23 | min: UndefOr[Double] = 0, 24 | now: UndefOr[Double] = undefined, 25 | max: UndefOr[Double] = 100, 26 | label: UndefOr[ReactNode] = undefined, 27 | srOnly: UndefOr[Boolean] = undefined, 28 | striped: UndefOr[Boolean] = undefined, 29 | active: UndefOr[Boolean] = undefined, 30 | isChild: UndefOr[Boolean] = undefined, 31 | interpolateClass: UndefOr[ReactNode] = undefined, 32 | bsClass: UndefOr[Classes.Value] = Classes.`progress-bar`, 33 | bsStyle: UndefOr[Styles.Value] = Styles.default, 34 | bsSize: UndefOr[Sizes.Value] = undefined, 35 | addClasses: String = "") extends BsProps with MergeableProps[ProgressBar] { 36 | 37 | def merge(t: Map[String, Any]): ProgressBar = implicitly[Mergeable[ProgressBar]].merge(this, t) 38 | 39 | def asMap: Map[String, Any] = implicitly[Mappable[ProgressBar]].toMap(this) 40 | 41 | def apply(children: ReactNode*) = component(this, children) 42 | 43 | def apply() = component(this) 44 | } 45 | 46 | val component = ReactComponentB[ProgressBar]("Progressbar") 47 | .render((P, C) => { 48 | 49 | def getPercentage(now: Double, min: Double, max: Double): Double = 50 | Math.ceil((now - min) / (max - min) * 100) 51 | 52 | def renderLabel(percentage: Double): ReactNode = { 53 | if (P.interpolateClass.isDefined) { 54 | //val interpolateClass = P.interpolateClass.get.reactTag 55 | // TODO P.interpolateClass 56 | "" 57 | } 58 | else { 59 | if (P.label.isDefined) 60 | Interpolate.Interpolate(now = P.now, min = P.min, max = P.max, percent = percentage, 61 | bsStyle = P.bsStyle)(P.label.get) 62 | else 63 | Interpolate.Interpolate(now = P.now, min = P.min, max = P.max, percent = percentage, 64 | bsStyle = P.bsStyle)() 65 | } 66 | } 67 | 68 | def renderScreenReaderOnlyLabel(label: ReactNode) = { 69 | <.span(^.className := "sr-only", label) 70 | } 71 | 72 | def renderChildBar(child: ReactNode, index: Int) = { 73 | val keyAndRef = getChildKeyAndRef2(child, index) 74 | cloneWithProps(child, keyAndRef, Map("isChild" -> true)) 75 | } 76 | 77 | def renderProgressBar() = { 78 | val percentage = getPercentage(P.now.getOrElse(0), P.min.getOrElse(0), P.max.getOrElse(100)) 79 | var label: ReactNode = if (!React.isValidElement(P.label)) 80 | renderLabel(percentage) 81 | else 82 | P.label.getOrElse("") 83 | 84 | if (P.srOnly.getOrElse(false)) 85 | label = renderScreenReaderOnlyLabel(label) 86 | 87 | // FIXME spread props 88 | <.div(^.classSet1M(P.addClasses, P.bsClassSet), ^.role := "progpressbar", 89 | ^.width := s"$percentage%", ^.aria.valuenow := P.now, ^.aria.valuemin := P.min, ^.aria.valuemax := P.max, 90 | label 91 | ) 92 | } 93 | 94 | var classes = Map("progress" -> true) 95 | 96 | if (P.active.getOrElse(false)) { 97 | classes += ("progress-striped" -> true) 98 | classes += ("active" -> true) 99 | } else if (P.striped.getOrElse(false)) 100 | classes += ("progress-striped" -> true) 101 | 102 | if (!ValidComponentChildren.hasValidComponents(C)) { 103 | if (!P.isChild.getOrElse(false)) { 104 | // FIXME spread props 105 | <.div(^.classSet1M(P.addClasses, classes), 106 | renderProgressBar() 107 | ) 108 | } 109 | else 110 | renderProgressBar() 111 | } 112 | else 113 | <.div(^.classSet1M(P.addClasses, classes), 114 | ValidComponentChildren.map(C, renderChildBar) 115 | ) 116 | 117 | }).build 118 | 119 | } 120 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/Row.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import japgolly.scalajs.react._ 4 | import japgolly.scalajs.react.vdom.prefix_<^._ 5 | 6 | /** 7 | * Created by weiyin on 10/03/15. 8 | */ 9 | object Row extends BootstrapComponent { 10 | override type P = Row 11 | override type S = Unit 12 | override type B = Unit 13 | override type N = TopNode 14 | 15 | override def defaultProps = Row() 16 | 17 | case class Row(componentClass: String = "div", addClasses: String = "") { 18 | 19 | def apply(children: ReactNode*) = component(this, children) 20 | 21 | def apply() = component(this) 22 | } 23 | 24 | override val component = ReactComponentB[Row]("Row") 25 | .render { (P, C) => 26 | val componentClass = P.componentClass.reactTag 27 | componentClass(^.classSet1(P.addClasses, "row" -> true))(C) 28 | }.build 29 | 30 | } 31 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/Sizes.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | /** 4 | * Created by weiyin on 09/03/15. 5 | */ 6 | object Sizes extends Enumeration { 7 | val lg, md, sm, xs = Value 8 | } 9 | 10 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/SplitButton.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.utils.{Mappable, Mergeable} 4 | import japgolly.scalajs.react.Ref 5 | 6 | /** 7 | * Created by weiyin on 11/03/15. 8 | */ 9 | 10 | import japgolly.scalajs.react._ 11 | import japgolly.scalajs.react.vdom.prefix_<^._ 12 | 13 | import scala.scalajs.js.{UndefOr, undefined} 14 | 15 | object SplitButton extends BootstrapComponent { 16 | override type P = SplitButton 17 | override type S = DropdownState 18 | override type B = Backend 19 | override type N = TopNode 20 | 21 | override def defaultProps = SplitButton() 22 | 23 | case class SplitButton( 24 | /*== start react bootstraps ==*/ 25 | id: UndefOr[String] = undefined, 26 | pullRight: UndefOr[Boolean] = undefined, 27 | title: UndefOr[ReactNode] = undefined, 28 | href: UndefOr[String] = undefined, 29 | target: UndefOr[String] = undefined, 30 | dropdownTitle: UndefOr[String] = "Toggle dropdown", 31 | onClick: UndefOr[(ReactEvent) => Unit] = undefined, 32 | onSelect: UndefOr[(String) => Unit] = undefined, 33 | disabled: UndefOr[Boolean] = undefined, 34 | dropup: UndefOr[Boolean] = undefined, 35 | /*== end react bootstraps ==*/ 36 | bsClass: UndefOr[Classes.Value] = Classes.btn, 37 | bsStyle: UndefOr[Styles.Value] = Styles.default, 38 | bsSize: UndefOr[Sizes.Value] = undefined, 39 | addClasses: String = "") 40 | extends BsProps with MergeableProps[SplitButton] { 41 | 42 | def merge(t: Map[String, Any]): SplitButton = implicitly[Mergeable[SplitButton]].merge(this, t) 43 | 44 | def asMap: Map[String, Any] = implicitly[Mappable[SplitButton]].toMap(this) 45 | 46 | def apply(children: ReactNode*) = component(this, children) 47 | 48 | def apply() = component(this) 49 | } 50 | 51 | class Backend(val scope: BackendScope[SplitButton, DropdownState]) extends DropdownStateMixin[SplitButton] { 52 | def handleButtonClick(e: ReactEvent) = { 53 | if (scope.state.open) 54 | setDropdownState(false) 55 | 56 | if (scope.props.onClick.isDefined) 57 | scope.props.onClick.get(e) // FIXME , scope.props.href, scope.props.target) 58 | } 59 | 60 | def handleOptionSelect(key: String): Unit = { 61 | if (scope.props.onSelect.isDefined) 62 | scope.props.onSelect.get(key) 63 | 64 | setDropdownState(false) 65 | } 66 | 67 | def handleDropdownClick(e: ReactEvent) = { 68 | e.preventDefault() 69 | setDropdownState(!scope.state.open) 70 | } 71 | } 72 | 73 | override val component = ReactComponentB[SplitButton]("SplitButton") 74 | .initialState(DropdownState(open = false)) 75 | .backend(new Backend(_)) 76 | .render((P, C, S, B) => { 77 | 78 | 79 | val buttonRef = Ref("button") 80 | val button = Button.withRef("button")(Button.Button(bsStyle = P.bsStyle, bsClass = P.bsClass, 81 | onClick = (e: ReactEvent) => B.handleButtonClick(e)), P.title.getOrElse(""): ReactNode) 82 | 83 | val dropdownButtonRef = Ref("dropdownButton") 84 | val dropdownButton = Button.withRef("dropdownButton")(Button.Button(bsStyle = P.bsStyle, bsClass = P.bsClass, addClasses = s"${P.addClasses} dropdown-toggle", 85 | onClick = (e: ReactEvent) => B.handleDropdownClick(e)), 86 | <.span(^.className := "sr-only", P.title), 87 | <.span(^.className := "caret")) 88 | 89 | val menuRef = Ref("menu") 90 | var groupClasses = "" 91 | if (S.open) 92 | groupClasses += " open" 93 | if (P.dropup.getOrElse(false)) 94 | groupClasses += " dropup" 95 | 96 | ButtonGroup(ButtonGroup.ButtonGroup(bsSize = P.bsSize, id = P.id, addClasses = groupClasses), button, dropdownButton, 97 | DropdownMenu.withRef("menu")(DropdownMenu.DropdownMenu(pullRight = P.pullRight, 98 | ariaLabelledBy = P.id, onSelect = (key: String) => B.handleOptionSelect(key)), C) 99 | ) 100 | 101 | }) 102 | .componentWillUnmount(_.backend.onComponentWillUnmount()) 103 | .build 104 | 105 | } 106 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/Styles.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | /** 4 | * Created by weiyin on 09/03/15. 5 | */ 6 | // Common Bootstrap contextual styles 7 | object Styles extends Enumeration { 8 | val default, primary, success, info, warning, danger, link, inline, tabs, pills, error = Value 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/SubNav.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.components.bootstrap.Utils._ 4 | import com.acework.js.utils.{Mappable, Mergeable} 5 | import japgolly.scalajs.react.Addons.ReactCloneWithProps 6 | import japgolly.scalajs.react._ 7 | import japgolly.scalajs.react.vdom.prefix_<^._ 8 | 9 | import scala.scalajs.js 10 | import scala.scalajs.js.{UndefOr, undefined} 11 | 12 | 13 | /** 14 | * Created by weiyin on 09/03/15. 15 | */ 16 | 17 | object SubNav extends BootstrapComponent { 18 | override type P = SubNav 19 | override type S = Unit 20 | override type B = Unit 21 | override type N = TopNode 22 | 23 | override def defaultProps = SubNav() 24 | 25 | case class SubNav(active: Boolean = true, 26 | disabled: Boolean = false, 27 | href: UndefOr[String] = undefined, 28 | target: UndefOr[String] = undefined, 29 | title: UndefOr[String] = undefined, 30 | text: UndefOr[String] = undefined, 31 | eventKey: UndefOr[String] = undefined, 32 | onSelect: UndefOr[(String) => Unit] = undefined, 33 | bsClass: UndefOr[Classes.Value] = Classes.nav, 34 | bsStyle: UndefOr[Styles.Value] = undefined, 35 | bsSize: UndefOr[Sizes.Value] = undefined, 36 | addClasses: String = "") extends BsProps with MergeableProps[SubNav] { 37 | 38 | def merge(t: Map[String, Any]): SubNav = implicitly[Mergeable[SubNav]].merge(this, t) 39 | 40 | def asMap: Map[String, Any] = implicitly[Mappable[SubNav]].toMap(this) 41 | 42 | def apply(children: ReactNode*) = component(this, children) 43 | 44 | def apply() = component(this) 45 | } 46 | 47 | // TODO 48 | def getChildActiveProp(child: ReactNode): Boolean = true 49 | 50 | override val component = ReactComponentB[SubNav]("SubNav") 51 | .render((P, C) => { 52 | 53 | val handleClick = (e: ReactEvent) => { 54 | if (P.onSelect.isDefined) { 55 | e.preventDefault() 56 | if (!P.disabled) 57 | P.onSelect.get(P.eventKey.getOrElse("")) // FIXME , P.href, P.target) 58 | } 59 | 60 | } 61 | 62 | def renderNavItem(child: ReactNode, idx: Int): ReactNode = { 63 | val keyAndRef = getChildKeyAndRef(child, idx) 64 | ReactCloneWithProps(child, keyAndRef ++ Map[String, js.Any]( 65 | "active" -> getChildActiveProp(child), 66 | "onSelect" -> P.onSelect.getOrElse(null) // FIXME create chain function 67 | ) 68 | ) 69 | } 70 | 71 | val classes = Map("active" -> P.active, "disabled" -> P.disabled) 72 | val anchorRef = Ref("anchor") 73 | <.li(^.classSet1M(P.addClasses, classes), 74 | <.a(^.href := P.href, ^.title := P.title, ^.target := P.target, 75 | ^.onClick ==> handleClick, ^.ref := anchorRef, P.text), 76 | <.ul(^.className := "nav", 77 | ValidComponentChildren.map(C, renderNavItem) 78 | ) 79 | ) 80 | } 81 | ).build 82 | 83 | } 84 | 85 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/TabPane.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.utils.{TransitionEvent, Mappable, Mergeable} 4 | import japgolly.scalajs.react._ 5 | import japgolly.scalajs.react.vdom.prefix_<^._ 6 | import org.scalajs.dom.Event 7 | 8 | import scala.scalajs.js 9 | import scala.scalajs.js.{UndefOr, undefined} 10 | import Utils._ 11 | 12 | /** 13 | * Created by weiyin on 22/03/15. 14 | */ 15 | object TabPane extends BootstrapComponent { 16 | override type P = TabPane 17 | override type S = State 18 | override type B = Backend 19 | override type N = TopNode 20 | 21 | override def defaultProps = TabPane() 22 | 23 | case class TabPane(active: Boolean = false, 24 | animation: Boolean = true, 25 | tab: UndefOr[String] = undefined, 26 | eventKey: UndefOr[String] = undefined, 27 | onAnimateOutEnd: UndefOr[() => Unit] = undefined, 28 | bsStyle: UndefOr[Styles.Value] = Styles.default, 29 | addClasses: String = "") extends MergeableProps[TabPane] { 30 | 31 | def merge(t: Map[String, Any]): TabPane = implicitly[Mergeable[TabPane]].merge(this, t) 32 | 33 | def asMap: Map[String, Any] = implicitly[Mappable[TabPane]].toMap(this) 34 | 35 | def apply(children: ReactNode*) = component(this, children) 36 | 37 | def apply() = component(this) 38 | 39 | } 40 | 41 | case class State(animateIn: Boolean, animateOut: Boolean) 42 | 43 | class Backend(scope: BackendScope[TabPane, State]) { 44 | 45 | def onComponentWillReceiveProps(nextProps: TabPane) = { 46 | if (scope.props.animation) { 47 | if (!scope.state.animateIn && nextProps.active && !scope.props.active) 48 | scope.modState(_.copy(animateIn = true)) 49 | else if (!scope.state.animateIn && !nextProps.active && scope.props.active) 50 | scope.modState(_.copy(animateOut = true)) 51 | } 52 | } 53 | 54 | def onComponentDidUpdate() = { 55 | if (scope.state.animateIn) 56 | js.timers.setTimeout(0)(startAnimateIn()) 57 | if (scope.state.animateOut) 58 | TransitionEvent.addEndEventListener( 59 | scope.getDOMNode(), (_: Event) => stopAnimateOut() 60 | ) 61 | } 62 | 63 | def startAnimateIn() = { 64 | if (scope.isMounted()) 65 | scope.modState(_.copy(animateIn = false)) 66 | } 67 | 68 | def stopAnimateOut() = { 69 | if (scope.isMounted()) 70 | scope.modState(_.copy(animateOut = false)) 71 | 72 | if (scope.props.onAnimateOutEnd.isDefined) 73 | scope.props.onAnimateOutEnd.get() 74 | } 75 | } 76 | 77 | override val component = ReactComponentB[TabPane]("TabPane") 78 | .initialState(State(animateIn = false, animateOut = false)) 79 | .backend(new Backend(_)) 80 | .render((P, C, S, B) => { 81 | val classes = Map( 82 | "active" -> (P.active || S.animateOut), 83 | "in" -> (P.active && !S.animateIn) 84 | ) 85 | // FIXME spread props 86 | <.div(^.classSet1M("tab-pane fade", classes), C) 87 | }) 88 | .componentWillReceiveProps((scope, nextProps) => { 89 | scope.backend.onComponentWillReceiveProps(nextProps) 90 | }) 91 | .componentDidUpdate((scope, prevProps, prevState) => scope.backend.onComponentDidUpdate()) 92 | .build 93 | 94 | } 95 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/TabbedArea.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.utils.{Mappable, Mergeable} 4 | import japgolly.scalajs.react._ 5 | import japgolly.scalajs.react.vdom.prefix_<^._ 6 | 7 | import scala.scalajs.js 8 | import scala.scalajs.js.{UndefOr, undefined} 9 | import Utils._ 10 | 11 | /** 12 | * Created by weiyin on 22/03/15. 13 | */ 14 | object TabbedArea extends BootstrapComponent { 15 | override type P = TabbedArea 16 | override type S = State 17 | override type B = Backend 18 | override type N = TopNode 19 | 20 | override def defaultProps = TabbedArea() 21 | 22 | 23 | case class TabbedArea(id: UndefOr[String] = undefined, 24 | activeKey: UndefOr[String] = undefined, 25 | animation: Boolean = true, 26 | defaultActiveKey: UndefOr[String] = undefined, 27 | onSelect: UndefOr[Seq[UndefOr[String]] => Unit] = undefined, 28 | bsStyle: UndefOr[Styles.Value] = Styles.tabs, 29 | addClasses: String = "") extends MergeableProps[TabbedArea] { 30 | 31 | def merge(t: Map[String, Any]): TabbedArea = implicitly[Mergeable[TabbedArea]].merge(this, t) 32 | 33 | def asMap: Map[String, Any] = implicitly[Mappable[TabbedArea]].toMap(this) 34 | 35 | def apply(children: ReactNode*) = component(this, children) 36 | 37 | def apply() = component(this) 38 | } 39 | 40 | case class State(activeKey: UndefOr[String] = undefined, previousActiveKey: UndefOr[String] = undefined) 41 | 42 | case class Backend(scope: BackendScope[TabbedArea, State]) { 43 | var _isChanging = false 44 | 45 | def handlePaneAnimateOutEnd() = { 46 | scope.modState(_.copy(previousActiveKey = undefined)) 47 | } 48 | 49 | def handleSelect(args: Seq[UndefOr[String]]): Unit = { 50 | val key = args.head 51 | if (scope.props.onSelect.isDefined) { 52 | _isChanging = true 53 | scope.props.onSelect.get(args) 54 | _isChanging = false 55 | } else if (key.get != getActiveKey) { 56 | scope.setState(State(key, getActiveKey)) 57 | } 58 | } 59 | 60 | def onComponentWillReceiveProps(nextProps: TabbedArea): Unit = { 61 | if (nextProps.activeKey.isDefined && nextProps.activeKey.get != scope.props.activeKey.getOrElse(false)) 62 | scope.modState(_.copy(previousActiveKey = scope.props.activeKey)) 63 | } 64 | 65 | def getActiveKey: String = { 66 | if (scope.props.activeKey.isDefined) 67 | scope.props.activeKey.get 68 | else 69 | scope.state.activeKey.getOrElse("") 70 | } 71 | 72 | def shouldComponentUpdate(): Boolean = !_isChanging 73 | } 74 | 75 | override val component = ReactComponentB[TabbedArea]("PanelGroup") 76 | // FIXME get default from children 77 | .initialStateP(P => State(P.defaultActiveKey)) 78 | .backend(new Backend(_)) 79 | .render( 80 | (P, C, S, B) => { 81 | val activeKey = if (P.activeKey.isDefined) P.activeKey else S.activeKey 82 | 83 | def getChildEventKey(child: ReactNode): UndefOr[String] = { 84 | val childPropsAny = getChildProps[Any](child) 85 | 86 | childPropsAny match { 87 | case props: TabPane.TabPane => 88 | props.eventKey 89 | case _ => undefined 90 | } 91 | } 92 | def getChildTab(child: ReactNode): UndefOr[String] = { 93 | val childPropsAny = getChildProps[Any](child) 94 | 95 | childPropsAny match { 96 | case props: TabPane.TabPane => 97 | props.tab 98 | case _ => undefined 99 | } 100 | } 101 | 102 | def renderTab(child: ReactNode, childTab: ReactNode): TagMod = { 103 | val eventKey = getChildEventKey(child) 104 | NavItem.withRef(s"tab$eventKey")(NavItem.NavItem(eventKey = eventKey), childTab) 105 | } 106 | 107 | def renderPane(child: ReactNode, index: Int) = { 108 | 109 | val eventKey = getChildEventKey(child) 110 | 111 | val keyAndRef = getChildKeyAndRef2(child, index) 112 | val active = eventKey.getOrElse("NA1") == activeKey.getOrElse("NA2") && 113 | (S.previousActiveKey.isEmpty || !P.animation) 114 | 115 | var propsMap = Map[String, Any]( 116 | "active" -> active, 117 | "animation" -> P.animation 118 | ) 119 | 120 | if (S.previousActiveKey.isDefined && eventKey.getOrElse("NA1") == S.previousActiveKey.get) 121 | propsMap += ("onAnimateOutEnd" -> B.handlePaneAnimateOutEnd _) 122 | 123 | cloneWithProps(child, keyAndRef, propsMap) 124 | } 125 | 126 | def renderTabIfSet(child: ReactNode, index: Int) = { 127 | val tab = getChildTab(child) 128 | if (tab.isDefined) 129 | renderTab(child, tab.get) 130 | else 131 | EmptyTag 132 | } 133 | 134 | val nav = Nav.withRef("tabs")(Nav.Nav(activeKey = activeKey, 135 | addClasses = s"nav-${P.bsStyle.toString}", 136 | onSelect = (args: Seq[UndefOr[String]]) => B.handleSelect(args)), 137 | ValidComponentChildren.map(C, renderTabIfSet) 138 | ) 139 | 140 | val panesRef = Ref("panes") 141 | <.div( 142 | nav, 143 | <.div(^.id := P.id, ^.className := "tab-content", ^.ref := panesRef, 144 | ValidComponentChildren.map(C, renderPane) 145 | ) 146 | ) 147 | }) 148 | .componentWillReceiveProps((scope, nextProps) => scope.backend.onComponentWillReceiveProps(nextProps)) 149 | .shouldComponentUpdate((scope, _, _) => scope.backend.shouldComponentUpdate()) 150 | .build 151 | } 152 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/Table.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import japgolly.scalajs.react._ 4 | import japgolly.scalajs.react.vdom.prefix_<^._ 5 | 6 | import scala.scalajs.js._ 7 | 8 | 9 | /** 10 | * Created by weiyin on 10/03/15. 11 | */ 12 | object Table extends BootstrapComponent { 13 | override type P = Table 14 | override type S = Unit 15 | override type B = Unit 16 | override type N = TopNode 17 | 18 | override def defaultProps = Table() 19 | 20 | case class Table(striped: UndefOr[Boolean] = undefined, 21 | bordered: UndefOr[Boolean] = undefined, 22 | condensed: UndefOr[Boolean] = undefined, 23 | hover: UndefOr[Boolean] = undefined, 24 | responsive: UndefOr[Boolean] = undefined, 25 | addClasses: String = "") { 26 | 27 | def apply(children: ReactNode*) = component(this, children) 28 | 29 | def apply() = component(this) 30 | } 31 | 32 | val component = ReactComponentB[Table]("Table") 33 | .stateless 34 | .render { (P, C, _) => 35 | val classes = Map( 36 | "table" -> true, 37 | "table-striped" -> P.striped.getOrElse(false), 38 | "table-bordered" -> P.bordered.getOrElse(false), 39 | "table-condensed" -> P.condensed.getOrElse(false), 40 | "table-hover" -> P.hover.getOrElse(false) 41 | ) 42 | 43 | val table = <.table(^.classSet1M(P.addClasses, classes))(C) 44 | 45 | if (P.responsive.getOrElse(false)) 46 | <.div(^.className := "table-responsive")(table) 47 | else 48 | table 49 | }.build 50 | 51 | } 52 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/Tooltip.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.utils.{Mappable, Mergeable} 4 | import japgolly.scalajs.react._ 5 | import japgolly.scalajs.react.vdom.prefix_<^._ 6 | 7 | import scala.scalajs.js.{UndefOr, undefined} 8 | 9 | /** 10 | * Created by weiyin on 09/03/15. 11 | */ 12 | object Tooltip extends BootstrapComponent { 13 | override type P = Tooltip 14 | override type S = Unit 15 | override type B = Unit 16 | override type N = TopNode 17 | 18 | override def defaultProps = Tooltip() 19 | 20 | case class Tooltip(placement: UndefOr[Placements.Value] = Placements.right, 21 | positionLeft: UndefOr[Int] = undefined, 22 | positionTop: UndefOr[Int] = undefined, 23 | arrowOffsetLeft: UndefOr[Int] = undefined, 24 | arrowOffsetTop: UndefOr[Int] = undefined, 25 | bsClass: UndefOr[Classes.Value] = Classes.panel, 26 | bsStyle: UndefOr[Styles.Value] = Styles.default, 27 | bsSize: UndefOr[Sizes.Value] = undefined, 28 | addClasses: String = "") extends BsProps with MergeableProps[Tooltip] { 29 | 30 | def merge(t: Map[String, Any]): Tooltip = implicitly[Mergeable[Tooltip]].merge(this, t) 31 | 32 | def asMap: Map[String, Any] = implicitly[Mappable[Tooltip]].toMap(this) 33 | 34 | def apply(children: ReactNode*) = component(this, children) 35 | 36 | def apply() = component(this) 37 | } 38 | 39 | override val component = ReactComponentB[Tooltip]("Tooltip") 40 | .render((P, C) => { 41 | val classes = Map(P.placement.get.toString -> true, 42 | "in" -> (P.positionLeft.isDefined || P.positionTop.isDefined) 43 | ) 44 | 45 | <.div(^.classSet1M("tooltip", classes), ^.left := P.positionLeft, ^.top := P.positionTop, 46 | <.div(^.className := "tooltip-arrow", ^.left := P.arrowOffsetLeft, ^.top := P.arrowOffsetTop), 47 | <.div(^.className := "tooltip-inner", C) 48 | ) 49 | }) 50 | .build 51 | 52 | } 53 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/Well.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components.bootstrap 2 | 3 | import com.acework.js.utils.{Mappable, Mergeable} 4 | import japgolly.scalajs.react._ 5 | import japgolly.scalajs.react.vdom.prefix_<^._ 6 | 7 | import scala.scalajs.js.{UndefOr, undefined} 8 | 9 | /** 10 | * Created by weiyin on 10/03/15. 11 | */ 12 | object Well extends BootstrapComponent { 13 | override type P = Well 14 | override type S = Unit 15 | override type B = Unit 16 | override type N = TopNode 17 | 18 | override def defaultProps = Well() 19 | 20 | case class Well(bsClass: UndefOr[Classes.Value] = Classes.well, 21 | bsStyle: UndefOr[Styles.Value] = undefined, 22 | bsSize: UndefOr[Sizes.Value] = undefined, 23 | addClasses: String = "") 24 | extends BsProps with MergeableProps[Well] { 25 | 26 | def merge(t: Map[String, Any]): Well = implicitly[Mergeable[Well]].merge(this, t) 27 | 28 | def asMap: Map[String, Any] = implicitly[Mappable[Well]].toMap(this) 29 | 30 | def apply(children: ReactNode*) = component(this, children) 31 | 32 | def apply() = component(this) 33 | } 34 | 35 | override val component = ReactComponentB[Well]("Well") 36 | .render { (P, C) => 37 | 38 | // TODO spread props 39 | <.div(^.classSet1M(P.addClasses, P.bsClassSet))(C) 40 | 41 | }.build 42 | 43 | } 44 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/components/bootstrap/package.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.components 2 | 3 | import scala.scalajs.js 4 | 5 | package object bootstrap extends js.GlobalScope { 6 | val jQuery: JQueryStatic = js.native 7 | 8 | import scala.language.implicitConversions 9 | 10 | implicit def jq2bootstrap(jq: JQuery): BootstrapJQuery = jq.asInstanceOf[BootstrapJQuery] 11 | 12 | } 13 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/logger/Log4JavaScript.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.logger 2 | 3 | import scala.annotation.elidable 4 | import scala.scalajs.js 5 | import scala.scalajs.js.annotation.JSName 6 | 7 | /** 8 | * Facade for functions in log4javascript that we need 9 | */ 10 | private[logger] trait Log4JavaScript extends js.Object { 11 | def getLogger(name:js.UndefOr[String]):JSLogger = js.native 12 | def setEnabled(enabled:Boolean):Unit = js.native 13 | def isEnabled:Boolean = js.native 14 | } 15 | 16 | @JSName("log4javascript.Level") 17 | private[logger] trait Level extends js.Object { 18 | val ALL:Level = js.native 19 | val TRACE:Level = js.native 20 | val DEBUG:Level = js.native 21 | val INFO:Level = js.native 22 | val WARN:Level = js.native 23 | val ERROR:Level = js.native 24 | val FATAL:Level = js.native 25 | } 26 | 27 | @JSName("log4javascript.Logger") 28 | private[logger] trait JSLogger extends js.Object { 29 | def addAppender(appender:Appender):Unit = js.native 30 | def removeAppender(appender:Appender):Unit = js.native 31 | def removeAllAppenders(appender:Appender):Unit = js.native 32 | def setLevel(level:Level):Unit = js.native 33 | def getLevel:Level = js.native 34 | def trace(msg:String, error:js.UndefOr[js.Error]):Unit = js.native 35 | def debug(msg:String, error:js.UndefOr[js.Error]):Unit = js.native 36 | def info(msg:String, error:js.UndefOr[js.Error]):Unit = js.native 37 | def warn(msg:String, error:js.UndefOr[js.Error]):Unit = js.native 38 | def error(msg:String, error:js.UndefOr[js.Error]):Unit = js.native 39 | def fatal(msg:String, error:js.UndefOr[js.Error]):Unit = js.native 40 | def trace(msg:String):Unit = js.native 41 | def debug(msg:String):Unit = js.native 42 | def info(msg:String):Unit = js.native 43 | def warn(msg:String):Unit = js.native 44 | def error(msg:String):Unit = js.native 45 | def fatal(msg:String):Unit = js.native 46 | } 47 | 48 | @JSName("log4javascript.Layout") 49 | private[logger] trait Layout extends js.Object 50 | 51 | @JSName("log4javascript.JsonLayout") 52 | private[logger] class JsonLayout extends Layout 53 | 54 | @JSName("log4javascript.Appender") 55 | private[logger] trait Appender extends js.Object { 56 | def setLayout(layout:Layout):Unit = js.native 57 | def setThreshold(level:Level):Unit = js.native 58 | } 59 | 60 | @JSName("log4javascript.BrowserConsoleAppender") 61 | private[logger] class BrowserConsoleAppender extends Appender 62 | 63 | @JSName("log4javascript.PopUpAppender") 64 | private[logger] class PopUpAppender extends Appender 65 | 66 | @JSName("log4javascript.AjaxAppender") 67 | private[logger] class AjaxAppender(url:String) extends Appender { 68 | def addHeader(header:String, value:String):Unit = js.native 69 | } 70 | 71 | private[logger] object Log4JavaScript extends js.GlobalScope { 72 | val log4javascript:Log4JavaScript = js.native 73 | } 74 | 75 | class L4JSLogger(jsLogger:JSLogger) extends Logger { 76 | 77 | private var ajaxAppender:AjaxAppender = null 78 | 79 | private def undefOrError(e:Exception):js.UndefOr[js.Error] = { 80 | if(e == null) 81 | js.undefined 82 | else 83 | e.asInstanceOf[js.Error] 84 | } 85 | 86 | override def trace(msg: String, e: Exception): Unit = jsLogger.trace(msg, undefOrError(e)) 87 | override def trace(msg: String): Unit = jsLogger.trace(msg) 88 | override def debug(msg: String, e: Exception): Unit = jsLogger.debug(msg, undefOrError(e)) 89 | override def debug(msg: String): Unit = jsLogger.debug(msg) 90 | override def info(msg: String, e: Exception): Unit = jsLogger.info(msg, undefOrError(e)) 91 | override def info(msg: String): Unit = jsLogger.info(msg) 92 | override def warn(msg: String, e: Exception): Unit = jsLogger.warn(msg, undefOrError(e)) 93 | override def warn(msg: String): Unit = jsLogger.warn(msg) 94 | override def error(msg: String, e: Exception): Unit = jsLogger.error(msg, undefOrError(e)) 95 | override def error(msg: String): Unit = jsLogger.error(msg) 96 | override def fatal(msg: String, e: Exception): Unit = jsLogger.fatal(msg, undefOrError(e)) 97 | override def fatal(msg: String): Unit = jsLogger.fatal(msg) 98 | 99 | override def enableServerLogging(url: String): Unit = { 100 | if(ajaxAppender == null) { 101 | ajaxAppender = new AjaxAppender(url) 102 | ajaxAppender.addHeader("Content-Type", "application/json") 103 | ajaxAppender.setLayout(new JsonLayout) 104 | jsLogger.addAppender(ajaxAppender) 105 | 106 | } 107 | } 108 | 109 | override def disableServerLogging():Unit = { 110 | if(ajaxAppender != null) { 111 | jsLogger.removeAppender(ajaxAppender) 112 | ajaxAppender = null 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/logger/LoggerFactory.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.logger 2 | 3 | import scala.annotation.elidable 4 | import scala.annotation.elidable._ 5 | 6 | trait Logger { 7 | /* 8 | * Use @elidable annotation to completely exclude functions from the compiler generated byte-code based on 9 | * the specified level. In a production build most logging functions will simply disappear with no runtime 10 | * performance penalty. 11 | * 12 | * Specify level as a compiler parameter 13 | * > scalac -Xelide-below INFO 14 | */ 15 | @elidable(FINEST) def trace(msg: String, e: Exception): Unit 16 | @elidable(FINEST) def trace(msg: String): Unit 17 | @elidable(FINE) def debug(msg: String, e: Exception): Unit 18 | @elidable(FINE) def debug(msg: String): Unit 19 | @elidable(INFO) def info(msg: String, e: Exception): Unit 20 | @elidable(INFO) def info(msg: String): Unit 21 | @elidable(WARNING) def warn(msg: String, e: Exception): Unit 22 | @elidable(WARNING) def warn(msg: String): Unit 23 | @elidable(SEVERE) def error(msg: String, e: Exception): Unit 24 | @elidable(SEVERE) def error(msg: String): Unit 25 | @elidable(SEVERE) def fatal(msg: String, e: Exception): Unit 26 | @elidable(SEVERE) def fatal(msg: String): Unit 27 | 28 | def enableServerLogging(url: String): Unit 29 | def disableServerLogging(): Unit 30 | } 31 | 32 | object LoggerFactory { 33 | private[logger] def createLogger(name: String) = {} 34 | 35 | lazy val consoleAppender = new BrowserConsoleAppender 36 | lazy val popupAppender = new PopUpAppender 37 | 38 | /** 39 | * Create a logger that outputs to browser console 40 | */ 41 | def getLogger(name: String): Logger = { 42 | val nativeLogger = Log4JavaScript.log4javascript.getLogger(name) 43 | nativeLogger.addAppender(consoleAppender) 44 | new L4JSLogger(nativeLogger) 45 | } 46 | 47 | /** 48 | * Create a logger that outputs to a separate popup window 49 | */ 50 | def getPopUpLogger(name: String): Logger = { 51 | val nativeLogger = Log4JavaScript.log4javascript.getLogger(name) 52 | nativeLogger.addAppender(popupAppender) 53 | new L4JSLogger(nativeLogger) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/logger/package.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js 2 | 3 | package object logger { 4 | private val defaultLogger = LoggerFactory.getLogger("Log") 5 | 6 | def log = defaultLogger 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/utils/EventListener.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.utils 2 | 3 | import org.scalajs.dom.raw.{Event, EventTarget, HTMLElement} 4 | 5 | import org.scalajs.dom.{raw, document} 6 | 7 | import scala.scalajs.js 8 | 9 | /** 10 | * Created by weiyin on 11/03/15. 11 | */ 12 | 13 | trait EventListener { 14 | def remove(): Unit 15 | } 16 | 17 | object EventListener { 18 | 19 | /** 20 | * Listen to DOM events during the bubble phase. 21 | * 22 | * @param target DOM element to register listener on. 23 | * @param eventType Event type, e.g. 'click' or 'mouseover'. 24 | * @param listener Callback function. 25 | * @return EventListener with a `remove` method. 26 | * @return 27 | */ 28 | def listen[T <: Event](target: EventTarget, eventType: String, listener: (T) => _) = { 29 | target.addEventListener(eventType, listener, false) 30 | 31 | new EventListener { 32 | override def remove() = { 33 | target.removeEventListener(eventType, listener.asInstanceOf[js.Function1[Event, _]], false) 34 | } 35 | } 36 | } 37 | 38 | // TODO implement attachEvent 39 | /* 40 | if (target.attachEvent) { 41 | target.attachEvent('on ' +eventType, callback); 42 | return { 43 | remove: function () { 44 | target.detachEvent('on ' +eventType, callback); 45 | } 46 | }; 47 | }*/ 48 | 49 | } 50 | -------------------------------------------------------------------------------- /core/src/main/scala/com/acework/js/utils/TransitionEvent.scala: -------------------------------------------------------------------------------- 1 | package com.acework.js.utils 2 | 3 | import org.scalajs.dom._ 4 | import org.scalajs.dom.raw.HTMLElement 5 | 6 | /** 7 | * Created by weiyin on 20/03/15. 8 | */ 9 | object TransitionEvent { 10 | 11 | val EVENT_NAME_MAP = Map( 12 | "transitioned" -> Map( 13 | "transition" -> "transitionend", 14 | "WebkitTransition" -> "webkitTransitionEnd", 15 | "MozTransition" -> "mozTransitionEnd", 16 | "OTransition" -> "oTransitionEnd", 17 | "msTransition" -> "MSTransitionEnd" 18 | ), 19 | "animationend" -> Map( 20 | "animation" -> "animationend", 21 | "WebkitAnimation" -> "webkitAnimationEnd", 22 | "MozAnimation" -> "mozAnimationEnd", 23 | "OAnimation" -> "oAnimationEnd", 24 | "msAnimation" -> "MSAnimationEnd" 25 | ) 26 | ) 27 | 28 | lazy val endEvents: List[String] = { 29 | try { 30 | var events: List[String] = Nil 31 | val testEl = document.createElement("div").asInstanceOf[HTMLElement] 32 | val style = testEl.style 33 | 34 | // On some platforms, in particular some releases of Android 4.x, 35 | // the un-prefixed "animation" and "transition" properties are defined on the 36 | // style object but the events that fire will still be prefixed, so we need 37 | // to check if the un-prefixed events are usable, and if not remove them 38 | // from the map 39 | val excludeAnimationEvent = !window.hasOwnProperty("AnimationEvent") 40 | 41 | val excludeTransition = !window.hasOwnProperty("TransitionEvent") 42 | 43 | for ((baseEventName, baseEvents) <- EVENT_NAME_MAP) { 44 | for ((styleName, eventName) <- baseEvents 45 | if style.hasOwnProperty(styleName) && 46 | !(excludeAnimationEvent && styleName == "animation") && 47 | !(excludeTransition && styleName == "transition")) { 48 | events = eventName :: events 49 | } 50 | } 51 | events 52 | } 53 | catch { 54 | case e: Throwable => 55 | Nil 56 | } 57 | } 58 | 59 | def addEventListener(node: EventTarget, eventName: String, listener: (Event) => _) = { 60 | node.addEventListener(eventName, listener, false) 61 | } 62 | 63 | def removeEventListener(node: EventTarget, eventName: String, listener: (Event) => _) = { 64 | node.removeEventListener(eventName, listener, false) 65 | } 66 | 67 | def addEndEventListener(node: EventTarget, listener: (Event) => _) = { 68 | if (endEvents.isEmpty) { 69 | // if CSS transitions are not supported, trigger an "end animation event immediately 70 | //window.setTimeout(listener.asInstanceOf[Function0[Any]], 0) 71 | window.setTimeout(() => listener(null), 0) 72 | } 73 | else { 74 | endEvents.foreach { endEvent => 75 | addEventListener(node, endEvent, listener) 76 | } 77 | } 78 | } 79 | 80 | def removeEndEventListener(node: EventTarget, listener: (Event) => _) = { 81 | endEvents.foreach { endEvent => 82 | removeEventListener(node, endEvent, listener) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /demo/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weiyinteo/scalajs-react-bootstrap/34512f77fe9524bc1bdd4518454861d6339cacf0/demo/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /demo/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weiyinteo/scalajs-react-bootstrap/34512f77fe9524bc1bdd4518454861d6339cacf0/demo/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /demo/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weiyinteo/scalajs-react-bootstrap/34512f77fe9524bc1bdd4518454861d6339cacf0/demo/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /demo/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weiyinteo/scalajs-react-bootstrap/34512f77fe9524bc1bdd4518454861d6339cacf0/demo/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |