Move all field related stuff to Graphics.Input.Field and add the ability to style fields

This commit is contained in:
Evan Czaplicki 2014-03-01 22:43:21 -08:00
parent 10506b5663
commit 093d7afb34
4 changed files with 213 additions and 67 deletions

View file

@ -19,14 +19,15 @@ examples in this library, so just read on to get a better idea of how it works!
@docs Input, input
# Basic Input Elements
Text fields come later.
To learn about text fields, see the
[`Graphics.Input.Field`](Graphics-Input-Field) library.
@docs button, customButton, checkbox, dropDown
# Clicks and Hovers
@docs clickable, hoverable
# Text Fields
@docs field, password, email, noContent, FieldContent, Selection, Direction
-}
import Signal (Signal)
@ -175,57 +176,3 @@ distinguished with IDs or more complex data structures.
-}
clickable : Handle a -> a -> Element -> Element
clickable = Native.Graphics.Input.clickable
{-| Represents the current content of a text field. For example:
FieldContent "She sells sea shells" (Selection 0 3 Backward)
This means the user highlighted the substring `"She"` backwards.
-}
type FieldContent = { string:String, selection:Selection }
{-| The selection within a text field. `start` is never greater than `end`:
Selection 0 0 Forward -- cursor precedes all characters
Selection 5 9 Backward -- highlighting characters starting after
-- the 5th and ending after the 9th
-}
type Selection = { start:Int, end:Int, direction:Direction }
{-| The direction of selection.-}
data Direction = Forward | Backward
{-| A field with no content:
FieldContent "" (Selection 0 0 Forward)
-}
noContent : FieldContent
noContent = FieldContent "" (Selection 0 0 Forward)
{-| 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.
name : Input FieldContent
name = input noContent
nameField : Signal Element
nameField = field name.handle id "Name" <~ name.signal
-}
field : Handle a -> (FieldContent -> a) -> String -> FieldContent -> Element
field = Native.Graphics.Input.field
{-| Same as `field` but the UI element blocks out each characters. -}
password : Handle 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 : Handle a -> (FieldContent -> a) -> String -> FieldContent -> Element
email = Native.Graphics.Input.email
-- area : Handle a -> (FieldContent -> a) -> Handle b -> ((Int,Int) -> b) -> (Int,Int) -> String -> FieldContent -> Element
-- area = Native.Graphics.Input.area

View file

