[]<![CDATA[ "use strict" pencilscroller(); var counter = 0; /* * Pencil Carousel */ function pencilscroller() { if (document.getElementById('fixedpencil-subs')) { carousel(); } else if (counter 0 || !!navigator.userAgent.match(/Trident.*rv:11./)); /* * Get Cookie – to read any cookie */ function getCookie(cname) { var name = cname + “=”; var decodedCookie = decodeURIComponent(document.cookie); var ca = decodedCookie.split(‘;’); for (var i = 0; i -1) { hidearcpencil(); } /* * Returns whether or not localStorage is supported/settable in current browser * @returns {Boolean} */ function localStorageIsSupported() { const testKey = “test”; const localStorageExists = “localStorage” in window; if (!localStorageExists) { // localStorage is not a property of window return false; } try { window.localStorage.setItem(testKey, “1”); window.localStorage.removeItem(testKey); console.log(“[Darwin bg] Local storage is supported”); return true; } catch (error) { // Local storage does not exist or is full. console.log(“[Darwin bg] Local storage is unsupported”); return false; } } /* * Detect if the browser is running in Private Browsing mode * @export * @returns {Promise} */ function isPrivateMode() { return new Promise(function (resolve) { const on = function () { return resolve(true); } // is in private mode const off = function () { return resolve(false); } // not private mode const testLocalStorage = function () { try { if (localStorage.length) off(); else { localStorage.x = 1; localStorage.removeItem(‘x’); off(); } } catch (e) { // Safari only enables cookie in private mode // if cookie is disabled then all client side storage is disabled // if all client side storage is disabled, then there is no point // in using private mode navigator.cookieEnabled ? on() : off(); } }; // Chrome & Opera if (window.webkitRequestFileSystem) { return void window.webkitRequestFileSystem(0, 0, off, on); } // Firefox if (‘MozAppearance’ in document.documentElement.style) { const db = indexedDB.open(‘test’); db.onerror = on; db.onsuccess = off; return void 0; } // Safari const isSafari = navigator.userAgent.match(/Version/([0-9._]+).*Safari/); if (isSafari) { const version = parseInt(isSafari[1], 10); if (version >= 11) { try { var dbname = “hasDB”; window.openDatabase(dbname, “1.0”, dbname, 0); return off(); } catch (_) { return on(); }; } else if (version < 11) { return testLocalStorage(); } } // if (/constructor/i.test(window.HTMLElement)) { // return testLocalStorage(); // } // IE10+ & Edge if (!window.indexedDB && (window.PointerEvent || window.MSPointerEvent)) { return on(); } // others return off(); }); } const supportsLocalStorage = localStorageIsSupported(); /* * In Incognito set a local storage item */ if (notIE && supportsLocalStorage) { isPrivateMode().then(function (isPrivate) { localStorage.setItem('isPrivate', isPrivate); }); } /* * Adblock check * @returns {Boolean} */ function isAdBlocked() { var adElement = document.getElementsByClassName('c-ad')[0]; var isadblock = adElement ? !(adElement.offsetWidth || adElement.offsetHeight || adElement.getClientRects() .length) : true; return isadblock; } /* * Hide any elements */ function hideUnit(testid, featureid) { setDarwinState(testid, 'hasClosed', true); document.getElementById(featureid).className = 'u-hidden'; event.target.setAttribute("aria-expanded", false); } /* * Call any api to get the corresponding results * @returns {Object} */ function getapi(url, compare) { if (window.navigator.userAgent.indexOf("MSIE ") === -1) { return fetch(url) .then(function (resp) { return resp.json(); }) .then(function (data) { //cem1706 if (compare.test === 'cem1706') { return { data } } }) .catch(function (error) { console.err('fetch error: ', error) return error; }); } else { return false; } } /* * Call graphiql to get the corresponding results * @returns {Object} */ function getapi_graphiql(url, compare) { if (window.navigator.userAgent.indexOf("MSIE ") === -1) { return fetch(url, { method: "POST", body: compare[0] }) .then(function (resp) { return resp.json(); }) .then(function (data) { var output = []; output[0] = compare[1]; output[1] = data; document.cookie = "getloginsessions_api= " + JSON.stringify(output) + ";" + "expires=" + new Date(new Date() .getTime() + 60 * 60 * 1000 * 24).toGMTString() + ";path=/"; return getArticle(data); }) .catch(function (error) { return error; }); } else { return false; } } /* * Call to flatten the results from api * @returns {Object} */ function flattenObj(data) { var result = {}; function recurse(cur, prop) { if (Object(cur) !== cur) { result[prop] = cur; } else if (Array.isArray(cur)) { for (var i = 0, l = cur.length; i

[]

Search stocks, ETFs and Commodities []More stories below advertisement

Earnings

[]Corporate earnings are provided from Zacks Investment Research, including Income Statements, Balance Sheets, Cash Flow Statements, and Statement of Retained Earnings.

StockReports+

[]A premium report that provides detailed quantitative assessments of earnings, fundamentals, relative valuation, risk, price momentum and more. For subscribers only.

StockCalc

[]What is this stock worth in intrinsic value? View this premium StockCalc report to see this stock’s valuation and how it was calculated using fundamental analysis. For subscribers only.

ValuEngine

[]A stock valuation and forecasting report include rating, fair value assessment, return forecasts, market ratio-based valuations and comparable analysis. Available for free with registration.

[]More stories below advertisement

-1; const isHermes = getSegment(‘hermesData.takeover’, true); var keytarWall = getSegment(‘article.paywallStatus’, ‘red’) || (getSegment(‘article.paywallStatus’, ‘yellow’) && meterCount !== null && (JSON.parse(meterCount).split(/[(,*?)]/).length === 6)) var isRightArticle = isHermes ? getSegment(‘hermesData.wall’, ‘redwall’) : keytarWall; var isRightSection = unit == ‘paywall’ ? isRightArticle : !isRightArticle; var isNotAdv = isArticle ? getSegment(‘article.advContent’, false) : true; const isNotProduct = !getSegment(“page.hierarchy”, “globeproducts”) && !getSegment(“page.hierarchy”, “newsletters”); var isQA = window.location.href.indexOf(“CEMQA1339_”+unit)>-1; var count = count || 1; if(darwinState.hasOwnProperty(“cem1339”) && count ==1 && darwinState.cem1339.variant){ count +=1; setDarwinVariant(‘products-cem1339’, darwinState.cem1339.variant); } return isNotSubscriber && (isRightSection || isQA) && isNotPb && isNotAdv && isNotProduct; } }]); } window.tgam.darwin.tests.segments.push(dwSegment_productscem1339); function dwExecute_productscem1339(data) { window._dw.push([‘global’, ‘ready’, function onReady(api) { var variant = api.get(“products-cem1339”); var variantData = data; const isQA = window.location.href.indexOf(‘CEMQA1339’) > -1; if (isQA) { var variant = window.location.search.split(‘-‘)[1].split(‘&’)[0]; variantDisplay(variant); }else if (variant.is(‘control’)) { console.info(‘products:cem-1339:control’); variantDisplay(‘control’); setUnitState(“cem1339”, “control”); } else if (variant.is(‘experience’)) { console.info(‘products:cem-1339:experience’); variantDisplay(‘experience’); setUnitState(“cem1339”, “experience”); } function variantDisplay(testVar) { // if pencil or paywall var segment = api.get(‘products-cem1339’)[0].getTest().segments; var paywall = ”; var paywallType = ”; if (segment[0] === ‘nonsub-paywall’) { paywall = { ‘red’: { ‘name’: ‘red’, ‘type’: ‘hardPaywall’ }, ‘thresh’: { ‘name’: ‘thresh’, ‘type’: ‘meteredPaywall’ } }; paywallType = getSegment(‘article.paywallStatus’, ‘red’) ? paywall.red : paywall.thresh; displayPaywall(paywallType, testVar); } else if (segment[0] === ‘nonsub-pencil’) { displayPencil(testVar); } } }]); } window.tgam.darwin.tests.before.push(dwExecute_productscem1339); })(); ]]> <![CDATA[ var paywallInnerHTML = ''; // add the unit to the page function displayPaywall(paywall_type, testVar) { paywallInnerHTML = [ ' ‘, ”, ‘ This content is available to globeandmail.com subscribers.’, ‘]]>’, ”, ‘ ‘, ‘ []Join a national community of curious and ambitious Canadians’, ‘

‘, ‘ ‘, ‘ ‘, ‘ []Subscribe to globeandmail.com for unlimited access to’, ‘ Canada’s’, ‘ leading independent’, ‘ journalism.

‘, ‘ ‘, ‘ [] Just$1.99’, ‘

‘, ‘ [] per week for first 24 weeks

‘, ‘ ‘, ‘ ‘, ‘ []Cancel Anytime

‘, ‘ ‘, ‘ ‘, ‘ ‘, ‘ ‘, ‘ ‘, ‘ ‘, ”, ”, ].join(”); // add tracking for paywall display here pushAnalytics(‘cem1339’, ‘paywall’, ‘display’, ‘display’, testVar, ‘nonsub-paywall’); const renderPaywall = function () { return paywallInnerHTML; } setKeytarWall(paywall_type.type, ‘teaser’, { template: renderPaywall }); } // Replace the Pencil unit function displayPencil(testVar) { if (document.getElementById(‘fixedpencil-subs’)) { // update pencil url var redirectURL = ‘https://subscribe.theglobeandmail.com/#/digital?cid=pencil_cem1339-‘ + testVar; document.querySelector(‘#bottom_subspencil-n button’).setAttribute(‘onclick’, ‘window.open(“‘ + redirectURL + ‘”)’); document.querySelector(‘#mb-fixedpencil a’).href = redirectURL; if (testVar === ‘experience’) { var node = document.createElement(‘div’); node.setAttribute(“id”, “subscancel-label”); node.innerHTML = [ ‘Cancel Anytime’, ” ].join(”); document.getElementsByClassName(‘subsOffer-cta’)[0].appendChild(node); } // add tracking for pencil display pushAnalytics(‘cem1339’, ‘pencil’, ‘display’, ‘display’, testVar, ‘nonsub-pencil’); } else { hidearcpencil(); var newNode = document.createElement(‘div’); newNode.setAttribute(“id”, “fixedpencil”); var refNode = document.getElementById(“pb-root”); refNode.parentNode.insertBefore(newNode, refNode.nextSibling); var clickString = ”’ + testVar + ”,’pencil”; document.getElementById(‘fixedpencil’).innerHTML = [” ].join(”); // add tracking for pencil display pushAnalytics(‘cem1339’, ‘pencil’, ‘display’, ‘display’, testVar, ‘nonsub-pencil’); } } 768;var isNotPb=window.location.href.indexOf(“/ellipsis/”)===-1&&window.location.href.indexOf(“/pb/”)===-1;var isNotProduct=!getSegment(“page.hierarchy”,”globeproducts”)&&!getSegment(“page.hierarchy”, “newsletters”)&&!getSegment(“page.hierarchy”,”welcomeregister”);var isArticle=window.location.href.indexOf(“/article”)>-1;var isHermes=tgam.datalayer.hermesData;var meterCount=localStorage.getItem(“tgam.keytar.wall.subscriber.free”);var isPaywallArticle=isArticle?isHermes?getSegment(“hermesData.wall”,”paywall”):getSegment(“article.paywallStatus”,”red”)||getSegment(“article.paywallStatus”,”yellow”)&&meterCount!==null&&JSON.parse(meterCount).split(/[(,*?)]/).length===6:false;var isRegwallArticle=isArticle? isHermes?getSegment(“hermesData.wall”,”regwall”):getSegment(“article.paywallStatus”,”red”)||getSegment(“article.paywallStatus”,”yellow”)&&meterCount!==null&&JSON.parse(meterCount).split(/[(,*?)]/).length===3:false;var isNotAdv=isArticle?getSegment(“article.advContent”,false):true;var isNotInteractive=isArticle?!getSegment(“article.pagesubtype”,”interactive”)&&!getSegment(“article.pagesubtype”,”longread”):true;var isSectionOrNoWallArticle=isArticle?!isPaywallArticle&&!isRegwallArticle:true;var isQA= window.location.href.indexOf(“CEMQA1920”)>-1;var count=count||1;if(darwinState.hasOwnProperty(“cem1920”)&&count===1&&darwinState.cem1920.variant){count+=1;setDarwinVariant(“promotions-cem1920”,darwinState.cem1920.variant)}console.log(“cem-1920 segment start: “,notIE&&isNotSubscriber&&isNotPb&&isNotInteractive&&isNotProduct&&isNotAdv&&isSectionOrNoWallArticle&&(isQA||isDesktop));return notIE&&isNotSubscriber&&isNotPb&&isNotInteractive&&isNotProduct&&isNotAdv&&isSectionOrNoWallArticle&&(isQA||isDesktop)})}])} window.tgam.darwin.tests.segments.push(dwSegment_promotionscem1920);function dwExecute_promotionscem1920(data){window._dw.push([“global”,”ready”,function onReady(api){var variant=api.get(“promotions-cem1920”);var variantData=data;var isQA=window.location.href.indexOf(“CEMQA1920”)>-1;var isArticlePage=window.location.href.indexOf(“/article”)>-1;var sectionName=window.tgam&&window.tgam.datalayer&&window.tgam.datalayer.page&&window.tgam.datalayer.page&&window.tgam.datalayer.page.hierarchy.split(“:”)[0]|| null;var specialTopics=isArticlePage?window.tgam.datalayer.article.keywords.includes(“health”)?”health”:window.tgam.datalayer.article.keywords.includes(“china”)?”china”:window.tgam.datalayer.article.keywords.includes(“indigenous”)?”ethnicity”:window.tgam.datalayer.article.label.includes(“investigation”)?”investigations”:window.tgam.datalayer.page.hierarchy.split(“:”)[1]?”personalfinance”:null:sectionName;var journalist=getSectionJournalist(specialTopics?specialTopics:sectionName);var darwinState= getDarwinState()||{};var isFirstTwoVisit=darwinState.hasOwnProperty(“cem1920″)&&darwinState.cem1920.hasSeenUnit ‘, ‘ ‘, ‘ ‘, ‘ ‘, ‘ ‘, ”, ” ].join(”); // for control, render exit popup if (variant === ‘control’) { bioEp.init({ html: html, width: 608, cookieExp: 0, delay: 0, onPopup: function () { pushAnalytics(“cem1920”, “promotions”, “popupDisplay”, “popupDisplay”, variant, “popup”) } }); } else { // for experience, render scroll popup document.addEventListener(“scroll”, scrollHelper); function scrollHelper() { var p = document.body.parentNode var pos = (document.body.scrollTop || p.scrollTop) / (p.scrollHeight – p.clientHeight) * 100; console.log(pos); // scroll percentage 52% if (pos >= 52 && !hasSeenPopup) { renderExperiencePopup(html) hasSeenPopup = true; window.removeEventListener(“scroll”, scrollHelper); } } } } // render experience popup, function edited from previous interstitial test function renderExperiencePopup(html) { var expHtml = [‘‘ ].join(“”) + html; document.getElementsByTagName(‘body’)[0].className += ” u-no-scroll modal-open”; var newNode = document.createElement(‘div’); newNode.setAttribute(‘id’, ‘exit-popup’); newNode.className = ‘o-modal-outer-container o-modal-outer-container–opened’; document.getElementById(‘pb-root’).appendChild(newNode); document.getElementById(‘exit-popup’).innerHTML = expHtml; // dark background: var bgdark = document.createElement(‘div’); bgdark.setAttribute(“class”, “c-lightbox”); bgdark.setAttribute(“id”, “firstvisit-bgdark”); var refNode = document.getElementById(“exit-popup”); refNode.parentNode.insertBefore(bgdark, refNode); pushAnalytics(“cem1920”, “promotions”, “popupDisplay”, “popupDisplay”, “experience”, “popup”) } function popupClickSubscribe(variant) { pushAnalytics(“cem1920”, “promotions”, “clickSubscribe”, “clickSubscribe”, variant, “popup”) var url = ‘https://subscribe.theglobeandmail.com/#/digital?intcmp=promotions_cem1920-‘ + variant; window.location.href = url; } function closeExperiencePopup() { document.getElementById(‘exit-popup’).className = ‘ u-hidden’; document.getElementById(‘firstvisit-bgdark’).className = ‘ u-hidden’; document.getElementsByTagName(‘body’)[0].classList.remove(‘u-no-scroll’, ‘modal-open’); pushAnalytics(“cem1920”, “promotions”, “popupClose”, “popupClose”, “experience”, “popup”) } [] -1&&window.location.href.indexOf(“cem1891”)>-1){var variant=localStorage.getItem(“Dw__header-cem1891|global”);var regwallbtn=document.getElementsByClassName(“js-newsletter-wall-register”);var registerbtn=document.getElementsByClassName(“js-onclick–register”);if(registerbtn)for(i=0;i

<![CDATA[ ;(function() { window._dw = window._dw || []; function dwSegment_UXTestARC7231() { window._dw.push(['global', 'before', function onBefore(api) { api.segment("default7231", function seg() { console.info("[ARC-7231] Segment started"); var darwinState = getDarwinState() || {}; var oldTest = "7130"; var newTest = "7231"; var oldVariant = darwinState.hasOwnProperty("arc" + oldTest) && darwinState["arc" + oldTest].variant || null; var newVariant = darwinState.hasOwnProperty("arc" + newTest) && darwinState["arc" + newTest].variant || null; if (oldVariant === "control") { // For debugging only console.info("[ARC-7231] Old test was control"); } if (oldVariant === "variant") { // If user was in the "variant" segment of the old test, assign them to the same segment for the new test // (we don't want to assign them to the "control" segment becuase we're trying to increase traffic) console.info("[ARC-7231] Assigning new test to same segment as old test: ARC-" + newTest + " & ARC-" + oldTest + " to " + oldVariant); setDarwinVariant("UXTest-ARC" + newTest, "variant"); } else if (newVariant) { // New test: keep user in the same segment group if they've previously encountered the test console.info("[ARC-7231] New test previously encountered, assigning: ARC-" + newTest + " to " + newVariant); setDarwinVariant("UXTest-ARC" + newTest, newVariant); } // Exclude IE users var ua = window.navigator.userAgent.toLowerCase(); var isModernBrowser = ua.indexOf("msie") === -1 && ua.indexOf("trident") === -1; console.info("[ARC-7231] isModernBrowser: " + isModernBrowser); // Registered users and subscribers only var shouldRun = isModernBrowser && (getSegment("identity.role.id", 1) || getSegment("identity.role.id", 2)); console.info("[ARC-7231] Segment returned: " + shouldRun); return shouldRun; }); }]); } window.tgam.darwin.tests.segments.push(dwSegment_UXTestARC7231); function dwExecute_UXTestARC7231(data) { window._dw.push(['global', 'ready', function onReady(api) { var variant = api.get("UXTest-ARC7231"); var variantData = data; // DOM elements var siteHeader; var scrollMenuContainer; var scrollMenuList; var scrollMenuLeftMask; var scrollMenuRightMask; var overlayTrigger; var overlayCloseButtons = []; var overlay; // Classes var hiddenClass = "c-your-globe–hidden"; var hidingClass = "c-your-globe–hiding"; var revealingClass = "c-your-globe–revealing"; var noScrollClass = "u-no-scroll–not-fixed"; var overlayTriggerDotClasses = [ "c-your-globe__trigger–dot–unread", "c-your-globe__trigger–dot–no-follow" ]; var keyLocalStorageOld = "tgam.darwin.arc7130.globeOverlayDate"; var keyLocalStorage = "tgam.globeOverlayDate"; // State (for throttled requestAnimationFrame) var waiting = false; // ************************************************ // Helper functions // ************************************************ // Get one element function qs(selectors, baseElement) { var base = baseElement || window.document; return base.querySelector(selectors); } // Get an array of elements function qsa(selectors, baseElement) { var base = baseElement || window.document; return [].slice.call(base.querySelectorAll(selectors)); } // Get the user guid function getUserGuid() { return window.tgam && window.tgam.datalayer && window.tgam.datalayer.identity && window.tgam.datalayer.identity.guid; } // Get the environment function getEnv() { var isProdOrStage = window.tgam.env.isProdOrStage; var env = isProdOrStage === true ? "prd" : "dev"; return env; } // ************************************************ // Functions to construct the story cards // ************************************************ /** * Get the label data if exists * @param {Object} article – that contains label * @returns {Object} of label */ function getLabel(article) { var labelText = ""; if (article.section_label && article.section_label !== "") { labelText = article.section_label.toLowerCase(); } if ( article.label && article.label.basic && article.label.basic.display === true && article.label.basic.text && labelText == "" ) { labelText = article.label.basic.text.toLowerCase(); } return labelText; } /** * Get all the data to display an article's photo * @param {Object} article – that contains image * @param {String} label – story label * @returns {Object} of image */ function getImage(article, label) { var path = null; var alt = null; var isOpinion = label.text === "opinion" || article.subtype === "column"; // Show author image if opinion/column article if (isOpinion) { if ( article.authors && article.authors.length && article.authors[0].metadata && article.authors[0].metadata.image ) { // This image is preferred because it has been resized path = article.authors[0].metadata.image; alt = article.authors[0].byline; } else if ( article.credits && article.credits.by && article.credits.by.length && article.credits.by[0].image && article.credits.by[0].image.url ) { path = article.credits.by[0].image.url; alt = article.credits.by[0].name; } if (path) { return { path: path, alt: alt }; } else { path = null; } } // Article image alt = article.promo_items && article.promo_items.basic && article.promo_items.basic.caption ? article.promo_items.basic.caption : ""; if ( article.pictureRel && article.pictureRel.length && article.pictureRel[0].url200 ) { // This image is preferred because it has been resized path = article.pictureRel[0].url200; if (!alt) { alt = article.pictureRel[0].caption; } } else if ( article.promo_items && article.promo_items.basic && article.promo_items.basic.url ) { path = article.promo_items.basic.url; } if (path) { return { path: path, alt: alt }; } return { path: null, alt: null }; } /** * Generate a template for the time. Based on PB dateDisplay.tag Note that `js-moment` stuff has not * been copied over, but we still add a class below. A future enhancement would be to complete * implementing this feature. I don't think it's worth it at this point, just for Darwin tests. * @param {Object} article – to process * @returns {String} */ function displayDateTag(article) { var isSectionPageContext = ["story", "video"].indexOf(window.tgam.meta.pagetype) === -1; var storyPublishedDate = storyPublishedDateTag(article); if (!storyPublishedDate || !window.Intl || !window.Intl.DateTimeFormat) { return ""; } var storyDate = storyTimeDisplayTag({ pubdate: storyPublishedDate, update: article.last_updated_date, isSectionPageContext: isSectionPageContext }); var publishedDate = storyDate.published; // Month D, YYYY var updatedDate = storyDate.updated; var hideUpdate = storyDate.field === "published"; var publishedPreface = isSectionPageContext ? "" : "Published "; var updatedPreface = "Updated" + (isSectionPageContext ? "" : " "); var publishedClassNames = storyDate.moment === "published" || storyDate.moment === "both" ? " jx-story-moment" : ""; var updatedClassNames = (storyDate.moment === "updated" || storyDate.moment === "both" ? " jx-story-moment" : "") + (hideUpdate ? " u-visually-hidden" : ""); updatedClassNames = updatedClassNames + (isSectionPageContext ? " c-timestamp–updated" : ""); // "Time ago" timestamp var date = since(storyPublishedDate); if (date === "") { // If "time ago" is empty (means article is older than three days) display the date date = publishedDate; } return [ "", publishedPreface, date, "", " ", // keep space for layout "", updatedPreface, (isSectionPageContext ? "" : updatedDate), "" ].join(""); } // Based on PB storyTimeDisplay.tag function storyPublishedDateTag(story) { return story.display_date || story.publish_date; } // Based on PB storyTimeDisplay.tag function storyTimeDisplayTag(params) { var isSectionPageContext = true; var pubDate = new Date(params.pubdate); var upDate = new Date(params.update); var now = new Date(); var oneYear = 31536000; var oneDay = 86400; var threeDays = 259200; var tenMinutes = 600; var unixNow = Math.round(now.getTime() / 1000); var unixPubdate = Math.round(pubDate.getTime() / 1000); var unixUpdate = Math.round(upDate.getTime() / 1000); var timeSincePublication = unixNow – unixPubdate; var timeSinceUpdate = unixNow – unixUpdate; var pattern = { year: "numeric", month: "long", day: "numeric", timeZone: "America/Toronto" }; if (params.format) { pattern = params.format; } else if (isSectionPageContext && timeSincePublication < oneYear) { pattern = { month: "long", day: "numeric", timeZone: "America/Toronto" }; } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat var formatter = new Intl.DateTimeFormat("en-US", pattern); var formattedPubDate = formatter.format(pubDate); var formattedUpDate = formatter.format(upDate); var publishedAndUpdatedAreTheSameDay = formattedPubDate === formattedUpDate; var publishedLessThanThreeDaysAgo = timeSincePublication tenMinutes); var showPublishedDateAsMoment = publishedLessThanThreeDaysAgo; var showUpdatedDateAsMoment = !isSectionPageContext && storyHasBeenUpdatedMoreThanTenMinutesSincePublication && updatedWasLessThanThreeDaysAgo; var field = “published”; // Which fields to show (published, updated, or both) var moment = null; // Which fields to show in relative date (i.e., moment) format if (storyHasBeenUpdatedMoreThanTenMinutesSincePublication) { if (publishedAndUpdatedAreTheSameDay && timeSinceUpdate < oneDay) { field = "both"; // Don't show updated date since it's the same as published date. } } if (showUpdatedDateAsMoment) { moment = "updated"; if (showPublishedDateAsMoment) { moment = "both"; } } else if (showPublishedDateAsMoment) { moment = "published"; } return { published: formattedPubDate, updated: formattedUpDate, field: field, moment: moment }; } // Show "time ago" timestamp function humanTime(left, right) { var amount = parseInt(right – left).toFixed(0); var ONE_MINUTE = 60; var ONE_HOUR = 3600; var ONE_DAY = 86400; var THREE_DAYS = 259200; var plural; if (amount < ONE_MINUTE) { plural = amount === 1 ? "" : "s"; return amount + " second" + plural + " ago"; } if (amount = ONE_HOUR && amount = ONE_DAY && amount “, “” ].join(“”); } return template; } /** * Generate a template for the label * @param {String} label – the label text * @returns {String} */ function displayLabel(label) { var template = “”; if (label === “opinion”) { template = [ “[]”, ” “, label, “”, “

“, ].join(“”); } return template; } /** * Display “Follow”, “Following” buttons * @param {Object} tData * @returns {String} */ function displayFollowingButton(tData) { var topicName = tData.topicName; var topicSlug = tData.topicSlug; var topicType = tData.topicType; var topicImg = tData.topicImg; var isAuthor = topicType === “author”; var authorImg = isAuthor && topicImg ? “” : “”; var template = isAuthor ? “/authors/” : “/topics/”; var href = window.tgam.env.baseRootAbsoluteUrl + template + topicSlug; var linkClasses = isAuthor ? “c-topic-link c-topic-link–author” : “c-topic-link”; return [ ” “, ].join(“”); } /** * Generates story card markup * @param {Object} article – to display * @param {Object} topicData – (topicName, topicSlug, topicType, topicVariation, topicImg) * @param {Boolean} addFollowButton * @returns {String} story card markup */ function storyCard(article, topicData, addFollowButton) { // console.info(“[ARC-7231] storyCard”, { article: article, topic: topicData }); if (!article || !topicData) { return “”; } var tName = topicData.topicName; var tType = topicData.topicType; var tVariation = topicData.topicVariation; var followingTopic; var timeTemplate; var analyticsModifier = tType + “: “; if (addFollowButton) { // Display a follow button beside the topic name followingTopic = displayFollowingButton(topicData); timeTemplate = displayDateTag(article); } var label = getLabel(article); var image = getImage(article, label); var labelTemplate = displayLabel(label); var imageTemplate = displayImage(image); var headline = article.headlines.basic; var href = window.tgam.env.baseUrl + article.canonical_url; var sophiId = article._id; var dataAnalyticsClick = JSON.stringify({ type: “link”, feature: “following feed”, contentId: sophiId, label: analyticsModifier + tName.toLowerCase() + “: ” + headline.toLowerCase(), page: “sec:homepage:personalized feed:” + tVariation, hierarchy: 1 }); var cardMarkup = “”; if (!followingTopic) { cardMarkup = [ “”, ” “, ” []”, ” “, tName, “”, ”

“, ” []”, ” “, headline, “”, ”

“, labelTemplate, ” “, ” []”, imageTemplate, “

“, “” ].join(“”); } else { cardMarkup = [ ” []”, followingTopic, ”

“, ” “, ” “, ” []”, ” “, headline, “”, ”

“, ” “, timeTemplate, “”, ” “, ” []”, imageTemplate, “

“, ” “, ].join(“”); } return [ ” []”, cardMarkup, ”

“, ].join(“”); } /** * Adds the overlay trigger dot class * @param {String} type – “unread” or “no-follow” */ function addOverlayTriggerDot(type) { overlayTriggerDotClasses.forEach(function fn(dotClass) { if (dotClass === “c-your-globe__trigger–dot–” + type) { overlayTrigger.classList.add(dotClass); } else { overlayTrigger.classList.remove(dotClass); } }); overlayTrigger.classList.add(“c-your-globe__trigger–dot”); } /** * Removes the overlay trigger dot class */ function removeOverlayTriggerDot() { overlayTriggerDotClasses.forEach(function fn(dotClass) { overlayTrigger.classList.remove(dotClass); }); overlayTrigger.classList.remove(“c-your-globe__trigger–dot”); } /** * Returns a heading element to be displayed inside the overlay * @param {String} text * @returns {String} */ function overlayLabel(text) { return “

” + text + “

“; } var overlayHeadingHasFollowed = ( overlayLabel(“The latest in topics and authors you follow”) + “[]View more in Following

” ); var overlayHeadingNoFollowed = ( overlayLabel(“Get started: build your personal news feed”) + “

    ” + “

  1. Follow topics relevant to your reading interests.
  2. ” + “

  3. Check back here or your Following page to view the latest articles on your topics.
  4. ” + “

” ); var upToDateMessage = “[]You’re up to date on your Following feed. Check again later for new stories.

“; // ************************************************ // Parse API response and inject markup into overlay // ************************************************ // Story card markup /** * Stories originally came from the following locations in the API response: * – data.articles[i].items[i].topics * – data.articles[i].items[i].authors * @param {Array} stories * @returns {String} story card markup */ function latestStoryCards(stories) { console.info(“[ARC-7231] Display latest stories based on these stories:”, stories); var storyCards = stories.map(function fn(story) { // The “topics” and “authors” arrays will only contain one item // (i.e. the topic or author that the user is following) var topic; var topicData; if (story.topics && story.topics.length) { // Normal topic topic = story.topics[0]; topicData = { topicName: topic.name, topicSlug: topic.slug, topicType: “topic”, topicVariation: “following” }; } else if (story.authors && story.authors.length) { // Author topic topic = story.authors[0]; // Only authors have images associated with them – normal topics do not var authorImg = topic.metadata && topic.metadata.image ? topic.metadata.image : null; topicData = { topicName: topic.byline, topicSlug: topic.slug, topicType: “author”, topicVariation: “following”, topicImg: authorImg }; } // Don’t display a follow button beside the topic because the user is already following it return storyCard(story, topicData, false); }).join(“”); return storyCards; } /** * @param {Array} topics * @param {String} variation – “recommended” or “trending” (used for the click tracking analytics) * @returns {String} story card markup */ function recommendedTrendingStoryCards(topics, variation) { var uniqueStories = generateUniqueStory(topics); var storyCards = uniqueStories.map(function fn(topic) { // Grab one article var story = topic.items[0]; // Only authors have images associated with them – normal topics do not var authorImg = topic.authorTopic && topic.authorMetadata && topic.authorMetadata.image ? topic.authorMetadata.image : null; // Normally we’d display all of the topics that are assigned to an article, but // this API groups articles by topic, so we only have access to that one topic. // Even if we could display additional topics, we wouldn’t want to becuase having // multiple follow buttons would junk up the UI. var topicData = { topicName: topic.name, topicSlug: topic.slug, topicType: topic.authorTopic ? “author” : “topic”, topicVariation: variation, topicImg: authorImg }; // Display the follow button beside the topic because we’re suggesting new topics to follow return storyCard(story, topicData, true); }).join(“”); return storyCards; } /** * Topics originally came from the following locations in the API response: * – data.recommendedAuthors * – data.recommendedTopics * @param {Array} topics * @returns {String} story card markup */ function recommendedStoryCards(topics) { console.info(“[ARC-7231] Display recommended stories based on these topics:”, topics); return recommendedTrendingStoryCards(topics, “recommended”); } /** * Topics originally came from the following location in the API response: * – data.trendingTopics * @param {Array} topics * @returns {String} story card markup */ function trendingStoryCards(topics) { console.info(“[ARC-7231] Display trending stories based on these topics:”, topics); return recommendedTrendingStoryCards(topics, “trending”); } // Markup inside the overlay /** * @param {Array} stories * @returns {Object} markup for the overlay’s header and body content areas */ function showLatestStories(stories) { console.info(“[ARC-7231] Scenario: latest stories”); var storyCardsMarkup = latestStoryCards(stories); return { header: overlayHeadingHasFollowed, body: storyCardsMarkup }; } /** * @param {Array} topics * @returns {Object} markup for the overlay’s header and body content areas */ function upToDateShowRecommended(topics) { console.info(“[ARC-7231] Scenario: up to date, show recommended”); var storyCardsMarkup = recommendedStoryCards(topics); return { header: overlayHeadingHasFollowed, body: ( upToDateMessage + overlayLabel(“Recommended for you”) + storyCardsMarkup ) }; } /** * @param {Array} topics * @returns {Object} markup for the overlay’s header and body content areas */ function upToDateShowTrending(topics) { console.info(“[ARC-7231] Scenario: up to date, show trending”); var storyCardsMarkup = trendingStoryCards(topics); return { header: overlayHeadingHasFollowed, body: ( upToDateMessage + overlayLabel(“Trending topics to follow”) + storyCardsMarkup ) }; } /** * @param {Array} topics * @returns {Object} markup for the overlay’s header and body content areas */ function notFollowingShowRecommended(topics) { console.info(“[ARC-7231] Scenario: not following, show recommended”); var storyCardsMarkup = recommendedStoryCards(topics); return { header: overlayHeadingNoFollowed, body: ( overlayLabel(“Recommended for you”) + storyCardsMarkup ) }; } /** * @param {Array} topics * @returns {Object} markup for the overlay’s header and body content areas */ function notFollowingShowTrending(topics) { console.info(“[ARC-7231] Scenario: not following, show trending”); var storyCardsMarkup = trendingStoryCards(topics); return { header: overlayHeadingNoFollowed, body: ( overlayLabel(“Trending topics to follow”) + storyCardsMarkup ) }; } /** * Parse data from the personalized API and inject markup into the overlay * @see https://confluence.theglobeandmail.com/display/ARC/Logic+for+Embedded+on+Homepage++and+Your+Globe+Overlay * @param {Object} data – topic and story data provided by the API * @param {String} hashId – user’s hash id */ function parsePerzonalizedTopicsData(data, hashId) { console.info(“[ARC-7231] parsePerzonalizedTopicsData”, data); var totalTopicsFollowed = data.totalTopicsFollowed || 0; var totalAuthorsFollowed = data.totalAuthorsFollowed || 0; var recommendedTopics = data.recommendedTopics || []; var recommendedAuthors = data.recommendedAuthors || []; var latestStories = data.articles || []; var topics = []; var stories = []; var markup = “”; if (totalTopicsFollowed || totalAuthorsFollowed) { if (latestStories.length) { // Scenario 1: “Latest stories” // Count the number of stories returned (for logging/debugging purposes only) var latestStoriesCount = latestStories.reduce(function _reduceStories(accumulator, date) { var urls = date.items.map(function _mapStories(article) { return article.canonical_url; }); return accumulator.concat(urls); }, []).length; console.info(“[ARC-7231] latestStoryUrls (” + latestStoriesCount + “)”); // Fetch the user’s reading history // TODO: this API request will no longer be necessary when Data Science makes // the “isRead” property functional (it’s currently “false” 100% of the time) getReadingHistory(hashId).then(function _historyFetched(historyData) { var readingHistoryUrls = historyData.map(function _mapHistory(item) { return item.url; }); console.info(“[ARC-7231] readingHistoryUrls (” + readingHistoryUrls.length + “)”); var currentArticleUrl = window.tgam.meta.pagetype === “story” ? window.tgam.meta.urlRelative : null; // Filter out stories that exist in the user’s reading history var filteredStories = latestStories.map(function _mapDates(dateObj) { var items = dateObj.items.filter(function _filterStories(article) { var articleUrl = article.canonical_url; var shouldRemove = readingHistoryUrls.includes(articleUrl) || article.isRead || articleUrl === currentArticleUrl; if (shouldRemove) { if (articleUrl === currentArticleUrl) { console.info(“[ARC-7231] Currently reading this article, so removing: ” + articleUrl); } else { console.info(“[ARC-7231] Has already been read, so removing: ” + articleUrl); } } return !shouldRemove; }); return { date: dateObj.date, items: items }; }, []); stories = generateLatestStories(filteredStories); markup = showLatestStories(stories); var atLeastOneStoryIsNew = isLocalStorageSupported() ? checkNewStories(stories) : true; if (atLeastOneStoryIsNew) { // Display indicator only when there is a new story since the last time the user checked the feed addOverlayTriggerDot(“unread”); } populateOverlay(markup, true); }); } else { if (recommendedTopics.length || recommendedAuthors.length) { // Scenario 2: “Up to date, show recommended” topics = recommendedAuthors.concat(recommendedTopics); markup = upToDateShowRecommended(topics); } else { // Scenario 3: “Up to date, show trending” topics = data.trendingTopics || []; markup = upToDateShowTrending(topics); } populateOverlay(markup, true); } } else { topics = recommendedAuthors.concat(recommendedTopics); if (topics.length) { // Scenario 4: “Not following, show recommended” markup = notFollowingShowRecommended(topics); } else { // Scenario 5: “Not following, show trending” topics = data.trendingTopics || []; markup = notFollowingShowTrending(topics); } addOverlayTriggerDot(“no-follow”); populateOverlay(markup, false); } } /** * Populate the overlay with content and activate the “following” functionality * @param {String} markup – HTML markup * @param {Boolean} headerBorder – whether to include a header border */ function populateOverlay(markup, headerBorder) { var overlayHeader = qs(“.c-your-globe__overlay-header”); var overlayHeaderText = qs(“.c-your-globe__overlay-header-text”); var overlayBody = qs(“.c-your-globe__overlay-body”); if (!overlayHeader || !overlayHeaderText || !overlayBody) { return; } console.info(“[ARC-7231] Append markup”); var spinner = qs(“.c-spinner”); spinner && spinner.parentElement.removeChild(spinner); overlayHeaderText.insertAdjacentHTML(“afterbegin”, markup.header); overlayBody.insertAdjacentHTML(“afterbegin”, markup.body); if (!headerBorder) { overlayHeader.classList.add(“c-your-globe__overlay-header–no-border”); } addFollowingFunctionality(); } // ************************************************ // API calls // ************************************************ // Reading History API /** * Validate a response has an “ok” status, and is in the range 200-299 inclusive. * @param {Response} response – to validate * @returns {PromiseWe have more than 20 newsletters. Explore[]

[]