@elliemae/em-ssf-dom
Version:
ICE Secure Scripting Framework DOM Controls
1,362 lines (1,135 loc) • 61.8 kB
JavaScript
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else if(typeof exports === 'object')
exports["dom"] = factory();
else
root["elli"] = root["elli"] || {}, root["elli"]["dom"] = factory();
})(typeof self !== 'undefined' ? self : this, function() {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, {
/******/ configurable: false,
/******/ enumerable: true,
/******/ get: getter
/******/ });
/******/ }
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 1);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
//
// Provides very basic logging capabilities to the SSF system
//
module.exports = {
// Constants for the different log levels
levels: {
Verbose: 0,
Trace: 0,
Info: 1,
Warning: 2,
Error: 3,
None: 10
},
// Colors used for logging
colors: ['grey', 'black', 'darkorange', 'firebrick'],
// The current log verbosity level
logLevel: 3,
// Writes an entry to the log
log: function log(text, level, src) {
level = level ? level : this.levels.Verbose;
if (level >= this.logLevel) {
var val = (src ? '(' + src + '): ' : '') + text;
if (this.colors && this.colors[level]) console.log('%c' + val, 'color: ' + this.colors[level]);else console.log(val);
}
},
// Writes a warning to the log
info: function info(text, src) {
this.log(text, this.levels.Info, src);
},
// Writes a warning to the log
warn: function warn(text, src) {
this.log(text, this.levels.Warning, src);
},
// Writes a warning to the log
error: function error(text, src) {
this.log(text, this.levels.Error, src);
},
// Writes a verbose/trace to the log
trace: function trace(text, src) {
this.log(text, this.levels.Trace, src);
}
};
/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
//
// elli.dom provides a namespace for the controls ppublished by a host object
//
var emssf = __webpack_require__(2);
var logger = __webpack_require__(0);
var logSource = "ssf-dom";
// Indicates if a value is defined
var _def = function _def(value) {
return arguments.length > 0 && value !== undefined;
};
// Retrieves the current element of child
var _input = function _input(domElement, type) {
var tagNames = ["input", "select", "textarea"];
for (var i = 0; i < tagNames.length; i++) {
var tag = tagNames[i];
if (domElement.tagName.toLowerCase() == tag && (!type || domElement.type.toLowerCase() == type.toLowerCase())) {
return domElement;
} else if (type) {
var e = domElement.querySelector(tag + "[type='" + inputType + "']");
if (e != null) return e;
} else {
var _e = domElement.querySelector(tag);
if (_e != null) return _e;
}
}
return null;
};
// Creates the dom object to be exposed
var dom = {
// Creates an automation object from a DOM element
getObject: function getObject(objectId) {
// Retrieve the element from the document either by data-dom-id or id
var element = document.querySelector('[data-dom-id="' + objectId + '"]') || document.getElementById(objectId);
if (!element) return null;
return this.fromElement(element);
},
// Creates an automation object from a DOM element
fromElement: function fromElement(element, objectType) {
// Read the object's ID
var objectId = element.getAttribute("data-dom-id") || element.id;
if (!objectId) return null;
// Make sure the object is marked as data-dom-type
if (!objectType) objectType = element.getAttribute("data-dom-type");
if (!objectType) return null;
// Instantiate the object type
return new dom[objectType](objectId, element);
},
bindAll: function bindAll(host) {
// Discovers all scriptable types in the current document and creates event handlers on them
var domElements = document.querySelectorAll('[data-dom-type]');
for (var i = 0; i < domElements.length; i++) {
this.bind(domElements[i], host);
}
},
// Connects the automation framework to the remoting systems
bind: function bind(element, host) {
// Convert to object
var obj = this.fromElement(element);
if (obj != null && obj.domElement) {
for (var eventName in obj) {
var eventDef = obj[eventName];
if (eventDef instanceof emssf.Event) {
if (eventDef.eventBinder) {
// This control has custom binding
eventDef.eventBinder(obj, eventName, eventDef);
} else {
// Read the mapped event type and event source element
var eventSource = eventDef.eventSource || obj.domElement;
this.bindEvent(obj, eventName, eventDef, eventSource);
}
}
}
}
if (obj && host) {
host.publish(obj);
}
},
// Binds an event on a DOM object to an EM-SSF event.
// scriptObject: The ScriptingObject-derived object for the event
// eventName: The name of the event, as seen by the guest
// eventDef: An Event object that contains the event metadata (see createEvent below)
// domObject: The DOM object on which the event will be hooked up
bindEvent: function bindEvent(scriptObject, eventName, eventDef, domObject) {
// Pull the event object and event handler off the DOM element
var eventTarget = scriptObject.domElement.getAttribute('data-dom-' + eventName + '-handler');
var domEvent = eventDef.domEvent || eventName;
// Add the event listener to the DOM object
domObject.addEventListener(domEvent, function (domEventObj) {
logger.log('Caught DOM event ' + domEvent + ' on ' + scriptObject.id + ' -> ' + 'raising ' + eventName + ' event' + (eventTarget ? ' -> target = ' + eventTarget : ''), logSource);
var eventObj = eventDef.eventTransformer ? eventDef.eventTransformer(scriptObject, domEventObj) : null;
emssf.raiseEvent(scriptObject.id, eventName, eventObj, eventTarget ? { eventHandler: eventTarget } : null);
});
logger.log('Bound DOM event ' + domEvent + ' to ' + eventName + ' on ' + scriptObject.id, logSource);
},
// Publishes the controls for this object to a host
publishAll: function publishAll(host) {
// Discovers all scriptable types in the current document and creates event handlers on them
var elements = document.querySelectorAll('[data-dom-type]');
for (var i = 0; i < elements.length; i++) {
// Convert to object
var obj = this.fromElement(elements[i]);
if (obj != null) {
host.publish(obj);
}
};
},
// Creates an event, possibly mapped to a DOM event and with a converter callback
createEvent: function createEvent(eventOptions) {
var e = new emssf.Event();
// Shallow copy the event options to the event. Expected options are:
// domEvent: name of DOM event to map this event to
// eventSource: the DOM element from which the event will be emitted
// eventTransformer: a Function that will handle event object transformation
// eventBinder: a Function that perform event binding
if (eventOptions) {
for (var o in eventOptions) {
e[o] = eventOptions[o];
}
}
return e;
},
// Fires a DOM event for a control
fireDOMEvent: function fireDOMEvent(domElement, eventName) {
var e = document.createEvent("Event");
e.initEvent(eventName, true, true);
domElement.dispatchEvent(e);
}
// Control provides a base class for all DOM-backed controls
};dom.Control = function Control(objectId, element) {
// Invoke base class constructor
emssf.ScriptingObject.call(this, objectId);
// Set the object type
this.domElement = element;
this.objectType = 'Control';
// Override this value in derived classed to mark the control as
// a "container" (i.e. can have sub-controls that get enabled/disabled
// with the container).
this.isContainer = false;
// Get or set the background color of the control
this.color = function (colorVal) {
if (_def(colorVal)) this.domElement.style.backgroundColor = colorVal;
return this.domElement.style.backgroundColor;
};
// Gets or sets the visible state of the control
this.visible = function (visibleVal) {
var hideClass = null;
if (this.domElement.hasAttribute('data-dom-hidden-class')) {
hideClass = this.domElement.getAttribute('data-dom-hidden-class');
}
if (_def(visibleVal)) {
if (hideClass) {
if (visibleVal) {
this.domElement.classList.remove(hideClass);
} else {
this.domElement.classList.add(hideClass);
}
} else {
this.domElement.style.display = visibleVal ? 'inline' : 'none';
}
}
if (hideClass) {
return !this.domElement.classList.contains(hideClass);
} else {
return this.domElement.style.display != 'none';
}
};
// Toggles the disabled state of the control. This state needs to propagate to
// child controls if it's a container.
var _setDOMInteractive = function _setDOMInteractive(domNode, nodeState, containerState) {
// See if the node is a control
var ctrl = dom.fromElement(domNode);
if (ctrl) {
// Read the enabled/disabled state of the control
nodeState = !ctrl.disabled();
if (containerState === undefined) {
containerState = ctrl.domElement.getAttribute("data-dom-container-state") !== "inactive";
} else {
ctrl.domElement.setAttribute("data-dom-container-state", containerState ? "" : "inactive");
}
}
// The node is "interactive" if the control is enabled and the container
// is interactive
var interactive = nodeState && containerState;
// Set the state of the control
if (domNode.hasAttribute('data-dom-disabled-class')) {
var disabledClass = domNode.getAttribute('data-dom-disabled-class');
interactive ? domNode.classList.remove(disabledClass) : domNode.classList.add(disabledClass);
}
// Disable the node
domNode.disabled = !interactive;
// If the current control is a container, we
if (ctrl && ctrl.isContainer) {
containerState = interactive;
}
// Propagate the state to the children
if (domNode.children.length > 0) {
for (var i = 0; i < domNode.children.length; i++) {
_setDOMInteractive(domNode.children[i], nodeState, containerState);
}
}
};
// Indicates of the control is currently interactive or not
this.interactive = function () {
return this.domElement.getAttribute("data-dom-container-state") !== "inactive" && this.domElement.getAttribute("data-dom-disabled") !== "true";
};
// Expose the ability to change the enabled state of the control
this.disabled = function (newVal) {
// Set the enabled/disabled state of the control and then toggle the interactive state
if (_def(newVal)) {
this.domElement.setAttribute("data-dom-disabled", newVal ? "true" : "false");
_setDOMInteractive(this.domElement);
}
var state = this.domElement.getAttribute("data-dom-disabled") === "true";
return state;
};
};
// FieldControl provides a base class for controls that contain data from the Model object (Loan)
dom.FieldControl = function Control(objectId, element) {
// Invoke base class constructor
dom.Control.call(this, objectId, element);
// Protected method to transforms the DOM change event object into a scripting event object
this._transformChangeEvent = function (scriptObject, domEvent) {
return { value: scriptObject.value() };
};
// Map events from the DOM object
this.databind = dom.createEvent();
this.datacommit = dom.createEvent();
this.domInput = _input(this.domElement) || this.domElement;
// Map events from the DOM object
this.focusin = dom.createEvent({ eventSource: this.domInput, domEvent: "focus" });
this.focusout = dom.createEvent({ eventSource: this.domInput, domEvent: "blur" });
this.change = dom.createEvent({ eventSource: this.domInput, eventTransformer: this._transformChangeEvent });
// Override the Color attribute to affect the input element
this.color = function (colorVal) {
if (_def(colorVal)) this.domInput.style.backgroundColor = colorVal;
return this.domInput.style.backgroundColor;
};
// Get and set field's value -- these functions can be overriden in derived classes based on the
// requirements of the appropriate control.
this.value = function (valueVal) {
if (_def(valueVal)) {
if (this.domInput.value != valueVal) {
this.domInput.value = valueVal;
dom.fireDOMEvent(this.domInput, 'change');
}
}
return this.domInput.value;
};
};
// Button control
dom.Button = function Button(objectId, element) {
// Invoke base class constructor
dom.Control.call(this, objectId, element);
// Set the object type
this.objectType = 'Button';
// Map the Click event through
this.click = dom.createEvent();
};
// Hyperlink control
dom.HyperLink = function HyperLink(objectId, element) {
// Invoke base class constructor
dom.Control.call(this, objectId, element);
// Set the object type
this.objectType = 'HyperLink';
// Map the Click event through
this.click = dom.createEvent();
};
// TextBox control
dom.TextBox = function TextBox(objectId, element) {
// Invoke base class constructor
dom.FieldControl.call(this, objectId, element);
// Set the object type
this.objectType = 'TextBox';
// Add the events for this type
this.text = function (textVal) {
return this.value(textVal);
};
};
// Label control
dom.Label = function Label(objectId, element) {
// Invoke base class constructor
dom.Control.call(this, objectId, element);
// Set the object type
this.objectType = 'Label';
// Add a supported event for changes
this.change = dom.createEvent();
// Gets or set the text in the Label
this.text = function (textVal) {
if (_def(textVal)) {
this.domElement.innerText = textVal;
emssf.raiseEvent(this, "change", { text: textVal });
}
return this.domElement.innerText;
};
// Gets or sets the font color
this.fontColor = function (colorVal) {
if (_def(colorVal)) this.domElement.style.color = colorVal;
return this.domElement.style.color;
};
};
// DropdownBox control
dom.Dropdown = function Dropdown(objectId, element) {
// Invoke base class constructor
dom.FieldControl.call(this, objectId, element);
// Set the object type
this.objectType = 'Dropdown';
// Override the value method on the Dropdown to work properly for a <select> element
this.value = function (valueVal) {
if (_def(valueVal)) {
for (var i = 0; i < this.domInput.options.length; i++) {
if (this.domInput.options[i].value == valueVal) {
if (this.domInput.selectedIndex != i) {
this.domInput.selectedIndex = i;
dom.fireDOMEvent(this.domInput, 'change');
}
break;
}
}
}
return this.domInput.options[this.domInput.selectedIndex].value;
};
// Set/Get dropdown options
// Optional param: Values -> Array of { key: string, value: any }
this.options = function (values) {
var selectBoxOptions = this.domInput.options;
if (_def(values)) {
// Erase current options at the dropdown
for (var i = selectBoxOptions.length - 1; i >= 0; i--) {
selectBoxOptions.remove(i);
};
// Add new ones
for (var _i = 0; _i < values.length; _i++) {
var newOption = document.createElement("option");
newOption.value = values[_i].value;
newOption.text = values[_i].key;
selectBoxOptions.add(newOption);
};
return values;
} else {
// Cycle through selectBoxOptions, get the array of options by {key,value} return that array
var currentOptions = [];
for (var i = 0; i < selectBoxOptions.length; i++) {
currentOptions.push({
value: selectBoxOptions[i].value,
key: selectBoxOptions[i].text
});
}
return currentOptions;
}
};
};
// Checkbox control
dom.CheckBox = function CheckBox(objectId, element) {
// Invoke base class constructor
dom.FieldControl.call(this, objectId, element);
// Set the object type
this.objectType = 'CheckBox';
// Get or set the checked state of a Checkbox
this.checked = function (checkedVal) {
if (_def(checkedVal)) {
this.domInput.checked = checkedVal;
dom.fireDOMEvent(this.domInput, 'change');
}
return this.domInput.checked;
};
// Override the value function for the checkbox to return the string value
// if the checkbox is checked, an empty string otherwise
this.value = function (valueVal) {
if (_def(valueVal)) {
this.checked(valueVal == this.domInput.value);
}
return this.domInput.checked ? this.domInput.value : "";
};
};
// RadioButton control -- this control is really a clone of CheckBox
dom.RadioButton = function RadioButton(objectId, element) {
// Invoke base class constructor
dom.CheckBox.call(this, objectId, element);
// Set the object type
this.objectType = 'RadioButton';
};
// RadioButtonGroup control
// The create a RadioButton Group, do the following:
// 1. Place the data-dom-type="RadioButtonGroup" attribute on the container (e.g. DIV) of the group
// 2. Set the "id" of the container control to be the desired id of the control that will be exposed
// (or set the data-dom-id attribute)
// 3. On each <input type="radio">, set the "name" attribute to match the "id" of the parent container
dom.RadioButtonGroup = function RadioButtonGroup(objectId, element) {
// Invoke base class constructor
dom.FieldControl.call(this, objectId, element);
// Set the object type
this.objectType = 'RadioButtonGroup';
// Override the change event
this.change = dom.createEvent({ eventTransformer: this._transformChangeEvent, eventBinder: bindChangeEvent });
// Gets or sets the value of the radio button control
this.value = function (valueVal) {
var elements = this.domElement.querySelectorAll('input[name="' + this.id + '"]');
if (_def(valueVal)) {
var checkedElem;
var uncheckedElem;
for (var i = 0; i < elements.length; i++) {
if (elements[i].value == valueVal) {
if (!elements[i].checked) {
elements[i].checked = true;
checkedElem = elements[i];
}
} else if (elements[i].checked) {
elements[i].checked = false;
uncheckedElem = elements[i];
}
}
if (checkedElem || uncheckedElem) {
dom.fireDOMEvent(checkedElem || uncheckedElem, 'change');
emssf.raiseEvent(this, 'change');
}
}
// Retrieve the current value of the group
for (var _i2 = 0; _i2 < elements.length; _i2++) {
if (elements[_i2].checked) {
return elements[_i2].value;
}
}
return "";
};
// Performs event binding for the group object
function bindChangeEvent(groupObj, eventName, eventDef) {
var name = groupObj.id;
var elements = groupObj.domElement.querySelectorAll('input[name="' + name + '"]');
for (var i = 0; i < elements.length; i++) {
dom.bindEvent(groupObj, eventName, eventDef, elements[i]);
}
}
this.color = function (colorVal) {
if (_def(colorVal)) {
var internalRadios = this.domElement.querySelectorAll('label.control-label-radiobutton');
for (var i = 0; i < internalRadios.length; i++) {
internalRadios[i].style.backgroundColor = colorVal;
}
};
var internalRadio = this.domElement.querySelector('label.control-label-radiobutton');
return internalRadio.style.backgroundColor;
};
};
// CheckBoxGroup control
// See RadioButtonGroup instructions for setting up a CheckBoxGroup
dom.CheckBoxGroup = function CheckBoxGroup(objectId, element) {
// Invoke base class constructor
dom.RadioButtonGroup.call(this, objectId, element);
// Set the object type
this.objectType = 'CheckBoxGroup';
this.color = function (colorVal) {
if (_def(colorVal)) {
var internalCheckbox = this.domElement.querySelectorAll('label.control-label-checkbox');
for (var i = 0; i < internalCheckbox.length; i++) {
internalCheckbox[i].style.backgroundColor = colorVal;
}
};
var internalCheckbox = this.domElement.querySelector('label.control-label-checkbox');
return internalCheckbox.style.backgroundColor;
};
};
// Panel control
dom.Panel = function Panel(objectId, element) {
// Invoke base class constructor
dom.Control.call(this, objectId, element);
// Set the object type
this.objectType = 'Panel';
this.isContainer = true;
};
// Image control
dom.Image = function Image(objectId, element) {
// Invoke base class constructor
dom.Control.call(this, objectId, element);
// Set the object type
this.objectType = 'Image';
};
// DatePicker control
dom.DatePicker = function DatePicker(objectId, element) {
// Invoke base class constructor
dom.FieldControl.call(this, objectId, element);
// Set the object type
this.objectType = 'DatePicker';
// Override the value method for this control
this.value = function (valueVal) {
// Set the value if appropriate
if (_def(valueVal)) {
if (this.domInput.value != valueVal) {
this.domInput.value = valueVal;
dom.fireDOMEvent(this.domInput, "change");
}
}
return this.domInput.value;
};
};
// Number control
dom.Number = function Number(objectId, element) {
// Invoke base class constructor
dom.FieldControl.call(this, objectId, element);
// Set the object type
this.objectType = 'Number';
};
// CollectionRowGroup control
dom.CollectionRowGroup = function CollectionRowGroup(objectId, element) {
// Invoke base class constructor
dom.Control.call(this, objectId, element);
// Set the object type
this.objectType = 'CollectionRowGroup';
this.isContainer = true;
// Get the data-dom-ids of the objects contained in this row group
this.getAllControls = function () {
var elements = this.domElement.querySelectorAll('[data-dom-type]');
var elementIDs = [];
for (var i = 0; i < elements.length; i++) {
var control = dom.fromElement(elements[i]);
if (control) {
// Validate the controls which need to be excluded from the controls array
var collectionControl = control.domElement.getAttribute("data-dom-exclude-from-collection");
if (collectionControl === 'true') continue;
elementIDs.push(control.id);
}
}
return elementIDs;
};
};
// CollectionBox Control
dom.CollectionBox = function CollectionBox(objectId, element) {
// Invoke base class constructor
dom.Control.call(this, objectId, element);
// Set the object type
this.objectType = 'CollectionBox';
this.isContainer = true;
// Returns Internal CollectionRowGroup within CollectionBox
var getInternalRows = function getInternalRows() {
return this.domElement.querySelectorAll('[data-dom-type="CollectionRowGroup"]');
};
// Returns number of rows within CollectionBox
this.getRowCount = function () {
var elements = getInternalRows.call(this);
return elements.length;
};
// Returns control instance of selected row
this.getRowAt = function (rowId) {
if (_def(rowId)) {
var elements = getInternalRows.call(this);
if (elements[rowId]) {
return dom.fromElement(elements[rowId]);
}
return null;
}
};
// Returns add/remove button within CollectionBox
this.getCollectionButton = function (buttonType, row) {
var buttonId = row !== undefined ? "" + objectId + buttonType + row : "" + objectId + buttonType;
var element = dom.getObject(buttonId);
return element;
};
// Map the add and remove events through
this.add = dom.createEvent({ eventTransformer: this._transformAddEvent });
this.remove = dom.createEvent();
// Protected method to transforms the DOM add event object to include index
this._transformAddEvent = function (scriptObject, domEvent) {
return { index: scriptObject.getRowCount() };
};
};
// Export the dom object
module.exports = dom;
/***/ }),
/* 2 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
//
// The elli.automation object provides a namespace for static functions and objects
// related to the scritpable controls
//
// Create the elli namespace, if not already present
var remoting = __webpack_require__(3);
var logger = __webpack_require__(0);
var logSource = "ssf-host";
var automation = function () {
// String constants
var eventMessageType = "object:event";
var configMessageType = "host:config";
// Represents the set of windows to which events will be broadcast
var isConnected = false;
var guests = [];
// Initializes a guest when the guest is full loaded
function handleGuestReady(sourceWin, msgId, msgType, msgBody) {
// Verify the guest belongs to this host
var guest = getGuestForWindow(sourceWin);
if (guest == null) {
logger.warn('Received ready event for unknown guest. Known guests = ' + guests.length, logSource);
return;
}
if (!guest.initialized) {
// Initialize the capabilities object
guest.initialized = true;
guest.capabilities = msgBody || {};
// Invoke the control function on the guest
dispatchConfigEvent(guest);
logger.info('Guest (id = ' + guest.id + ') in frame ' + guest.domElement.id + ' is initialized.', logSource);
}
// If the guest has not indicated that it will provide explicit notification
// when it's ready, we'll fire the ready event now
if (!msgBody || !msgBody.onReady) {
handleGuestReadyComplete(sourceWin, msgId, msgType, msgBody);
}
}
// Handles the ReadyComplete event from the guest (invoked by calling ready())
function handleGuestReadyComplete(sourceWin, msgId, msgType, msgBody) {
// Verify the guest belongs to this host
var guest = getGuestForWindow(sourceWin);
if (guest == null) {
logger.warn('Received ready event for unknown guest. Known guests = ' + guests.length, logSource);
return;
}
// Ensure this guest is initialized
if (!guest.initialized) {
throw "Guest must be initialized before it is marked as ready";
}
if (!guest.ready) {
// Set the ready state
guest.ready = true;
// Invoke the registered callback, if any
if (guest.host.options && guest.host.options.readyStateCallback) {
guest.host.options.readyStateCallback(guest);
}
logger.info('Guest (id = ' + guest.id + ') in frame ' + guest.domElement.id + ' is ready.', logSource);
}
}
// Dispatches a message to the guest to update configuration settings that
// must propagate from the host to the guests.
function dispatchConfigEvent(guest) {
if (guest.ready) {
guest.dispatchMessage(configMessageType, {
logLevel: logger.logLevel
});
}
}
// Handles object get requests from the remote automation framework
function handleObjectGet(sourceWin, msgId, msgType, msgBody) {
var objectId = msgBody.objectId;
logger.info('Processing request getObject("' + objectId + '") (requestId = ' + msgId + ')...', logSource);
// Get the guest from which the request came
var guest = getGuestForWindow(sourceWin);
if (guest == null) {
logger.warn('Rejected object request from unknown guest window', logSource);
return false;
}
// Get the object from the guest's host
var obj = guest.host.getObjectById(objectId);
if (obj == null) {
logger.warn('Rejected object request from unauthorized guest window', logSource);
throw "Invocation of unknown or unauthorized object '" + objectId + "' from guest";
}
remoting.respond(sourceWin, msgId, encodeResponse(guest, obj));
logger.info('Returned scripting object "' + objectId + '" (requestId = ' + msgId + ')', logSource);
}
// Handles object invoke requests from the remote automation framework
function handleObjectInvoke(sourceWin, msgId, msgType, msgBody) {
var objectId = msgBody.objectId;
// Get the guest from which the request came
var guest = getGuestForWindow(sourceWin);
if (guest == null) {
logger.warn('Rejected invocation of ' + objectId + '.' + msgBody.functionName + ' from unknown guest window', logSource);
return false;
}
logger.info('Function ' + objectId + '.' + msgBody.functionName + '() called from guest "' + guest.id + '" (requestId = ' + msgId + ')', logSource);
// Get the object from the guest's host
var obj = guest.host.getObjectById(objectId);
if (obj == null) {
logger.warn('Invocation of unknown or unauthorized object (' + objectId + ') from guest "' + guest.id + '"', logSource);
remoting.raiseException('The requested object (' + objectId + ') is not available');
return false;
}
// Invoke the function, which will always return a Promise
invoke(guest, obj, msgBody.functionName, msgBody.functionParams).then(function (val) {
remoting.respond(sourceWin, msgId, encodeResponse(guest, val));
logger.info('Dispatched return value for ' + objectId + '.' + msgBody.functionName + '() to guest "' + guest.id + '" (requestId = ' + msgId + ')', logSource);
}).catch(function (ex) {
remoting.raiseException(sourceWin, msgId, encodeException(guest, ex));
logger.info('Dispatched exception for ' + objectId + '.' + msgBody.functionName + '() to guest "' + guest.id + '" (requestId = ' + msgId + ')', logSource);
});
}
// Encodes the response to a function call to prepare it to be remoted
function encodeResponse(guest, val) {
if (val && val._toJSON) {
// Ensure the object is published so it can receive callbacks
guest.host.publish(val);
return { type: 'object', object: val._toJSON() };
} else {
return { type: 'value', value: val };
}
}
// Encodes an exception thrown by a host function
function encodeException(guest, ex) {
if (typeof ex === "string") {
return ex;
} else if (ex instanceof Error) {
return ex.message;
} else if (ex.toString && typeof ex.toString === "function") {
return ex.toString();
} else {
return 'An unexpected error occurred in the host application';
}
}
// Returns the guest in a given window
function getGuestForWindow(guestWindow) {
return guests.find(function (guest) {
return guest.window === guestWindow;
});
}
// Invokes a function on a elli.automation object
function invoke(guest, obj, functionName, functionParams) {
// Ensure the function exists
if (typeof obj[functionName] != "function") {
logger.warn("Attempt to call invalid function on object type " + obj.objectType + ": " + functionName, logSource);
return Promise.reject("Function not found");
}
// Invoke the object's function
logger.info('Invoking host implementation of ' + obj.id + '.' + functionName + '()', logSource);
return new Promise(function (resolve, reject) {
obj[functionName].callContext = { guest: guest };
resolve(obj[functionName].apply(obj, _toConsumableArray(functionParams)));
});
}
// Connects the elli.automation framework to the remoting systems
function connect() {
if (!isConnected) {
remoting.setLogSource(logSource);
remoting.listen("object:get", handleObjectGet);
remoting.listen("object:invoke", handleObjectInvoke);
remoting.listen("guest:ready", handleGuestReady);
remoting.listen("guest:readyComplete", handleGuestReadyComplete);
isConnected = true;
}
}
// Flattens an array or arrays into a single array (equivalent to the Array.flat() method,
// which is not supported in IE or Safari)
function flatten(source, target) {
var retVal = target || [];
if (source && source.forEach) {
source.forEach(function (item) {
flatten(item, retVal);
});
} else if (typeof source != "undefined") {
retVal.push(source);
}
return retVal;
}
// Return the public implementation of the class
return {
// Registers a callback for elli.automation events
registerGuest: function registerGuest(guest) {
if (!guest || !guest.host || !guest.window) {
throw "Invalid guest object";
}
// Connect the framework
connect();
// Add to the guest list
guests.push(guest);
},
// Removes a guest from the list of registered guests
unregisterGuest: function unregisterGuest(guestId) {
guests = guests.filter(function (guest) {
return guest.id != guestId;
});
},
// Raises an event from a elli.automation object. The eventOptions is an optional parameter
// to set other options for the event. Valid options include:
// window: the guest window handle for a targeted event
// eventHandler: the name of the event handler function in the guest, if known
// timeout: the timeout (in ms) for feedback to be returned
raiseEvent: function raiseEvent(elementOrId, eventName, eventParams, eventOptions) {
// Resolve the elli.automation object
var objectId = elementOrId.id || elementOrId;
logger.info('Raising event "' + objectId + '.' + eventName + '"...', logSource);
// Invoke the event handler in all guests which are exposed to this object
var guestPromises = [];
eventOptions = eventOptions || {};
guests.forEach(function (guest) {
// If a target is supplied, be sure to honor that target to send the message only to
// the intended window
if (!eventOptions.window || eventOptions.window === guest.window) {
// Get the object from the host
var obj = guest.host.getObjectById(objectId);
if (obj != null) {
var eventObj = {
object: obj._toJSON(),
eventName: eventName,
eventParams: eventParams,
eventHandler: eventOptions.eventHandler,
eventOptions: {
allowsFeedback: eventOptions.timeout ? true : false,
timeout: eventOptions.timeout
}
};
if (eventOptions.timeout && guest.capabilities.eventFeedback) {
// For an event with feedback, we call the invoke() method, which returns
// a Promise that will be fulfilled when the guest responds
guestPromises.push(remoting.invoke(guest.window, eventMessageType, eventObj, eventOptions.timeout));
logger.info('Dispatched interactive event "' + objectId + '.' + eventName + '" to guest (id = "' + guest.id + '"), ' + '"timeout = ' + eventOptions.timeout + 'ms', logSource);
} else {
// If no feedback is needed, we fire and forget
remoting.send(guest.window, eventMessageType, eventObj);
logger.info('Dispatched fire-and-forget event "' + objectId + '.' + eventName + '" to guest (id = "' + guest.id + '")', logSource);
}
}
}
});
// Return a promise that aggregates all of the guest promises
return Promise.all(guestPromises).then(function (values) {
// Flatten the values into a single array
logger.info('Completed event processing for "' + objectId + '.' + eventName + '"', logSource);
return flatten(values);
});
},
// Sets the log level for the host and guest
setLogLevel: function setLogLevel(level) {
// Set the logging level at the host
logger.logLevel = level;
// Notify all guests of the change
guests.forEach(dispatchConfigEvent);
logger.info('Dispatched config events to all guests', logSource);
},
// Expose the log levels from the logger object
logLevels: logger.levels
};
}();
// A Guest is a hosted iframe containing a sandboxed script
automation.Guest = function Guest(guestId, host, domElement, params) {
this.id = guestId;
this.params = params;
this.host = host;
this.domElement = domElement;
this.window = domElement.contentWindow;
this.ready = false;
this.capabilities = {};
// Dispatches a message to the window
this.dispatchMessage = function (messageType, messageBody) {
remoting.send(this.window, messageType, messageBody);
};
};
// A Host is an object which exposes automation objects to a guest
automation.Host = function Host(hostId, hostOptions) {
// Counter for the guests
var nextGuestId = 1;
// Set the local variables
this.hostId = hostId;
this.options = {
readyStateCallback: null
};
// Merge in the host options
if (hostOptions) {
for (var o in hostOptions) {
this.options[o] = hostOptions[o];
}
}
// The host's set of guests
var guests = new Map();
// A global set of automation objects which are exposed to the child window
var publishedObjects = new Map();
// Gets a elli.automation object from the host.
this.getObjectById = function (objectId) {
// Check the globals
var obj = publishedObjects.get(objectId);
if (obj) return obj;
return null;
};
// Publishes a global object to be available to the guest window(s)
this.publish = function (globalObject) {
if (!globalObject.id || !globalObject._toJSON) {
throw "Object is not derived from ScriptingObject";
}
publishedObjects.set(globalObject.id, globalObject);
};
// Disposes all published objects, as needed
this.unpublish = function (objectId) {
var obj = this.getObjectById(objectId);
if (obj) {
// Dispose of the object if it supports this capability
if (obj._dispose && typeof obj._dispose == "function") {
obj._dispose();
}
publishedObjects.delete(objectId);
}
};
// Unpublish all published objects
this.unpublishAll = function () {
var _this = this;
var objectIds = Array.from(publishedObjects.keys());
objectIds.forEach(function (objectId) {
_this.unpublish(objectId);
});
};
// Creates a guest of the host window
this.attachGuest = function (guestId, guestDomElement, guestOptions) {
var guest = new automation.Guest(guestId, this, guestDomElement, guestOptions);
// Register the guest with the automation framework
automation.registerGuest(guest);
// Add the guest
guests.set(guestId, guest);
return guest;
};
// Renders a list of guests into sandboxed iframes
// This is the primary method for kicking off the automation framework.
this.renderGuest = function (guestParams, sandboxSrc, targetElement, options) {
var guestId = "guest-" + this.hostId + "-sandbox-" + nextGuestId++;
// Build the querystring for the sanbox page
var querystring = "";
for (var p in guestParams) {
querystring += (querystring.length ? "&" : "") + encodeURIComponent(p) + "=" + encodeURIComponent(guestParams[p]);
}
// Generate the iframe
var targetElementDocument = targetElement.ownerDocument ? targetElement.ownerDocument : document;
var frame = targetElementDocument.createElement("iframe");
frame.setAttribute("id", guestId);
var guestOrigin = ((sandboxSrc || '').match(/^(https?:\/\/[^\/#?&]+)/) || [])[1] || '';
var parentOrigin = (window.location.origin || '').toLowerCase();
if (options && options.excludeSandboxAttribute && parentOrigin === guestOrigin.toLowerCase()) {
logger.info('Creating guest excluding sandbox attribute ...');
} else {
logger.info('Creating guest with sandbox attribute ...');
frame.setAttribute("sandbox", "allow-scripts allow-popups allow-modals allow-forms allow-downloads" + (options && options.allowSameOrigin ? " allow-same-origin" : "") + (options && options.allowPopupsToEscapeSandbox ? " allow-popups-to-escape-sandbox" : ""));
}
if (options && options.permissionPolicy) {
// PUI-16106 - support iframe permission policies
if (typeof options.permissionPolicy == "string") {
frame.setAttribute("allow", options.permissionPolicy);
} else {
frame.setAttribute("allow", (options && options.permissionPolicy.allowClipboardRead ? "clipboard-read;" : "") + (options && options.permissionPolicy.allowClipboardWrite ? " clipboard-write;" : ""));
}
}
frame.setAttribute("title", options && options.title ? options.title : "");
frame.setAttribute("src", sandboxSrc + (sandboxSrc.indexOf("?") >= 0 ? "&" : "?") + querystring);
targetElement.appendChild(frame);
// Find the window object and create the guest
var guestFrame = targetElementDocument.getElementById(guestId);
var guest = this.attachGuest(guestId, guestFrame, guestParams);
logger.info('Created guest window with id = ' + guest.id, logSource);
// Initialize the messenger
remoting.initialize(targetElementDocument.defaultView || targetElementDocument.parentWindow);
return guest;
};
// Returns an array with all of the guests for this host
this.getGuests = function () {
var guestList = [];
guests.forEach(function (guest, guestId) {
guestList.push(guest);
});
return guestList;
};
// Renders a list of guests into sandboxed iframes
// This is the primary method for kicking off the automation framework.
this.renderGuests = function (guestParamsList, sandboxSrc, targetElement, options) {
// Render the plugins into sandboxed iframe elements
guestParamsList.forEach(function (guestParams) {
this.renderGuest(guestParams, sandboxSrc, targetElement, options);
}, this);
};
// Destroys a guest
this.destroyGuest = function (guestIdOrWindow) {
// Find the guest object
var guest = guests.get(guestIdOrWindow);
if (!guest) {
guests.forEach(function (value, key) {
if (value.window === guestIdOrWindow) {
guest = value;
} else if (value.domElement === guestIdOrWindow) {
guest = value;
}
});
}
// If we still don't have a guest, throw an error
if (!guest) {
throw "Invalid guestId or guestWindow reference";
}
// Unregister the guest from the automation framework
automation.unregisterGuest(guest.id);
// Remove the guest element from the DOM
if (guest.domElement.remove) {
guest.domElement.remove();
} else {
guest.domElement.removeNode(true);
}
// Remove from the host's guest list
guests.delete(guest.id);
};
};
// Provides an Event class for use within the automation framework
automation.Event = function Event() {}
// This is just an empty object for now -- reserved for future use
// Provides a base class for all automation objects
;automation.ScriptingObject = function ScriptingObject(objectId) {
// Set the basic properties
this.id = objectId;
this.objectType = 'Object';
// Serializes to a JSON object
this._toJSON = function () {
// Enumerate the list of public functions
var functions = [];
var events = [];
for (var p in this) {
if (typeof this[p] === 'function' && !p.startsWith('_')) {
functions.push(p);
} else if (this[p] instanceof automation.Event) {
events.push(p);
}
}
// Con