diff --git a/elm-package.json b/elm-package.json index 26ea10e..f750237 100644 --- a/elm-package.json +++ b/elm-package.json @@ -12,6 +12,7 @@ "Ext.Date", "Ext.Number", "Ext.Color", + "Ext.Signal", "Html.Extra", "Storage.Local", "Ui", @@ -45,6 +46,7 @@ ], "dependencies": { "circuithub/elm-number-format": "1.0.2 <= v < 2.0.0", + "circuithub/elm-maybe-extra": "1.5.1 <= v < 2.0.0", "circuithub/elm-json-extra": "2.2.0 <= v < 3.0.0", "circuithub/elm-list-extra": "3.7.1 <= v < 4.0.0", "deadfoxygrandpa/elm-test": "3.0.1 <= v < 4.0.0", diff --git a/source/Ext/Signal.elm b/source/Ext/Signal.elm new file mode 100644 index 0000000..bc08937 --- /dev/null +++ b/source/Ext/Signal.elm @@ -0,0 +1,19 @@ +module Ext.Signal where + +{-| Utility functions for working with signals. + +@docs sendAsEffect +-} +import Effects +import Signal + +{-| Sends the given value to the given address and turns it into an effect +for a different address. + + Signal.sendAsEffect address value action +-} +sendAsEffect : Signal.Address a -> a -> (() -> b) -> Effects.Effects b +sendAsEffect address value action = + Signal.send address value + |> Effects.task + |> Effects.map action diff --git a/source/Html/Extra.elm b/source/Html/Extra.elm index 654ccda..3d5a4f7 100644 --- a/source/Html/Extra.elm +++ b/source/Html/Extra.elm @@ -214,7 +214,7 @@ onEnter control address handler = decoder = if control then controlKey else enterIf in onWithOptions - "keydown" + "keyup" stopOptions decoder (\code -> Signal.message address handler) @@ -246,7 +246,7 @@ onKeysWithDimensions address mappings = {-| An event listener that runs on input. -} onInput : Signal.Address a -> (String -> a) -> Html.Attribute onInput address handler = - on "input" targetValue (\str -> Signal.message address (handler str)) + on "input" targetValue (\str -> Signal.message address (handler str)) {-| A load event listener .-} onLoad : Signal.Address a -> a -> Html.Attribute diff --git a/source/Main.elm b/source/Main.elm index a73b56b..1e43c17 100644 --- a/source/Main.elm +++ b/source/Main.elm @@ -1,4 +1,7 @@ import Signal exposing (forwardTo) +import Maybe.Extra +import Date.Format +import List.Extra import Ext.Date import StartApp import Keyboard @@ -7,6 +10,8 @@ import Mouse import Color import Task import Date +import Time +import Set import Html.Attributes exposing (style, classList, colspan, href) import Html.Events exposing (onClick) @@ -30,8 +35,10 @@ import Ui.Chooser import Ui.Ratings import Ui.Button import Ui.Slider +import Ui.Pager import Ui.Modal import Ui.Image +import Ui.Input import Ui.App import Ui @@ -53,11 +60,14 @@ type Action | Ratings Ui.Ratings.Action | Slider Ui.Slider.Action | Image Ui.Image.Action + | Input Ui.Input.Action | Modal Ui.Modal.Action + | Pager Ui.Pager.Action | App Ui.App.Action | MousePosition (Int, Int) | MouseIsDown Bool | ShowNotification + | AppAction String | EscIsDown Bool | Open String | CloseMenu @@ -65,6 +75,16 @@ type Action | OpenModal | Nothing | Alert + | PreviousPage + | NextPage + | ChooserChanged (Set.Set String) + | DatePickerChanged Time.Time + | InplaceInputChanged String + | CalendarChanged Time.Time + | Checkbox2Changed Bool + | Checkbox3Changed Bool + | CheckboxChanged Bool + | RatingsChanged Float type alias Model = { app : Ui.App.Model @@ -86,6 +106,8 @@ type alias Model = , slider : Ui.Slider.Model , modal : Ui.Modal.Model , image : Ui.Image.Model + , input : Ui.Input.Model + , pager : Ui.Pager.Model , clicked : Bool } @@ -93,15 +115,19 @@ init : Model init = let datePickerOptions = Ui.DatePicker.init (Ext.Date.now ()) + input = Ui.Input.init "" + pager = Ui.Pager.init 0 in - { app = Ui.App.init "Elm-UI Kitchen Sink" - , notifications = Ui.NotificationCenter.init 4000 320 + { calendar = Ui.Calendar.init (Ext.Date.createDate 2015 5 1) , datePicker = { datePickerOptions | format = "%Y %B %e." } , chooser = Ui.Chooser.init data "Select a country..." "" + , pager = { pager | width = "100%", height = "200px" } + , notifications = Ui.NotificationCenter.init 4000 320 + , input = { input | placeholder = "Type here..." } , inplaceInput = Ui.InplaceInput.init "Test Value" , colorPicker = Ui.ColorPicker.init Color.yellow , colorPanel = Ui.ColorPanel.init Color.blue - , calendar = Ui.Calendar.init (Ext.Date.createDate 2015 5 1) + , app = Ui.App.init "Elm-UI Kitchen Sink" , numberRange = Ui.NumberRange.init 0 , checkbox3 = Ui.Checkbox.init False , checkbox2 = Ui.Checkbox.init False @@ -109,7 +135,7 @@ init = , textarea = Ui.Textarea.init "Test" , numberPad = Ui.NumberPad.init 0 , image = Ui.Image.init imageUrl - , ratings = Ui.Ratings.init 5 0.5 + , ratings = Ui.Ratings.init 5 0.4 , slider = Ui.Slider.init 50 , menu = Ui.DropdownMenu.init , modal = Ui.Modal.init @@ -147,7 +173,7 @@ view address model = let { chooser, colorPanel, datePicker, colorPicker, numberRange, slider , checkbox, checkbox2, checkbox3, calendar, inplaceInput, textarea - , numberPad, ratings } = model + , numberPad, ratings, pager, input } = model clicked = if model.clicked then [node "clicked" [] [text ""]] else [] @@ -363,6 +389,14 @@ view address model = (Ui.Slider.view (forwardTo address Slider) { slider | disabled = True }) + , componentHeader "Input" + , tableRow (Ui.Input.view (forwardTo address Input) + input) + (Ui.Input.view (forwardTo address Input) + { input | readonly = True }) + (Ui.Input.view (forwardTo address Input) + { input | disabled = True }) + , componentHeader "Autogrow Textarea" , tableRow (Ui.Textarea.view (forwardTo address TextArea) textarea) @@ -387,6 +421,26 @@ view address model = (Ui.NumberPad.view (forwardTo address NumberPad) numberPadViewModel { numberPad | disabled = True }) + , componentHeader "Pager" + , tr [] + [ td [colspan 3] + [ Ui.Pager.view (forwardTo address Pager) + [ text "Page 1" + , text "Page 2" + , text "Page 3" + ] + pager + ] + ] + , tr [] + [ td [colspan 3] + [ Ui.Container.row [] + [ Ui.IconButton.primary "Previous Page" "chevron-left" "left" address PreviousPage + , Ui.spacer + , Ui.IconButton.primary "Next Page" "chevron-right" "right" address NextPage + ] + ] + ] , componentHeader "Image" , tr [] [ td [] @@ -398,73 +452,47 @@ view address model = ] ] -fxNone : Model -> (Model, Effects.Effects Action) -fxNone model = - (model, Effects.none) - update : Action -> Model -> Model update action model = case action of - InplaceInput act -> - { model | inplaceInput = Ui.InplaceInput.update act model.inplaceInput } - NumberRange act -> - { model | numberRange = Ui.NumberRange.update act model.numberRange } - ColorPicker act -> - { model | colorPicker = Ui.ColorPicker.update act model.colorPicker } - DatePicker act -> - { model | datePicker = Ui.DatePicker.update act model.datePicker } - ColorPanel act -> - { model | colorPanel = Ui.ColorPanel.update act model.colorPanel } - NumberPad act -> - { model | numberPad = Ui.NumberPad.update act model.numberPad } - Checkbox2 act -> - { model | checkbox2 = Ui.Checkbox.update act model.checkbox2 } - Checkbox3 act -> - { model | checkbox3 = Ui.Checkbox.update act model.checkbox3 } - Checkbox act -> - { model | checkbox = Ui.Checkbox.update act model.checkbox } - TextArea act -> - { model | textarea = Ui.Textarea.update act model.textarea } - Calendar act -> - { model | calendar = Ui.Calendar.update act model.calendar } - Chooser act -> - { model | chooser = Ui.Chooser.update act model.chooser } DropdownMenu act -> - { model | menu = Ui.DropdownMenu.update act model.menu } - Slider act -> - { model | slider = Ui.Slider.update act model.slider } + { model | menu = Ui.DropdownMenu.update act model.menu } + Modal act -> - { model | modal = Ui.Modal.update act model.modal } + { model | modal = Ui.Modal.update act model.modal } + Image act -> - { model | image = Ui.Image.update act model.image } - App act -> - case act of - Ui.App.Scrolled -> - { model | menu = Ui.DropdownMenu.close model.menu } - _ -> - { model | app = Ui.App.update act model.app } + { model | image = Ui.Image.update act model.image } + + Pager act -> + { model | pager = Ui.Pager.update act model.pager } MouseIsDown value -> { model | numberRange = Ui.NumberRange.handleClick value model.numberRange - , colorPanel = Ui.ColorPanel.handleClick value model.colorPanel , colorPicker = Ui.ColorPicker.handleClick value model.colorPicker - , slider = Ui.Slider.handleClick value model.slider + , colorPanel = Ui.ColorPanel.handleClick value model.colorPanel , menu = Ui.DropdownMenu.handleClick value model.menu + , slider = Ui.Slider.handleClick value model.slider } - MousePosition (x,y) -> - { model - | numberRange = Ui.NumberRange.handleMove x y model.numberRange - , colorPicker = Ui.ColorPicker.handleMove x y model.colorPicker - , colorPanel = Ui.ColorPanel.handleMove x y model.colorPanel - , slider = Ui.Slider.handleMove x y model.slider - } + + AppAction act -> + case act of + "scroll" -> { model | menu = Ui.DropdownMenu.close model.menu } + _ -> model CloseMenu -> { model | menu = Ui.DropdownMenu.close model.menu } + Open url -> Ui.open url model + NextPage -> + { model | pager = Ui.Pager.select (clamp 0 2 (model.pager.active + 1)) model.pager } + + PreviousPage -> + { model | pager = Ui.Pager.select (clamp 0 2 (model.pager.active - 1)) model.pager } + EscIsDown bool -> { model | menu = Ui.DropdownMenu.close model.menu , modal = Ui.Modal.close model.modal @@ -485,34 +513,179 @@ update action model = update' : Action -> Model -> (Model, Effects.Effects Action) update' action model = case action of + Input act -> + let + (input, effect) = Ui.Input.update act model.input + in + ({ model | input = input }, Effects.map Input effect) + TextArea act -> + let + (textarea, effect) = Ui.Textarea.update act model.textarea + in + ({ model | textarea = textarea }, Effects.map TextArea effect) + NumberPad act -> + let + (numberPad, effect) = Ui.NumberPad.update act model.numberPad + in + ({ model | numberPad = numberPad }, Effects.map NumberPad effect) + InplaceInput act -> + let + (inplaceInput, effect) = Ui.InplaceInput.update act model.inplaceInput + in + ({ model | inplaceInput = inplaceInput }, Effects.map InplaceInput effect) + Chooser act -> + let + (chooser, effect) = Ui.Chooser.update act model.chooser + in + ({ model | chooser = chooser }, Effects.map Chooser effect) + Checkbox2 act -> + let + (checkbox2, effect) = Ui.Checkbox.update act model.checkbox2 + in + ({ model | checkbox2 = checkbox2 }, Effects.map Checkbox2 effect) + Checkbox3 act -> + let + (checkbox3, effect) = Ui.Checkbox.update act model.checkbox3 + in + ({ model | checkbox3 = checkbox3 }, Effects.map Checkbox3 effect) + Checkbox act -> + let + (checkbox, effect) = Ui.Checkbox.update act model.checkbox + in + ({ model | checkbox = checkbox }, Effects.map Checkbox effect) + ColorPicker act -> + let + (colorPicker, effect) = Ui.ColorPicker.update act model.colorPicker + in + ({ model | colorPicker = colorPicker }, Effects.map ColorPicker effect) + ColorPanel act -> + let + (colorPanel, effect) = Ui.ColorPanel.update act model.colorPanel + in + ({ model | colorPanel = colorPanel }, Effects.map ColorPanel effect) + DatePicker act -> + let + (datePicker, effect) = Ui.DatePicker.update act model.datePicker + in + ({ model | datePicker = datePicker }, Effects.map DatePicker effect) + Calendar act -> + let + (calendar, effect) = Ui.Calendar.update act model.calendar + in + ({ model | calendar = calendar}, Effects.map Calendar effect) Ratings act -> let (ratings, effect) = Ui.Ratings.update act model.ratings in ({ model | ratings = ratings }, Effects.map Ratings effect) + App act -> + let + (app, effect) = Ui.App.update act model.app + in + ({ model | app = app }, Effects.map App effect) Notis act -> let (notis, effect) = Ui.NotificationCenter.update act model.notifications in ({ model | notifications = notis }, Effects.map Notis effect) + NumberRange act -> + let + (numberRange, effect) = Ui.NumberRange.update act model.numberRange + in + ({ model | numberRange = numberRange}, Effects.map NumberRange effect) + Slider act -> + let + (slider, effect) = Ui.Slider.update act model.slider + in + ({ model | slider = slider }, Effects.map Slider effect) + + MousePosition (x,y) -> + let + (colorPicker, colorPickerEffect) = + Ui.ColorPicker.handleMove x y model.colorPicker + (colorPanel, colorPanelEffect) = + Ui.ColorPanel.handleMove x y model.colorPanel + (numberRange, numberRangeEffect) = + Ui.NumberRange.handleMove x y model.numberRange + (slider, sliderEffect) = + Ui.Slider.handleMove x y model.slider + in + ({ model + | numberRange = numberRange + , colorPicker = colorPicker + , colorPanel = colorPanel + , slider = slider + }, Effects.batch [ Effects.map ColorPanel colorPanelEffect + , Effects.map ColorPicker colorPickerEffect + , Effects.map NumberRange numberRangeEffect + , Effects.map Slider sliderEffect + ]) + + InplaceInputChanged value -> + notify ("Inplace input changed to: " ++ value) model + CheckboxChanged value -> + notify ("Checkbox changed to: " ++ (toString value)) model + Checkbox2Changed value -> + notify ("Toggle changed to: " ++ (toString value)) model + Checkbox3Changed value -> + notify ("Radio changed to: " ++ (toString value)) model ShowNotification -> + notify "Test Notification" model + ChooserChanged set -> let - (notis, effect) = Ui.NotificationCenter.notify (text "Test Notification") model.notifications + selected = + Ui.Chooser.getFirstSelected model.chooser + |> Maybe.map (\value -> List.Extra.find (\item -> item.value == value) data) + |> Maybe.Extra.join + |> Maybe.map .label + |> Maybe.withDefault "" in - ({ model | notifications = notis }, Effects.map Notis effect) + notify ("Chooser changed to: " ++ selected) model + CalendarChanged time -> + notify ("Calendar changed to: " ++ (Date.Format.format "%Y-%m-%d" (Date.fromTime time))) model + DatePickerChanged time -> + notify ("Date picker changed to: " ++ (Date.Format.format "%Y-%m-%d" (Date.fromTime time))) model + RatingsChanged value -> + notify ("Ratings changed to: " ++ (toString (Ui.Ratings.valueAsStars value model.ratings))) model _ -> - update action model - |> fxNone + (update action model, Effects.none) + +notify : String -> Model -> (Model, Effects.Effects Action) +notify message model = + let + (notis, effect) = Ui.NotificationCenter.notify (text message) model.notifications + in + ({ model | notifications = notis }, Effects.map Notis effect) app = - StartApp.start { init = (init, Effects.none) - , view = view - , update = update' - , inputs = [ Signal.map MousePosition Mouse.position - , Signal.map MouseIsDown Mouse.isDown - , Signal.map EscIsDown (Keyboard.isDown 27) - ] - } + let + initial = + init + + inputs = + -- Lifecycle + [ Signal.map EscIsDown (Keyboard.isDown 27) + , Signal.map MousePosition Mouse.position + , Signal.map MouseIsDown Mouse.isDown + -- Components + , Signal.map DatePicker initial.datePicker.signal + , Signal.map AppAction initial.app.signal + -- Changes + , Signal.map InplaceInputChanged initial.inplaceInput.valueSignal + , Signal.map DatePickerChanged initial.datePicker.valueSignal + , Signal.map Checkbox2Changed initial.checkbox2.valueSignal + , Signal.map Checkbox3Changed initial.checkbox3.valueSignal + , Signal.map CalendarChanged initial.calendar.valueSignal + , Signal.map CheckboxChanged initial.checkbox.valueSignal + , Signal.map RatingsChanged initial.ratings.valueSignal + , Signal.map ChooserChanged initial.chooser.valueSignal + ] + in + StartApp.start { init = (initial, Effects.none) + , view = view + , update = update' + , inputs = inputs + } main = app.html diff --git a/source/Ui/App.elm b/source/Ui/App.elm index 2ce48cd..75b9114 100644 --- a/source/Ui/App.elm +++ b/source/Ui/App.elm @@ -1,10 +1,10 @@ module Ui.App (Model, Action(..), init, update, view) where {-| Base frame for a web/mobile application: - - Loads the stylesheet - - Sets up a scroll handler + - Provides a signal for **load** and **scroll** events - Sets the viewport to be mobile friendly - Sets the title of the application + - Loads the stylesheet # Model @docs Model, Action, update, init @@ -12,6 +12,9 @@ module Ui.App (Model, Action(..), init, update, view) where # View @docs view -} +import Ext.Signal +import Effects + import Html.Attributes exposing (name, content, style) import Html.Extra exposing (onScroll) import Html exposing (node, text) @@ -19,39 +22,53 @@ import Html.Lazy import Ui -{-| Representation of an application. -} +{-| Representation of an application: + - **signal** - The signal of the mailbox for events (scroll / load) + - **title** - The title of the application (and the window) + - **loaded** (internal) - Whether or not the applications stylesheet is loaded + - **mailbox** (internal) - The mailbox of the application +-} type alias Model = - { title: String - , loaded: Bool + { mailbox : Signal.Mailbox String + , signal : Signal String + , title : String + , loaded : Bool } -{-| Actions an application can make: - - **Scrolled** - Dispatched when something inside the application has scrolled - - **Loaded** - Dispatched when the stylesheet is loaded --} +{-| Actions an application can make. -} type Action = Scrolled | Loaded + | Tasks () -{-| Initializes an application. +{-| Initializes an application with the given title. App.init "My Application" -} init : String -> Model init title = - { loaded = False - , title = title - } + let + mailbox = Signal.mailbox "" + in + { signal = mailbox.signal + , mailbox = mailbox + , loaded = False + , title = title + } {-| Updates an application. -} -update : Action -> Model -> Model +update : Action -> Model -> (Model, Effects.Effects Action) update action model = case action of Loaded -> - { model | loaded = True } + ({ model | loaded = True } + , Ext.Signal.sendAsEffect model.mailbox.address "load" Tasks) Scrolled -> - model + (model, Ext.Signal.sendAsEffect model.mailbox.address "scroll" Tasks) + + Tasks _ -> + (model, Effects.none) {-| Renders an application. diff --git a/source/Ui/Button.elm b/source/Ui/Button.elm index 415503d..ed403a3 100644 --- a/source/Ui/Button.elm +++ b/source/Ui/Button.elm @@ -35,7 +35,12 @@ import Html.Lazy import Ui -{-| Representation of a button. -} +{-| Representation of a button: + - **disabled** - Whether or not the button is disabled + - **kind** - The type of the button + - **size** - The size of the button + - **text** - The text of the button +-} type alias Model = { disabled: Bool , kind: String diff --git a/source/Ui/Calendar.elm b/source/Ui/Calendar.elm index f2953c9..b1532f9 100644 --- a/source/Ui/Calendar.elm +++ b/source/Ui/Calendar.elm @@ -2,8 +2,9 @@ module Ui.Calendar ( Model, Action(..), init, update, view, setValue, nextDay , previousDay ) where -{-| This is a calendar component where the user -can select a date by clicking on it. +{-| This is a calendar component where the user can: + - Select a date by clicking on it + - Change the month with arrows # Model @docs Model, Action, init, update @@ -16,41 +17,46 @@ can select a date by clicking on it. -} import Html.Attributes exposing (classList) import Html.Events exposing (onMouseDown) -import Html exposing (node, text) +import Html exposing (node, text, span) import Html.Lazy -import List import Date.Format exposing (format) +import Time exposing (Time) +import Ext.Signal import Ext.Date +import Effects +import Signal +import List import Date import Ui.Container import Ui {-| Representation of a calendar component: - - **selectable** - Whether the user can select a date by clicking - - **date** - The month in which this date is will be displayed - - **readonly** - Whether the calendar is interactive - - **disabled** - Whether the calendar is disabled + - **selectable** - Whether or not the user can select a date by clicking + - **readonly** - Whether or not the calendar is interactive + - **disabled** - Whether or not the calendar is disabled + - **valueSignal** - The calendars value as a signal - **value** - The current selected date + - **date** (internal) - The month in which this date is will be displayed + - **mailbox** (internal) - The mailbox of the calendar -} type alias Model = - { selectable : Bool + { mailbox : Signal.Mailbox Time + , valueSignal : Signal Time + , selectable : Bool , value : Date.Date , date : Date.Date , disabled : Bool , readonly : Bool } -{-| Actions that a calendar can make: - - **PreviousMonth** - Steps the calendar to show previous month - - **NextMonth** - Steps the calendar to show next month - - **Select Date.Date** - Selects a date --} +{-| Actions that a calendar can make. -} type Action - = NextMonth + = Select Date.Date | PreviousMonth - | Select Date.Date + | NextMonth + | Tasks () {-| Initializes a calendar with the given values. @@ -58,25 +64,34 @@ type Action -} init : Date.Date -> Model init date = - { selectable = True - , disabled = False - , readonly = False - , value = date - , date = date - } + let + mailbox = Signal.mailbox 0 + in + { valueSignal = Signal.dropRepeats mailbox.signal + , mailbox = mailbox + , selectable = True + , disabled = False + , readonly = False + , value = date + , date = date + } {-| Updates a calendar. -} -update : Action -> Model -> Model +update : Action -> Model -> (Model, Effects.Effects Action) update action model = case action of NextMonth -> - { model | date = Ext.Date.nextMonth model.date } + ({ model | date = Ext.Date.nextMonth model.date }, Effects.none) PreviousMonth -> - { model | date = Ext.Date.previousMonth model.date } + ({ model | date = Ext.Date.previousMonth model.date }, Effects.none) Select date -> - { model | value = date } + ({ model | value = date } + , Ext.Signal.sendAsEffect model.mailbox.address (Date.toTime date) Tasks) + + Tasks _ -> + (model, Effects.none) {-| Renders a calendar. -} view : Signal.Address Action -> Model -> Html.Html @@ -135,10 +150,12 @@ render address model = ] ] [ container + , node "ui-calendar-header" [] + (List.map (\item -> span [] [text item]) dayNames) , node "ui-calendar-table" [] cells ] -{-| Sets the value of a calendar -} +{-| Sets the value of a calendar. -} setValue : Date.Date -> Model -> Model setValue date model = { model | value = date } @@ -176,6 +193,11 @@ paddingLeft date = Date.Sat -> 5 Date.Sun -> 6 +-- Short names of days +dayNames : List String +dayNames = + ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] + -- Renders a single cell renderCell : Signal.Address Action -> Date.Date -> Model -> Html.Html renderCell address date model = diff --git a/source/Ui/Checkbox.elm b/source/Ui/Checkbox.elm index bedd959..461453b 100644 --- a/source/Ui/Checkbox.elm +++ b/source/Ui/Checkbox.elm @@ -1,7 +1,7 @@ module Ui.Checkbox (Model, Action(..), init, update, setValue, view, toggleView, radioView) where -{-| Checkbox component. +{-| Checkbox component with three different views. # Model @docs Model, Action, init, update @@ -17,37 +17,62 @@ import Html.Events exposing (onClick) import Html.Extra exposing (onKeys) import Html exposing (node) import Html.Lazy + +import Ext.Signal +import Effects import Dict import Ui -{-| Representation of a checkbox. -} +{-| Representation of a checkbox: + - **disabled** - Whether or not the checkbox is disabled + - **readonly** - Whether or not the checkbox is readonly + - **value** - Whether or not the checkbox is checked + - **valueSignal** - The checkboxes value as a signal + - **mailbox** (internal) - The mailbox of the checkbox +-} type alias Model = - { disabled : Bool + { mailbox : Signal.Mailbox Bool + , valueSignal : Signal Bool + , disabled : Bool , readonly : Bool , value : Bool } -{-| Actions that a checkbox can make: - - **Toggle** - Changes the value to the opposite --} +{-| Actions that a checkbox can make. -} type Action - = Toggle + = Tasks () + | Toggle + +{-| Initiaizes a checkbox with the given value. -{-| Initiaizes a checkbox with the given value. -} + Checkbox.init False +-} init : Bool -> Model init value = - { disabled = False - , readonly = False - , value = value - } + let + mailbox = Signal.mailbox value + in + { valueSignal = Signal.dropRepeats mailbox.signal + , mailbox = mailbox + , disabled = False + , readonly = False + , value = value + } {-| Updates a checkbox. -} -update : Action -> Model -> Model +update : Action -> Model -> (Model, Effects.Effects Action) update action model = case action of Toggle -> - { model | value = not model.value } + let + value = not model.value + in + ({ model | value = value } + , Ext.Signal.sendAsEffect model.mailbox.address value Tasks) + + Tasks _ -> + (model, Effects.none) {-| Sets the value of a checkbox to the given one. -} setValue : Bool -> Model -> Model diff --git a/source/Ui/Chooser.elm b/source/Ui/Chooser.elm index eb323c5..16de3b8 100644 --- a/source/Ui/Chooser.elm +++ b/source/Ui/Chooser.elm @@ -1,5 +1,5 @@ module Ui.Chooser - (Model, Item, Action(..), init, update, close, toggleItem, + (Model, Item, Action, init, update, close, toggleItem, getFirstSelected, view, updateData, selectFirst, select) where {-| This is a component for selecting a single / multiple items @@ -24,6 +24,8 @@ import Html.Lazy import Set exposing (Set) import Native.Browser +import Ext.Signal +import Effects import String import Regex import List @@ -42,23 +44,27 @@ type alias Item = } {-| Representation of a chooser component: - - **placeholder** - The text to display when no item is selected - - **selected** - A *Set* of values of selected items - - **searchable** - Whether or not a user can filter the items + - **deselectable** - True if the component can have no selected value false if not - **closeOnSelect** - Whether or not to close the dropdown after selecting - **data** - List of items to select from and display in the dropdown - **multiple** - Whether or not the user can select multiple items - - **deselectable** - True if the component can have no selected value false if not + - **placeholder** - The text to display when no item is selected + - **searchable** - Whether or not a user can filter the items + - **readonly** - Whether or not the chooser is readonly - **disabled** - Whether or not the chooser is disabled - - **render** - Function to render the items - - **value** - (Internal) The value of the input - - **intended** - (Internal) The currently intended value (for keyboard selection) + - **selected** - A *Set* of values of selected items + - **valueSignal** - The choosers value as a signal - **open** - Whether or not the dropdown is open - - **readonly** - Whether or not the dropdown is readonly - - **dropdownPosition** (Internal) - Where the dropdown is positioned + - **render** - Function to render the items + - **intended** (internal) - The currently intended value (for keyboard selection) + - **dropdownPosition** (internal) - Where the dropdown is positioned + - **mailbox** (internal) - The mailbox of the chooser + - **value** (internal) - The value of the input -} type alias Model = - { dropdownPosition : String + { mailbox : Signal.Mailbox (Set String) + , valueSignal : Signal (Set String) + , dropdownPosition : String , render : Item -> Html , selected : Set String , placeholder : String @@ -74,15 +80,7 @@ type alias Model = , open : Bool } -{-| Actions that a chooser can make: - - **Filter** - Filters the list of choises - - **Focus** - Opens the dropdown - - **Close** - Closes the dropdown - - **Select** - Selects the given value - - **Next** - Intends the next item for selection - - **Prev** - Intends the previous item for selection - - **Enter** - Selects the currently intended selection --} +{-| Actions that a chooser can make. -} type Action = Focus Html.Extra.DropdownDimensions | Close Html.Extra.DropdownDimensions @@ -91,6 +89,7 @@ type Action | Prev Html.Extra.DropdownDimensions | Filter String | Select String + | Tasks () | Blur {-| Initializes a chooser with the given values. @@ -101,14 +100,17 @@ init : List Item -> String -> String -> Model init data placeholder value = let selected = if value == "" then Set.empty else Set.singleton value + mailbox = Signal.mailbox selected in - { render = (\item -> span [] [text item.label]) + { valueSignal = Signal.dropRepeats mailbox.signal + , render = (\item -> span [] [text item.label]) , dropdownPosition = "bottom" , placeholder = placeholder , closeOnSelect = False , deselectable = False , selected = selected , searchable = False + , mailbox = mailbox , multiple = False , disabled = False , readonly = False @@ -120,45 +122,53 @@ init data placeholder value = |> intendFirst {-| Updates a chooser. -} -update : Action -> Model -> Model +update : Action -> Model -> (Model, Effects.Effects Action) update action model = - let - model' = - case action of - Filter value -> - setValue value model - |> intendFirst - - Focus dimensions -> - Dropdown.openWithDimensions dimensions model - |> intendFirst - - Close _ -> - close model - - Blur -> - close model - - Enter dimensions -> - toggleItem model.intended model - |> Dropdown.toggleWithDimensions dimensions - - Next dimensions -> - { model | intended = Intendable.next - model.intended - (availableItems model) } - |> Dropdown.openWithDimensions dimensions - - Prev dimensions -> - { model | intended = Intendable.previous - model.intended - (availableItems model) } - |> Dropdown.openWithDimensions dimensions - - Select value -> - toggleItemAndClose value model - in - model' + case action of + Enter dimensions -> + let + (updatedModel, effect) = toggleItem model.intended model + in + (Dropdown.toggleWithDimensions dimensions updatedModel, effect) + + Select value -> + toggleItemAndClose value model + + _ -> + (update' action model, Effects.none) + +-- Non effects updates +update' : Action -> Model -> Model +update' action model = + case action of + Filter value -> + setValue value model + |> intendFirst + + Focus dimensions -> + Dropdown.openWithDimensions dimensions model + |> intendFirst + + Close _ -> + close model + + Blur -> + close model + + Next dimensions -> + { model | intended = Intendable.next + model.intended + (availableItems model) } + |> Dropdown.openWithDimensions dimensions + + Prev dimensions -> + { model | intended = Intendable.previous + model.intended + (availableItems model) } + |> Dropdown.openWithDimensions dimensions + + _ -> + model {-| Renders a chooser. -} view : Signal.Address Action -> Model -> Html.Html @@ -206,18 +216,19 @@ render address model = ] ++ actions) []] ++ dropdown) {-| Selects the given value of chooser. -} -select : String -> Model -> Model +select : String -> Model -> (Model, Effects.Effects Action) select value model = { model | selected = Set.singleton value } + |> sendValue {-| Closes the dropdown of a chooser. -} close : Model -> Model close model = Dropdown.close model - |> setValue "" + |> setValue "" {-| Selects or deselects the item with the given value. -} -toggleItem : String -> Model -> Model +toggleItem : String -> Model -> (Model, Effects.Effects Action) toggleItem value model = if model.multiple then toggleMultipleItem value model @@ -235,23 +246,33 @@ updateData data model = { model | data = data } {-| Selects the first item if available. -} -selectFirst : Model -> Model +selectFirst : Model -> (Model, Effects.Effects Action) selectFirst model = case List.head model.data of - Just item -> { model | selected = Set.singleton item.value } - _ -> model + Just item -> + { model | selected = Set.singleton item.value } + |> sendValue + _ -> + (model, Effects.none) -- ========================= PRIVATE ========================= +-- Sends the current value of the model to the signal +sendValue : Model -> (Model, Effects.Effects Action) +sendValue model = + (model, Ext.Signal.sendAsEffect model.mailbox.address model.selected Tasks) + {- Select or deslect a single item with the given value and closes the dropdown if needed. -} -toggleItemAndClose : String -> Model -> Model +toggleItemAndClose : String -> Model -> (Model, Effects.Effects Action) toggleItemAndClose value model = - toggleItem value model - |> closeIfShouldClose + let + (model, effect) = toggleItem value model + in + (closeIfShouldClose model, effect) -- Toggle item if multiple is True. -toggleMultipleItem : String -> Model -> Model +toggleMultipleItem : String -> Model -> (Model, Effects.Effects Action) toggleMultipleItem value model = let updated_set = @@ -264,14 +285,19 @@ toggleMultipleItem value model = Set.insert value model.selected in {model | selected = updated_set } + |> sendValue -- Toggle item if multiple is False. -toggleSingleItem : String -> Model -> Model +toggleSingleItem : String -> Model -> (Model, Effects.Effects Action) toggleSingleItem value model = - if (Set.member value model.selected) && model.deselectable then - { model | selected = Set.empty } - else - { model | selected = Set.singleton value } + let + updatedModel = + if (Set.member value model.selected) && model.deselectable then + { model | selected = Set.empty } + else + { model | selected = Set.singleton value } + in + sendValue updatedModel -- Intends the first item if it is available. intendFirst : Model -> Model diff --git a/source/Ui/ColorPanel.elm b/source/Ui/ColorPanel.elm index 57f8e0d..3abad76 100644 --- a/source/Ui/ColorPanel.elm +++ b/source/Ui/ColorPanel.elm @@ -16,23 +16,30 @@ module Ui.ColorPanel import Html.Attributes exposing (style, classList) import Html.Extra exposing (onWithDimensions) import Html exposing (node, div, text) +import Html.Lazy + import Ext.Color exposing (Hsv) import Color exposing (Color) -import Html.Lazy +import Ext.Signal +import Effects import Ui.Helpers.Drag as Drag import Ui {-| Representation of a color panel: + - **valueSignal** - The color panels value as a signal + - **readonly** - Whether or not the color panel is editable + - **disabled** - Whether or not the color panel is disabled + - **value** - The current vlaue - **drag** (internal) - The drag model of the value / saturation rectangle - **alphaDrag** (internal) - The drag model of the alpha slider - **hueDrag** (internal) - The drag model of the hue slider - - **value** - The current vlaue - - **readonly** - Whether the color panel is editable - - **disabled** - Whether the color panel is disabled + - **mailbox** (internal) - The mailbox of the color panel -} type alias Model = - { alphaDrag : Drag.Model + { mailbox : Signal.Mailbox Hsv + , valueSignal : Signal Hsv + , alphaDrag : Drag.Model , hueDrag : Drag.Model , drag : Drag.Model , disabled : Bool @@ -45,6 +52,7 @@ type Action = LiftAlpha (Html.Extra.PositionAndDimension) | LiftRect (Html.Extra.PositionAndDimension) | LiftHue (Html.Extra.PositionAndDimension) + | Tasks () {-| Initializes a color panel with the given Elm color. @@ -52,16 +60,22 @@ type Action -} init : Color -> Model init color = - { value = Ext.Color.toHsv color - , alphaDrag = Drag.init - , hueDrag = Drag.init - , drag = Drag.init - , disabled = False - , readonly = False - } + let + hsv = Ext.Color.toHsv color + mailbox = Signal.mailbox hsv + in + { valueSignal = Signal.dropRepeats mailbox.signal + , alphaDrag = Drag.init + , hueDrag = Drag.init + , drag = Drag.init + , disabled = False + , readonly = False + , mailbox = mailbox + , value = hsv + } {-| Updates a color panel. -} -update : Action -> Model -> Model +update : Action -> Model -> (Model, Effects.Effects Action) update action model = case action of LiftRect {dimensions, position} -> @@ -76,6 +90,9 @@ update action model = { model | hueDrag = Drag.lift dimensions position model.hueDrag } |> handleMove (round position.pageX) (round position.pageY) + Tasks _ -> + (model, Effects.none) + {-| Renders a color panel. -} view : Signal.Address Action -> Model -> Html.Html view address model = @@ -134,7 +151,7 @@ render address model = ] {-| Updates a color panel color by coordinates. -} -handleMove : Int -> Int -> Model -> Model +handleMove : Int -> Int -> Model -> (Model, Effects.Effects Action) handleMove x y model = let color = @@ -147,9 +164,10 @@ handleMove x y model = else model.value in - { model | value = color } + ({ model | value = color } + , Ext.Signal.sendAsEffect model.mailbox.address color Tasks) -{-| Updates a color panel, stopping the drags if the mouse isnt pressed. -} +{-| Updates a color panel, stopping the drags if the mouse isn't pressed. -} handleClick : Bool -> Model -> Model handleClick value model = { model | alphaDrag = Drag.handleClick value model.alphaDrag diff --git a/source/Ui/ColorPicker.elm b/source/Ui/ColorPicker.elm index 73e367a..8aba08f 100644 --- a/source/Ui/ColorPicker.elm +++ b/source/Ui/ColorPicker.elm @@ -19,7 +19,8 @@ import Html exposing (node, div, text) import Html.Lazy import Signal exposing (forwardTo) -import Ext.Color +import Ext.Color exposing (Hsv) +import Effects import Color import Dict @@ -28,15 +29,17 @@ import Ui.ColorPanel as ColorPanel import Ui {-| Representation of a color picker: - - **colorPanel** (internal) - The model of a color panel + - **readonly** - Whether or not the color picker is readonly - **disabled** - Whether or not the color picker is disabled + - **valueSignal** - The color pickers value as a signal - **open** - Whether or not the color picker is open - - **readonly** - Whether or not the color picker is readonly - **dropdownPosition** (Internal) - Where the dropdown is positioned + - **colorPanel** (internal) - The model of a color panel -} type alias Model = { colorPanel : ColorPanel.Model , dropdownPosition : String + , valueSignal : Signal Hsv , disabled : Bool , readonly : Bool , open : Bool @@ -44,10 +47,11 @@ type alias Model = {-| Actions that a color picker can make. -} type Action - = Focus Html.Extra.DropdownDimensions + = Toggle Html.Extra.DropdownDimensions + | Focus Html.Extra.DropdownDimensions | Close Html.Extra.DropdownDimensions - | Toggle Html.Extra.DropdownDimensions | ColorPanel ColorPanel.Action + | ColorChanged Hsv | Blur {-| Initializes a color picker with the given color. @@ -56,16 +60,33 @@ type Action -} init : Color.Color -> Model init color = - { colorPanel = ColorPanel.init color - , dropdownPosition = "bottom" - , disabled = False - , readonly = False - , open = False - } + let + colorPanel = ColorPanel.init color + in + { valueSignal = colorPanel.valueSignal + , dropdownPosition = "bottom" + , colorPanel = colorPanel + , disabled = False + , readonly = False + , open = False + } {-| Updates a color picker. -} -update : Action -> Model -> Model +update : Action -> Model -> (Model, Effects.Effects Action) update action model = + case action of + ColorPanel act -> + let + (colorPanel, effect) = ColorPanel.update act model.colorPanel + in + ({ model | colorPanel = colorPanel }, Effects.map ColorPanel effect) + + _ -> + (update' action model, Effects.none) + +-- Effectless update +update' : Action -> Model -> Model +update' action model = case action of Focus dimensions -> Dropdown.openWithDimensions dimensions model @@ -79,8 +100,8 @@ update action model = Toggle dimensions -> Dropdown.toggleWithDimensions dimensions model - ColorPanel act -> - { model | colorPanel = ColorPanel.update act model.colorPanel } + _ -> + model {-| Renders a color picker. -} view : Signal.Address Action -> Model -> Html.Html @@ -88,9 +109,12 @@ view address model = Html.Lazy.lazy2 render address model {-| Updates a color picker by coordinates. -} -handleMove : Int -> Int -> Model -> Model +handleMove : Int -> Int -> Model -> (Model, Effects.Effects Action) handleMove x y model = - { model | colorPanel = ColorPanel.handleMove x y model.colorPanel } + let + (colorPanel, effect) = ColorPanel.handleMove x y model.colorPanel + in + ({ model | colorPanel = colorPanel }, Effects.map ColorPanel effect) {-| Updates a color picker, stopping the drags if the mouse isnt pressed. -} handleClick : Bool-> Model -> Model diff --git a/source/Ui/Container.elm b/source/Ui/Container.elm index 45253b1..b86b95d 100644 --- a/source/Ui/Container.elm +++ b/source/Ui/Container.elm @@ -22,16 +22,16 @@ type alias Model = -- Options for row rowOptions : Model rowOptions = - { align = "stretch" - , direction = "row" + { direction = "row" + , align = "stretch" , compact = False } -- Options for column columnOptions : Model columnOptions = - { align = "stretch" - , direction = "column" + { direction = "column" + , align = "stretch" , compact = False } @@ -64,7 +64,7 @@ render model attributes children = classes : Model -> Html.Attribute classes model = classList [ - ("align-" ++ model.align, True), ("direction-" ++ model.direction, True), + ("align-" ++ model.align, True), ("compact", model.compact) ] diff --git a/source/Ui/DatePicker.elm b/source/Ui/DatePicker.elm index 2c00322..2cee684 100644 --- a/source/Ui/DatePicker.elm +++ b/source/Ui/DatePicker.elm @@ -1,5 +1,4 @@ -module Ui.DatePicker - (Model, Action, init, update, view, setValue) where +module Ui.DatePicker (Model, Action, init, update, view, setValue) where {-| Date picker input component. @@ -19,9 +18,12 @@ import Html exposing (node, div, text) import Html.Lazy import Signal exposing (forwardTo) +import Ext.Signal +import Effects import Dict import Date.Format exposing (format) +import Time import Date import Ui.Helpers.Dropdown as Dropdown @@ -29,16 +31,23 @@ import Ui.Calendar as Calendar import Ui {-| Representation of a date picker component: - - **calendar** - The model of a calendar - - **format** - The format of the date to render in the input - **closeOnSelect** - Whether or not to close the dropdown after selecting - - **disabled** - Whether or not the chooser is disabled - - **readonly** - Whether or not the dropdown is readonly + - **format** - The format of the date to render in the input + - **readonly** - Whether or not the date picker is readonly + - **disabled** - Whether or not the date picker is disabled + - **valueSignal** - The date pickers value as a signal - **open** - Whether or not the dropdown is open + - **signal** - The date pickers signal + - **dropdownPosition** (internal) - The dropdowns position + - **mailbox** (internal) - The date pickers mailbox + - **calendar** (internal) - The model of a calendar -} type alias Model = - { calendar : Calendar.Model + { mailbox : Signal.Mailbox Time.Time + , valueSignal : Signal Time.Time + , calendar : Calendar.Model , dropdownPosition : String + , signal : Signal Action , closeOnSelect : Bool , format : String , disabled : Bool @@ -46,21 +55,16 @@ type alias Model = , open : Bool } -{-| Actions that a date picker can make: - - **Focus** - Opens the dropdown - - **Close** - Closes the dropdown - - **Toggle** - Toggles the dropdown - - **Decrement** - Selects the previous day - - **Increment** - Selects the next day - - **Calendar** - Calendar actions --} +{-| Actions that a date picker can make. -} type Action - = Focus Html.Extra.DropdownDimensions - | Increment Html.Extra.DropdownDimensions + = Increment Html.Extra.DropdownDimensions | Decrement Html.Extra.DropdownDimensions - | Close Html.Extra.DropdownDimensions | Toggle Html.Extra.DropdownDimensions + | Focus Html.Extra.DropdownDimensions + | Close Html.Extra.DropdownDimensions | Calendar Calendar.Action + | Select Time.Time + | Tasks () | Blur {-| Initializes a date picker with the given values. @@ -69,18 +73,48 @@ type Action -} init : Date.Date -> Model init date = - { calendar = Calendar.init date - , dropdownPosition = "bottom" - , closeOnSelect = False - , format = "%Y-%m-%d" - , disabled = False - , readonly = False - , open = False - } + let + calendar = Calendar.init date + mailbox = Signal.mailbox 0 + in + { signal = Signal.map Select calendar.valueSignal + , valueSignal = mailbox.signal + , dropdownPosition = "bottom" + , closeOnSelect = False + , calendar = calendar + , format = "%Y-%m-%d" + , mailbox = mailbox + , disabled = False + , readonly = False + , open = False + } {-| Updates a date picker. -} -update : Action -> Model -> Model +update : Action -> Model -> (Model, Effects.Effects Action) update action model = + case action of + Calendar act -> + let + (calendar, effect) = Calendar.update act model.calendar + in + ({ model | calendar = calendar }, Effects.map Calendar effect) + + Select time -> + let + updatedModel = + if model.closeOnSelect then + Dropdown.close model + else + model + in + (updatedModel, Ext.Signal.sendAsEffect model.mailbox.address time Tasks) + + _ -> + (update' action model, Effects.none) + +-- Effectless updates +update' : Action -> Model -> Model +update' action model = case action of Focus dimensions -> Dropdown.openWithDimensions dimensions model @@ -102,18 +136,7 @@ update action model = { model | calendar = Calendar.nextDay model.calendar } |> Dropdown.openWithDimensions dimensions - Calendar act -> - let - updatedModel = - { model | calendar = Calendar.update act model.calendar } - in - case act of - Calendar.Select date -> - if model.closeOnSelect then - Dropdown.close updatedModel - else - updatedModel - _ -> updatedModel + _ -> model {-| Renders a date picker. -} view : Signal.Address Action -> Model -> Html.Html diff --git a/source/Ui/DropdownMenu.elm b/source/Ui/DropdownMenu.elm index 45dec9d..80c68b7 100644 --- a/source/Ui/DropdownMenu.elm +++ b/source/Ui/DropdownMenu.elm @@ -1,7 +1,7 @@ module Ui.DropdownMenu (Model, Action, init, update, view, item, handleClick, close) where -{-| Dropdown menu that is allways visible on the screen. +{-| Dropdown menu that is always visible on the screen. # Model @docs Model, Action, init, update @@ -12,19 +12,29 @@ module Ui.DropdownMenu # Functions @docs handleClick, close -} +import Html.Extra exposing ( onStopNothing, WindowDimensions + , windowDimensionsDecoder) import Html.Attributes exposing (style, classList) import Html.Events exposing (onWithOptions) -import Html.Extra exposing (onStopNothing, WindowDimensions, windowDimensionsDecoder) import Html exposing (node) -import Json.Decode as Json exposing ((:=)) - -{-| Represents a dropdown menu. -} +import Json.Decode as Json + +{-| Represents a dropdown menu: + - **favoredSides** - The sides to open the dropdown when there is space + - **horizontal** - Either "left" or "right" + - **vertical** - Either "top" or "bottom" + - **offsetX** - The x-axis offset for the dropdown + - **offsetY** - The y-axis offset for the dropdown + - **open** - Whether or not the dropdown is open + - **left** (internal) - The left position of the dropdown + - **top** (internal) - The top position of the dropdown +-} type alias Model = - { top : Float + { offsetX : Float + , offsetY : Float , left : Float + , top : Float , open : Bool - , offsetX : Float - , offsetY : Float , favoredSides : { horizontal : String , vertical : String } @@ -37,11 +47,11 @@ type Action {-| Initializes a dropdown. -} init : Model init = - { top = 0 - , left = 0 - , open = False + { open = False , offsetX = 0 , offsetY = 5 + , left = 0 + , top = 0 , favoredSides = { horizontal = "left" , vertical = "bottom" } diff --git a/source/Ui/IconButton.elm b/source/Ui/IconButton.elm index 31dc0c4..801183d 100644 --- a/source/Ui/IconButton.elm +++ b/source/Ui/IconButton.elm @@ -27,7 +27,14 @@ import Html.Lazy import Ui.Button exposing (attributes) import Ui -{-| Representation of an icon button. -} +{-| Representation of an icon button: + - **disabled** - Whether or not the icon button is disabled + - **glyph** - The glyph to use form IonIcons + - **side** - The side to display the icon + - **kind** - The type of the icon button + - **size** - The size of the icon button + - **text** - The text of the icon button +-} type alias Model = { disabled : Bool , glyph : String diff --git a/source/Ui/Image.elm b/source/Ui/Image.elm index 939a711..d4091a8 100644 --- a/source/Ui/Image.elm +++ b/source/Ui/Image.elm @@ -1,5 +1,4 @@ -module Ui.Image - (Model, Action, init, update, view) where +module Ui.Image (Model, Action, init, update, view) where {-| Image component that fades when loaded. @@ -14,16 +13,23 @@ import Html.Extra exposing (onLoad) import Html exposing (node, img) import Html.Lazy -{-| Representation of an image. -} +{-| Representation of an image: + - **loaded** (internal) - Whether or not the image is loaded + - **src** - The url for the image +-} type alias Model = { loaded : Bool , src : String } {-| Actions that an image can make. -} -type Action = Loaded +type Action + = Loaded + +{-| Initializes an image from an URL. -{-| Initializes an image from an URL. -} + Image.init "http://some.url/image.png" +-} init : String -> Model init url = { loaded = False diff --git a/source/Ui/InplaceInput.elm b/source/Ui/InplaceInput.elm index c5832d0..aedf4f9 100644 --- a/source/Ui/InplaceInput.elm +++ b/source/Ui/InplaceInput.elm @@ -1,5 +1,4 @@ -module Ui.InplaceInput - (Model, Action, init, update, view) where +module Ui.InplaceInput (Model, Action, init, update, view) where {-| Inplace editing textarea / input component. @@ -9,13 +8,14 @@ module Ui.InplaceInput # View @docs view -} -import Html exposing (node, textarea, div, text, button) import Html.Extra exposing (onEnter, onKeys) -import Html.Attributes exposing (value) +import Html exposing (node, div, text) import Html.Events exposing (onClick) import Html.Lazy import Signal exposing (forwardTo) +import Ext.Signal +import Effects import String import Native.Browser @@ -25,66 +25,92 @@ import Ui.Button import Ui.Textarea import Ui -import Debug exposing (log) - {-| Represents an inplace input: - - **ctrlSave** - Whether or not to save on ctrl+enter - **required** - Whether or not to disable the component if the value is empty - **disabled** - Whether or not the component is disabled - **readonly** - Whether or not the component is readonly + - **ctrlSave** - Whether or not to save on ctrl+enter - **value** - The value of the component + - **open** (internal) - Whether or not the component is open + - **mailbox** (internal) - The mailbox of the textarea - **textarea** (internal) - The state of the textarea - - **open** (internal) - Whether the component is open or not -} type alias Model = - { open : Bool + { mailbox : Signal.Mailbox String + , textarea : Ui.Textarea.Model + , valueSignal : Signal String , required : Bool , ctrlSave : Bool , disabled : Bool , readonly : Bool , value : String - , textarea : Ui.Textarea.Model + , open : Bool } {-| Actions that an inplace input can make. -} type Action - = Edit - | Textarea Ui.Textarea.Action - | Save + = Textarea Ui.Textarea.Action + | Tasks () | Close + | Save + | Edit {-| Initializes an inplace input with the given value. -} init : String -> Model init value = - { required = True - , open = False - , ctrlSave = True - , disabled = False - , readonly = False - , value = value - , textarea = Ui.Textarea.init value - } + let + mailbox = Signal.mailbox value + in + { valueSignal = Signal.dropRepeats mailbox.signal + , textarea = Ui.Textarea.init value + , mailbox = mailbox + , disabled = False + , readonly = False + , required = True + , ctrlSave = True + , value = value + , open = False + } {-| Updates an inplace input. -} -update: Action -> Model -> Model +update : Action -> Model -> (Model, Effects.Effects Action) update action model = case action of Textarea act -> - { model | textarea = Ui.Textarea.update act model.textarea } + let + (textarea, effect) = Ui.Textarea.update act model.textarea + in + ({ model | textarea = textarea }, Effects.map Textarea effect) + Save -> + if (isEmpty model) && model.required then + (model, Effects.none) + else + ( close { model | value = model.textarea.value } + , Ext.Signal.sendAsEffect + model.mailbox.address + model.textarea.value + Tasks ) + + _ -> + (update' action model, Effects.none) + +-- Effectless updates +update' : Action -> Model -> Model +update' action model = + case action of Edit -> if model.disabled then model - else open model + else + open model Close -> close model - Save -> - if (isEmpty model) && model.required then - model - else - close { model | value = model.textarea.value } + _ -> + model + {-| Renders an inplace input. -} view : Signal.Address Action -> Model -> Html.Html @@ -109,29 +135,18 @@ form address model = let disabled = (isEmpty model) && model.required in - Ui.Container.view { align = "stretch" - , direction = "column" - , compact = False - } - [ onEnter model.ctrlSave address Save - , onKeys address [(27, Close)] - ] + Ui.Container.column [ onEnter model.ctrlSave address Save + , onKeys address [(27, Close)] + ] [ Ui.Textarea.view (forwardTo address Textarea) model.textarea - , Ui.Container.view { align = "stretch" - , direction = "row" - , compact = False - } [] + , Ui.Container.row [] [ Ui.Button.view address Save { disabled = disabled , kind = "primary" , text = "Save" , size = "medium" } , Ui.spacer - , Ui.Button.view address Close { disabled = False - , kind = "secondary" - , text = "Close" - , size = "medium" - } + , Ui.Button.secondary "Close" address Close ] ] diff --git a/source/Ui/Input.elm b/source/Ui/Input.elm index 4939dd1..8b4a025 100644 --- a/source/Ui/Input.elm +++ b/source/Ui/Input.elm @@ -12,28 +12,40 @@ module Ui.Input # Functions @docs setValue -} -import Html.Attributes exposing (value, spellcheck, placeholder, type') +import Html.Attributes exposing (value, spellcheck, placeholder, type', + readonly, disabled, classList) import Html.Extra exposing (onInput) import Html exposing (node) import Html.Lazy + +import Ext.Signal +import Effects +import Signal import String {-| Representation of an input: - **placeholder** - The text to display when there is no value - - **value** - The value + - **valueSignal** - The value of the input as a signal + - **disabled** - Whether or not the input is disabled + - **readonly** - Whether or not the input is readonly - **kind** - The type of the input + - **value** - The value + - **mailbox** (internal) - The mailbox of the input -} type alias Model = - { placeholder : String + { mailbox : Signal.Mailbox String + , valueSignal : Signal String + , placeholder : String + , disabled : Bool + , readonly : Bool , value : String , kind : String } -{-| Actions that an input can make: - - **input** - Updates the value after an input event --} +{-| Actions that an input can make. -} type Action = Input String + | Tasks () {-| Initializes an input. @@ -41,17 +53,27 @@ type Action -} init : String -> Model init value = - { placeholder = "" - , value = value - , kind = "text" - } + let + mailbox = Signal.mailbox "" + in + { valueSignal = Signal.dropRepeats mailbox.signal + , mailbox = mailbox + , placeholder = "" + , disabled = False + , readonly = False + , value = value + , kind = "text" + } {-| Updates an input. -} -update : Action -> Model -> Model +update : Action -> Model -> (Model, Effects.Effects Action) update action model = case action of Input value -> - { model | value = value } + setValue value model + + Tasks _ -> + (model, Effects.none) {-| Renders an input. -} view : Signal.Address Action -> Model -> Html.Html @@ -61,15 +83,23 @@ view address model = -- Render internal render : Signal.Address Action -> Model -> Html.Html render address model = - node "input" [ placeholder model.placeholder - , onInput address Input - , value model.value - , spellcheck False - , type' model.kind - ] - [] + node "ui-input" [classList [ ("disabled", model.disabled) + , ("readonly", model.readonly) + ] + ] + [ node "input" [ placeholder model.placeholder + , onInput address Input + , value model.value + , spellcheck False + , type' model.kind + , readonly model.readonly + , disabled model.disabled + ] + [] + ] {-| Sets the value of the model. -} -setValue : String -> Model -> Model +setValue : String -> Model -> (Model, Effects.Effects Action) setValue value model = - { model | value = value } + ( { model | value = value } + , Ext.Signal.sendAsEffect model.mailbox.address value Tasks) diff --git a/source/Ui/Modal.elm b/source/Ui/Modal.elm index a29d3bb..b3719b3 100644 --- a/source/Ui/Modal.elm +++ b/source/Ui/Modal.elm @@ -20,10 +20,10 @@ import Html.Lazy import Ui {-| Representation of a modal window: - - **backdrop** - Whether or not to show a backdrop - - **open** - Whether or not the modal is open - **closeable** - Whether or not the modal is closeable by clicking on the backdrop or the close button. + - **backdrop** - Whether or not to show a backdrop + - **open** - Whether or not the modal is open -} type alias Model = { closeable : Bool diff --git a/source/Ui/NotificationCenter.elm b/source/Ui/NotificationCenter.elm index 6f1553a..581d61a 100644 --- a/source/Ui/NotificationCenter.elm +++ b/source/Ui/NotificationCenter.elm @@ -1,5 +1,4 @@ -module Ui.NotificationCenter - (Model, Action(..), init, update, view, notify) where +module Ui.NotificationCenter (Model, Action, init, update, view, notify) where {-| Notification center for displaying messages to the user. @@ -24,7 +23,11 @@ import Effects import Vendor import Task -{-| Representation of a notification center. -} +{-| Representation of a notification center: + - **timeout** - The timeout of the notification before it's hidden + - **notifications** - The list of notifications that is displayed + - **duration** - The duration of the notifications animation +-} type alias Model = { notifications : List Notification , duration : Float @@ -60,8 +63,10 @@ update action model = case action of AutoHide id -> autoHide id model + Remove id -> remove id model + Hide id -> hide id model diff --git a/source/Ui/NumberPad.elm b/source/Ui/NumberPad.elm index 2831637..15b7a7b 100644 --- a/source/Ui/NumberPad.elm +++ b/source/Ui/NumberPad.elm @@ -19,40 +19,48 @@ import Html exposing (node, text) import Html.Lazy import Number.Format exposing (prettyInt) +import Ext.Signal +import Effects +import Signal import String import Dict import Ui {-| Representation of a number pad. - - **value** - The current value + - **readonly** - Whether or not the number pad is interactive + - **disabled** - Whether or not the number pad is disabled - **maximumDigits** - The maximum length of the value - **format** - Wheter or not to format the value - **prefix** - The prefix to use + - **value** - The current value - **affix** - The affix to use -} type alias Model = - { value: Int + { mailbox : Signal.Mailbox Int + , valueSignal : Signal Int , maximumDigits : Int - , format : Bool - , prefix : String - , affix : String , disabled : Bool , readonly : Bool + , prefix : String + , affix : String + , format : Bool + , value: Int } {-| Represents elements for the view: - - **bottomLeft** - What to display in the bottom left button - **bottomRight** - What to display in the bottom right button + - **bottomLeft** - What to display in the bottom left button -} type alias ViewModel = - { bottomLeft : Html.Html - , bottomRight : Html.Html + { bottomRight : Html.Html + , bottomLeft : Html.Html } {-| Actions that a number pad can make. -} type Action = Pressed Int + | Tasks () | Delete {-| Initializes a number pad with the given value. @@ -61,23 +69,39 @@ type Action -} init : Int -> Model init value = - { value = value - , maximumDigits = 10 - , format = False - , prefix = "" - , affix = "" - , disabled = False - , readonly = False - } + let + mailbox = Signal.mailbox value + in + { valueSignal = Signal.dropRepeats mailbox.signal + , maximumDigits = 10 + , mailbox = mailbox + , disabled = False + , readonly = False + , format = True + , value = value + , prefix = "" + , affix = "" + } {-| Updates a number pad. -} -update : Action -> Model -> Model +update : Action -> Model -> (Model, Effects.Effects Action) update action model = case action of Pressed number -> addDigit number model + |> sendValue + Delete -> deleteDigit model + |> sendValue + + Tasks _ -> + (model, Effects.none) + +-- Sends the value to the signal +sendValue : Model -> (Model, Effects.Effects Action) +sendValue model = + (model, Ext.Signal.sendAsEffect model.mailbox.address model.value Tasks) {-| Renders a number pad. -} view : Signal.Address Action -> ViewModel -> Model -> Html.Html @@ -144,9 +168,10 @@ render address viewModel model = ] {-| Sets the value of a number pad. -} -setValue : Int -> Model -> Model +setValue : Int -> Model -> (Model, Effects.Effects Action) setValue value model = { model | value = clampValue value model } + |> sendValue {- Renders a digit button. -} renderButton : Signal.Address Action -> Int -> Model -> Html.Html diff --git a/source/Ui/NumberRange.elm b/source/Ui/NumberRange.elm index 8b8a1da..31871f2 100644 --- a/source/Ui/NumberRange.elm +++ b/source/Ui/NumberRange.elm @@ -15,7 +15,8 @@ double clicking on the component. # Functions @docs focus, handleClick, handleMove, setValue, increment, decrement -} -import Html.Extra exposing (onWithDimensions, onKeys, onInput, onEnterPreventDefault) +import Html.Extra exposing (onWithDimensions, onKeys, onInput, + onEnterPreventDefault, onStop) import Html.Attributes exposing (value, readonly, disabled, classList) import Html.Events exposing (onFocus, onBlur) import Html exposing (node, input) @@ -24,6 +25,8 @@ import Html.Lazy import Ext.Number exposing (toFixed) import Json.Decode as Json import Native.Browser +import Ext.Signal +import Effects import Result import String import Dict @@ -32,87 +35,121 @@ import Ui.Helpers.Drag as Drag import Ui {-| Representation of a number range: - - **value** - The current value - **step** - The step to increment / decrement by (per pixel, or per keyboard action) - **affix** - The affix string to display (for example px, %, em, s) + - **disabled** - Whether or not the number range is disabled + - **readonly** - Whether or not the number range is readonly + - **valueSignal** - The number ranges value as a signal + - **round** - The decimals to round the value - **min** - The minimum allowed value - **max** - The maximum allowed value - - **round** - The decimals to round the value - - **disabled** - Whether or not the component is disabled - - **readonly** - Whether or not the component is readonly + - **value** - The current value + - **editing** (internal) - Whether or not the number range is in edit mode + - **startValue** (internal) - The value when the dragging starts + - **focusNext** (internal) - Whether or not to focus the input + - **focus** (internal) - Whether or not the input is focused + - **inputValue** (internal) - The inputs value when editing + - **mailbox** (internal) - The number ranges mailbox + - **drag** (internal) - The drag model -} type alias Model = - { drag : Drag.Model - , startValue : Float + { mailbox : Signal.Mailbox Float + , valueSignal : Signal Float , inputValue : String + , startValue : Float + , drag : Drag.Model + , focusNext : Bool + , disabled : Bool + , readonly : Bool + , editing : Bool + , focused : Bool + , affix : String , value : Float , step : Float - , affix : String , min : Float , max : Float , round : Int - , focusNext : Bool - , focused : Bool - , editing : Bool - , disabled : Bool - , readonly : Bool } {-| Actions that a number range can make. -} type Action = Lift (Html.Extra.PositionAndDimension) - | DoubleClick (Html.Extra.PositionAndDimension) - | Focus - | Blur | Input String | Increment | Decrement + | Tasks () + | Focus + | Edit + | Blur | Save -{-| Initializes a number range by the given value. -} +{-| Initializes a number range by the given value. + + NumberRange.init 0 +-} init : Float -> Model init value = - { drag = Drag.init - , startValue = value - , inputValue = "" - , value = value - , step = 1 - , affix = "px" - , min = -(1/0) - , max = (1/0) - , round = 0 - , focusNext = False - , focused = False - , editing = False - , disabled = False - , readonly = False - } + let + mailbox = Signal.mailbox value + in + { valueSignal = Signal.dropRepeats mailbox.signal + , startValue = value + , focusNext = False + , mailbox = mailbox + , drag = Drag.init + , disabled = False + , readonly = False + , focused = False + , editing = False + , inputValue = "" + , value = value + , affix = "px" + , min = -(1/0) -- Minus Infinity + , max = (1/0) -- Plus Infinity + , round = 0 + , step = 1 + } {-| Updates a number range. -} -update: Action -> Model -> Model +update: Action -> Model -> (Model, Effects.Effects Action) update action model = case action of Increment -> increment model + Decrement -> decrement model + Save -> endEdit model + Input value -> - { model | inputValue = value } + ({ model | inputValue = value }, Effects.none) + Focus -> - { model | focusNext = False, focused = True } + ({ model | focusNext = False, focused = True }, Effects.none) + Blur -> { model | focused = False } |> endEdit - DoubleClick {dimensions, position} -> - { model | editing = True - , inputValue = toFixed model.round model.value } - |> focus + + Edit -> + ({ model | editing = True + , inputValue = toFixed model.round model.value } + |> focus, Effects.none) + Lift {dimensions, position} -> - { model | drag = Drag.lift dimensions position model.drag - , startValue = model.value } - |> focus + ({ model | drag = Drag.lift dimensions position model.drag + , startValue = model.value } + |> focus, Effects.none) + + Tasks _ -> + (model, Effects.none) + +-- Sends the value to the signal +sendValue : Model -> (Model, Effects.Effects Action) +sendValue model = + (model, Ext.Signal.sendAsEffect model.mailbox.address model.value Tasks) {-| Renders a number range. -} view: Signal.Address Action -> Model -> Html.Html @@ -131,11 +168,12 @@ render address model = ] else [ onWithDimensions "mousedown" False address Lift - , onWithDimensions "dblclick" True address DoubleClick + , onStop "dblclick" address Edit , onKeys address [ (40, Decrement) , (38, Increment) , (37, Decrement) , (39, Increment) + , (13, Edit) ] ] attributes = @@ -171,7 +209,7 @@ focus model = False -> { model | focusNext = True } {-| Updates a number range value by coordinates. -} -handleMove : Int -> Int -> Model -> Model +handleMove : Int -> Int -> Model -> (Model, Effects.Effects Action) handleMove x y model = let diff = (Drag.diff x y model.drag).left @@ -179,7 +217,7 @@ handleMove x y model = if model.drag.dragging then setValue (model.startValue - (-diff * model.step)) model else - model + (model, Effects.none) {-| Updates a number range, stopping the drag if the mouse isnt pressed. -} handleClick : Bool -> Model -> Model @@ -187,24 +225,27 @@ handleClick value model = { model | drag = Drag.handleClick value model.drag } {-| Sets the value of a number range. -} -setValue : Float -> Model -> Model +setValue : Float -> Model -> (Model, Effects.Effects Action) setValue value model = { model | value = clamp model.min model.max value } + |> sendValue {-| Increments a number ranges value by it's defined step. -} -increment : Model -> Model +increment : Model -> (Model, Effects.Effects Action) increment model = setValue (model.value + model.step) model {-| Decrements a number ranges value by it's defined step. -} -decrement : Model -> Model +decrement : Model -> (Model, Effects.Effects Action) decrement model = setValue (model.value - model.step) model -- Exits a number range from its editing mode. -endEdit : Model -> Model +endEdit : Model -> (Model, Effects.Effects Action) endEdit model = case model.editing of - False -> model - True -> { model | value = Result.withDefault 0 (String.toFloat model.inputValue) - , editing = False } + False -> + (model, Effects.none) + True -> + { model | editing = False } + |> setValue (Result.withDefault 0 (String.toFloat model.inputValue)) diff --git a/source/Ui/Pager.elm b/source/Ui/Pager.elm index 71a6903..7cc6e1f 100644 --- a/source/Ui/Pager.elm +++ b/source/Ui/Pager.elm @@ -18,19 +18,19 @@ import Html exposing (node) import Html.Lazy import List.Extra -{-| Representation of a pager. +{-| Representation of a pager: - **left** (internal) - Pages at the left side - **center** (internal) - Pages at the center - **active** (internal) - The active page - - **width** - The width of the pager - **height** - The height of the pager + - **width** - The width of the pager -} type alias Model = - { left : List Int - , center : List Int - , active : Int - , width : String + { center : List Int + , left : List Int , height : String + , width : String + , active : Int } {-| Actions that a pager can take. -} @@ -41,11 +41,11 @@ type Action {-| Initailizes a pager with the given page as active. -} init : Int -> Model init active = - { left = [] - , center = [] - , active = active + { height = "100vh" , width = "100vw" - , height = "100vh" + , active = active + , center = [] + , left = [] } {-| Updates a pager. -} @@ -54,6 +54,7 @@ update action model = case action of End page -> { model | left = [] } + Active page -> { model | center = [], active = page } @@ -98,12 +99,18 @@ render address pages model = {-| Selects the page with the given index. - {- Selects the first page. -} - select 0 pager + select 0 pager -- Selects the first page -} select : Int -> Model -> Model select page model = - if model.left == [] && model.center == [] then - { model | left = [model.active], center = [page] } - else - model + let + canAnimate = + model.left == [] && model.center == [] + + isSamePage = + model.active == page + in + if canAnimate && not isSamePage then + { model | left = [model.active], center = [page] } + else + model diff --git a/source/Ui/Ratings.elm b/source/Ui/Ratings.elm index 436ecd7..350c8b6 100644 --- a/source/Ui/Ratings.elm +++ b/source/Ui/Ratings.elm @@ -1,4 +1,5 @@ -module Ui.Ratings (Model, Action(..), init, update, view, setValue) where +module Ui.Ratings + (Model, Action, init, update, view, setValue, valueAsStars) where {-| A simple star rating component. @@ -9,8 +10,10 @@ module Ui.Ratings (Model, Action(..), init, update, view, setValue) where @docs view # Functions -@docs setValue +@docs setValue, valueAsStars -} +import Ext.Number exposing (roundTo) +import Ext.Signal import Effects import Signal import Array @@ -23,15 +26,23 @@ import Html.Lazy import Ui +import Debug exposing (log) + {-| Representation of a ratings component. + - **clearable** - Whether or not the component is clearable - **disabled** - Whether or not the component is disabled - **readonly** - Whether or not the component is readonly - **value** - The current value of the component (0..1) + - **valueSignal** - The componens value as a signal - **size** - The number of starts to display + - **hoverValue** (internal) - The transient value of the component + - **mailbox** (internal) - The mailbox of the component -} type alias Model = { mailbox : Signal.Mailbox Float + , valueSignal : Signal Float , hoverValue : Float + , clearable : Bool , disabled : Bool , readonly : Bool , value : Float @@ -45,6 +56,7 @@ type Action | Increment | Decrement | Click Int + | Tasks () {-| Initializes a ratings component with the given number of stars and initial value. @@ -53,13 +65,18 @@ value. -} init : Int -> Float -> Model init size value = - { mailbox = Signal.mailbox value - , hoverValue = value - , disabled = False - , readonly = False - , value = value - , size = size - } + let + mailbox = Signal.mailbox value + in + { valueSignal = Signal.dropRepeats mailbox.signal + , hoverValue = value + , mailbox = mailbox + , clearable = False + , disabled = False + , readonly = False + , value = value + , size = size + } {-| Updates a ratings component. -} update : Action -> Model -> (Model, Effects.Effects Action) @@ -67,15 +84,26 @@ update action model = case action of MouseEnter index -> ({ model | hoverValue = calculateValue index model }, Effects.none) + MouseLeave -> ({ model | hoverValue = model.value }, Effects.none) + Increment -> setValue (clamp 0 1 (model.value + (1 / (toFloat model.size)))) model + Decrement -> - setValue (clamp 0 1 (model.value - (1 / (toFloat model.size)))) model + let + oneStarValue = 1 / (toFloat model.size) + min = if model.clearable then 0 else oneStarValue + in + setValue (clamp oneStarValue 1 (model.value - oneStarValue)) model + Click index -> setValue (calculateValue index model) model + Tasks _ -> + (model, Effects.none) + {-| Renders a ratings component. -} view : Signal.Address Action -> Model -> Html.Html view address model = @@ -83,14 +111,39 @@ view address model = {-| Sets the value of a ratings component. -} setValue : Float -> Model -> (Model, Effects.Effects Action) -setValue value model = - ({ model | value = value - , hoverValue = value }, Effects.none) +setValue value' model = + let + value = + roundTo 2 value' + + updatedModel = + { model | value = value + , hoverValue = value } + + effect = + Ext.Signal.sendAsEffect + model.mailbox.address + value + Tasks + in + (updatedModel, effect) + +{-| Returns the value of a ratings component as number of stars. -} +valueAsStars : Float -> Model -> Int +valueAsStars value model = + round (value * (toFloat model.size)) -- Calculates the value for the given index calculateValue : Int -> Model -> Float calculateValue index model = - clamp 0 1 ((toFloat index) / (toFloat model.size)) + let + value = + clamp 0 1 ((toFloat index) / (toFloat model.size)) + + currentIndex = + valueAsStars model.value model + in + if currentIndex == index && model.clearable then 0 else value -- Render internal render : Signal.Address Action -> Model -> Html.Html diff --git a/source/Ui/Slider.elm b/source/Ui/Slider.elm index 985efdc..4f5ce87 100644 --- a/source/Ui/Slider.elm +++ b/source/Ui/Slider.elm @@ -1,26 +1,7 @@ module Ui.Slider - (Model, Action, init, update, view, handleMove, handleClick) where + (Model, Action, init, update, view, handleMove, handleClick, setValue) where -{-| Slider component. In order to use it property you need to -handle mouse events like so: - - model = { slider: Ui.Slider.init 0 } - - update act model = - case act of - MousePosition (x, y) -> - { model | slider = Ui.Slider.handleMove x y model.slider } - MouseIsDown value -> - { model | slider = Ui.Slider.handleClick value model.slider } - Slider act -> - { model | slider = Ui.Slider.update act model.slider } - - StartApp.start { init = (model, Effects.none) - , view = view - , update = update - , inputs = [Signal.map MousePosition Mouse.position, - Signal.map MouseIsDown Mouse.isDown] - } +{-| Simple slider component. # Model @docs Model, Action, init, update @@ -29,7 +10,7 @@ handle mouse events like so: @docs view # Functions -@docs handleMove, handleClick +@docs handleMove, handleClick, setValue -} import Html.Extra exposing (onWithDimensions, onKeys) import Html.Attributes exposing (style, classList) @@ -37,29 +18,33 @@ import Html exposing (node) import Html.Lazy import Native.Browser +import Ext.Signal +import Effects +import Signal import Dict import Ui.Helpers.Drag as Drag import Ui {-| Representation of a slider: - - **value** - The current value (0 - 100) - **startDistance** - The distance in pixels when the dragging can start - - **disabled** - Whether or not the slide is disabled - - **readonly** - Whether or not the slide is readonly - - **dragging** (internal) - Wheter or not the slider is currently dragging - - **top** (internal) - The top position of the handle + - **disabled** - Whether or not the slider is disabled + - **readonly** - Whether or not the slider is readonly + - **valueSignal** - The sliders value as a signal + - **value** - The current value (0 - 100) - **left** (internal) - The left position of the handle - - **dimensions** (internal) - The dimensions of the slider - - **mouseStartPosition** (internal) - The start position of the mouse + - **drag** (internal) - The drag for the slider + - **mailbox** (internal) - The sliders mailbox -} type alias Model = - { drag : Drag.Model - , left : Float - , value : Float + { mailbox : Signal.Mailbox Float + , valueSignal : Signal Float , startDistance : Float + , drag : Drag.Model , disabled : Bool , readonly : Bool + , value : Float + , left : Float } {-| Actions that a slider can make. -} @@ -67,6 +52,7 @@ type Action = Lift (Html.Extra.PositionAndDimension) | Increment | Decrement + | Tasks () {-| Initializes a slider with the given value. @@ -74,16 +60,21 @@ type Action -} init : Float -> Model init value = - { drag = Drag.init - , left = 0 - , value = value - , startDistance = 0 - , disabled = False - , readonly = False - } + let + mailbox = Signal.mailbox value + in + { valueSignal = Signal.dropRepeats mailbox.signal + , mailbox = mailbox + , startDistance = 0 + , drag = Drag.init + , disabled = False + , readonly = False + , value = value + , left = 0 + } {-| Updates a slider. -} -update : Action -> Model -> Model +update : Action -> Model -> (Model, Effects.Effects Action) update action model = case action of Decrement -> @@ -97,6 +88,9 @@ update action model = , left = position.pageX - dimensions.left } |> clampLeft + Tasks _ -> + (model, Effects.none) + {-| Renders a slider. -} view : Signal.Address Action -> Model -> Html.Html view address model = @@ -135,7 +129,7 @@ render address model = element {-| Updates a sliders value by coordinates. -} -handleMove : Int -> Int -> Model -> Model +handleMove : Int -> Int -> Model -> (Model, Effects.Effects Action) handleMove x y model = let dist = distance diff @@ -152,7 +146,7 @@ handleMove x y model = { model | left = left } |> clampLeft else - model + (model, Effects.none) {-| Updates a slider, stopping the drag if the mouse isnt pressed. -} handleClick : Bool -> Model -> Model @@ -160,34 +154,36 @@ handleClick value model = { model | drag = Drag.handleClick value model.drag } {-| Clamps left position of the handle to width of the slider. -} -clampLeft : Model -> Model +clampLeft : Model -> (Model, Effects.Effects Action) clampLeft model = { model | left = clamp 0 model.drag.dimensions.width model.left } |> updatePrecent -{-| Clamps the value of the model. -} -clampValue : Model -> Model -clampValue model = - { model | value = clamp 0 100 model.value } - {-| Updates the value to match the current position. -} -updatePrecent : Model -> Model +updatePrecent : Model -> (Model, Effects.Effects Action) updatePrecent model = - { model | value = model.left / model.drag.dimensions.width * 100 } + setValue (model.left / model.drag.dimensions.width * 100) model {-| Returns the length of a point from 0,0. -} distance : Drag.Point -> Float distance diff = sqrt diff.top^2 + diff.left^2 -{-| Increments the model by 1 percent. -} -increment : Model -> Model +{-| Increments the slider by 1 percent. -} +increment : Model -> (Model, Effects.Effects Action) increment model = - { model | value = model.value + 1 } - |> clampValue + setValue (model.value + 1) model -{-| Decrements the model by 1 percent. -} -decrement : Model -> Model +{-| Decrements the slider by 1 percent. -} +decrement : Model -> (Model, Effects.Effects Action) decrement model = - { model | value = model.value - 1 } - |> clampValue + setValue (model.value - 1) model + +{-| Sets the value of the slider. -} +setValue : Float -> Model -> (Model, Effects.Effects Action) +setValue value model = + let + clampedValue = clamp 0 100 value + in + ( { model | value = clampedValue } + , Ext.Signal.sendAsEffect model.mailbox.address clampedValue Tasks) diff --git a/source/Ui/Textarea.elm b/source/Ui/Textarea.elm index 453f341..967fbbc 100644 --- a/source/Ui/Textarea.elm +++ b/source/Ui/Textarea.elm @@ -1,7 +1,7 @@ module Ui.Textarea (Model, Action, init, update, view, setValue, focus) where -{-| Autogrow textarea, using a mirror object to render the contents the same way, -thus creating an automatically growing textarea. +{-| Autogrow textarea, using a mirror object to render the contents the same +way, thus creating an automatically growing textarea. # Model @docs Model, Action, init, update @@ -12,36 +12,46 @@ thus creating an automatically growing textarea. # Functions @docs setValue, focus -} -import Html.Attributes exposing (value, spellcheck, placeholder, classList, readonly, disabled) +import Html.Attributes exposing (value, spellcheck, placeholder, classList, + readonly, disabled) import Html.Extra exposing (onEnterPreventDefault, onInput, onStop) import Html exposing (node, textarea, text, br) import Html.Events exposing (onFocus) import Html.Lazy + import Native.Browser +import Ext.Signal +import Effects +import Signal import String import List import Ui {-| Representation of a textarea: + - **enterAllowed** - Whether or not to allow new lines when pressing enter - **placeholder** - The text to display when there is no value + - **disabled** - Whether or not the textarea is disabled + - **readonly** - Whether or not the textarea is readonly + - **valueSingal** - The textareas value as a signal - **value** - The value - - **enterAllowed** - Whether or not to allow new lines when pressing enter - - **disabled** - Whether or not the component is disabled - - **readonly** - Whether or not the component is readonly + - **mailbox** - (internal) The mailbox of the textarea -} type alias Model = - { placeholder : String - , value : String + { mailbox : Signal.Mailbox String + , valueSignal : Signal String + , placeholder : String , enterAllowed : Bool , focusNext : Bool , disabled : Bool , readonly : Bool + , value : String } {-| Actions a textrea can make. -} type Action = Input String + | Tasks () | Nothing | Focus @@ -51,26 +61,31 @@ type Action -} init : String -> Model init value = - { placeholder = "" - , value = value - , enterAllowed = True - , focusNext = False - , disabled = False - , readonly = False - } + let + mailbox = Signal.mailbox value + in + { valueSignal = Signal.dropRepeats mailbox.signal + , enterAllowed = True + , mailbox = mailbox + , focusNext = False + , placeholder = "" + , disabled = False + , readonly = False + , value = value + } {-| Updates a textrea. -} -update : Action -> Model -> Model +update : Action -> Model -> (Model, Effects.Effects Action) update action model = case action of Input value -> setValue value model Focus -> - { model | focusNext = False } + ({ model | focusNext = False }, Effects.none) _ -> - model + (model, Effects.none) {-| Renders a textarea. -} view : Signal.Address Action -> Model -> Html.Html @@ -116,9 +131,10 @@ render address model = ] {-| Sets the value of the given textarea. -} -setValue : String -> Model -> Model +setValue : String -> Model -> (Model, Effects.Effects Action) setValue value model = - { model | value = value } + ( { model | value = value } + , Ext.Signal.sendAsEffect model.mailbox.address value Tasks) {-| Focuses the textarea. -} focus : Model -> Model diff --git a/stylesheets/main.scss b/stylesheets/main.scss index dae9d5c..3a344e4 100644 --- a/stylesheets/main.scss +++ b/stylesheets/main.scss @@ -13,6 +13,26 @@ ui-modal-wrapper { width: 500px; } +ui-pager { + ui-page { + justify-content: center; + align-items: center; + display: flex; + + &:nth-child(1) { + @include colors($primary); + } + + &:nth-child(2) { + @include colors($warning); + } + + &:nth-child(3) { + @include colors($danger); + } + } +} + kitchen-sink { display: block; margin: 0 auto; diff --git a/stylesheets/ui/components/ui-calendar.scss b/stylesheets/ui/components/ui-calendar.scss index 1255b6d..b8d8cf5 100644 --- a/stylesheets/ui/components/ui-calendar.scss +++ b/stylesheets/ui/components/ui-calendar.scss @@ -9,7 +9,6 @@ ui-calendar { ui-container { border-bottom: $border; border-bottom-style: dashed; - margin-bottom: 5px; height: 45px; padding: 5px; @@ -40,9 +39,26 @@ ui-calendar { justify-content: space-around; flex-wrap: wrap; display: flex; + width: 300px; + } - margin: 0 -5px; + // Day names + ui-calendar-header { + border-bottom: $border; + border-bottom-style: dashed; + justify-content: space-around; + margin-bottom: 5px; + display: flex; width: 300px; + + span { + text-align: center; + font-weight: 600; + font-size: 14px; + margin: 5px 0; + opacity: 0.7; + width: 34px; + } } // Table Cell diff --git a/stylesheets/ui/components/ui-chooser.scss b/stylesheets/ui/components/ui-chooser.scss index 4572b2a..bfc84a5 100644 --- a/stylesheets/ui/components/ui-chooser.scss +++ b/stylesheets/ui/components/ui-chooser.scss @@ -80,6 +80,7 @@ ui-chooser { } } + // Readonly State &.readonly { input:not([type]):focus { @extend %focused-readonly; diff --git a/stylesheets/ui/components/ui-inplace-input.scss b/stylesheets/ui/components/ui-inplace-input.scss index d3bac88..1a21eb4 100644 --- a/stylesheets/ui/components/ui-inplace-input.scss +++ b/stylesheets/ui/components/ui-inplace-input.scss @@ -1,14 +1,11 @@ ui-inplace-input { + // Textarea is in the background ui-textarea { z-index: 0; } + // Mimic textarea white space > div { white-space: pre-wrap; } - - button { - position: relative; - z-index: 1; - } } diff --git a/stylesheets/ui/components/ui-input.scss b/stylesheets/ui/components/ui-input.scss index 4acb9f2..0edd4c6 100644 --- a/stylesheets/ui/components/ui-input.scss +++ b/stylesheets/ui/components/ui-input.scss @@ -1,3 +1,13 @@ +ui-input { + display: inline-block; + + &.disabled input { + @extend %disabled; + @include colors($disabled); + border-color: $disabled; + } +} + input:not([type]), input[type=text], input[type=email], @@ -17,10 +27,14 @@ input[type=password] { line-height: 22px; } - &[readonly]:focus, &:focus { + &:focus { @extend %focused; } + &[readonly]:focus { + @extend %focused-readonly; + } + &[readonly] { &::-moz-selection { background: transparent; } &::selection { background: transparent; } diff --git a/stylesheets/ui/components/ui-notification-center.scss b/stylesheets/ui/components/ui-notification-center.scss index d22a70f..184959c 100644 --- a/stylesheets/ui/components/ui-notification-center.scss +++ b/stylesheets/ui/components/ui-notification-center.scss @@ -35,6 +35,10 @@ ui-notification-center { right: 30px; top: 20px; + flex-direction: column; + align-items: flex-end; + display: flex; + ui-notification { animation-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1); animation-fill-mode: both; @@ -47,13 +51,13 @@ ui-notification-center { cursor: pointer; div { - background: rgba(0,0,0,0.65); + background: rgba(#000, 0.65); } } div { border-radius: $border-radius; - background: rgba(0,0,0,0.8); + background: rgba(#000, 0.8); padding-bottom: 16px; padding: 14px 24px; color: #FFF; diff --git a/stylesheets/ui/components/ui-number-pad.scss b/stylesheets/ui/components/ui-number-pad.scss index 5e8669c..a314957 100644 --- a/stylesheets/ui/components/ui-number-pad.scss +++ b/stylesheets/ui/components/ui-number-pad.scss @@ -1,21 +1,21 @@ ui-number-pad { - display: flex; flex-direction: column; min-height: 300px; + display: flex; + // Input like box to show the value ui-number-pad-value { @extend %focused-idle; - @include colors($input); @include border; - display: flex; - padding: 10px 15px; margin-bottom: 10px; + padding: 10px 15px; + display: flex; span { - font-size: 24px; font-weight: 600; + font-size: 24px; flex: 1; } @@ -24,44 +24,47 @@ ui-number-pad { } } + // Buttons ui-number-pad-buttons { - display: flex; - flex-wrap: wrap; justify-content: space-between; + flex-wrap: wrap; + display: flex; flex: 1; + // Button ui-number-pad-button { - width: calc((100% - 20px) / 3); - margin-bottom: 10px; - display: flex; - justify-content: center; - align-items: center; + @include colors($secondary); - background: $secondary; - color: readable($secondary); border-radius: $border-radius; - - font-size: 20px; - font-weight: 600; - + width: calc((100% - 20px) / 3); + margin-bottom: 10px; position: relative; transition: 200ms; + font-weight: 600; + font-size: 20px; cursor: pointer; + justify-content: center; + align-items: center; + display: flex; + &:hover { - background: dampen($secondary, 7%); + @include colors(dampen($secondary, 7%)); } + // Last row no margin &:nth-child(n+10) { margin-bottom: 0; } + // Don't show empty buttons (bottom left / bottom right) &:empty { background: transparent; pointer-events: none; cursor: auto; } + // Position children > * { position: absolute; bottom: 0; @@ -69,19 +72,19 @@ ui-number-pad { left: 0; top: 0; - display: flex; justify-content: center; align-items: center; + display: flex; } &:active { - color: readable($primary); - background: $primary; + @include colors($primary); transition: 50ms; } } } + // Disabled state &.disabled { @extend %disabled; @@ -90,11 +93,12 @@ ui-number-pad { } ui-number-pad-value { - background: $disabled; border-color: $disabled; + background: $disabled; } } + // Readnoly state &.readonly { ui-number-pad-button, ui-icon { pointer-events: none; @@ -109,6 +113,7 @@ ui-number-pad { } } + // Normal state &:not(.readonly):not(.disabled):focus { outline: none; diff --git a/stylesheets/ui/components/ui-pager.scss b/stylesheets/ui/components/ui-pager.scss index 6c278b4..cd4f826 100644 --- a/stylesheets/ui/components/ui-pager.scss +++ b/stylesheets/ui/components/ui-pager.scss @@ -1,8 +1,9 @@ ui-pager { - display: block; - overflow: hidden; position: relative; + overflow: hidden; + display: block; + // Page ui-page { transform: translate3d(0, 0, 0); position: absolute; diff --git a/stylesheets/ui/components/ui-ratings.scss b/stylesheets/ui/components/ui-ratings.scss index a092a94..e574bc1 100644 --- a/stylesheets/ui/components/ui-ratings.scss +++ b/stylesheets/ui/components/ui-ratings.scss @@ -2,6 +2,7 @@ ui-ratings { display: inline-block; height: 36px; + // Individual star ui-ratings-star { justify-content: center; display: inline-flex; @@ -14,15 +15,18 @@ ui-ratings { font-size: 36px; } + // Full star glyph &.ui-ratings-full:before { - content: '\f2fc' + content: '\f2fc'; } + // Empty star glyph &.ui-ratings-empty:before { - content: '\f3ae' + content: '\f3ae'; } } + // Normal state &:not(.readonly):not(.disabled) { &:focus, &:hover { cursor: pointer; @@ -31,11 +35,13 @@ ui-ratings { } } + // Readonly state &.readonly:focus { - outline: none; color: $focus-readonly; + outline: none; } + // Disabled state &.disabled { @extend %disabled; color: $disabled; diff --git a/stylesheets/ui/components/ui-slider.scss b/stylesheets/ui/components/ui-slider.scss index bd4feff..726ea30 100644 --- a/stylesheets/ui/components/ui-slider.scss +++ b/stylesheets/ui/components/ui-slider.scss @@ -8,10 +8,6 @@ ui-slider { display: flex; height: 36px; - &:not(.readonly):not(.disabled) { - cursor: pointer; - } - * { pointer-events: none; } @@ -52,6 +48,11 @@ ui-slider { left: 0; } + // Normal State + &:not(.readonly):not(.disabled) { + cursor: pointer; + } + // Focused state &.readonly:focus { outline: none; diff --git a/stylesheets/ui/ui.scss b/stylesheets/ui/ui.scss index 2fb4a5c..1749280 100644 --- a/stylesheets/ui/ui.scss +++ b/stylesheets/ui/ui.scss @@ -29,6 +29,7 @@ @import 'components/ui-color-panel'; @import 'components/ui-input'; @import 'components/ui-input-group'; +@import 'components/ui-inplace-input'; @import 'components/ui-modal'; @import 'components/ui-fab'; @import 'components/ui-dropdown-menu'; diff --git a/tests/Ui/AppTests.elm b/tests/Ui/AppTests.elm index d482465..350df67 100644 --- a/tests/Ui/AppTests.elm +++ b/tests/Ui/AppTests.elm @@ -12,8 +12,7 @@ updateLoaded : Test updateLoaded = let initial = Ui.App.init "Test" - expected = - { loaded = True, title = "Test" } + (changed, effect) = Ui.App.update Ui.App.Loaded initial in test "Loaded should set loaded value" - (assertEqual (Ui.App.update Ui.App.Loaded initial) expected) + (assertEqual changed.loaded True) diff --git a/tests/Ui/CalendarTests.elm b/tests/Ui/CalendarTests.elm index 20b51cf..7e62459 100644 --- a/tests/Ui/CalendarTests.elm +++ b/tests/Ui/CalendarTests.elm @@ -47,7 +47,7 @@ updatePreviousMonth : Test updatePreviousMonth = let initial = Ui.Calendar.init (Date.fromTime 1430438400000) - changed = Ui.Calendar.update Ui.Calendar.PreviousMonth initial + (changed, effect) = Ui.Calendar.update Ui.Calendar.PreviousMonth initial expected = Date.fromTime 1427846400000 in test "Should switch to previous month" @@ -57,8 +57,8 @@ updateNextMonth : Test updateNextMonth = let initial = Ui.Calendar.init (Date.fromTime 1430438400000) + (changed, effect) = Ui.Calendar.update Ui.Calendar.NextMonth initial expected = Date.fromTime 1433116800000 - changed = Ui.Calendar.update Ui.Calendar.NextMonth initial in test "Should switch to next month" (assertEqual (Ext.Date.isSameDate changed.date expected) True) diff --git a/tests/Ui/CheckboxTests.elm b/tests/Ui/CheckboxTests.elm index 918aca7..4fa63c0 100644 --- a/tests/Ui/CheckboxTests.elm +++ b/tests/Ui/CheckboxTests.elm @@ -12,7 +12,7 @@ updateValue : Test updateValue = let initial = Ui.Checkbox.init False - expected = Ui.Checkbox.init True + (changed, effect) = Ui.Checkbox.update Ui.Checkbox.Toggle initial in test "Toggle should toggle value" - (assertEqual (Ui.Checkbox.update Ui.Checkbox.Toggle initial) expected) + (assertEqual changed.value True)