diff --git a/src/ReactSelectize.ls b/src/ReactSelectize.ls index 2ad8ae5..dbd47c6 100644 --- a/src/ReactSelectize.ls +++ b/src/ReactSelectize.ls @@ -1,5 +1,5 @@ -# prelude ls -{each, filter, find, find-index, id, initial, last, map, obj-to-pairs, +# prelude ls +{each, filter, find, find-index, id, initial, last, map, obj-to-pairs, partition, reject, reverse, Str, sort-by, sum, values} = require \prelude-ls {clamp, is-equal-to-object} = require \prelude-extension @@ -29,6 +29,7 @@ module.exports = create-class do # class-name :: String delimiters: [] disabled: false + maxLength: 255 dropdown-direction: 1 first-option-index-to-highlight: (options) -> 0 group-id: (.group-id) # Item -> a @@ -57,7 +58,7 @@ module.exports = create-class do # render-value :: Int -> Item -> ReactElement render-value: ({label}) -> - div do + div do class-name: \simple-value span null, label @@ -68,7 +69,7 @@ module.exports = create-class do render-reset-button: ResetButton # restore-on-backspace: ((value) -> ) # Item -> String - + # used to block default behaviour of option.mouseover when triggered by scrolling using arrow keys scroll-lock: false @@ -87,16 +88,16 @@ module.exports = create-class do # render :: () -> ReactElement render: -> - anchor-index = + anchor-index = | (typeof @props.anchor == \undefined) or @props.anchor == null => -1 | _ => (find-index (~> it `@is-equal-to-object` @props.anchor), @props.values) ? @props.values.length - 1 # render-selected-values :: [Int] -> [ValueWrapper] - render-selected-values = (selected-values) ~> - selected-values |> map (index) ~> + render-selected-values = (selected-values) ~> + selected-values |> map (index) ~> item = @props.values[index] uid = @props.uid item - ValueWrapper do + ValueWrapper do key: @uid-to-string uid uid: uid item: item @@ -105,29 +106,30 @@ module.exports = create-class do flipped = @props.dropdown-direction == -1 # REACT SELECTIZE - div do + div do class-name: class-name-from-object do \react-selectize : 1 "#{@props.theme}" : 1 \root-node : 1 "#{@props.class-name}" : 1 disabled: @props.disabled + maxLength: @props.maxLength open: @props.open flipped: flipped tethered: @props.tether style: @props.style - + if !!@props.name # HIDDEN INPUT (for form submission) - input do + input do type: \hidden name: @props.name - value: @props.serialize @props.values + value: @props.serialize @props.values # CONTROL - div do + div do class-name: \react-selectize-control ref: \control @@ -141,7 +143,7 @@ module.exports = create-class do <~ @on-open-change true @highlight-and-focus! - # avoid cancelling the event when the dropdown is already open + # avoid cancelling the event when the dropdown is already open # as this would block selection of text in the search field if !@props.open cancel-event e @@ -151,18 +153,19 @@ module.exports = create-class do # PLACEHOLDER div class-name: \react-selectize-placeholder, @props.placeholder - div do + div do class-name: \react-selectize-search-field-and-selected-values # LIST OF SELECTED VALUES (BEFORE & INCLUDING THE ANCHOR) render-selected-values [0 to anchor-index] - + # SEARCH INPUT BOX ResizableInput do {disabled: @props.disabled} <<< @props.input-props <<< { ref: \search type: \text value: @props.search + maxLength: @props.maxLength # update the search text & highlight the first option on-change: ({current-target:{value}}) ~> @@ -175,11 +178,11 @@ module.exports = create-class do on-focus: (e) !~> # @focus-lock propery is set to true by invoking the @focus-on-input! method # if @focus-lock is false, it implies this focus event was fired as a result of an external action - <~ do ~> (callback) ~> - if !!@focus-lock - callback @focus-lock = false - - else + <~ do ~> (callback) ~> + if !!@focus-lock + callback @focus-lock = false + + else <~ @on-open-change true callback true @@ -204,11 +207,11 @@ module.exports = create-class do # LIST OF SELECTED VALUES (AFTER THE ANCHOR) render-selected-values [anchor-index + 1 til @props.values.length] - + if @props.values.length > 0 and !@props.hide-reset-button # RESET BUTTON - div do + div do class-name: \react-selectize-reset-button-container on-click: (e) ~> do ~> @@ -219,7 +222,7 @@ module.exports = create-class do @props.render-reset-button! # TOGGLE BUTTON - div do + div do class-name: \react-selectize-toggle-button-container on-mouse-down: (e) ~> if @props.open @@ -228,13 +231,13 @@ module.exports = create-class do <~ @props.on-anchor-change last @props.values <~ @on-open-change true cancel-event e - @props.render-toggle-button do + @props.render-toggle-button do open: @props.open flipped: flipped - - + + # (TETHERED / ANIMATED / SIMPLE) DROPDOWN - DropdownMenu {} <<< @props <<< + DropdownMenu {} <<< @props <<< ref: \dropdownMenu class-name: class-name-from-object do \react-selectize : 1 @@ -250,7 +253,7 @@ module.exports = create-class do # bottom-anchor :: () -> ReactElement bottom-anchor: ~> find-DOM-node @refs.control - tether-props: {} <<< @props.tether-props <<< + tether-props: {} <<< @props.tether-props <<< # used when @props.tether is true # target :: () -> ReactElement @@ -274,8 +277,8 @@ module.exports = create-class do switch e.which # BACKSPACE - | 8 => - + | 8 => + return if @props.search.length > 0 or anchor-index == -1 do ~> @@ -283,11 +286,11 @@ module.exports = create-class do anchor-index-on-remove = anchor-index next-anchor = if (anchor-index - 1) < 0 then undefined else @props.values[anchor-index - 1] - # remove the item at the current carret position, - # by requesting the user to update the values array, + # remove the item at the current carret position, + # by requesting the user to update the values array, # via (@props.on-value-change new-values, callback) value-to-remove = @props.values[anchor-index] - <~ @props.on-values-change do + <~ @props.on-values-change do (@props.values |> reject ~> it `@is-equal-to-object` value-to-remove) ? [] # result is true if the user removed the value we requested him to remove @@ -299,23 +302,23 @@ module.exports = create-class do <~ @props.on-search-change @props.restore-on-backspace value-to-remove callback true - else + else callback true - else + else callback false if !!result - # highlight the first option in the dropdown - @highlight-and-scroll-to-selectable-option do + # highlight the first option in the dropdown + @highlight-and-scroll-to-selectable-option do @props.first-option-index-to-highlight @props.options 1 - # change the anchor iff the user removed the requested value + # change the anchor iff the user removed the requested value # and the predicted next-anchor is still present if anchor-index == anchor-index-on-remove and ( - typeof next-anchor == \undefined or + typeof next-anchor == \undefined or !!(@props.values |> find ~> it `@is-equal-to-object` next-anchor) ) <~ @props.on-anchor-change next-anchor @@ -325,20 +328,20 @@ module.exports = create-class do # ESCAPE | 27 => # first hit closes the list of options, second hit will reset the selected values - <~ do ~> - if @props.open + <~ do ~> + if @props.open ~> @on-open-change false, it - else + else ~> @props.on-values-change [], it <~ @props.on-search-change "" @focus-on-input! # ENTER - if @props.open and + if @props.open and e.which in [13] ++ @props.delimiters and # do not interfere with hotkeys like control + enter or command + enter !(e?.meta-key or e?.ctrl-key or e?.shift-key) - + # select the highlighted option (if any) result = @select-highlighted-uid anchor-index, (selected-value) ~> if typeof selected-value == \undefined @@ -349,24 +352,24 @@ module.exports = create-class do # move anchor position left / right using arrow keys (only when search field is empty) if @props.search.length == 0 - + switch e.which # LEFT ARROW | 37 => @props.on-anchor-change do - if ((anchor-index - 1) < 0 or e.meta-key) - undefined - else + if ((anchor-index - 1) < 0 or e.meta-key) + undefined + else @props.values[clamp (anchor-index - 1), 0, (@props.values.length - 1)] (->) # RIGHT ARROW | 39 => @props.on-anchor-change do - if e.meta-key - last @props.values - else + if e.meta-key + last @props.values + else @props.values[clamp (anchor-index + 1), 0, (@props.values.length - 1)] (->) @@ -374,9 +377,9 @@ module.exports = create-class do # wrap around upon hitting the boundary # UP ARROW - | 38 => + | 38 => @props.on-scroll-lock-change true - index = + index = | typeof @props.highlighted-uid == \undefined => 0 | _ => -1 + @option-index-from-uid @props.highlighted-uid result <~ @highlight-and-scroll-to-selectable-option index, -1 @@ -384,9 +387,9 @@ module.exports = create-class do @highlight-and-scroll-to-selectable-option (@props.options.length - 1), -1 # DOWN ARROW - | 40 => + | 40 => @props.on-scroll-lock-change true - index = + index = | typeof @props.highlighted-uid == \undefined => 0 | _ => 1 + @option-index-from-uid @props.highlighted-uid result <~ @highlight-and-scroll-to-selectable-option index, 1 @@ -410,13 +413,13 @@ module.exports = create-class do if @props.open and !prev-props.open and @props.highlighted-uid == undefined @highlight-and-focus! - # if the list of options was closed then reset highlighted-uid + # if the list of options was closed then reset highlighted-uid if !@props.open and prev-props.open <~ @props.on-highlighted-uid-change undefined # component-will-receive-props :: Props -> () component-will-receive-props: (props) !-> - if (typeof @props.disabled == \undefined or @props.disabled == false) and + if (typeof @props.disabled == \undefined or @props.disabled == false) and (typeof props.disabled != \undefined and props.disabled == true) @on-open-change false, ~> @@ -426,7 +429,7 @@ module.exports = create-class do # close-dropdown :: (() -> ()) -> () close-dropdown: (callback) !-> <~ @on-open-change false - @props.on-anchor-change do + @props.on-anchor-change do last @props.values callback @@ -453,7 +456,7 @@ module.exports = create-class do # highlights the first selectable option & moves the cursor to end of the search field # highlight-and-focus :: () -> () highlight-and-focus: !-> - @highlight-and-scroll-to-selectable-option do + @highlight-and-scroll-to-selectable-option do @props.first-option-index-to-highlight @props.options 1 @focus-on-input! @@ -474,7 +477,7 @@ module.exports = create-class do # on-open-change :: Boolean -> (() -> ()) -> () on-open-change: (open, callback) -> - @props.on-open-change do + @props.on-open-change do if @props.disabled then false else open callback @@ -484,7 +487,7 @@ module.exports = create-class do if @props.highlighted-uid == undefined callback! return false - + # sanity check index = @option-index-from-uid @props.highlighted-uid if typeof index != \number @@ -497,39 +500,39 @@ module.exports = create-class do # values = (values behind & including the anchor) + highlighted option + (values ahead of the anchor) <~ @props.on-values-change do - (map (~> @props.values[it]), [0 to anchor-index]) ++ - [option] ++ + (map (~> @props.values[it]), [0 to anchor-index]) ++ + [option] ++ (map (~> @props.values[it]), [anchor-index + 1 til @props.values.length]) value = find (~> it `@is-equal-to-object` option), @props.values if !value callback! return - + # if the user did what we asked, then clear the search and move the anchor ahead of the selected value <~ @props.on-search-change "" <~ @props.on-anchor-change value if !@props.open callback value return - + # highlight the next selectable option result <~ @highlight-and-scroll-to-selectable-option index, 1 if !!result callback value return - + # if there are no highlightable/selectable options (then close the dropdown) - result <~ @highlight-and-scroll-to-selectable-option do + result <~ @highlight-and-scroll-to-selectable-option do @props.first-option-index-to-highlight @props.options 1 - if !result + if !result <~ @on-open-change false callback value else callback value true - + # uid-to-string :: () -> String, only used for the key prop (required by react render), & for refs - uid-to-string: (uid) -> (if typeof uid == \object then JSON.stringify else id) uid \ No newline at end of file + uid-to-string: (uid) -> (if typeof uid == \object then JSON.stringify else id) uid diff --git a/src/SimpleSelect.ls b/src/SimpleSelect.ls index 175f600..839a6f9 100644 --- a/src/SimpleSelect.ls +++ b/src/SimpleSelect.ls @@ -18,7 +18,7 @@ module.exports = React.create-class do delimiters: [] # editable :: Item -> String # filter-options :: [Item] -> String -> [Item] - filter-options: (options, search) --> + filter-options: (options, search) --> options |> filter ~> (it.label.to-lower-case!.trim!.index-of search.to-lower-case!.trim!) > -1 first-option-index-to-highlight: id @@ -31,7 +31,7 @@ module.exports = React.create-class do on-keyboard-selection-failed: ((which) !-> ) # :: Int -> () on-paste: ((e) !-> true) # Event -> Boolean # on-search-change :: String -> () - # on-value-change :: Item -> () + # on-value-change :: Item -> () # open :: Boolean # options :: [Item] # on-open-change :: Boolean -> () @@ -41,8 +41,8 @@ module.exports = React.create-class do # render-reset-button :: () -> ReactElement # render-toggle-button :: ({open :: Boolean, flipped :: Boolean}) -> ReactElement # render-value :: Int -> Item -> ReactElement - render-value: ({label}) -> - div do + render-value: ({label}) -> + div do class-name: \simple-value span null, label @@ -55,33 +55,34 @@ module.exports = React.create-class do # theme :: String uid: id # uid :: (Equatable e) => Item -> e # value :: Item - + # render :: () -> ReactElement - render: -> - + render: -> + # computed state { - filtered-options, highlighted-uid, on-highlighted-uid-change, on-open-change, on-search-change, on-value-change, + filtered-options, highlighted-uid, on-highlighted-uid-change, on-open-change, on-search-change, on-value-change, open, options, search, value, values } = @get-computed-state! # props { - autofocus, autosize, cancel-keyboard-event-on-selection, delimiters, disabled, dropdown-direction, group-id, + autofocus, autosize, cancel-keyboard-event-on-selection, delimiters, disabled, maxLength, dropdown-direction, group-id, groups, groups-as-columns, hide-reset-button, name, input-props, on-blur-resets-input, render-toggle-button, render-group-title, render-reset-button, serialize, tether, tether-props, theme, transition-enter, transition-leave, transition-enter-timeout, transition-leave-timeout, uid }? = @props - + ReactSelectize { - + autofocus autosize cancel-keyboard-event-on-selection class-name: "simple-select" + if !!@props.class-name then " #{@props.class-name}" else "" delimiters disabled + maxLength dropdown-direction group-id groups @@ -115,7 +116,7 @@ module.exports = React.create-class do open: open on-open-change: on-open-change - # OPTIONS + # OPTIONS first-option-index-to-highlight: ~> @first-option-index-to-highlight options, value options: options render-option: @props.render-option @@ -130,7 +131,7 @@ module.exports = React.create-class do on-values-change: (new-values, callback) ~> if new-values.length == 0 - # deselect + # deselect <~ on-value-change undefined callback! @@ -149,13 +150,13 @@ module.exports = React.create-class do render-value: (item) ~> # hide the selected value only when: - # the dropdown is open and - # either the search-string length is > 0 or + # the dropdown is open and + # either the search-string length is > 0 or # selected value is editable if open and (!!@props.editable or search.length > 0) null - # always show the selected value when the dropdown is closed + # always show the selected value when the dropdown is closed else @props.render-value item @@ -166,7 +167,7 @@ module.exports = React.create-class do # TODO: distinguish between uid for selected value & option, this will improve performance # by not having to compare against additional open & search properties added to uid below - uid: (item) ~> + uid: (item) ~> # add open and search to uid since the render-value above depends on them uid: @props.uid item @@ -177,15 +178,15 @@ module.exports = React.create-class do serialize: (items) ~> serialize items.0 # BLUR & FOCUS - on-blur: (e) !~> + on-blur: (e) !~> # clear the search text on-blur-resets-input = @props.on-blur-resets-input - <~ do ~> - (callback) ~> + <~ do ~> + (callback) ~> if search.length > 0 && on-blur-resets-input - on-search-change "", callback + on-search-change "", callback - else + else callback! # fire on-blur event @@ -194,7 +195,7 @@ module.exports = React.create-class do on-focus: (e) !~> @props.on-focus {value, open, original-event: e} # on-paste :: Event -> Boolean - on-paste: + on-paste: | typeof @props?.value-from-paste == \undefined => @props.on-paste | _ => ({clipboard-data}:e) ~> value-from-paste = @props.value-from-paste options, value, clipboard-data.get-data \text @@ -216,7 +217,7 @@ module.exports = React.create-class do | _ => {}) <<< (switch - | typeof @props.render-no-results-found == \function => + | typeof @props.render-no-results-found == \function => render-no-results-found: ~> @props.render-no-results-found value, search | _ => {}) @@ -240,22 +241,22 @@ module.exports = React.create-class do result = switch # both p & its change callback are coming from props - | @props.has-own-property p and @props.has-own-property camelize "on-#{p}-change" => - (o, callback) ~> + | @props.has-own-property p and @props.has-own-property camelize "on-#{p}-change" => + (o, callback) ~> @props[camelize "on-#{p}-change"] o, (->) # trick react into running batch update, this indirectly updates the props @set-state {}, callback - # p is coming from prop but the change callback is coming from state + # p is coming from prop but the change callback is coming from state # (do nothing, just invoke the callback - p remains unchanged -) - | @props.has-own-property p and !(@props.has-own-property camelize "on-#{p}-change") => + | @props.has-own-property p and !(@props.has-own-property camelize "on-#{p}-change") => (, callback) ~> callback! # p is coming from state but the change callback is coming from props # update the value of p in state and invoke the change callback (present in props) - | !(@props.has-own-property p) and @props.has-own-property camelize "on-#{p}-change" => - (o, callback) ~> + | !(@props.has-own-property p) and @props.has-own-property camelize "on-#{p}-change" => + (o, callback) ~> <~ @set-state {"#{p}" : o} callback! @@ -263,13 +264,13 @@ module.exports = React.create-class do # both p and its change callback are coming from state # update the state & on success invoke the change callback - | !(@props.has-own-property p) and !(@props.has-own-property camelize "on-#{p}-change") => + | !(@props.has-own-property p) and !(@props.has-own-property camelize "on-#{p}-change") => (o, callback) ~> @set-state {"#{p}" : o}, callback # get options from props.children options-from-children = switch - | !!@props?.children => - (if typeof! @props.children == \Array then @props.children else [@props.children]) |> map -> + | !!@props?.children => + (if typeof! @props.children == \Array then @props.children else [@props.children]) |> map -> {value, children}? = it?.props label: children, value: value | _ => [] @@ -279,11 +280,11 @@ module.exports = React.create-class do # filter options and create new one from search text filtered-options = @props.filter-options unfiltered-options, search - new-option = + new-option = | typeof @props.create-from-search == \function => @props.create-from-search filtered-options, search | _ => null - # the final list of options is the concatination of any new-option, created from search, or [] with + # the final list of options is the concatination of any new-option, created from search, or [] with # the list of filtered options options = (if !!new-option then [{} <<< new-option <<< new-option: true] else []) ++ filtered-options @@ -299,7 +300,7 @@ module.exports = React.create-class do on-open-change: (open, callback) !~> <~ on-open-change open callback! - + # if props.editable is defined, then populate the search field with the result of `props.editable selected-value` if !!@props.editable and (@is-open! and !!value) <~ on-search-change "#{@props.editable value}#{if search.length == 1 then search else ''}" @@ -329,21 +330,21 @@ module.exports = React.create-class do # highlight the currently select option (if any) | typeof index != \undefined => index - # highlight the first option if there is only one option + # highlight the first option if there is only one option | options.length == 1 => 0 - # highlight the first option if isn't coming from (create-from-search prop) + # highlight the first option if isn't coming from (create-from-search prop) | (typeof options.0?.new-option) == \undefined => 0 | _ => - # highlight the first option if the remaining are not selectable + # highlight the first option if the remaining are not selectable if (options |> drop 1 |> all -> (typeof it.selectable == \boolean) and !it.selectable) 0 - # alas, highlight the second option + # alas, highlight the second option # happens when: # the first option is coming from `create-from-search` prop AND # number of options are greater than 1 AND @@ -367,7 +368,7 @@ module.exports = React.create-class do highlight-first-selectable-option: (callback = (->)) !-> if @state.open {options, value} = @get-computed-state! - @refs.select.highlight-and-scroll-to-selectable-option do + @refs.select.highlight-and-scroll-to-selectable-option do @first-option-index-to-highlight options, value 1 callback @@ -379,4 +380,4 @@ module.exports = React.create-class do value: -> if @props.has-own-property \value then @props.value else @state.value # is-open :: () -> Boolean - is-open: -> if @props.has-own-property \open then @props.open else @state.open \ No newline at end of file + is-open: -> if @props.has-own-property \open then @props.open else @state.open