Merge branch 'text' into dev

This commit is contained in:
Evan Czaplicki 2014-03-02 12:43:45 -10:00
commit 5e7ed2bec5
11 changed files with 365 additions and 165 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

@ -6,6 +6,12 @@ Elm.Native.Color.make = function(elm) {
var Utils = Elm.Native.Utils.make(elm);
function toCss(c) {
return (c._3 === 1)
? ('rgb(' + c._0 + ', ' + c._1 + ', ' + c._2 + ')')
: ('rgba(' + c._0 + ', ' + c._1 + ', ' + c._2 + ', ' + c._3 + ')');
}
function complement(rgb) {
var hsv = toHSV(rgb);
hsv.hue = (hsv.hue + 180) % 360;
@ -63,7 +69,8 @@ Elm.Native.Color.make = function(elm) {
return elm.Native.Color.values = {
hsva:F4(hsva),
hsv:F3(hsv),
complement:complement
complement:complement,
toCss:toCss
};
};
};

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);
@ -78,7 +80,7 @@ Elm.Native.Graphics.Input.make = function(elm) {
function updateButton(node, oldModel, newModel) {
node.elm_signal = newModel.signal;
node.elm_value = nemModel.value;
node.elm_value = newModel.value;
var txt = newModel.text;
if (oldModel.text !== txt) node.innerHTML = txt;
}
@ -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

@ -10,7 +10,6 @@ Elm.Native.String.make = function(elm) {
var Maybe = Elm.Maybe.make(elm);
var JS = Elm.JavaScript.make(elm);
var Utils = Elm.Native.Utils.make(elm);
var show = Elm.Native.Show.make(elm);
function isEmpty(str) {
return str.length === 0;
@ -244,7 +243,6 @@ Elm.Native.String.make = function(elm) {
endsWith: F2(endsWith),
indexes: F2(indexes),
show:show,
toInt: toInt,
toFloat: toFloat,
toList: toList,

View file

@ -4,11 +4,10 @@ Elm.Native.Text.make = function(elm) {
elm.Native.Text = elm.Native.Text || {};
if (elm.Native.Text.values) return elm.Native.Text.values;
var JS = Elm.JavaScript.make(elm);
var Utils = Elm.Native.Utils.make(elm);
var Color = Elm.Native.Color.make(elm);
var toCss = Elm.Native.Color.make(elm).toCss;
var Element = Elm.Graphics.Element.make(elm);
var show = Elm.Native.Show.make(elm).show;
var List = Elm.Native.List.make(elm);
var Utils = Elm.Native.Utils.make(elm);
function makeSpaces(s) {
if (s.length == 0) { return s; }
@ -51,13 +50,51 @@ Elm.Native.Text.make = function(elm) {
return arr.join('<br/>');
}
function toText(str) { return Utils.txt(properEscape(JS.fromString(str))); }
function toText(str) { return Utils.txt(properEscape(str)); }
// conversions from Elm values to CSS
function toTypefaces(list) {
var typefaces = List.toArray(list);
for (var i = typefaces.length; i--; ) {
var typeface = typefaces[i];
if (typeface.contains(' ')) {
typefaces[i] = "'" + typeface + "'";
}
}
return typefaces.join(',');
}
function toLine(line) {
var ctor = line.ctor;
return ctor === 'Under' ? 'underline' :
ctor === 'Over' ? 'overline' : 'line-through';
}
// setting styles of Text
function style(style, text) {
var newText = '<span style="color:' + toCss(style.color) + ';'
if (style.typeface.ctor !== '[]') {
newText += 'font-family:' + toTypefaces(style.typeface) + ';'
}
if (style.height.ctor !== "Nothing") {
newText += 'font-size:' + style.height._0 + 'px;';
}
if (style.bold) {
newText += 'font-weight:bold;';
}
if (style.italic) {
newText += 'font-style:italic;';
}
if (style.line.ctor !== 'Nothing') {
newText += 'text-decoration:' + toLine(style.line._0) + ';';
}
newText += '">' + Utils.makeText(text) + '</span>'
return Utils.txt(newText);
}
function height(px, text) {
return { style: 'font-size:' + px + 'px;', text:text }
}
function typeface(name, text) {
return { style: 'font-family:' + name + ';', text:text }
function typeface(names, text) {
return { style: 'font-family:' + toTypefaces(names) + ';', text:text }
}
function monospace(text) {
return { style: 'font-family:monospace;', text:text }
@ -71,25 +108,16 @@ Elm.Native.Text.make = function(elm) {
function link(href, text) {
return { href: toText(href), text:text };
}
function underline(text) {
return { line: ' underline', text:text };
}
function overline(text) {
return { line: ' overline', text:text };
}
function strikeThrough(text) {
return { line: ' line-through', text:text };
function line(line, text) {
return { style: 'text-decoration:' + toLine(line) + ';', text:text };
}
function color(c, text) {
var color = (c._3 === 1)
? ('rgb(' + c._0 + ', ' + c._1 + ', ' + c._2 + ')')
: ('rgba(' + c._0 + ', ' + c._1 + ', ' + c._2 + ', ' + c._3 + ')');
return { style: 'color:' + color + ';', text:text };
function color(color, text) {
return { style: 'color:' + toCss(color) + ';', text:text };
}
function position(align) {
function create(text) {
function block(align) {
return function(text) {
var raw = {
ctor :'RawHtml',
html : Utils.makeText(text),
@ -100,7 +128,6 @@ Elm.Native.Text.make = function(elm) {
var pos = A2(Utils.htmlHeight, 0, raw);
return A3(Element.newElement, pos._0, pos._1, raw);
}
return create;
}
function markdown(text, guid) {
@ -115,36 +142,25 @@ Elm.Native.Text.make = function(elm) {
return A3(Element.newElement, pos._0, pos._1, raw);
}
var text = position('left');
function asText(v) {
return text(monospace(toText(show(v))));
}
function plainText(v) {
return text(toText(v));
}
return elm.Native.Text.values = {
toText: toText,
height : F2(height),
italic : italic,
bold : bold,
underline : underline,
overline : overline,
strikeThrough : strikeThrough,
line : F2(line),
monospace : monospace,
typeface : F2(typeface),
color : F2(color),
link : F2(link),
justified : position('justify'),
centered : position('center'),
righted : position('right'),
text : text,
plainText : plainText,
markdown : markdown,
leftAligned : block('left'),
rightAligned : block('right'),
centered : block('center'),
justified : block('justify'),
markdown : markdown,
asText : asText,
toTypefaces:toTypefaces,
toLine:toLine,
};
};

View file

@ -76,14 +76,8 @@ Elm.Native.Utils.make = function(elm) {
function makeText(text) {
var style = '';
var line = '';
var href = '';
while (true) {
if (text.line) {
line += text.line;
text = text.text;
continue;
}
if (text.style) {
style += text.style;
text = text.text;
@ -95,7 +89,6 @@ Elm.Native.Utils.make = function(elm) {
continue;
}
if (href) text = '<a href="' + href + '">' + text + '</a>';
if (line) style += 'text-decoration:' + line + ';';
if (style) text = '<span style="' + style + '">' + text + '</span>';
return text;
}

View file

@ -28,6 +28,7 @@ Cosmetic operations such as padding with extra characters or trimming whitespace
@docs map, filter, foldl, foldr, any, all
-}
import Native.Show
import Native.String
import Maybe (Maybe)
@ -284,7 +285,7 @@ indices = Native.String.indexes
show [1,2] == "[1,2]"
-}
show : a -> String
show = Native.String.show
show = Native.Show.show
{-| Try to convert a string into an int, failing on improperly formatted strings.

View file

@ -6,94 +6,132 @@ module Text where
@docs toText
# Creating Elements
@docs plainText, asText, text, centered, justified, righted
@docs leftAligned, rightAligned, centered, justified
# Formatting
@docs color, typeface, height, link
# Links and Style
@docs link, Style, style, Line, defaultStyle
# Simple Formatting
@docs monospace, bold, italic, underline, overline, strikeThrough
# Convenience Functions
There are two convenience functions for creating an `Element` which can be
useful when debugging or prototyping:
@docs plainText, asText
There are also a bunch of functions to set parts of a `Style` individually:
@docs typeface, monospace, height, color, bold, italic, line
-}
import Basics (..)
import Color (Color)
import String
import Color (Color, black)
import Graphics.Element (Element, Three, Pos, ElementPrim, Properties)
import Maybe (Maybe)
import Maybe (Maybe, Nothing)
import JavaScript (JSString)
import Native.Show
import Native.Text
data Text = Text
data Line = Under | Over | Through
{-| Representation of all the ways you can style `Text`.
-}
type Style =
{ typeface : [String]
, height : Maybe Float
, color : Color
, bold : Bool
, italic : Bool
, line : Maybe Line
}
{-| Plain black text. It uses the browsers default typeface and text height.
No decorations are used:
{ typeface = []
, height = Nothing
, color = black
, bold = False
, italic = False
, line = Nothing
}
-}
defaultStyle : Style
defaultStyle =
{ typeface = []
, height = Nothing
, color = black
, bold = False
, italic = False
, line = Nothing
}
{-| Convert a string into text which can be styled and displayed. -}
toText : String -> Text
toText = Native.Text.toText
{-| Set the typeface of some text. The first argument should be a comma
separated listing of the desired typefaces:
"helvetica, arial, sans-serif"
Works the same as the CSS font-family property.
{-| Set the style of some text.
-}
typeface : String -> Text -> Text
style : Style -> Text -> Text
style = Native.Text.style
{-| Provide a list of prefered typefaces for some text.
["helvetica","arial","sans-serif"]
Not everyone has access to the same typefaces, so rendering will use the first
typeface in the list that is found on the user's computer. If there are no
matches, it will use their default typeface. Works the same as the CSS
font-family property.
-}
typeface : [String] -> Text -> Text
typeface = Native.Text.typeface
{-| Switch to a monospace typeface. Good for code snippets. -}
monospace : Text -> Text
monospace = Native.Text.monospace
{-| Create a link. -}
{-| Create a link.
link "http://elm-lang.org" (toText "Elm Website")
-}
link : String -> Text -> Text
link = Native.Text.link
{-| Set the height of text in pixels. -}
height : Float -> Text -> Text
height = Native.Text.height
{-| Set the color of a string. -}
color : Color -> Text -> Text
color = Native.Text.color
{-| Make a string bold. -}
bold : Text -> Text
bold = Native.Text.bold
{-| Italicize a string. -}
italic : Text -> Text
italic = Native.Text.italic
{-| Draw a line above a string. -}
overline : Text -> Text
overline = Native.Text.overline
line : Line -> Text -> Text
line = Native.Text.line
{-| Underline a string. -}
underline : Text -> Text
underline = Native.Text.underline
leftAligned : Text -> Element
leftAligned = Native.Text.leftAligned
{-| Draw a line through a string. -}
strikeThrough : Text -> Text
strikeThrough = Native.Text.strikeThrough
rightAligned : Text -> Element
rightAligned = Native.Text.rightAligned
{-| Display justified, styled text. -}
justified : Text -> Element
justified = Native.Text.justified
{-| Display centered, styled text. -}
centered : Text -> Element
centered = Native.Text.centered
{-| Display right justified, styled text. -}
righted : Text -> Element
righted = Native.Text.righted
{-| Display styled text. -}
text : Text -> Element
text = Native.Text.text
justified : Text -> Element
justified = Native.Text.justified
{-| Display a plain string. -}
plainText : String -> Element
plainText = Native.Text.plainText
plainText str =
leftAligned (toText str)
{-| for internal use only -}
markdown : Element
@ -107,4 +145,5 @@ the browser:
Excellent for debugging.
-}
asText : a -> Element
asText = Native.Text.asText
asText value =
leftAligned (monospace (toText (Native.Show.show value)))

View file

@ -91,7 +91,7 @@ function init(display, container, module, ports, moduleToReplace) {
checkPorts(elm);
} catch(e) {
var directions = "<br/>&nbsp; &nbsp; Open the developer console for more details."
Module.main = Elm.Text.make(elm).text('<code>' + e.message + directions + '</code>');
Module.main = Elm.Text.make(elm).leftAligned('<code>' + e.message + directions + '</code>');
reportAnyErrors = function() { throw e; }
}
inputs = ElmRuntime.filterDeadInputs(inputs);

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) {