Move all field related stuff to Graphics.Input.Field and add the ability to style fields
This commit is contained in:
parent
10506b5663
commit
093d7afb34
4 changed files with 213 additions and 67 deletions
|
@ -19,14 +19,15 @@ examples in this library, so just read on to get a better idea of how it works!
|
||||||
@docs Input, input
|
@docs Input, input
|
||||||
|
|
||||||
# Basic Input Elements
|
# 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
|
@docs button, customButton, checkbox, dropDown
|
||||||
|
|
||||||
# Clicks and Hovers
|
# Clicks and Hovers
|
||||||
@docs clickable, hoverable
|
@docs clickable, hoverable
|
||||||
|
|
||||||
# Text Fields
|
|
||||||
@docs field, password, email, noContent, FieldContent, Selection, Direction
|
|
||||||
-}
|
-}
|
||||||
|
|
||||||
import Signal (Signal)
|
import Signal (Signal)
|
||||||
|
@ -175,57 +176,3 @@ distinguished with IDs or more complex data structures.
|
||||||
-}
|
-}
|
||||||
clickable : Handle a -> a -> Element -> Element
|
clickable : Handle a -> a -> Element -> Element
|
||||||
clickable = Native.Graphics.Input.clickable
|
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
|
|
||||||
|
|
130
libraries/Graphics/Input/Field.elm
Normal file
130
libraries/Graphics/Input/Field.elm
Normal 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
|
|
@ -8,6 +8,8 @@ Elm.Native.Graphics.Input.make = function(elm) {
|
||||||
var Render = ElmRuntime.use(ElmRuntime.Render.Element);
|
var Render = ElmRuntime.use(ElmRuntime.Render.Element);
|
||||||
var newNode = ElmRuntime.use(ElmRuntime.Render.Utils).newElement;
|
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 Signal = Elm.Signal.make(elm);
|
||||||
var newElement = Elm.Graphics.Element.make(elm).newElement;
|
var newElement = Elm.Graphics.Element.make(elm).newElement;
|
||||||
var JS = Elm.Native.JavaScript.make(elm);
|
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) {
|
function renderField(model) {
|
||||||
var field = newNode('input');
|
var field = newNode('input');
|
||||||
field.style.border = 'none';
|
updateFieldStyle(field.style, model.style);
|
||||||
field.style.outline = 'none';
|
field.style.borderStyle = 'solid';
|
||||||
field.style.backgroundColor = 'transparent';
|
|
||||||
field.style.pointerEvents = 'auto';
|
field.style.pointerEvents = 'auto';
|
||||||
|
|
||||||
field.type = model.type;
|
field.type = model.type;
|
||||||
|
@ -302,6 +345,9 @@ Elm.Native.Graphics.Input.make = function(elm) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateField(field, oldModel, newModel) {
|
function updateField(field, oldModel, newModel) {
|
||||||
|
if (oldModel.style !== newModel.style) {
|
||||||
|
updateFieldStyle(field.style, newModel.style);
|
||||||
|
}
|
||||||
field.elm_signal = newModel.signal;
|
field.elm_signal = newModel.signal;
|
||||||
field.elm_handler = newModel.handler;
|
field.elm_handler = newModel.handler;
|
||||||
|
|
||||||
|
@ -316,10 +362,16 @@ Elm.Native.Graphics.Input.make = function(elm) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function mkField(type) {
|
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, {
|
return A3(newElement, 200, 30, {
|
||||||
ctor: 'Custom',
|
ctor: 'Custom',
|
||||||
type: type + 'Input',
|
type: type + 'Field',
|
||||||
|
adjustWidth: adjustWidth,
|
||||||
|
adjustHeight: adjustHeight,
|
||||||
render: renderField,
|
render: renderField,
|
||||||
update: updateField,
|
update: updateField,
|
||||||
model: {
|
model: {
|
||||||
|
@ -327,14 +379,14 @@ Elm.Native.Graphics.Input.make = function(elm) {
|
||||||
handler:handler,
|
handler:handler,
|
||||||
placeHolder:placeHolder,
|
placeHolder:placeHolder,
|
||||||
content:content,
|
content:content,
|
||||||
|
style:style,
|
||||||
type:type
|
type:type
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return F4(field);
|
return F5(field);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function hoverable(signal, handler, elem) {
|
function hoverable(signal, handler, elem) {
|
||||||
function onHover(bool) {
|
function onHover(bool) {
|
||||||
elm.notify(signal.id, handler(bool));
|
elm.notify(signal.id, handler(bool));
|
||||||
|
|
|
@ -7,7 +7,15 @@ var newElement = Utils.newElement, extract = Utils.extract,
|
||||||
addTransform = Utils.addTransform, removeTransform = Utils.removeTransform,
|
addTransform = Utils.addTransform, removeTransform = Utils.removeTransform,
|
||||||
fromList = Utils.fromList, eq = Utils.eq;
|
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.width = (props.width |0) + 'px';
|
||||||
e.style.height = (props.height|0) + 'px';
|
e.style.height = (props.height|0) + 'px';
|
||||||
if (props.opacity !== 1) { e.style.opacity = props.opacity; }
|
if (props.opacity !== 1) { e.style.opacity = props.opacity; }
|
||||||
|
@ -200,7 +208,7 @@ function rawHtml(elem) {
|
||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(elem) { return setProps(elem.props, makeElement(elem)); }
|
function render(elem) { return setProps(elem, makeElement(elem)); }
|
||||||
function makeElement(e) {
|
function makeElement(e) {
|
||||||
var elem = e.element;
|
var elem = e.element;
|
||||||
switch(elem.ctor) {
|
switch(elem.ctor) {
|
||||||
|
@ -308,7 +316,16 @@ function update(node, curr, next) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateProps(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.width !== currP.width) e.style.width = (props.width |0) + 'px';
|
||||||
if (props.height !== currP.height) e.style.height = (props.height|0) + 'px';
|
if (props.height !== currP.height) e.style.height = (props.height|0) + 'px';
|
||||||
if (props.opacity !== 1 && props.opacity !== currP.opacity) {
|
if (props.opacity !== 1 && props.opacity !== currP.opacity) {
|
||||||
|
|
Loading…
Reference in a new issue