@atlassian/aui
Version:
Atlassian User Interface library
346 lines (298 loc) • 12.2 kB
JavaScript
import $ from '../jquery';
import globalize from './globalize';
var has = Object.prototype.hasOwnProperty;
var deprecationCalls = [];
function toSentenceCase(str) {
str += '';
if (!str) {
return '';
}
return str.charAt(0).toUpperCase() + str.substring(1);
}
function getDeprecatedLocation(printFrameOffset) {
var err = new Error();
var stack = err.stack || err.stacktrace;
var stackMessage = (stack && stack.replace(/^Error\n/, '')) || '';
if (stackMessage) {
stackMessage = stackMessage.split('\n');
return stackMessage[printFrameOffset + 2];
}
return stackMessage;
}
function logger() {
if (typeof console !== 'undefined' && console.warn) {
Function.prototype.apply.call(console.warn, console, arguments);
}
}
/**
* @typedef {Object} DeprecationOptions
* @property {string} sinceVersion the version this has been deprecated since
* @property {string} removeInVersion the version this will be removed in
* @property {string} [alternativeName] the name of an alternative to use
* @property {string} [extraInfo] extra information to be printed at the end of the deprecation log
* @property {string} [extraObject] an extra object that will be printed at the end
* @property {string} [displayName] a human-readable name to show in the deprecation message. If not provided, it is inferred from the function or object being deprecated.
* @property {string} [deprecationType] type of the deprecation to append to the start of the deprecation message. e.g. JS or CSS
*/
/**
* Return a function that logs a deprecation warning to the console the first time it is called from a certain location.
* It will also print the stack frame of the calling function.
*
* @param {string} displayName the name of the thing being deprecated
* @param {DeprecationOptions} options
* @return {Function} that logs the warning and stack frame of the calling function. Takes in an optional parameter for the offset of
* the stack frame to print, the default is 0. For example, 0 will log it for the line of the calling function,
* -1 will print the location the logger was called from
*/
function getShowDeprecationMessage(displayName, options) {
// This can be used internally to pas in a showmessage fn
if (typeof displayName === 'function') {
return displayName;
}
var called = false;
options = options || {};
return function (printFrameOffset) {
var deprecatedLocation =
getDeprecatedLocation(printFrameOffset ? printFrameOffset : 1) || '';
// Only log once if the stack frame doesn't exist to avoid spamming the console/test output
if (!called || deprecationCalls.indexOf(deprecatedLocation) === -1) {
deprecationCalls.push(deprecatedLocation);
called = true;
var deprecationType = options.deprecationType + ' ' || '';
var message =
'DEPRECATED ' +
deprecationType +
'- ' +
toSentenceCase(displayName) +
' has been deprecated' +
(options.sinceVersion ? ' since ' + options.sinceVersion : '') +
' and will be removed in ' +
(options.removeInVersion || 'a future release') +
'.';
if (options.alternativeName) {
message += ' Use ' + options.alternativeName + ' instead. ';
}
if (options.extraInfo) {
message += ' ' + options.extraInfo;
}
if (deprecatedLocation === '') {
deprecatedLocation =
' \n ' +
'No stack trace of the deprecated usage is available in your current browser.';
} else {
deprecatedLocation = ' \n ' + deprecatedLocation;
}
if (options.extraObject) {
message += '\n';
logger(message, options.extraObject, deprecatedLocation);
} else {
logger(message, deprecatedLocation);
}
}
};
}
function logCssDeprecation(selectorMap, newNode) {
var displayName = selectorMap.options.displayName;
displayName = displayName ? ' (' + displayName + ')' : '';
var options = $.extend(
{
deprecationType: 'CSS',
extraObject: newNode,
},
selectorMap.options
);
getShowDeprecationMessage("'" + selectorMap.selector + "' pattern" + displayName, options)();
}
/**
* Returns a wrapped version of the function that logs a deprecation warning when the function is used.
* @param {Function} fn the fn to wrap
* @param {string} displayName the name of the fn to be displayed in the message
* @param {DeprecationOptions} options
* @return {Function} wrapping the original function
*/
function deprecateFunctionExpression(fn, displayName, options) {
options = options || {};
options.deprecationType = options.deprecationType || 'JS';
var showDeprecationMessage = getShowDeprecationMessage(
displayName || fn.name || 'this function',
options
);
return function () {
showDeprecationMessage();
return fn.apply(this, arguments);
};
}
/**
* Returns a wrapped version of the constructor that logs a deprecation warning when the constructor is instantiated.
* @param {Function} constructorFn the constructor function to wrap
* @param {string} displayName the name of the fn to be displayed in the message
* @param {DeprecationOptions} options
* @return {Function} wrapping the original function
*/
function deprecateConstructor(constructorFn, displayName, options) {
options = options || {};
options.deprecationType = options.deprecationType || 'JS';
var deprecatedConstructor = deprecateFunctionExpression(constructorFn, displayName, options);
deprecatedConstructor.prototype = constructorFn.prototype;
$.extend(deprecatedConstructor, constructorFn); //copy static methods across;
return deprecatedConstructor;
}
var supportsProperties = false;
try {
if (Object.defineProperty) {
Object.defineProperty({}, 'blam', { get: function () {}, set: function () {} });
supportsProperties = true;
}
// eslint-disable-next-line no-unused-vars
} catch (e) {
/* IE8 doesn't support on non-DOM elements */
}
/**
* Wraps a "value" object property in a deprecation warning in browsers supporting Object.defineProperty
* @param {Object} obj the object containing the property
* @param {string} prop the name of the property to deprecate
* @param {DeprecationOptions} options
*/
function deprecateValueProperty(obj, prop, options) {
if (supportsProperties) {
var oldVal = obj[prop];
options = options || {};
options.deprecationType = options.deprecationType || 'JS';
var displayNameOrShowMessageFn = options.displayName || prop;
var showDeprecationMessage = getShowDeprecationMessage(displayNameOrShowMessageFn, options);
Object.defineProperty(obj, prop, {
get: function () {
showDeprecationMessage();
return oldVal;
},
set: function (val) {
oldVal = val;
showDeprecationMessage();
return val;
},
});
}
}
/**
* Wraps an object property in a deprecation warning, if possible. functions will always log warnings, but other
* types of properties will only log in browsers supporting Object.defineProperty
* @param {Object} obj the object containing the property
* @param {string} prop the name of the property to deprecate
* @param {DeprecationOptions} options
*/
function deprecateObjectProperty(obj, prop, options) {
if (typeof obj[prop] === 'function') {
options = options || {};
options.deprecationType = options.deprecationType || 'JS';
var displayNameOrShowMessageFn = options.displayName || prop;
obj[prop] = deprecateFunctionExpression(obj[prop], displayNameOrShowMessageFn, options);
} else {
deprecateValueProperty(obj, prop, options);
}
}
/**
* Wraps all an objects properties in a deprecation warning, if possible. functions will always log warnings, but other
* types of properties will only log in browsers supporting Object.defineProperty
* @param {Object} obj the object to be wrapped
* @param {string} objDisplayPrefix the object's prefix to be used in logs
* @param {DeprecationOptions} options
* @param {string} options.alternativeNamePrefix a prefix for the alternative property name. Used to generate alternativeName per property.
*/
function deprecateAllProperties(obj, objDisplayPrefix, options) {
options = options || {};
for (var attr in obj) {
if (has.call(obj, attr)) {
options.deprecationType = options.deprecationType || 'JS';
options.displayName = objDisplayPrefix + attr;
options.alternativeName =
options.alternativeNamePrefix && options.alternativeNamePrefix + attr;
deprecateObjectProperty(obj, attr, $.extend({}, options));
}
}
}
function matchesSelector(el, selector) {
return (
el.matches ||
el.msMatchesSelector ||
el.webkitMatchesSelector ||
el.mozMatchesSelector ||
el.oMatchesSelector
).call(el, selector);
}
function handleAddingSelector(options) {
return function (selector) {
var selectorMap = {
selector: selector,
options: options || {},
};
// Search if matches have already been added
var matches = document.querySelectorAll(selector);
for (var i = 0; i < matches.length; i++) {
logCssDeprecation(selectorMap, matches[i]);
}
observeFutureChange(selectorMap);
};
}
/**
* Return a function that logs a deprecation warning to the console the first time it is called from a certain location.
* It will also print the stack frame of the calling function.
*
* @param {string|Array} selectors a selector or list of selectors that match deprecated markup
* @param {DeprecationOptions} options
*/
function deprecateCSS(selectors, options) {
if (typeof selectors === 'string') {
selectors = [selectors];
}
selectors.forEach(handleAddingSelector(options));
}
function testAndHandleDeprecation(newNode) {
return function (selectorMap) {
if (matchesSelector(newNode, selectorMap.selector)) {
logCssDeprecation(selectorMap, newNode);
}
};
}
const deprecatedSelectorMap = [];
let observer;
function observeFutureChange(selectorMap) {
deprecatedSelectorMap.push(selectorMap);
// Lazily instantiate a mutation observer because they're expensive.
if (!observer) {
observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
// TODO - should this also look at class changes, if possible?
var addedNodes = mutation.addedNodes;
for (var i = 0; i < addedNodes.length; i++) {
var newNode = addedNodes[i];
if (newNode.nodeType === 1) {
deprecatedSelectorMap.forEach(testAndHandleDeprecation(newNode));
}
}
});
});
var config = {
childList: true,
subtree: true,
};
observer.observe(document, config);
}
}
globalize('deprecate', {
fn: deprecateFunctionExpression,
construct: deprecateConstructor,
css: deprecateCSS,
prop: deprecateObjectProperty,
obj: deprecateAllProperties,
propertyDeprecationSupported: supportsProperties,
getMessageLogger: getShowDeprecationMessage,
});
export {
deprecateFunctionExpression as fn,
deprecateConstructor as construct,
deprecateCSS as css,
deprecateObjectProperty as prop,
deprecateAllProperties as obj,
supportsProperties as propertyDeprecationSupported,
getShowDeprecationMessage as getMessageLogger,
};