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