1- package mhtml . examples
1+ package examples
22
3- import scala .scalajs .js
43import scala .xml .Node
5-
4+ import cats .implicits ._
5+ import mhtml .cats ._
66import mhtml ._
7- import org .scalajs .dom
8- import org .scalajs .dom .Event
97import org .scalajs .dom .KeyboardEvent
108import org .scalajs .dom .ext .KeyCode
119import org .scalajs .dom .raw .HTMLInputElement
1210
1311/** Typeclass for [[Chosen ]] select lists */
1412trait Searcheable [T ] {
1513 def show (t : T ): String
16- def isCandidate (query : String )(t : T ): Boolean =
17- show(t).toLowerCase().contains(query)
1814}
1915
2016object Searcheable {
17+ def apply [T ](implicit ev : Searcheable [T ]): Searcheable [T ] = ev
2118 def instance [T ](f : T => String ): Searcheable [T ] = new Searcheable [T ] {
2219 override def show (t : T ): String = f(t)
2320 }
@@ -29,124 +26,117 @@ object Searcheable {
2926object Chosen {
3027 def underline (toUnderline : String , query : String ): Node = {
3128 val index = toUnderline.toLowerCase.indexOf(query)
32- if (index == - 1 ) <span >{toUnderline}</span >
29+ if (index == - 1 ) <span >{ toUnderline }</span >
3330 else {
34- val before = toUnderline.substring(0 , index)
35- val after = toUnderline.substring(index + query.length)
31+ val before = toUnderline.substring(0 , index)
32+ val after = toUnderline.substring(index + query.length)
3633 val matched = toUnderline.substring(index, index + query.length)
37- <span >{before}<u >{matched}</u >{after}</span >
34+ <span >{ before }<u >{ matched }</u >{ after }</span >
3835 }
3936 }
4037
41- def singleSelect [T ](getCandidates : String => Rx [Seq [T ]],
42- placeholder : String = " " ,
43- maxCandidates : Int = 10 )(
44- implicit ev : Searcheable [T ]): (Node , Rx [Option [T ]]) = {
45- val id = " chosen-" + Math .random().toInt // to reference input dom
46- val rxFocused = Var (false )
47- val rxIndex = Var (0 )
48- val rxQuery = Var (" " )
49- val rxSelected = Var (Option .empty[T ])
50- def setQuery (value : String ): Unit = {
51- rxQuery := value
52- rxIndex := 0
53- rxFocused := true
54- }
55- def setCandidate (candidate : T ): Unit = {
56- rxSelected := Some (candidate)
57- rxFocused := false
58- dom.document.getElementById(id) match {
59- case input : HTMLInputElement => input.value = ev.show(candidate)
60- case _ =>
61- }
62- }
63- val rxCandidatesWithApp : Rx [(Node , Seq [T ])] = for {
64- query <- rxQuery
65- index <- rxIndex
66- allCandidates <- getCandidates(query)
67- queryLower = query.toLowerCase
68- } yield {
69- val candidates =
70- allCandidates.filter(ev.show(_).toLowerCase.contains(queryLower))
71- val toDrop = Math .max(0 , index - 3 )
72- val listItems =
73- candidates.zipWithIndex.slice(toDrop, toDrop + maxCandidates).map {
74- case (candidate, i) =>
75- val cssClass =
76- if (i == index) " chosen-highlight"
77- else " "
78- <li class ={cssClass}>
79- <a onclick ={() => setCandidate(candidate)}>
80- {underline(ev.show(candidate), queryLower)}
81- </a >
82- </li >
83- }
84- val itemsBefore : Node =
85- if (toDrop == 0 ) <span ></span >
86- else <li >{toDrop.toString} more items...</li >
87- val itemsAfter : Node = {
88- val remaining =
89- Math .max(0 , candidates.length - (toDrop + maxCandidates))
90- if (remaining == 0 ) <span ></span >
91- else <li >{remaining.toString} more items...</li >
38+ def singleSelect [T : Searcheable ](candidates : Rx [List [T ]], placeholder : String ): (Node , Rx [Option [T ]]) = {
39+ // These are the 5 streams of events involved in into this component.
40+ // By events, we mean that these are actually binded exactly once to
41+ // external sources (via :=). Given that scalac prohibits forward
42+ // references, and everything is composed functionally, this code is
43+ // guaranteed to have no infinite loops or race conditions.
44+
45+ val focusEvents : Var [Unit ] = Var (())
46+ val queryEvents : Var [String ] = Var (" " )
47+ val arrowPressedEvents : Var [Int ] = Var (- 1 ) // -1 → up; +1 → down
48+ val enterPressedEvents : Var [Unit ] = Var (())
49+ val clickSelectionEvents : Var [Option [T ]] = Var (None )
50+
51+ val maxCandidates : Int = 10
52+ val rxFilteredCandidates =
53+ (queryEvents |@| candidates).map { case (query, allCandidates) =>
54+ allCandidates.filter(Searcheable [T ].show(_).toLowerCase.contains(query.toLowerCase))
9255 }
93- val style = rxFocused.map { focused =>
94- val display = if (focused) " " else " display: none"
95- s " $display"
56+
57+ val rxIndex : Rx [Int ] = (
58+ arrowPressedEvents.map(Option (_)) |+|
59+ focusEvents.map(_ => None ) |@|
60+ rxFilteredCandidates
61+ ).map { case (event, filteredCandidates) =>
62+ (event, filteredCandidates.size - 1 )
63+ }.foldp(0 ) {
64+ case (last, (Some (delta), limit)) =>
65+ 0 max (last + delta) min limit
66+ case _ => 0 // This reset corresponds to an acquisition of focus.
9667 }
97- val div =
98- <div class =" chosen-options" >
99- <ul style ={style}>
100- {itemsBefore}
101- {listItems}
102- {itemsAfter}
68+
69+ val rxFocused : Rx [Boolean ] = // LOL scalafmt
70+ focusEvents.map(_ => true ) |+|
71+ queryEvents.map(_ => true ) |+|
72+ arrowPressedEvents.map(_ => true ) |+|
73+ enterPressedEvents.map(_ => false ) |+|
74+ clickSelectionEvents.map(_ => false )
75+
76+ val rxHighlightedCandidate : Rx [Option [T ]] =
77+ (rxFilteredCandidates |@| rxIndex).map { case (cands, index) =>
78+ cands.zipWithIndex.find(_._2 == index).map(_._1)
79+ }.keepIf(_.nonEmpty)(None )
80+
81+ val rxSelected : Rx [Option [T ]] =
82+ rxHighlightedCandidate.sampleOn(enterPressedEvents) |+| clickSelectionEvents
83+
84+ val rxChosenOptions : Rx [Node ] =
85+ (rxIndex |@| rxFocused |@| queryEvents |@| rxFilteredCandidates).map {
86+ case (index, focus, query, fcand) =>
87+ def bounds (i : Int ): Int = if (fcand.size > maxCandidates) i max 0 else 0
88+ val toDrop = bounds(index - 3 )
89+ val remain = bounds(fcand.size - (toDrop + maxCandidates))
90+ val style = if (focus) None else Some (" display: none" )
91+ val itemsBefore = if (toDrop == 0 ) None else Some (<li >{ toDrop } more items...</li >)
92+ val itemsAfter = if (remain == 0 ) None else Some (<li >{ remain } more items...</li >)
93+ val listItems =
94+ fcand.zipWithIndex.slice(toDrop, toDrop + maxCandidates).map { case (candidate, i) =>
95+ <li class ={ if (i == index) " chosen-highlight" else " " }>
96+ <a onclick ={ () => clickSelectionEvents := Some (candidate) }>
97+ { underline(Searcheable [T ].show(candidate), query.toLowerCase) }
98+ </a >
99+ </li >
100+ }
101+ <div class =" chosen-options" >
102+ <ul style ={ style }>
103+ { itemsBefore }
104+ { listItems }
105+ { itemsAfter }
103106 </ul >
104107 </div >
105- div -> candidates
106- }
107- val rxCandidates : Rx [Seq [T ]] = rxCandidatesWithApp.map(_._2)
108- val highlightedCandidate : Rx [T ] = {
109- val filtered = Var [T ](rxCandidates.value.head)
108+ }
110109
111- (for { index <- rxIndex; candidates <- rxCandidates } yield {
112- candidates.zipWithIndex.find(_._2 == index).map(_._1)
113- }).foreach(_.foreach(filtered.:= ))
110+ var cancelableSelectionHandler = Cancelable .empty
111+ def selectionHandler (node : HTMLInputElement ): Unit =
112+ cancelableSelectionHandler =
113+ rxSelected.impure.foreach(c => node.value = c.map(Searcheable [T ].show).getOrElse(" " ))
114114
115- filtered
116- }
117- // event handlers
118- val onkeyup = { e : KeyboardEvent =>
115+ def onkeydown (e : KeyboardEvent ): Unit =
119116 e.keyCode match {
120- case KeyCode .Up =>
121- rxIndex.update(x => Math .max(x - 1 , 0 ))
122- rxFocused := true
123- case KeyCode .Down =>
124- rxCandidates.foreach { candidates =>
125- rxIndex.update(x => Math .min(x + 1 , candidates.length - 1 ))
126- }.cancel()
127- rxFocused := true
128- case KeyCode .Enter =>
129- highlightedCandidate.foreach(setCandidate).cancel()
130- case _ =>
117+ case KeyCode .Up => arrowPressedEvents := - 1
118+ case KeyCode .Down => arrowPressedEvents := + 1
119+ case KeyCode .Enter => enterPressedEvents := (())
120+ case _ => ()
131121 }
132- ()
133- }
134- val onblur = { _ : Event =>
135- js.timers.setTimeout( 300 )(rxFocused := false )
136- ()
137- }
122+
123+ // @olfa: This implementation does not really makes sense as it's a tick
124+ // over the absolute clock. Proper implementation should start/cancel the
125+ // timeout on focus gained/lost.
126+ // def onblur(): Unit = { scala.scalajs.js.timers.setTimeout(1000)(rxFocused := false); () }
127+
138128 val app =
139129 <div class =" chosen-wrapper" >
140- < input type = " text"
141- id= {id}
142- placeholder= {placeholder}
143- class = " chosen-searchbar"
144- onblur= {onblur}
145- onfocus= {() => rxFocused := true }
146- onkeydown= {onkeyup}
147- oninput= {Utils .inputEvent(input => setQuery(input.value))}/>
148- {rxCandidatesWithApp.map(_._1)}
130+ < input type = " text" class = " chosen-searchbar"
131+ mhtml- onmount = { selectionHandler _ }
132+ mhtml- onunmount = { cancelableSelectionHandler.cancel _ }
133+ placeholder = { placeholder }
134+ onfocus = { () => focusEvents := (()) }
135+ onkeydown = { onkeydown _ }
136+ oninput = { Utils .inputEvent(e => queryEvents := e.value) }/>
137+ { rxChosenOptions }
149138 </div >
139+
150140 (app, rxSelected)
151141 }
152142}
0 commit comments