UNPKG

@elliemae/em-ssf-dom

Version:

ICE Secure Scripting Framework DOM Controls

1,362 lines (1,135 loc) 61.8 kB
(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