Attempt to revamp the Graphics.Input library, much testing still needed

This commit is contained in:
Evan Czaplicki 2014-02-17 17:38:37 +01:00
parent 8fbb9c799d
commit 1c832af6b0
4 changed files with 438 additions and 401 deletions

View file

@ -53,7 +53,8 @@ type Properties = {
color : Maybe Color,
href : JSString,
tag : JSString,
hover : ()
hover : (),
click : ()
}
type Element = { props : Properties, element : ElementPrim }
@ -128,7 +129,9 @@ link href e = let p = e.props in
emptyStr = JS.fromString ""
newElement w h e =
{ props = Properties (Native.Utils.guid ()) w h 1 Nothing emptyStr emptyStr (), element = e }
{ props = Properties (Native.Utils.guid ()) w h 1 Nothing emptyStr emptyStr () ()
, element = e
}
data ElementPrim
= Image ImageStyle Int Int JSString

View file

@ -1,199 +1,193 @@
module Graphics.Input where
{-| This module is for creating standard input widgets such as buttons and
text boxes. In general, functions in this library return a signal representing
events from the user.
text fields. All functions in this library follow this general pattern:
The simplest inputs are *one-way inputs*, meaning the user can update
them, but the programmer cannot. If you need to update an input from
within the program you want the slightly more complex *two-way inputs*.
This document will always show the one-way inputs first, *then* the
two-way inputs.
```haskell
-- (Signal (), SignalID ())
(clicks, clickID) = input ()
# Buttons
@docs button, customButton, buttons, customButtons
clickableYogi : Element
clickableYogi = clickable clickID () (image 40 40 "/yogi.jpg")
```
# Fields
@docs field, password, email, fields, FieldState, emptyFieldState
Whenever the user clicks on the resulting `clickableYogi` element, it sends a
`()` to the `clicks` signal. Typically, you would create all your input signals
when setting up the signal graph, so then your graphics code can refer to them
easily.
# Checkboxes
@docs checkbox, checkboxes
# Creating Inputs
@docs input
# Drop Downs
@docs stringDropDown, dropDown
# Basic Input Elements
Text fields come later.
@docs button, customButton, checkbox, dropDown
# Mouse Hover
@docs hoverable, hoverables
# Clicks and Hovers
@docs clickable, hoverable
# Text Fields
@docs field, password, email, noContent, FieldContent, Selection, Direction
-}
import Basics (String)
import Signal (Signal,lift,dropRepeats)
import Native.Graphics.Input
import List
import Signal (Signal)
import Graphics.Element (Element)
import Maybe (Maybe)
import JavaScript (JSString)
id x = x
data SignalID a = SignalID
{-| Create a group of buttons.
{-| This function is the key to using this library. It creates a signal that
input elements (buttens, checkboxes, etc.) can report to. It also create a
unique `SignalID` that allows input elements to refer to this particular
signal. It may be best to look at examples in the rest of the docs and return
to this function.
* The first argument is the default value of the `events` signal.
* The `events` signal represents all of the activity in this group
of buttons.
* The `button` function creates a button
with the given name, like “Submit” or “Cancel”.
The `a` value is sent to `events` whenever the button is pressed.
Note: creating an input signal is an inherently imperative action, so this is
one of very few impure functions in Elm. It is designed such that it can only
be used as you build your signal graph at startup, keeping Elm pure at runtime.
-}
buttons : a -> { events : Signal a,
button : a -> String -> Element }
buttons = Native.Graphics.Input.buttons
input : a -> (Signal a, SignalID a)
input = Native.Graphics.Input.input
{-| Create a button with a given label. The result is an `Element` and
a signal of units. This signal triggers whenever the button is pressed.
{-| Create a standard button. The following example begins making a basic
calculator:
data Input = Number Int | Plus | Minus | Clear
-- (Signal Input, SignalID Input)
(calcInput, pressID) = input Clear
buttons : Element
buttons =
flow right [ button pressID (Number 1) "1"
, button pressID (Number 2) "2"
, button pressID Plus "+"
]
If the user presses the "plus" button, the `calcInput` signal will update to
`Plus`. If the users presses "2", `calcInput` will update to `(Number 2)`.
-}
button : String -> (Element, Signal ())
button txt =
let pool = buttons ()
in (pool.button () txt, pool.events)
button : SignalID a -> a -> String -> Element
button = Native.Graphics.Input.button
{-| Create a group of custom buttons.
customButton : SignalID a -> a -> Element -> Element -> Element -> Element
customButton = Native.Graphics.Input.customButton
* The first argument is the default value of the `events` signal.
* The `events` signal represents all of the activity in this group
of custom buttons.
* The `customButton` function creates a button with three different visual
states, one for up, hovering, and down. The resulting button has dimensions
large enough to fit all three possible `Elements`.
The `a` value is sent to `events` whenever the button is pressed.
checkbox : SignalID a -> (Bool -> a) -> Bool -> Element
checkbox = Native.Graphics.Input.checkboxes
{-| Create a drop-down menu. The following drop-down lets you choose your
favorite British sport:
data Sport = Football | Cricket | Snooker
-- (Signal (Maybe Sport), SignalID (Maybe Sport))
(dropStatus, dropID) = input Nothing
sportDropDown : Element
sportDropDown =
dropDown dropID
[ ("" , Nothing)
, ("Football", Just Football)
, ("Cricket" , Just Cricket)
, ("Snooker" , Just Snooker)
]
If the user selects "Football" from the drop down menue, the `dropStatus`
signal will update to `Just Football`.
-}
customButtons : a -> { events : Signal a,
customButton : a -> Element -> Element -> Element -> Element }
customButtons = Native.Graphics.Input.customButtons
dropDown : SignalID a -> [(String,a)] -> Element
dropDown = Native.Graphics.Input.dropDown
{-| Create a button with custom states for up, hovering, and down
(given in that order). The result is an `Element` and a signal of
units. This signal triggers whenever the button is pressed.
{-| Detect mouse hovers over a specific `Element`. In the following example,
we will create a hoverable picture called `cat`.
-- (Signal Bool, SignalID Bool)
(hover, hoverID) = input False
cat : Element
cat = image 30 30 "/cat.jpg"
|> hoverable hoverID (\hover -> hover)
When the mouse hovers above the `cat` element, the `hover` signal will become
`True`. When the mouse leaves it, `hover` will become `False`.
-}
customButton : Element -> Element -> Element -> (Element, Signal ())
customButton up hover down =
let pool = customButtons ()
in (pool.customButton () up hover down, pool.events)
hoverable : SignalID a -> (Bool -> a) -> Element -> Element
hoverable = Native.Graphics.Input.hoverable
{-| Create a group of checkboxes.
{-| Detect mouse clicks on a specific `Element`. In the following example,
we will create a clickable picture called `cat`.
* The first argument is the default value of the `events` signal.
* The `events` signal represents all of the activity in this group
of checkboxes.
* The `checkbox` function creates a
checkbox with a given state. The `(Bool -> a)` function is used
when the checkbox is modified. It takes the new state and turns
it into a value that can be sent to `events`. For example, this
lets you add an ID to distinguish between checkboxes.
data Picture = Cat | Hat
-- (Signal Picture, SignalID Picture)
(click, clickID) = input Cat
cat : Element
cat = image 30 30 "/cat.jpg"
|> clickable clickID Cat
hat : Element
hat = image 30 30 "/hat.jpg"
|> clickable clickID Hat
When the user clicks on the `cat` element, the `click` signal receives
an update containing the value `Cat`. When the user clicks on the `hat` element,
the `click` signal receives an update containing the value `Hat`. This lets you
distinguish which element was clicked. In a more complex example, they could be
distinguished with IDs or more complex data structures.
-}
checkboxes : a -> { events : Signal a,
checkbox : (Bool -> a) -> Bool -> Element }
checkboxes = Native.Graphics.Input.checkboxes
clickable : SignalID a -> a -> Element -> Element
clickable = Native.Graphics.Input.clickable
{-| Create a checkbox with a given start state. Unlike `button`, this
result is a *signal* of elements. That is because a checkbox has state
that updates based on user input. The boolean signal represents the
current state of the checkbox.
-}
checkbox : Bool -> (Signal Element, Signal Bool)
checkbox b =
let cbs = checkboxes b
in (lift (cbs.checkbox id) cbs.events, cbs.events)
{-| Represents the current content of a text field. For example:
{-| Detect when the mouse is hovering over some elements. This
allows you to create and destroy elements dynamically and still
detect hover information.
-}
hoverables : a -> { events : Signal a,
hoverable : (Bool -> a) -> Element -> Element }
hoverables = Native.Graphics.Input.hoverables
{-| Detect when the mouse is hovering over a specific `Element`. -}
hoverable : Element -> (Element, Signal Bool)
hoverable elem =
let pool = hoverables False
in (pool.hoverable id elem, pool.events)
{-| Represents the current state of a text field. The `string` represents the
characters filling the text field. The `selectionStart` and `selectionEnd`
values represent what the user has selected with their mouse or keyboard.
For example:
{ string="She sells sea shells", selectionStart=3, selectionEnd=0 }
FieldContent "She sells sea shells" (Selection 0 3 Backward)
This means the user highlighted the substring `"She"` backwards.
-}
type FieldState = { string:String, selectionStart:Int, selectionEnd:Int }
type FieldContent = { string:String, selection:Selection }
{-| Create a group of text input fields.
{-| The selection within a text field. `start` is never greater than `end`:
* The first argument is the default value of the `events` signal.
* The `events` signal represents all of the activity in this group
of text fields.
* The `field` function creates a
field with the given ghost text and initial field state.
When the field is modified, the `(FieldState -> a)` function
takes the new state and turns
it into a value that can be sent to `events`. For example, this
lets you add an ID to distinguish between input fields.
Selection 0 0 Forward -- cursor precedes all characters
Selection 5 9 Backward -- highlighting characters starting after
-- the 5th and ending after the 9th
-}
fields : a -> { events : Signal a,
field : (FieldState -> a) -> String -> FieldState -> Element }
fields = Native.Graphics.Input.fields
type Selection = { start:Int, end:Int, direction:Direction }
{-| The empty field state:
{-| The direction of selection.-}
data Direction = Forward | Backward
{ string="", selectionStart=0, selectionEnd=0 }
{-| A field with no content:
FieldContent "" (Selection 0 0 Forward)
-}
emptyFieldState : FieldState
emptyFieldState = { string="", selectionStart=0, selectionEnd=0 }
noContent : FieldContent
noContent = FieldContent "" (Selection 0 0 Forward)
{-| Create a field with the given default text. The output is an element
that updates to match the user input and a signal of strings representing
the content of the field.
{-| Create a text field. The following example creates a time-varying element
called `nameField`. As the user types their name, the field will be updated
to match what they have entered.
-- (Signal FieldContent, SignalID FieldContent)
(nameContent,nameID) = input noContent
nameField : Signal Element
nameField = field nameID (\content -> content) "Name" <~ nameContent
-}
field : String -> (Signal Element, Signal String)
field placeHolder =
let tfs = fields emptyFieldState
changes = dropRepeats tfs.events
in (lift (tfs.field id placeHolder) changes,
dropRepeats (lift .string changes))
field : SignalID a -> (FieldContent -> a) -> String -> FieldContent -> Element
field = Native.Graphics.Input.field
{-| Same as `field` but the UI element blocks out each characters. -}
password : String -> (Signal Element, Signal String)
password placeHolder =
let tfs = Native.Graphics.Input.passwords emptyFieldState
changes = dropRepeats tfs.events
in (lift (tfs.field id placeHolder) changes,
dropRepeats (lift .string changes))
password : SignalID a -> (FieldContent -> a) -> String -> FieldContent -> Element
password = Native.Graphics.Input.password
{-| Same as `field` but it adds an annotation that this field is for email
addresses. This is helpful for auto-complete and for mobile users who may
get a custom keyboard with an `@` and `.com` button.
-}
email : String -> (Signal Element, Signal String)
email placeHolder =
let tfs = Native.Graphics.Input.emails emptyFieldState
changes = dropRepeats tfs.events
in (lift (tfs.field id placeHolder) changes,
dropRepeats (lift .string changes))
email : SignalID a -> (FieldContent -> a) -> String -> FieldContent -> Element
email = Native.Graphics.Input.email
{-| Create a drop-down menu. When the user selects a string,
the current state of the drop-down is set to the associated
value. This lets you avoid manually mapping the string onto
functions and values.
-}
dropDown : [(String,a)] -> (Signal Element, Signal a)
dropDown = Native.Graphics.Input.dropDown
{-| Create a drop-down menu for selecting strings. The resulting
signal of strings represents the string that is currently selected.
-}
stringDropDown : [String] -> (Signal Element, Signal String)
stringDropDown strs =
dropDown (List.map (\s -> (s,s)) strs)
-- area : SignalID a -> (FieldContent -> a) -> SignalID b -> ((Int,Int) -> b) -> (Int,Int) -> String -> FieldContent -> Element
-- area = Native.Graphics.Input.area

View file

@ -15,188 +15,184 @@ Elm.Native.Graphics.Input.make = function(elm) {
var Utils = Elm.Native.Utils.make(elm);
var Tuple2 = Utils.Tuple2;
function dropDown(values) {
var entries = JS.fromList(values);
var events = Signal.constant(entries[0]._1);
function input(initialValue) {
var sig = Signal.constant(initialValue);
return Tuple2(sig, sig);
}
var drop = newNode('select');
drop.style.border = '0 solid';
for (var i = 0; i < entries.length; ++i) {
var option = newNode('option');
var name = JS.fromString(entries[i]._0);
option.value = name;
option.innerHTML = name;
drop.appendChild(option);
}
drop.addEventListener('change', function() {
elm.notify(events.id, entries[drop.selectedIndex]._1);
});
function renderDropDown(signal, values) {
return function(_) {
var entries = JS.fromList(values);
var t = drop.cloneNode(true);
t.style.visibility = "hidden";
var drop = newNode('select');
drop.style.border = '0 solid';
for (var i = 0; i < entries.length; ++i) {
var option = newNode('option');
var name = JS.fromString(entries[i]._0);
option.value = name;
option.innerHTML = name;
drop.appendChild(option);
}
drop.addEventListener('change', function() {
elm.notify(signal.id, entries[drop.selectedIndex]._1);
});
elm.node.appendChild(t);
var style = window.getComputedStyle(t, null);
var w = Math.ceil(style.getPropertyValue("width").slice(0,-2) - 0);
var h = Math.ceil(style.getPropertyValue("height").slice(0,-2) - 0);
elm.node.removeChild(t);
var element = A3(newElement, w, h, {
var t = drop.cloneNode(true);
t.style.visibility = "hidden";
elm.node.appendChild(t);
var style = window.getComputedStyle(t, null);
var w = Math.ceil(style.getPropertyValue("width").slice(0,-2) - 0);
var h = Math.ceil(style.getPropertyValue("height").slice(0,-2) - 0);
elm.node.removeChild(t);
return drop;
};
}
function updateDropDown(node, oldModel, newModel) {
}
function dropDown(signal, values) {
return A3(newElement, w, h, {
ctor: 'Custom',
type: 'DropDown',
render: function render(model) { return drop; },
update: function update(node, oldModel, newModel) {},
render: renderDropDown(signal,values),
update: updateDropDown,
model: {}
});
return Tuple2(Signal.constant(element), events);
}
function buttons(defaultValue) {
var events = Signal.constant(defaultValue);
function render(model) {
var b = newNode('button');
b.style.display = 'block';
b.elmEvent = model.event;
function click() { elm.notify(events.id, b.elmEvent); }
b.addEventListener('click', click);
b.innerHTML = model.text;
return b;
function renderButton(model) {
var node = newNode('button');
node.style.display = 'block';
node.elm_signal = model.signal;
node.elm_value = model.value;
function click() {
elm.notify(node.elm_signal.id, node.elm_value);
}
function update(node, oldModel, newModel) {
node.elmEvent = newModel.event;
var txt = newModel.text;
if (oldModel.text !== txt) node.innerHTML = txt;
}
function button(evnt, txt) {
return A3(newElement, 100, 40, {
ctor: 'Custom',
type: 'Button',
render: render,
update: update,
model: { event:evnt, text:JS.fromString(txt) }
});
}
return { _:{}, button:F2(button), events:events };
node.addEventListener('click', click);
node.innerHTML = model.text;
return node;
}
function customButtons(defaultValue) {
var events = Signal.constant(defaultValue);
function render(model) {
var btn = newNode('div');
btn.elmEvent = model.event;
btn.elmUp = Render.render(model.up);
btn.elmHover = Render.render(model.hover);
btn.elmDown = Render.render(model.down);
function replace(node) {
if (node !== btn.firstChild) btn.replaceChild(node, btn.firstChild);
}
var overCount = 0;
function over(e) {
if (overCount++ > 0) return;
replace(btn.elmHover);
}
function out(e) {
if (btn.contains(e.toElement || e.relatedTarget)) return;
overCount = 0;
replace(btn.elmUp);
}
function up() {
replace(btn.elmHover);
elm.notify(events.id, btn.elmEvent);
}
function down() { replace(btn.elmDown); }
btn.addEventListener('mouseover', over);
btn.addEventListener('mouseout' , out);
btn.addEventListener('mousedown', down);
btn.addEventListener('mouseup' , up);
btn.appendChild(btn.elmUp);
var clicker = newNode('div');
clicker.style.width = btn.elmUp.style.width;
clicker.style.height = btn.elmUp.style.height;
clicker.style.position = 'absolute';
clicker.style.top = 0;
btn.appendChild(clicker);
return btn;
}
function update(node, oldModel, newModel) {
node.elmEvent = newModel.event;
Render.update(node.elmUp, oldModel.up, newModel.up)
Render.update(node.elmHover, oldModel.hover, newModel.hover)
Render.update(node.elmDown, oldModel.down, newModel.down)
}
function button(evnt, up, hover, down) {
return A3(newElement,
Math.max(up.props.width, hover.props.width, down.props.width),
Math.max(up.props.height, hover.props.height, down.props.height),
{ ctor: 'Custom',
type: 'CustomButton',
render: render,
update: update,
model: { event:evnt, up:up, hover:hover, down:down }
});
}
return { _:{}, customButton:F4(button), events:events };
function updateButton(node, oldModel, newModel) {
node.elm_signal = newModel.signal;
node.elm_value = nemModel.value;
var txt = newModel.text;
if (oldModel.text !== txt) node.innerHTML = txt;
}
function hoverables(defaultValue) {
var events = Signal.constant(defaultValue);
function hoverable(handler, elem) {
function onHover(bool) {
elm.notify(events.id, handler(bool));
}
var props = Utils.replace([['hover',onHover]], elem.props);
return { props:props, element:elem.element };
}
return { _:{}, hoverable:F2(hoverable), events:events };
function button(signal, value, text) {
return A3(newElement, 100, 40, {
ctor: 'Custom',
type: 'Button',
render: renderButton,
update: updateButton,
model: { signal:signal, value:value, text:JS.fromString(text) }
});
}
function renderCustomButton(model) {
var btn = newNode('div');
btn.elm_value = model.value;
function checkboxes(defaultValue) {
var events = Signal.constant(defaultValue);
btn.elm_up = Render.render(model.up);
btn.elm_hover = Render.render(model.hover);
btn.elm_down = Render.render(model.down);
function render(model) {
var b = newNode('input');
b.type = 'checkbox';
b.checked = model.checked;
b.style.display = 'block';
b.elmHandler = model.handler;
function change() { elm.notify(events.id, b.elmHandler(b.checked)); }
b.addEventListener('change', change);
return b;
function replace(node) {
if (node !== btn.firstChild) {
btn.replaceChild(node, btn.firstChild);
}
}
function update(node, oldModel, newModel) {
node.elmHandler = newModel.handler;
node.checked = newModel.checked;
return true;
var overCount = 0;
function over(e) {
if (overCount++ > 0) return;
replace(btn.elm_hover);
}
function box(handler, checked) {
return A3(newElement, 13, 13, {
ctor: 'Custom',
type: 'CheckBox',
render: render,
update: update,
model: { checked:checked, handler:handler }
});
function out(e) {
if (btn.contains(e.toElement || e.relatedTarget)) return;
overCount = 0;
replace(btn.elm_up);
}
function up() {
replace(btn.elm_hover);
elm.notify(btn.elm_signal.id, btn.elm_value);
}
function down() {
replace(btn.elm_down);
}
btn.addEventListener('mouseover', over);
btn.addEventListener('mouseout' , out);
btn.addEventListener('mousedown', down);
btn.addEventListener('mouseup' , up);
return { _:{}, checkbox:F2(box), events:events };
btn.appendChild(btn.elm_up);
var clicker = newNode('div');
clicker.style.width = btn.elm_up.style.width;
clicker.style.height = btn.elm_up.style.height;
clicker.style.position = 'absolute';
clicker.style.top = 0;
btn.appendChild(clicker);
return btn;
}
function updateCustomButton(node, oldModel, newModel) {
node.elm_signal = newModel.signal;
node.elm_value = newModel.value;
Render.update(node.elm_up, oldModel.up, newModel.up)
Render.update(node.elm_hover, oldModel.hover, newModel.hover)
Render.update(node.elm_down, oldModel.down, newModel.down)
}
function max3(a,b,c) {
var ab = a > b ? a : b;
return ab > c ? ab : c;
}
function customButton(signal, value, up, hover, down) {
return A3(newElement,
max3(up.props.width, hover.props.width, down.props.width),
max3(up.props.height, hover.props.height, down.props.height),
{ ctor: 'Custom',
type: 'CustomButton',
render: renderCustomButton,
update: updateCustomButton,
model: { signal:signal, value:value, up:up, hover:hover, down:down }
});
}
function renderCheckbox(model) {
var node = newNode('input');
node.type = 'checkbox';
node.checked = model.checked;
node.style.display = 'block';
node.elm_signal = model.signal;
node.elm_handler = model.handler;
function change() {
elm.notify(node.elm_signal.id, node.elm_handler(node.checked));
}
node.addEventListener('change', change);
return node;
}
function updateCheckbox(node, oldModel, newModel) {
node.elm_signal = newModel.signal;
node.elm_handler = newModel.handler;
node.checked = newModel.checked;
return true;
}
function checkbox(signal, handler, checked) {
return A3(newElement, 13, 13, {
ctor: 'Custom',
type: 'CheckBox',
render: renderCheckbox,
update: updateCheckbox,
model: { signal:signal, handler:handler, checked:checked }
});
}
function setRange(node, start, end, dir) {
@ -207,97 +203,114 @@ Elm.Native.Graphics.Input.make = function(elm) {
}
}
function mkTextPool(type) { return function fields(defaultValue) {
var events = Signal.constant(defaultValue);
function renderField(model) {
var field = newNode('input');
field.elm_signal = model.signal;
field.elm_handler = model.handler;
var state = null;
field.id = 'test';
field.type = type;
field.placeholder = JS.fromString(model.placeHolder);
field.value = JS.fromString(model.state.string);
setRange(field, model.state.selectionStart, model.state.selectionEnd, 'forward');
field.style.border = 'none';
state = model.state;
function render(model) {
var field = newNode('input');
field.elmHandler = model.handler;
field.id = 'test';
field.type = type;
field.placeholder = JS.fromString(model.placeHolder);
field.value = JS.fromString(model.state.string);
setRange(field, model.state.selectionStart, model.state.selectionEnd, 'forward');
field.style.border = 'none';
state = model.state;
function update() {
var start = field.selectionStart,
end = field.selectionEnd;
if (field.selectionDirection === 'backward') {
start = end;
end = field.selectionStart;
}
state = { _:{},
string:JS.toString(field.value),
selectionStart:start,
selectionEnd:end };
elm.notify(events.id, field.elmHandler(state));
}
function mousedown() {
update();
elm.node.addEventListener('mouseup', mouseup);
}
function mouseup() {
update();
elm.node.removeEventListener('mouseup', mouseup)
}
field.addEventListener('keyup', update);
field.addEventListener('mousedown', mousedown);
return field;
}
function update(node, oldModel, newModel) {
node.elmHandler = newModel.handler;
if (state === newModel.state) return;
var newStr = JS.fromString(newModel.state.string);
if (node.value !== newStr) node.value = newStr;
var start = newModel.state.selectionStart;
var end = newModel.state.selectionEnd;
var direction = 'forward';
if (end < start) {
function update() {
var start = field.selectionStart;
var end = field.selectionEnd;
if (field.selectionDirection === 'backward') {
start = end;
end = newModel.state.selectionStart;
direction = 'backward';
}
if (node.selectionStart !== start
|| node.selectionEnd !== end
|| node.selectionDirection !== direction) {
setRange(node, start, end, direction);
end = field.selectionStart;
}
elm.notify(events.id, field.elm_handler({
_:{},
string:JS.toString(field.value),
selectionStart:start,
selectionEnd:end
}));
}
function field(handler, placeHolder, state) {
return A3(newElement, 200, 30,
{ ctor: 'Custom',
type: type + 'Input',
render: render,
update: update,
model: { handler:handler,
placeHolder:placeHolder,
state:state }
});
function mousedown() {
update();
elm.node.addEventListener('mouseup', mouseup);
}
function mouseup() {
update();
elm.node.removeEventListener('mouseup', mouseup)
}
field.addEventListener('keyup', update);
field.addEventListener('mousedown', mousedown);
return { _:{}, field:F3(field), events:events };
return field;
}
function updateField(node, oldModel, newModel) {
node.elmHandler = newModel.handler;
var newStr = JS.fromString(newModel.state.string);
if (node.value !== newStr) node.value = newStr;
var start = newModel.state.selectionStart;
var end = newModel.state.selectionEnd;
var direction = 'forward';
if (end < start) {
start = end;
end = newModel.state.selectionStart;
direction = 'backward';
}
if (node.selectionStart !== start
|| node.selectionEnd !== end
|| node.selectionDirection !== direction) {
setRange(node, start, end, direction);
}
}
function mkField(type) {
function field(signal, handler, placeHolder, state) {
return A3(newElement, 200, 30, {
ctor: 'Custom',
type: type + 'Input',
render: render,
update: update,
model: {
signal:signal,
handler:handler,
placeHolder:placeHolder,
state:state
}
});
}
return F4(field);
}
function hoverable(signal, handler, element) {
function onHover(bool) {
elm.notify(signal.id, handler(bool));
}
var props = Utils.replace([['hover',onHover]], elem.props);
return { props:props, element:elem.element };
}
function clickable(signal, value, element) {
function onClick(bool) {
elm.notify(signal.id, value);
}
var props = Utils.replace([['click',onClick]], elem.props);
return { props:props, element:elem.element };
}
}
return elm.Native.Graphics.Input.values = {
buttons:buttons,
customButtons:customButtons,
hoverables:hoverables,
checkboxes:checkboxes,
fields:mkTextPool('text'),
emails:mkTextPool('email'),
passwords:mkTextPool('password'),
dropDown:dropDown
input:input,
button:F3(button),
customButton:F5(customButton),
checkbox:F3(checkbox),
dropDown:F2(dropDown),
field:mkField('text'),
email:mkField('email'),
password:mkField('password'),
hoverable:F3(hoverable),
clickable:F3(clickable)
};
};

View file

@ -28,15 +28,28 @@ function setProps(props, e) {
e.appendChild(a);
}
if (props.hover.ctor !== '_Tuple0') {
var overCount = 0;
e.elm_hover_handler = props.hover;
e.elm_hover_count = 0;
e.addEventListener('mouseover', function() {
if (overCount++ > 0) return;
props.hover(true);
if (e.elm_hover_count++ > 0) return;
var handler = e.elm_hover_handler;
if (handler !== null) {
handler(true);
}
});
e.addEventListener('mouseout', function(evt) {
if (e.contains(evt.toElement || evt.relatedTarget)) return;
overCount = 0;
props.hover(false);
e.elm_hover_count = 0;
var handler = e.elm_hover_handler;
if (handler !== null) {
handler(false);
}
});
}
if (props.click.ctor !== '_Tuple0') {
e.elm_click_handler = props.click;
e.addEventListener('click', function() {
e.elm_click_handler(Tuple0);
});
}
return e;
@ -319,6 +332,20 @@ function updateProps(node, curr, next) {
node.lastNode.href = props.href;
}
}
// update hover handlers
if (props.hover.ctor !== '_Tuple0') {
e.elm_hover_handler = props.hover;
} else if (e.elm_hover_handler) {
e.elm_hover_handler = null;
}
// update click handlers
if (props.click.ctor !== '_Tuple0') {
e.elm_click_handler = props.click;
} else if (e.elm_click_handler) {
e.elm_click_handler = null;
}
}
return { render:render, update:update };