ids-enterprise
Version:
Infor Design System (IDS) Enterprise Components for the web
1,785 lines (1,510 loc) • 2.27 MB
JavaScript
/**
* 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, '&');
newValue = newValue.replace(/</g, '<').replace(/>/g, '>');
}
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(/</g, '<').replace(/>/g, '>');
newValue = newValue.replace(/&/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