blob: cfcb89ec564da3305b4eb6bb4b15ab1b531ca061 [file] [log] [blame]
"use strict";
const conversions = require("webidl-conversions");
const idlUtils = require("../generated/utils");
const ErrorEvent = require("../generated/ErrorEvent");
const reportException = require("./runtime-script-errors");
exports.appendHandler = function appendHandler(el, eventName) {
// tryImplForWrapper() is currently required due to use in Window.js
idlUtils.tryImplForWrapper(el).addEventListener(eventName, event => {
// https://html.spec.whatwg.org/#the-event-handler-processing-algorithm
const callback = el["on" + eventName];
if (callback === null) {
return;
}
const specialError = ErrorEvent.isImpl(event) && event.type === "error" &&
event.currentTarget.constructor.name === "Window";
let returnValue = null;
const thisValue = idlUtils.tryWrapperForImpl(event.currentTarget);
// https://heycam.github.io/webidl/#es-invoking-callback-functions
if (typeof callback === "function") {
if (specialError) {
returnValue = callback.call(
thisValue, event.message,
event.filename, event.lineno, event.colno, event.error
);
} else {
// This will no longer be necessary once EventHandler and Window are implemented in IDL:
const eventWrapper = idlUtils.wrapperForImpl(event);
returnValue = callback.call(thisValue, eventWrapper);
}
}
if (event.type === "beforeunload") { // TODO: we don't implement BeforeUnloadEvent so we can't brand-check here
// Perform conversion which in the spec is done by the event handler return type being DOMString?
returnValue = returnValue === undefined || returnValue === null ? null : conversions.DOMString(returnValue);
if (returnValue !== null) {
event._canceledFlag = true;
if (event.returnValue === "") {
event.returnValue = returnValue;
}
}
} else if (specialError) {
if (returnValue === true) {
event._canceledFlag = true;
}
} else if (returnValue === false) {
event._canceledFlag = true;
}
});
};
// "Simple" in this case means "no content attributes involved"
exports.setupForSimpleEventAccessors = (prototype, events) => {
prototype._getEventHandlerFor = function (event) {
return this._eventHandlers ? this._eventHandlers[event] : undefined;
};
prototype._setEventHandlerFor = function (event, handler) {
if (!this._registeredHandlers) {
this._registeredHandlers = new Set();
this._eventHandlers = Object.create(null);
}
if (!this._registeredHandlers.has(event) && handler !== null) {
this._registeredHandlers.add(event);
exports.appendHandler(this, event);
}
this._eventHandlers[event] = handler;
};
for (const event of events) {
exports.createEventAccessor(prototype, event);
}
};
// https://html.spec.whatwg.org/#event-handler-idl-attributes
exports.createEventAccessor = function createEventAccessor(obj, event) {
Object.defineProperty(obj, "on" + event, {
configurable: true,
enumerable: true,
get() { // https://html.spec.whatwg.org/#getting-the-current-value-of-the-event-handler
const value = this._getEventHandlerFor(event);
if (!value) {
return null;
}
if (value.body !== undefined) {
let element;
let document;
if (this.constructor.name === "Window") {
element = null;
document = idlUtils.implForWrapper(this.document);
} else {
element = this;
document = element.ownerDocument;
}
const { body } = value;
const formOwner = element !== null && element.form ? element.form : null;
const window = this.constructor.name === "Window" && this._document ? this : document.defaultView;
try {
// eslint-disable-next-line no-new-func
Function(body); // properly error out on syntax errors
// Note: this won't execute body; that would require `Function(body)()`.
} catch (e) {
if (window) {
reportException(window, e);
}
this._setEventHandlerFor(event, null);
return null;
}
// Note: the with (window) { } is not necessary in Node, but is necessary in a browserified environment.
let fn;
const createFunction = document.defaultView.Function;
if (event === "error" && element === null) {
const wrapperBody = document ? body + `\n//# sourceURL=${document.URL}` : body;
// eslint-disable-next-line no-new-func
fn = createFunction("window", `with (window) { return function onerror(event, source, lineno, colno, error) {
${wrapperBody}
}; }`)(window);
} else {
const argNames = [];
const args = [];
argNames.push("window");
args.push(window);
if (element !== null) {
argNames.push("document");
args.push(idlUtils.wrapperForImpl(document));
}
if (formOwner !== null) {
argNames.push("formOwner");
args.push(idlUtils.wrapperForImpl(formOwner));
}
if (element !== null) {
argNames.push("element");
args.push(idlUtils.wrapperForImpl(element));
}
let wrapperBody = `
return function on${event}(event) {
${body}
};`;
for (let i = argNames.length - 1; i >= 0; --i) {
wrapperBody = `with (${argNames[i]}) { ${wrapperBody} }`;
}
if (document) {
wrapperBody += `\n//# sourceURL=${document.URL}`;
}
argNames.push(wrapperBody);
fn = createFunction(...argNames)(...args);
}
this._setEventHandlerFor(event, fn);
}
return this._getEventHandlerFor(event);
},
set(val) {
val = eventHandlerArgCoercion(val);
this._setEventHandlerFor(event, val);
}
});
};
function typeIsObject(v) {
return (typeof v === "object" && v !== null) || typeof v === "function";
}
// Implements:
// [TreatNonObjectAsNull]
// callback EventHandlerNonNull = any (Event event);
// typedef EventHandlerNonNull? EventHandler;
// Also implements the part of https://heycam.github.io/webidl/#es-invoking-callback-functions which treats
// non-callable callback functions as callback functions that return undefined.
// TODO: replace with webidl2js typechecking when it has sufficient callback support
function eventHandlerArgCoercion(val) {
if (!typeIsObject(val)) {
return null;
}
return val;
}