Created
October 31, 2017 14:31
-
-
Save alies-dev/2b2bad8347c7d0848188f586097fb200 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @module modules/courseAdInjector | |
* @description Inject advertisement block to the document using specified rules | |
* @example courseAdInjector.addCourseAd('.js-courseAd-area'); | |
*/ | |
define(['jquery', 'modules/countdown'], ($, Countdown) => { | |
/** | |
* @returns {Object} | |
* @private | |
*/ | |
function getDefaultOptions() { | |
return { | |
nthElementPosition: 6, | |
minWordsInElement: 60, | |
immediateChildrenOnly: true, | |
elementType: 'P', | |
mixPixelsBetweenTwoAds: Math.max(window.screen.height * 2, 1000), | |
}; | |
} | |
/** | |
* Merge custom options with default | |
* | |
* @private | |
* @param {Object} customOptions | |
* @returns {Object} | |
*/ | |
function getOptions(customOptions) { | |
customOptions = customOptions || {}; | |
const options = $.extend({}, getDefaultOptions(), customOptions); | |
options.elementType = options.elementType.toUpperCase(); | |
return options; | |
} | |
/** | |
* Calculate and return number of words in a given string | |
* @param {string} str | |
* @returns {number} Number of words in the string | |
* @private | |
*/ | |
function countWords(str) { | |
str = str.replace(/(^\s*)|(\s*$)/gi, ''); // exclude start and end white-space | |
str = str.replace(/[ ]{2,}/gi, ' '); // 2 or more space to 1 | |
str = str.replace(/\n /, '\n'); // exclude newline with a start spacing | |
return str.split(' ').length; | |
} | |
/** | |
* Calculate and return number of words in a given HTMLElement | |
* @private | |
* @param {HTMLElement} element | |
* @returns {number} Number of words in HTMLElement | |
*/ | |
function countWordsInElement(element) { | |
const $element = $(element); | |
const text = $element.text(); | |
return countWords(text); | |
} | |
/** | |
* Returns an array of HTML elements which satisfy the individual conditions | |
* and next sibling is also satisfy the individual conditions. | |
* E.g. if we have A + B + C elements in a DOM tree (located in a row), | |
* and all of them are satisfy the individual conditions, then this method | |
* returns [A,B, C] array. | |
* Thus, ad can be injected right AFTER A, B or C elements. | |
* | |
* @private | |
* @param {string} holderSelector | |
* @param {Object} options | |
* @returns {Array<HTMLElement>} | |
*/ | |
function getPreviousSiblings(holderSelector, options) { | |
/** | |
* REQUIREMENT SPECIFICATIONS FOR PLACING ADS: | |
* | |
* - There should be at least 6 paragraphs before course ads placed. | |
* - It should be as simple as possible to change the number of paragraphs needed before the course ads placed. | |
* Thus, if we want to change the minimum number of paragraphs needed before we can place course ads in the | |
* future, we should be able to do it easily. | |
* - Course ads should be placed after paragraph (e.g, <p>TEXT</p>COURSE ADS.<anytag>TEXT</anytag>). | |
* - Paragraph before the course ads should be longer than 50 words each, or equivalent number of characters. | |
* - The course ads should not be shown to logged-in members. | |
* - If the conditions are not met, then course ads should NOT be placed. | |
*/ | |
const $elements = $(holderSelector + (options.immediateChildrenOnly ? '>' : ' ') + options.elementType); | |
const elementsFiltered = []; | |
if ($elements.length < options.nthElementPosition) { | |
return elementsFiltered; | |
} | |
$elements.each((index, element) => { | |
// filter out first nthElementPosition elements | |
if (index < options.nthElementPosition) { | |
return; | |
} | |
// filter out by previous siblings type/tag | |
const previousElementSibling = element.previousElementSibling; | |
if (previousElementSibling === null || previousElementSibling.tagName !== options.elementType) { | |
return; | |
} | |
// filter out by next siblings type/tag | |
const nextElementSibling = element.nextElementSibling; | |
if (nextElementSibling === null || nextElementSibling.tagName !== options.elementType) { | |
return; | |
} | |
// filter out by words count | |
const wordsInElementText = countWordsInElement(element); | |
if (wordsInElementText <= options.minWordsInElement) { | |
return; | |
} | |
elementsFiltered.push(element); | |
}); | |
const elementsFilteredHaveFilteredSibling = []; | |
$.each(elementsFiltered, (index, element) => { | |
// filter out last element (which has no filtered nextElementSibling) | |
if (index === elementsFiltered.length + 1) { | |
return; | |
} | |
// select only elements that have filtered next sibling | |
const nextFilteredElement = elementsFiltered[index + 1]; | |
if (element.nextElementSibling === nextFilteredElement) { | |
elementsFilteredHaveFilteredSibling.push(element); | |
} | |
}); | |
return elementsFilteredHaveFilteredSibling; | |
} | |
/** | |
* Use API to get course ad block (in HTML). | |
* @see \IDF\Http\Controllers\Courses\CourseController::getCoursesAdCode | |
* | |
* @private | |
* | |
* @param {string} publicationType | |
* @param {string} publicationId | |
* | |
* @returns {Promise<{html: string}>} | |
*/ | |
function getAdCode(publicationType, publicationId) { | |
return window | |
.fetch(`/courses/ad-code?publicationType=${publicationType}&publicationId=${publicationId}`, { | |
method: 'GET', | |
credentials: 'same-origin', | |
// cache: 'no-cache', | |
headers: { | |
Accept: 'application/json', | |
'X-CSRF-TOKEN': window.csrfToken, | |
}, | |
}) | |
.then(response => response.json()); | |
} | |
/** | |
* Generate a template for countdown | |
* | |
* @private | |
* @param {Date} endDate | |
* @returns {string} Template for Countdown | |
*/ | |
function getCountdownTemplate(endDate) { | |
const now = new Date(); | |
const diffInMilliSecs = endDate.getTime() - now.getTime(); | |
// 1000 * 60 * 60 * 24 = 86400000 (1 day) | |
if (diffInMilliSecs > 86400000) { | |
return `<span>in <span class="day"> | |
<span class="number">{days}</span> | |
<span class="unit">days</span></span> | |
</span>`; | |
} | |
// 1000 * 60 * 60 = 3600000 (1hour) | |
if (diffInMilliSecs > 3600000) { | |
return `<span>in | |
<span class="hour"><span class="number">{hours}</span> | |
<span class="unit">hrs</span></span> <span class="minute"><span class="number">{minutes}</span> | |
<span class="unit">mins</span></span> <span class="second"><span class="number">{seconds}</span> | |
<span class="unit">secs</span></span> | |
</span>`; | |
} | |
// 1000 * 60 = 60000 (1min) | |
if (diffInMilliSecs > 60000) { | |
return `<span>in <span class="minute"><span class="number">{minutes}</span> | |
<span class="unit">mins</span> </span><span class="second"><span class="number">{seconds}</span> | |
<span class="unit">secs</span></span> | |
</span>`; | |
} | |
// 1000 (1sec) | |
if (diffInMilliSecs > 1000) { | |
return `<span>in <span class="second"><span class="number">{seconds}</span> <span class="unit">secs</span> | |
</span></span>`; | |
} | |
return `<span>Enrollment is closed</span>`; | |
} | |
/** | |
* Inject Ad block and run countdowns for courses | |
* | |
* @private | |
* @param {jQuery} $adBlock | |
* @param {jQuery} $siblingPrevious | |
*/ | |
function injectAdAndRunCountdowns($adBlock, $siblingPrevious) { | |
$siblingPrevious.after($adBlock); | |
// run countdown for each course in ad block | |
const clocks = $('.js-schedule-period', $adBlock); | |
$.each(clocks, (index, clock) => { | |
const $courseRow = $(clock).closest('.js-courseAdItem'); | |
const endDate = new Date($courseRow.data('startTimestamp') * 1000); | |
const $clockElement = $courseRow.find('.js-schedule-period'); | |
new Countdown({ | |
// eslint-disable-line no-new | |
selector: `#${$clockElement.attr('id')}`, | |
dateEnd: endDate, | |
msgAfter: '', | |
msgPattern: getCountdownTemplate(endDate), | |
onEnd: () => { | |
$courseRow.hide(); | |
}, | |
}); | |
}); | |
} | |
/** | |
* Determenate whether Ad block can be added to the given element. | |
* @param {HTMLElement} element HTMLElement to potentially inject ad | |
* @param {Array} previousSiblingsWithAd Previous Siblings With Ad | |
* @param {number} viewportSize Minimum space between 2 different ad blocks | |
* @returns {boolean} | |
*/ | |
function getIsAdCanBeInjectedToViewport(element, previousSiblingsWithAd, viewportSize) { | |
if (previousSiblingsWithAd.length === 0) { | |
return true; | |
} | |
const lastElementHasAdAfter = previousSiblingsWithAd[previousSiblingsWithAd.length - 1]; | |
const previousAdBlock = lastElementHasAdAfter.nextElementSibling; | |
const previousAdBlockBottomCoordinate = previousAdBlock.offsetTop + previousAdBlock.offsetHeight; | |
return element.offsetTop > previousAdBlockBottomCoordinate + viewportSize; | |
} | |
/** | |
* Finds qualifying previous siblings for ad elements and (if any) get ad code using ajax and add it | |
* after the first qualifying sibling | |
* | |
* @param {string} holderSelector | |
* @param {string} publicationType | |
* @param {string} publicationId | |
* @param {Object} customOptions see full list in this::getDefaultOptions() | |
*/ | |
function addCourseAd(holderSelector, publicationType, publicationId, customOptions) { | |
const options = getOptions(customOptions); | |
const previousSiblings = getPreviousSiblings(holderSelector, options); | |
if (!previousSiblings.length) { | |
return; | |
} | |
getAdCode(publicationType, publicationId).then(parsedJsonResponseData => { | |
const previousSiblingsWithAd = []; | |
$.each(previousSiblings, (index, previousSibling) => { | |
const isAdCanBeInjectedToViewport = getIsAdCanBeInjectedToViewport( | |
previousSibling, | |
previousSiblingsWithAd, | |
options.mixPixelsBetweenTwoAds | |
); | |
if (!isAdCanBeInjectedToViewport) { | |
// skip current element because ad is already injected exist in viewport | |
return; | |
} | |
const $adBlock = $(parsedJsonResponseData.html); | |
injectAdAndRunCountdowns($adBlock, $(previousSibling)); | |
previousSiblingsWithAd.push(previousSibling); | |
}); | |
}); | |
} | |
return /** @alias module:modules/courseAdInjector */ { | |
/** | |
* Finds qualifying previous siblings for ad elements and (if any) get ad code using ajax and add it | |
* after the first qualifying sibling | |
* | |
* @method | |
* @param {string} holderSelector | |
* @param {Object} customOptions see full list in this::getDefaultOptions() | |
*/ | |
addCourseAd: addCourseAd, | |
}; | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment