on master help sign in
Attempt to revamp the Graphics.Input library, much testing still needed
commit1c832af6b0d3176e76742f5397748b59cef0ddba authorEvan Czaplicki <info@elm-lang.org> dateMon 17 Feb 2014, 17:38 (13 years ago) parent8fbb9c7
4 files changed+435−398
libraries/Graphics/Element.elm+5−2 libraries/Graphics/Input.elm+153−159 libraries/Native/Graphics/Input.js+245−232 runtime/Render/Element.js+32−5
libraries/Graphics/Element.elm+5−2
@@ -53,7 +53,8 @@ type Properties = {
5353 color : Maybe Color,
5454 href : JSString,
5555 tag : JSString,
56− hover : ()
56+ hover : (),
57+ click : ()
5758 }
5859
5960type Element = { props : Properties, element : ElementPrim }
@@ -128,7 +129,9 @@ link href e = let p = e.props in
128129
129130emptyStr = JS.fromString ""
130131newElement w h e =
131− { props = Properties (Native.Utils.guid ()) w h 1 Nothing emptyStr emptyStr (), element = e }
132+ { props = Properties (Native.Utils.guid ()) w h 1 Nothing emptyStr emptyStr () ()
133+ , element = e
134+ }
132135
133136data ElementPrim
134137 = Image ImageStyle Int Int JSString
libraries/Graphics/Input.elm+153−159
@@ -1,199 +1,193 @@
11module Graphics.Input where
2−
32{-| This module is for creating standard input widgets such as buttons and
4−text boxes. In general, functions in this library return a signal representing
5−events from the user.
3+text fields. All functions in this library follow this general pattern:
4+
5+```haskell
6+-- (Signal (), SignalID ())
7+(clicks, clickID) = input ()
68
7−The simplest inputs are *one-way inputs*, meaning the user can update
8−them, but the programmer cannot. If you need to update an input from
9−within the program you want the slightly more complex *two-way inputs*.
10−This document will always show the one-way inputs first, *then* the
11−two-way inputs.
9+clickableYogi : Element
10+clickableYogi = clickable clickID () (image 40 40 "/yogi.jpg")
11+```
1212
13−# Buttons
14−@docs button, customButton, buttons, customButtons
13+Whenever the user clicks on the resulting `clickableYogi` element, it sends a
14+`()` to the `clicks` signal. Typically, you would create all your input signals
15+when setting up the signal graph, so then your graphics code can refer to them
16+easily.
1517
16−# Fields
17−@docs field, password, email, fields, FieldState, emptyFieldState
18+# Creating Inputs
19+@docs input
1820
19−# Checkboxes
20−@docs checkbox, checkboxes
21+# Basic Input Elements
22+Text fields come later.
23+@docs button, customButton, checkbox, dropDown
2124
22−# Drop Downs
23−@docs stringDropDown, dropDown
25+# Clicks and Hovers
26+@docs clickable, hoverable
2427
25−# Mouse Hover
26−@docs hoverable, hoverables
28+# Text Fields
29+@docs field, password, email, noContent, FieldContent, Selection, Direction
2730-}
2831
29−import Basics (String)
30−import Signal (Signal,lift,dropRepeats)
31−import Native.Graphics.Input
32−import List
32+import Signal (Signal)
3333import Graphics.Element (Element)
34−import Maybe (Maybe)
35−import JavaScript (JSString)
3634
37−id x = x
35+data SignalID a = SignalID
3836
39−{-| Create a group of buttons.
37+{-| This function is the key to using this library. It creates a signal that
38+input elements (buttens, checkboxes, etc.) can report to. It also create a
39+unique `SignalID` that allows input elements to refer to this particular
40+signal. It may be best to look at examples in the rest of the docs and return
41+to this function.
4042
41− * The first argument is the default value of the `events` signal.
42− * The `events` signal represents all of the activity in this group
43− of buttons.
44− * The `button` function creates a button
45− with the given name, like &ldquo;Submit&rdquo; or &ldquo;Cancel&rdquo;.
46− The `a` value is sent to `events` whenever the button is pressed.
43+Note: creating an input signal is an inherently imperative action, so this is
44+one of very few impure functions in Elm. It is designed such that it can only
45+be used as you build your signal graph at startup, keeping Elm pure at runtime.
4746-}
48−buttons : a -> { events : Signal a,
49− button : a -> String -> Element }
50−buttons = Native.Graphics.Input.buttons
47+input : a -> (Signal a, SignalID a)
48+input = Native.Graphics.Input.input
5149
52−{-| Create a button with a given label. The result is an `Element` and
53−a signal of units. This signal triggers whenever the button is pressed.
54−-}
55−button : String -> (Element, Signal ())
56−button txt =
57− let pool = buttons ()
58− in (pool.button () txt, pool.events)
59−
60−{-| Create a group of custom buttons.
61−
62− * The first argument is the default value of the `events` signal.
63− * The `events` signal represents all of the activity in this group
64− of custom buttons.
65− * The `customButton` function creates a button with three different visual
66− states, one for up, hovering, and down. The resulting button has dimensions
67− large enough to fit all three possible `Elements`.
68− The `a` value is sent to `events` whenever the button is pressed.
69−-}
70−customButtons : a -> { events : Signal a,
71− customButton : a -> Element -> Element -> Element -> Element }
72−customButtons = Native.Graphics.Input.customButtons
50+{-| Create a standard button. The following example begins making a basic
51+calculator:
7352
74−{-| Create a button with custom states for up, hovering, and down
75−(given in that order). The result is an `Element` and a signal of
76−units. This signal triggers whenever the button is pressed.
77−-}
78−customButton : Element -> Element -> Element -> (Element, Signal ())
79−customButton up hover down =
80− let pool = customButtons ()
81− in (pool.customButton () up hover down, pool.events)
82−
83−{-| Create a group of checkboxes.
84−
85− * The first argument is the default value of the `events` signal.
86− * The `events` signal represents all of the activity in this group
87− of checkboxes.
88− * The `checkbox` function creates a
89− checkbox with a given state. The `(Bool -> a)` function is used
90− when the checkbox is modified. It takes the new state and turns
91− it into a value that can be sent to `events`. For example, this
92− lets you add an ID to distinguish between checkboxes.
53+ data Input = Number Int | Plus | Minus | Clear
54+
55+ -- (Signal Input, SignalID Input)
56+ (calcInput, pressID) = input Clear
57+
58+ buttons : Element
59+ buttons =
60+ flow right [ button pressID (Number 1) "1"
61+ , button pressID (Number 2) "2"
62+ , button pressID Plus "+"
63+ ]
64+
65+If the user presses the "plus" button, the `calcInput` signal will update to
66+`Plus`. If the users presses "2", `calcInput` will update to `(Number 2)`.
9367-}
94−checkboxes : a -> { events : Signal a,
95− checkbox : (Bool -> a) -> Bool -> Element }
96−checkboxes = Native.Graphics.Input.checkboxes
97−
98−{-| Create a checkbox with a given start state. Unlike `button`, this
99−result is a *signal* of elements. That is because a checkbox has state
100−that updates based on user input. The boolean signal represents the
101−current state of the checkbox.
68+button : SignalID a -> a -> String -> Element
69+button = Native.Graphics.Input.button
70+
71+customButton : SignalID a -> a -> Element -> Element -> Element -> Element
72+customButton = Native.Graphics.Input.customButton
73+
74+checkbox : SignalID a -> (Bool -> a) -> Bool -> Element
75+checkbox = Native.Graphics.Input.checkboxes
76+
77+{-| Create a drop-down menu. The following drop-down lets you choose your
78+favorite British sport:
79+
80+ data Sport = Football | Cricket | Snooker
81+
82+ -- (Signal (Maybe Sport), SignalID (Maybe Sport))
83+ (dropStatus, dropID) = input Nothing
84+
85+ sportDropDown : Element
86+ sportDropDown =
87+ dropDown dropID
88+ [ ("" , Nothing)
89+ , ("Football", Just Football)
90+ , ("Cricket" , Just Cricket)
91+ , ("Snooker" , Just Snooker)
92+ ]
93+
94+If the user selects "Football" from the drop down menue, the `dropStatus`
95+signal will update to `Just Football`.
10296-}
103−checkbox : Bool -> (Signal Element, Signal Bool)
104−checkbox b =
105− let cbs = checkboxes b
106− in (lift (cbs.checkbox id) cbs.events, cbs.events)
107−
108−{-| Detect when the mouse is hovering over some elements. This
109−allows you to create and destroy elements dynamically and still
110−detect hover information.
97+dropDown : SignalID a -> [(String,a)] -> Element
98+dropDown = Native.Graphics.Input.dropDown
99+
100+{-| Detect mouse hovers over a specific `Element`. In the following example,
101+we will create a hoverable picture called `cat`.
102+
103+ -- (Signal Bool, SignalID Bool)
104+ (hover, hoverID) = input False
105+
106+ cat : Element
107+ cat = image 30 30 "/cat.jpg"
108+ |> hoverable hoverID (\hover -> hover)
109+
110+When the mouse hovers above the `cat` element, the `hover` signal will become
111+`True`. When the mouse leaves it, `hover` will become `False`.
111112-}
112−hoverables : a -> { events : Signal a,
113− hoverable : (Bool -> a) -> Element -> Element }
114−hoverables = Native.Graphics.Input.hoverables
113+hoverable : SignalID a -> (Bool -> a) -> Element -> Element
114+hoverable = Native.Graphics.Input.hoverable
115+
116+{-| Detect mouse clicks on a specific `Element`. In the following example,
117+we will create a clickable picture called `cat`.
115118
116−{-| Detect when the mouse is hovering over a specific `Element`. -}
117−hoverable : Element -> (Element, Signal Bool)
118−hoverable elem =
119− let pool = hoverables False
120− in (pool.hoverable id elem, pool.events)
119+ data Picture = Cat | Hat
121120
122−{-| Represents the current state of a text field. The `string` represents the
123−characters filling the text field. The `selectionStart` and `selectionEnd`
124−values represent what the user has selected with their mouse or keyboard.
125−For example:
121+ -- (Signal Picture, SignalID Picture)
122+ (click, clickID) = input Cat
126123
127− { string="She sells sea shells", selectionStart=3, selectionEnd=0 }
124+ cat : Element
125+ cat = image 30 30 "/cat.jpg"
126+ |> clickable clickID Cat
127+
128+ hat : Element
129+ hat = image 30 30 "/hat.jpg"
130+ |> clickable clickID Hat
131+
132+When the user clicks on the `cat` element, the `click` signal receives
133+an update containing the value `Cat`. When the user clicks on the `hat` element,
134+the `click` signal receives an update containing the value `Hat`. This lets you
135+distinguish which element was clicked. In a more complex example, they could be
136+distinguished with IDs or more complex data structures.
137+-}
138+clickable : SignalID a -> a -> Element -> Element
139+clickable = Native.Graphics.Input.clickable
140+
141+{-| Represents the current content of a text field. For example:
142+
143+ FieldContent "She sells sea shells" (Selection 0 3 Backward)
128144
129145This means the user highlighted the substring `"She"` backwards.
130146-}
131−type FieldState = { string:String, selectionStart:Int, selectionEnd:Int }
132−
133−{-| Create a group of text input fields.
134−
135− * The first argument is the default value of the `events` signal.
136− * The `events` signal represents all of the activity in this group
137− of text fields.
138− * The `field` function creates a
139− field with the given ghost text and initial field state.
140− When the field is modified, the `(FieldState -> a)` function
141− takes the new state and turns
142− it into a value that can be sent to `events`. For example, this
143− lets you add an ID to distinguish between input fields.
147+type FieldContent = { string:String, selection:Selection }
148+
149+{-| The selection within a text field. `start` is never greater than `end`:
150+
151+ Selection 0 0 Forward -- cursor precedes all characters
152+
153+ Selection 5 9 Backward -- highlighting characters starting after
154+ -- the 5th and ending after the 9th
144155-}
145−fields : a -> { events : Signal a,
146− field : (FieldState -> a) -> String -> FieldState -> Element }
147−fields = Native.Graphics.Input.fields
156+type Selection = { start:Int, end:Int, direction:Direction }
148157
149−{-| The empty field state:
158+{-| The direction of selection.-}
159+data Direction = Forward | Backward
150160
151− { string="", selectionStart=0, selectionEnd=0 }
161+{-| A field with no content:
162+
163+ FieldContent "" (Selection 0 0 Forward)
152164-}
153−emptyFieldState : FieldState
154−emptyFieldState = { string="", selectionStart=0, selectionEnd=0 }
165+noContent : FieldContent
166+noContent = FieldContent "" (Selection 0 0 Forward)
167+
168+{-| Create a text field. The following example creates a time-varying element
169+called `nameField`. As the user types their name, the field will be updated
170+to match what they have entered.
155171
156−{-| Create a field with the given default text. The output is an element
157−that updates to match the user input and a signal of strings representing
158−the content of the field.
172+ -- (Signal FieldContent, SignalID FieldContent)
173+ (nameContent,nameID) = input noContent
174+
175+ nameField : Signal Element
176+ nameField = field nameID (\content -> content) "Name" <~ nameContent
159177-}
160−field : String -> (Signal Element, Signal String)
161−field placeHolder =
162− let tfs = fields emptyFieldState
163− changes = dropRepeats tfs.events
164− in (lift (tfs.field id placeHolder) changes,
165− dropRepeats (lift .string changes))
178+field : SignalID a -> (FieldContent -> a) -> String -> FieldContent -> Element
179+field = Native.Graphics.Input.field
166180
167181{-| Same as `field` but the UI element blocks out each characters. -}
168−password : String -> (Signal Element, Signal String)
169−password placeHolder =
170− let tfs = Native.Graphics.Input.passwords emptyFieldState
171− changes = dropRepeats tfs.events
172− in (lift (tfs.field id placeHolder) changes,
173− dropRepeats (lift .string changes))
182+password : SignalID a -> (FieldContent -> a) -> String -> FieldContent -> Element
183+password = Native.Graphics.Input.password
174184
175185{-| Same as `field` but it adds an annotation that this field is for email
176186addresses. This is helpful for auto-complete and for mobile users who may
177187get a custom keyboard with an `@` and `.com` button.
178188-}
179−email : String -> (Signal Element, Signal String)
180−email placeHolder =
181− let tfs = Native.Graphics.Input.emails emptyFieldState
182− changes = dropRepeats tfs.events
183− in (lift (tfs.field id placeHolder) changes,
184− dropRepeats (lift .string changes))
185−
186−{-| Create a drop-down menu. When the user selects a string,
187−the current state of the drop-down is set to the associated
188−value. This lets you avoid manually mapping the string onto
189−functions and values.
190−-}
191−dropDown : [(String,a)] -> (Signal Element, Signal a)
192−dropDown = Native.Graphics.Input.dropDown
189+email : SignalID a -> (FieldContent -> a) -> String -> FieldContent -> Element
190+email = Native.Graphics.Input.email
193191
194−{-| Create a drop-down menu for selecting strings. The resulting
195−signal of strings represents the string that is currently selected.
196−-}
197−stringDropDown : [String] -> (Signal Element, Signal String)
198−stringDropDown strs =
199− dropDown (List.map (\s -> (s,s)) strs)
192+-- area : SignalID a -> (FieldContent -> a) -> SignalID b -> ((Int,Int) -> b) -> (Int,Int) -> String -> FieldContent -> Element
193+-- area = Native.Graphics.Input.area
libraries/Native/Graphics/Input.js+245−232
@@ -15,188 +15,184 @@ Elm.Native.Graphics.Input.make = function(elm) {
1515 var Utils = Elm.Native.Utils.make(elm);
1616 var Tuple2 = Utils.Tuple2;
1717
18− function dropDown(values) {
19− var entries = JS.fromList(values);
20− var events = Signal.constant(entries[0]._1);
21−
22− var drop = newNode('select');
23− drop.style.border = '0 solid';
24− for (var i = 0; i < entries.length; ++i) {
25− var option = newNode('option');
26− var name = JS.fromString(entries[i]._0);
27− option.value = name;
28− option.innerHTML = name;
29− drop.appendChild(option);
30− }
31− drop.addEventListener('change', function() {
32− elm.notify(events.id, entries[drop.selectedIndex]._1);
33− });
18+ function input(initialValue) {
19+ var sig = Signal.constant(initialValue);
20+ return Tuple2(sig, sig);
21+ }
3422
35− var t = drop.cloneNode(true);
36− t.style.visibility = "hidden";
23+ function renderDropDown(signal, values) {
24+ return function(_) {
25+ var entries = JS.fromList(values);
26+
27+ var drop = newNode('select');
28+ drop.style.border = '0 solid';
29+ for (var i = 0; i < entries.length; ++i) {
30+ var option = newNode('option');
31+ var name = JS.fromString(entries[i]._0);
32+ option.value = name;
33+ option.innerHTML = name;
34+ drop.appendChild(option);
35+ }
36+ drop.addEventListener('change', function() {
37+ elm.notify(signal.id, entries[drop.selectedIndex]._1);
38+ });
3739
38− elm.node.appendChild(t);
39− var style = window.getComputedStyle(t, null);
40− var w = Math.ceil(style.getPropertyValue("width").slice(0,-2) - 0);
41− var h = Math.ceil(style.getPropertyValue("height").slice(0,-2) - 0);
42− elm.node.removeChild(t);
43−
44− var element = A3(newElement, w, h, {
40+ var t = drop.cloneNode(true);
41+ t.style.visibility = "hidden";
42+
43+ elm.node.appendChild(t);
44+ var style = window.getComputedStyle(t, null);
45+ var w = Math.ceil(style.getPropertyValue("width").slice(0,-2) - 0);
46+ var h = Math.ceil(style.getPropertyValue("height").slice(0,-2) - 0);
47+ elm.node.removeChild(t);
48+ return drop;
49+ };
50+ }
51+
52+ function updateDropDown(node, oldModel, newModel) {
53+ }
54+
55+ function dropDown(signal, values) {
56+ return A3(newElement, w, h, {
4557 ctor: 'Custom',
4658 type: 'DropDown',
47− render: function render(model) { return drop; },
48− update: function update(node, oldModel, newModel) {},
59+ render: renderDropDown(signal,values),
60+ update: updateDropDown,
4961 model: {}
5062 });
51−
52− return Tuple2(Signal.constant(element), events);
5363 }
5464
55− function buttons(defaultValue) {
56− var events = Signal.constant(defaultValue);
57−
58− function render(model) {
59− var b = newNode('button');
60− b.style.display = 'block';
61− b.elmEvent = model.event;
62− function click() { elm.notify(events.id, b.elmEvent); }
63− b.addEventListener('click', click);
64− b.innerHTML = model.text;
65− return b;
66− }
67−
68− function update(node, oldModel, newModel) {
69− node.elmEvent = newModel.event;
70− var txt = newModel.text;
71− if (oldModel.text !== txt) node.innerHTML = txt;
72− }
73−
74− function button(evnt, txt) {
75− return A3(newElement, 100, 40, {
76− ctor: 'Custom',
77− type: 'Button',
78− render: render,
79− update: update,
80− model: { event:evnt, text:JS.fromString(txt) }
81− });
65+ function renderButton(model) {
66+ var node = newNode('button');
67+ node.style.display = 'block';
68+ node.elm_signal = model.signal;
69+ node.elm_value = model.value;
70+ function click() {
71+ elm.notify(node.elm_signal.id, node.elm_value);
8272 }
73+ node.addEventListener('click', click);
74+ node.innerHTML = model.text;
75+ return node;
76+ }
8377
84− return { _:{}, button:F2(button), events:events };
78+ function updateButton(node, oldModel, newModel) {
79+ node.elm_signal = newModel.signal;
80+ node.elm_value = nemModel.value;
81+ var txt = newModel.text;
82+ if (oldModel.text !== txt) node.innerHTML = txt;
8583 }
8684
87− function customButtons(defaultValue) {
88− var events = Signal.constant(defaultValue);
85+ function button(signal, value, text) {
86+ return A3(newElement, 100, 40, {
87+ ctor: 'Custom',
88+ type: 'Button',
89+ render: renderButton,
90+ update: updateButton,
91+ model: { signal:signal, value:value, text:JS.fromString(text) }
92+ });
93+ }
8994
90− function render(model) {
91− var btn = newNode('div');
92− btn.elmEvent = model.event;
95+ function renderCustomButton(model) {
96+ var btn = newNode('div');
97+ btn.elm_value = model.value;
9398
94− btn.elmUp = Render.render(model.up);
95− btn.elmHover = Render.render(model.hover);
96− btn.elmDown = Render.render(model.down);
99+ btn.elm_up = Render.render(model.up);
100+ btn.elm_hover = Render.render(model.hover);
101+ btn.elm_down = Render.render(model.down);
97102
98− function replace(node) {
99− if (node !== btn.firstChild) btn.replaceChild(node, btn.firstChild);
100− }
101− var overCount = 0;
102− function over(e) {
103− if (overCount++ > 0) return;
104− replace(btn.elmHover);
105− }
106− function out(e) {
107− if (btn.contains(e.toElement || e.relatedTarget)) return;
108− overCount = 0;
109− replace(btn.elmUp);
110− }
111− function up() {
112− replace(btn.elmHover);
113− elm.notify(events.id, btn.elmEvent);
103+ function replace(node) {
104+ if (node !== btn.firstChild) {
105+ btn.replaceChild(node, btn.firstChild);
114106 }
115− function down() { replace(btn.elmDown); }
116− btn.addEventListener('mouseover', over);
117− btn.addEventListener('mouseout' , out);
118− btn.addEventListener('mousedown', down);
119− btn.addEventListener('mouseup' , up);
120−
121− btn.appendChild(btn.elmUp);
122−
123− var clicker = newNode('div');
124− clicker.style.width = btn.elmUp.style.width;
125− clicker.style.height = btn.elmUp.style.height;
126− clicker.style.position = 'absolute';
127− clicker.style.top = 0;
128− btn.appendChild(clicker);
129−
130− return btn;
131107 }
132−
133− function update(node, oldModel, newModel) {
134− node.elmEvent = newModel.event;
135− Render.update(node.elmUp, oldModel.up, newModel.up)
136− Render.update(node.elmHover, oldModel.hover, newModel.hover)
137− Render.update(node.elmDown, oldModel.down, newModel.down)
108+ var overCount = 0;
109+ function over(e) {
110+ if (overCount++ > 0) return;
111+ replace(btn.elm_hover);
138112 }
139−
140− function button(evnt, up, hover, down) {
141− return A3(newElement,
142− Math.max(up.props.width, hover.props.width, down.props.width),
143− Math.max(up.props.height, hover.props.height, down.props.height),
144− { ctor: 'Custom',
145− type: 'CustomButton',
146− render: render,
147− update: update,
148− model: { event:evnt, up:up, hover:hover, down:down }
149− });
113+ function out(e) {
114+ if (btn.contains(e.toElement || e.relatedTarget)) return;
115+ overCount = 0;
116+ replace(btn.elm_up);
117+ }
118+ function up() {
119+ replace(btn.elm_hover);
120+ elm.notify(btn.elm_signal.id, btn.elm_value);
150121 }
122+ function down() {
123+ replace(btn.elm_down);
124+ }
125+ btn.addEventListener('mouseover', over);
126+ btn.addEventListener('mouseout' , out);
127+ btn.addEventListener('mousedown', down);
128+ btn.addEventListener('mouseup' , up);
151129
152− return { _:{}, customButton:F4(button), events:events };
153− }
130+ btn.appendChild(btn.elm_up);
154131
132+ var clicker = newNode('div');
133+ clicker.style.width = btn.elm_up.style.width;
134+ clicker.style.height = btn.elm_up.style.height;
135+ clicker.style.position = 'absolute';
136+ clicker.style.top = 0;
137+ btn.appendChild(clicker);
155138
156− function hoverables(defaultValue) {
157− var events = Signal.constant(defaultValue);
158− function hoverable(handler, elem) {
159− function onHover(bool) {
160− elm.notify(events.id, handler(bool));
161− }
162− var props = Utils.replace([['hover',onHover]], elem.props);
163− return { props:props, element:elem.element };
164− }
165− return { _:{}, hoverable:F2(hoverable), events:events };
139+ return btn;
166140 }
167141
142+ function updateCustomButton(node, oldModel, newModel) {
143+ node.elm_signal = newModel.signal;
144+ node.elm_value = newModel.value;
145+ Render.update(node.elm_up, oldModel.up, newModel.up)
146+ Render.update(node.elm_hover, oldModel.hover, newModel.hover)
147+ Render.update(node.elm_down, oldModel.down, newModel.down)
148+ }
168149
169− function checkboxes(defaultValue) {
170− var events = Signal.constant(defaultValue);
150+ function max3(a,b,c) {
151+ var ab = a > b ? a : b;
152+ return ab > c ? ab : c;
153+ }
171154
172− function render(model) {
173− var b = newNode('input');
174− b.type = 'checkbox';
175− b.checked = model.checked;
176− b.style.display = 'block';
177− b.elmHandler = model.handler;
178− function change() { elm.notify(events.id, b.elmHandler(b.checked)); }
179− b.addEventListener('change', change);
180− return b;
181− }
155+ function customButton(signal, value, up, hover, down) {
156+ return A3(newElement,
157+ max3(up.props.width, hover.props.width, down.props.width),
158+ max3(up.props.height, hover.props.height, down.props.height),
159+ { ctor: 'Custom',
160+ type: 'CustomButton',
161+ render: renderCustomButton,
162+ update: updateCustomButton,
163+ model: { signal:signal, value:value, up:up, hover:hover, down:down }
164+ });
165+ }
182166
183− function update(node, oldModel, newModel) {
184− node.elmHandler = newModel.handler;
185− node.checked = newModel.checked;
186− return true;
167+ function renderCheckbox(model) {
168+ var node = newNode('input');
169+ node.type = 'checkbox';
170+ node.checked = model.checked;
171+ node.style.display = 'block';
172+ node.elm_signal = model.signal;
173+ node.elm_handler = model.handler;
174+ function change() {
175+ elm.notify(node.elm_signal.id, node.elm_handler(node.checked));
187176 }
177+ node.addEventListener('change', change);
178+ return node;
179+ }
188180
189− function box(handler, checked) {
190− return A3(newElement, 13, 13, {
191− ctor: 'Custom',
192− type: 'CheckBox',
193− render: render,
194− update: update,
195− model: { checked:checked, handler:handler }
196− });
197− }
181+ function updateCheckbox(node, oldModel, newModel) {
182+ node.elm_signal = newModel.signal;
183+ node.elm_handler = newModel.handler;
184+ node.checked = newModel.checked;
185+ return true;
186+ }
198187
199− return { _:{}, checkbox:F2(box), events:events };
188+ function checkbox(signal, handler, checked) {
189+ return A3(newElement, 13, 13, {
190+ ctor: 'Custom',
191+ type: 'CheckBox',
192+ render: renderCheckbox,
193+ update: updateCheckbox,
194+ model: { signal:signal, handler:handler, checked:checked }
195+ });
200196 }
201197
202198 function setRange(node, start, end, dir) {
@@ -207,97 +203,114 @@ Elm.Native.Graphics.Input.make = function(elm) {
207203 }
208204 }
209205
210− function mkTextPool(type) { return function fields(defaultValue) {
211− var events = Signal.constant(defaultValue);
212−
213− var state = null;
214−
215− function render(model) {
216− var field = newNode('input');
217− field.elmHandler = model.handler;
218−
219− field.id = 'test';
220− field.type = type;
221− field.placeholder = JS.fromString(model.placeHolder);
222− field.value = JS.fromString(model.state.string);
223− setRange(field, model.state.selectionStart, model.state.selectionEnd, 'forward');
224− field.style.border = 'none';
225− state = model.state;
226−
227− function update() {
228− var start = field.selectionStart,
229− end = field.selectionEnd;
230− if (field.selectionDirection === 'backward') {
231− start = end;
232− end = field.selectionStart;
233− }
234− state = { _:{},
235− string:JS.toString(field.value),
236− selectionStart:start,
237− selectionEnd:end };
238− elm.notify(events.id, field.elmHandler(state));
239− }
240− function mousedown() {
241− update();
242− elm.node.addEventListener('mouseup', mouseup);
243− }
244− function mouseup() {
245− update();
246− elm.node.removeEventListener('mouseup', mouseup)
206+ function renderField(model) {
207+ var field = newNode('input');
208+ field.elm_signal = model.signal;
209+ field.elm_handler = model.handler;
210+
211+ field.id = 'test';
212+ field.type = type;
213+ field.placeholder = JS.fromString(model.placeHolder);
214+ field.value = JS.fromString(model.state.string);
215+ setRange(field, model.state.selectionStart, model.state.selectionEnd, 'forward');
216+ field.style.border = 'none';
217+ state = model.state;
218+
219+ function update() {
220+ var start = field.selectionStart;
221+ var end = field.selectionEnd;
222+ if (field.selectionDirection === 'backward') {
223+ start = end;
224+ end = field.selectionStart;
247225 }
248− field.addEventListener('keyup', update);
249− field.addEventListener('mousedown', mousedown);
250−
251− return field;
226+ elm.notify(events.id, field.elm_handler({
227+ _:{},
228+ string:JS.toString(field.value),
229+ selectionStart:start,
230+ selectionEnd:end
231+ }));
232+ }
233+ function mousedown() {
234+ update();
235+ elm.node.addEventListener('mouseup', mouseup);
236+ }
237+ function mouseup() {
238+ update();
239+ elm.node.removeEventListener('mouseup', mouseup)
252240 }
241+ field.addEventListener('keyup', update);
242+ field.addEventListener('mousedown', mousedown);
253243
254− function update(node, oldModel, newModel) {
255− node.elmHandler = newModel.handler;
256− if (state === newModel.state) return;
257− var newStr = JS.fromString(newModel.state.string);
258− if (node.value !== newStr) node.value = newStr;
244+ return field;
245+ }
259246
260− var start = newModel.state.selectionStart;
261− var end = newModel.state.selectionEnd;
262− var direction = 'forward';
263− if (end < start) {
264− start = end;
265− end = newModel.state.selectionStart;
266− direction = 'backward';
267− }
268−
269− if (node.selectionStart !== start
270− || node.selectionEnd !== end
271− || node.selectionDirection !== direction) {
272− setRange(node, start, end, direction);
273− }
247+ function updateField(node, oldModel, newModel) {
248+ node.elmHandler = newModel.handler;
249+ var newStr = JS.fromString(newModel.state.string);
250+ if (node.value !== newStr) node.value = newStr;
251+
252+ var start = newModel.state.selectionStart;
253+ var end = newModel.state.selectionEnd;
254+ var direction = 'forward';
255+ if (end < start) {
256+ start = end;
257+ end = newModel.state.selectionStart;
258+ direction = 'backward';
259+ }
260+
261+ if (node.selectionStart !== start
262+ || node.selectionEnd !== end
263+ || node.selectionDirection !== direction) {
264+ setRange(node, start, end, direction);
265+ }
266+ }
267+
268+ function mkField(type) {
269+ function field(signal, handler, placeHolder, state) {
270+ return A3(newElement, 200, 30, {
271+ ctor: 'Custom',
272+ type: type + 'Input',
273+ render: render,
274+ update: update,
275+ model: {
276+ signal:signal,
277+ handler:handler,
278+ placeHolder:placeHolder,
279+ state:state
280+ }
281+ });
274282 }
283+ return F4(field);
284+ }
285+
275286
276− function field(handler, placeHolder, state) {
277− return A3(newElement, 200, 30,
278− { ctor: 'Custom',
279− type: type + 'Input',
280− render: render,
281− update: update,
282− model: { handler:handler,
283− placeHolder:placeHolder,
284− state:state }
285− });
287+ function hoverable(signal, handler, element) {
288+ function onHover(bool) {
289+ elm.notify(signal.id, handler(bool));
286290 }
291+ var props = Utils.replace([['hover',onHover]], elem.props);
292+ return { props:props, element:elem.element };
293+ }
287294
288− return { _:{}, field:F3(field), events:events };
295+ function clickable(signal, value, element) {
296+ function onClick(bool) {
297+ elm.notify(signal.id, value);
298+ }
299+ var props = Utils.replace([['click',onClick]], elem.props);
300+ return { props:props, element:elem.element };
289301 }
290− }
291302
292303 return elm.Native.Graphics.Input.values = {
293− buttons:buttons,
294− customButtons:customButtons,
295− hoverables:hoverables,
296− checkboxes:checkboxes,
297− fields:mkTextPool('text'),
298− emails:mkTextPool('email'),
299− passwords:mkTextPool('password'),
300− dropDown:dropDown
304+ input:input,
305+ button:F3(button),
306+ customButton:F5(customButton),
307+ checkbox:F3(checkbox),
308+ dropDown:F2(dropDown),
309+ field:mkField('text'),
310+ email:mkField('email'),
311+ password:mkField('password'),
312+ hoverable:F3(hoverable),
313+ clickable:F3(clickable)
301314 };
302315
303316};
runtime/Render/Element.js+32−5
@@ -28,15 +28,28 @@ function setProps(props, e) {
2828 e.appendChild(a);
2929 }
3030 if (props.hover.ctor !== '_Tuple0') {
31− var overCount = 0;
31+ e.elm_hover_handler = props.hover;
32+ e.elm_hover_count = 0;
3233 e.addEventListener('mouseover', function() {
33− if (overCount++ > 0) return;
34− props.hover(true);
34+ if (e.elm_hover_count++ > 0) return;
35+ var handler = e.elm_hover_handler;
36+ if (handler !== null) {
37+ handler(true);
38+ }
3539 });
3640 e.addEventListener('mouseout', function(evt) {
3741 if (e.contains(evt.toElement || evt.relatedTarget)) return;
38− overCount = 0;
39− props.hover(false);
42+ e.elm_hover_count = 0;
43+ var handler = e.elm_hover_handler;
44+ if (handler !== null) {
45+ handler(false);
46+ }
47+ });
48+ }
49+ if (props.click.ctor !== '_Tuple0') {
50+ e.elm_click_handler = props.click;
51+ e.addEventListener('click', function() {
52+ e.elm_click_handler(Tuple0);
4053 });
4154 }
4255 return e;
@@ -319,6 +332,20 @@ function updateProps(node, curr, next) {
319332 node.lastNode.href = props.href;
320333 }
321334 }
335+
336+ // update hover handlers
337+ if (props.hover.ctor !== '_Tuple0') {
338+ e.elm_hover_handler = props.hover;
339+ } else if (e.elm_hover_handler) {
340+ e.elm_hover_handler = null;
341+ }
342+
343+ // update click handlers
344+ if (props.click.ctor !== '_Tuple0') {
345+ e.elm_click_handler = props.click;
346+ } else if (e.elm_click_handler) {
347+ e.elm_click_handler = null;
348+ }
322349}
323350
324351return { render:render, update:update };