coffeescript-ui
Version:
Coffeescript User Interface System
1,283 lines (1,033 loc) • 29.7 kB
text/coffeescript
###
* coffeescript-ui - Coffeescript User Interface System (CUI)
* Copyright (c) 2013 - 2016 Programmfabrik GmbH
* MIT Licence
* https://github.com/programmfabrik/coffeescript-ui, http://www.coffeescript-ui.org
###
class CUI.Input extends CUI.DataFieldInput
constructor: (opts) ->
super(opts)
if
if
=
else
=
if
if
if
=
if
if
if
= {}
= {}
for k in ["empty", "invalid", "valid"]
hint = @["_"+k+"Hint"]
if not hint
continue
[k]
if hint instanceof CUI.Label
[k] = hint
else
[k] = new CUI.defaults.class.Label(hint)
[k].addClass("cui-input-"+k+"-hint")
[k] = [k].getText()
= =>
return
initOpts: ->
super()
spellcheck:
default: false
check: Boolean
autocomplete:
default: false
check: Boolean
overwrite:
check: Boolean
checkInput:
check: Function
getValueForDisplay:
check: Function
getValueForInput:
check: Function
correctValueForInput:
check: Function
emptyHint:
check: (v) ->
CUI.util.isString(v) or v instanceof CUI.Label or CUI.util.isPlainObject(v)
invalidHint:
check: (v) ->
CUI.util.isString(v) or v instanceof CUI.Label or CUI.util.isPlainObject(v)
validHint:
check: (v) ->
CUI.util.isString(v) or v instanceof CUI.Label or CUI.util.isPlainObject(v)
maxLength:
check: (v) ->
v >= 0
onFocus:
check: Function
onClick:
check: Function
onKeyup:
check: Function
onSelectionchange:
check: Function
incNumbers:
default: true
check: Boolean
onBlur:
check: Function
regexp:
check: String
regexp_flags:
default: ""
check: String
getInputBlocks:
check: Function
# if cursor blocks are provived, no automatic incNumberBlocks
# takes place
getCursorBlocks:
check: (v) ->
CUI.util.isFunction(v) and not
placeholder:
check: (v) ->
CUI.util.isFunction(v) or CUI.util.isString(v)
readonly:
check: Boolean
readonly_select_all:
default: true
check: Boolean
textarea:
check: Boolean
min_rows:
check: (v) ->
v >= 2
default: 2
# limit the amount of rows in textarea input
rows:
check: (v) ->
v >= 1
content_size:
default: false
check: Boolean
prevent_invalid_input:
default: false
check: Boolean
required:
default: false
check: Boolean
appearance:
check: ["code"]
readOpts: ->
if .readonly
CUI.util.assert(not (.getCursorBlocks or .getInputBlocks or .checkInput), "new CUI.Input", "opts.readonly conflicts with opts.getCursorBlocks, opts.getInputBlocks, opts.checkInput.")
if .textarea
CUI.util.assert(not .autocomplete, "new CUI.Input", "opts.textarea does not work with opts.autocomplete", opts: )
CUI.util.assert(not .incNumbers, "new CUI.Input", "opts.textarea does not work with opts.incNumbers", opts: )
super()
if and
= (v) =>
[ new CUI.InputBlock(start: 0, string: v) ]
if
= new RegExp(, )
# = false
= (value) =>
if not
false
else if
else
true
if
= (value) =>
if value.trim().length == 0
false
else if
else
true
if == false
= "false"
else
= "default"
# if
# CUI.util.assert(, "new CUI.Input", "opts.rows can only be used with opts.content_size set.", opts: )
if == true
= "on"
else if == false
= "off"
@
__checkInputRegexp: (value) ->
if .exec(value)
true
else
false
setSpellcheck: (spellcheck) ->
if spellcheck
CUI.dom.setAttribute(, "spellcheck", "default")
else
CUI.dom.setAttribute(, "spellcheck", "false")
setPlaceholder: (placeholder) ->
CUI.dom.setAttribute(, "placeholder", placeholder)
getPlaceholder: ->
if not
return undefined
if CUI.util.isFunction()
else
# MISSING FEATURES:
# - tab block advance
# - up/down cursor number decrement/increment
# - input masking
__createElement: (input_type="text") ->
if == true
= CUI.dom.$element "textarea", "cui-textarea",
placeholder:
tabindex: "0"
maxLength:
id: "cui-input-"+
spellcheck:
rows:
.style.setProperty("--textarea-min-rows", )
resize = =>
.rows =
rows = Math.ceil((.scrollHeight - ) / );
.rows = + rows;
calculateBaseHeight = =>
value = .value
.value = ""
= .scrollHeight
.value = value
= parseInt(CUI.dom.getComputedStyle().lineHeight, 10)
CUI.Events.listen
node:
type: "input"
call: resize
CUI.dom.waitForDOMInsert(node: ).done(=>
if
return
calculateBaseHeight()
resize()
)
else
= CUI.dom.$element "input", "cui-input",
type: input_type
size: 1
placeholder:
tabindex: "0"
maxLength:
id: "cui-input-"+
spellcheck:
autocomplete:
CUI.Events.listen
node:
type: "dragstart"
call: (ev) ->
ev.preventDefault()
CUI.Events.listen
node:
type: "keydown"
call: (ev) =>
# console.debug "keydown on input", ev.hasModifierKey(), ev.keyCode(),
# dont return here if CTRL-Z is pressed
if (ev.ctrlKey() and not ev.keyCode() == 90) or ev.metaKey()
# console.debug "leaving keydown"
return
= ev
if and not and not
if ev.keyCode() in [37, 39, 36, 35] # LEFT, RIGHT, POS1, END
return
if ev.keyCode() in [9, 16, 17, 18, 27, 33, 34, 35, 36, 38, 40]
return
# Select all, copy, paste, cut.
if (ev.ctrlKey() or ev.metaKey()) and ev.keyCode() in [65, 67, 86, 88] # 'A', 'C', 'V', 'X'
return
if not and ev.keyCode() == 13
return
# backspace and the cursor is slim and at the beginning
if ev.keyCode() == 8 and 0 == .selectionStart == .selectionEnd
return
# if and and not
# # in this case we are not focussing the shadow input,
# # so that spellchecking gets not confused when
# # we set the value of our textarea when unfocusing the shadow
# # input.
# return
return
CUI.Events.listen
type: "keyup"
node:
call: (ev) =>
if ev.keyCode() in [37, 39, 36, 35] # LEFT, RIGHT, POS1, END
ev.preventDefault()
# movement was already done by us
if not
# if we dont have a cursor, we still call showCursor
# because after a cursor movement was done by the browser
# a derived class might overwrite showCursor and do stuff
# although we (this Input class) does not do anything
return
# if and and not
#
if
return
CUI.Events.listen
type: "focus"
node:
call: (ev) =>
# console.debug "input focus event", [0], "immediate:", , "shadow:",
if
return
?(@, ev)
?.show()
return
oldSizes = null
CUI.Events.listen
type: "mousedown"
node:
call: (ev) =>
oldSizes = [.offsetWidth, .offsetHeight]
trigger = =>
if oldSizes[0] != .offsetWidth or
oldSizes[1] != .offsetHeight
CUI.Events.trigger
type: "content-resize"
node:
mev = CUI.Events.listen
type: "mousemove"
call: =>
trigger()
return
CUI.Events.listen
type: "mouseup"
only_once: true
capture: true
call: (ev) =>
CUI.Events.ignore(mev)
return
CUI.Events.listen
type: "mouseup"
node:
call: (ev) =>
return
CUI.Events.listen
type: "blur"
node:
call: (ev) =>
if
return
?(@, ev)
return
CUI.Events.listen
type: "input"
node:
call: (ev, info) =>
# console.debug "#{@__cls}", ev.type, ev.isDefaultPrevented()
if not ev.isDefaultPrevented()
# this can happen thru CTRL-X, so we need to check again
if !=
return
CUI.Events.listen
type: "paste"
node:
call: (ev) =>
CUI.Events.listen
type: "click"
node:
call: (ev) =>
ev.stopPropagation()
?(@, ev)
return
# console.debug "listening for dom insert on ",
#
if
CUI.dom.waitForDOMInsert(node: )
.done =>
if
return
__setCursor: (ev) ->
# console.debug "setting timeout"
CUI.setTimeout =>
# console.debug "focus?",
if == null and
(s = .selectionStart) == .selectionEnd and
.selectionEnd != .value.length
blocks =
if blocks.length > 0
for block in blocks
if block.start <= s <= block.end
break
,
0
getValueForStore: (value) ->
value
storeValue: (value, flags={}) ->
super(, flags)
handleSelectionChange: ->
?.apply(@, arguments)
getElement: ->
getUniqueIdForLabel: ->
"cui-input-"+
markBlock: (ev, bl) ->
.setSelectionRange(bl.start, bl.end)
remove: ->
super()
__focusShadowInput: ->
if not
return
# console.debug "focus shadow input"
= true
.value = .value
.focus()
.setSelectionRange(.selectionStart, .selectionEnd)
__unfocusShadowInput: ->
if not
return
# console.debug "unfocus shadow input"
.focus()
= false
hasShadowFocus: ->
setContentSize: ->
# console.error "setting content size", !!,
if not
return @
if
CUI.scheduleCallback
call:
ms: 100
else
@
__initContentSize: ->
# console.debug "initContentSize", ,
if
return
= CUI.dom.$element("textarea", "cui-input-shadow", tabindex: "-1", autocomplete: "off")
CUI.dom.append(document.body, )
style = window.getComputedStyle()
# console.debug style
css = {}
for k in [
"fontFamily"
"fontKerning"
"fontSize"
"wordBreak"
"wordSpacing"
"wordWrap"
"fontStretch"
"lineHeight"
"fontStyle"
"fontVariant"
"fontVariantLigatures"
"fontWeight"
]
# console.debug k, style[k]
css[k] = style[k]
if not
css.whiteSpace = "nowrap"
CUI.dom.setStyle(, css)
CUI.dom.height(, 1)
if
CUI.dom.width(, CUI.dom.width())
= parseFloat(CUI.dom.getComputedStyle()["max-height"])
.style.overflow = "hidden"
if isNaN()
= null
else
correct_height = parseFloat(CUI.dom.getComputedStyle()["height"]) - CUI.dom.height()
-= correct_height
else
CUI.dom.width(, 1)
@
__setContentSize: ->
if not
return
.value = .value
if
# we can only do this when shadow is focused,
# otherwise the "blur" event on
# will remove and we will run into errors
.focus()
changed = false
if
if .value.length == 0
.value = "A" # help IE out, so we get a height
if CUI.dom.width() != CUI.dom.width()
CUI.dom.width(, CUI.dom.width())
h = .scrollHeight
if == null or h <=
.style.overflow = "hidden"
else
.style.overflow = ""
previous_height = CUI.dom.height()
CUI.dom.height(, h)
if CUI.dom.height() != previous_height
changed = true
# console.error "__setContentSize", , .value, .value, h
else
w = .scrollWidth
if .value.length == 0
# help IE out here, IE measures one " "
w = 1
else
# for now, add 1 pixel to the measurement
# Chrome measures a Textarea width different than an Input width
w = w + 1
if CUI.dom.width() != w
changed = true
CUI.dom.width(, w)
if changed
CUI.Events.trigger
type: "content-resize"
node:
@
checkBlocks: (blocks) ->
if not CUI.util.isArray(blocks)
return false
for b, idx in blocks
CUI.util.assert(b instanceof CUI.InputBlock, "Input.getInputBlocks", "Block[#{idx}] needs to be instance of CUI.InputBlock.", blocks: blocks, block: b)
b.idx = idx
blocks
getInputBlocks: ->
if
blocks =
else if
blocks =
else
blocks =
__getInputBlocks: (v) ->
blocks = []
v = .value
re = /[0-9]+/g
blocks = []
while (match = re.exec(v)) != null
match_str = match[0]
match_start = match.index
if match_start > 0
char_1_before = v.substr(match_start-1, 1)
else
char_1_before = null
if match_start > 1
char_2_before = v.substr(match_start-2, 1)
else
char_2_before = null
if char_1_before == "-" and not char_2_before?.match(/[0-9]/)
match_str = "-"+match_str
match_start -= 1
blocks.push new CUI.NumberInputBlock
start: match_start
string: match_str
# console.debug "blocks", blocks
blocks
__overwriteBlocks: (v) ->
blocks = []
for i in [0...v.length]
blocks.push new CUI.InputBlock
start: i
string: v.substr(i, 1)
blocks.push new CUI.InputBlock
start: v.length
string: ""
blocks
# returns the currently exactly marked block
# if no block is found, returns null
getMarkedBlock: ->
blocks =
if blocks == false or blocks.length == 0
return null
s = .selectionStart
e = .selectionEnd
for block, idx in blocks
# console.debug match_start, match_end, match_str
if block.start == s and block.end == e
return block
return null
getSelection: ->
s = .selectionStart
e = .selectionEnd
start: s
end: e
value: .value
before: .value.substring(0, s)
selected: .value.substring(s, e)
after: .value.substring(e)
setSelection: (selection) ->
.selectionStart = selection.start
.selectionEnd = selection.end
selectAll: ->
.selectionStart = 0
.selectionEnd = .value.length
@
updateSelection: (txt="") ->
sel =
start = sel.before.length
end = start + txt.length
if sel.start == sel.end
start = end
setValue: (v, flags = {}) ->
if not
?.value = v
super(v, flags)
incNumberBounds: (ev) ->
if ev.keyCode() not in [38, 40, 33, 34] # not in TAB
return
s = .selectionStart
e = .selectionEnd
v = .value
blocks =
if blocks == false or blocks.length == 0
return
parts_inbetween = [v.substring(0, blocks[0].start)]
for block, idx in blocks
if idx == blocks.length-1
break
# console.debug idx, block.end, blocks[idx+1].start
parts_inbetween.push(v.substring(block.end, blocks[idx+1].start))
last_block = blocks[blocks.length-1]
parts_inbetween.push(v.substring(last_block.end))
# console.debug "blocks:", blocks
# console.debug "parts_inbetween:", parts_inbetween
# console.debug "s", s, "e", e, "v", v
block_move = 0
if ev.keyCode() in [9, 33, 34]
# TAB, PAGE UP/DOWN # TAB removed (above)
if ev.shiftKey() or
ev.keyCode() == 33
block_move = -1
else
block_move = 1
for block, idx in blocks
# console.debug match_start, match_end, match_str
if block.start == s and block.end == e
if block_move
block_jump_to = idx+block_move
break
if ev.keyCode() == 38
block.incrementBlock(block, blocks)
else
block.decrementBlock(block, blocks)
block_jump_to = idx
break
if (s == e or ( and .start == .end)) and block.start <= s <= block.end
# mark block
# console.debug "cursor in block", s, e
block_jump_to = idx
continue
if block_move and s == 0 and e == v.length and blocks.length > 1
if block_move == -1
block_jump_to = blocks.length-1
else
block_jump_to = 0
if bl = blocks[block_jump_to]
new_str = [parts_inbetween[0]]
for block, idx in blocks
new_str.push(block.string)
new_str.push(parts_inbetween[idx+1])
new_value = new_str.join("")
if not
ev.preventDefault()
return
.value = new_value
ev.preventDefault()
return
__removeContentSize: ->
CUI.dom.remove()
= null
@
__removeShadowInput: ->
# console.error "removeShadowInput",
CUI.dom.remove()
= null
= false
@
preventInvalidInput: ->
if and
true
else
false
__initShadowInput: ->
if not ( or or or or )
return
if
return
# console.debug "initShadowInput",
#
if
= CUI.dom.$element("textarea", "cui-input-shadow")
else
= CUI.dom.$element("input", "cui-input-shadow", type: "text")
.setAttribute("tabindex", "-1")
.setAttribute("autocomplete", "off")
CUI.dom.append(document.body, )
if
CUI.Events.listen
type: "input"
node:
call: (ev) =>
# console.debug ev.type, "unfocus shadow input"
new CUI.Event
type: "input"
node:
.dispatch()
return
CUI.Events.listen
type: "keyup"
node:
call: (ev) =>
# console.debug ev.type, "unfocus shadow input"
# console.debug "shadow", ev.type
return
@
__shadowInput: (ev) ->
shadow_v = .value
if and shadow_v.split("\n").length >
return
if and shadow_v.length > 0
ret =
# console.debug "checking shadow input", ret, shadow_v
if ret == false
return
if not
.value =
.setSelectionRange(.selectionStart, .selectionEnd)
# console.debug "shadow before init cursor", ?.start.idx, "-", ?.end.idx
# console.debug "shadow after init cursor", ?.start.idx, "-", ?.end.idx
return
checkValue: (v) ->
if not CUI.util.isString(v) or null
throw new Error("#{@__cls}.checkValue(value): Value needs to be String or null.")
@
render: ->
super()
#
for k in ["empty", "invalid", "valid"]
@
getTemplateKeyForRender: ->
null
isRequired: ->
updateInputState: (=) ->
if
else
state =
switch state
when "empty", "valid"
when "invalid"
for k in ["empty", "invalid", "valid"]
CUI.dom.hideElement([k]?.DOM)
if not and state == "invalid"
CUI.dom.showElement(.empty?.DOM)
else
CUI.dom.showElement([state]?.DOM)
@
getInputState: ->
if != false
return "valid"
if or
return "invalid"
return "empty"
leaveInput: ->
if != "invalid"
.value =
@
enterInput: ->
if != "invalid"
.value =
@
hasUserInput: ->
.value.length > 0
checkInput: (value) ->
state =
if not
state
__checkInputInternal: (value = .value) ->
if
else
true
setInputHint: (txt) ->
.input?.setText(txt)
setInvalidHint: (txt) ->
.invalid?.setText(txt)
setValidHint: (txt) ->
.valid?.setText(txt)
displayValue: ->
super()
value = or ""
if value != .value
# prevent focus loss if value is the same
.value = value
@
getValueForDisplay: ->
if
else
getValueForInput: ->
if
else
correctValueForInput: (value) ->
if
else
value
getDefaultValue: ->
""
getValue: ->
if
super()
else
?.value
enable: ->
super()
?.removeAttribute("disabled")
disable: ->
super()
?.setAttribute("disabled", true)
focus: ->
?.focus()
@
getCursorBlocks: ->
blocks = ?(.value)
findBlock: (blocks, idx, cut) ->
for block in blocks
if idx == block.start == block.end
return block
if cut == "full" and idx >= block.start and idx <= block.end
return block
if cut == "left" and idx >= block.start and idx < block.end
return block
if cut == "right" and idx > block.start and idx <= block.end
return block
if cut == "touch" and idx >= block.start and idx <= block.end
return block
return null
initCursor: (ev) ->
blocks =
if blocks == false
= null
return
if blocks.length == 0
console.warn "initCursor: 0 cursor blocks"
= null
return
# console.debug "initCursor", ev.type, ev.which, ev.shiftKey # , blocks
# find block which fits the current selection
# positions
#
s = .selectionStart
e = .selectionEnd
len = .value.length
# console.debug "requested: start: ",s, "end: ",e
=
shift: ?.shift
start: null
end: null
if ev.getType() == "keyup" and ev.keyCode() == 16
.shift = null
if ev.getType() == "keydown" and ev.keyCode() in [46, 8]
.shift = null
if CUI.util.isUndef(.shift)
.shift = null
.start =
.end =
# console.debug "found cursors", .start, .end
if .end?.idx < .start?.idx
.end = .start
if s == e and not .start and not .end
# find closest blocks to the left and right
dist_left = null
dist_right = null
for i in [s..0] by -1
block_left =
if block_left
dist_left = (s-i)
break
for i in [s...len] by 1
block_right =
if block_right
dist_right = (i-s)
break
if block_right and not block_left
.start = block_right
else if block_left and not block_right
.start = block_left
else if block_left and block_right
if dist_left > dist_right
.start = block_right
else
.start = block_left
# console.debug "found block in dist:", dist_left, dist_right
range =
# console.debug "cursor", "start:", .start?.idx, "end:", .end?.idx, range
if not .start and not .end
.start = .end = blocks[blocks.length-1]
else if not .start
.start = .end
else if not .end
.end = .start
# console.debug "cursor CLEANED", "start:", .start?.idx, "end:", .end?.idx, range
# console.debug "range", range
if range[0] == s and range[1] == e
1
# console.debug "cursor is good"
return
showCursor: (ev) ->
if
r =
.setSelectionRange(r[0], r[1])
@
checkSelectionChange: ->
sel =
if and (
.start != sel.start or
.end != sel.end )
= sel
@
getRangeFromCursor: ->
[ .start?.start, .end?.end ]
moveCursor: (ev) ->
if not
return
ev.preventDefault()
# console.debug "moveCursor", ev.type, ev.which
blocks =
if blocks == false or blocks.length == 0
# console.debug "no block found"
= null
return
if ev.keyCode() == 36 # POS1
.start = blocks[0]
.end = blocks[0]
return
if ev.keyCode() == 35 # END
.start = blocks[blocks.length-1]
.end = blocks[blocks.length-1]
return
if ?.keyCode() == 46 # DELETE
# dont move cursor, positioning will be down
# in keyup
return
if ?.keyCode() == 8 # BACKSPACE
return
left = ev.keyCode() == 37
right = (ev.keyCode() == 39 or ev.getType() == "input")
s_idx = .start.idx
e_idx = .end.idx
if not blocks[s_idx] or not blocks[e_idx]
console.warn "repositioning cursor, not executing cursor move"
return
if ev.keyCode() == 46 # DELETE
return
if ev.shiftKey() and .shift == null
.shift = .end.idx
if .shift == null
if s_idx == e_idx # move only if we have a single cursor
if left
if s_idx > 0
.start = blocks[s_idx-1]
else if right
if s_idx < blocks.length-1
.start = blocks[s_idx+1]
.end = .start
else if left
.end = .start
else if right
.start = .end
else
c_idx = .shift
# console.debug "SHIFT ME! ", "start:", s_idx, "end:", e_idx, "shift:", c_idx
if left
if c_idx >= e_idx
if s_idx > 0
.start = blocks[s_idx-1]
else
.end = blocks[e_idx-1]
else if right
if c_idx > s_idx
.start = blocks[s_idx+1]
else
if e_idx < blocks.length-1
.end = blocks[e_idx+1]
#console.debug "moveCursor new range",
@
destroy: ->
super()
: 0