Fix issues with cy.type (#2016)

this grew to a large PR fixing many cy.type issues.

fix #365
fix #420
fix #586 
fix #593 
fix #596 
fix #610 
fix #651
fix #940
fix #1002 
fix #1108
fix #1171
fix #1209 
fix #1234 
fix #1366
fix #1381 
fix #1684 
fix #1686
fix #1926 
fix #2056
fix #2096 
fix #2110 
fix #2173
fix #2187
This commit is contained in:
Ben Kucera
2018-07-23 04:43:16 -04:00
committed by Brian Mann
parent 5231e79c62
commit 9f28aea5dc
17 changed files with 1629 additions and 1143 deletions

View File

@@ -7,7 +7,7 @@
"scripts": {
"start": "../coffee/node_modules/.bin/coffee test/support/server.coffee",
"cypress:open": "node ../../cli/bin/cypress open --dev --project ./test",
"cypress:run": "node ../../scripts/run-cypress-tests.js --browser chrome --dir test",
"cypress:run": "node ../../scripts/run-cypress-tests.js --dir test",
"clean-deps": "rm -rf node_modules"
},
"files": [
@@ -53,9 +53,11 @@
"parse-domain": "2.0.0",
"setimmediate": "^1.0.2",
"sinon": "3.2.0",
"text-mask-addons": "^3.7.2",
"underscore": "^1.8.3",
"underscore.string": "3.3.4",
"url-parse": "^1.1.7",
"vanilla-text-mask": "^5.1.1",
"wait-on": "^2.0.2",
"zone.js": "^0.8.18"
}

View File

@@ -247,7 +247,7 @@ verify = (cy, $el, options, callbacks) ->
## then do not perform these additional ensures...
if (force isnt true) and (options.waitForAnimations isnt false)
## store the coords that were absolute
## from the window or from the viewport for sticky elements
## from the window or from the viewport for sticky elements
## (see https://github.com/cypress-io/cypress/pull/1478)
sticky = !!getStickyEl($el)

View File

@@ -4,6 +4,7 @@ Promise = require("bluebird")
$dom = require("../../../dom")
$utils = require("../../../cypress/utils")
$elements = require("../../../dom/elements")
checkOrUncheck = (type, subject, values = [], options = {}) ->
## we're not handling conversion of values to strings
@@ -46,13 +47,23 @@ checkOrUncheck = (type, subject, values = [], options = {}) ->
## in the values array?
## or values array is empty
elHasMatchingValue = ($el) ->
values.length is 0 or $el.val() in values
value = $elements.getNativeProp($el.get(0), "value")
values.length is 0 or value in values
## blow up if any member of the subject
## isnt a checkbox or radio
checkOrUncheckEl = (el, index) =>
$el = $(el)
if not isAcceptableElement($el)
node = $dom.stringify($el)
word = $utils.plural(options.$el, "contains", "is")
phrase = if type is "check" then " and :radio" else ""
$utils.throwErrByPath "check_uncheck.invalid_element", {
onFail: options._log
args: { node, word, phrase, cmd: type }
}
isElActionable = elHasMatchingValue($el)
if isElActionable
@@ -78,14 +89,6 @@ checkOrUncheck = (type, subject, values = [], options = {}) ->
options._log.snapshot("before", {next: "after"})
if not isAcceptableElement($el)
node = $dom.stringify($el)
word = $utils.plural(options.$el, "contains", "is")
phrase = if type is "check" then " and :radio" else ""
$utils.throwErrByPath "check_uncheck.invalid_element", {
onFail: options._log
args: { node, word, phrase, cmd: type }
}
## if the checkbox was already checked
## then notify the user of this note

View File

@@ -6,6 +6,8 @@ $Mouse = require("../../../cypress/mouse")
$dom = require("../../../dom")
$utils = require("../../../cypress/utils")
$elements = require("../../../dom/elements")
$selection = require("../../../dom/selection")
$actionability = require("../../actionability")
module.exports = (Commands, Cypress, cy, state, config) ->
@@ -183,7 +185,9 @@ module.exports = (Commands, Cypress, cy, state, config) ->
onReady: ($elToClick, coords) ->
## TODO: get focused through a callback here
$focused = cy.getFocused()
el = $elToClick.get(0)
## record the previously focused element before
## issuing the mousedown because browsers may
## automatically shift the focus to the element
@@ -199,11 +203,14 @@ module.exports = (Commands, Cypress, cy, state, config) ->
if domEvents.mouseDown.preventedDefault or not $dom.isAttached($elToClick)
afterMouseDown($elToClick, coords)
else
if $elements.isInput(el) or $elements.isTextarea(el) or $elements.isContentEditable(el)
if !$elements.isNeedSingleValueChangeInputElement(el)
$selection.moveSelectionToEnd(el)
## retrieve the first focusable $el in our parent chain
$elToFocus = getFirstFocusableEl($elToClick)
$focused = cy.getFocused()
if shouldFireFocusEvent($focused, $elToFocus)
## if our mousedown went through and
## we are focusing a different element

View File

@@ -4,6 +4,7 @@ Promise = require("bluebird")
$dom = require("../../../dom")
$utils = require("../../../cypress/utils")
$elements = require('../../../dom/elements')
newLineRe = /\n/g
@@ -70,7 +71,7 @@ module.exports = (Commands, Cypress, cy, state, config) ->
optionsObjects = options.$el.find("option").map((index, el) ->
## push the value in values array if its
## found within the valueOrText
value = el.value
value = $elements.getNativeProp(el, "value")
optEl = $(el)
if value in valueOrText
@@ -101,7 +102,8 @@ module.exports = (Commands, Cypress, cy, state, config) ->
_.each optionsObjects, (obj, index) ->
if obj.text in valueOrText
optionEls.push obj.$el
values.push(obj.value)
objValue = obj.value
values.push(objValue)
## if we didnt set multiple to true and
## we have more than 1 option to set then blow up

View File

@@ -4,6 +4,8 @@ Promise = require("bluebird")
moment = require("moment")
$dom = require("../../../dom")
$elements = require("../../../dom/elements")
$selection = require("../../../dom/selection")
$Keyboard = require("../../../cypress/keyboard")
$utils = require("../../../cypress/utils")
$actionability = require("../../actionability")
@@ -21,6 +23,7 @@ module.exports = (Commands, Cypress, cy, state, config) ->
Commands.addAll({ prevSubject: "element" }, {
type: (subject, chars, options = {}) ->
options = _.clone(options)
## allow the el we're typing into to be
## changed by options -- used by cy.clear()
_.defaults(options, {
@@ -61,10 +64,10 @@ module.exports = (Commands, Cypress, cy, state, config) ->
memo
, {}
options._log = Cypress.log
options._log = Cypress.log {
message: [chars, deltaOptions]
$el: options.$el
consoleProps: ->
consoleProps: -> {
"Typed": chars
"Applied To": $dom.getElements(options.$el)
"Options": deltaOptions
@@ -74,6 +77,8 @@ module.exports = (Commands, Cypress, cy, state, config) ->
data: getTableData()
columns: ["typed", "which", "keydown", "keypress", "textInput", "input", "keyup", "change", "modifiers"]
}
}
}
options._log.snapshot("before", {next: "after"})
@@ -210,27 +215,24 @@ module.exports = (Commands, Cypress, cy, state, config) ->
## consider changing type to a Promise and juggle logging
cy.now("submit", form, {log: false, $el: form})
dispatchChangeEvent = (id) ->
dispatchChangeEvent = (el, id) ->
change = document.createEvent("HTMLEvents")
change.initEvent("change", true, false)
dispatched = options.$el.get(0).dispatchEvent(change)
dispatched = el.dispatchEvent(change)
if id and updateTable
updateTable(id, null, "change", null, dispatched)
return dispatched
needSingleValueChange = ->
isDate or
isMonth or
isWeek or
isTime or
($dom.isType(options.$el, "number") and _.includes(options.chars, "."))
return $elements.isNeedSingleValueChangeInputElement(options.$el.get(0))
## see comment in updateValue below
typed = ""
isContentEditable = $elements.isContentEditable(options.$el.get(0))
isTextarea = $elements.isTextarea(options.$el.get(0))
$Keyboard.type({
$el: options.$el
chars: options.chars
@@ -238,17 +240,17 @@ module.exports = (Commands, Cypress, cy, state, config) ->
release: options.release
window: win
updateValue: (rng, key) ->
updateValue: (el, key) ->
## in these cases, the value must only be set after all
## the characters are input because attemping to set
## a partial/invalid value results in the value being
## set to an empty string
if needSingleValueChange()
## in these cases, the value must only be set after all
## the characters are input because attemping to set
## a partial/invalid value results in the value being
## set to an empty string
typed += key
if typed is options.chars
options.$el.val(options.chars)
$elements.setNativeProp(el, "value", options.chars)
else
rng.text(key, "end")
$selection.replaceSelectionContents(el, key)
onBeforeType: (totalKeys) ->
## for the total number of keys we're about to
@@ -277,25 +279,35 @@ module.exports = (Commands, Cypress, cy, state, config) ->
## fires only when the 'value'
## of input/text/contenteditable
## changes
onTypeChange: ->
## never fire any change events for contenteditable
return if options.$el.is("[contenteditable]")
onValueChange: (originalText, el) ->
## contenteditable should never be called here.
## only input's and textareas can have change events
if changeEvent = state("changeEvent")
if !changeEvent(null, true)
state("changeEvent", null)
return
state "changeEvent", ->
dispatchChangeEvent()
state "changeEvent", null
state "changeEvent", (id, readOnly) ->
changed = $elements.getNativeProp(el, 'value') isnt originalText
onEnterPressed: (changed, id) ->
if !readOnly
if changed
dispatchChangeEvent(el, id)
state "changeEvent", null
return changed
onEnterPressed: (id) ->
## dont dispatch change events or handle
## submit event if we've pressed enter into
## a textarea or contenteditable
return if options.$el.is("textarea,[contenteditable]")
return if isTextarea || isContentEditable
## if our value has changed since our
## element was activated we need to
## fire a change event immediately
if changed
dispatchChangeEvent(id)
if changeEvent = state("changeEvent")
changeEvent(id)
## handle submit event handler here
simulateSubmitHandler()
@@ -315,32 +327,33 @@ module.exports = (Commands, Cypress, cy, state, config) ->
## if it's the body, don't need to worry about focus
return type() if isBody
cy.now("focused", {log: false, verify: false})
.then ($focused) ->
$actionability.verify(cy, options.$el, options, {
onScroll: ($el, type) ->
Cypress.action("cy:scrolled", $el, type)
$actionability.verify(cy, options.$el, options, {
onScroll: ($el, type) ->
Cypress.action("cy:scrolled", $el, type)
onReady: ($elToClick) ->
## if we dont have a focused element
## or if we do and its not ourselves
## then issue the click
if not $focused or ($focused and $focused.get(0) isnt options.$el.get(0))
## click the element first to simulate focus
## and typical user behavior in case the window
## is out of focus
cy.now("click", $elToClick, {
$el: $elToClick
log: false
verify: false
_log: options._log
force: true ## force the click, avoid waiting
timeout: options.timeout
interval: options.interval
}).then(type)
else
## don't click, just type
onReady: ($elToClick) ->
$focused = cy.getFocused()
## if we dont have a focused element
## or if we do and its not ourselves
## then issue the click
if not $focused or ($focused and $focused.get(0) isnt options.$el.get(0))
## click the element first to simulate focus
## and typical user behavior in case the window
## is out of focus
cy.now("click", $elToClick, {
$el: $elToClick
log: false
verify: false
_log: options._log
force: true ## force the click, avoid waiting
timeout: options.timeout
interval: options.interval
})
.then ->
type()
else
type()
})
handleFocused()
@@ -377,13 +390,15 @@ module.exports = (Commands, Cypress, cy, state, config) ->
## figure out the options which actually change the behavior of clicks
deltaOptions = $utils.filterOutOptions(options)
options._log = Cypress.log
options._log = Cypress.log {
message: deltaOptions
$el: $el
consoleProps: ->
consoleProps: () -> {
"Applied To": $dom.getElements($el)
"Elements": $el.length
"Options": deltaOptions
}
}
node = $dom.stringify($el)

View File

@@ -6,7 +6,6 @@ moment = require("moment")
Promise = require("bluebird")
sinon = require("sinon")
lolex = require("lolex")
bililiteRange = require("../vendor/bililiteRange")
$dom = require("./dom")
$errorMessages = require("./cypress/error_messages")
@@ -477,7 +476,6 @@ class $Cypress
minimatch: minimatch
sinon: sinon
lolex: lolex
bililiteRange: bililiteRange
_.extend $Cypress.prototype.$, _.pick($, "Event", "Deferred", "ajax", "get", "getJSON", "getScript", "post", "when")

View File

@@ -1,10 +1,12 @@
_ = require("lodash")
Promise = require("bluebird")
bililiteRange = require("../../vendor/bililiteRange")
$elements = require("../dom/elements")
$selection = require("../dom/selection")
$Cypress = require("../cypress")
charsBetweenCurlyBraces = /({.+?})/
isSingleDigitRe = /^\d$/
isStartingDigitRe = /^\d/
charsBetweenCurlyBracesRe = /({.+?})/
# Keyboard event map
# https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
@@ -71,55 +73,60 @@ $Keyboard = {
}
specialChars: {
"{selectall}": (el, options) ->
options.rng.bounds('all').select()
"{selectall}": $selection.selectAll
## charCode = 46
## no keyPress
## no textInput
## yes input (if value is actually changed)
"{del}": (el, options) ->
{rng} = options
bounds = rng.bounds()
if @boundsAreEqual(bounds)
rng.bounds([bounds[0], bounds[0] + 1])
options.charCode = 46
options.keypress = false
options.textInput = false
options.setKey = "{del}"
@ensureKey el, null, options, ->
prev = rng.all()
rng.text("", "end")
## after applying the {del}
## if our text didnt change
## dont send the input event
if prev is rng.all()
options.input = false
@ensureKey el, null, options, ->
bounds = $selection.getSelectionBounds(el)
if $selection.isCollapsed(el)
## if there's no text selected, delete the prev char
## if deleted char, send the input event
options.input = $selection.deleteRightOfCursor(el)
return
## text is selected, so delete the selection
## contents and send the input event
$selection.deleteSelectionContents(el)
options.input = true
return
## charCode = 8
## no keyPress
## no textInput
## yes input (if value is actually changed)
"{backspace}": (el, options) ->
{rng} = options
bounds = rng.bounds()
if @boundsAreEqual(bounds)
rng.bounds([bounds[0] - 1, bounds[0]])
options.charCode = 8
options.keypress = false
options.textInput = false
options.setKey = "{backspace}"
@ensureKey el, null, options, ->
prev = rng.all()
rng.text("", "end")
## after applying the {backspace}
## if our text didnt change
## dont send the input event
if prev is rng.all()
options.input = false
if $selection.isCollapsed(el)
## if there's no text selected, delete the prev char
## if deleted char, send the input event
options.input = $selection.deleteLeftOfCursor(el)
return
## text is selected, so delete the selection
## contents and send the input event
$selection.deleteSelectionContents(el)
options.input = true
return
## charCode = 27
## no keyPress
## no textInput
@@ -144,45 +151,39 @@ $Keyboard = {
## no input
## yes change (if input is different from last change event)
"{enter}": (el, options) ->
{rng} = options
options.charCode = 13
options.textInput = false
options.input = false
options.setKey = "{enter}"
@ensureKey el, "\n", options, ->
rng.insertEOL()
changed = options.prev isnt rng.all()
options.onEnterPressed(changed, options.id)
$selection.replaceSelectionContents(el, "\n")
options.onEnterPressed(options.id)
## charCode = 37
## no keyPress
## no textInput
## no input
"{leftarrow}": (el, options) ->
{rng} = options
bounds = rng.bounds()
options.charCode = 37
options.keypress = false
options.textInput = false
options.input = false
options.setKey = "{leftarrow}"
@ensureKey el, null, options, ->
switch
when @boundsAreEqual(bounds)
## if bounds are equal move the caret
## 1 to the left
left = bounds[0] - 1
right = left
when bounds[0] > 0
## just set the cursor back to the left
## position
left = bounds[0]
right = left
else
left = 0
right = 0
$selection.moveCursorLeft(el)
rng.bounds([left, right])
## charCode = 39
## no keyPress
## no textInput
## no input
"{rightarrow}": (el, options) ->
options.charCode = 39
options.keypress = false
options.textInput = false
options.input = false
options.setKey = "{rightarrow}"
@ensureKey el, null, options, ->
$selection.moveCursorRight(el)
## charCode = 38
## no keyPress
@@ -194,34 +195,9 @@ $Keyboard = {
options.textInput = false
options.input = false
options.setKey = "{uparrow}"
@ensureKey(el, null, options)
## charCode = 39
## no keyPress
## no textInput
## no input
"{rightarrow}": (el, options) ->
{rng} = options
bounds = rng.bounds()
options.charCode = 39
options.keypress = false
options.textInput = false
options.input = false
options.setKey = "{rightarrow}"
@ensureKey el, null, options, ->
switch
when @boundsAreEqual(bounds)
## if bounds are equal move the caret
## 1 to the right
left = bounds[0] + 1
right = left
else
## just set the cursor back to the left
## position
right = bounds[1]
left = right
$selection.moveCursorUp(el)
rng.bounds([left, right])
## charCode = 40
## no keyPress
@@ -233,7 +209,8 @@ $Keyboard = {
options.textInput = false
options.input = false
options.setKey = "{downarrow}"
@ensureKey(el, null, options)
@ensureKey el, null, options, ->
$selection.moveCursorDown(el)
}
modifierChars: {
@@ -259,7 +236,7 @@ $Keyboard = {
onEvent: ->
onBeforeEvent: ->
onBeforeType: ->
onTypeChange: ->
onValueChange: ->
onEnterPressed: ->
onNoMatchingSpecialChars: ->
onBeforeSpecialCharAction: ->
@@ -267,79 +244,8 @@ $Keyboard = {
el = options.$el.get(0)
bililiteRangeSelection = el.bililiteRangeSelection
rng = bililiteRange(el).bounds("selection")
## if the value has changed since previously typing, we need to
## update the caret position if the value has changed
if el.prevValue and @expectedValueDoesNotMatchCurrentValue(el.prevValue, rng)
@moveCaretToEnd(rng)
el.prevValue = rng.all()
bililiteRangeSelection = el.bililiteRangeSelection = rng.bounds()
## store the previous text value
## so we know to fire change events
## and change callbacks
options.prev = rng.all()
resetBounds = (start, end) ->
if start? and end?
bounds = [start, end]
else
len = rng.length()
bounds = [len, len]
## resets the bounds to the
## end of the element's text
if not _.isEqual(rng._bounds, bounds)
el.bililiteRangeSelection = bounds
rng.bounds(bounds)
## restore the bounds if our el already has this
if bililiteRangeSelection
rng.bounds(bililiteRangeSelection)
else
## native date/moth/datetime/time input types
## do not have selectionStart so we have to
## manually fix the range on those elements.
## we know we need to do that when
## el.selectionStart throws or if the element
## does not have a selectionStart property
try
if "selectionStart" of el
el.selectionStart
else
resetBounds()
catch
## currently if this throws we're likely on
## a native input type (number, etc)
## and we're just going to take a shortcut here
## by figuring out if there is currently a
## selection range of the window. whatever that
## value is we need to set the range of the el.
## now this will fail if there is a PARTIAL range
## for instance if our element has value of: 121234
## and the selection range is '12' we cannot know
## if it is the [0,1] index or the [2,3] index. to
## fix this we need to walk forward and backward by
## s.modify('extend', 'backward', 'character') until
## we can definitely figure out where the selection is
## check if this fires selectionchange events. if it does
## we may need an option that enables to use to simply
## silence these events, or perhaps just TELL US where
## to type via the index.
try
selection = el.ownerDocument.getSelection().toString()
index = options.$el.val().indexOf(selection)
if selection.length and index > -1
resetBounds(index, selection.length)
else
resetBounds()
catch
resetBounds()
keys = options.chars.split(charsBetweenCurlyBraces).map (chars) ->
if charsBetweenCurlyBraces.test(chars)
keys = options.chars.split(charsBetweenCurlyBracesRe).map (chars) ->
if charsBetweenCurlyBracesRe.test(chars)
## allow special chars and modifiers to be case-insensitive
chars.toLowerCase()
else
@@ -351,18 +257,9 @@ $Keyboard = {
## how keystrokes come into javascript naturally
Promise
.each keys, (key) =>
@typeChars(el, rng, key, options)
@typeChars(el, key, options)
.then =>
## if after typing we ended up changing
## our value then fire the onTypeChange callback
if @expectedValueDoesNotMatchCurrentValue(options.prev, rng)
options.onTypeChange()
## after typing be sure to clear all ranges
if sel = options.window.getSelection()
sel.removeAllRanges()
unless options.release is false
if options.release isnt false
@resetModifiers(el, options.window)
countNumIndividualKeyStrokes: (keys) ->
@@ -377,9 +274,8 @@ $Keyboard = {
memo + chars.length
, 0
typeChars: (el, rng, chars, options) ->
typeChars: (el, chars, options) ->
options = _.clone(options)
options.rng = rng
switch
when @isSpecialChar(chars)
@@ -392,7 +288,7 @@ $Keyboard = {
.resolve @handleModifier(el, chars, options)
.delay(options.delay)
when charsBetweenCurlyBraces.test(chars)
when charsBetweenCurlyBracesRe.test(chars)
## between curly braces, but not a valid special
## char or modifier
allChars = _.keys(@specialChars).concat(_.keys(@modifierChars)).join(", ")
@@ -422,6 +318,7 @@ $Keyboard = {
simulateKey: (el, eventType, key, options) ->
## bail if we've said not to fire this specific event
## in our options
return true if options[eventType] is false
key = options.key ? key
@@ -503,54 +400,71 @@ $Keyboard = {
return dispatched
typeKey: (el, key, options) ->
## if we have an afterKey value it means
## we've typed in prior to this
if after = options.afterKey
## if this afterKey value is no longer the current value
## then something has altered the value and we need to
## automatically shift the caret to the end like a real browser
if @expectedValueDoesNotMatchCurrentValue(after, options.rng)
@moveCaretToEnd(options.rng)
@ensureKey el, key, options, ->
options.updateValue(options.rng, key)
## update the selection that's cached on the element
## and store the value for comparison in any future typing
el.bililiteRangeSelection = options.rng.bounds()
el.prevValue = el.value
isDigit = isSingleDigitRe.test(key)
isNumberInputType = $elements.isInput(el) and $elements.isInputType(el, 'number')
if isNumberInputType
selectionStart = el.selectionStart
valueLength = $elements.getNativeProp(el, "value").length
isDigitsInText = isStartingDigitRe.test(options.chars)
isValidCharacter = key is '.' or (key is '-' and valueLength)
prevChar = options.prevChar
if !isDigit and (isDigitsInText or !isValidCharacter or selectionStart isnt 0)
options.prevChar = key
return
## only type '.' and '-' if it is the first symbol and there already is a value, or if
## '.' or '-' are appended to a digit. If not, value cannot be set.
if isDigit and (prevChar is '.' or (prevChar is '-' and !valueLength))
options.prevChar = key
key = prevChar + key
options.updateValue(el, key)
ensureKey: (el, key, options, fn) ->
_.defaults(options, {
prevText: null
})
options.id = _.uniqueId("char")
options.beforeKey = options.rng.all()
# options.beforeKey = el.value
maybeUpdateValueAndFireInput = =>
## only call this function if we haven't been told not to
if fn and options.onBeforeSpecialCharAction(options.id, options.key) isnt false
if not $elements.isContentEditable(el)
prevText = $elements.getNativeProp(el, "value")
fn.call(@)
if options.prevText is null and not $elements.isContentEditable(el)
options.prevText = prevText
options.onValueChange(options.prevText, el)
@simulateKey(el, "input", key, options)
if @simulateKey(el, "keydown", key, options)
if @simulateKey(el, "keypress", key, options)
if @simulateKey(el, "textInput", key, options)
ml = el.maxLength
if $elements.isInput(el) or $elements.isTextarea(el)
ml = el.maxLength
## maxlength is -1 by default when omitted
## but could also be null or undefined :-/
if ml is 0 or ml > 0
## only cafe if we are trying to type a key
if (ml is 0 or ml > 0 ) and key
## check if we should update the value
## and fire the input event
## as long as we're under maxlength
if el.value.length < ml
if $elements.getNativeProp(el, "value").length < ml
maybeUpdateValueAndFireInput()
else
maybeUpdateValueAndFireInput()
## store the afterKey value so we know
## if something mutates the value between typing keys
options.afterKey = options.rng.all()
@simulateKey(el, "keyup", key, options)
isSpecialChar: (chars) ->

View File

@@ -7,7 +7,161 @@ $utils = require("../cypress/utils")
fixedOrStickyRe = /(fixed|sticky)/
focusable = "a[href],link[href],button,input,select,textarea,[tabindex],[contenteditable]"
contentEditable = '[contenteditable]'
focusable = "a[href],link[href],button,select,[tabindex],input,textarea,#{contentEditable}"
inputTypeNeedSingleValueChangeRe = /^(date|time|month|week)$/
canSetSelectionRangeElementRe = /^(text|search|URL|tel|password)$/
## rules for native methods and props
## if a setter or getter or function then add a native method
## if a traversal, don't
descriptor = (klass, prop) ->
Object.getOwnPropertyDescriptor(window[klass].prototype, prop)
_getValue = ->
switch
when isInput(this)
descriptor("HTMLInputElement", "value").get
when isTextarea(this)
descriptor("HTMLTextAreaElement", "value").get
when isSelect(this)
descriptor("HTMLSelectElement", "value").get
else
## is an option element
descriptor("HTMLOptionElement", "value").get
_setValue = ->
switch
when isInput(this)
descriptor("HTMLInputElement", "value").set
when isTextarea(this)
descriptor("HTMLTextAreaElement", "value").set
when isSelect(this)
descriptor("HTMLSelectElement", "value").set
else
## is an options element
descriptor("HTMLOptionElement", "value").set
_setSelectionRange = () ->
switch
when isInput(this)
window.HTMLInputElement.prototype.setSelectionRange
when isTextarea(this)
window.HTMLTextAreaElement.prototype.setSelectionRange
nativeGetters = {
value: _getValue
selectionStart: descriptor("HTMLInputElement", "selectionStart").get
isContentEditable: descriptor("HTMLElement", "isContentEditable").get
}
nativeSetters = {
value: _setValue
}
nativeMethods = {
createRange: window.document.createRange
execCommand: window.document.execCommand
getAttribute: window.Element.prototype.getAttribute
setSelectionRange: _setSelectionRange
modify: window.Selection.prototype.modify
}
tryCallNativeMethod = ->
try
callNativeMethod.apply(null, arguments)
catch
null
callNativeMethod = (obj, fn, args...) ->
if not nativeFn = nativeMethods[fn]
fns = _.keys(nativeMethods).join(", ")
throw new Error("attempted to use a native fn called: #{fn}. Available fns are: #{fns}")
retFn = nativeFn.apply(obj, args)
if _.isFunction(retFn)
retFn = retFn.apply(obj, args)
return retFn
getNativeProp = (obj, prop) ->
if not nativeProp = nativeGetters[prop]
props = _.keys(nativeGetters).join(", ")
throw new Error("attempted to use a native getter prop called: #{prop}. Available props are: #{props}")
retProp = nativeProp.call(obj, prop)
if _.isFunction(retProp)
## if we got back another function
## then invoke it again
retProp = retProp.call(obj, prop)
return retProp
setNativeProp = (obj, prop, val) ->
if not nativeProp = nativeSetters[prop]
fns = _.keys(nativeSetters).join(", ")
throw new Error("attempted to use a native setter prop called: #{fn}. Available props are: #{fns}")
retProp = nativeProp.call(obj, val)
if _.isFunction(retProp)
retProp = retProp.call(obj, val)
return retProp
isNeedSingleValueChangeInputElement = (el) ->
if !isInput(el)
return false
return inputTypeNeedSingleValueChangeRe.test(el.type)
## TODO: switch this to not use this
# getValue = (el) ->
# return getNativeProp(el, "value")
# if isTextarea(el)
# return nativeTextareaValueGetter.call(el)
## TODO: switch this to not use this
# _setValue = (el, val) ->
# ## sets value for <input> or <textarea>
# if isInput(el)
# return setNativeProp(el, "value", val)
# if isTextarea(el)
# return setNativeProp.call(el, val)
canSetSelectionRangeElement = (el) ->
canSetSelectionRangeElementRe.test(el.type)
getTagName = (el) ->
tagName = el.tagName or ""
tagName.toLowerCase()
isContentEditable = (el) ->
## this property is the tell-all for contenteditable
## should be true for elements:
## - with [contenteditable]
## - with document.designMode = 'on'
getNativeProp(el, "isContentEditable")
isTextarea = (el) ->
getTagName(el) is 'textarea'
isInput = (el) ->
getTagName(el) is 'input'
isSelect = (el) ->
getTagName(el) is 'select'
isOption = (el) ->
getTagName(el) is 'option'
isElement = (obj) ->
try
@@ -19,7 +173,12 @@ isFocusable = ($el) ->
$el.is(focusable)
isType = ($el, type) ->
($el.attr("type") or "").toLowerCase() is type
## NOTE: use DOMElement.type instead of getAttribute('type') since
## <input type="asdf"> will have type="text", and behaves like text type
($el.get(0).type or "").toLowerCase() is type
isInputType = (el, type) ->
el.type.toLowerCase() is type
isScrollOrAuto = (prop) ->
prop is "scroll" or prop is "auto"
@@ -300,8 +459,28 @@ module.exports = {
isDescendent
isContentEditable
isTextarea
isInputType
isInput
isNeedSingleValueChangeInputElement
canSetSelectionRangeElement
stringify
getNativeProp
setNativeProp
callNativeMethod
tryCallNativeMethod
getElements
getContainsSelector

View File

@@ -0,0 +1,467 @@
$document = require('./document')
$elements = require('./elements')
$ = require('jquery')
_getSelectionBoundsFromTextarea = (el) ->
{
start: el.selectionStart
end: el.selectionEnd
}
_getSelectionBoundsFromInput = (el) ->
{ type } = el
## HACK:
## newer versions of Chrome incorrectly report the selection
## for number and email types, so we change it to type=text
## and blur it (then set it back and focus it further down
## after the selection algorithm has taken place)
shouldChangeType = type is 'email' || type is 'number'
if shouldChangeType
el.blur()
el.type = 'text'
bounds = {
start: el.selectionStart
end: el.selectionEnd
}
## HACK:
## selection start and end don't report correctly when input
## already has a value set, so if there's a value and there is no
## native selection, force it to be at the end of the text
if shouldChangeType
el.type = type
el.focus()
return bounds
_getSelectionBoundsFromContentEditable = (el) ->
doc = $document.getDocumentFromElement(el)
if doc.getSelection
## global selection object
sel = doc.getSelection()
## selection has at least one range (most always 1; only 0 at page load)
if sel.rangeCount
## get the first (usually only) range obj
range = sel.getRangeAt(0)
## if div[contenteditable] > text
hostContenteditable = _getHostContenteditable(range.commonAncestorContainer)
if hostContenteditable is el
return {
start: range.startOffset
end: range.endOffset
}
return {
start: null
end: null
}
## TODO get ACTUAL caret position in contenteditable, not line
_replaceSelectionContentsContentEditable = (el, text) ->
doc = $document.getDocumentFromElement(el)
## NOTE: insertText will also handle '\n', and render newlines
$elements.callNativeMethod(doc, "execCommand", 'insertText', true, text)
return
## Keeping around native implementation
## for same reasons as listed below
##
# if text is "\n"
# return _insertNewlineIntoContentEditable(el)
# doc = $document.getDocumentFromElement(el)
# range = _getSelectionRangeByEl(el)
# ## delete anything in the selection
# startNode = range.startContainer
# range.deleteContents()
# newTextNode
# if text is ' '
# newTextNode = doc.createElement('p')
# else
# newTextNode = doc.createTextNode(text)
# if $elements.isElement(startNode)
# if startNode.firstChild?.tagName is 'BR'
# range.selectNode(startNode.firstChild)
# range.deleteContents()
# ## else startNode is el, so just insert the node
# startNode.appendChild(newTextNode)
# if text is ' '
# newTextNode.outerHTML = '&nbsp;'
# range.selectNodeContents(startNode.lastChild)
# range.collapse()
# return
# else
# # nodeOffset = range.startOffset
# # oldValue = startNode.nodeValue || ''
# range.insertNode(newTextNode)
# range.selectNodeContents(newTextNode)
# range.collapse()
# if text is ' '
# newTextNode.outerHTML = '&nbsp;'
# # updatedValue = _insertSubstring(oldValue, text, [nodeOffset, nodeOffset])
# # newNodeOffset = nodeOffset + text.length
# # startNode.nodeValue = updatedValue
# el.normalize()
_insertSubstring = (curText, newText, [start, end]) ->
curText.substring(0, start) + newText + curText.substring(end)
_getHostContenteditable = (el) ->
while el.parentElement && !$elements.tryCallNativeMethod(el, "getAttribute", "contenteditable")
el = el.parentElement
return el
_getInnerLastChild = (el) ->
while (el.lastChild)
el = el.lastChild
return el
_getSelectionByEl = (el) ->
doc = $document.getDocumentFromElement(el)
doc.getSelection()
_getSelectionRangeByEl = (el) ->
sel = _getSelectionByEl(el)
if sel.rangeCount > 0
sel.getRangeAt(0)
else throw new Error('No selection in document')
deleteSelectionContents = (el) ->
if $elements.isContentEditable(el)
doc = $document.getDocumentFromElement(el)
# return doc.execCommand('delete', true, null)
$elements.callNativeMethod(doc, "execCommand", 'delete', true, null)
return
## for input and textarea, update selected text with empty string
replaceSelectionContents(el, "")
setInputSelectionRange = (el, start, end) ->
if $elements.canSetSelectionRangeElement(el)
## el.setSelectionRange(start, end)
$elements.callNativeMethod(el, "setSelectionRange", start, end)
return
## NOTE: Some input elements have mobile implementations
## and thus may not always have a cursor, so calling setSelectionRange will throw.
## we are assuming desktop here, so we cast to type='text', and get the cursor from there.
{ type } = el
el.blur()
el.type = 'text'
$elements.callNativeMethod(el, "setSelectionRange", start, end)
el.type = type
## changing the type will lose focus, so focus again
el.focus()
deleteRightOfCursor = (el) ->
if $elements.isTextarea(el) || $elements.isInput(el)
{start, end} = getSelectionBounds(el)
if start is $elements.getNativeProp(el, "value").length
## nothing to delete, nothing to right of selection
return false
setInputSelectionRange(el, start, end + 1)
deleteSelectionContents(el)
## successful delete
return true
if $elements.isContentEditable(el)
selection = _getSelectionByEl(el)
$elements.callNativeMethod(selection, "modify", 'extend', 'forward', 'character')
if selection.isCollapsed
## there's nothing to delete
return false
deleteSelectionContents(el)
## successful delete
return true
deleteLeftOfCursor = (el) ->
if $elements.isTextarea(el) || $elements.isInput(el)
{start, end} = getSelectionBounds(el)
if start is 0
## there's nothing to delete, nothing before cursor
return false
setInputSelectionRange(el, start - 1, end)
deleteSelectionContents(el)
## successful delete
return true
if $elements.isContentEditable(el)
## there is no 'backwardDelete' command for execCommand, so use the Selection API
selection = _getSelectionByEl(el)
$elements.callNativeMethod(selection, 'modify', 'extend', 'backward', 'character')
if selection.isCollapsed
## there's nothing to delete
## since extending the selection didn't do anything
return false
deleteSelectionContents(el)
## successful delete
return true
_collapseInputOrTextArea = (el, toIndex) ->
setInputSelectionRange(el, toIndex, toIndex)
moveCursorLeft = (el) ->
if $elements.isTextarea(el) || $elements.isInput(el)
{start, end} = getSelectionBounds(el)
if start isnt end
return _collapseInputOrTextArea(el, start)
if start is 0
return
return setInputSelectionRange(el, start - 1, start - 1)
if $elements.isContentEditable(el)
selection = _getSelectionByEl(el)
$elements.callNativeMethod(selection, "modify", "move", "backward", "character")
## Keeping around native implementation
## for same reasons as listed below
##
# range = _getSelectionRangeByEl(el)
# if !range.collapsed
# return range.collapse(true)
# if range.startOffset is 0
# return _contenteditableMoveToEndOfPrevLine(el)
# newOffset = range.startOffset - 1
# range.setStart(range.startContainer, newOffset)
# range.setEnd(range.startContainer, newOffset)
moveCursorRight = (el) ->
if $elements.isTextarea(el) || $elements.isInput(el)
{start, end} = getSelectionBounds(el)
if start isnt end
return _collapseInputOrTextArea(el, end)
## Don't worry about moving past the end of the string
## nothing will happen and there is no error.
return setInputSelectionRange(el, start + 1, end + 1)
if $elements.isContentEditable(el)
selection = _getSelectionByEl(el)
$elements.callNativeMethod(selection, "modify", "move", "forward", "character")
moveCursorUp = (el) ->
_moveCursorUpOrDown(el, true)
moveCursorDown = (el) ->
_moveCursorUpOrDown(el, false)
_moveCursorUpOrDown = (el, up) ->
if $elements.isInput(el)
## on an input, instead of moving the cursor
## we want to perform the native browser action
## which is to increment the step/interval
if $elements.isInputType(el, 'number')
if up then el.stepUp?() else el.stepDown?()
return
if $elements.isTextarea(el) || $elements.isContentEditable(el)
selection = _getSelectionByEl(el)
$elements.callNativeMethod(selection, "modify",
"move"
if up then "backward" else "forward"
"line"
)
isCollapsed = (el) ->
if $elements.isTextarea(el) || $elements.isInput(el)
{start, end} = getSelectionBounds(el)
return start is end
if $elements.isContentEditable(el)
selection = _getSelectionByEl(el)
return selection.isCollapsed
selectAll = (el) ->
if $elements.isTextarea(el) || $elements.isInput(el)
return el.select()
if $elements.isContentEditable(el)
doc = $document.getDocumentFromElement(el)
## doc.execCommand('selectAll', true, null)
$elements.callNativeMethod(doc, 'execCommand', 'selectAll', true, null)
## Keeping around native implementation
## for same reasons as listed below
##
# range = _getSelectionRangeByEl(el)
# range.selectNodeContents(el)
# range.deleteContents()
# return
# startTextNode = _getFirstTextNode(el.firstChild)
# endTextNode = _getInnerLastChild(el.lastChild)
# range.setStart(startTextNode, 0)
# range.setEnd(endTextNode, endTextNode.length)
getSelectionBounds = (el) ->
## this function works for input, textareas, and contentEditables
switch
when $elements.isInput(el)
_getSelectionBoundsFromInput(el)
when $elements.isTextarea(el)
_getSelectionBoundsFromTextarea(el)
when $elements.isContentEditable(el)
_getSelectionBoundsFromContentEditable(el)
else
{
start: null
end: null
}
moveSelectionToEnd = (el) ->
if $elements.isInput(el) || $elements.isTextarea(el)
length = $elements.getNativeProp(el, "value").length
setInputSelectionRange(el, length, length)
else if $elements.isContentEditable(el)
## NOTE: can't use execCommand API here because we would have
## to selectAll and then collapse so we use the Selection API
doc = $document.getDocumentFromElement(el)
range = $elements.callNativeMethod(doc, "createRange")
hostContenteditable = _getHostContenteditable(el)
lastTextNode = _getInnerLastChild(hostContenteditable)
if lastTextNode.tagName is 'BR'
lastTextNode = lastTextNode.parentNode
range.setStart(lastTextNode, lastTextNode.length)
range.setEnd(lastTextNode, lastTextNode.length)
sel = doc.getSelection()
sel.removeAllRanges()
sel.addRange(range)
## TODO: think about renaming this
replaceSelectionContents = (el, key) ->
if $elements.isContentEditable(el)
return _replaceSelectionContentsContentEditable(el, key)
if $elements.isInput(el) or $elements.isTextarea(el)
{start, end} = getSelectionBounds(el)
value = $elements.getNativeProp(el, "value") or ''
updatedValue = _insertSubstring(value, key, [start, end])
$elements.setNativeProp(el, 'value', updatedValue)
setInputSelectionRange(el, start + key.length, start + key.length)
getCaretPosition = (el) ->
bounds = getSelectionBounds(el)
if !bounds.start?
## no selection
return null
if bounds.start is bounds.end
return bounds.start
return null
## Selection API implementation of insert newline.
## Worth keeping around if we ever have to insert native
## newlines if we are trying to support a browser or
## environment without the document.execCommand('insertText', etc...)
##
# _insertNewlineIntoContentEditable = (el) ->
# selection = _getSelectionByEl(el)
# selection.deleteFromDocument()
# $elements.callNativeMethod(selection, "modify", 'extend', 'forward', 'lineboundary')
# range = selection.getRangeAt(0)
# clonedElements = range.cloneContents()
# selection.deleteFromDocument()
# elementToInsertAfter
# if range.startContainer is el
# elementToInsertAfter = _getInnerLastChild(el)
# else
# curEl = range.startContainer
# ## make sure we have firstLevel child element from contentEditable
# while (curEl.parentElement && curEl.parentElement isnt el )
# curEl = curEl.parentElement
# elementToInsertAfter = curEl
# range = _getSelectionRangeByEl(el)
# outerNewElement = '<div></div>'
# ## TODO: In contenteditables, should insert newline element as either <div> or <p> depending on existing nodes
# ## but this shouldn't really matter that much, so ignore for now
# # if elementToInsertAfter.tagName is 'P'
# # typeOfNewElement = '<p></p>'
# $newElement = $(outerNewElement).append(clonedElements)
# $newElement.insertAfter(elementToInsertAfter)
# newElement = $newElement.get(0)
# if !newElement.innerHTML
# newElement.innerHTML = '<br>'
# range.selectNodeContents(newElement)
# else
# newTextNode = _getFirstTextNode(newElement)
# range.selectNodeContents(newTextNode)
# range.collapse(true)
# _contenteditableMoveToEndOfPrevLine = (el) ->
# bounds = _contenteditableGetNodesAround(el)
# if bounds.prev
# range = _getSelectionRangeByEl(el)
# prevTextNode = _getInnerLastChild(bounds.prev)
# range.setStart(prevTextNode, prevTextNode.length)
# range.setEnd(prevTextNode, prevTextNode.length)
# _contenteditableMoveToStartOfNextLine = (el) ->
# bounds = _contenteditableGetNodesAround(el)
# if bounds.next
# range = _getSelectionRangeByEl(el)
# nextTextNode = _getFirstTextNode(bounds.next)
# range.setStart(nextTextNode, 1)
# range.setEnd(nextTextNode, 1)
# _contenteditableGetNodesAround = (el) ->
# range = _getSelectionRangeByEl(el)
# textNode = range.startContainer
# curEl = textNode
# while curEl && !curEl.nextSibling?
# curEl = curEl.parentNode
# nextTextNode = _getFirstTextNode(curEl.nextSibling)
# curEl = textNode
# while curEl && !curEl.previousSibling?
# curEl = curEl.parentNode
# prevTextNode = _getInnerLastChild(curEl.previousSibling)
# {
# prev: prevTextNode
# next: nextTextNode
# }
# _getFirstTextNode = (el) ->
# while (el.firstChild)
# el = el.firstChild
# return el
module.exports = {
getSelectionBounds
deleteRightOfCursor
deleteLeftOfCursor
selectAll
deleteSelectionContents
moveSelectionToEnd
getCaretPosition
moveCursorLeft
moveCursorRight
moveCursorUp
moveCursorDown
replaceSelectionContents
isCollapsed
}

View File

@@ -44,6 +44,11 @@
margin-left: 0%;
}
}
[contenteditable] p {
display: block
}
</style>
DOM Fixture
@@ -143,6 +148,7 @@
<input type="checkbox" name="colors" value="blue" />
<input type="checkbox" name="colors" value="green" />
<input type="checkbox" name="colors" value="red" />
<input type="tel">
</form>
<form>

View File

@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script
type="text/javascript"
src="/node_modules/vanilla-text-mask/dist/vanillaTextMask.js"></script>
<script src="/node_modules/text-mask-addons/dist/createNumberMask.js"></script>
</head>
<body>
<label>Phone:</label><input id="phone" />
<br>
<label>Date:</label><input type="text" id="date">
<br>
<label>Dollar:</label><input type="text" id="dollar">
<br>
<label>Card:</label><input type="text" id="card">
<script type="text/javascript">
// Assuming you have an input element in your HTML with the class .myInput
const masks = {
phone: {
inputElement: document.querySelector('#phone'),
mask: ['(', /[1-9]/, /\d/, /\d/, ')', ' ', /\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/]
},
date: {
inputElement: document.querySelector('#date'),
mask: [/\d/, /\d/, '/', /\d/, /\d/, '/', /\d/, /\d/, /\d/, /\d/]
},
dollar: {
inputElement: document.querySelector('#dollar'),
mask: createNumberMask.default({
prefix: '',
suffix: ' $',
allowDecimal: true,
})
},
card: {
inputElement: document.querySelector('#card'),
mask: [/\d/, /\d/, /\d/, /\d/, ' ', /\d/, /\d/, /\d/, /\d/, ' ', /\d/, /\d/, /\d/, /\d/, ' ', /\d/, /\d/, /\d/, /\d/],
}
}
Object.keys(masks).forEach((name) => {
vanillaTextMask.maskInput(masks[name])
})
// Calling `vanillaTextMask.maskInput` adds event listeners to the input element.
// If you need to remove those event listeners, you can call
// maskedInputController.destroy()
</script>
</body>
</html>

View File

@@ -188,9 +188,12 @@ describe "src/cy/commands/actions/check", ->
$("[name=colors][value=blue]").change -> done()
cy.get("[name=colors]").check("blue")
it "emits focus event", (done) ->
$("[name=colors][value=blue]").focus -> done()
cy.get("[name=colors]").check("blue")
it "emits focus event", () ->
focus = false
$("[name=colors][value=blue]").focus -> focus = true
cy.get("[name=colors]")
.check("blue")
.then -> expect(focus).to.eq true
describe "errors", ->
beforeEach ->

View File

@@ -334,6 +334,67 @@ describe "src/cy/commands/actions/click", ->
it "can click a tr", ->
cy.get("#table tr:first").click()
it "places cursor at the end of input", ->
cy.get('input:first').invoke('val', 'foobar').click().then ($el) ->
el = $el.get(0)
expect(el.selectionStart).to.eql(6)
expect(el.selectionEnd).to.eql(6)
cy.get('input:first').invoke('val', '').click().then ($el) ->
el = $el.get(0)
expect(el.selectionStart).to.eql(0)
expect(el.selectionEnd).to.eql(0)
it "places cursor at the end of textarea", ->
cy.get('textarea:first').invoke('val', 'foo\nbar\nbaz').click().then ($el) ->
el = $el.get(0)
expect(el.selectionStart).to.eql(11)
expect(el.selectionEnd).to.eql(11)
cy.get('textarea:first').invoke('val', '').click().then ($el) ->
el = $el.get(0)
expect(el.selectionStart).to.eql(0)
expect(el.selectionEnd).to.eql(0)
it "places cursor at the end of [contenteditable]", ->
cy.get('[contenteditable]:first')
.invoke('html', '<div><br></div>').click()
.then ($el) ->
range = $el.get(0).ownerDocument.getSelection().getRangeAt(0)
expect(range.startContainer.outerHTML).to.eql('<div><br></div>')
expect(range.startOffset).to.eql(0)
expect(range.endContainer.outerHTML).to.eql('<div><br></div>')
expect(range.endOffset).to.eql(0)
cy.get('[contenteditable]:first')
.invoke('html', 'foo').click()
.then ($el) ->
range = $el.get(0).ownerDocument.getSelection().getRangeAt(0)
expect(range.startContainer.nodeValue).to.eql('foo')
expect(range.startOffset).to.eql(3)
expect(range.endContainer.nodeValue).to.eql('foo')
expect(range.endOffset).to.eql(3)
cy.get('[contenteditable]:first')
.invoke('html', '<div>foo</div>').click()
.then ($el) ->
range = $el.get(0).ownerDocument.getSelection().getRangeAt(0)
expect(range.startContainer.nodeValue).to.eql('foo')
expect(range.startOffset).to.eql(3)
expect(range.endContainer.nodeValue).to.eql('foo')
expect(range.endOffset).to.eql(3)
cy.get('[contenteditable]:first')
.invoke('html', '').click()
.then ($el) ->
el = $el.get(0)
range = el.ownerDocument.getSelection().getRangeAt(0)
expect(range.startContainer).to.eql(el)
expect(range.startOffset).to.eql(0)
expect(range.endContainer).to.eql(el)
expect(range.endOffset).to.eql(0)
describe "actionability", ->
it "can click elements which are hidden until scrolled within parent container", ->
cy.get("#overflow-auto-container").contains("quux").click()
@@ -440,8 +501,10 @@ describe "src/cy/commands/actions/click", ->
width: "100%"
}).appendTo($content)
cy.get('[data-cy=button]').click().then =>
expect(scrolled).to.deep.eq(["element", "element"])
## make scrolling deterministic by ensuring we don't wait for coordsHistory
## to build up
cy.get('[data-cy=button]').click({ waitForAnimations: false }).then ->
expect(scrolled).to.deep.eq(["element"])
it "can force click on hidden elements", ->
cy.get("button:first").invoke("hide").click({ force: true })

View File

@@ -1,8 +1,8 @@
$ = Cypress.$.bind(Cypress)
_ = Cypress._
Keyboard = Cypress.Keyboard
bililiteRange = Cypress.bililiteRange
Promise = Cypress.Promise
$selection = require("../../../../../src/dom/selection")
describe "src/cy/commands/actions/type", ->
before ->
@@ -78,11 +78,12 @@ describe "src/cy/commands/actions/type", ->
expect(blurred).to.be.true
it "can type into contenteditable", ->
oldText = cy.$$("#contenteditable").text()
oldText = cy.$$("#contenteditable").get(0).innerText
cy.get("#contenteditable").type("foo").then ($div) ->
text = _.clean $div.text()
expect(text).to.eq _.clean(oldText + "foo")
cy.get("#contenteditable")
.type(" foo")
.then ($div) ->
expect($div.get(0).innerText).to.eq (oldText + " foo")
it "delays 50ms before resolving", ->
cy.$$(":text:first").on "change", (e) =>
@@ -239,22 +240,29 @@ describe "src/cy/commands/actions/type", ->
expect(args[2]).to.eq(animationDistanceThreshold)
describe "input types where no extra formatting required", ->
_.each ["password", "email", "number", "search", "url", "tel"], (type) ->
it "accepts input [type=#{type}]", ->
input = cy.$$("<input type='#{type}' id='input-type-#{type}' />")
_.each [
"password"
"email"
"number"
"search"
"url"
"tel"
], (type) ->
it "accepts input [type=#{type}]", ->
input = cy.$$("<input type='#{type}' id='input-type-#{type}' />")
cy.$$("body").append(input)
cy.$$("body").append(input)
cy.get("#input-type-#{type}").type("1234").then ($input) ->
expect($input).to.have.value "1234"
expect($input.get(0)).to.eq $input.get(0)
cy.get("#input-type-#{type}").type("1234").then ($input) ->
expect($input).to.have.value "1234"
expect($input.get(0)).to.eq $input.get(0)
it "accepts type [type=#{type}], regardless of capitalization", ->
input = cy.$$("<input type='#{type.toUpperCase()}' id='input-type-#{type}' />")
it "accepts type [type=#{type}], regardless of capitalization", ->
input = cy.$$("<input type='#{type.toUpperCase()}' id='input-type-#{type}' />")
cy.$$("body").append(input)
cy.$$("body").append(input)
cy.get("#input-type-#{type}").type("1234")
cy.get("#input-type-#{type}").type("1234")
describe "tabindex", ->
beforeEach ->
@@ -512,6 +520,95 @@ describe "src/cy/commands/actions/type", ->
it "fires events for each key stroke"
it "does fire input event when value changes", ->
fired = false
cy.$$(":text:first").on "input", ->
fired = true
fired = false
cy.get(":text:first")
.invoke("val", "bar")
.type("{selectAll}{rightarrow}{backspace}")
.then ->
expect(fired).to.eq true
fired = false
cy.get(":text:first")
.invoke("val", "bar")
.type("{selectAll}{leftarrow}{del}")
.then ->
expect(fired).to.eq true
cy.$$('[contenteditable]:first').on "input", () ->
fired = true
fired = false
cy.get('[contenteditable]:first')
.invoke('html', 'foobar')
.type('{selectAll}{rightarrow}{backspace}')
.then ->
expect(fired).to.eq true
fired = false
cy.get('[contenteditable]:first')
.invoke('html', 'foobar')
.type('{selectAll}{leftarrow}{del}')
.then ->
expect(fired).to.eq true
it "does not fire input event when value does not change", ->
fired = false
cy.$$(":text:first").on "input", (e) ->
fired = true
fired = false
cy.get(":text:first")
.invoke("val", "bar")
.type('{selectAll}{rightarrow}{del}')
.then ->
expect(fired).to.eq false
fired = false
cy.get(":text:first")
.invoke("val", "bar")
.type('{selectAll}{leftarrow}{backspace}')
.then ->
expect(fired).to.eq false
cy.$$("textarea:first").on "input", (e) ->
fired = true
fired = false
cy.get("textarea:first")
.invoke("val", "bar")
.type('{selectAll}{rightarrow}{del}')
.then ->
expect(fired).to.eq false
fired = false
cy.get("textarea:first")
.invoke("val", "bar")
.type('{selectAll}{leftarrow}{backspace}')
.then ->
expect(fired).to.eq false
cy.$$('[contenteditable]:first').on "input", () ->
fired = true
fired = false
cy.get('[contenteditable]:first')
.invoke('html', 'foobar')
.type('{selectAll}{rightarrow}{del}')
.then ->
expect(fired).to.eq false
fired = false
cy.get('[contenteditable]:first')
.invoke('html', 'foobar')
.type('{selectAll}{leftarrow}{backspace}')
.then ->
expect(fired).to.eq false
describe "maxlength", ->
it "limits text entered to the maxlength attribute of a text input", ->
$input = cy.$$(":text:first")
@@ -605,12 +702,49 @@ describe "src/cy/commands/actions/type", ->
expect($text).to.have.value("foo bar")
it "overwrites text when currently has selection", ->
cy.$$("#input-without-value").val("0").click ->
$(@).select()
cy.get("#input-without-value").invoke('val', '0').then (el) ->
el.select()
cy.get("#input-without-value").type("50").then ($input) ->
expect($input).to.have.value("50")
it "overwrites text when selectAll in click handler", ->
cy.$$("#input-without-value").val("0").click ->
$(@).select()
it "overwrites text when selectAll in mouseup handler", ->
cy.$$("#input-without-value").val("0").mouseup ->
$(@).select()
it "overwrites text when selectAll in mouseup handler", ->
cy.$$("#input-without-value").val("0").mouseup ->
$(@).select()
it "responsive to keydown handler", ->
cy.$$("#input-without-value").val("1234").keydown ->
$(@).get(0).setSelectionRange(0,0)
cy.get("#input-without-value").type("56").then ($input) ->
expect($input).to.have.value("651234")
it "responsive to keyup handler", ->
cy.$$("#input-without-value").val("1234").keyup ->
$(@).get(0).setSelectionRange(0,0)
cy.get("#input-without-value").type("56").then ($input) ->
expect($input).to.have.value("612345")
it "responsive to input handler", ->
cy.$$("#input-without-value").val("1234").keyup ->
$(@).get(0).setSelectionRange(0,0)
cy.get("#input-without-value").type("56").then ($input) ->
expect($input).to.have.value("612345")
it "responsive to change handler", ->
cy.$$("#input-without-value").val("1234").change ->
$(@).get(0).setSelectionRange(0,0)
## no change event should be fired
cy.get("#input-without-value").type("56").then ($input) ->
expect($input).to.have.value("123456")
it "automatically moves the caret to the end if value is changed manually", ->
cy.$$("#input-without-value").keypress (e) ->
e.preventDefault()
@@ -621,6 +755,7 @@ describe "src/cy/commands/actions/type", ->
val = $input.val()
## setting value updates cursor to the end of input
$input.val(val + key + "-")
cy.get("#input-without-value").type("foo").then ($input) ->
@@ -720,12 +855,17 @@ describe "src/cy/commands/actions/type", ->
expect($text).to.have.value("1234")
it "overwrites text on input[type=number] when input has existing text selected", ->
cy.$$("#number-without-value").val("0").click ->
$(@).select()
cy.get("#number-without-value").invoke('val', "0").then (el) ->
el.get(0).select()
cy.get("#number-without-value").type("50").then ($input) ->
expect($input).to.have.value("50")
it "can type negative numbers", ->
cy.get('#number-without-value')
.type('-123.12')
.should('have.value', '-123.12')
describe "input[type=email]", ->
it "can change values", ->
cy.get("#email-without-value").type("brian@foo.com").then ($text) ->
@@ -748,8 +888,7 @@ describe "src/cy/commands/actions/type", ->
expect($text).to.have.value("brian@foo.com")
it "overwrites text when input has existing text selected", ->
cy.$$("#email-without-value").val("foo@bar.com").click ->
$(@).select()
cy.get("#email-without-value").invoke('val', "foo@bar.com").invoke('select')
cy.get("#email-without-value").type("bar@foo.com").then ($input) ->
expect($input).to.have.value("bar@foo.com")
@@ -768,12 +907,34 @@ describe "src/cy/commands/actions/type", ->
expect($text).to.have.value("secret")
it "overwrites text when input has existing text selected", ->
cy.$$("#password-without-value").val("secret").click ->
$(@).select()
cy.get("#password-without-value").invoke('val', "secret").invoke('select')
cy.get("#password-without-value").type("agent").then ($input) ->
expect($input).to.have.value("agent")
it "overwrites text when input has selected range of text in click handler", ->
cy.$$("#input-with-value").mouseup (e) ->
# e.preventDefault()
e.target.setSelectionRange(1, 1)
select = (e) ->
e.target.select()
cy
.$$("#password-without-value")
.val("secret")
.click(select)
.keyup (e) ->
switch e.key
when "g"
select(e)
when "n"
e.target.setSelectionRange(0, 1)
cy.get("#password-without-value").type("agent").then ($input) ->
expect($input).to.have.value("tn")
describe "input[type=date]", ->
it "can change values", ->
cy.get("#date-without-value").type("1959-09-13").then ($text) ->
@@ -855,6 +1016,51 @@ describe "src/cy/commands/actions/type", ->
cy.get("#input-types [contenteditable]").invoke("text", "foo").type(" bar").then ($text) ->
expect($text).to.have.text("foo bar")
it "can type into [contenteditable] with existing <div>", ->
cy.$$('[contenteditable]:first').get(0).innerHTML = '<div>foo</div>'
cy.get("[contenteditable]:first")
.type("bar").then ($div) ->
expect($div.get(0).innerText).to.eql("foobar\n")
expect($div.get(0).textContent).to.eql("foobar")
expect($div.get(0).innerHTML).to.eql("<div>foobar</div>")
it "can type into [contenteditable] with existing <p>", ->
cy.$$('[contenteditable]:first').get(0).innerHTML = '<p>foo</p>'
cy.get("[contenteditable]:first")
.type("bar").then ($div) ->
expect($div.get(0).innerText).to.eql("foobar\n\n")
expect($div.get(0).textContent).to.eql("foobar")
expect($div.get(0).innerHTML).to.eql("<p>foobar</p>")
it "collapses selection to start on {leftarrow}", ->
cy.$$('[contenteditable]:first').get(0).innerHTML = '<div>bar</div>'
cy.get("[contenteditable]:first")
.type("{selectall}{leftarrow}foo").then ($div) ->
expect($div.get(0).innerText).to.eql("foobar\n")
it "collapses selection to end on {rightarrow}", ->
cy.$$('[contenteditable]:first').get(0).innerHTML = '<div>bar</div>'
cy.get("[contenteditable]:first")
.type("{selectall}{leftarrow}foo{selectall}{rightarrow}baz").then ($div) ->
expect($div.get(0).innerText).to.eql("foobarbaz\n")
it "can remove a placeholder <br>", ->
cy.$$('[contenteditable]:first').get(0).innerHTML = '<div><br></div>'
cy.get("[contenteditable]:first")
.type("foobar").then ($div) ->
expect($div.get(0).innerHTML).to.eql("<div>foobar</div>")
describe.skip "element reference loss", ->
it 'follows the focus of the cursor', ->
charCount = 0
cy.$$('input:first').keydown ->
if charCount is 3
cy.$$('input').eq(1).focus()
charCount++
cy.get('input:first').type('foobar').then ->
cy.get('input:first').should('have.value', 'foo')
cy.get('input').eq(1).should('have.value', 'bar')
describe "specialChars", ->
context "{{}", ->
it "sets which and keyCode to 219", (done) ->
@@ -931,15 +1137,14 @@ describe "src/cy/commands/actions/type", ->
context "{backspace}", ->
it "backspaces character to the left", ->
cy.get(":text:first").invoke("val", "bar").type("{leftarrow}{backspace}").then ($input) ->
expect($input).to.have.value("br")
cy.get(":text:first").invoke("val", "bar").type("{leftarrow}{backspace}u").then ($input) ->
expect($input).to.have.value("bur")
it "can backspace a selection range of characters", ->
cy
.get(":text:first").invoke("val", "bar").focus().then ($input) ->
## select the 'ar' characters
b = bililiteRange($input.get(0))
b.bounds([1, 3]).select()
$input.get(0).setSelectionRange(1,3)
.get(":text:first").type("{backspace}").then ($input) ->
expect($input).to.have.value("b")
@@ -961,28 +1166,6 @@ describe "src/cy/commands/actions/type", ->
cy.get(":text:first").invoke("val", "ab").type("{backspace}").then -> done()
it "does fire input event when value changes", (done) ->
cy.$$(":text:first").on "input", (e) ->
done()
cy
.get(":text:first").invoke("val", "bar").focus().then ($input) ->
## select the 'a' characters
b = bililiteRange($input.get(0))
b.bounds([1, 2]).select()
.get(":text:first").type("{backspace}")
it "does not fire input event when value does not change", (done) ->
cy.$$(":text:first").on "input", (e) ->
done("should not have fired input")
cy
.get(":text:first").invoke("val", "bar").focus().then ($input) ->
## set the range at the beggining
b = bililiteRange($input.get(0))
b.bounds([0, 0]).select()
.get(":text:first").type("{backspace}").then -> done()
it "can prevent default backspace movement", (done) ->
cy.$$(":text:first").on "keydown", (e) ->
if e.keyCode is 8
@@ -1001,8 +1184,7 @@ describe "src/cy/commands/actions/type", ->
cy
.get(":text:first").invoke("val", "bar").focus().then ($input) ->
## select the 'ar' characters
b = bililiteRange($input.get(0))
b.bounds([1, 3]).select()
$input.get(0).setSelectionRange(1,3)
.get(":text:first").type("{del}").then ($input) ->
expect($input).to.have.value("b")
@@ -1031,8 +1213,7 @@ describe "src/cy/commands/actions/type", ->
cy
.get(":text:first").invoke("val", "bar").focus().then ($input) ->
## select the 'a' characters
b = bililiteRange($input.get(0))
b.bounds([1, 2]).select()
$input.get(0).setSelectionRange(0,1)
.get(":text:first").type("{del}")
it "does not fire input event when value does not change", (done) ->
@@ -1063,8 +1244,7 @@ describe "src/cy/commands/actions/type", ->
cy
.get(":text:first").invoke("val", "bar").focus().then ($input) ->
## select the 'a' character
b = bililiteRange($input.get(0))
b.bounds([1, 2]).select()
$input.get(0).setSelectionRange(1,2)
.get(":text:first").type("{leftarrow}n").then ($input) ->
expect($input).to.have.value("bnar")
@@ -1072,8 +1252,8 @@ describe "src/cy/commands/actions/type", ->
cy
.get(":text:first").invoke("val", "bar").focus().then ($input) ->
## select the 'a' character
b = bililiteRange($input.get(0))
b.bounds("all").select()
$input.get(0).setSelectionRange(0,1)
.get(":text:first").type("{leftarrow}n").then ($input) ->
expect($input).to.have.value("nbar")
@@ -1114,9 +1294,9 @@ describe "src/cy/commands/actions/type", ->
context "{rightarrow}", ->
it "can move the cursor from the beginning to beginning + 1", ->
cy.get(":text:first").invoke("val", "bar").focus().then ($input) ->
## select the all characters
b = bililiteRange($input.get(0))
b.bounds("start").select()
## select the beginning
$input.get(0).setSelectionRange(0,0)
.get(":text:first").type("{rightarrow}n").then ($input) ->
expect($input).to.have.value("bnar")
@@ -1128,17 +1308,16 @@ describe "src/cy/commands/actions/type", ->
cy
.get(":text:first").invoke("val", "bar").focus().then ($input) ->
## select the 'a' character
b = bililiteRange($input.get(0))
b.bounds([1, 2]).select()
$input.get(0).setSelectionRange(1,2)
.get(":text:first").type("{rightarrow}n").then ($input) ->
expect($input).to.have.value("banr")
it "sets the cursor to the very beginning", ->
cy
.get(":text:first").invoke("val", "bar").focus().then ($input) ->
## select the all characters
b = bililiteRange($input.get(0))
b.bounds("all").select()
$input.select()
.get(":text:first").type("{leftarrow}n").then ($input) ->
expect($input).to.have.value("nbar")
@@ -1205,6 +1384,45 @@ describe "src/cy/commands/actions/type", ->
cy.get("#comments").type("{uparrow}").then -> done()
it "up and down arrow on contenteditable", ->
cy.$$('[contenteditable]:first').get(0).innerHTML =
'<div>foo</div>' +
'<div>bar</div>' +
'<div>baz</div>'
cy.get("[contenteditable]:first")
.type("{leftarrow}{leftarrow}{uparrow}11{uparrow}22{downarrow}{downarrow}33").then ($div) ->
expect($div.get(0).innerText).to.eql("foo22\nb11ar\nbaz33\n")
it "uparrow ignores current selection", ->
ce = cy.$$('[contenteditable]:first').get(0)
ce.innerHTML =
'<div>foo</div>' +
'<div>bar</div>' +
'<div>baz</div>'
## select 'bar'
line = cy.$$('[contenteditable]:first div:nth-child(1)').get(0)
cy.document().then (doc) ->
ce.focus()
doc.getSelection().selectAllChildren(line)
cy.get("[contenteditable]:first")
.type("{uparrow}11").then ($div) ->
expect($div.get(0).innerText).to.eql("11foo\nbar\nbaz\n")
it "up and down arrow on textarea", ->
cy.$$('textarea:first').get(0).value = 'foo\nbar\nbaz'
cy.get("textarea:first")
.type("{leftarrow}{leftarrow}{uparrow}11{uparrow}22{downarrow}{downarrow}33").should('have.value', "foo22\nb11ar\nbaz33")
it "increments input[type=number]", ->
cy.get('input[type="number"]:first')
.invoke('val', '12.34')
.type('{uparrow}{uparrow}')
.should('have.value', '14')
context "{downarrow}", ->
beforeEach ->
cy.$$("#comments").val("foo\nbar\nbaz")
@@ -1234,6 +1452,34 @@ describe "src/cy/commands/actions/type", ->
cy.get("#comments").type("{downarrow}").then -> done()
it "{downarrow} will move to EOL on textarea", ->
cy.$$('textarea:first').get(0).value = 'foo\nbar\nbaz'
cy.get("textarea:first")
.type("{leftarrow}{leftarrow}{uparrow}11{uparrow}22{downarrow}{downarrow}33{leftarrow}{downarrow}44").should('have.value', "foo22\nb11ar\nbaz3344")
it "decrements input[type='number']", ->
cy.get('input[type="number"]:first')
.invoke('val', '12.34')
.type('{downarrow}{downarrow}')
.should('have.value', '11')
it "downarrow ignores current selection", ->
ce = cy.$$('[contenteditable]:first').get(0)
ce.innerHTML =
'<div>foo</div>' +
'<div>bar</div>' +
'<div>baz</div>'
## select 'foo'
line = cy.$$('[contenteditable]:first div:first').get(0)
cy.document().then (doc) ->
ce.focus()
doc.getSelection().selectAllChildren(line)
cy.get("[contenteditable]:first")
.type("{downarrow}22").then ($div) ->
expect($div.get(0).innerText).to.eql("foo\n22bar\nbaz\n")
context "{selectall}{del}", ->
it "can select all the text and delete", ->
cy.get(":text:first").invoke("val", "1234").type("{selectall}{del}").type("foo").then ($text) ->
@@ -1291,9 +1537,20 @@ describe "src/cy/commands/actions/type", ->
cy.get("#input-types textarea").invoke("val", "foo").type("bar{enter}baz{enter}quux").then ($textarea) ->
expect($textarea).to.have.value("foobar\nbaz\nquux")
it "inserts new line into [contenteditable]", ->
cy.get("#input-types [contenteditable]").invoke("text", "foo").type("bar{enter}baz{enter}quux").then ($div) ->
expect($div).to.have.text("foobar\nbaz\nquux")
it "inserts new line into [contenteditable] ", ->
cy.get("#input-types [contenteditable]:first").invoke("text", "foo")
.type("bar{enter}baz{enter}{enter}{enter}quux").then ($div) ->
expect($div.get(0).innerText).to.eql("foobar\nbaz\n\n\nquux\n")
expect($div.get(0).textContent).to.eql("foobarbazquux")
expect($div.get(0).innerHTML).to.eql("foobar<div>baz</div><div><br></div><div><br></div><div>quux</div>")
it "inserts new line into [contenteditable] from midline", ->
cy.get("#input-types [contenteditable]:first").invoke("text", "foo")
.type("bar{leftarrow}{enter}baz{leftarrow}{enter}quux").then ($div) ->
expect($div.get(0).innerText).to.eql("fooba\nba\nquuxzr\n")
expect($div.get(0).textContent).to.eql("foobabaquuxzr")
expect($div.get(0).innerHTML).to.eql("fooba<div>ba</div><div>quuxzr</div>")
describe "modifiers", ->
@@ -1523,7 +1780,16 @@ describe "src/cy/commands/actions/type", ->
describe "click events", ->
it "passes timeout and interval down to click", (done) ->
input = $("<input />").attr("id", "input-covered-in-span").prependTo(cy.$$("body"))
span = $("<span>span on input</span>").css(position: "absolute", left: input.offset().left, top: input.offset().top, padding: 5, display: "inline-block", backgroundColor: "yellow").prependTo(cy.$$("body"))
span = $("<span>span on input</span>")
.css {
position: "absolute"
left: input.offset().left
top: input.offset().top
padding: 5
display: "inline-block"
backgroundColor: "yellow"
}
.prependTo cy.$$("body")
cy.on "command:retry", (options) ->
expect(options.timeout).to.eq 1000
@@ -1654,8 +1920,12 @@ describe "src/cy/commands/actions/type", ->
cy.$$(":text:first").change ->
changed += 1
cy.get(":text:first").invoke("val", "foo").clear().type("o").click().then ->
cy.get(":text:first").invoke("val", "foo").clear().type("o").click().then ($el) ->
expect(changed).to.eq 0
$el
.blur()
.then ->
expect(changed).to.eq 1
it "does not fire if {enter} is preventedDefault", ->
changed = 0
@@ -1743,10 +2013,194 @@ describe "src/cy/commands/actions/type", ->
.get("button:first").click().then ->
expect(changed).to.eq 0
describe "caret position", ->
it "leaves caret at the end of the input"
it "does not fire on .clear() without blur", ->
changed = 0
cy.$$("input:first").change ->
changed += 1
cy.get("input:first").invoke('val', 'foo')
.clear()
.then ($el) ->
expect(changed).to.eq 0
$el
.type('foo')
.blur()
.then ->
expect(changed).to.eq 0
it "fires change for single value change inputs", ->
changed = 0
cy.$$('input[type="date"]:first').change ->
changed++
cy.get('input[type="date"]:first')
.type("1959-09-13")
.blur()
.then ->
expect(changed).to.eql 1
it "does not fire change for non-change single value input", ->
changed = 0
cy.$$('input[type="date"]:first').change ->
changed++
cy.get('input[type="date"]:first')
.invoke('val', "1959-09-13")
.type("1959-09-13")
.blur()
.then ->
expect(changed).to.eql(0)
it "does not fire change for type'd change that restores value", ->
changed = 0
cy.$$('input:first').change ->
changed++
cy.get('input:first')
.invoke('val', 'foo')
.type('{backspace}o')
.invoke('val', 'bar')
.type('{backspace}r')
.blur()
.then ->
expect(changed).to.eql 0
describe "caret position", ->
it "respects being formatted by input event handlers"
it "can arrow from maxlength", ->
cy.get('input:first').invoke('attr', 'maxlength', "5").type('foobar{leftarrow}')
cy.window().then (win) ->
expect $selection.getSelectionBounds Cypress.$('input:first').get(0)
.to.deep.eq({start:4, end:4})
it "won't arrowright past length", ->
cy.get('input:first').type('foo{rightarrow}{rightarrow}{rightarrow}bar{rightarrow}')
cy.window().then (win) ->
expect $selection.getSelectionBounds Cypress.$('input:first').get(0)
.to.deep.eq({start:6, end:6})
it "won't arrowleft before word", ->
cy.get('input:first').type('oo' + '{leftarrow}{leftarrow}{leftarrow}' + 'f' + '{leftarrow}'.repeat(5))
cy.window().then (win) ->
expect $selection.getSelectionBounds Cypress.$('input:first').get(0)
.to.deep.eq({start:0, end:0})
it "leaves caret at the end of contenteditable", ->
cy.get('[contenteditable]:first').type('foobar')
cy.window().then (win) ->
expect $selection.getSelectionBounds Cypress.$('[contenteditable]:first').get(0)
.to.deep.eq({start:6, end:6})
it "leaves caret at the end of contenteditable when prefilled", ->
$el = cy.$$('[contenteditable]:first')
el = $el.get(0)
el.innerHTML = 'foo'
cy.get('[contenteditable]:first').type('bar')
cy.window().then (win) ->
expect $selection.getSelectionBounds Cypress.$('[contenteditable]:first').get(0)
.to.deep.eq({start:6, end:6})
it "can move the caret left on contenteditable", ->
cy.get('[contenteditable]:first').type('foo{leftarrow}{leftarrow}')
cy.window().then (win) ->
expect $selection.getSelectionBounds Cypress.$('[contenteditable]:first').get(0)
.to.deep.eq({start:1, end:1})
##make sure caret is correct
## type left left
## make sure caret correct
## text is fboo
## fix input-mask issue
it "leaves caret at the end of input", ->
cy.get(':text:first').type('foobar')
cy.window().then (win) ->
expect $selection.getSelectionBounds Cypress.$(':text:first').get(0)
.to.deep.eq({start:6, end:6})
it "leaves caret at the end of textarea", ->
cy.get('#comments').type('foobar')
cy.window().then (win) ->
expect $selection.getSelectionBounds Cypress.$('#comments').get(0)
.to.deep.eq({start:6, end:6})
it "can wrap cursor to next line in [contenteditable] with {rightarrow}", ->
$el = cy.$$('[contenteditable]:first')
el = $el.get(0)
el.innerHTML = 'start'+
'<div>middle</div>'+
'<div>end</div>'
cy.get('[contenteditable]:first')
## move cursor to beginning of div
.type('{selectall}{leftarrow}')
.type('{rightarrow}'.repeat(14)+'[_I_]').then ->
expect(cy.$$('[contenteditable]:first').get(0).innerText).to.eql('start\nmiddle\ne[_I_]nd\n')
it "can wrap cursor to prev line in [contenteditable] with {leftarrow}", ->
$el = cy.$$('[contenteditable]:first')
el = $el.get(0)
el.innerHTML = 'start'+
'<div>middle</div>'+
'<div>end</div>'
cy.get('[contenteditable]:first').type('{leftarrow}'.repeat(12)+'[_I_]').then ->
expect(cy.$$('[contenteditable]:first').get(0).innerText).to.eql('star[_I_]t\nmiddle\nend\n')
it "can wrap cursor to next line in [contenteditable] with {rightarrow} and empty lines", ->
$el = cy.$$('[contenteditable]:first')
el = $el.get(0)
el.innerHTML = '<div><br></div>'.repeat(4)+
'<div>end</div>'
cy.get('[contenteditable]:first')
.type('{selectall}{leftarrow}')
# .type('foobar'+'{rightarrow}'.repeat(6)+'[_I_]').then ->
# expect(cy.$$('[contenteditable]:first').get(0).innerText).to.eql('foobar\n\n\n\nen[_I_]d\n')
it "can use {rightarrow} and nested elements", ->
$el = cy.$$('[contenteditable]:first')
el = $el.get(0)
el.innerHTML = '<div><b>s</b>ta<b>rt</b></div>'
cy.get('[contenteditable]:first')
.type('{selectall}{leftarrow}')
.type('{rightarrow}'.repeat(3)+'[_I_]').then ->
expect(cy.$$('[contenteditable]:first').get(0).innerText).to.eql('sta[_I_]rt\n')
it "enter and \\n should act the same for [contenteditable]", ->
cleanseText = (text) ->
text.replace(/ /g, ' ')
expectMatchInnerText = ($el , innerText) ->
expect(cleanseText($el.get(0).innerText)).to.eql(innerText)
## NOTE: this may only pass in Chrome since the whitespace may be different in other browsers
## even if actual and expected appear the same.
expected = "{\n foo: 1\n bar: 2\n baz: 3\n}\n"
cy.get('[contenteditable]:first')
.invoke('html', '<div><br></div>')
.type('{{}{enter} foo: 1{enter} bar: 2{enter} baz: 3{enter}}')
.should ($el) ->
expectMatchInnerText($el, expected)
.clear()
.type('{{}\n foo: 1\n bar: 2\n baz: 3\n}')
.should ($el) ->
expectMatchInnerText($el, expected)
it "enter and \\n should act the same for textarea", ->
expected = "{\n foo: 1\n bar: 2\n baz: 3\n}"
cy.get('textarea:first')
.clear()
.type('{{}{enter} foo: 1{enter} bar: 2{enter} baz: 3{enter}}')
.should('have.prop', 'value', expected)
.clear()
.type('{{}\n foo: 1\n bar: 2\n baz: 3\n}')
.should('have.prop', 'value', expected)
it "always types at the end of the input"
describe "{enter}", ->
beforeEach ->
@@ -2097,7 +2551,7 @@ describe "src/cy/commands/actions/type", ->
"typed", "which", "keydown", "keypress", "textInput", "input", "keyup", "change", "modifiers"
]
expect(table.name).to.eq "Key Events Table"
expect(table.data).to.deep.eq {
expectedTable = {
1: {typed: "<meta>", which: 91, keydown: true, modifiers: "meta"}
2: {typed: "<alt>", which: 18, keydown: true, modifiers: "alt, meta"}
3: {typed: "f", which: 70, keydown: true, keypress: true, textInput: true, input: true, keyup: true, modifiers: "alt, meta"}
@@ -2107,9 +2561,17 @@ describe "src/cy/commands/actions/type", ->
7: {typed: "b", which: 66, keydown: true, keypress: true, textInput: true, input: true, keyup: true, modifiers: "alt, meta"}
8: {typed: "{leftarrow}", which: 37, keydown: true, keyup: true, modifiers: "alt, meta"}
9: {typed: "{del}", which: 46, keydown: true, input: true, keyup: true, modifiers: "alt, meta"}
10: {typed: "{enter}", which: 13, keydown: true, keypress: true, keyup: true, change: true, modifiers: "alt, meta"}
10: {typed: "{enter}", which: 13, keydown: true, keypress: true, keyup: true, modifiers: "alt, meta"}
}
for i in [1..10]
expect(table.data[i]).to.deep.eq(expectedTable[i])
# table.data.forEach (item, i) ->
# expect(item).to.deep.eq(expectedTable[i])
# expect(table.data).to.deep.eq(expectedTable)
it "has no modifiers when there are none activated", ->
cy.get(":text:first").type("f").then ->
table = @lastLog.invoke("consoleProps").table()
@@ -2373,6 +2835,19 @@ describe "src/cy/commands/actions/type", ->
cy.get("#month-without-value").type("1989-13")
context "[type=tel]", ->
it "can edit tel", ->
cy.get('input[type="tel"]')
.type('1234567890')
.should('have.prop', 'value', '1234567890')
# it "throws when chars is invalid format", (done) ->
# cy.on "fail", (err) =>
# expect(@logs.length).to.eq(2)
# expect(err.message).to.eq("Typing into a week input with cy.type() requires a valid week with the format 'yyyy-Www', where W is the literal character 'W' and ww is the week number (00-53). You passed: 2005/W18")
# done()
context "[type=week]", ->
it "throws when chars is not a string", (done) ->
cy.on "fail", (err) =>

View File

@@ -0,0 +1,97 @@
$ = Cypress.$.bind(Cypress)
changed = 0
describe "src/cy/commands/actions/type text_mask_spec", ->
before ->
cy.visit("/fixtures/text-mask.html")
## count the number of change events
cy.get('input').then ($els) ->
$els.change ($el) ->
changed++
beforeEach ->
## reset number of change events before test
changed = 0
context "#type", ->
it "can type into phone", ->
cy.get("#phone")
.type('7701234567')
.should('have.value', "(770) 123-4567")
.blur()
.then ->
expect(changed).to.eql 1
it "backspace works properly", ->
cy.get('#phone')
.clear()
.type('7{backspace}7709{backspace}123')
.should('have.value', "(770) 123-____")
.blur()
.then ->
expect(changed).to.eql 1
it "can accept bad key and arrowkeys in phone", ->
cy.get('#phone')
.clear()
.type('{rightarrow}777q{leftarrow}{leftarrow}{leftarrow}01{leftarrow}{rightarrow}26{leftarrow}{leftarrow}345')
.should('have.value', "(770) 123-4567")
.blur()
.then ->
expect(changed).to.eql 1
it "can type into date", ->
cy.get('#date')
.type('10282011')
.should('have.value', '10/28/2011')
.blur()
.then ->
expect(changed).to.eql 1
it "can type into dollar", ->
cy.get('#dollar')
.type('50000')
.should('have.value', "50,000 $")
.blur()
.then ->
expect(changed).to.eql 1
it "can type decimal into dollar", ->
cy.get('#dollar')
.clear()
.type('50.1234')
.should('have.value', "50.12 $")
.blur()
.then ->
expect(changed).to.eql 1
it "can accept bad key and arrowkeys in dollar", ->
cy.get('#dollar')
.clear()
.type('50q{leftarrow}5{leftarrow}{rightarrow}00{rightarrow}{rightarrow}{backspace}{backspace}1{enter}')
.should('have.value', "55,001 $")
.then ->
expect(changed).to.eql 1
it "can type into credit card", ->
cy.get('#card')
.clear()
.type('1214q{leftarrow}{leftarrow}2343{rightarrow}56567878{leftarrow}{del}8')
.should('have.value', "1212 3434 5656 7878")
.blur()
.then ->
expect(changed).to.eql 1
it "can backspace in card", ->
cy.get('#card')
.clear()
.type('1111222233334444'+'{backspace}'.repeat(8))
.should('have.value', "1111 2222 ____ ____")
.clear()
.type('1111222233334444{selectall}5555555555555555{backspace}')
.should('have.value', "5555 5555 5555 555_")
.blur()
.then ->
expect(changed).to.eql 1

View File

@@ -1,802 +0,0 @@
/* eslint-disable */
// Cross-broswer implementation of text ranges and selections
// documentation: http://bililite.com/blog/2011/01/17/cross-browser-text-ranges-and-selections/
// Version: 2.6
// Copyright (c) 2013 Daniel Wachsstock
// MIT license:
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
(function(){
// a bit of weirdness with IE11: using 'focus' is flaky, even if I'm not bubbling, as far as I can tell.
var focusEvent = 'onfocusin' in document.createElement('input') ? 'focusin' : 'focus';
// https://github.com/cypress-io/cypress/issues/647
var nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set
var nativeTextareaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set
function setValue (el, val) {
if (el.tagName.toLowerCase() === "input") {
return nativeInputValueSetter.call(el, val)
}
return nativeTextareaValueSetter.call(el, val)
}
// IE11 normalize is buggy (http://connect.microsoft.com/IE/feedback/details/809424/node-normalize-removes-text-if-dashes-are-present)
var n = document.createElement('div');
n.appendChild(document.createTextNode('x-'));
n.appendChild(document.createTextNode('x'));
n.normalize();
var canNormalize = n.firstChild.length == 3;
var bililiteRange = function bililiteRange (el, debug){
var ret;
if (debug){
ret = new NothingRange(); // Easier to force it to use the no-selection type than to try to find an old browser
}else if (window.getSelection && el.setSelectionRange){
// Standards. Element is an input or textarea
// note that some input elements do not allow selections
try{
el.selectionStart; // even getting the selection in such an element will throw
ret = new InputRange();
}catch(e){
ret = new NothingRange();
}
}else if (window.getSelection){
// Standards, with any other kind of element
ret = new W3CRange();
}else if (document.selection){
// Internet Explorer
ret = new IERange();
}else{
// doesn't support selection
ret = new NothingRange();
}
ret._el = el;
// determine parent document, as implemented by John McLear <john@mclear.co.uk>
ret._doc = el.ownerDocument;
ret._win = 'defaultView' in ret._doc ? ret._doc.defaultView : ret._doc.parentWindow;
ret._textProp = textProp(el);
ret._bounds = [0, ret.length()];
// There's no way to detect whether a focus event happened as a result of a click (which should change the selection)
// or as a result of a keyboard event (a tab in) or a script action (el.focus()). So we track it globally, which is a hack, and is likely to fail
// in edge cases (right-clicks, drag-n-drop), and is vulnerable to a lower-down handler preventing bubbling.
// I just don't know a better way.
// I'll hack my event-listening code below, rather than create an entire new bilililiteRange, potentially before the DOM has loaded
// if (!('bililiteRangeMouseDown' in ret._doc)){
// var _doc = {_el: ret._doc};
// ret._doc.bililiteRangeMouseDown = false;
// bililiteRange.fn.listen.call(_doc, 'mousedown', function() {
// ret._doc.bililiteRangeMouseDown = true;
// });
// bililiteRange.fn.listen.call(_doc, 'mouseup', function() {
// ret._doc.bililiteRangeMouseDown = false;
// });
// }
// note that bililiteRangeSelection is an array, which means that copying it only copies the address, which points to the original.
// make sure that we never let it (always do return [bililiteRangeSelection[0], bililiteRangeSelection[1]]), which means never returning
// this._bounds directly
if (!('bililiteRangeSelection' in el)){
// start tracking the selection
function trackSelection(evt){
if (evt && evt.which == 9){
// do tabs my way, by restoring the selection
// there's a flash of the browser's selection, but I don't see a way of avoiding that
ret._nativeSelect(ret._nativeRange(el.bililiteRangeSelection));
}else{
el.bililiteRangeSelection = ret._nativeSelection();
}
}
trackSelection();
// // only IE does this right and allows us to grab the selection before blurring
// if ('onbeforedeactivate' in el){
// ret.listen('beforedeactivate', trackSelection);
// }else{
// // with standards-based browsers, have to listen for every user interaction
// ret.listen('mouseup', trackSelection).listen('keyup', trackSelection);
// }
// ret.listen(focusEvent, function(){
// // restore the correct selection when the element comes into focus (mouse clicks change the position of the selection)
// // Note that Firefox will not fire the focus event until the window/tab is active even if el.focus() is called
// // https://bugzilla.mozilla.org/show_bug.cgi?id=566671
// if (!ret._doc.bililiteRangeMouseDown){
// ret._nativeSelect(ret._nativeRange(el.bililiteRangeSelection));
// }
// });
// }
// if (!('oninput' in el)){
// // give IE8 a chance. Note that this still fails in IE11, which has has oninput on contenteditable elements but does not
// // dispatch input events. See http://connect.microsoft.com/IE/feedback/details/794285/ie10-11-input-event-does-not-fire-on-div-with-contenteditable-set
// // TODO: revisit this when I have IE11 running on my development machine
// var inputhack = function() {ret.dispatch({type: 'input'}) };
// ret.listen('keyup', inputhack);
// ret.listen('cut', inputhack);
// ret.listen('paste', inputhack);
// ret.listen('drop', inputhack);
// el.oninput = 'patched';
}
return ret;
}
function textProp(el){
// returns the property that contains the text of the element
// note that for <body> elements the text attribute represents the obsolete text color, not the textContent.
// we document that these routines do not work for <body> elements so that should not be relevant
if (typeof el.value != 'undefined') return 'value';
if (typeof el.text != 'undefined') return 'text';
if (typeof el.textContent != 'undefined') return 'textContent';
return 'innerText';
}
// base class
function Range(){}
Range.prototype = {
length: function() {
return this._el[this._textProp].replace(/\r/g, '').length; // need to correct for IE's CrLf weirdness
},
bounds: function(s){
if (bililiteRange.bounds[s]){
this._bounds = bililiteRange.bounds[s].apply(this);
}else if (s){
this._bounds = s; // don't do error checking now; things may change at a moment's notice
}else{
var b = [
Math.max(0, Math.min (this.length(), this._bounds[0])),
Math.max(0, Math.min (this.length(), this._bounds[1]))
];
b[1] = Math.max(b[0], b[1]);
return b; // need to constrain it to fit
}
return this; // allow for chaining
},
select: function(){
var b = this._el.bililiteRangeSelection = this.bounds();
if (this._el === this._doc.activeElement){
// only actually select if this element is active!
this._nativeSelect(this._nativeRange(b));
}
this.dispatch({type: 'select'});
return this; // allow for chaining
},
text: function(text, select){
if (arguments.length){
var bounds = this.bounds(), el = this._el;
// signal the input per DOM 3 input events, http://www.w3.org/TR/DOM-Level-3-Events/#h4_events-inputevents
// we add another field, bounds, which are the bounds of the original text before being changed.
this.dispatch({type: 'beforeinput', data: text, bounds: bounds});
this._nativeSetText(text, this._nativeRange(bounds));
if (select == 'start'){
this.bounds ([bounds[0], bounds[0]]);
}else if (select == 'end'){
this.bounds ([bounds[0]+text.length, bounds[0]+text.length]);
}else if (select == 'all'){
this.bounds ([bounds[0], bounds[0]+text.length]);
}
this.dispatch({type: 'input', data: text, bounds: bounds});
return this; // allow for chaining
}else{
return this._nativeGetText(this._nativeRange(this.bounds())).replace(/\r/g, ''); // need to correct for IE's CrLf weirdness
}
},
insertEOL: function (){
this._nativeEOL();
this._bounds = [this._bounds[0]+1, this._bounds[0]+1]; // move past the EOL marker
return this;
},
// sendkeys: function (text){
// var self = this;
// this.data().sendkeysOriginalText = this.text();
// this.data().sendkeysBounds = undefined;
// function simplechar (rng, c){
// if (/^{[^}]*}$/.test(c)) c = c.slice(1,-1); // deal with unknown {key}s
// for (var i =0; i < c.length; ++i){
// var x = c.charCodeAt(i);
// rng.dispatch({type: 'keypress', keyCode: x, which: x, charCode: x});
// }
// rng.text(c, 'end');
// }
// text.replace(/{[^}]*}|[^{]+|{/g, function(part){
// (bililiteRange.sendkeys[part] || simplechar)(self, part, simplechar);
// });
// this.bounds(this.data().sendkeysBounds);
// this.dispatch({type: 'sendkeys', which: text});
// return this;
// },
top: function(){
return this._nativeTop(this._nativeRange(this.bounds()));
},
scrollIntoView: function(scroller){
var top = this.top();
// scroll into position if necessary
if (this._el.scrollTop > top || this._el.scrollTop+this._el.clientHeight < top){
if (scroller){
scroller.call(this._el, top);
}else{
this._el.scrollTop = top;
}
}
return this;
},
wrap: function (n){
this._nativeWrap(n, this._nativeRange(this.bounds()));
return this;
},
selection: function(text){
if (arguments.length){
return this.bounds('selection').text(text, 'end').select();
}else{
return this.bounds('selection').text();
}
},
clone: function(){
return bililiteRange(this._el).bounds(this.bounds());
},
all: function(text){
if (arguments.length){
this.dispatch ({type: 'beforeinput', data: text});
this._el[this._textProp] = text;
this.dispatch ({type: 'input', data: text});
return this;
}else{
return this._el[this._textProp].replace(/\r/g, ''); // need to correct for IE's CrLf weirdness
}
},
element: function() { return this._el },
// includes a quickie polyfill for CustomEvent for IE that isn't perfect but works for me
// IE10 allows custom events but not "new CustomEvent"; have to do it the old-fashioned way
dispatch: function(opts){return this;},
// dispatch: function(opts){
// opts = opts || {};
// var event = document.createEvent ? document.createEvent('CustomEvent') : this._doc.createEventObject();
// event.initCustomEvent && event.initCustomEvent(opts.type, !!opts.bubbles, !!opts.cancelable, opts.detail);
// for (var key in opts) event[key] = opts[key];
// // dispatch event asynchronously (in the sense of on the next turn of the event loop; still should be fired in order of dispatch
// var el = this._el;
// setTimeout(function(){
// try {
// el.dispatchEvent ? el.dispatchEvent(event) : el.fireEvent("on" + opts.type, document.createEventObject());
// }catch(e){
// // IE8 will not let me fire custom events at all. Call them directly
// var listeners = el['listen'+opts.type];
// if (listeners) for (var i = 0; i < listeners.length; ++i){
// listeners[i].call(el, event);
// }
// }
// }, 0);
// return this;
// },
// listen: function (type, func){
// var el = this._el;
// if (el.addEventListener){
// el.addEventListener(type, func);
// }else{
// el.attachEvent("on" + type, func);
// // IE8 can't even handle custom events created with createEventObject (though it permits attachEvent), so we have to make our own
// var listeners = el['listen'+type] = el['listen'+type] || [];
// listeners.push(func);
// }
// return this;
// },
// dontlisten: function (type, func){
// var el = this._el;
// if (el.removeEventListener){
// el.removeEventListener(type, func);
// }else try{
// el.detachEvent("on" + type, func);
// }catch(e){
// var listeners = el['listen'+type];
// if (listeners) for (var i = 0; i < listeners.length; ++i){
// if (listeners[i] === func) listeners[i] = function(){}; // replace with a noop
// }
// }
// return this;
// }
};
// allow extensions ala jQuery
bililiteRange.fn = Range.prototype; // to allow monkey patching
bililiteRange.extend = function(fns){
for (fn in fns) Range.prototype[fn] = fns[fn];
};
//bounds functions
bililiteRange.bounds = {
all: function() { return [0, this.length()] },
start: function () { return [0,0] },
end: function () { return [this.length(), this.length()] },
selection: function(){
if (this._el === this._doc.activeElement){
this.bounds ('all'); // first select the whole thing for constraining
return this._nativeSelection();
}else{
return this._el.bililiteRangeSelection;
}
}
};
// sendkeys functions
bililiteRange.sendkeys = {
'{enter}': function (rng){
simplechar(rng, '\n');
rng.insertEOL();
},
'{tab}': function (rng, c, simplechar){
simplechar(rng, '\t'); // useful for inserting what would be whitespace
},
'{newline}': function (rng, c, simplechar){
simplechar(rng, '\n'); // useful for inserting what would be whitespace (and if I don't want to use insertEOL, which does some fancy things)
},
'{backspace}': function (rng){
var b = rng.bounds();
if (b[0] == b[1]) rng.bounds([b[0]-1, b[0]]); // no characters selected; it's just an insertion point. Remove the previous character
rng.text('', 'end'); // delete the characters and update the selection
},
'{del}': function (rng){
var b = rng.bounds();
if (b[0] == b[1]) rng.bounds([b[0], b[0]+1]); // no characters selected; it's just an insertion point. Remove the next character
rng.text('', 'end'); // delete the characters and update the selection
},
'{rightarrow}': function (rng){
var b = rng.bounds();
if (b[0] == b[1]) ++b[1]; // no characters selected; it's just an insertion point. Move to the right
rng.bounds([b[1], b[1]]);
},
'{leftarrow}': function (rng){
var b = rng.bounds();
if (b[0] == b[1]) --b[0]; // no characters selected; it's just an insertion point. Move to the left
rng.bounds([b[0], b[0]]);
},
'{selectall}' : function (rng){
rng.bounds('all');
},
'{selection}': function (rng){
// insert the characters without the sendkeys processing
var s = rng.data().sendkeysOriginalText;
for (var i =0; i < s.length; ++i){
var x = s.charCodeAt(i);
rng.dispatch({type: 'keypress', keyCode: x, which: x, charCode: x});
}
rng.text(s, 'end');
},
'{mark}' : function (rng){
rng.data().sendkeysBounds = rng.bounds();
}
};
// Synonyms from the proposed DOM standard (http://www.w3.org/TR/DOM-Level-3-Events-key/)
bililiteRange.sendkeys['{Enter}'] = bililiteRange.sendkeys['{enter}'];
bililiteRange.sendkeys['{Backspace}'] = bililiteRange.sendkeys['{backspace}'];
bililiteRange.sendkeys['{Delete}'] = bililiteRange.sendkeys['{del}'];
bililiteRange.sendkeys['{ArrowRight}'] = bililiteRange.sendkeys['{rightarrow}'];
bililiteRange.sendkeys['{ArrowLeft}'] = bililiteRange.sendkeys['{leftarrow}'];
function IERange(){}
IERange.prototype = new Range();
IERange.prototype._nativeRange = function (bounds){
var rng;
if (this._el.tagName == 'INPUT'){
// IE 8 is very inconsistent; textareas have createTextRange but it doesn't work
rng = this._el.createTextRange();
}else{
rng = this._doc.body.createTextRange ();
rng.moveToElementText(this._el);
}
if (bounds){
if (bounds[1] < 0) bounds[1] = 0; // IE tends to run elements out of bounds
if (bounds[0] > this.length()) bounds[0] = this.length();
if (bounds[1] < rng.text.replace(/\r/g, '').length){ // correct for IE's CrLf weirdness
// block-display elements have an invisible, uncounted end of element marker, so we move an extra one and use the current length of the range
rng.moveEnd ('character', -1);
rng.moveEnd ('character', bounds[1]-rng.text.replace(/\r/g, '').length);
}
if (bounds[0] > 0) rng.moveStart('character', bounds[0]);
}
return rng;
};
IERange.prototype._nativeSelect = function (rng){
rng.select();
};
IERange.prototype._nativeSelection = function (){
// returns [start, end] for the selection constrained to be in element
var rng = this._nativeRange(); // range of the element to constrain to
var len = this.length();
var sel = this._doc.selection.createRange();
try{
return [
iestart(sel, rng),
ieend (sel, rng)
];
}catch (e){
// TODO: determine if this is still necessary, since we only call _nativeSelection if _el is active
// IE gets upset sometimes about comparing text to input elements, but the selections cannot overlap, so make a best guess
return (sel.parentElement().sourceIndex < this._el.sourceIndex) ? [0,0] : [len, len];
}
};
IERange.prototype._nativeGetText = function (rng){
return rng.text;
};
IERange.prototype._nativeSetText = function (text, rng){
rng.text = text;
};
IERange.prototype._nativeEOL = function(){
if ('value' in this._el){
this.text('\n'); // for input and textarea, insert it straight
}else{
this._nativeRange(this.bounds()).pasteHTML('\n<br/>');
}
};
IERange.prototype._nativeTop = function(rng){
var startrng = this._nativeRange([0,0]);
return rng.boundingTop - startrng.boundingTop;
}
IERange.prototype._nativeWrap = function(n, rng) {
// hacky to use string manipulation but I don't see another way to do it.
var div = document.createElement('div');
div.appendChild(n);
// insert the existing range HTML after the first tag
var html = div.innerHTML.replace('><', '>'+rng.htmlText+'<');
rng.pasteHTML(html);
};
// IE internals
function iestart(rng, constraint){
// returns the position (in character) of the start of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after
var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf weirdness
if (rng.compareEndPoints ('StartToStart', constraint) <= 0) return 0; // at or before the beginning
if (rng.compareEndPoints ('StartToEnd', constraint) >= 0) return len;
for (var i = 0; rng.compareEndPoints ('StartToStart', constraint) > 0; ++i, rng.moveStart('character', -1));
return i;
}
function ieend (rng, constraint){
// returns the position (in character) of the end of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after
var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf weirdness
if (rng.compareEndPoints ('EndToEnd', constraint) >= 0) return len; // at or after the end
if (rng.compareEndPoints ('EndToStart', constraint) <= 0) return 0;
for (var i = 0; rng.compareEndPoints ('EndToStart', constraint) > 0; ++i, rng.moveEnd('character', -1));
return i;
}
// an input element in a standards document. "Native Range" is just the bounds array
function InputRange(){}
InputRange.prototype = new Range();
InputRange.prototype._nativeRange = function(bounds) {
return bounds || [0, this.length()];
};
InputRange.prototype._nativeSelect = function (rng){
try {
this._el.setSelectionRange(rng[0], rng[1]);
} catch (e) {
if (typeof this._el.select === 'function') {
this._el.select()
} else {
console.error('Failed to select text on', this._el)
}
}
};
InputRange.prototype._nativeSelection = function(){
var originalType = this._el.type
//// HACK:
//// newer versions of Chrome incorrectly report the selection
//// for number and email types, so we change it to type=text
//// and blur it (then set it back and focus it further down
//// after the selection algorithm has taken place)
var shouldChangeType = originalType === 'email' || originalType === 'number'
if (shouldChangeType) {
this._el.blur()
this._el.type = 'text'
}
var start = this._el.selectionStart
var end = this._el.selectionEnd
var selection = [start, end]
//// HACK:
//// selection start and end don't report correctly when input
//// already has a value set, so if there's a value and there is no
//// native selection, force it to be at the end of the text
if (this._el.value && !start && !end) {
var length = this._el.value.length
selection = [length, length]
}
if (shouldChangeType) {
this._el.type = originalType
this._el.focus()
}
return selection
};
InputRange.prototype._nativeGetText = function(rng){
return this._el.value.substring(rng[0], rng[1]);
};
InputRange.prototype._nativeSetText = function(text, rng){
var val = this._el.value;
setValue(this._el, val.substring(0, rng[0]) + text + val.substring(rng[1]));
};
InputRange.prototype._nativeEOL = function(){
this.text('\n');
};
InputRange.prototype._nativeTop = function(rng){
// I can't remember where I found this clever hack to find the location of text in a text area
var clone = this._el.cloneNode(true);
clone.style.visibility = 'hidden';
clone.style.position = 'absolute';
this._el.parentNode.insertBefore(clone, this._el);
clone.style.height = '1px';
setValue(clone, this._el.value.slice(0, rng[0]));
var top = clone.scrollHeight;
// this gives the bottom of the text, so we have to subtract the height of a single line
setValue(clone, 'X');
top -= clone.scrollHeight;
clone.parentNode.removeChild(clone);
return top;
}
InputRange.prototype._nativeWrap = function() {throw new Error("Cannot wrap in a text element")};
function W3CRange(){}
W3CRange.prototype = new Range();
W3CRange.prototype._nativeRange = function (bounds){
var rng = this._doc.createRange();
rng.selectNodeContents(this._el);
if (bounds){
w3cmoveBoundary (rng, bounds[0], true, this._el);
rng.collapse (true);
w3cmoveBoundary (rng, bounds[1]-bounds[0], false, this._el);
}
return rng;
};
W3CRange.prototype._nativeSelect = function (rng){
this._win.getSelection().removeAllRanges();
this._win.getSelection().addRange (rng);
};
W3CRange.prototype._nativeSelection = function (){
// returns [start, end] for the selection constrained to be in element
var rng = this._nativeRange(); // range of the element to constrain to
if (this._win.getSelection().rangeCount == 0) return [this.length(), this.length()]; // append to the end
var sel = this._win.getSelection().getRangeAt(0);
return [
w3cstart(sel, rng),
w3cend (sel, rng)
];
}
W3CRange.prototype._nativeGetText = function (rng){
return String.prototype.slice.apply(this._el.textContent, this.bounds());
// return rng.toString(); // this fails in IE11 since it insists on inserting \r's before \n's in Ranges. node.textContent works as expected
};
W3CRange.prototype._nativeSetText = function (text, rng){
rng.deleteContents();
rng.insertNode (this._doc.createTextNode(text));
if (canNormalize) this._el.normalize(); // merge the text with the surrounding text
};
W3CRange.prototype._nativeEOL = function(){
var rng = this._nativeRange(this.bounds());
rng.deleteContents();
var br = this._doc.createElement('br');
br.setAttribute ('_moz_dirty', ''); // for Firefox
rng.insertNode (br);
rng.insertNode (this._doc.createTextNode('\n'));
rng.collapse (false);
};
W3CRange.prototype._nativeTop = function(rng){
if (this.length == 0) return 0; // no text, no scrolling
if (rng.toString() == ''){
var textnode = this._doc.createTextNode('X');
rng.insertNode (textnode);
}
var startrng = this._nativeRange([0,1]);
var top = rng.getBoundingClientRect().top - startrng.getBoundingClientRect().top;
if (textnode) textnode.parentNode.removeChild(textnode);
return top;
}
W3CRange.prototype._nativeWrap = function(n, rng) {
rng.surroundContents(n);
};
// W3C internals
function nextnode (node, root){
// in-order traversal
// we've already visited node, so get kids then siblings
if (node.firstChild) return node.firstChild;
if (node.nextSibling) return node.nextSibling;
if (node===root) return null;
while (node.parentNode){
// get uncles
node = node.parentNode;
if (node == root) return null;
if (node.nextSibling) return node.nextSibling;
}
return null;
}
function w3cmoveBoundary (rng, n, bStart, el){
// move the boundary (bStart == true ? start : end) n characters forward, up to the end of element el. Forward only!
// if the start is moved after the end, then an exception is raised
if (n <= 0) return;
var node = rng[bStart ? 'startContainer' : 'endContainer'];
if (node.nodeType == 3){
// we may be starting somewhere into the text
n += rng[bStart ? 'startOffset' : 'endOffset'];
}
while (node){
if (node.nodeType == 3){
var length = node.nodeValue.length;
if (n <= length){
rng[bStart ? 'setStart' : 'setEnd'](node, n);
// special case: if we end next to a <br>, include that node.
if (n == length){
// skip past zero-length text nodes
for (var next = nextnode (node, el); next && next.nodeType==3 && next.nodeValue.length == 0; next = nextnode(next, el)){
rng[bStart ? 'setStartAfter' : 'setEndAfter'](next);
}
if (next && next.nodeType == 1 && next.nodeName == "BR") rng[bStart ? 'setStartAfter' : 'setEndAfter'](next);
}
return;
}else{
rng[bStart ? 'setStartAfter' : 'setEndAfter'](node); // skip past this one
n -= length; // and eat these characters
}
}
node = nextnode (node, el);
}
}
var START_TO_START = 0; // from the w3c definitions
var START_TO_END = 1;
var END_TO_END = 2;
var END_TO_START = 3;
// from the Mozilla documentation, for range.compareBoundaryPoints(how, sourceRange)
// -1, 0, or 1, indicating whether the corresponding boundary-point of range is respectively before, equal to, or after the corresponding boundary-point of sourceRange.
// * Range.END_TO_END compares the end boundary-point of sourceRange to the end boundary-point of range.
// * Range.END_TO_START compares the end boundary-point of sourceRange to the start boundary-point of range.
// * Range.START_TO_END compares the start boundary-point of sourceRange to the end boundary-point of range.
// * Range.START_TO_START compares the start boundary-point of sourceRange to the start boundary-point of range.
function w3cstart(rng, constraint){
if (rng.compareBoundaryPoints (START_TO_START, constraint) <= 0) return 0; // at or before the beginning
if (rng.compareBoundaryPoints (END_TO_START, constraint) >= 0) return constraint.toString().length;
rng = rng.cloneRange(); // don't change the original
rng.setEnd (constraint.endContainer, constraint.endOffset); // they now end at the same place
return constraint.toString().replace(/\r/g, '').length - rng.toString().replace(/\r/g, '').length;
}
function w3cend (rng, constraint){
if (rng.compareBoundaryPoints (END_TO_END, constraint) >= 0) return constraint.toString().length; // at or after the end
if (rng.compareBoundaryPoints (START_TO_END, constraint) <= 0) return 0;
rng = rng.cloneRange(); // don't change the original
rng.setStart (constraint.startContainer, constraint.startOffset); // they now start at the same place
return rng.toString().replace(/\r/g, '').length;
}
function NothingRange(){}
NothingRange.prototype = new Range();
NothingRange.prototype._nativeRange = function(bounds) {
return bounds || [0,this.length()];
};
NothingRange.prototype._nativeSelect = function (rng){ // do nothing
};
NothingRange.prototype._nativeSelection = function(){
return [0,0];
};
NothingRange.prototype._nativeGetText = function (rng){
return this._el[this._textProp].substring(rng[0], rng[1]);
};
NothingRange.prototype._nativeSetText = function (text, rng){
var val = this._el[this._textProp];
var newVal = val.substring(0, rng[0]) + text + val.substring(rng[1])
if (this._textProp === "value") {
setValue(this._el, newVal)
} else {
this._el[this._textProp] = newVal
}
};
NothingRange.prototype._nativeEOL = function(){
this.text('\n');
};
NothingRange.prototype._nativeTop = function(){
return 0;
};
NothingRange.prototype._nativeWrap = function() {throw new Error("Wrapping not implemented")};
// data for elements, similar to jQuery data, but allows for monitoring with custom events
var data = []; // to avoid attaching javascript objects to DOM elements, to avoid memory leaks
bililiteRange.fn.data = function(){
var index = this.element().bililiteRangeData;
if (index == undefined){
index = this.element().bililiteRangeData = data.length;
data[index] = new Data(this);
}
return data[index];
}
try {
Object.defineProperty({},'foo',{}); // IE8 will throw an error
var Data = function(rng) {
// we use JSON.stringify to display the data values. To make some of those non-enumerable, we have to use properties
Object.defineProperty(this, 'values', {
value: {}
});
Object.defineProperty(this, 'sourceRange', {
value: rng
});
Object.defineProperty(this, 'toJSON', {
value: function(){
var ret = {};
for (var i in Data.prototype) if (i in this.values) ret[i] = this.values[i];
return ret;
}
});
// to display all the properties (not just those changed), use JSON.stringify(state.all)
Object.defineProperty(this, 'all', {
get: function(){
var ret = {};
for (var i in Data.prototype) ret[i] = this[i];
return ret;
}
});
}
Data.prototype = {};
Object.defineProperty(Data.prototype, 'values', {
value: {}
});
Object.defineProperty(Data.prototype, 'monitored', {
value: {}
});
bililiteRange.data = function (name, newdesc){
newdesc = newdesc || {};
var desc = Object.getOwnPropertyDescriptor(Data.prototype, name) || {};
if ('enumerable' in newdesc) desc.enumerable = !!newdesc.enumerable;
if (!('enumerable' in desc)) desc.enumerable = true; // default
if ('value' in newdesc) Data.prototype.values[name] = newdesc.value;
if ('monitored' in newdesc) Data.prototype.monitored[name] = newdesc.monitored;
desc.configurable = true;
desc.get = function (){
if (name in this.values) return this.values[name];
return Data.prototype.values[name];
};
desc.set = function (value){
this.values[name] = value;
if (Data.prototype.monitored[name]) this.sourceRange.dispatch({
type: 'bililiteRangeData',
bubbles: true,
detail: {name: name, value: value}
});
}
Object.defineProperty(Data.prototype, name, desc);
}
}catch(err){
// if we can't set object property properties, just use old-fashioned properties
Data = function(rng){ this.sourceRange = rng };
Data.prototype = {};
bililiteRange.data = function(name, newdesc){
if ('value' in newdesc) Data.prototype[name] = newdesc.value;
}
}
module.exports = bililiteRange
})();