2020-05-26 18:17:24 +02:00

410 lines
14 KiB
JavaScript

"use strict"
module.exports = function() {
function isModernEvent(type) {
return type === "transitionstart" || type === "transitionend" || type === "animationstart" || type === "animationend"
}
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) this.appendChild(child.firstChild)
child.childNodes = []
}
else {
this.childNodes.push(child)
if (child.parentNode != null && child.parentNode !== this) child.parentNode.removeChild(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) this.appendChild(child)
else {
if (child.nodeType === 11) {
this.childNodes.splice.apply(this.childNodes, [refIndex, 0].concat(child.childNodes))
while (child.firstChild) {
var subchild = child.firstChild
child.removeChild(subchild)
subchild.parentNode = this
}
child.childNodes = []
}
else {
this.childNodes.splice(refIndex, 0, child)
if (child.parentNode != null && child.parentNode !== this) child.parentNode.removeChild(child)
child.parentNode = this
}
}
}
function getAttribute(name) {
if (this.attributes[name] == null) return null
return this.attributes[name].nodeValue
}
function setAttribute(name, value) {
var nodeValue = String(value)
this.attributes[name] = {
namespaceURI: null,
get nodeValue() {return nodeValue},
set nodeValue(value) {nodeValue = String(value)},
}
}
function setAttributeNS(ns, name, value) {
this.setAttribute(name, value)
this.attributes[name].namespaceURI = ns
}
function removeAttribute(name) {
delete this.attributes[name]
}
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
}
var activeElement
var $window = {
document: {
createElement: function(tag, is) {
var cssText = ""
var style = {}
Object.defineProperty(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 = rawKey.replace(/-\D/g, function(match) {return match[1].toUpperCase()})
var value = rule.slice(colonIndex + 1).trim()
if (key !== "cssText") {
style[key] = value
buf.push(rawKey + ": " + value + ";")
}
}
}
cssText = buf.join(" ")
}
}
})
var events = {}
var element = {
nodeType: 1,
nodeName: tag.toUpperCase(),
namespaceURI: "http://www.w3.org/1999/xhtml",
appendChild: appendChild,
removeChild: removeChild,
insertBefore: insertBefore,
getAttribute: getAttribute,
setAttribute: setAttribute,
setAttributeNS: setAttributeNS,
removeAttribute: removeAttribute,
parentNode: null,
childNodes: [],
attributes: {},
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
},
set textContent(value) {
this.childNodes = []
if (value !== "") this.appendChild($window.document.createTextNode(value))
},
set innerHTML(value) {
while (this.firstChild) this.removeChild(this.firstChild)
var stack = [this], depth = 0, voidElements = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"]
value.replace(/<([a-z0-9\-]+?)((?:\s+?[^=]+?=(?:"[^"]*?"|'[^']*?'|[^\s>]*))*?)(\s*\/)?>|<\/([a-z0-9\-]+?)>|([^<]+)/g, function(match, startTag, attrs, selfClosed, endTag, text) {
if (startTag) {
var element = $window.document.createElement(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)
})
stack[depth].appendChild(element)
if (!selfClosed && voidElements.indexOf(startTag.toLowerCase()) < 0) stack[++depth] = element
}
else if (endTag) {
depth--
}
else if (text) {
stack[depth].appendChild($window.document.createTextNode(text)) // FIXME handle html entities
}
})
},
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"].nodeValue : ""
},
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, callback, useCapture) {
if (events[type] == null) events[type] = [callback]
else events[type].push(callback)
},
removeEventListener: function(type, callback, useCapture) {
if (events[type] != null) {
var index = events[type].indexOf(callback)
if (index > -1) events[type].splice(index, 1)
}
},
dispatchEvent: function(e) {
if (this.nodeName === "INPUT" && this.attributes["type"] != null && this.attributes["type"].nodeValue === "checkbox" && e.type === "click") {
this.checked = !this.checked
}
e.target = this
if (events[e.type] != null) {
for (var i = 0; i < events[e.type].length; i++) {
events[e.type][i].call(this, e)
}
}
e.preventDefault = function() {
// TODO: should this do something?
}
if (typeof this["on" + e.type] === "function" && !isModernEvent(e.type)) this["on" + e.type](e)
},
onclick: null,
}
if (element.nodeName === "A") {
var href
Object.defineProperty(element, "href", {
get: function() {return this.attributes["href"] === undefined ? "" : "[FIXME implement]"},
set: function(value) {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,
})
element.value = ""
}
if (element.nodeName === "TEXTAREA") {
var value
Object.defineProperty(element, "value", {
get: function() {
return value != null ? value :
this.firstChild ? this.firstChild.nodeValue : ""
},
set: function(v) {value = v},
enumerable: true,
})
}
if (element.nodeName === "CANVAS") {
Object.defineProperty(element, "width", {
get: function() {return this.attributes["width"] ? Math.floor(parseInt(this.attributes["width"].nodeValue) || 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"].nodeValue) || 0) : 300},
set: function(value) {this.setAttribute("height", Math.floor(Number(value) || 0).toString())},
})
}
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"].nodeValue :
element.firstChild != null ? element.firstChild.nodeValue : ""
}
if (element.nodeName === "SELECT") {
var selectedValue, 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,
})
Object.defineProperty(element, "value", {
get: function() {
if (this.selectedIndex > -1) return getOptionValue(getOptions(this)[this.selectedIndex])
return ""
},
set: function(value) {
var options = getOptions(this)
var stringValue = String(value)
for (var i = 0; i < options.length; i++) {
if (getOptionValue(options[i]) === stringValue) {
selectedValue = stringValue
selectedIndex = i
return
}
}
selectedValue = stringValue
selectedIndex = -1
},
enumerable: true,
})
}
if (element.nodeName === "OPTION") {
Object.defineProperty(element, "value", {
get: function() {return getOptionValue(this)},
set: function(value) {
this.setAttribute("value", value)
},
enumerable: true,
})
Object.defineProperty(element, "selected", {
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) {
var nodeValue = String(text)
return {
nodeType: 3,
nodeName: "#text",
parentNode: null,
get nodeValue() {return nodeValue},
set nodeValue(value) {nodeValue = String(value)},
}
},
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 {
initEvent: function(type) {this.type = type},
}
},
get activeElement() {return activeElement},
},
}
$window.document.documentElement = $window.document.createElement("html")
$window.document.documentElement.appendChild($window.document.createElement("head"))
$window.document.body = $window.document.createElement("body")
$window.document.documentElement.appendChild($window.document.body)
activeElement = $window.document.body
return $window
}