UNPKG

ids-enterprise

Version:

Infor Design System (IDS) Enterprise Components for the web

1,785 lines (1,510 loc) 2.27 MB
/** * Soho XI Controls v4.7.0-dev * Date: 09/05/2018, 11:57:34 am * Revision: c485f1881e73d64a0ac6fb644bf498cde373ff63 * * * The Soho Xi Component Library (the “Runtime”) is made available to you under this Runtime License. * Infor, Inc. grants you a nonexclusive, non-transferable, non-sublicenseable, and revocable license to use * the Runtime within the Application for the sole purpose of enabling the Runtime’s functionality within the * Application. You may not, and you may not permit anyone else to, copy, modify, combine with other software, * or redistribute the Runtime. You are prohibited from removing, altering, or obscuring the above copyright * notice or this Runtime License. * * Disclaimer. You assume all responsibility and risk of use of the Runtime. The Runtime is provided "AS IS" * without warranty or condition of any kind. INFOR DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, * INCLUDING ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT, * AND ANY WARRANTIES OR CONDITIONS ARISING OUT OF COURSE OF DEALING OR USAGE OF TRADE. Infor is not responsible * or liable (and makes no representation or warranty) for the accuracy, content, * completeness, legality, * reliability, or availability of the Runtime. * * Limitation of Liability. IN NO EVENT WILL INFOR BE LIABLE TO YOU FOR ANY SPECIAL, INCIDENTAL, EXEMPLARY, PUNITIVE OR * CONSEQUENTIAL DAMAGES (INCLUDING LOSS OF USE, DATA, BUSINESS OR PROFITS) ARISING OUT OF OR IN CONNECTION WITH * THIS RUNTIME LICENSE, WHETHER SUCH LIABILITY ARISES FROM ANY CLAIM BASED UPON CONTRACT, WARRANTY, * TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, AND WHETHER OR NOT INFOR HAS BEEN * ADVISED OF THE POSSIBILITY OF SUCH LOSS OR DAMAGE. THE FOREGOING LIMITATIONS * WILL SURVIVE AND * APPLY EVEN IF ANY LIMITED REMEDY SPECIFIED IN THIS AGREEMENT IS FOUND TO HAVE FAILED OF ITS ESSENTIAL PURPOSE. * IN NO EVENT WILL INFOR’S TOTAL LIABILITY ARISING OUT OF OR IN CONNECTION WITH THIS AGREEMENT OR FROM THE USE OF * OR INABILITY TO USE THE RUNTIME EXCEED $100. */ var Soho = (function (exports) { 'use strict'; /* eslint-disable no-console */ // Easy flag for determining whether or not time will be logged to the console. var enableTimeLogging = false; /** * Start the logging timer * @param {string} label Provide a way to match a timing operation. * @returns {void} */ function logTimeStart(label) { if (enableTimeLogging) { console.time(label); } } /** * End the logging timer and print the result * @param {string} label End this matching timing operation * @returns {void} */ function logTimeEnd(label) { if (enableTimeLogging) { console.timeEnd(label); } } // Easy flag for allowing console debugging var enableConsoleLogging = false; /** * Simple wrapper for `console.[whatever]` to abstract out console access. * @param {string} type console display type * @param {string} message message type * @returns {void} */ function log(type, message) { if (!enableConsoleLogging) { return; } if (!console) { // eslint-disable-line return; } if (!message && typeof type === 'string') { message = type; type = 'log'; } if (typeof !console[type] !== 'function') { // eslint-disable-line type = 'log'; } console[type]('' + message); // eslint-disable-line } var debug = Object.freeze({ enableTimeLogging: enableTimeLogging, logTimeStart: logTimeStart, logTimeEnd: logTimeEnd, enableConsoleLogging: enableConsoleLogging, log: log }); var DOM = {}; /** * Returns an array containing an element's attributes. * @param {HTMLElement|SVGElement} element the element whose attributes are being accessed * @returns {object} list of attributes in name/value pairs. */ DOM.getAttributes = function getAttributes(element) { if (!element || !(element instanceof HTMLElement) && !(element instanceof SVGElement)) { return {}; } return element.attributes; }; /** * Adding, removing, and testing for classes * @param {HTMLElement} element the element to test * @returns {boolean} whether or not a className exists */ DOM.classNameExists = function classNameExists(element) { var cn = element.className; return cn && cn.length > 0; }; /** * Checks the contents of a string for the existence of a particular substring. * @param {string} classNameString a string to test * @param {string} targetContents the contents that need to exist inside the `classNameString` * @returns {boolean} whether or not a className exists */ DOM.classNameHas = function has(classNameString, targetContents) { return classNameString.indexOf(targetContents) > -1; }; /** * @param {HTMLElement} el a element being checked. * @param {string} className a string representing a class name to check for. * @returns {boolean} whether or not the element's class attribute contains the string. */ DOM.hasClass = function hasClass(el, className) { return el.classList ? el.classList.contains(className) : new RegExp("\\b" + className + "\\b").test(el.className); }; /** * @param {HTMLElement} el a element being checked. * @param {string} className a string representing a class name. */ DOM.addClass = function addClass(el, className) { if (el.classList) { el.classList.add(className); } else if (!DOM.hasClass(el, className)) { el.className += " " + className; } }; /** * Checks if an element is valid * @param {HTMLElement|SVGElement|jQuery[]} el The element being checked * @returns {boolean} represents all values normally contained by a DOMRect or ClientRect */ DOM.isElement = function isElement(el) { if (el instanceof HTMLElement || el instanceof SVGElement || el instanceof $ && el.length) { return true; } return false; }; /** * Runs the generic _getBoundingClientRect()_ method on an element, but returns its results * as a plain object instead of a ClientRect * @param {HTMLElement|SVGElement|jQuery[]} el The element being manipulated * @returns {object} represents all values normally contained by a DOMRect or ClientRect */ DOM.getDimensions = function getDimensions(el) { if (!DOM.isElement(el)) { return {}; } if (el instanceof $) { if (!el.length) { return {}; } el = el[0]; } var rect = el.getBoundingClientRect(); var rectObj = {}; for (var prop in rect) { // eslint-disable-line if (!isNaN(rect[prop])) { rectObj[prop] = rect[prop]; } } return rectObj; }; /** * HideFocus Behavior * Only shows the focus state on key entry (tabs or arrows). * @param {HTMLElement|SVGElement} element the base element * @returns {HideFocus} component instance */ function HideFocus(element) { return this.init(element); } HideFocus.prototype = { init: function init(element) { if (!this.element && (element instanceof HTMLElement || element instanceof SVGElement)) { this.element = element; } var $el = $(element); var isClick = false; var isFocused = false; var labelClicked = false; // Checkbox, Radio buttons or Switch if ($el.is('.checkbox, .radio, .switch')) { var label = $el.next(); if (label.is('[type="hidden"]')) { label = label.next(); } this.label = label[0]; $el.addClass('hide-focus').on('focusin.hide-focus', function (e) { if (!isClick && !isFocused && !labelClicked) { $el.removeClass('hide-focus'); $el.triggerHandler('hidefocusremove', [e]); } isClick = false; isFocused = true; labelClicked = false; }).on('focusout.hide-focus', function (e) { $el.addClass('hide-focus'); labelClicked = label.is(labelClicked); isClick = false; isFocused = false; $el.triggerHandler('hidefocusadd', [e]); }); label.on('mousedown.hide-focus', function (e) { labelClicked = this; isClick = true; $el.addClass('hide-focus'); $el.triggerHandler('hidefocusadd', [e]); }); } else { // All other elements (ie. Hyperlinks) $el.addClass('hide-focus').on('mousedown.hide-focus touchstart.hide-focus', function (e) { isClick = true; $el.addClass('hide-focus'); $el.triggerHandler('hidefocusadd', [e]); }).on('focusin.hide-focus', function (e) { if (!isClick && !isFocused) { $el.removeClass('hide-focus'); $el.triggerHandler('hidefocusremove', [e]); } isClick = false; isFocused = true; }).on('focusout.hide-focus', function (e) { $el.addClass('hide-focus'); isClick = false; isFocused = false; $el.triggerHandler('hidefocusadd', [e]); }); } return this; }, updated: function updated() { return this.teardown().init(); }, teardown: function teardown() { if (this.label) { $(this.label).off('mousedown.hide-focus'); } var elemEvents = ['focusin.hide-focus', 'focusout.hide-focus', 'mousedown.hide-focus', 'touchstart.hide-focus']; $(this.element).off(elemEvents.join(' ')); return this; } }; /** * jQuery component wrapper for the HideFocus behavior * @returns {jQuery[]} components being acted on */ $.fn.hideFocus = function () { return this.each(function () { var instance = $.data(this, 'hidefocus'); if (instance) { instance.updated(); } else { instance = $.data(this, 'hidefocus', new HideFocus(this)); instance.destroy = function destroy() { this.teardown(); $.removeData(this, 'hidefocus'); }; } }); }; /** * Allows for the smooth scrolling of an element's content area. * @param {HTMLElement|SVGElement|jQuery[]} el The element being manipulated. * @param {number} target target distance. * @param {number} duration the time that will be needed for the scrolling to complete. * @returns {$.Deferred} promise that resolved when scrolling completes. */ function smoothScrollTo(el, target, duration) { var dfd = $.Deferred(); if (!DOM.isElement(el)) { // Not a workable element return dfd.reject(); } // Strip the jQuery if (el instanceof $ && el.length) { el = el[0]; } // undefined (not zero) target should instantly resolve if (target === undefined || target === null) { return dfd.resolve(); } if (isNaN(duration)) { duration = 0; } target = Math.round(target); duration = Math.round(duration); if (duration < 0) { // bad duration return dfd.fail(); } if (duration === 0) { el.scrollLeft += target; return dfd.resolve(); } var startTime = Date.now(); var endTime = startTime + duration; var startLeft = el.scrollLeft; var distance = target; // based on http://en.wikipedia.org/wiki/Smoothstep function smoothStep(start, end, point) { if (point <= start) { return 0; } if (point >= end) { return 1; } var x = (point - start) / (end - start); // interpolation return x * x * (3 - 2 * x); } // This is to keep track of where the element's scrollLeft is // supposed to be, based on what we're doing var previousLeft = el.scrollLeft; // This is like a think function from a game loop function scrollFrame() { if (el.scrollLeft !== previousLeft) { // interrupted dfd.reject(); return; } // set the scrollLeft for this frame var now = Date.now(); var point = smoothStep(startTime, endTime, now); var frameLeft = Math.round(startLeft + distance * point); el.scrollLeft = frameLeft; // check if we're done! if (now >= endTime) { dfd.resolve(); return; } // If we were supposed to scroll but didn't, then we // probably hit the limit, so consider it done; not // interrupted. if (el.scrollLeft === previousLeft && el.scrollLeft !== frameLeft) { dfd.resolve(); return; } previousLeft = el.scrollLeft; // schedule next frame for execution setTimeout(scrollFrame, 0); } // boostrap the animation process setTimeout(scrollFrame, 0); return dfd; } /** * Binds the Soho Behavior _smoothScrollTo()_ to a jQuery selector * @param {number} target target distance to scroll the element * @param {number} duration the time that will be needed for the scrolling to complete. * @returns {$.Deferred} promise that resolved when scrolling completes. */ $.fn.smoothScroll = function (target, duration) { return smoothScrollTo(this, target, duration); }; /** * Uses 'requestAnimationFrame' or 'setTimeout' to defer a function. * @param {function} callback the callback that runs on a deferment. * @param {number} timer how long to delay before running the callback. * @returns {function} either `requestAnimationFrame` or `setTimeout` */ function defer(callback, timer) { var deferMethod = typeof window.requestAnimationFrame !== 'undefined' ? window.requestAnimationFrame : setTimeout; return deferMethod(callback, timer); } var behaviors = Object.freeze({ HideFocus: HideFocus, smoothScrollTo: smoothScrollTo, defer: defer }); var version = "4.7.0-dev"; // ================================================================= // Soho JS-level Breakpoint Access // NOTE: these should match whatever the breakpoints are in "/sass/_config.scss" // ================================================================= var breakpoints = { phone: 320, slim: 400, phablet: 610, 'phone-to-tablet': 767, 'wide-tablet': 968, 'tablet-to-desktop': 1280, desktop: 1024, 'desktop-to-extralarge': 1600 }; /** * Get the name of the current CSS breakpoint by checking the popuplated 'content' value of the * <body> tag's `::after` pseudo-element. These names should be reflected in the breakpoints object * above. * @returns {string} name of the current breakpoint */ breakpoints.current = function () { var afterElement = window.getComputedStyle ? window.getComputedStyle(document.body, ':after') : false; if (!afterElement) { return ''; } return (afterElement.getPropertyValue('content') || '').replace(/"/g, ''); }; /** * @param {string} breakpoint matches one of the entries in the "Soho.breakpoints" object. * @returns {boolean} whether or not the window is currently wider than the breakpoint provided. */ breakpoints.isAbove = function isAbove(breakpoint) { var bp = breakpoints[breakpoint]; if (!bp) { return false; } var windowWidth = $(window).width(); return windowWidth > bp - 1; }; /** * @param {string} breakpoint matches one of the entries in the "Soho.breakpoints" object. * @returns {boolean} whether or not the window is currently more narrow * than the breakpoint provided. */ breakpoints.isBelow = function isBelow(breakpoint) { var bp = breakpoints[breakpoint]; if (!bp) { return false; } var windowWidth = $(window).width(); return windowWidth < bp; }; /** * Compares the last-stored breakpoint with a check on the "current" breakpoint to see if the * breakpoint has changed. * @returns {void} */ breakpoints.compare = function compare() { if (!this.last) { this.last = ''; } var cur = this.current(); if (this.last !== cur) { $('body').triggerHandler('breakpoint-change', [{ previous: this.last, current: cur }]); this.last = cur; } }; /** * Checks an element for Soho visibility classes and determines whether or not * should be hidden based on those values at the current breakpoint. * NOTE: this method does NOT determine if the element is ACTUALLY hidden with a * `display: none;` or `visibility: hidden;` rule. It determines whether or not a CSS * visibility rule alone would hide the element. * @param {HTMLElement} element the element being checked. * @returns {boolean} whether or not the element is hidden at this breakpoint. */ breakpoints.isHidden = function (element) { if (!element || !DOM.isElement(element)) { return false; } // If there are no CSS classes on the element, return false. var cl = element.classList; if (!cl.length) { return false; } // If it's always hidden, always return true. if (cl.contains('hidden')) { return true; } var bp = this.current(); var map = { phonedown: 'xs', phone: 'sm', tablet: 'md', desktop: 'lg', extralarge: 'xl' }; var size = map[bp]; var hiddenClassName = 'hidden-' + size; var visibleClassName = 'visible-' + size + '-'; // Should be hidden on this breakpoint if (cl.contains(hiddenClassName)) { return true; } // If explicitly visible, return if (cl.toString().indexOf(visibleClassName) > -1) { return false; } // Simply return false if none of these thing are found return false; }; /** * jQuery wrapper for `Soho.breakpoints.isHidden()` * NOTE: if a jQuery selector with multiple elements is passed to this function, * it will only operate on the first one. * This method is NOT chainable. * @returns {boolean} whether or not the element is hidden at this breakpoint. */ $.fn.isHiddenAtBreakpoint = function () { if (!this.length) { return false; } return breakpoints.isHidden($(this).first()[0]); }; /** * Debounce method * @param {function} func the callback function to be run on a stagger. * @param {number} [threshold] the amount of time in CPU ticks to delay. * @param {boolean} [execAsap] if true, executes the callback immediately * instead of waiting for the threshold to complete. * @returns {void} */ function debounce(func, threshold, execAsap) { var timeout = void 0; return function debounced() { for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } var obj = this; function delayed() { if (!execAsap) { func.apply(obj, args); } timeout = null; } if (timeout) { clearTimeout(timeout); } else if (execAsap) { func.apply(obj, args); } timeout = setTimeout(delayed, threshold || 250); }; } var debouncedResizeName = 'debouncedResize'; /** * Bind the smartResize method to $.fn() * @param {function} fn the callback function to be bound on debounced resize * @returns {void} */ $.fn[debouncedResizeName] = function (fn) { if (fn) { return this.bind('resize', debounce(fn)); } return this.trigger(debouncedResizeName); }; // jQuery Components /** * @class {Environment} */ var Environment = { browser: {}, os: {}, rtl: $('html').attr('dir') === 'rtl', /** * Builds run-time environment settings */ set: function set() { $('html').attr('data-sohoxi-version', version); this.addBrowserClasses(); this.addGlobalResize(); }, /** * Global Classes for browser, version and device as needed. */ addBrowserClasses: function addBrowserClasses() { var ua = navigator.userAgent || navigator.vendor || window.opera; var html = $('html'); var cssClasses = ''; // User-agent string if (ua.indexOf('Safari') !== -1 && ua.indexOf('Chrome') === -1 && ua.indexOf('Android') === -1) { cssClasses += 'is-safari '; this.browser.name = 'safari'; } if (ua.indexOf('Chrome') !== -1) { cssClasses += 'is-chrome '; this.browser.name = 'chrome'; } if (ua.indexOf('Mac OS X') !== -1) { cssClasses += 'is-mac '; this.os.name = 'Mac OS X'; } if (ua.indexOf('Firefox') > 0) { cssClasses += 'is-firefox '; this.browser.name = 'firefox'; } // Class-based detection for IE if (ua.match(/Edge\//)) { cssClasses += 'ie ie-edge '; this.browser.name = 'edge'; } if (ua.match(/Trident/)) { cssClasses += 'ie '; this.browser.name = 'ie'; } if (navigator.appVersion.indexOf('MSIE 8.0') > -1 || ua.indexOf('MSIE 8.0') > -1 || document.documentMode === 8) { cssClasses += 'ie8 '; this.browser.version = '8'; } if (navigator.appVersion.indexOf('MSIE 9.0') > -1) { cssClasses += 'ie9 '; this.browser.version = '9'; } if (navigator.appVersion.indexOf('MSIE 10.0') > -1) { cssClasses += 'ie10 '; this.browser.version = '10'; } else if (ua.match(/Trident\/7\./)) { cssClasses += 'ie11 '; this.browser.version = '11'; } // Class-based detection for iOS // /iPhone|iPod|iPad|Silk|Android|BlackBerry|Opera Mini|IEMobile/ if (/iPhone|iPod|iPad/.test(ua)) { cssClasses += 'ios '; this.os.name = 'ios'; var iDevices = ['iPod', 'iPad', 'iPhone']; for (var i = 0; i < iDevices.length; i++) { if (new RegExp(iDevices[i]).test(ua)) { cssClasses += iDevices[i].toLowerCase() + ' '; this.device = iDevices[i]; } } } if (/Android/.test(ua)) { cssClasses += 'android '; this.os.name = 'android'; } html.addClass(cssClasses); }, /** * Setup a global resize event trigger for controls to listen to */ addGlobalResize: function addGlobalResize() { // Global resize event $(window).debouncedResize(function () { $('body').triggerHandler('resize', [window]); breakpoints.compare(); }); // Also detect whenenver a load or orientation change occurs $(window).on('orientationchange load', function () { return breakpoints.compare(); }); } }; /** * Automatically set up the environment by virtue of including this script */ Environment.set(); var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var toConsumableArray = function (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); } }; /** * Used for changing the stacking order of jQuery events. This is needed to override certain * Events invoked by other plugins http://stackoverflow.com/questions/2360655 * @private * @param {string} name the event name * @param {function} fn callback function that will be called during the supplied event name * @returns {void} */ $.fn.bindFirst = function (name, fn) { this.on(name, fn); this.each(function () { var handlers = $._data(this, 'events')[name.split('.')[0]]; // eslint-disable-line // take out the handler we just inserted from the end var handler = handlers.pop(); // move it at the beginning handlers.splice(0, 0, handler); }); }; /** * @private * uniqueIdCount is a baseline unique number that will be used when generating * uniqueIds for elements and components. */ exports.uniqueIdCount = 0; // eslint-disable-line /** * Generates a unique ID for an element based on the element's configuration, any * Soho components that are generated against it, and provided prefixes/suffixes. * @private * @param {string} [className] CSS classname (will be interpreted automatically * if it's not provided) * @param {string} [prefix] optional prefix * @param {string} [suffix] optional suffix * @returns {string} the compiled uniqueID */ $.fn.uniqueId = function (className, prefix, suffix) { var predefinedId = $(this).attr('id'); if (predefinedId && $('#' + predefinedId).length < 2) { return predefinedId; } prefix = !prefix ? '' : prefix + '-'; suffix = !suffix ? '' : '-' + suffix; className = !className ? $(this).attr('class') : className; var str = prefix + className + exports.uniqueIdCount + suffix; exports.uniqueIdCount += 1; return str; }; /** * Detect whether or not a text string represents a valid CSS property. This check * includes an attempt at checking for vendor-prefixed versions of the CSS property * provided. * @private * @param {string} prop a possible CSS property * @returns {string|null} If the property exists, it will be returned in string format. * If the property doesn't exist, a null result is returned. */ $.fn.cssPropSupport = function (prop) { if (!prop) { return null; } var el = $('<div></div>')[0]; var propStr = prop.toString(); var prefixes = ['Moz', 'Webkit', 'O', 'ms']; var capitalizedProp = propStr.charAt(0).toUpperCase() + propStr.substr(1); if (prop in el.style) { $(el).remove(); return prop; } for (var i = 0; i < prefixes.length; i++) { var vendorProp = prefixes[i] + capitalizedProp; if (vendorProp in el.style) { $(el).remove(); return vendorProp; } } $(el).remove(); return null; }; /** * Returns the name of the TransitionEnd event. * @private * @returns {string} a (possibly) vendor-adjusted CSS transition property name. */ $.fn.transitionEndName = function () { var prop = $.fn.cssPropSupport('transition'); var eventNames = { WebkitTransition: 'webkitTransitionEnd', MozTransition: 'transitionend', MSTransition: 'msTransitionEnd', OTransition: 'oTransitionEnd', transition: 'transitionend' }; return eventNames[prop] || null; }; /** * Checks to see if a provided element is visible based on its CSS `visibility` property. * @private * @param {HTMLElement} element the element being checked. * @returns {boolean} whether or not the element is visible. */ function visible(element) { return $.expr.filters.visible(element) && !$(element).parents().addBack().filter(function () { return $.css(this, 'visibility') === 'hidden'; }).length; } /** * From jQueryUI Core: https://github.com/jquery/jquery-ui/blob/24756a978a977d7abbef5e5bce403837a01d964f/ui/jquery.ui.core.js#L93 * Adapted from: http://stackoverflow.com/questions/7668525/is-there-a-jquery-selector-to-get-all-elements-that-can-get-focus * Adds the ':focusable' selector to Sizzle to allow for the selection of elements * that can currently be focused. * @private * @param {HTMLElement} element the element being checked * @returns {boolean} whether or not the element is focusable. */ function _focusable(element) { var map = void 0; var mapName = void 0; var img = void 0; var nodeName = element.nodeName.toLowerCase(); var isTabIndexNotNaN = !isNaN($.attr(element, 'tabindex')); if (nodeName === 'area') { map = element.parentNode; mapName = map.name; if (!element.href || !mapName || map.nodeName.toLowerCase() !== 'map') { return false; } img = $('img[usemap=#' + mapName + ']')[0]; return !!img && visible(img); } // The element and all of its ancestors must be visible. // Return out fast if this isn't the case. if (!visible(element)) { return false; } var match = /input|select|textarea|button|object/.test(nodeName); if (match) { return !element.disabled; } if (nodeName === 'a') { return element.href !== undefined || isTabIndexNotNaN; } return isTabIndexNotNaN; } // Adds a `:focusable` selector to jQuery's selector library. $.extend($.expr[':'], { focusable: function focusable(element) { return _focusable(element, !isNaN($.attr(element, 'tabindex'))); } }); /** * Returns a key/value list of currently attached event listeners * @private * @returns {object} containing list of event names as keys, and event listener functions as values. */ $.fn.listEvents = function () { var data = {}; this.each(function () { data = $._data(this, 'events'); // eslint-disable-line }); return data; }; var utils = {}; /** * Grabs an attribute from an HTMLElement containing stringified JSON syntax, * and interprets it into options. * @private * @param {HTMLElement} element the element whose settings are being interpreted * @param {string} [attr] optional different attribute to parse for settings * @returns {object} a list of interpreted settings for this element */ utils.parseSettings = function parseSettings(element, attr) { var options = {}; if (!element || !(element instanceof HTMLElement) && !(element instanceof $) || element instanceof $ && !element.length) { return options; } if (element instanceof $) { element = element[0]; } // Use `data-options` as a default. attr = attr || 'data-options'; var str = element.getAttribute(attr); if (!str || typeof str !== 'string' || str.indexOf('{') === -1) { return options; } // replace single to double quotes, since single-quotes may be necessary // due to entry in markup. function replaceDoubleQuotes(changedStr) { return changedStr.replace(/'/g, '"'); } // Manually parse a string more in-depth function manualParse(changedStr) { // get keys var regex = /({|,)(?:\s*)(?:')?([A-Za-z_$\.][A-Za-z0-9_ \-\.$]*)(?:')?(?:\s*):/g; // eslint-disable-line // add double quotes to keys changedStr = changedStr.replace(regex, '$1\"$2\":'); // eslint-disable-line // get strings in values regex = /:(?:\s*)(?!(true|false|null|undefined))([A-Za-z_$\.#][A-Za-z0-9_ \-\.$]*)/g; // eslint-disable-line // add double quotes to strings in values changedStr = changedStr.replace(regex, ':\"$2\"'); // eslint-disable-line changedStr = replaceDoubleQuotes(changedStr); return changedStr; } try { options = JSON.parse(replaceDoubleQuotes(str)); } catch (err) { options = JSON.parse(manualParse(str)); } return options; }; /** * Deprecate `utils.parseOptions` in favor of `utils.parseSettings` * @private * @deprecated * TODO: Remove in 4.4.1 ? */ utils.parseOptions = utils.parseSettings; /** * jQuery Behavior Wrapper for `utils.parseOptions`. * @deprecated * @private * @param {HTMLElement|jQuery[]} element the element whose options are being parsed * @param {string} [attr] an optional alternate attribute name to use when obtaining settings * @returns {Object|Object[]} an object representation of parsed settings. */ $.fn.parseOptions = function (element, attr) { var results = []; var isCalledDirectly = element instanceof HTMLElement || element instanceof SVGElement || element instanceof $; var targets = this; if (isCalledDirectly) { targets = $(element); } else { attr = element; element = undefined; } targets.each(function (i, item) { results.push({ element: this, options: utils.parseOptions(item, attr) }); }); if (results.length === 1) { return results[0].options; } return results; }; /** * Timer - can be used for play/pause or stop for given time. * Use as new instance [ var timer = new $.fn.timer(function() {}, 6000); ] * then can be listen events as: * [ $(timer.event).on('update', function(e, data){console.log(data.counter)}); ] * or can access as [ timer.cancel(); -or- timer.pause(); -or- timer.resume(); ] * @private * @param {function} [callback] method that will run on each timer update * @param {number} delay amount of time between timer ticks * @returns {object} containing methods that can be run on the timer */ $.fn.timer = function (callback, delay) { var self = $(this); var speed = 10; var interval = void 0; var counter = 0; function cancel() { self.triggerHandler('cancel'); clearInterval(interval); counter = 0; } function pause() { self.triggerHandler('pause'); clearInterval(interval); } function update() { interval = setInterval(function () { counter += speed; self.triggerHandler('update', [{ counter: counter }]); if (counter > delay) { self.triggerHandler('timeout'); callback.apply(arguments); // eslint-disable-line clearInterval(interval); counter = 0; } }, speed); } function resume() { self.triggerHandler('resume'); update(); } update(); return { event: this, cancel: cancel, pause: pause, resume: resume }; }; /** * Copies a string to the clipboard. Must be called from within an event handler such as click. * May return false if it failed, but this is not always * possible. Browser support for Chrome 43+, Firefox 42+, Edge and IE 10+. * No Safari support, as of (Nov. 2015). Returns false. * IE: The clipboard feature may be disabled by an adminstrator. By default a prompt is * shown the first time the clipboard is used (per session). * @private * @param {string} text incoming text content * @returns {string|boolean} copied text, or a false result if there was an error */ $.copyToClipboard = function (text) { // eslint-disable-line if (window.clipboardData && window.clipboardData.setData) { // IE specific code path to prevent textarea being shown while dialog is visible. return window.clipboardData.setData('Text', text); } else if (document.queryCommandSupported && document.queryCommandSupported('copy')) { var textarea = document.createElement('textarea'); textarea.textContent = text; textarea.style.position = 'fixed'; // Prevent scrolling to bottom of page in MS Edge. document.body.appendChild(textarea); textarea.select(); try { return document.execCommand('copy'); // Security exception may be thrown by some browsers. } catch (ex) { // console.warn('Copy to clipboard failed.', ex); return false; } finally { document.body.removeChild(textarea); } } }; /** * Escapes HTML, replacing special characters with encoded symbols. * @private * @param {string} value HTML in string form * @returns {string} the modified value */ $.escapeHTML = function (value) { var newValue = value; if (typeof value === 'string') { newValue = newValue.replace(/&/g, '&amp;'); newValue = newValue.replace(/</g, '&lt;').replace(/>/g, '&gt;'); } return newValue; }; /** * Un-escapes HTML, replacing encoded symbols with special characters. * @private * @param {string} value HTML in string form * @returns {string} the modified value */ $.unescapeHTML = function (value) { var newValue = value; if (typeof value === 'string') { newValue = newValue.replace(/&lt;/g, '<').replace(/&gt;/g, '>'); newValue = newValue.replace(/&amp;/g, '&'); } return newValue; }; /** * Remove Script tags and all onXXX functions * @private * @param {string} html HTML in string form * @returns {string} the modified value */ $.sanitizeHTML = function (html) { var santizedHtml = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/g, ''); santizedHtml = santizedHtml.replace(/<[^>]+/g, function (match) { return match.replace(/(\/|\s)on\w+=(\'|")?[^"]*(\'|")?/g, ''); }); // eslint-disable-line return santizedHtml; }; /** * Clearable (Shows an X to clear) * @private */ $.fn.clearable = function () { var self = this; this.element = $(this); // Create an X icon button styles in icons.scss this.xButton = $.createIconElement({ classes: 'close is-empty', icon: 'close' }).icon(); // Create a function this.checkContents = function () { var text = self.element.val(); if (!text || !text.length) { this.xButton.addClass('is-empty'); } else { this.xButton.removeClass('is-empty'); } this.element.trigger('contents-checked'); }; // Add the button to field parent this.xButton.insertAfter(self.element); // Handle Events this.xButton.off().on('click.clearable', function () { self.element.val('').trigger('change').focus().trigger('cleared'); self.checkContents(); }); this.element.on('change.clearable, blur.clearable, keyup.clearable', function () { self.checkContents(); }); // Set initial state this.checkContents(); }; /** * Replacement for String.fromCharCode() that takes meta keys into account when determining which * @private * character key was pressed. * @param {jQuery.Event} e jQuery-wrapped `keypress` event * @returns {string} text tcharacter */ utils.actualChar = function (e) { var key = e.which; var character = ''; var toAscii = { 188: '44', // '109': '45', // changes "m" to "-" when using keypress 190: '46', 191: '47', 192: '96', 220: '92', 222: '39', 221: '93', 219: '91', 173: '45', 187: '61', // IE Key codes 186: '59', // IE Key codes 189: '45' // IE Key codes }; var shiftUps = { 96: '~', 49: '!', 50: '@', 51: '#', 52: '$', 53: '%', 54: '^', 55: '&', 56: '*', 57: '(', 48: ')', 45: '_', 61: '+', 91: '{', 93: '}', 92: '|', 59: ':', 37: '%', 38: '&', 39: '"', 44: '<', 46: '>', 47: '?' }; // Normalize weird keycodes if (Object.prototype.hasOwnProperty.call(toAscii, key)) { key = toAscii[key]; } // Handle Numpad keys if (key >= 96 && key <= 105) { key -= 48; } // Convert Keycode to Character String if (!e.shiftKey && key >= 65 && key <= 90) { character = String.fromCharCode(key + 32); } else if (e.shiftKey && Object.prototype.hasOwnProperty.call(shiftUps, key)) { // User was pressing Shift + any key character = shiftUps[key]; } else { character = String.fromCharCode(key); } return character; }; /** * Get the actualy typed key from the event. * @private * @param {object} e The event to check for the key. * @returns {string} The actual key typed. */ $.actualChar = function (e) { return utils.actualChar(e); }; /** * Equate two values quickly in a truthy fashion * @private * @param {any} a first value * @param {any} b second value * @returns {boolean} whether the two items compare in a truthy fashion. */ utils.equals = function equals(a, b) { return JSON.stringify(a) === JSON.stringify(b); }; /** * Converts an element wrapped in a jQuery collection down to its original HTMLElement reference. * If an HTMLElement is passed in, simply returns it. * If anything besides HTMLElements or jQuery[] is passed in, returns undefined; * @private * @param {any} item the item being evaluated * @returns {HTMLElement|undefined} the unwrapped item, or nothing. */ DOM.convertToHTMLElement = function convertToHTMLElement(item) { if (item instanceof HTMLElement) { return item; } if (item instanceof $) { if (item.length) { item = item[0]; } else { item = undefined; } return item; } return undefined; }; /** * Object deep copy. * For now, alias jQuery.extend * Eventually we'll replace this with a non-jQuery extend method. * @private */ utils.extend = $.extend; /** * Hack for IE11 and SVGs that get moved around/appended at inconvenient times. * The action of changing the xlink:href attribute to something else and back will fix the problem. * @private * @param {HTMLElement} rootElement the base element * @returns {void} */ utils.fixSVGIcons = function fixSVGIcons(rootElement) { if (Environment.browser.name !== 'ie' && Environment.browser.version !== '11') { return; } if (rootElement === undefined) { return; } if (rootElement instanceof $) { if (!rootElement.length) { return; } rootElement = rootElement[0]; } setTimeout(function () { var uses = rootElement.getElementsByTagName('use'); for (var i = 0; i < uses.length; i++) { var attr = uses[i].getAttribute('xlink:href'); uses[i].setAttribute('xlink:href', 'x'); uses[i].setAttribute('xlink:href', attr); } }, 1); }; /** * Gets the current size of the viewport * @private * @returns {object} width/height of the viewport */ utils.getViewportSize = function getViewportSize() { return { width: Math.max(document.documentElement.clientWidth, window.innerWidth || 0), height: Math.max(document.documentElement.clientHeight, window.innerHeight || 0) }; }; /** * Gets the various scrollable containers that an element is nested inside of, and returns * their scrollHeight and scrollLeft values. * @private * @param {HTMLElement} element the base element to check for containment * @returns {object} containing references to the container element and its top/left */ utils.getContainerScrollDistance = function getContainerScrollDistance(element) { if (!DOM.isElement(element)) { return []; } var containers = []; var scrollableElements = ['.scrollable', '.scrollable-x', '.scrollable-y', '.modal', '.card-content', '.widget-content', '.tab-panel', '.datagrid-content']; $(element).parents(scrollableElements.join(', ')).each(function () { var el = this; containers.push({ element: el, left: el.scrollLeft, top: el.scrollTop }); }); // Push the body's scroll area if it's not a "no-scroll" area if (!document.body.classList.contains('no-scroll')) { containers.push({ element: document.body, left: document.body.scrollLeft, top: document.body.scrollTop }); } return containers; }; /** * Takes an element that is currently hidden by some means (FX: "display: none;") * and gets its potential dimensions by checking a clone of the element that is NOT hidden. * @private * @param {HTMLElement|SVGElement|jQuery[]} el The element being manipulated. * @param {object} options incoming options. * @param {jQuery[]} [parentElement] the parent element where a clone of this * hidden element will be attached. * @returns {object} containing various width/height properties of the element provided. */ utils.getHiddenSize = function getHiddenSize(el, options) { var defaults$$1 = { dims: { width: 0, height: 0, innerWidth: 0, innerHeight: 0, outerWidth: 0, outerHeight: 0 }, parentElement: undefined, includeMargin: false }; if (!DOM.isElement(el)) { return defaults$$1.dims; } el = $(el); options = $.extend({}, defaults$$1, options); // element becomes clone and appended to a parentElement, if defined var hasDefinedParentElement = DOM.isElement(options.parentElement); if (hasDefinedParentElement) { el = el.clone().appendTo(options.parentElement); } var dims = options.dims; var hiddenParents = el.parents().add(el); var props = { transition: 'none', webkitTransition: 'none', mozTransition: 'none', msTransition: 'none', visibility: 'hidden', display: 'block' }; var oldProps = []; hiddenParents.each(function () { var _this = this; var old = {}; var propTypes = Object.keys(props); propTypes.forEach(function (name) { if (_this.style[name]) { old[name] = _this.style[name]; _this.style[name] = props[name]; } }); oldProps.push(old); }); dims.padding = { bottom: el.css('padding-bottom'), left: el.css('padding-left'), right: el.css('padding-right'), top: el.css('padding-top') }; dims.width = el.width(); dims.outerWidth = el.outerWidth(options.includeMargin); dims.innerWidth = el.innerWidth(); dims.scrollWidth = el[0].scrollWidth; dims.height = el.height(); dims.innerHeight = el.innerHeight(); dims.outerHeight = el.outerHeight(options.includeMargin); dims.scrollHeight = el[0].scrollHeight; hiddenParents.each(function (i) { var _this2 = this; var old = oldProps[i]; var propTypes = Object.keys(props); propTypes.forEach(function (name) { if (old[name]) { _this2.style[name] = old[name]; } }); }); // element is ONLY removed when a parentElement is defined because it was cloned. if (hasDefinedParentElement) { el.remove(); } return dims; }; /** * Binds the Soho Util _getHiddenSize()_ to a jQuery selector * @private * @param {object} options - incoming options * @returns {object} hidden size */ $.fn.getHiddenSize = function (options) { return utils.getHiddenSize(this, options); }; /** * Checks if a specific input is a String * @private * @param {any} value an object of unknown type to check * @returns {boolean} whether or not a specific input is a String */ utils.isString = function isString(value) { return typeof value === 'string' || value instanceof String; }; /** * Checks if a specific input is a Number * @private * @param {any} value an object of unknown type to check * @returns {boolean} whether or not a specific input is a Number */ utils.isNumber = function isNumber(value) { return typeof value === 'number' && value.length === undefined && !isNaN(value); }; /** * Safely changes the position of a text caret inside of an editable element. * In most cases, will call "setSelectionRange" on an editable element immediately, but in some * cases, will be deferred with `requestAnimationFrame` or `setTimeout`. * @private * @param {HTMLElement} element the element to get selection * @param {number} startPos starting position of the text caret * @param {number} endPos ending position of the text caret */ utils.safeSetSelection = function safeSetSelection(element, startPos, endPos) { if (startPos && endPos === undefined) { endPos = startPos; } if (document.activeElement === element) { if (Environment.os.name === 'android') { defer(function () { element.setSelectionRange(startPos, endPos, 'none'); }, 0); } else { element.setSelectionRange(startPos, endPos, 'none'); } } }; /** * Checks to see if a variable is valid for containing Soho component options. * @private * @param {object|function} o an object or function * @returns {boolean} whether or not the object type is valid */ function isValidOptions(o) { return (typeof o === 'undefined' ? 'undefined' : _typeof(o)) === 'object' || typeof o === 'function'; } /** * In some cases, functions are passed to component constructors as the settings argument. * This method runs the settings function if it's present and returns the resulting object. * @private * @param {object|function} o represents settings * @returns {object} processed settings */ function resolveFunctionBasedSettings(o) { if (typeof o === 'function') { return o(); } return o; } /** * Merges various sets of options into a single object, * whose intention is to be set as options on a Soho component. * @private * @param {HTMLElement|SVGElement|jQuery[]} [element] the element to process for inline-settings * @param {Object|function} incomingOptions desired settings * @param {Object|function} [defaultOptions] optional base settings * @returns {object} processed settings */ utils.mergeSettings = function mergeSettings(element, incomingOptions, defaultOptions) { if (!incomingOptions || !isValidOptions(incomingOptions)) { if (isValidOptions(defaultOptions)) { incomingOptions = defaultOptions; } else { incomingOptions = {}; } } // Actually get ready to merge incoming options if we get to this point. return utils.extend(true, {}, resolveFunctionBasedSettings(defaultOptions || {}), resolveFunctionBasedSettings(incomingOptions), element !== undefined ? utils.parseSettings(element) : {}); // possible to run this without an element present -- will simply skip this part }; /** * Test if a string is Html or not * @private * @param {string} string The string to test. * @returns {boolean} True if it is html. */ utils.isHTML = function (string) { return (/(<([^>]+)>)/i.test(string) ); }; var math = {}; /** * Convert `setTimeout/Interval` delay values (CPU ticks) into frames-per-second * (FPS) numeric values. * @private * @param {number} delay CPU Ticks * @returns {number} Frames Per Second */ math.convertDelayToFPS = function convertDelayToFPS(delay) { if (isNaN(delay)) { throw new Error('provided delay value is not a number'); } return delay / 16.7; }; /** * Convert `setTimeout/Interval` delay values (CPU ticks) into frames-per-second * (FPS) numeric values. * @private * @param {number} fps (Frames Per Second) * @returns {number} delay in CPU ticks */ math.convertFPSToDelay = function convertFPSToDelay(fps) { if (isNaN(fps)) { throw new Error('provided delay value is not a number'); } return fps * 16.7; }; /** * Determines whether the passed value is a finite number. * @private * @param {number} value The number * @returns {boolean} If it is finite or not. */ math.isFinite = function isFinite(value) { // 1. If Type(number) is not Number, return false. if (typeof value !== 'number') { return false; } // 2. If number is NaN, +∞, or −∞, return false. if (value !== value || value === Infinity || value === -Infinity) { //eslint-disable-line return false; } // 3. Otherwise, return true. return true; }; /** * `Array.ForEach()`-style method that is also friendly to `NodeList` types. * @param {Array|NodeList} array incoming items * @param {function} callback the method to run * @param {object} scope the context in which to run the method */ utils.forEach = function forEach(array, callback, scope) { for (var i = 0; i < array.length; i++) { callback.call(scope, array[i], i, array); // passes back stuff we need } }; /* eslint-disable no-nested-ternary, no-useless-escape */ // If `SohoConfig` exists with a `culturesPath` property, use that path for retrieving // culture files. This allows manually setting the directory for the culture files. var existingCulturePath = ''; if (_typeof(window.SohoConfig) === 'object' && typeof window.SohoConfig.culturesPath === 'string') { existingCulturePath = window.SohoConfig.culturesPath; } /** * The Locale component handles i18n * Data From: http://www.unicode.org/repos/cldr-aux/json/22.1/main/ * For Docs See: http://ibm.co/1nXyNxp * @class Locale * @constructor * * @param {string} currentLocale The Currently Set Locale * @param {object} cultures Contains all currently-stored cultures. * @param {string} culturesPath the web-server's path to culture files. */ var Locale = { // eslint-disable-line currentLocale: { name: '', data: {} }, // default cultures: {}, culturesPath: existingCulturePath, /** * Sets the