class InvalidSelector extends Error {} class TimedOutPromise extends Error {} class MouseEventFailed extends Error {}
const EVENTS = {
FOCUS: ["blur", "focus", "focusin", "focusout"], MOUSE: ["click", "dblclick", "mousedown", "mouseenter", "mouseleave", "mousemove", "mouseover", "mouseout", "mouseup", "contextmenu"], FORM: ["submit"]
}
class Cuprite {
constructor() { this._json = JSON; // In case someone overrides it like mootools } find(method, selector, within = document) { try { let results = []; if (method == "xpath") { let xpath = document.evaluate(selector, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); for (let i = 0; i < xpath.snapshotLength; i++) { results.push(xpath.snapshotItem(i)); } } else { results = Array.from(within.querySelectorAll(selector)); } return results; } catch (error) { // DOMException.INVALID_EXPRESSION_ERR is undefined, using pure code if (error.code == DOMException.SYNTAX_ERR || error.code == 51) { throw new InvalidSelector; } else { throw error; } } } parents(node) { let nodes = []; let parent = node.parentNode; while (parent != document && parent !== null) { nodes.push(parent); parent = parent.parentNode; } return nodes; } visibleText(node) { if (this.isVisible(node)) { if (node.nodeName == "TEXTAREA") { return node.textContent; } else { if (node instanceof SVGElement) { return node.textContent; } else { return node.innerText; } } } } isVisible(node) { let mapName, style; // if node is area, check visibility of relevant image if (node.tagName === "AREA") { mapName = document.evaluate("./ancestor::map/@name", node, null, XPathResult.STRING_TYPE, null).stringValue; node = document.querySelector(`img[usemap="#${mapName}"]`); if (node == null) { return false; } } while (node) { style = window.getComputedStyle(node); if (style.display === "none" || style.visibility === "hidden" || parseFloat(style.opacity) === 0) { return false; } node = node.parentElement; } return true; } isDisabled(node) { let xpath = "parent::optgroup[@disabled] | \ ancestor::select[@disabled] | \ parent::fieldset[@disabled] | \ ancestor::*[not(self::legend) or preceding-sibling::legend][parent::fieldset[@disabled]]"; return node.disabled || document.evaluate(xpath, node, null, XPathResult.BOOLEAN_TYPE, null).booleanValue; } path(node) { let nodes = [node]; let parent = node.parentNode; while (parent !== document && parent !== null) { nodes.unshift(parent); parent = parent.parentNode; } let selectors = nodes.map(node => { let prevSiblings = []; let xpath = document.evaluate(`./preceding-sibling::${node.tagName}`, node, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); for (let i = 0; i < xpath.snapshotLength; i++) { prevSiblings.push(xpath.snapshotItem(i)); } return `${node.tagName}[${(prevSiblings.length + 1)}]`; }); return `//${selectors.join("/")}`; } set(node, value) { if (node.readOnly) return; if (node.maxLength >= 0) { value = value.substr(0, node.maxLength); } let valueBefore = node.value; this.trigger(node, "focus"); this.setValue(node, ""); if (node.type == "number" || node.type == "date" || node.type == "range") { this.setValue(node, value); this.input(node); } else if (node.type == "time") { this.setValue(node, new Date(value).toTimeString().split(" ")[0]); this.input(node); } else if (node.type == "datetime-local") { value = new Date(value); let year = value.getFullYear(); let month = ("0" + (value.getMonth() + 1)).slice(-2); let date = ("0" + value.getDate()).slice(-2); let hour = ("0" + value.getHours()).slice(-2); let min = ("0" + value.getMinutes()).slice(-2); let sec = ("0" + value.getSeconds()).slice(-2); this.setValue(node, `${year}-${month}-${date}T${hour}:${min}:${sec}`); this.input(node); } else { for (let i = 0; i < value.length; i++) { let char = value[i]; let keyCode = this.characterToKeyCode(char); // call the following functions in order, if one returns false (preventDefault), // stop the call chain [ () => this.keyupdowned(node, "keydown", keyCode), () => this.keypressed(node, false, false, false, false, char.charCodeAt(0), char.charCodeAt(0)), () => { this.setValue(node, node.value + char) this.input(node) } ].some(fn => fn()) this.keyupdowned(node, "keyup", keyCode); } } if (valueBefore !== node.value) { this.changed(node); } this.trigger(node, "blur"); } setValue(node, value) { let nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set; let nativeTextareaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set; if (node.tagName.toLowerCase() === 'input') { return nativeInputValueSetter.call(node, value); } return nativeTextareaValueSetter.call(node, value); } input(node) { let event = document.createEvent("HTMLEvents"); event.initEvent("input", true, false); node.dispatchEvent(event); } /** * @return {boolean} false when an event handler called preventDefault() */ keyupdowned(node, eventName, keyCode) { let event = document.createEvent("UIEvents"); event.initEvent(eventName, true, true); event.keyCode = keyCode; event.charCode = 0; return !node.dispatchEvent(event); } /** * @return {boolean} false when an event handler called preventDefault() */ keypressed(node, altKey, ctrlKey, shiftKey, metaKey, keyCode, charCode) { event = document.createEvent("UIEvents"); event.initEvent("keypress", true, true); event.window = window; event.altKey = altKey; event.ctrlKey = ctrlKey; event.shiftKey = shiftKey; event.metaKey = metaKey; event.keyCode = keyCode; event.charCode = charCode; return !node.dispatchEvent(event); } characterToKeyCode(char) { const specialKeys = { 96: 192, // ` 45: 189, // - 61: 187, // = 91: 219, // [ 93: 221, // ] 92: 220, // \ 59: 186, // ; 39: 222, // ' 44: 188, // , 46: 190, // . 47: 191, // / 127: 46, // delete 126: 192, // ~ 33: 49, // ! 64: 50, // @ 35: 51, // # 36: 52, // $ 37: 53, // % 94: 54, // ^ 38: 55, // & 42: 56, // * 40: 57, // ( 41: 48, // ) 95: 189, // _ 43: 187, // + 123: 219, // { 125: 221, // } 124: 220, // | 58: 186, // : 34: 222, // " 60: 188, // < 62: 190, // > 63: 191, // ? } let code = char.toUpperCase().charCodeAt(0); return specialKeys[code] || code; } scrollIntoViewport(node) { let areaImage = this._getAreaImage(node); if (areaImage) { return this.scrollIntoViewport(areaImage); } else { node.scrollIntoViewIfNeeded(); if (!this._isInViewport(node)) { node.scrollIntoView({block: "center", inline: "center", behavior: "instant"}); return this._isInViewport(node); } return true; } } mouseEventTest(node, name, x, y) { let frameOffset = this._frameOffset(); x -= frameOffset.left; y -= frameOffset.top; let element = document.elementFromPoint(x, y); let el = element; while (el) { if (el == node) { return true; } else { el = el.parentNode; } } let selector = element && this._getSelector(element) || "none"; throw new MouseEventFailed([name, selector, x, y].join(", ")); } _getAreaImage(node) { if ("area" == node.tagName.toLowerCase()) { let map = node.parentNode; if (map.tagName.toLowerCase() != "map") { throw new Error("the area is not within a map"); } let mapName = map.getAttribute("name"); if (typeof mapName === "undefined" || mapName === null) { throw new Error("area's parent map must have a name"); } mapName = `#${mapName.toLowerCase()}`; let imageNode = this.find("css", `img[usemap='${mapName}']`)[0]; if (typeof imageNode === "undefined" || imageNode === null) { throw new Error("no image matches the map"); } return imageNode; } } _frameOffset() { let win = window; let offset = { top: 0, left: 0 }; while (win.frameElement) { let rect = win.frameElement.getClientRects()[0]; let style = win.getComputedStyle(win.frameElement); win = win.parent; offset.top += rect.top + parseInt(style.getPropertyValue("padding-top"), 10) offset.left += rect.left + parseInt(style.getPropertyValue("padding-left"), 10) } return offset; } _getSelector(el) { let selector = (el.tagName != 'HTML') ? this._getSelector(el.parentNode) + " " : ""; selector += el.tagName.toLowerCase(); if (el.id) { selector += `#${el.id}` }; el.classList.forEach(c => selector += `.${c}`); return selector; } _isInViewport(node) { let rect = node.getBoundingClientRect(); return rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth; } select(node, value) { if (this.isDisabled(node)) { return false; } else if (value == false && !node.parentNode.multiple) { return false; } else { this.trigger(node.parentNode, "focus"); node.selected = value; this.changed(node); this.trigger(node.parentNode, "blur"); return true; } } changed(node) { let element; let event = document.createEvent("HTMLEvents"); event.initEvent("change", true, false); // In the case of an OPTION tag, the change event should come // from the parent SELECT if (node.nodeName == "OPTION") { element = node.parentNode if (element.nodeName == "OPTGROUP") { element = element.parentNode } element } else { element = node } element.dispatchEvent(event) } trigger(node, name, options = {}) { let event; if (EVENTS.MOUSE.indexOf(name) != -1) { event = document.createEvent("MouseEvent"); event.initMouseEvent( name, true, true, window, 0, options["screenX"] || 0, options["screenY"] || 0, options["clientX"] || 0, options["clientY"] || 0, options["ctrlKey"] || false, options["altKey"] || false, options["shiftKey"] || false, options["metaKey"] || false, options["button"] || 0, null ) } else if (EVENTS.FOCUS.indexOf(name) != -1) { event = this.obtainEvent(name); } else if (EVENTS.FORM.indexOf(name) != -1) { event = this.obtainEvent(name); } else { throw "Unknown event"; } node.dispatchEvent(event); } obtainEvent(name) { let event = document.createEvent("HTMLEvents"); event.initEvent(name, true, true); return event; } getAttributes(node) { let attrs = {}; for (let i = 0, len = node.attributes.length; i < len; i++) { let attr = node.attributes[i]; attrs[attr.name] = attr.value.replace("\n", "\\n"); } return this._json.stringify(attrs); } getAttribute(node, name) { if (name == "checked" || name == "selected") { return node[name]; } else { return node.getAttribute(name); } } value(node) { if (node.tagName == "SELECT" && node.multiple) { let result = [] for (let i = 0, len = node.children.length; i < len; i++) { let option = node.children[i]; if (option.selected) { result.push(option.value); } } return result; } else { return node.value; } } deleteText(node) { let range = document.createRange(); range.selectNodeContents(node); window.getSelection().removeAllRanges(); window.getSelection().addRange(range); window.getSelection().deleteFromDocument(); } containsSelection(node) { let selectedNode = document.getSelection().focusNode; if (!selectedNode) { return false; } if (selectedNode.nodeType == 3) { selectedNode = selectedNode.parentNode; } return node.contains(selectedNode); } // This command is purely for testing error handling browserError() { throw new Error("zomg"); }
}
window._cuprite = new Cuprite;