ids-enterprise
Version:
Infor Design System (IDS) Enterprise Components for the web
944 lines (834 loc) • 27 kB
JavaScript
import { defer } from './behaviors';
import { Environment as env } from './environment';
import { DOM } from './dom';
/**
* 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 () {
const handlers = $._data(this, 'events')[name.split('.')[0]]; // eslint-disable-line
// take out the handler we just inserted from the end
const 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.
*/
export let 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) {
const predefinedId = $(this).attr('id');
if (predefinedId && $(`#${predefinedId}`).length < 2) {
return predefinedId;
}
prefix = (!prefix ? '' : `${prefix}-`);
suffix = (!suffix ? '' : `-${suffix}`);
className = (!className ? $(this).attr('class') : className);
const str = prefix + className + uniqueIdCount + suffix;
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;
}
const el = $('<div></div>')[0];
const propStr = prop.toString();
const prefixes = ['Moz', 'Webkit', 'O', 'ms'];
const capitalizedProp = propStr.charAt(0).toUpperCase() + propStr.substr(1);
if (prop in el.style) {
$(el).remove();
return prop;
}
for (let i = 0; i < prefixes.length; i++) {
const 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 () {
const prop = $.fn.cssPropSupport('transition');
const 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) {
let map;
let mapName;
let img;
const nodeName = element.nodeName.toLowerCase();
const 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;
}
const 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(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 () {
let data = {};
this.each(function () {
data = $._data(this, 'events'); // eslint-disable-line
});
return data;
};
const 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) {
let 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';
const 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
let 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) {
const results = [];
const isCalledDirectly = (element instanceof HTMLElement ||
element instanceof SVGElement || element instanceof $);
let 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) {
const self = $(this);
const speed = 10;
let interval;
let 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 }]);
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,
pause,
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')) {
const 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) {
let 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) {
let 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) {
let santizedHtml = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/g, '');
santizedHtml = santizedHtml.replace(/<[^>]+/g, match => match.replace(/(\/|\s)on\w+=(\'|")?[^"]*(\'|")?/g, '')); // eslint-disable-line
return santizedHtml;
};
/**
* Clearable (Shows an X to clear)
* @private
*/
$.fn.clearable = function () {
const 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 () {
const 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', () => {
self.element.val('').trigger('change').focus().trigger('cleared');
self.checkContents();
});
this.element.on('change.clearable, blur.clearable, keyup.clearable', () => {
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) {
let key = e.which;
let character = '';
const 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
};
const 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 (env.browser.name !== 'ie' && env.browser.version !== '11') {
return;
}
if (rootElement === undefined) {
return;
}
if (rootElement instanceof $) {
if (!rootElement.length) {
return;
}
rootElement = rootElement[0];
}
setTimeout(() => {
const uses = rootElement.getElementsByTagName('use');
for (let i = 0; i < uses.length; i++) {
const 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 [];
}
const containers = [];
const scrollableElements = [
'.scrollable', '.scrollable-x', '.scrollable-y', '.modal',
'.card-content', '.widget-content', '.tab-panel',
'.datagrid-content'
];
$(element).parents(scrollableElements.join(', ')).each(function () {
const 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) {
const defaults = {
dims: { width: 0, height: 0, innerWidth: 0, innerHeight: 0, outerWidth: 0, outerHeight: 0 },
parentElement: undefined,
includeMargin: false
};
if (!DOM.isElement(el)) {
return defaults.dims;
}
el = $(el);
options = $.extend({}, defaults, options);
// element becomes clone and appended to a parentElement, if defined
const hasDefinedParentElement = DOM.isElement(options.parentElement);
if (hasDefinedParentElement) {
el = el.clone().appendTo(options.parentElement);
}
const dims = options.dims;
const hiddenParents = el.parents().add(el);
const props = {
transition: 'none',
webkitTransition: 'none',
mozTransition: 'none',
msTransition: 'none',
visibility: 'hidden',
display: 'block',
};
const oldProps = [];
hiddenParents.each(function () {
const old = {};
const propTypes = Object.keys(props);
propTypes.forEach((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) {
const old = oldProps[i];
const propTypes = Object.keys(props);
propTypes.forEach((name) => {
if (old[name]) {
this.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 (env.os.name === 'android') {
defer(() => {
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 === '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);
};
const 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 (let i = 0; i < array.length; i++) {
callback.call(scope, array[i], i, array); // passes back stuff we need
}
};
export { utils, math };