define('core/utils',[
'jquery',
'underscore',
'core/config',
'core/utils/browser',
'core/utils/isMobileUserAgent',
], function (
$,
_,
config,
browser,
isMobileUserAgent
) {
'use strict';
var doc = window.document;
// From https://github.com/ryanseddon/H5F/blob/039588f4b1d8513b09a49f903d70ff269435d4b3/src/H5F.js#L20
var emailPatt = /^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
var validateEmail = function (email) {
return emailPatt.test(email);
};
/*
* This uses the same url RegExp as Bleach version 1.0.3 and returns
* the links it finds. This is not a direct match to Bleach, as there is no
* true HTML parsing, but it is sufficient for finding urls.
*/
// TODO: config.TLDS is included to support an Embed override. Embed gets
// an up-to-date list every build via manifest.yaml and the interpolator;
// an equivalent system should be incorporated in next-core
var TLDS = config.TLDS ||
'zw|zuerich|zone|zm|zip|za|yt|youtube|yokohama|yoga|yodobashi|ye|' +
'yandex|yachts|xyz|xxx|xin|xerox|wtf|wtc|ws|world|works|work|wme|win|' +
'williamhill|wiki|wien|whoswho|wf|weir|wedding|wed|website|webcam|' +
'watch|wang|wales|vu|voyage|voto|voting|vote|vodka|vn|vlaanderen|' +
'vision|villas|video|viajes|vi|vg|vet|versicherung|ventures|vegas|ve|' +
'vc|vacations|va|uz|uy|us|uol|uno|university|uk|ug|ua|tz|tw|tv|tui|' +
'tt|trust|travel|training|trading|trade|tr|toys|town|tours|toshiba|' +
'toray|top|tools|tokyo|today|to|tn|tm|tl|tk|tj|tirol|tires|tips|' +
'tienda|tickets|theater|th|tg|tf|tennis|temasek|tel|technology|tech|' +
'team|td|tc|taxi|tax|tattoo|tatar|taipei|sz|systems|sydney|sy|sx|' +
'swiss|sv|suzuki|surgery|surf|support|supply|supplies|sucks|su|style|' +
'study|st|sr|spreadbetting|spiegel|space|soy|sony|solutions|solar|' +
'sohu|software|social|so|sn|sm|sl|sky|sk|sj|site|singles|si|shriram|' +
'show|shoes|shiksha|sh|sg|sexy|sex|sew|services|sener|seat|se|sd|' +
'scot|science|schwarz|schule|school|scholarships|schmidt|scb|sca|sc|' +
'sb|saxo|sarl|sap|samsung|sale|saarland|sa|ryukyu|rw|run|ruhr|ru|' +
'rsvp|rs|rodeo|rocks|ro|rip|rio|rich|reviews|review|restaurant|rest|' +
'republican|report|repair|rentals|rent|ren|reit|reisen|reise|rehab|' +
'redstone|red|recipes|realtor|re|racing|quebec|qpon|qa|py|pw|pub|pt|' +
'ps|property|properties|prof|productions|prod|pro|press|praxi|pr|' +
'post|porn|poker|pohl|pn|pm|plus|plumbing|place|pl|pk|pizza|pink|' +
'pictures|pictet|pics|piaget|physio|photos|photography|photo|philips|' +
'pharmacy|ph|pg|pf|pe|party|parts|partners|paris|panerai|page|pa|ovh|' +
'otsuka|osaka|organic|org|oracle|ooo|online|onl|ong|one|om|okinawa|' +
'nz|nyc|nu|ntt|nrw|nra|nr|np|no|nl|nissan|ninja|nico|ni|nhk|ngo|ng|' +
'nf|nexus|news|new|neustar|network|net|nec|ne|nc|navy|name|nagoya|' +
'nadex|na|mz|my|mx|mw|mv|museum|mu|mtpc|mtn|mt|ms|mr|mq|mp|movie|mov|' +
'motorcycles|moscow|mortgage|mormon|money|monash|moe|moda|mobi|mo|mn|' +
'mma|mm|ml|mk|mini|mil|miami|mh|mg|menu|memorial|meme|melbourne|meet|' +
'media|me|md|mc|marriott|markets|marketing|market|mango|management|' +
'maison|maif|madrid|ma|ly|lv|luxury|luxe|lu|ltda|lt|ls|lr|love|lotto|' +
'lotte|london|lol|loans|loan|lk|link|limo|limited|lighting|life|lidl|' +
'liaison|li|lgbt|legal|leclerc|lease|lds|lc|lb|lawyer|latrobe|lat|' +
'land|lacaixa|la|kz|kyoto|ky|kw|kred|krd|kr|kp|komatsu|koeln|kn|km|' +
'kiwi|kitchen|kim|ki|kh|kg|ke|kddi|kaufen|juegos|jp|joburg|jobs|jo|' +
'jm|jewelry|jetzt|je|jcb|java|iwc|it|is|irish|ir|iq|io|investments|' +
'international|int|insure|institute|ink|ing|info|infiniti|industries|' +
'in|immobilien|immo|im|il|ifm|ie|id|icu|ibm|hu|ht|hr|how|house|' +
'hosting|host|horse|honda|homes|holiday|holdings|hockey|hn|hm|hk|hiv|' +
'hitachi|hiphop|hermes|here|help|healthcare|haus|hangout|hamburg|gy|' +
'gw|guru|guitars|guide|guge|gu|gt|gs|gripe|green|gratis|graphics|gr|' +
'gq|gp|gov|gop|google|goog|goo|golf|goldpoint|gold|gn|gmx|gmo|gmail|' +
'gm|globo|global|gle|glass|gl|gives|gifts|gift|gi|gh|ggee|gg|gf|gent|' +
'ge|gdn|gd|gbiz|gb|garden|gallery|gal|ga|futbol|furniture|fund|' +
'frogans|frl|fr|foundation|forsale|forex|football|foo|fo|fm|fly|' +
'flsmidth|flowers|florist|flights|fk|fj|fitness|fit|fishing|fish|' +
'firmdale|financial|finance|film|fi|feedback|fashion|farm|fans|fan|' +
'faith|fail|express|exposed|expert|exchange|everbank|events|eus|' +
'eurovision|eu|et|estate|esq|es|erni|er|equipment|epson|enterprises|' +
'engineering|engineer|energy|emerck|email|eg|ee|education|edu|ec|eat|' +
'dz|dvag|durban|download|doosan|domains|doha|dog|docs|do|dnp|dm|dk|' +
'dj|discount|directory|direct|digital|diet|diamonds|dev|design|desi|' +
'dentist|dental|democrat|delivery|degree|deals|de|dclk|day|datsun|' +
'dating|date|dance|dad|dabur|cz|cyou|cymru|cy|cx|cw|cv|cuisinella|cu|' +
'cruises|crs|cricket|creditcard|credit|cr|courses|country|coop|cool|' +
'cooking|contractors|consulting|construction|condos|computer|company|' +
'community|com|cologne|college|coffee|codes|coach|co|cn|cm|club|' +
'clothing|clinic|click|cleaning|claims|cl|ck|city|citic|ci|church|' +
'chrome|christmas|chloe|cheap|chat|channel|ch|cg|cfd|cfa|cf|cern|ceo|' +
'center|cd|cc|cbn|catering|cat|casino|cash|casa|cartier|cars|careers|' +
'career|care|cards|caravan|capital|capetown|canon|cancerresearch|' +
'camp|camera|cal|cafe|cab|ca|bzh|bz|by|bw|bv|buzz|business|builders|' +
'build|budapest|bt|bs|brussels|brother|broker|bridgestone|br|' +
'boutique|boo|bond|boats|bo|bnpparibas|bn|bmw|bm|blue|bloomberg|' +
'blackfriday|black|bj|biz|bio|bingo|bike|bid|bi|bh|bg|bf|best|berlin|' +
'beer|be|bd|bbc|bb|bayern|bauhaus|bargains|barclays|barclaycard|bar|' +
'bank|band|ba|az|axa|ax|aw|autos|auto|audio|auction|au|attorney|at|' +
'associates|asia|as|arpa|army|archi|ar|aquarelle|aq|apartments|ao|' +
'android|an|amsterdam|am|alsace|allfinanz|al|airforce|aig|ai|agency|' +
'ag|afl|af|aero|ae|adult|ads|ad|actor|active|accountants|accountant|' +
'accenture|academy|ac|abogado|abbott|abb';
// Url RegExp
var URL_RE = new RegExp(
// http://
// Note: First group must be removed from match when
// processing, in python it was a look-behind.
'([^@.]|^)\\b(?:\\w[\\w-]*:/{0,3}(?:(?:\\w+:)?\\w+@)?)?' +
// xx.yy.tld
'([\\w-]+\\.)+(?:' + TLDS + ')(?!\\.\\w)\\b' +
// optional port
'(?::\\d+)?' +
// /path/zz (excluding "unsafe" chars from RFC 1738,
// except for # and ~, which happen in practice)
'(?:[/?][^\\s\\{\\}\\|\\\\\\^\\[\\]`<>"\\x80-\\xFF\\x00-\\x1F\\x7F]*)?',
// Flags.
// Note: ignore case is NOT set because Bleach version 1.0.3 does not
// set it. This is a bug, resulting in urls being ignored if the TLD
// is capitalized. However, it is repeated here for consistency with
// the server; if we update Bleach we should update this method as well.
'g'
);
var isUrl = function (urlStr) {
return Boolean(urlStr.match(URL_RE));
};
// Protocol RegExp
var PROTO_RE = /^[\w-]+:\/{0,3}/;
// Punctuation RegExp.
// This is global so that lastIndex is set.
var PUNCT_RE = /([.,]+)$/g;
var bleachFindUrls = function (message) {
var urlRecord = [];
var match, matchedStr, punctMatch, href, group1;
if (!message) {
return urlRecord;
}
while (match = URL_RE.exec(message)) { // eslint-disable-line no-cond-assign
matchedStr = match[0];
group1 = match[1];
// Remove first group of match (see comment in URL_RE)
matchedStr = matchedStr.slice(group1.length);
// Remove trailing punctuation
PUNCT_RE.lastIndex = 0;
punctMatch = PUNCT_RE.exec(matchedStr);
if (punctMatch) {
matchedStr = matchedStr.slice(0, matchedStr.length - punctMatch[0].length);
}
// Normalize protocol
if (PROTO_RE.test(matchedStr)) {
href = matchedStr;
} else {
href = 'http://' + matchedStr;
}
// Store match
var index = match.index + group1.length;
urlRecord.push({
text: matchedStr,
url: href,
index: index,
endIndex: index + matchedStr.length,
});
}
return urlRecord;
};
// Regex to match sequence of 1 or more punctuation
// characters at the end of a string
// See: www.unicode.org/charts/
var terminatingPuncRe = new RegExp('[' +
// Basic Latin (ASCII punctuation blocks)
'\\u0021-\\u002F' +
'\\u003A-\\u0040' +
'\\u005B-\\u0060' +
'\\u007B-\\u007E' +
// Latin-1 Supplement (Latin-1 punctuation)
'\\u00A1-\\u00BF' +
// General Punctuation
'\\u2010-\\u2027' +
'\\u2030-\\u205E' +
// Miscellaneous technical
'\\u2300-\\u23FF' +
// Supplemental punctuation
'\\u2E00-\\u2E7F' +
// CJK Symbols and Punctuation
'\\u3001-\\u303F' +
// Vertical forms
'\\uFE10-\\uFE19' +
// CJK Compatibility Forms
'\\uFE30-\\uFE4F' +
// Small form variants
'\\uFE50-\\uFE6B' +
// Fullwidth ASCII Forms
'\\uFF01-\\uFF0F' +
'\\uFF1A-\\uFF20' +
'\\uFF3B-\\uFF40' +
'\\uFF5B-\\uFF60' +
// Halfwidth CJK Punctuation
'\\uFF5F-\\uFF64' +
']+$');
var MAX_TRUNCATION_RATIO = 0.5;
// Given a character limit (upperLimit) return the whole text
// if it fits within the limit, otherwise try to truncate
// within that limit without chopping off a word halfway or
// returning text with dangling punctuation but also without
// truncating the text more than halfway.
var niceTruncate = function (text, upperLimit) {
if (text.length <= upperLimit) {
return text;
}
text = text.slice(0, upperLimit - 1); // account the ellipsis char
var max = text;
// There's no way to know if the last sequence of chars is
// a whole word or not, so scan the sliced text backwards
// for the first whitespace character and truncate there
// Greedily match everything up to the last chunk of whitespace
var matches = /(^.*\S)\s/.exec(text);
if (matches) {
text = matches[1];
}
var endPunctuation = terminatingPuncRe.exec(text);
// Remove any trailing punctuation
if (endPunctuation) {
text = text.slice(0, text.length - endPunctuation[0].length);
}
// If the further truncation above caused the text we're returning
// to be less than 1/2 what it was before further truncation, we undo
if (text.length < MAX_TRUNCATION_RATIO * max.length) {
text = max;
}
return text + '\u2026'; // Ellipsis
};
var _hasStyleProperty = (function () {
var el = doc.createElement('fakeelement');
return function (_cssName, jsName) {
return el.style[jsName] !== undefined;
};
}());
// http://stackoverflow.com/questions/5023514/how-do-i-normalize-css3-transition-functions-across-browsers
var transitionEndEvent = (function () {
var transitions = {
transition: 'transitionend',
OTransition: 'otransitionend',
MozTransition: 'transitionend',
WebkitTransition: 'webkitTransitionEnd',
};
return _.find(transitions, _hasStyleProperty) || null;
})();
// https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Using_CSS_animations/Detecting_CSS_animation_support
var animationEndEvent = (function () {
var animations = {
animation: 'animationend',
OAnimation: 'oAnimationEnd',
MozAnimation: 'animationend',
WebkitAnimation: 'webkitAnimationEnd',
};
return _.find(animations, _hasStyleProperty) || null;
})();
/**
* Utility helper for DOM event handlers that automatically calls preventDefault for you
*
* @param {function} handler The event handler to be wrapped/modified
* @returns {function} The new, augmented event handler that prevents the default action for the event
*/
function preventDefaultHandler(handler) {
return function (evt) {
if (evt && evt.preventDefault) {
evt.preventDefault();
}
return handler.apply(this, arguments);
};
}
/**
* Utility helper for DOM event handlers that automatically calls stopPropagation for you
*
* @param {function} handler The event handler to be wrapped/modified
* @returns {function} The new, augmented event handler that stops the propagation of the event
*/
function stopPropagationHandler(handler) {
return function (evt) {
if (evt && evt.stopPropagation) {
evt.stopPropagation();
}
return handler.apply(this, arguments);
};
}
function stopEventHandler(handler) {
return preventDefaultHandler(stopPropagationHandler(handler));
}
function getDomain(url) {
// Extracts hostname from url, without the www prefix.
if (!url) {
return '';
}
// Force the protocol to http.
// This resolves an issue where some browsers do not extract
// the hostname if the url does not use a typical protocol.
// Forcing a protocol is also required since if missing the
// url will be resolved as a relative url on our domain.
url = 'http://' + url.replace(/^([a-z+.-]+:)?\/+/i, '');
var link = doc.createElement('a');
link.href = url;
// Remove www prefix and any trailing numbers.
// Since getDomain is used only for display purposes this is a
// safe operation.
var domain = link.hostname.replace(/^www\d*\./i, '');
// Normalize capitalization since browsers are inconsistent
// about this.
domain = domain.toLowerCase();
return domain;
}
function serializeArgs(params) {
var pcs = [];
_.each(params, function (val, key) {
if (val !== undefined) {
pcs.push(key + (val === null ? '' : '=' + encodeURIComponent(val)));
}
});
return pcs.join('&');
}
function serialize(url, params, nocache) {
if (params) {
if (url.indexOf('?') === -1) {
url += '?';
} else if (url.charAt(url.length - 1) !== '&') {
url += '&';
}
url += this.serializeArgs(params);
}
if (nocache) {
var ncp = {};
ncp[(new Date()).getTime()] = null;
return this.serialize(url, ncp);
}
var len = url.length;
return url.charAt(len - 1) === '&' ? url.slice(0, len - 1) : url;
}
/**
* window.open wrapper
*
* IMPORTANT: must obey same contract as window.open
* i.e., same function signature, same return value
*
* We wrap it because it is not possible to stub `window.open`
* reliably across browsers for testing, which causes issues from time to time.
*
* @param {string} url The URL for the new popup window
* @param {string} [title] The title of the new popup window
* @param {Object} [options] Options for the new window such as height and width. The following are overridden:
* location, status, resizable, scrollbars for UX and cross-browser consistency.
* @returns {window|null} The reference to the newly created popup window, or `null` if popup is blocked.
*/
function openWindow(url, title, options) {
// Don't add any defaults if options object is not provided. Not using
// options is the only way to default to opening a new tab instead of a
// window when using `window.open`
if (options) {
_.extend(options, {
'location': 1,
'status': 1,
'resizable': 1,
'scrollbars': 1,
});
} else {
options = {};
}
if (options.width && options.height) {
// By default, position window in the middle of the browser screen
_.defaults(options, {
left: window.screen.width / 2 - options.width / 2,
top: window.screen.height / 2 - options.height / 2,
});
}
var optionsStr = _.map(options, function (val, key) {
return key + '=' + val;
}).join(',');
return window.open(url, title, optionsStr);
}
/**
* Safely insert a string into another string at specified index.
* Spaces will be guaranteed to surround the inserted string, except
* at the beginning of the new string.
*
* @param {string} string The string to be inserted into
* @param {number} index The position to insert into
* @param {string} stringToInsert The string to be inserted
*
* @returns {string} The newly crafted string
*/
function insertWithWhitespace(string, index, stringToInsert) {
if (index < 0) {
index = 0;
}
var before = string.substring(0, index);
var after = string.substring(index);
// Place a space before the new string if necessary
if (before.length && !/\s$/.test(before)) {
before += ' ';
}
// Place a space after the new string if necessary
// Will also place a space if at the end of the text
if (!/^\s/.test(after)) {
after = ' ' + after;
}
return before + stringToInsert + after;
}
/**
* Check if a clicked link will open a new window or tab.
*
* We don't have to delay link clicks if they will open a new tab via
* command / shift / control key modifiers or if the 'target' attribute is '_blank'.
*
* @param {$.Event} evt The event object from the original click event.
* @param {jQuery} $link jQuery wrapped link object.
* @returns {boolean}
*/
var willOpenNewWindow = function (evt, $link) {
if (!$link) {
$link = $(evt.currentTarget);
}
return $link.attr('target') === '_blank' ||
evt.ctrlKey || // For Windows and Linux
evt.metaKey || // For OS X
evt.shiftKey || // For new window
evt.altKey; // God knows what
};
var LINK_CLICK_RECENCY_TIMEOUT = 500; /* ms */
/**
* Determine if a click event should be logged to our analytics servers.
*
* We currently log clicks only if the link's href is intended to cause
* the parent page to change locations (disregarding modifier keys, etc.).
*
* @param {$.Event} evt The jQuery event object.
* @param {jQuery} $link jQuery wrapped link object.
* @returns {boolean}
*/
var clickShouldBeLogged = (function () {
var logged = {};
var urlFragmentReg = /#.*$/;
var getLinkTrackingId = function ($link) {
// Because a link's href may be modified during the course
// of a click event, we need to attach our own IDs.
var id = $link.attr('data-tid');
if (!id) {
id = _.uniqueId();
$link.attr('data-tid', id);
}
return id;
};
return function (evt, $link) {
// We don't do anything if we're already handling this event elsewhere.
if (evt.isDefaultPrevented()) {
return false;
}
// We don't bother if the clicked element is not a link.
if (!$link.is('a')) {
return false;
}
// We don't bother if the clicked link doesn't have an href attribute,
// or if the clicked link's href attribute is just a hash.
var href = ($link.attr('href') || '').replace(urlFragmentReg, '');
if (!href) {
return false;
}
// Because we re-trigger clicks, it is very possible that we're being
// asked to log the same click twice. We keep an internal hash of
// links clicks we've logged within the last .5s so that we don't
// enter an infinite click loop.
var linkId = getLinkTrackingId($link);
var timestamp = new Date().getTime();
// If we recently delayed this click, then bail out now.
if (logged[linkId] && timestamp - logged[linkId] < LINK_CLICK_RECENCY_TIMEOUT) {
return false;
}
logged[linkId] = timestamp;
return true;
};
})();
/**
* This is a workaround for creating a deep-clone, since underscore.clone
* and Object.assign only create shallow-copies
* NOTE: This function is incompatible with values that don't have an
* equivalent type in JSON (ie: Date, Function, Infinity). Additionally
* undefined properties will be omitted from the cloned object.
*
* Ref: https://github.com/jashkenas/underscore/pull/595#issuecomment-34858191
*
* @param {*} original - The item that will be cloned
* @returns {*}
*/
function deepClone(original) {
if (typeof original === 'undefined') {
return null;
}
return JSON.parse(JSON.stringify(original));
}
return {
validateEmail: validateEmail,
isUrl: isUrl,
bleachFindUrls: bleachFindUrls,
niceTruncate: niceTruncate,
transitionEndEvent: transitionEndEvent,
animationEndEvent: animationEndEvent,
isMobileUserAgent: isMobileUserAgent,
preventDefaultHandler: preventDefaultHandler,
stopPropagationHandler: stopPropagationHandler,
stopEventHandler: stopEventHandler,
getDomain: getDomain,
serializeArgs: serializeArgs,
serialize: serialize,
openWindow: openWindow,
insertWithWhitespace: insertWithWhitespace,
willOpenNewWindow: willOpenNewWindow,
clickShouldBeLogged: clickShouldBeLogged,
deepClone: deepClone,
browser: browser,
};
});
// https://c.disquscdn.com/next/next-core/core/utils.js