mirror of
https://github.com/cypress-io/cypress.git
synced 2026-02-04 22:30:00 -06:00
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:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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) ->
|
||||
|
||||
@@ -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
|
||||
|
||||
467
packages/driver/src/dom/selection.coffee
Normal file
467
packages/driver/src/dom/selection.coffee
Normal 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 = ' '
|
||||
# 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 = ' '
|
||||
# # 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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
57
packages/driver/test/cypress/fixtures/text-mask.html
Normal file
57
packages/driver/test/cypress/fixtures/text-mask.html
Normal 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>
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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
|
||||
|
||||
802
packages/driver/vendor/bililiteRange.js
vendored
802
packages/driver/vendor/bililiteRange.js
vendored
@@ -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
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user