* Minify stream, add stream stuff to releases again * Kill off a lot of tech debt, drop internal utilities from npm 1. Kill `module/`, internalize `bundler/`, privatize `test-utils/` We've been telling people to move elsewhere from these for a while, and it's about time we just pull the plug here and finally remove them. - We officially removed the bundler from the public API in v2.0, and that was the only one of these that was ever publicly documented. Usage should be low enough by now it shouldn't break anyone- I'm not seeing bundler bugs being reported anymore, either. - The `module/` utility was so narrow and caveat-filled that I'm not sure anyone really used it (even us core Mithril devs never really used it), and we only had it documented in the repo folder it lived in. I think only one bug was ever filed, and it's because it somehow ended up completely non-functional without any of us realizing it. - The test utilities were meant to be internal from day 1, but people started using it despite us core developers constantly telling people to look elsewhere and even the docs recommending specific alternatives without mention of our internal mocks. (Now if people would RTFM, that'd be nice...) 2. Add dedicated HTML test files to verify ospec and the promise polyfill, and ensure the promise tests are in pure ES5. These are made specially for those and should be much easier to just run now. 3. Fix the benchmark script to use the real DOM in browsers and to not require as many dependencies to create. Also, tweak them to be much more effective and precise on what's being tested. Previously, it was rendering to the HTML file itself, while now it's rendering to the `body`. This means in browsers, it's triggering layout and everything, benchmarking how well Mithril optimizes for style and layout recalcs, too. It also puts some pressure on the hyperscript parser attribute application, so that can be noticed as well. * Update dependencies
717 lines
24 KiB
JavaScript
717 lines
24 KiB
JavaScript
"use strict"
|
|
|
|
/*
|
|
Known limitations:
|
|
- the innerHTML setter and the DOMParser only support a small subset of the true HTML/XML syntax.
|
|
- `option.selected` can't be set/read when the option doesn't have a `select` parent
|
|
- `element.attributes` is just a map of attribute names => Attr objects stubs
|
|
- ...
|
|
|
|
*/
|
|
|
|
/*
|
|
options:
|
|
- spy:(f: Function) => Function
|
|
*/
|
|
|
|
module.exports = function(options) {
|
|
options = options || {}
|
|
var spy = options.spy || function(f){return f}
|
|
var spymap = []
|
|
|
|
// This way I'm not also implementing a partial `URL` polyfill. Based on the
|
|
// regexp at https://urlregex.com/, but adapted to allow relative URLs and
|
|
// care only about HTTP(S) URLs.
|
|
var urlHash = "#[?!/+=&;%@.\\w_-]*"
|
|
var urlQuery = "\\?[!/+=&;%@.\\w_-]*"
|
|
var urlPath = "/[+~%/.\\w_-]*"
|
|
var urlRelative = urlPath + "(?:" + urlQuery + ")?(?:" + urlHash + ")?"
|
|
var urlDomain = "https?://[A-Za-z0-9][A-Za-z0-9.-]+[A-Za-z0-9]"
|
|
var validURLRegex = new RegExp(
|
|
"^" + urlDomain + "(" + urlRelative + ")?$|" +
|
|
"^" + urlRelative + "$|" +
|
|
"^" + urlQuery + "(?:" + urlHash + ")?$|" +
|
|
"^" + urlHash + "$"
|
|
)
|
|
|
|
var hasOwn = ({}.hasOwnProperty)
|
|
|
|
function registerSpies(element, spies) {
|
|
if(options.spy) {
|
|
var i = spymap.indexOf(element)
|
|
if (i === -1) {
|
|
spymap.push(element, spies)
|
|
} else {
|
|
var existing = spymap[i + 1]
|
|
for (var k in spies) existing[k] = spies[k]
|
|
}
|
|
}
|
|
}
|
|
function getSpies(element) {
|
|
if (element == null || typeof element !== "object") throw new Error("Element expected")
|
|
if(options.spy) return spymap[spymap.indexOf(element) + 1]
|
|
}
|
|
|
|
function isModernEvent(type) {
|
|
return type === "transitionstart" || type === "transitionend" || type === "animationstart" || type === "animationend"
|
|
}
|
|
function dispatchEvent(e) {
|
|
var stopped = false
|
|
e.stopImmediatePropagation = function() {
|
|
e.stopPropagation()
|
|
stopped = true
|
|
}
|
|
e.currentTarget = this
|
|
if (this._events[e.type] != null) {
|
|
for (var i = 0; i < this._events[e.type].handlers.length; i++) {
|
|
var useCapture = this._events[e.type].options[i].capture
|
|
if (useCapture && e.eventPhase < 3 || !useCapture && e.eventPhase > 1) {
|
|
var handler = this._events[e.type].handlers[i]
|
|
if (typeof handler === "function") try {handler.call(this, e)} catch(e) {setTimeout(function(){throw e})}
|
|
else try {handler.handleEvent(e)} catch(e) {setTimeout(function(){throw e})}
|
|
if (stopped) return
|
|
}
|
|
}
|
|
}
|
|
// this is inaccurate. Normally the event fires in definition order, including legacy events
|
|
// this would require getters/setters for each of them though and we haven't gotten around to
|
|
// adding them since it would be at a high perf cost or would entail some heavy refactoring of
|
|
// the mocks (prototypes instead of closures).
|
|
if (e.eventPhase > 1 && typeof this["on" + e.type] === "function" && !isModernEvent(e.type)) try {this["on" + e.type](e)} catch(e) {setTimeout(function(){throw e})}
|
|
}
|
|
function appendChild(child) {
|
|
var ancestor = this
|
|
while (ancestor !== child && ancestor !== null) ancestor = ancestor.parentNode
|
|
if (ancestor === child) throw new Error("Node cannot be inserted at the specified point in the hierarchy")
|
|
|
|
if (child.nodeType == null) throw new Error("Argument is not a DOM element")
|
|
|
|
var index = this.childNodes.indexOf(child)
|
|
if (index > -1) this.childNodes.splice(index, 1)
|
|
if (child.nodeType === 11) {
|
|
while (child.firstChild != null) appendChild.call(this, child.firstChild)
|
|
child.childNodes = []
|
|
}
|
|
else {
|
|
this.childNodes.push(child)
|
|
if (child.parentNode != null && child.parentNode !== this) removeChild.call(child.parentNode, child)
|
|
child.parentNode = this
|
|
}
|
|
}
|
|
function removeChild(child) {
|
|
var index = this.childNodes.indexOf(child)
|
|
if (index > -1) {
|
|
this.childNodes.splice(index, 1)
|
|
child.parentNode = null
|
|
}
|
|
else throw new TypeError("Failed to execute 'removeChild'")
|
|
}
|
|
function insertBefore(child, reference) {
|
|
var ancestor = this
|
|
while (ancestor !== child && ancestor !== null) ancestor = ancestor.parentNode
|
|
if (ancestor === child) throw new Error("Node cannot be inserted at the specified point in the hierarchy")
|
|
|
|
if (child.nodeType == null) throw new Error("Argument is not a DOM element")
|
|
|
|
var refIndex = this.childNodes.indexOf(reference)
|
|
var index = this.childNodes.indexOf(child)
|
|
if (reference !== null && refIndex < 0) throw new TypeError("Invalid argument")
|
|
if (index > -1) this.childNodes.splice(index, 1)
|
|
if (reference === null) appendChild.call(this, child)
|
|
else {
|
|
if (index !== -1 && refIndex > index) refIndex--
|
|
if (child.nodeType === 11) {
|
|
this.childNodes.splice.apply(this.childNodes, [refIndex, 0].concat(child.childNodes))
|
|
while (child.firstChild) {
|
|
var subchild = child.firstChild
|
|
removeChild.call(child, subchild)
|
|
subchild.parentNode = this
|
|
}
|
|
child.childNodes = []
|
|
}
|
|
else {
|
|
this.childNodes.splice(refIndex, 0, child)
|
|
if (child.parentNode != null && child.parentNode !== this) removeChild.call(child.parentNode, child)
|
|
child.parentNode = this
|
|
}
|
|
}
|
|
}
|
|
function getAttribute(name) {
|
|
if (this.attributes[name] == null) return null
|
|
return this.attributes[name].value
|
|
}
|
|
function setAttribute(name, value) {
|
|
/*eslint-disable no-implicit-coercion*/
|
|
// this is the correct kind of conversion, passing a Symbol throws in browsers too.
|
|
var nodeValue = "" + value
|
|
/*eslint-enable no-implicit-coercion*/
|
|
this.attributes[name] = {
|
|
namespaceURI: hasOwn.call(this.attributes, name) ? this.attributes[name].namespaceURI : null,
|
|
get value() {return nodeValue},
|
|
set value(value) {
|
|
/*eslint-disable no-implicit-coercion*/
|
|
nodeValue = "" + value
|
|
/*eslint-enable no-implicit-coercion*/
|
|
},
|
|
get nodeValue() {return nodeValue},
|
|
set nodeValue(value) {
|
|
this.value = value
|
|
}
|
|
}
|
|
}
|
|
function setAttributeNS(ns, name, value) {
|
|
this.setAttribute(name, value)
|
|
this.attributes[name].namespaceURI = ns
|
|
}
|
|
function removeAttribute(name) {
|
|
delete this.attributes[name]
|
|
}
|
|
function hasAttribute(name) {
|
|
return name in this.attributes
|
|
}
|
|
var declListTokenizer = /;|"(?:\\.|[^"\n])*"|'(?:\\.|[^'\n])*'/g
|
|
/**
|
|
* This will split a semicolon-separated CSS declaration list into an array of
|
|
* individual declarations, ignoring semicolons in strings.
|
|
*
|
|
* Comments are also stripped.
|
|
*
|
|
* @param {string} declList
|
|
* @return {string[]}
|
|
*/
|
|
function splitDeclList(declList) {
|
|
var indices = [], res = [], match
|
|
|
|
// remove comments, preserving comments in strings.
|
|
declList = declList.replace(
|
|
/("(?:\\.|[^"\n])*"|'(?:\\.|[^'\n])*')|\/\*[\s\S]*?\*\//g,
|
|
function(m, str){
|
|
return str || ""
|
|
}
|
|
)
|
|
/*eslint-disable no-cond-assign*/
|
|
while (match = declListTokenizer.exec(declList)) {
|
|
if (match[0] === ";") indices.push(match.index)
|
|
}
|
|
/*eslint-enable no-cond-assign*/
|
|
for (var i = indices.length; i--;){
|
|
res.unshift(declList.slice(indices[i] + 1))
|
|
declList = declList.slice(0, indices[i])
|
|
}
|
|
res.unshift(declList)
|
|
return res
|
|
}
|
|
function parseMarkup(value, root, voidElements, xmlns) {
|
|
var depth = 0, stack = [root]
|
|
value.replace(/<([a-z0-9\-]+?)((?:\s+?[^=]+?=(?:"[^"]*?"|'[^']*?'|[^\s>]*))*?)(\s*\/)?>|<\/([a-z0-9\-]+?)>|([^<]+)/g, function(match, startTag, attrs, selfClosed, endTag, text) {
|
|
if (startTag) {
|
|
var element = xmlns == null ? $window.document.createElement(startTag) : $window.document.createElementNS(xmlns, startTag)
|
|
attrs.replace(/\s+?([^=]+?)=(?:"([^"]*?)"|'([^']*?)'|([^\s>]*))/g, function(match, key, doubleQuoted, singleQuoted, unquoted) {
|
|
var keyParts = key.split(":")
|
|
var name = keyParts.pop()
|
|
var ns = keyParts[0]
|
|
var value = doubleQuoted || singleQuoted || unquoted || ""
|
|
if (ns != null) element.setAttributeNS(ns, name, value)
|
|
else element.setAttribute(name, value)
|
|
})
|
|
appendChild.call(stack[depth], element)
|
|
if (!selfClosed && voidElements.indexOf(startTag.toLowerCase()) < 0) stack[++depth] = element
|
|
}
|
|
else if (endTag) {
|
|
depth--
|
|
}
|
|
else if (text) {
|
|
appendChild.call(stack[depth], $window.document.createTextNode(text)) // FIXME handle html entities
|
|
}
|
|
})
|
|
}
|
|
function DOMParser() {}
|
|
DOMParser.prototype.parseFromString = function(src, mime) {
|
|
if (mime !== "image/svg+xml") throw new Error("The DOMParser mock only supports the \"image/svg+xml\" MIME type")
|
|
var match = src.match(/^<svg xmlns="http:\/\/www\.w3\.org\/2000\/svg">(.*)<\/svg>$/)
|
|
if (!match) throw new Error("Please provide a bare SVG tag with the xmlns as only attribute")
|
|
var value = match[1]
|
|
var root = $window.document.createElementNS("http://www.w3.org/2000/svg", "svg")
|
|
parseMarkup(value, root, [], "http://www.w3.org/2000/svg")
|
|
return {documentElement: root}
|
|
}
|
|
function camelCase(string) {
|
|
return string.replace(/-\D/g, function(match) {return match[1].toUpperCase()})
|
|
}
|
|
var activeElement
|
|
var delay = 16, last = 0
|
|
var $window = {
|
|
DOMParser: DOMParser,
|
|
requestAnimationFrame: function(callback) {
|
|
var elapsed = Date.now() - last
|
|
return setTimeout(function() {
|
|
callback()
|
|
last = Date.now()
|
|
}, delay - elapsed)
|
|
},
|
|
document: {
|
|
createElement: function(tag) {
|
|
var cssText = ""
|
|
var style = {}
|
|
Object.defineProperties(style, {
|
|
cssText: {
|
|
get: function() {return cssText},
|
|
set: function (value) {
|
|
var buf = []
|
|
if (typeof value === "string") {
|
|
for (var key in style) style[key] = ""
|
|
var rules = splitDeclList(value)
|
|
for (var i = 0; i < rules.length; i++) {
|
|
var rule = rules[i]
|
|
var colonIndex = rule.indexOf(":")
|
|
if (colonIndex > -1) {
|
|
var rawKey = rule.slice(0, colonIndex).trim()
|
|
var key = camelCase(rawKey)
|
|
var value = rule.slice(colonIndex + 1).trim()
|
|
if (key !== "cssText") {
|
|
style[key] = style[rawKey] = value
|
|
buf.push(rawKey + ": " + value + ";")
|
|
}
|
|
}
|
|
}
|
|
element.setAttribute("style", cssText = buf.join(" "))
|
|
}
|
|
}
|
|
},
|
|
getPropertyValue: {value: function(key){
|
|
return style[key]
|
|
}},
|
|
removeProperty: {value: function(key){
|
|
style[key] = style[camelCase(key)] = ""
|
|
}},
|
|
setProperty: {value: function(key, value){
|
|
style[key] = style[camelCase(key)] = value
|
|
}}
|
|
})
|
|
var events = {}
|
|
var element = {
|
|
nodeType: 1,
|
|
nodeName: tag.toUpperCase(),
|
|
namespaceURI: "http://www.w3.org/1999/xhtml",
|
|
appendChild: appendChild,
|
|
removeChild: removeChild,
|
|
insertBefore: insertBefore,
|
|
hasAttribute: hasAttribute,
|
|
getAttribute: getAttribute,
|
|
setAttribute: setAttribute,
|
|
setAttributeNS: setAttributeNS,
|
|
removeAttribute: removeAttribute,
|
|
parentNode: null,
|
|
childNodes: [],
|
|
attributes: {},
|
|
contains: function(child) {
|
|
while (child != null) {
|
|
if (child === this) return true
|
|
child = child.parentNode
|
|
}
|
|
return false
|
|
},
|
|
get firstChild() {
|
|
return this.childNodes[0] || null
|
|
},
|
|
get nextSibling() {
|
|
if (this.parentNode == null) return null
|
|
var index = this.parentNode.childNodes.indexOf(this)
|
|
if (index < 0) throw new TypeError("Parent's childNodes is out of sync")
|
|
return this.parentNode.childNodes[index + 1] || null
|
|
},
|
|
// eslint-disable-next-line accessor-pairs
|
|
set textContent(value) {
|
|
this.childNodes = []
|
|
if (value !== "") appendChild.call(this, $window.document.createTextNode(value))
|
|
},
|
|
// eslint-disable-next-line accessor-pairs
|
|
set innerHTML(value) {
|
|
var voidElements = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"]
|
|
while (this.firstChild) removeChild.call(this, this.firstChild)
|
|
var match = value.match(/^<svg xmlns="http:\/\/www\.w3\.org\/2000\/svg">(.*)<\/svg>$/), root, ns
|
|
if (match) {
|
|
var value = match[1]
|
|
root = $window.document.createElementNS("http://www.w3.org/2000/svg", "svg")
|
|
ns = "http://www.w3.org/2000/svg"
|
|
appendChild.call(this, root)
|
|
} else {
|
|
root = this
|
|
}
|
|
parseMarkup(value, root, voidElements, ns)
|
|
},
|
|
get style() {
|
|
return style
|
|
},
|
|
set style(_){
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style#Setting_style
|
|
throw new Error("setting element.style is not portable")
|
|
},
|
|
get className() {
|
|
return this.attributes["class"] ? this.attributes["class"].value : ""
|
|
},
|
|
set className(value) {
|
|
if (this.namespaceURI === "http://www.w3.org/2000/svg") throw new Error("Cannot set property className of SVGElement")
|
|
else this.setAttribute("class", value)
|
|
},
|
|
focus: function() {activeElement = this},
|
|
addEventListener: function(type, handler, options) {
|
|
if (arguments.length > 2) {
|
|
if (typeof options === "object" && options != null) throw new TypeError("NYI: addEventListener options")
|
|
else if (typeof options !== "boolean") throw new TypeError("boolean expected for useCapture")
|
|
else options = {capture: options}
|
|
} else {
|
|
options = {capture: false}
|
|
}
|
|
if (events[type] == null) events[type] = {handlers: [handler], options: [options]}
|
|
else {
|
|
var found = false
|
|
for (var i = 0; i < events[type].handlers.length; i++) {
|
|
if (events[type].handlers[i] === handler && events[type].options[i].capture === options.capture) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if (!found) {
|
|
events[type].handlers.push(handler)
|
|
events[type].options.push(options)
|
|
}
|
|
}
|
|
},
|
|
removeEventListener: function(type, handler, options) {
|
|
if (arguments.length > 2) {
|
|
if (typeof options === "object" && options != null) throw new TypeError("NYI: addEventListener options")
|
|
else if (typeof options !== "boolean") throw new TypeError("boolean expected for useCapture")
|
|
else options = {capture: options}
|
|
} else {
|
|
options = {capture: false}
|
|
}
|
|
if (events[type] != null) {
|
|
for (var i = 0; i < events[type].handlers.length; i++) {
|
|
if (events[type].handlers[i] === handler && events[type].options[i].capture === options.capture) {
|
|
events[type].handlers.splice(i, 1)
|
|
events[type].options.splice(i, 1)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
dispatchEvent: function(e) {
|
|
var parents = []
|
|
if (this.parentNode != null) {
|
|
var parent = this.parentNode
|
|
do {
|
|
parents.push(parent)
|
|
parent = parent.parentNode
|
|
} while (parent != null)
|
|
}
|
|
e.target = this
|
|
var prevented = false
|
|
e.preventDefault = function() {
|
|
prevented = true
|
|
}
|
|
Object.defineProperty(e, "defaultPrevented", {
|
|
configurable: true,
|
|
get: function () { return prevented }
|
|
})
|
|
var stopped = false
|
|
e.stopPropagation = function() {
|
|
stopped = true
|
|
}
|
|
e.eventPhase = 1
|
|
try {
|
|
for (var i = parents.length - 1; 0 <= i; i--) {
|
|
dispatchEvent.call(parents[i], e)
|
|
if (stopped) {
|
|
return
|
|
}
|
|
}
|
|
e.eventPhase = 2
|
|
dispatchEvent.call(this, e)
|
|
if (stopped) {
|
|
return
|
|
}
|
|
e.eventPhase = 3
|
|
for (var i = 0; i < parents.length; i++) {
|
|
dispatchEvent.call(parents[i], e)
|
|
if (stopped) {
|
|
return
|
|
}
|
|
}
|
|
} finally {
|
|
e.eventPhase = 0
|
|
if (!prevented) {
|
|
if (this.nodeName === "INPUT" && this.attributes["type"] != null && this.attributes["type"].value === "checkbox" && e.type === "click") {
|
|
this.checked = !this.checked
|
|
}
|
|
}
|
|
}
|
|
|
|
},
|
|
onclick: null,
|
|
_events: events
|
|
}
|
|
|
|
if (element.nodeName === "A") {
|
|
Object.defineProperty(element, "href", {
|
|
get: function() {
|
|
if (this.namespaceURI === "http://www.w3.org/2000/svg") {
|
|
var val = this.hasAttribute("href") ? this.attributes.href.value : ""
|
|
return {baseVal: val, animVal: val}
|
|
} else if (this.namespaceURI === "http://www.w3.org/1999/xhtml") {
|
|
if (!this.hasAttribute("href")) return ""
|
|
// HACK: if it's valid already, there's nothing to implement.
|
|
var value = this.attributes.href.value
|
|
if (validURLRegex.test(value)) return value
|
|
}
|
|
return "[FIXME implement]"
|
|
},
|
|
set: function(value) {
|
|
// This is a readonly attribute for SVG, todo investigate MathML which may have yet another IDL
|
|
if (this.namespaceURI !== "http://www.w3.org/2000/svg") this.setAttribute("href", value)
|
|
},
|
|
enumerable: true,
|
|
})
|
|
}
|
|
|
|
if (element.nodeName === "INPUT") {
|
|
var checked
|
|
Object.defineProperty(element, "checked", {
|
|
get: function() {return checked === undefined ? this.attributes["checked"] !== undefined : checked},
|
|
set: function(value) {checked = Boolean(value)},
|
|
enumerable: true,
|
|
})
|
|
|
|
var value = ""
|
|
var valueSetter = spy(function(v) {
|
|
/*eslint-disable no-implicit-coercion*/
|
|
value = v === null ? "" : "" + v
|
|
/*eslint-enable no-implicit-coercion*/
|
|
})
|
|
Object.defineProperty(element, "value", {
|
|
get: function() {
|
|
return value
|
|
},
|
|
set: valueSetter,
|
|
enumerable: true,
|
|
})
|
|
|
|
// we currently emulate the non-ie behavior, but emulating ie may be more useful (throw when an invalid type is set)
|
|
var typeSetter = spy(function(v) {
|
|
this.setAttribute("type", v)
|
|
})
|
|
Object.defineProperty(element, "type", {
|
|
get: function() {
|
|
if (!this.hasAttribute("type")) return "text"
|
|
var type = this.getAttribute("type")
|
|
return (/^(?:radio|button|checkbox|color|date|datetime|datetime-local|email|file|hidden|month|number|password|range|research|search|submit|tel|text|url|week|image)$/)
|
|
.test(type)
|
|
? type
|
|
: "text"
|
|
},
|
|
set: typeSetter,
|
|
enumerable: true,
|
|
})
|
|
registerSpies(element, {
|
|
valueSetter: valueSetter,
|
|
typeSetter: typeSetter
|
|
})
|
|
}
|
|
|
|
|
|
if (element.nodeName === "TEXTAREA") {
|
|
var wasNeverSet = true
|
|
var value = ""
|
|
var valueSetter = spy(function(v) {
|
|
wasNeverSet = false
|
|
/*eslint-disable no-implicit-coercion*/
|
|
value = v === null ? "" : "" + v
|
|
/*eslint-enable no-implicit-coercion*/
|
|
})
|
|
Object.defineProperty(element, "value", {
|
|
get: function() {
|
|
return wasNeverSet && this.firstChild ? this.firstChild.nodeValue : value
|
|
},
|
|
set: valueSetter,
|
|
enumerable: true,
|
|
})
|
|
registerSpies(element, {
|
|
valueSetter: valueSetter
|
|
})
|
|
}
|
|
|
|
/* eslint-disable radix */
|
|
|
|
if (element.nodeName === "CANVAS") {
|
|
Object.defineProperty(element, "width", {
|
|
get: function() {return this.attributes["width"] ? Math.floor(parseInt(this.attributes["width"].value) || 0) : 300},
|
|
set: function(value) {this.setAttribute("width", Math.floor(Number(value) || 0).toString())},
|
|
})
|
|
Object.defineProperty(element, "height", {
|
|
get: function() {return this.attributes["height"] ? Math.floor(parseInt(this.attributes["height"].value) || 0) : 300},
|
|
set: function(value) {this.setAttribute("height", Math.floor(Number(value) || 0).toString())},
|
|
})
|
|
}
|
|
|
|
/* eslint-enable radix */
|
|
|
|
function getOptions(element) {
|
|
var options = []
|
|
for (var i = 0; i < element.childNodes.length; i++) {
|
|
if (element.childNodes[i].nodeName === "OPTION") options.push(element.childNodes[i])
|
|
else if (element.childNodes[i].nodeName === "OPTGROUP") options = options.concat(getOptions(element.childNodes[i]))
|
|
}
|
|
return options
|
|
}
|
|
function getOptionValue(element) {
|
|
return element.attributes["value"] != null ?
|
|
element.attributes["value"].value :
|
|
element.firstChild != null ? element.firstChild.nodeValue : ""
|
|
}
|
|
if (element.nodeName === "SELECT") {
|
|
// var selectedValue
|
|
var selectedIndex = 0
|
|
Object.defineProperty(element, "selectedIndex", {
|
|
get: function() {return getOptions(this).length > 0 ? selectedIndex : -1},
|
|
set: function(value) {
|
|
var options = getOptions(this)
|
|
if (value >= 0 && value < options.length) {
|
|
// selectedValue = getOptionValue(options[selectedIndex])
|
|
selectedIndex = value
|
|
}
|
|
else {
|
|
// selectedValue = ""
|
|
selectedIndex = -1
|
|
}
|
|
},
|
|
enumerable: true,
|
|
})
|
|
var valueSetter = spy(function(value) {
|
|
if (value === null) {
|
|
selectedIndex = -1
|
|
} else {
|
|
var options = getOptions(this)
|
|
/*eslint-disable no-implicit-coercion*/
|
|
var stringValue = "" + value
|
|
/*eslint-enable no-implicit-coercion*/
|
|
for (var i = 0; i < options.length; i++) {
|
|
if (getOptionValue(options[i]) === stringValue) {
|
|
// selectedValue = stringValue
|
|
selectedIndex = i
|
|
return
|
|
}
|
|
}
|
|
// selectedValue = stringValue
|
|
selectedIndex = -1
|
|
}
|
|
})
|
|
Object.defineProperty(element, "value", {
|
|
get: function() {
|
|
if (this.selectedIndex > -1) return getOptionValue(getOptions(this)[this.selectedIndex])
|
|
return ""
|
|
},
|
|
set: valueSetter,
|
|
enumerable: true,
|
|
})
|
|
registerSpies(element, {
|
|
valueSetter: valueSetter
|
|
})
|
|
}
|
|
if (element.nodeName === "OPTION") {
|
|
var valueSetter = spy(function(value) {
|
|
/*eslint-disable no-implicit-coercion*/
|
|
this.setAttribute("value", "" + value)
|
|
/*eslint-enable no-implicit-coercion*/
|
|
})
|
|
Object.defineProperty(element, "value", {
|
|
get: function() {return getOptionValue(this)},
|
|
set: valueSetter,
|
|
enumerable: true,
|
|
})
|
|
registerSpies(element, {
|
|
valueSetter: valueSetter
|
|
})
|
|
|
|
Object.defineProperty(element, "selected", {
|
|
// TODO? handle `selected` without a parent (works in browsers)
|
|
get: function() {
|
|
var options = getOptions(this.parentNode)
|
|
var index = options.indexOf(this)
|
|
return index === this.parentNode.selectedIndex
|
|
},
|
|
set: function(value) {
|
|
if (value) {
|
|
var options = getOptions(this.parentNode)
|
|
var index = options.indexOf(this)
|
|
if (index > -1) this.parentNode.selectedIndex = index
|
|
}
|
|
else this.parentNode.selectedIndex = 0
|
|
},
|
|
enumerable: true,
|
|
})
|
|
}
|
|
return element
|
|
},
|
|
createElementNS: function(ns, tag, is) {
|
|
var element = this.createElement(tag, is)
|
|
element.nodeName = tag
|
|
element.namespaceURI = ns
|
|
return element
|
|
},
|
|
createTextNode: function(text) {
|
|
/*eslint-disable no-implicit-coercion*/
|
|
var nodeValue = "" + text
|
|
/*eslint-enable no-implicit-coercion*/
|
|
return {
|
|
nodeType: 3,
|
|
nodeName: "#text",
|
|
parentNode: null,
|
|
get childNodes() { return [] },
|
|
get firstChild() { return null },
|
|
get nodeValue() {return nodeValue},
|
|
set nodeValue(value) {
|
|
/*eslint-disable no-implicit-coercion*/
|
|
nodeValue = "" + value
|
|
/*eslint-enable no-implicit-coercion*/
|
|
},
|
|
get nextSibling() {
|
|
if (this.parentNode == null) return null
|
|
var index = this.parentNode.childNodes.indexOf(this)
|
|
if (index < 0) throw new TypeError("Parent's childNodes is out of sync")
|
|
return this.parentNode.childNodes[index + 1] || null
|
|
},
|
|
}
|
|
},
|
|
createDocumentFragment: function() {
|
|
return {
|
|
nodeType: 11,
|
|
nodeName: "#document-fragment",
|
|
appendChild: appendChild,
|
|
insertBefore: insertBefore,
|
|
removeChild: removeChild,
|
|
parentNode: null,
|
|
childNodes: [],
|
|
get firstChild() {
|
|
return this.childNodes[0] || null
|
|
},
|
|
}
|
|
},
|
|
createEvent: function() {
|
|
return {
|
|
eventPhase: 0,
|
|
initEvent: function(type) {this.type = type}
|
|
}
|
|
},
|
|
get activeElement() {return activeElement},
|
|
},
|
|
}
|
|
$window.document.documentElement = $window.document.createElement("html")
|
|
appendChild.call($window.document.documentElement, $window.document.createElement("head"))
|
|
$window.document.body = $window.document.createElement("body")
|
|
appendChild.call($window.document.documentElement, $window.document.body)
|
|
activeElement = $window.document.body
|
|
|
|
if (options.spy) $window.__getSpies = getSpies
|
|
|
|
return $window
|
|
}
|