@ -0,0 +1,130 @@
module Graphics.Input.Field where
{-| This library specifically addresses text fields. It uses the same genral
approach as the [`Graphics.Input`](Graphics-Input) module for describing an
`Input`, so this library focuses on creating and styling text fields.
# Create Fields
@docs field, password, email
# Field Content
@docs Content, Selection, Direction, noContent
# Field Style
@docs Style, Outline, noOutline, Highlight, noHighlight, Dimensions, uniformly
-}
import Color (Color)
import Color
import Graphics.Element (Element)
import Graphics.Input (Input, Handle)
import Native.Graphics.Input
import Text
{-| Easily create uniform dimensions:
uniformly 4 == { left=4, right=4, top=4, bottom=4 }
-}
uniformly : Int -> Dimensions
uniformly n = Dimensions n n n n
{-| For setting dimensions of a fields padding or border. The left, right, top,
and bottom may all have different sizes.
-}
type Dimensions = { left:Int, right:Int, top:Int, bottom:Int }
{-| A field can have a outline around it. This lets you set its color, width,
and radius. The radius allows you to round the corners of your field. Set the
width to zero to make it invisible.
-}
type Outline = { color:Color, width:Dimensions, radius:Int }
{-| An outline with zero width, so you cannot see it. -}
noOutline : Outline
noOutline = Outline Color.grey (uniformly 0) 0
{-| When a field is selected, it has an highlight around it by default. Set the
width of the `Highlight` to zero to make it go away.
-}
type Highlight = { color:Color, width:Int }
{-| An highlight with zero width, so you cannot see it. -}
noHighlight : Highlight
noHighlight = Highlight Color.blue 0
{-| Describes the style of a text box. The `style` field describes the style
of the text itself. The `outline` field describes the glowing blue outline that
shows up when the field has focus. Turn off `outline` by setting its width to
zero. The
-}
type Style =
{ padding : Dimensions
, outline : Outline
, highlight : Highlight
, style : Text.Style
}
{-| The default style for a text field. The outline is `Color.grey` with width
1 and radius 2. The highlight is `Color.blue` with width 1, and the default
text color is black.
-}
defaultStyle : Style
defaultStyle =
{ padding = uniformly 4
, outline = Outline Color.grey (uniformly 1) 2
, highlight = Highlight Color.blue 1
, style = Text.defaultStyle
}
{-| Represents the current content of a text field. For example:
Content "She sells sea shells" (Selection 0 3 Backward)
This means the user highlighted the substring `"She"` backwards.
-}
type Content = { string:String, selection:Selection }
{-| The selection within a text field. `start` is never greater than `end`:
Selection 0 0 Forward -- cursor precedes all characters
Selection 5 9 Backward -- highlighting characters starting after
-- the 5th and ending after the 9th
-}
type Selection = { start:Int, end:Int, direction:Direction }
{-| The direction of selection.-}
data Direction = Forward | Backward
{-| A field with no content:
Content "" (Selection 0 0 Forward)
-}
noContent : Content
noContent = Content "" (Selection 0 0 Forward)
{-| 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.
name : Input Content
name = input noContent
nameField : Signal Element
nameField = field name.handle id "Name" <~ name.signal
-}
field : Handle a -> (Content -> a) -> Style -> String -> Content -> Element
field = Native.Graphics.Input.field
{-| Same as `field` but the UI element blocks out each characters. -}
password : Handle a -> (Content -> a) -> Style -> String -> Content -> 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 : Handle a -> (Content -> a) -> Style -> String -> Content -> Element
email = Native.Graphics.Input.email
-- area : Handle a -> (Content -> a) -> Handle b -> ((Int,Int) -> b) -> (Int,Int) -> String -> Content -> Element
-- area = Native.Graphics.Input.area

View file

