UNPKG

kdf

Version:

KD: a non-document focused UI Framework for web applications.

588 lines (460 loc) 19 kB
KDView = require './../../core/view.coffee' KDInputValidator = require './inputvalidator.coffee' ###* * The base input field view. Similar to the classic `<input type="foo">` * element, but with additional options such as validation. * * ## Usage * * ```coffee * view = new KDInputView * placeholder: 'Type something here for an inspiring message!' * * view.on 'keyup', (e) -> * if e.keyCode is 13 #13==Enter * new KDNotificationView * content: "You said #{e.target.value}!" * * appView.addSubView view * ``` * * Create a simple text input view, with a placeholder. When the `keyup` * event is fired, we check what the key is. If the keyCode is `13` * *(An Enter key)*, we create a notification with the value of the field. ### module.exports = class KDInputView extends KDView ###* * Options supports the following keys. * - **options.type**: The type of this input. All html input types are * supported. It should be noted that `"textarea"` and `"select"` do not * create `<input>` elements, but rather they create `<textarea>` and * `<select>` respectively. * * Supports the options `"text"`, `"password"`, `"hidden"`, `"checkbox"`, * `"range"`, `"textarea"`, and `"select"`. * - **options.name**: The `name="foo"` attribute of this `<input>` element. * - **options.label**: The label instance for this input field. * - **options.defaultValue**: The default value for this instance. * - **options.placeholder**: The HTML5 placeholder for this input. * - **options.disabled**: Whether or not this input is disabled. Defaults to * `false` * - **options.selectOptions**: If this input is of the type `"select"`, this * list populates the select options. Defaults to `null` * - **options.validate**: An object containing validation options, which are * passed to the KDInputValidator for this input. Note that the validator is * created internally, you do not need to create it. Defaults to `null` * - **options.autogrow**: If the input type can grow, such as a `textarea`, * this will cause the input to grow to the content size, rather than scroll. * Defaults to `false` * - **options.bind**: A string of event names, separated by a space. Defaults * to `" blur change focus"` * - **options.forceCase**: Force either uppercase, or lowercase for this field * type. If `null`, case is not enforced. Supports the options: `"uppercase"`, * `"lowercase"`, `null` * * @param {Object} options * @param {Object} data ### constructor:(o = {}, data)-> o.type or= "text" # a String of one of input types "text","password","select", etc... o.name or= "" # a String o.label or= null # a KDLabelView instance o.cssClass or= "" # a String o.callback or= null # a Function o.defaultValue ?= "" # a String or a Boolean value depending on type o.placeholder or= "" # a String o.disabled ?= no # a Boolean value # o.readonly ?= no # a Boolean value o.selectOptions or= null # an Array of Strings o.validate or= null # an Object of Validation options see KDInputValidator for details o.decorateValidation ?= yes # a Boolean value o.hint or= null # a String of HTML o.autogrow ?= no # a Boolean o.enableTabKey ?= no # a Boolean # NOT YET READY needs some work o.forceCase or= null # a String of either "lowercase" or "uppercase" o.bind = KD.utils.curry 'blur change focus paste cut input copy', o.bind @setType o.type super o, data options = @getOptions() @validationNotifications = {} @valid = yes @inputCallback = null @setName options.name @setLabel() @setCallback() @setDefaultValue options.defaultValue @setPlaceholder options.placeholder @makeDisabled() if options.disabled if options.selectOptions? and 'function' isnt typeof options.selectOptions @setSelectOptions options.selectOptions @setAutoGrow() if options.autogrow @enableTabKey() if options.enableTabKey @setCase options.forceCase if options.forceCase if options.required do (v = options.validate ?= {}) -> v.rules ?= {} v.messages ?= {} v.rules.required = yes v.messages.required = options.required @setValidation options.validate if options.validate @bindValidationEvents() if options.type is "select" and options.selectOptions @on "viewAppended", => o = @getOptions() if 'function' is typeof o.selectOptions kallback = @bound "setSelectOptions" o.selectOptions.call @, kallback else unless o.selectOptions.length @setValue o.selectOptions[Object.keys(o.selectOptions)[0]][0].value unless o.defaultValue else @setValue o.selectOptions[0].value unless o.defaultValue if o.autogrow @once "focus", => @initialHeight = @$().height() unless @initialHeight setDomElement:(cssClass = "")-> name = "name='#{@options.name}'" @domElement = switch @getType() when "text" then $ "<input #{name} type='text' class='kdinput text #{cssClass}'/>" when "password" then $ "<input #{name} type='password' class='kdinput text #{cssClass}'/>" when "hidden" then $ "<input #{name} type='hidden' class='kdinput hidden #{cssClass}'/>" when "checkbox" then $ "<input #{name} type='checkbox' class='kdinput checkbox #{cssClass}'/>" when "textarea" then $ "<textarea #{name} class='kdinput text #{cssClass}'></textarea>" when "select" then $ "<select #{name} class='kdinput select #{cssClass}'/>" when "range" then $ "<input #{name} type='range' class='kdinput range #{cssClass}'/>" else $ "<input #{name} type='#{@getType()}' class='kdinput #{@getType()} #{cssClass}'/>" bindValidationEvents:-> @on "ValidationError", @bound "giveValidationFeedback" @on "ValidationPassed", @bound "giveValidationFeedback" @on "focus", @bound "clearValidationFeedback" setLabel:(label = @getOptions().label)-> return unless label @inputLabel = label @inputLabel.$()[0].setAttribute "for", @getName() @inputLabel.$().bind "click",()=> @$().trigger "focus" @$().trigger "click" getLabel:-> @inputLabel setCallback:-> @inputCallback = @getOptions().callback getCallback:-> @inputCallback setType:(@inputType = "text")-> getType:-> @inputType setName:(@inputName)-> getName:-> @inputName setFocus:-> (KD.getSingleton "windowController").setKeyView this @$().trigger 'focus' setBlur:-> (KD.getSingleton "windowController").setKeyView null @$().trigger 'blur' setSelectOptions:(options)-> unless options.length for own optGroup, subOptions of options $optGroup = $ "<optgroup label='#{optGroup}'/>" @$().append $optGroup for option in subOptions $optGroup.append "<option value='#{option.value}'>#{option.title}</option>" else if options.length for option in options @$().append "<option value='#{option.value}'>#{option.title}</option>" else warn "no valid options specified for the input:", @ @$().val @getDefaultValue() setDefaultValue:(value)-> return if not value? and value isnt '' KDInputView::setValue.call this, value @inputDefaultValue = value getDefaultValue:-> @inputDefaultValue setPlaceholder:(value)-> if @$().is("input") or @$().is("textarea") @$().attr "placeholder",value @options.placeholder = value ###* * Disable this input field. ### makeDisabled:-> @getDomElement().attr "disabled","disabled" ###* * Enable this input field. ### makeEnabled:-> @getDomElement().removeAttr "disabled" ###* * Get the value of this input field. ### getValue:-> if @getOption("type") is "checkbox" value = @$().is ':checked' else value = @getDomElement().val() {forceCase} = @getOptions() if forceCase value = if forceCase.toLowerCase() is 'uppercase' then value.toUpperCase() else value.toLowerCase() return value ###* * Set the value of this input field. ### setValue:(value)-> $el = @$() el = $el[0] if @getOption("type") in ["checkbox", "radio"] if value then el.setAttribute "checked", "checked" else el.removeAttribute "checked" else $el.val value setCase:(forceCase)-> cb = => $el = @getDomElement() el = $el[0] val = @getValue() return if val is $el.val() start = el.selectionStart end = el.selectionEnd @setValue val # FIXME: setSelectionRange not supported in IE < 9 - need polyfill el.setSelectionRange(start, end) if el.setSelectionRange @on "keyup", cb @on "blur", cb unsetValidation:-> @setValidation {} setValidation:(ruleSet)-> @valid = no @currentRuleset = ruleSet @validationCallbacks or= {} @createRuleChain ruleSet for own oldEventName, oldCallbacks of @validationCallbacks for oldCallback in oldCallbacks @off oldEventName, oldCallback @ruleChain.forEach (rule)=> eventName = \ if ruleSet.events if ruleSet.events[rule] then ruleSet.events[rule] else if ruleSet.event then ruleSet.event else if ruleSet.event then ruleSet.event if eventName @validationCallbacks[eventName] or= [] @validationCallbacks[eventName].push cb = (event)=> @validate rule, event if rule in @ruleChain @on eventName, cb validate:(rule, event = {})-> @ruleChain or= [] @validationResults or= {} rulesToBeValidated = if rule then [rule] else @ruleChain ruleSet = @currentRuleset or @getOptions().validate if @ruleChain.length > 0 rulesToBeValidated.forEach (rule)=> if KDInputValidator["rule#{rule.capitalize()}"]? result = KDInputValidator["rule#{rule.capitalize()}"] @, event @setValidationResult rule, result else if "function" is typeof ruleSet.rules[rule] ruleSet.rules[rule] @, event else @valid = yes allClear = yes for own result, errMsg of @validationResults if errMsg then allClear = no @valid = if allClear then yes else no @emit "ValidationPassed" if @valid @emit "ValidationResult", @valid return @valid createRuleChain:(ruleSet)-> {rules} = ruleSet @validationResults or= {} @ruleChain = if typeof rules is "object" then (rule for own rule,value of rules) else [rules] for rule in @ruleChain @validationResults[rule] = null setValidationResult:(rule, err, showNotification=yes)-> if err @validationResults[rule] = err @showValidationError err if @getOptions().validate.notifications and showNotification @emit "ValidationError", err @valid = no else @validationResults[rule] = null # if there is any true on validation results' values then is not valid @valid = not( (v for own k, v of @validationResults) .map((result)-> Boolean result) .indexOf(true) > -1 ) showValidationError:(message)-> @validationNotifications[message]?.destroy() {container, notifications} = @getOption('validate') if notifications?.type is 'tooltip' if @tooltip str = "- #{message}<br>#{@tooltip.getOption 'title'}" @unsetTooltip() notifications = cssClass : notifications.cssClass or 'input-validation' delegate : notifications.delegate or this title : notifications.title or str or message placement : notifications.placement or 'right' direction : notifications.direction or 'left' forcePosition : yes @validationNotifications[message] = notice = @setTooltip notifications notice.show() else if notifications @validationNotifications[message] = notice = new KDNotificationView container : container title : message type : 'growl' cssClass : 'mini' duration : 2500 notice.on "KDObjectWillBeDestroyed", => message = notice.getOptions().title delete @validationNotifications[message] clearValidationFeedback:-> @unsetClass "validation-error validation-passed" @emit "ValidationFeedbackCleared" giveValidationFeedback:(err)-> return unless @getOption "decorateValidation" if err @setClass "validation-error" else @setClass "validation-passed" @unsetClass "validation-error" setCaretPosition:(pos)-> @selectRange pos, pos getCaretPosition:-> el = @$()[0] # modern if el.selectionStart return el.selectionStart # ie else if document.selection el.focus() r = document.selection.createRange() return 0 unless r re = el.createTextRange() rc = re.duplicate() re.moveToBookmark r.getBookmark() rc.setEndPoint 'EndToStart', re return rc.text.length return 0 selectAll:-> @getDomElement().select() selectRange:(selectionStart, selectionEnd)-> input = @$()[0] if input.setSelectionRange input.focus() input.setSelectionRange selectionStart, selectionEnd else if input.createTextRange range = input.createTextRange() range.collapse yes range.moveEnd 'character', selectionEnd range.moveStart 'character', selectionStart range.select() setAutoGrow:-> $input = @$() $input.css 'overflow', 'hidden' @setClass 'autogrow' # input content is copied into clone # element to get calculated height @_clone = $ '<div/>', class : 'invisible' @on 'focus', => @_clone.appendTo 'body' @_clone.css height : 'auto' zIndex : 100000 width : $input.css 'width' boxSizing : $input.css 'box-sizing' borderTop : $input.css 'border-top' borderRight : $input.css 'border-right' borderBottom : $input.css 'border-bottom' borderLeft : $input.css 'border-left' minHeight : $input.css 'minHeight' maxHeight : $input.css 'maxHeight' paddingTop : $input.css 'padding-top' paddingRight : $input.css 'padding-right' paddingBottom : $input.css 'padding-bottom' paddingLeft : $input.css 'padding-left' wordBreak : $input.css 'wordBreak' fontSize : $input.css 'fontSize' fontWeight : $input.css 'fontWeight' lineHeight : $input.css 'lineHeight' whiteSpace : 'pre-line' @on 'blur', => @_clone.detach() @on 'input', (event) => KD.utils.defer => @resize event resize: do -> previousHeight = null (event) -> return unless @_clone @_clone.appendTo 'body' unless document.body.contains @_clone[0] val = @getElement().value.replace(/\n/g,'\n&nbsp;') safeValue = Encoder.XSSEncode val @_clone.html safeValue height = @_clone.height() getValue = (rule) => parseInt @_clone.css(rule), 10 if @$().css('boxSizing') is 'border-box' padding = getValue('paddingTop') + getValue('paddingBottom') border = getValue('borderTopWidth') + getValue('borderBottomWidth') height = height + border + padding newHeight = if @initialHeight then Math.max @initialHeight, height else height if previousHeight isnt newHeight @setHeight newHeight @emit 'InputHeightChanged' previousHeight = newHeight enableTabKey:-> @inputTabKeyEnabled = yes disableTabKey:-> @inputTabKeyEnabled = no change:-> keyUp:-> yes keyDown:(event)-> @checkTabKey event if @inputTabKeyEnabled yes focus:-> @setKeyView() yes blur:-> # this messes up things # if you switch between inputs on focus next input sets the keyview to itself # and this fires right afterwards and reverts it back to blurred one # hopefully fixed KD.getSingleton("windowController").revertKeyView @ yes mouseDown:-> @setFocus() #WHY NO? #NO because if it propagates, other stuff might become keyview no checkTabKey:(event)-> tab = " " tabLength = tab.length t = event.target ss = t.selectionStart se = t.selectionEnd # // Tab key - insert tab expansion if event.which is 9 event.preventDefault() # // Special case of multi line selection if ss isnt se and t.value.slice(ss,se).indexOf("n") isnt -1 # // In case selection was not of entire lines (e.g. selection begins in the middle of a line) # // we ought to tab at the beginning as well as at the start of every following line. pre = t.value.slice(0,ss) sel = t.value.slice(ss,se).replace(/n/g,"n"+tab) post = t.value.slice(se,t.value.length) t.value = pre.concat(tab).concat(sel).concat(post) t.selectionStart = ss + tab.length t.selectionEnd = se + tab.length # // "Normal" case (no selection or selection on one line only) else t.value = t.value.slice(0,ss).concat(tab).concat(t.value.slice(ss,t.value.length)) if ss is se t.selectionStart = t.selectionEnd = ss + tab.length else t.selectionStart = ss + tab.length t.selectionEnd = se + tab.length # // Backspace key - delete preceding tab expansion, if exists else if event.which is 8 and t.value.slice(ss - tabLength,ss) is tab event.preventDefault() t.value = t.value.slice(0,ss - tabLength).concat(t.value.slice(ss,t.value.length)) t.selectionStart = t.selectionEnd = ss - tab.length # // Delete key - delete following tab expansion, if exists else if event.which is 46 and t.value.slice(se,se + tabLength) is tab event.preventDefault() t.value = t.value.slice(0,ss).concat(t.value.slice(ss + tabLength,t.value.length)) t.selectionStart = t.selectionEnd = ss # // Left/right arrow keys - move across the tab in one go else if event.which is 37 && t.value.slice(ss - tabLength,ss) is tab event.preventDefault() t.selectionStart = t.selectionEnd = ss - tabLength else if event.which is 39 and t.value.slice(ss,ss + tabLength) is tab event.preventDefault() t.selectionStart = t.selectionEnd = ss + tabLength