@eclipse-scout/core
Version:
Eclipse Scout runtime
1,616 lines (1,410 loc) • 43.9 kB
JavaScript
/*
* Copyright (c) 2010, 2024 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
/**
* jQuery plugin with scout extensions
*/
import $ from 'jquery';
import {aria, arrays, Device, Dimension, events, IconDesc, icons, objects, Resizable, scout, strings} from '../index';
// === internal methods ===
/**
* Returns false when the component display is 'none', otherwise true.
*
* Note: this gives other results than $.is(':visible'), since that method will also return false
* when a component has absolute positioning and no width and height is defined (well, you cannot
* see a component with a style like this, but technically it is not set to 'not visible').
*
* Also note that this function _only_ checks the 'display' property! Other methods to make an element
* invisible to the user ('visibility: hidden', 'opacity: 0', off-screen position etc.) are _not_
* considered.
*/
function elemVisible(elem) {
// Check if element itself is hidden by its own style attribute
if (!elem || isHidden(elem.style)) {
return false;
}
// Must use correct window for element / computedStyle
let myWindow = (elem instanceof Document ? elem : elem.ownerDocument).defaultView;
// In some cases with popup windows the window object may be already set to null
// but we still have a valid reference to a DOM element. In that case we assume
// the element is not visible anymore.
if (!myWindow) {
return false;
}
// Check if element itself is hidden by external style-sheet
if (isHidden(myWindow.getComputedStyle(elem))) {
return false;
}
// Else visible
return true;
// ----- Helper functions -----
function isHidden(style) {
return style.display === 'none';
}
}
function explodeShorthandProperties(properties) {
let newProperties = [];
properties.forEach(prop => {
// shorthand css properties may not be copied directly (at least not in firefox) -> copy the actual properties
if (prop === 'margin' || prop === 'padding') {
newProperties.push(prop + '-top');
newProperties.push(prop + '-right');
newProperties.push(prop + '-bottom');
newProperties.push(prop + '-left');
} else if (prop === 'border') {
newProperties.push('border-top-style');
newProperties.push('border-right-style');
newProperties.push('border-bottom-style');
newProperties.push('border-left-style');
newProperties.push('border-top-color');
newProperties.push('border-right-color');
newProperties.push('border-bottom-color');
newProperties.push('border-left-color');
newProperties.push('border-top-width');
newProperties.push('border-right-width');
newProperties.push('border-bottom-width');
newProperties.push('border-left-width');
} else {
newProperties.push(prop);
}
});
return newProperties;
}
/**
* This function is required because most jQuery functions can be used with or without arguments
* and do return the jQuery instance when used as a setter (with arguments), ceiling should only
* be done, when used as getter (without arguments).
*/
function _ceilNumber(val) {
return objects.isNumber(val) ? Math.ceil(val) : val;
}
// === $ extensions ===
/* !
* jQuery UI Widget 1.11.2
* http://jqueryui.com
*
* Copyright 2014 jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*
* http://api.jqueryui.com/jQuery.widget/
*/
const __origCleanData = $.cleanData;
/**
* This function is copied from jQuery UI. It is used to fire a 'remove' event
* when we call the .remove() function on a jQuery object.
*/
$.cleanData = elems => {
let events, elem, i;
for (i = 0; (elem = elems[i]); i++) { // NOSONAR
try {
// Only trigger remove when necessary to save time
events = $._data(elem, 'events');
if (events && events.remove) {
$(elem).triggerHandler('remove');
}
// http://bugs.jquery.com/ticket/8235
} catch (e) {
// NOP
}
}
__origCleanData(elems);
};
$.ensure = $elem => {
if ($elem instanceof $) {
return $elem;
}
return $($elem);
};
$.suppressEvent = event => {
if (event) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
}
};
$.debounce = (fx, options) => {
if (typeof options === 'number') {
options = {
delay: options
};
}
options = $.extend({
delay: 250,
reschedule: true
}, options);
let timeoutId = null;
let fn = function(...args) {
let that = this;
if (timeoutId && !options.reschedule) {
// Function is already schedule but 'reschedule' option is set to false --> discard this request
return;
}
if (timeoutId) {
// Function is already scheduled --> cancel current scheduled call and re-schedule the call
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
timeoutId = null;
fx.apply(that, args);
}, options.delay);
};
fn.cancel = () => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
return true;
}
return false;
};
return fn;
};
$.throttle = (fx, delay) => {
delay = (delay !== undefined) ? delay : 250;
let timeoutId = null;
let lastExecution;
return function(...args) {
let that = this;
let now = new Date().getTime();
let callFx = () => {
lastExecution = now;
fx.apply(that, args);
};
if (lastExecution && lastExecution + delay > now) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
callFx();
}, delay);
} else {
callFx();
}
};
};
$.negate = fx => function(...args) {
return !fx.apply(this, args);
};
$.injectScript = (url, options) => {
options = options || {};
let deferred = $.Deferred();
let myDocument = options.document || window.document;
let linkTag = myDocument.createElement('script');
$(linkTag)
.attr('src', url)
.attr('async', true)
.on('load error', event => {
if (options.removeTag) {
myDocument.head.removeChild(linkTag);
}
if (event.type === 'error') {
deferred.reject($(linkTag));
} else {
deferred.resolve($(linkTag));
}
});
// Use raw JS function to append the <script> tag, because jQuery handles
// script tags specially (see "domManip" function) and uses eval() which
// is not CSP-safe.
myDocument.head.appendChild(linkTag);
return deferred.promise();
};
$.injectStyleSheet = (url, options) => {
options = options || {};
let deferred = $.Deferred();
let myDocument = options.document || window.document;
let linkTag = myDocument.createElement('link');
$(linkTag)
.attr('rel', 'stylesheet')
.attr('type', 'text/css')
.attr('href', url)
.on('load error', event => {
if (event.type === 'error') {
deferred.reject($(linkTag));
} else {
deferred.resolve($(linkTag));
}
});
// Use raw JS function to append the <script> tag, because jQuery handles
// script tags specially (see "domManip" function) and uses eval() which
// is not CSP-safe.
myDocument.head.appendChild(linkTag);
return deferred.promise();
};
$.injectStyle = (data, options) => {
options = options || {};
let myDocument = options.document || window.document;
let styleTag = myDocument.createElement('style');
let $styleTag = $(styleTag);
$styleTag
.attr('type', 'text/css')
.html(data);
myDocument.head.appendChild(styleTag);
return $styleTag;
};
$.pxToNumber = pixel => {
if (!pixel) {
// parseFloat would return NaN if pixel is '' or undefined
return 0;
}
// parseFloat ignores 'px' and just extracts the number
return parseFloat(pixel);
};
$.resolvedDeferred = (...args) => {
let deferred = $.Deferred();
deferred.resolve(...args);
return deferred;
};
$.resolvedPromise = (...args) => {
let deferred = $.Deferred();
deferred.resolve(...args);
return deferred.promise();
};
$.rejectedPromise = (...args) => {
let deferred = $.Deferred();
deferred.reject(...args);
return deferred.promise();
};
$.promiseAll = (promises, asArray) => {
asArray = scout.nvl(asArray, false);
promises = arrays.ensure(promises);
let deferred = $.Deferred();
$.when(...promises).done((...args) => {
if (asArray) {
deferred.resolve(args);
} else {
deferred.resolve(...args);
}
}).fail((...args) => {
deferred.reject(...args);
});
return deferred.promise();
};
$.ajaxJson = url => $.ajax({
url: url,
dataType: 'json',
contentType: 'application/json; charset=UTF-8'
}).catch(function(...args) {
// Reject the promise with usual arguments (jqXHR, textStatus, errorThrown), but add the request
// options as additional argument (e.g. to make the URL available to the error handler)
args.push(this);
return $.rejectedPromise(...args);
});
$.isJqXHR = obj => (typeof obj === 'object' && obj.hasOwnProperty('readyState') && obj.hasOwnProperty('status') && obj.hasOwnProperty('statusText'));
// === $.prototype extensions ===
$.fn.nvl = function($element) {
if (this.length || !($element instanceof $)) {
return this;
}
return $element;
};
$.fn.makeElement = function(element, cssClass, text) {
let myDocument = this.document(true);
if (myDocument === undefined || element === undefined) {
throw new Error('missing arguments: document, element');
}
let $element = $(element, myDocument);
if (cssClass) {
$element.addClass(cssClass);
}
if (text) {
$element.text(text);
}
return $element;
};
$.fn.makeDiv = function(cssClass, text) {
return /** @type JQuery<HTMLDivElement> */ this.makeElement('<div>', cssClass, text);
};
$.fn.makeSpan = function(cssClass, text) {
return /** @type JQuery<HTMLSpanElement> */ this.makeElement('<span>', cssClass, text);
};
$.fn.document = function(domElement) {
let myDocument = this.length ? (this[0] instanceof Document ? this[0] : this[0].ownerDocument) : null;
return domElement ? myDocument : $(myDocument);
};
$.fn.window = function(domElement) {
let myDocument = this.document(true),
myWindow = myDocument ? myDocument.defaultView : null;
return domElement ? myWindow : $(myWindow);
};
$.fn.activeElement = function(domElement) {
let myDocument = this.document(true),
activeElement = myDocument ? myDocument.activeElement : null;
return domElement ? activeElement : $(activeElement);
};
$.fn.body = function(domElement) {
let $body = $('body', this.document(true));
return domElement ? $body[0] : $body;
};
$.fn.entryPoint = function(domElement) {
let $element = this.closest('.scout');
return domElement ? $element[0] : $element;
};
$.fn.windowSize = function() {
let $window = this.window();
return new Dimension($window.width(), $window.height());
};
$.fn.elementFromPoint = function(x, y, selector) {
let $container = $(this),
doc = $container.document(true),
elements = [],
i = 0,
$element;
if (!doc) {
// If doc is null the $container itself is the document
doc = $container[0];
}
if (!doc) {
// If doc is still null (e.g. because the current jQuery collection does not contain any elements) return an empty collection
return $();
}
// eslint-disable-next-line no-constant-condition
while (true) {
$element = $(doc.elementFromPoint(x, y));
if ($element.length === 0 || $element[0] === doc.documentElement) {
break;
}
if ($container.isOrHas($element) && (!selector || $element.is(selector))) {
break;
}
elements.push($element);
// make the element invisible to get the underlying element (uses visibility: hidden to make sure element size and position won't be changed)
$element.addClass('invisible');
i++;
if (i > 1000) {
$.log.warn('Infinite loop aborted', $element);
$element = $();
break;
}
}
if ($element[0] === doc.documentElement && $container[0] !== doc) {
// return an empty element if the only element found is the document element and the document element is not the container
$element = $();
}
elements.forEach($element => {
// show element again
$element.removeClass('invisible');
});
return $element;
};
$.fn.prependDiv = function(cssClass, text) {
return this.makeDiv(cssClass, text).prependTo(this);
};
$.fn.appendDiv = function(cssClass, text) {
return this.makeDiv(cssClass, text).appendTo(this);
};
$.fn.prependElement = function(element, cssClass, text) {
return this.makeElement(element, cssClass, text).prependTo(this);
};
$.fn.appendElement = function(element, cssClass, text) {
return this.makeElement(element, cssClass, text).appendTo(this);
};
$.fn.afterDiv = function(cssClass, text) {
return this.makeDiv(cssClass, text).insertAfter(this);
};
$.fn.beforeDiv = function(cssClass, text) {
return this.makeDiv(cssClass, text).insertBefore(this);
};
$.fn.appendSpan = function(cssClass, text) {
return this.makeSpan(cssClass, text).appendTo(this);
};
$.fn.appendBr = function(cssClass) {
return this.makeElement('<br>', cssClass).appendTo(this);
};
$.fn.appendTable = function(cssClass) {
return this.appendElement('<table>', cssClass);
};
$.fn.appendColgroup = function(cssClass) {
return this.appendElement('<colgroup>', cssClass);
};
$.fn.appendCol = function(cssClass) {
return this.appendElement('<col>', cssClass);
};
$.fn.appendTr = function(cssClass) {
return this.appendElement('<tr>', cssClass);
};
$.fn.appendTd = function(cssClass, text) {
return this.appendElement('<td>', cssClass, text);
};
$.fn.appendTh = function(cssClass, text) {
return this.appendElement('<th>', cssClass, text);
};
$.fn.appendUl = function(cssClass) {
return this.appendElement('<ul>', cssClass);
};
$.fn.appendLi = function(cssClass, text) {
return this.appendElement('<li>', cssClass, text);
};
$.fn.appendTextNode = function(text) {
return $(this.document(true).createTextNode(text)).appendTo(this);
};
$.fn.appendIcon = function(iconId, cssClass) {
if (!iconId) {
let $icon = this.appendSpan(cssClass)
.addClass('icon');
aria.hidden($icon, true);
return $icon;
}
let icon;
if (iconId instanceof IconDesc) {
icon = iconId;
} else {
icon = icons.parseIconId(iconId);
}
if (icon.isFontIcon()) {
let $icon = this.makeSpan(cssClass, icon.iconCharacter)
.addClass('icon')
.addClass(icon.appendCssClass('font-icon'))
.appendTo(this);
if (strings.hasText(icon.altText)) {
aria.role($icon, 'img');
aria.label($icon, icon.altText, true);
} else {
aria.hidden($icon, true);
}
return $icon;
}
return this.appendImg(icon.iconUrl, cssClass)
.attr('alt', icon.altText)
.addClass('icon image-icon');
};
$.fn.appendImg = function(imageSrc, cssClass) {
let $icon = this.appendElement('<img>', cssClass);
if (imageSrc) {
$icon.attr('src', imageSrc);
}
return $icon;
};
// FIXME TS how to solve these issues?
$.fn.icon = function(iconId, addToDomFunc) {
let icon, $icon = this.data('$icon');
if (iconId) {
icon = icons.parseIconId(iconId);
if (icon.isFontIcon()) {
let $iconElem = getOrCreateIconElement.call(this, $icon, '<span>', addToDomFunc)
.addClass('icon')
.addClass(icon.appendCssClass('font-icon'))
.text(icon.iconCharacter);
aria.hidden($iconElem, true);
} else {
getOrCreateIconElement.call(this, $icon, '<img>', addToDomFunc)
.attr('src', icon.iconUrl)
.attr('alt', '')
.addClass('icon image-icon');
}
} else {
removeIconElement.call(this, $icon);
}
return this;
// ----- Helper functions -----
function getOrCreateIconElement($icon, newElement, addToDomFunc) {
// If element type does not match existing element, remove the existing element (e.g. when changing from font-icon to picture icon)
if ($icon && !$icon.is(newElement.replace(/[<>]/g, ''))) {
removeIconElement.call(this, $icon);
$icon = null;
}
// Create new element if necessary
if (!$icon) {
$icon = $(newElement);
this.data('$icon', $icon);
if (!addToDomFunc) {
this.prepend($icon);
} else {
addToDomFunc.call(this, $icon);
}
}
return $icon;
}
function removeIconElement($icon) {
if ($icon) {
$icon.remove();
}
this.removeData('$icon');
}
};
$.fn.makeSVG = function(type, cssClass, text, id) {
let myDocument = this.document(true);
if (myDocument === undefined || type === undefined) {
return new Error('missing arguments: document, type');
}
let $svg = $(myDocument.createElementNS('http://www.w3.org/2000/svg', type));
if (cssClass) {
$svg.attr('class', cssClass);
}
if (text) {
$svg.text(text);
}
if (id !== undefined) {
$svg.attr('id', id);
}
return $svg;
};
$.fn.appendSVG = function(type, cssClass, text, id) {
return this.makeSVG(type, cssClass, text, id).appendTo(this);
};
$.fn.attrXLINK = function(attributeName, value) {
if (this.length === 0) { // shortcut for empty collections
return this;
}
if (this.length === 1) { // shortcut for single element collections
this[0].setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:' + attributeName, value);
return this;
}
return this.each(function() {
this.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:' + attributeName, value);
});
};
$.fn.appendAppLink = function(appLinkBean, func) {
return this.appendSpan().appLink(appLinkBean, func);
};
$.fn.appLink = function(appLinkBean, func) {
if (!func) {
func = function(event) {
let widget = scout.widget(this);
if (widget && widget._onAppLinkAction) {
widget._onAppLinkAction(event);
}
}.bind(this);
} else if (typeof func === 'object' && func._onAppLinkAction) {
func = func._onAppLinkAction.bind(func);
}
this.addClass('app-link')
.attr('tabindex', '0')
.unfocusable()
.on('click', func);
if (typeof appLinkBean === 'string') {
this.attr('data-ref', appLinkBean);
} else {
this
.text(appLinkBean.name)
.attr('data-ref', appLinkBean.ref);
}
return this;
};
/**
* This function adds a device specific CSS class to the current element.
* The current implementation adds a class 'ios' if it is an ios device.
*/
$.fn.addDeviceClass = function() {
let device = Device.get();
if (device.isIos()) {
this.addClass('ios');
}
return this;
};
// select one and deselect siblings
$.fn.selectOne = function() {
this.siblings().removeClass('selected');
this.addClass('selected');
return this;
};
$.fn.select = function(selected) {
return this.toggleClass('selected', !!selected);
};
$.fn.isSelected = function() {
return this.hasClass('selected');
};
$.fn.setEnabled = function(enabled) {
enabled = !!enabled;
this.toggleClass('disabled', !enabled);
// Toggle disabled attribute for elements that support it (see http://www.w3.org/TR/html5/disabled-elements.html)
if (this.is('button, input, select, textarea, optgroup, option, fieldset')) {
this.toggleAttr('disabled', !enabled);
}
this.trigger(enabled ? 'enable' : 'disable');
return this;
};
$.fn.isEnabled = function() {
return !this.hasClass('disabled');
};
$.fn.setVisible = function(visible) {
let isVisible = !this.hasClass('hidden');
if (isVisible === visible) {
return this;
}
if (!visible) {
this.addClass('hidden');
this.trigger('hide');
} else {
this.removeClass('hidden');
this.trigger('show');
}
return this;
};
$.fn.isVisible = function() {
if (this.hasClass('hidden')) {
return false;
}
return elemVisible(this[0]);
};
$.fn.isEveryParentVisible = function() {
let everyParentVisible = true;
this.parents().each(function() {
if (!$(this).isVisible()) {
everyParentVisible = false;
return false;
}
});
return everyParentVisible;
};
$.fn.isDisplayNone = function() {
return this.css('display') === 'none';
};
$.fn.isVisibilityHidden = function() {
return this.css('visibility') === 'hidden';
};
$.fn.setTabbable = function(tabbable) {
return this.attr('tabindex', tabbable ? 0 : null);
};
$.fn.setTabbableOrFocusable = function(tabbable) {
return this.attr('tabindex', tabbable ? 0 : -1);
};
$.fn.isTabbable = function() {
return this.attr('tabindex') >= 0;
};
$.fn.placeholder = function(placeholder) {
return this.toggleAttr('placeholder', !!placeholder, placeholder);
};
$.fn.isAttached = function() {
return $.contains(this.document(true).documentElement, this[0]);
};
$.fn.scrollParent = function(includePseudoScrollParents) {
let $elem = this;
while ($elem.length > 0) {
if ($elem.data('scrollable') || (includePseudoScrollParents && $elem.data('pseudo-scrollable'))) {
return $elem;
}
$elem = $elem.parent();
}
return $();
};
$.fn.scrollParents = function(includePseudoScrollParents) {
let $scrollParents = $();
let $elem = this;
while ($elem.length > 0) {
if ($elem.data('scrollable') || (includePseudoScrollParents && $elem.data('pseudo-scrollable'))) {
$scrollParents.push($elem[0]);
}
$elem = $elem.parent();
}
return $scrollParents;
};
$.fn.findUp = function(predicate, $stop) {
let $elem = $(this);
while ($elem.length > 0) {
if (predicate($elem)) {
return $elem;
}
if ($stop && $elem[0] === $stop[0]) {
return $();
}
$elem = $elem.parent();
}
return $();
};
$.fn.isOrHas = function(elem) {
if (elem instanceof $) {
elem = elem[0];
}
return this[0] === elem || this.has(elem).length > 0;
};
$.fn.animateAVCSD = function(attr, value, complete, step, duration) {
let properties = {};
let options = {};
properties[attr] = value;
if (complete) {
options.complete = complete;
}
if (step) {
options.step = step;
}
if (duration) {
options.duration = duration;
}
options.queue = false;
this.animate(properties, options);
return this;
};
// SVG animate, array contains attr, endValue + startValue
$.fn.animateSVG = function(attr, endValue, duration, complete, withoutTabIndex) {
return this.each(function() {
let startValue = parseFloat($(this).attr(attr));
if (withoutTabIndex) {
let oldComplete = complete;
complete = function() {
if (oldComplete) {
oldComplete.call(this);
}
$(this).removeAttr('tabindex');
};
}
$(this).animate({
tabIndex: 0
}, {
step: function(now, fx) {
this.setAttribute(attr, startValue + (endValue - startValue) * fx.pos);
},
duration: duration,
complete: complete,
queue: false
});
});
};
$.fn.addClassForAnimation = function(className, options) {
let defaultOptions = {
classesToRemove: className
};
options = $.extend({}, defaultOptions, options);
this.addClass(className);
this.oneAnimationEnd(event => {
this.removeClass(options.classesToRemove);
});
return this;
};
$.fn.oneAnimationEnd = function(handler) {
if (!handler) {
return this;
}
let oneHandler = event => {
if (event.target !== this[0]) {
// Ignore events that bubble up from child elements
return;
}
// Unregister listener to implement "one" semantics
this.off('animationend webkitAnimationEnd', oneHandler);
// Notify actual event handler
handler(event);
};
return this.on('animationend webkitAnimationEnd', oneHandler);
};
$.fn.hasAnimationClass = function() {
// matches any CSS class that starts with 'animate-'
return /(^|\s)animate-/.test(this.attr('class'));
};
$.fn.cssWidthAnimated = function(oldWidth, newWidth, opts) {
opts = opts || {};
opts.duration = scout.nvl(opts.duration, 300);
// Reset to old width first
this.cssWidth(oldWidth);
// Then animate to new width
this.animate({
width: newWidth
}, opts);
return this;
};
$.fn.cssHeightAnimated = function(oldHeight, newHeight, opts) {
opts = opts || {};
opts.duration = scout.nvl(opts.duration, 300);
// Reset to old height first
this.cssHeight(oldHeight);
// Then animate to new height
this.animate({
height: newHeight
}, opts);
return this;
};
$.fn.cssLeftAnimated = function(from, to, opts) {
opts = opts || {};
opts.duration = scout.nvl(opts.duration, 300);
// Reset to from first
this.cssLeft(from);
// Then animate to new width
this.animate({
left: to
}, opts);
return this;
};
$.fn.cssTopAnimated = function(from, to, opts) {
opts = opts || {};
opts.duration = scout.nvl(opts.duration, 300);
// Reset to from first
this.cssTop(from);
// Then animate to new pos
this.animate({
top: to
}, opts);
return this;
};
$.fn.cssAnimated = function(fromVals, toVals, opts) {
opts = opts || {};
opts.duration = scout.nvl(opts.duration, 300);
// Reset to from first
this.css(fromVals);
// Then animate to new pos
this.animate(toVals, opts);
return this;
};
$.fn.cssWidthToContentAnimated = function(opts) {
let oldW = this.outerWidth(),
newW = this.css('width', 'auto').outerWidth();
this.cssWidthAnimated(oldW, newW, opts);
return this;
};
$.fn.offsetTo = function($to) {
let toOffset = $to.offset(),
offset = this.offset();
return {
top: offset.top - toOffset.top,
left: offset.left - toOffset.left
};
};
$.fn.cssPxValue = function(prop, value) {
if (value === undefined) {
return $.pxToNumber(this.css(prop));
}
if (value === null) {
value = ''; // "null" should also remove the CSS property
}
if (typeof value === 'string') {
return this.css(prop, value);
}
return this.css(prop, value + 'px');
};
// noinspection JSValidateTypes
$.fn.cssLeft = function(position) {
return this.cssPxValue('left', position);
};
// noinspection JSValidateTypes
$.fn.cssTop = function(position) {
return this.cssPxValue('top', position);
};
/**
* Sets the CSS properties 'left' and 'top' based on the x and y properties of the given point instance.
*
* @param {Point} point
*/
$.fn.cssPosition = function(point) {
return this.cssLeft(point.x).cssTop(point.y);
};
// noinspection JSValidateTypes
$.fn.cssBottom = function(position) {
return this.cssPxValue('bottom', position);
};
// noinspection JSValidateTypes
$.fn.cssRight = function(position) {
return this.cssPxValue('right', position);
};
// noinspection JSValidateTypes
$.fn.cssWidth = function(width) {
return this.cssPxValue('width', width);
};
$.fn.cssMinWidth = function(minWidth) {
if (minWidth === undefined) {
let value = this.css('min-width');
if (value === 'auto' || value.indexOf('%') !== -1) {
return 0;
}
return $.pxToNumber(value);
}
return this.cssPxValue('min-width', minWidth);
};
$.fn.cssMaxWidth = function(maxWidth) {
if (maxWidth === undefined) {
let value = this.css('max-width');
if (value === 'none' || value.indexOf('%') !== -1) {
return Number.MAX_VALUE;
}
return $.pxToNumber(value);
}
return this.cssPxValue('max-width', maxWidth);
};
// noinspection JSValidateTypes
$.fn.cssHeight = function(height) {
return this.cssPxValue('height', height);
};
$.fn.cssMinHeight = function(minHeight) {
if (minHeight === undefined) {
let value = this.css('min-height');
if (value === 'auto' || value.indexOf('%') !== -1) {
return 0;
}
return $.pxToNumber(value);
}
return this.cssPxValue('min-height', minHeight);
};
$.fn.cssMaxHeight = function(maxHeight) {
if (maxHeight === undefined) {
let value = this.css('max-height');
if (value === 'none' || value.indexOf('%') !== -1) {
return Number.MAX_VALUE;
}
return $.pxToNumber(value);
}
return this.cssPxValue('max-height', maxHeight);
};
$.fn.cssLineHeight = function(height) {
return this.cssPxValue('line-height', height);
};
$.fn.cssMarginLeft = function(value) {
return this.cssPxValue('margin-left', value);
};
$.fn.cssMarginBottom = function(value) {
return this.cssPxValue('margin-bottom', value);
};
$.fn.cssMarginRight = function(value) {
return this.cssPxValue('margin-right', value);
};
$.fn.cssMarginTop = function(value) {
return this.cssPxValue('margin-top', value);
};
$.fn.cssMarginX = function(value) {
if (value === undefined) {
return this.cssMarginLeft() + this.cssMarginRight();
}
this.cssMarginLeft(value);
this.cssMarginRight(value);
return this;
};
$.fn.cssMarginY = function(value) {
if (value === undefined) {
return this.cssMarginTop() + this.cssMarginBottom();
}
this.cssMarginTop(value);
this.cssMarginBottom(value);
return this;
};
// noinspection JSValidateTypes
$.fn.cssPaddingTop = function(value) {
return this.cssPxValue('padding-top', value);
};
// noinspection JSValidateTypes
$.fn.cssPaddingRight = function(value) {
return this.cssPxValue('padding-right', value);
};
// noinspection JSValidateTypes
$.fn.cssPaddingBottom = function(value) {
return this.cssPxValue('padding-bottom', value);
};
// noinspection JSValidateTypes
$.fn.cssPaddingLeft = function(value) {
return this.cssPxValue('padding-left', value);
};
$.fn.cssPaddingX = function(value) {
if (value === undefined) {
return this.cssPaddingLeft() + this.cssPaddingRight();
}
this.cssPaddingLeft(value);
this.cssPaddingRight(value);
return this;
};
$.fn.cssPaddingY = function(value) {
if (value === undefined) {
return this.cssPaddingTop() + this.cssPaddingBottom();
}
this.cssPaddingTop(value);
this.cssPaddingBottom(value);
return this;
};
// noinspection JSValidateTypes
$.fn.cssBorderBottomWidth = function(value) {
return this.cssPxValue('border-bottom-width', value);
};
// noinspection JSValidateTypes
$.fn.cssBorderLeftWidth = function(value) {
return this.cssPxValue('border-left-width', value);
};
// noinspection JSValidateTypes
$.fn.cssBorderRightWidth = function(value) {
return this.cssPxValue('border-right-width', value);
};
// noinspection JSValidateTypes
$.fn.cssBorderTopWidth = function(value) {
return this.cssPxValue('border-top-width', value);
};
$.fn.cssBorderWidthY = function(value) {
if (value === undefined) {
return this.cssBorderTopWidth() + this.cssBorderBottomWidth();
}
this.cssBorderTopWidth(value);
return this.cssBorderBottomWidth(value);
};
$.fn.cssBorderWidthX = function(value) {
if (value === undefined) {
return this.cssBorderLeftWidth() + this.cssBorderRightWidth();
}
this.cssBorderLeftWidth(value);
return this.cssBorderRightWidth(value);
};
$.fn.innerBottom = function() {
return this.position().top + this.outerHeight(true) - this.cssMarginBottom() - this.cssBorderBottomWidth();
};
$.fn.innerRight = function() {
return this.position().left + this.outerWidth(true) - this.cssMarginRight() - this.cssBorderRightWidth();
};
$.fn.copyCss = function($origin, props) {
let properties = props.split(' ');
let $this = this;
properties = explodeShorthandProperties(properties);
properties.forEach(prop => {
$this.css(prop, $origin.css(prop));
});
return $this;
};
$.fn.copyCssClasses = function($other, classString) {
let classes = classString.split(' ');
let $this = this;
classes.forEach(cssClass => {
if ($other.hasClass(cssClass)) {
$this.addClass(cssClass);
}
});
return $this;
};
$.fn.disableSpellcheck = function() {
return this.attr('spellcheck', false);
};
$.fn.resizable = function(model) {
let $this = $(this);
let resizable = $this.data('resizable');
if (resizable) {
// Already resizable
return this;
}
resizable = scout.create(Resizable, $.extend(model, {$container: $this}));
$this.data('resizable', resizable);
return this;
};
$.fn.unresizable = function() {
let $this = $(this);
let resizable = $this.data('resizable');
if (resizable) {
resizable.destroy();
$this.removeData('resizable');
}
return this;
};
$.fn.draggable = function($handle, callback) {
let $draggable = this;
$handle = $handle || $draggable;
return $handle.on('mousedown.draggable', event => {
$('iframe').addClass('dragging-in-progress');
let orig_offset = $draggable.offset();
let orig_event = event;
let handleWidth = $handle.width();
let windowWidth = $handle.window().width();
let windowHeight = $handle.window().height();
$handle.parents()
.on('mousemove.dragging', event => {
let top = orig_offset.top + (event.pageY - orig_event.pageY);
let left = orig_offset.left + (event.pageX - orig_event.pageX);
// do not drop outside of viewport (and leave a margin of 100 pixels)
left = Math.max(100 - handleWidth, left);
left = Math.min(windowWidth - 100, left);
top = Math.max(0, top); // must not be dragged outside of top, otherwise dragging back is impossible
top = Math.min(windowHeight - 100, top);
let newOffset = {
top: top,
left: left
};
$draggable.offset(newOffset);
callback && callback(newOffset);
})
.on('mouseup.dragging', e => {
$handle.parents().off('.dragging');
$('iframe').removeClass('dragging-in-progress');
});
event.preventDefault();
});
};
$.fn.undraggable = function($handle) {
let $draggable = this;
$handle = $handle || $draggable;
return $handle.off('mousedown.draggable');
};
$.fn.fadeOutAndRemove = function(duration, callback) {
if (callback === undefined && typeof duration === 'function') {
callback = duration;
duration = undefined;
}
duration = scout.nvl(duration, 150);
return this.stop(true).fadeOut(duration, function() {
$(this).remove();
if (callback) {
callback.call(this);
}
});
};
$.fn.removeAnimated = function(cssClass, callback) {
if (callback === undefined && typeof cssClass === 'function') {
callback = cssClass;
cssClass = undefined;
}
if (this.isDisplayNone()) {
// Remove without animation
this.remove();
callback && callback.call(this);
} else if (!Device.get().supportsCssAnimation()) {
// Cannot remove animated, remove with jQuery.fadeOut()
this.fadeOutAndRemove(callback);
} else {
// Add CSS class and wait for 'animationend' event
this.addClass(cssClass || 'animate-remove');
this.oneAnimationEnd(function() {
$(this).remove();
callback && callback.call(this);
});
}
};
const __origHide = $.fn.hide;
$.fn.hide = function(...args) {
this.trigger('hide');
return __origHide.apply(this, args);
};
const __origWidth = $.fn.width;
// noinspection JSValidateTypes
$.fn.width = function(...args) {
return _ceilNumber(__origWidth.apply(this, args));
};
const __origOuterWidth = $.fn.outerWidth;
// noinspection JSValidateTypes
$.fn.outerWidth = function(...args) {
return _ceilNumber(__origOuterWidth.apply(this, args));
};
const __origHeight = $.fn.height;
// noinspection JSValidateTypes
$.fn.height = function(...args) {
return _ceilNumber(__origHeight.apply(this, args));
};
let __origOuterHeight = $.fn.outerHeight;
// noinspection JSValidateTypes
$.fn.outerHeight = function(...args) {
return _ceilNumber(__origOuterHeight.apply(this, args));
};
$.fn.textOrNbsp = function(text, emptyCssClass) {
return this.contentOrNbsp(false, text, emptyCssClass);
};
$.fn.htmlOrNbsp = function(html, emptyCssClass) {
return this.contentOrNbsp(true, html, emptyCssClass);
};
$.fn.contentOrNbsp = function(htmlEnabled, content, emptyCssClass) {
let func = htmlEnabled ? this.html : this.text;
if (strings.hasText(content)) {
func.call(this, content);
if (emptyCssClass) {
this.removeClass(emptyCssClass);
}
} else {
this.html(' ');
if (emptyCssClass) {
this.addClass(emptyCssClass);
}
}
return this;
};
$.fn.toggleAttr = function(attr, state, value) {
if (!attr) {
return this;
}
if (value === undefined) {
value = attr;
}
return this.each(function() {
let $element = $(this);
if (state === undefined) {
// set state according to the current value
state = ($element.attr(attr) === undefined);
}
if (state) {
// set attr
$element.attr(attr, value);
} else {
// remove attr
$element.removeAttr(attr);
}
});
};
$.fn.attrOrRemove = function(attributeName, value) {
if (value) {
this.attr(attributeName, value);
} else {
this.removeAttr(attributeName);
}
return this;
};
$.fn.unfocusable = function() {
return this.addClass('unfocusable');
};
$.fn.selectAllText = function() {
let range,
myDocument = this.document(true),
myWindow = this.window(true),
element = this[0];
if (!myDocument || !myDocument.body || !myWindow || !element) {
return this;
}
if (myWindow.getSelection) {
range = myDocument.createRange();
range.selectNodeContents(element);
myWindow.getSelection().removeAllRanges();
myWindow.getSelection().addRange(range);
}
return this;
};
$.fn._getClientAndScrollWidthRounded = function() {
let element = this[0];
if (Device.get().isEdge()) {
// Edge seem to round up the scrollWidth. Therefore the clientWidth must be rounded up as well to have a valid comparison.
return {
clientWidth: Math.ceil(element.getBoundingClientRect().width) - this.cssBorderWidthX(), // getBoundingClientRect includes the border -> remove it again to have the clientWidth
scrollWidth: element.scrollWidth
};
}
return {
clientWidth: element.clientWidth,
scrollWidth: element.scrollWidth
};
};
$.fn._getClientAndScrollWidthReliable = function() {
let widths = this._getClientAndScrollWidthRounded();
if (!Device.get().isScrollWidthIncludingPadding()) {
// browser supports accurate client- and scroll widths.
return widths;
}
if (widths.scrollWidth > widths.clientWidth) {
// content is large enough so that the scroll-width is already larger than the client-width. Values are correct.
return widths;
}
let paddingRight = this.cssPaddingRight(),
oldStyle = this.attr('style');
if (paddingRight > 0) {
// Some browsers render text within the right-padding (even with overflow=hidden). This has an effect on the value of scrollWidth which may be wrong in these cases (scrollWidth == clientWidth but ellipsis is shown).
// Solution: temporary remove the padding and reduce the width by the padding-size to have the same space for the text but without padding.
this.css({
width: widths.clientWidth - paddingRight,
paddingRight: '0px'
});
widths = this._getClientAndScrollWidthRounded(); // read value again.
this.attrOrRemove('style', oldStyle);
if (widths.scrollWidth > widths.clientWidth) {
return widths;
}
}
// In some cases the browser returns the same values for clientWidth and scrollWidth,
// but will cut off the text nevertheless. At least in FF this seems to be a bug related
// to sub-pixel rendering. The text is "slightly" (0.2 pixels) larger than the clientWidth,
// but scrollWidth returns the same value.
// As a workaround, we do a second measurement of the uncut width before returning false.
let clientWidth = this[0].getBoundingClientRect().width;
this.css('width', 'auto');
this.css('max-width', 'none');
let scrollWidth = this[0].getBoundingClientRect().width;
this.attrOrRemove('style', oldStyle);
return {
clientWidth: clientWidth,
scrollWidth: scrollWidth
};
};
$.fn.isContentTruncated = function() {
let widths = this._getClientAndScrollWidthReliable();
return widths.scrollWidth > widths.clientWidth;
};
$.fn.onPassive = function(eventType, handler) {
let options = events.passiveOptions();
this[0].addEventListener(eventType, handler, options);
return this;
};
$.fn.offPassive = function(eventType, handler) {
let options = events.passiveOptions();
this[0].removeEventListener(eventType, handler, options);
return this;
};
/**
* Focuses the element if it is focusable.
*
* The focus function of jQuery triggers a focus event instead of focusing the element (since 3.7.1 even if the element is not focusable).
* Because that is not the expected behavior, this replacement calls the native focus function.
*/
$.fn.focus = function() {
if (this.length === 0) {
return this;
}
this[0].focus();
return this;
};
// === $.easing extensions ===
$.extend($.easing, {
easeInQuart: x => Math.pow(x, 4),
easeOutQuart: x => 1 - Math.pow(1 - x, 4)
});
/**
* jquery.binarytransport.js
*
* @description. jQuery ajax transport for making binary data type requests.
* @version 1.0
* @author Henry Algus <henryalgus@gmail.com>
*
* The MIT License (MIT)
*
* Copyright (c) 2014 Henry Algus
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// use this transport for "binary" data type
$.ajaxTransport('+binary', (options, originalOptions, jqXHR) => {
// check for conditions and support for blob / arraybuffer response type
if (window.FormData && ((options.dataType && (options.dataType === 'binary')) ||
(options.data && ((window.ArrayBuffer && options.data instanceof ArrayBuffer) ||
(window.Blob && options.data instanceof Blob))))) {
return {
// create new XMLHttpRequest
send: (headers, callback) => {
// setup all variables
let xhr = new XMLHttpRequest(),
url = options.url,
type = options.type,
async = options.async || true,
// blob or arraybuffer. Default is blob
dataType = options.responseType || 'blob',
data = options.data || null,
username = options.username || null,
password = options.password || null;
xhr.addEventListener('load', () => {
let data = {};
data[options.dataType] = xhr.response;
// make callback and send data
callback(xhr.status, xhr.statusText, data, xhr.getAllResponseHeaders());
});
xhr.open(type, url, async, username, password);
// setup custom headers
for (let i in headers) { // NOSONAR
xhr.setRequestHeader(i, headers[i]);
}
// apply custom fields (if provided)
// noinspection JSUnresolvedVariable
if (options.xhrFields) {
for (let j in options.xhrFields) {
xhr[j] = options.xhrFields[j];
}
}
xhr.responseType = dataType;
xhr.send(data);
},
abort: () => {
// nop
}
};
}
});
// jquery's noGlobal is true with webpack so jquery does not expose $ by itself
if (window.$ !== $) {
window.$ = $;
}