@ -8,6 +8,8 @@ Elm.Native.Graphics.Input.make = function(elm) {
var Render = ElmRuntime.use(ElmRuntime.Render.Element);
var newNode = ElmRuntime.use(ElmRuntime.Render.Utils).newElement;
var toCss = Elm.Native.Color.make(elm).toCss;
var Text = Elm.Native.Text.make(elm);
var Signal = Elm.Signal.make(elm);
var newElement = Elm.Graphics.Element.make(elm).newElement;
var JS = Elm.Native.JavaScript.make(elm);
@ -215,11 +217,52 @@ Elm.Native.Graphics.Input.make = function(elm) {
}
}
function updateIfNeeded(css, attribute, latestAttribute) {
if (css[attribute] !== latestAttribute) {
css[attribute] = latestAttribute;
}
}
function cssDimensions(dimensions) {
return dimensions.top + 'px ' +
dimensions.right + 'px ' +
dimensions.bottom + 'px ' +
dimensions.left + 'px';
}
function updateFieldStyle(css, style) {
updateIfNeeded(css, 'padding', cssDimensions(style.padding));
var outline = style.outline;
updateIfNeeded(css, 'border-width', cssDimensions(outline.width));
updateIfNeeded(css, 'border-color', toCss(outline.color));
updateIfNeeded(css, 'border-radius', outline.radius + 'px');
var highlight = style.highlight;
if (highlight.width === 0) {
css.outline = 'none';
} else {
updateIfNeeded(css, 'outline-width', highlight.width + 'px');
updateIfNeeded(css, 'outline-color', toCss(highlight.color));
}
var textStyle = style.style;
updateIfNeeded(css, 'color', toCss(textStyle.color));
if (textStyle.typeface.ctor !== '[]') {
updateIfNeeded(css, 'font-family', Text.toTypefaces(textStyle.typeface));
}
if (textStyle.height.ctor !== "Nothing") {
updateIfNeeded(css, 'font-size', textStyle.height._0 + 'px');
}
updateIfNeeded(css, 'font-weight', textStyle.bold ? 'bold' : 'normal');
updateIfNeeded(css, 'font-style', textStyle.italic ? 'italic' : 'normal');
if (textStyle.line.ctor !== 'Nothing') {
updateIfNeeded(css, 'text-decoration', Text.toLine(textStyle.line._0));
}
}
function renderField(model) {
var field = newNode('input');
field.style.border = 'none';
field.style.outline = 'none';
field.style.backgroundColor = 'transparent';
updateFieldStyle(field.style, model.style);
field.style.borderStyle = 'solid';
field.style.pointerEvents = 'auto';
field.type = model.type;
@ -302,6 +345,9 @@ Elm.Native.Graphics.Input.make = function(elm) {
}
function updateField(field, oldModel, newModel) {
if (oldModel.style !== newModel.style) {
updateFieldStyle(field.style, newModel.style);
}
field.elm_signal = newModel.signal;
field.elm_handler = newModel.handler;
@ -316,10 +362,16 @@ Elm.Native.Graphics.Input.make = function(elm) {
}
function mkField(type) {
function field(signal, handler, placeHolder, content) {
function field(signal, handler, style, placeHolder, content) {
var padding = style.padding;
var outline = style.outline.width;
var adjustWidth = padding.left + padding.right + outline.left + outline.right;
var adjustHeight = padding.top + padding.bottom + outline.top + outline.bottom;
return A3(newElement, 200, 30, {
ctor: 'Custom',
type: type + 'Input',
type: type + 'Field',
adjustWidth: adjustWidth,
adjustHeight: adjustHeight,
render: renderField,
update: updateField,
model: {
@ -327,14 +379,14 @@ Elm.Native.Graphics.Input.make = function(elm) {
handler:handler,
placeHolder:placeHolder,
content:content,
style:style,
type:type
}
});
}
return F4(field);
return F5(field);
}
function hoverable(signal, handler, elem) {
function onHover(bool) {
elm.notify(signal.id, handler(bool));

View file

@ -7,7 +7,15 @@ var newElement = Utils.newElement, extract = Utils.extract,
addTransform = Utils.addTransform, removeTransform = Utils.removeTransform,
fromList = Utils.fromList, eq = Utils.eq;
function setProps(props, e) {
function setProps(elem, e) {
var props = elem.props;
var element = elem.element;
if (element.adjustWidth) {
props.width -= element.adjustWidth;
}
if (element.adjustHeight) {
props.height -= element.adjustHeight;
}
e.style.width = (props.width |0) + 'px';
e.style.height = (props.height|0) + 'px';
if (props.opacity !== 1) { e.style.opacity = props.opacity; }
@ -200,7 +208,7 @@ function rawHtml(elem) {
return div;
}
function render(elem) { return setProps(elem.props, makeElement(elem)); }
function render(elem) { return setProps(elem, makeElement(elem)); }
function makeElement(e) {
var elem = e.element;
switch(elem.ctor) {
@ -308,7 +316,16 @@ function update(node, curr, next) {
}
function updateProps(node, curr, next) {
var props = next.props, currP = curr.props, e = node;
var props = next.props;
var currP = curr.props;
var e = node;
var element = next.element;
if (element.adjustWidth) {
props.width -= element.adjustWidth;
}
if (element.adjustHeight) {
props.height -= element.adjustHeight;
}
if (props.width !== currP.width) e.style.width = (props.width |0) + 'px';
if (props.height !== currP.height) e.style.height = (props.height|0) + 'px';
if (props.opacity !== 1 && props.opacity !== currP.opacity) {