version 4.13.0
This commit is contained in:
238
includes/builder/scripts/stores/document.js
Normal file
238
includes/builder/scripts/stores/document.js
Normal file
@ -0,0 +1,238 @@
|
||||
// External dependencies
|
||||
import { EventEmitter } from 'events';
|
||||
import debounce from 'lodash/debounce';
|
||||
import get from 'lodash/get';
|
||||
|
||||
// Internal dependencies
|
||||
import {
|
||||
maybeDecreaseEmitterMaxListeners,
|
||||
maybeIncreaseEmitterMaxListeners,
|
||||
registerFrontendComponent,
|
||||
} from '../utils/utils';
|
||||
|
||||
|
||||
const HEIGHT_CHANGE = 'height_change';
|
||||
const WIDTH_CHANGE = 'width_change';
|
||||
const DIMENSION_CHANGE = 'dimension_change';
|
||||
|
||||
// States
|
||||
const states = {
|
||||
height: 0,
|
||||
width: 0,
|
||||
};
|
||||
|
||||
/**
|
||||
* Document store; track document height (at the moment) and its changes. Builder elements
|
||||
* should listen and get this store's value instead of directly getting it from document.
|
||||
* ETScriptDocumentStore is not exported; intentionally export its instance so there'll only be one
|
||||
* ETScriptDocumentStore instance.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*/
|
||||
class ETScriptDocumentStore extends EventEmitter {
|
||||
/**
|
||||
* ETScriptDocumentStore constructor.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.setHeight(get(document, 'documentElement.offsetHeight'));
|
||||
this.setWidth(get(document, 'documentElement.offsetWidth'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Record document height.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param {number} height
|
||||
*
|
||||
* @returns {Window}
|
||||
*/
|
||||
setHeight = height => {
|
||||
if (height === states.height) {
|
||||
return this;
|
||||
}
|
||||
|
||||
states.height = height;
|
||||
|
||||
this.emit(HEIGHT_CHANGE);
|
||||
this.emit(DIMENSION_CHANGE);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Record document width.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param {number} width
|
||||
*
|
||||
* @returns {Window}
|
||||
*/
|
||||
setWidth = width => {
|
||||
if (width === states.width) {
|
||||
return this;
|
||||
}
|
||||
|
||||
states.width = width;
|
||||
|
||||
this.emit(WIDTH_CHANGE);
|
||||
this.emit(DIMENSION_CHANGE);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get recorded document height.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
get height() {
|
||||
return states.height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recorded document width.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
get width() {
|
||||
return states.width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add document dimension change event listener.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @returns {Window}
|
||||
*/
|
||||
addDimensionChangeListener = callback => {
|
||||
maybeIncreaseEmitterMaxListeners(this, DIMENSION_CHANGE);
|
||||
this.on(DIMENSION_CHANGE, callback);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove document dimension change event listener.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @returns {Window}
|
||||
*/
|
||||
removeDimensionChangeListener = callback => {
|
||||
this.removeListener(DIMENSION_CHANGE, callback);
|
||||
maybeDecreaseEmitterMaxListeners(this, DIMENSION_CHANGE);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add document height change event listener.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @returns {Window}
|
||||
*/
|
||||
addHeightChangeListener = callback => {
|
||||
maybeIncreaseEmitterMaxListeners(this, HEIGHT_CHANGE);
|
||||
this.on(HEIGHT_CHANGE, callback);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove document height change event listener.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @returns {Window}
|
||||
*/
|
||||
removeHeightChangeListener = callback => {
|
||||
this.removeListener(HEIGHT_CHANGE, callback);
|
||||
maybeDecreaseEmitterMaxListeners(this, HEIGHT_CHANGE);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add document width change event listener.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @returns {Window}
|
||||
*/
|
||||
addWidthChangeListener = callback => {
|
||||
maybeIncreaseEmitterMaxListeners(this, WIDTH_CHANGE);
|
||||
this.on(WIDTH_CHANGE, callback);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove document width change event listener.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @returns {Window}
|
||||
*/
|
||||
removeWidthChangeListener = callback => {
|
||||
this.removeListener(WIDTH_CHANGE, callback);
|
||||
maybeDecreaseEmitterMaxListeners(this, WIDTH_CHANGE);
|
||||
return this;
|
||||
};
|
||||
}
|
||||
|
||||
// Create document store instance
|
||||
const documentStoreInstance = new ETScriptDocumentStore();
|
||||
|
||||
/**
|
||||
* Event's function callback to update document store's props
|
||||
*
|
||||
* @since 4.6.2
|
||||
*/
|
||||
function updateDocumentStoreProps() {
|
||||
const documentHeight = get(document, 'documentElement.offsetHeight');
|
||||
const documentWidth = get(document, 'documentElement.offsetWidth');
|
||||
|
||||
// Store automatically ignore if given height value is equal to the current one; so this is fine
|
||||
documentStoreInstance.setHeight(documentHeight).setWidth(documentWidth);
|
||||
}
|
||||
|
||||
// Listen to document's DOM change, debounce its callback, and update store's props
|
||||
const documentObserver = new MutationObserver(debounce(updateDocumentStoreProps, 50));
|
||||
|
||||
// Observe document change
|
||||
// @todo probably plug this on only when necessary
|
||||
// @todo also enable to plug this off
|
||||
documentObserver.observe(document, {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
// Update document store properties when Divi's fixed header transition is completed
|
||||
window.addEventListener('ETDiviFixedHeaderTransitionEnd', updateDocumentStoreProps);
|
||||
|
||||
// Register store instance as component to be exposed via global object
|
||||
registerFrontendComponent('stores', 'document', documentStoreInstance);
|
||||
|
||||
// Export store instance.
|
||||
// IMPORTANT: For uniformity, import this as ETScriptDocumentStore
|
||||
export default documentStoreInstance;
|
946
includes/builder/scripts/stores/sticky.js
Normal file
946
includes/builder/scripts/stores/sticky.js
Normal file
@ -0,0 +1,946 @@
|
||||
// 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;
|
755
includes/builder/scripts/stores/window.js
Normal file
755
includes/builder/scripts/stores/window.js
Normal file
@ -0,0 +1,755 @@
|
||||
// External dependencies
|
||||
import { EventEmitter } from 'events';
|
||||
import forEach from 'lodash/forEach';
|
||||
import get from 'lodash/get';
|
||||
import includes from 'lodash/includes';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import $ from 'jquery';
|
||||
|
||||
// Internal dependencies
|
||||
import { top_window } from '@core-ui/utils/frame-helpers';
|
||||
import ETScriptStickyStore from './sticky';
|
||||
import {
|
||||
getContentAreaSelector,
|
||||
getTemplateEditorIframe,
|
||||
} from '../../frontend-builder/gutenberg/utils/selectors';
|
||||
import { isTemplateEditor } from '../../frontend-builder/gutenberg/utils/conditionals';
|
||||
import {
|
||||
getBuilderUtilsParams,
|
||||
isBFB,
|
||||
isExtraTheme,
|
||||
isFE,
|
||||
isLBB,
|
||||
isLBP,
|
||||
isTB,
|
||||
isVB,
|
||||
maybeDecreaseEmitterMaxListeners,
|
||||
maybeIncreaseEmitterMaxListeners,
|
||||
registerFrontendComponent,
|
||||
} from '../utils/utils';
|
||||
|
||||
// Builder window
|
||||
const $window = $(window);
|
||||
const $topWindow = top_window.jQuery(top_window);
|
||||
const hasTopWindow = ! isEqual(window, top_window);
|
||||
const windowLocations = hasTopWindow ? ['app', 'top'] : ['app'];
|
||||
|
||||
// Event Constants
|
||||
const HEIGHT_CHANGE = 'height_change';
|
||||
const WIDTH_CHANGE = 'width_change';
|
||||
const SCROLL_TOP_CHANGE = 'scroll_top_change';
|
||||
const BREAKPOINT_CHANGE = 'breakpoint_change';
|
||||
const SCROLL_LOCATION_CHANGE = 'scroll_location_change';
|
||||
const VERTICAL_SCROLL_BAR_CHANGE = 'vertical_scroll_bar_change';
|
||||
|
||||
// States.
|
||||
// Private, limited to this module (ETScriptWindowStore class) only
|
||||
const states = {
|
||||
breakpoint: 'desktop',
|
||||
extraMobileBreakpoint: false,
|
||||
isBuilderZoomed: false,
|
||||
scrollLocation: getBuilderUtilsParams().onloadScrollLocation, // app|top
|
||||
scrollTop: {
|
||||
app: 0,
|
||||
top: 0,
|
||||
},
|
||||
height: {
|
||||
app: 0,
|
||||
top: 0,
|
||||
},
|
||||
width: {
|
||||
app: 0,
|
||||
top: 0,
|
||||
},
|
||||
bfbIframeOffset: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
},
|
||||
lbpIframeOffset: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
},
|
||||
verticalScrollBar: {
|
||||
app: 0,
|
||||
top: 0,
|
||||
},
|
||||
};
|
||||
|
||||
// Valid values.
|
||||
// Retrieved from server, used for validating values
|
||||
const validValues = {
|
||||
scrollLocation: [...getBuilderUtilsParams().scrollLocations],
|
||||
};
|
||||
|
||||
// Variables
|
||||
const builderScrollLocations = {
|
||||
...getBuilderUtilsParams().builderScrollLocations,
|
||||
};
|
||||
|
||||
// @todo need to change how this works since builder already have et_screen_sizes(), unless
|
||||
// we prefer to add another breakpoint functions
|
||||
const deviceMinimumBreakpoints = {
|
||||
desktop: 980,
|
||||
tablet: 767,
|
||||
phone: 0,
|
||||
};
|
||||
const bfbFrameId = '#et-bfb-app-frame';
|
||||
|
||||
/**
|
||||
* Window store.
|
||||
*
|
||||
* This store listen to direct window's events; builder callback listen to this store's events
|
||||
* to avoid dom-based calculation whenever possible; use the property passed by this store.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*/
|
||||
class ETScriptWindowStore extends EventEmitter {
|
||||
/**
|
||||
* ETScriptWindowStore constructor.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Set app window onload values
|
||||
const windowWidth = $window.innerWidth();
|
||||
const windowHeight = $window.innerHeight();
|
||||
const windowScrollTop = $window.scrollTop();
|
||||
|
||||
this.setWidth('app', windowWidth).setHeight('app', windowHeight);
|
||||
this.setScrollTop('app', windowScrollTop);
|
||||
this.setVerticalScrollBarWidth('app', (window.outerWidth - windowWidth));
|
||||
|
||||
// Set top window onload values (if top window exist)
|
||||
if (hasTopWindow) {
|
||||
const topWindowWidth = $topWindow.innerWidth();
|
||||
const topWindowHeight = $topWindow.innerHeight();
|
||||
const topWindowScrollTop = top_window.jQuery(top_window).scrollTop();
|
||||
|
||||
this.setWidth('top', topWindowWidth).setHeight('top', topWindowHeight);
|
||||
this.setScrollTop('top', topWindowScrollTop);
|
||||
this.setVerticalScrollBarWidth('top', (top_window.outerWidth - topWindowWidth));
|
||||
}
|
||||
|
||||
// Set iframe offset
|
||||
if (isBFB) {
|
||||
this.setBfbIframeOffset();
|
||||
}
|
||||
|
||||
// Set Layout Block iframe offset
|
||||
if (isLBP) {
|
||||
this.setLayoutBlockPreviewIframeOffset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set window height.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param {string} windowLocation App|top.
|
||||
* @param {number} height
|
||||
*
|
||||
* @returns {Window}
|
||||
*/
|
||||
setHeight = (windowLocation = 'app', height) => {
|
||||
if (height === states.height[windowLocation]) {
|
||||
return this;
|
||||
}
|
||||
|
||||
states.height[windowLocation] = height;
|
||||
|
||||
this.emit(HEIGHT_CHANGE);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set window width.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param {string} windowLocation App|top.
|
||||
* @param {number} width
|
||||
*
|
||||
* @returns {Window}
|
||||
*/
|
||||
setWidth = (windowLocation = 'app', width) => {
|
||||
if (width === states.width[windowLocation]) {
|
||||
return this;
|
||||
}
|
||||
|
||||
// Only app window could set breakpoint
|
||||
if ('app' === windowLocation) {
|
||||
this.setBreakpoint(width);
|
||||
|
||||
// Extra theme has its own "mobile breakpoint" (below 1024px)
|
||||
if (isExtraTheme) {
|
||||
const outerWidth = this.width + this.verticalScrollBar;
|
||||
const extraMobileBreakpoint = 1024;
|
||||
const fixedNavActivation = ! states.extraMobileBreakpoint && outerWidth >= extraMobileBreakpoint;
|
||||
const fixedNavDeactivation = states.extraMobileBreakpoint && outerWidth < extraMobileBreakpoint;
|
||||
|
||||
// Re-set element props when Extra mobile breakpoint change happens
|
||||
if (fixedNavActivation || fixedNavDeactivation) {
|
||||
states.extraMobileBreakpoint = (outerWidth >= extraMobileBreakpoint);
|
||||
|
||||
ETScriptStickyStore.setElementsProps();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
states.width[windowLocation] = width;
|
||||
|
||||
this.emit(WIDTH_CHANGE);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set scroll location value.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param {string} scrollLocation App|top.
|
||||
*
|
||||
* @returns {ETScriptWindowStore}
|
||||
*/
|
||||
setScrollLocation = scrollLocation => {
|
||||
// Prevent incorrect scroll location value from being saved
|
||||
if (! includes(validValues.scrollLocation, scrollLocation)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (scrollLocation === states.scrollLocation) {
|
||||
return this;
|
||||
}
|
||||
|
||||
states.scrollLocation = scrollLocation;
|
||||
|
||||
this.emit(SCROLL_LOCATION_CHANGE);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set scroll top value.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param {string} windowLocation App|top.
|
||||
* @param {number} scrollTop
|
||||
*
|
||||
* @returns {ETScriptWindowStore}
|
||||
*/
|
||||
setScrollTop = (windowLocation, scrollTop) => {
|
||||
if (scrollTop === states.scrollTop[windowLocation]) {
|
||||
return this;
|
||||
}
|
||||
|
||||
states.scrollTop[windowLocation] = scrollTop;
|
||||
|
||||
this.emit(SCROLL_TOP_CHANGE);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set builder zoomed status (on builder only).
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param {string} builderPreviewMode Desktop|tablet|phone|zoom|wireframe.
|
||||
*/
|
||||
setBuilderZoomedStatus = builderPreviewMode => {
|
||||
const isBuilderZoomed = 'zoom' === builderPreviewMode;
|
||||
|
||||
states.isBuilderZoomed = isBuilderZoomed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set BFB iframe offset.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*/
|
||||
setBfbIframeOffset = () => {
|
||||
states.bfbIframeOffset = top_window.jQuery(bfbFrameId).offset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Layout Block iframe offset.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*/
|
||||
setLayoutBlockPreviewIframeOffset = () => {
|
||||
const blockId = get(window.ETBlockLayoutModulesScript, 'blockId', '');
|
||||
const previewIframeId = `#divi-layout-iframe-${blockId}`;
|
||||
const $block = top_window.jQuery(previewIframeId).closest('.wp-block[data-type="divi/layout"]');
|
||||
const blockPosition = $block.position();
|
||||
const contentSelectors = [
|
||||
// WordPress 5.4
|
||||
'block-editor-editor-skeleton__content',
|
||||
|
||||
// WordPress 5.5
|
||||
'interface-interface-skeleton__content',
|
||||
];
|
||||
|
||||
let blockOffsetTop = parseInt(get(blockPosition, 'top', 0));
|
||||
|
||||
// Since WordPress 5.4, blocks list position to its parent somehow is not considered
|
||||
// Previous inserted DOM are also gone + Block item now has collapsing margin top/bottom
|
||||
// These needs to be manually calculated here since the result is no longer identical
|
||||
if (includes(contentSelectors, getContentAreaSelector(top_window, false))) {
|
||||
// Find Block List Layout. By default, it's located on editor of top window.
|
||||
// When Template Editor is active, it's "moved" to editor of iframe window.
|
||||
const $blockEditorLayout = isTemplateEditor() ? getTemplateEditorIframe(top_window).find('.block-editor-block-list__layout.is-root-container') : top_window.jQuery('.block-editor-block-list__layout');
|
||||
|
||||
// Blocks list position to its parent (title + content wrapper)
|
||||
// WordPress 5.4 = 183px
|
||||
// WordPress 5.5 = 161px
|
||||
if ($blockEditorLayout.length) {
|
||||
blockOffsetTop += $blockEditorLayout.position().top;
|
||||
}
|
||||
|
||||
// Compensating collapsing block item margin top
|
||||
blockOffsetTop += parseInt($block.css('marginTop')) || 0;
|
||||
}
|
||||
|
||||
// Admin bar in less than 600 width window uses absolute positioning which stays on top of
|
||||
// document and affecting iframe top offset
|
||||
if (600 > this.width && ETScriptStickyStore.getElementProp('wpAdminBar', 'exist', false)) {
|
||||
blockOffsetTop += ETScriptStickyStore.getElementProp('wpAdminBar', 'height', 0);
|
||||
}
|
||||
|
||||
states.lbpIframeOffset.top = blockOffsetTop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set vertical scrollbar width.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param {string} windowLocation
|
||||
* @param {number} width
|
||||
*/
|
||||
setVerticalScrollBarWidth = (windowLocation = 'app', width) => {
|
||||
if (width === states.verticalScrollBar[windowLocation]) {
|
||||
return this;
|
||||
}
|
||||
|
||||
states.verticalScrollBar[windowLocation] = width;
|
||||
|
||||
this.emit(VERTICAL_SCROLL_BAR_CHANGE);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current window width.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
get width() {
|
||||
return states.width[this.scrollLocation];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current window height.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
get height() {
|
||||
return states.height[this.scrollLocation];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current window scroll location.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @returns {string} App|top.
|
||||
*/
|
||||
get scrollLocation() {
|
||||
return states.scrollLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current window scroll top / distance to document.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
get scrollTop() {
|
||||
const multiplier = this.isBuilderZoomed ? 2 : 1;
|
||||
|
||||
let appFrameOffset = 0;
|
||||
|
||||
// Add app iframe offset on scrollTop calculation in BFB
|
||||
if (isBFB) {
|
||||
appFrameOffset += states.bfbIframeOffset.top;
|
||||
}
|
||||
|
||||
// Add Layout Block preview iframe on scrollTop calculation
|
||||
if (isLBP) {
|
||||
appFrameOffset += states.lbpIframeOffset.top;
|
||||
}
|
||||
|
||||
return (states.scrollTop[this.scrollLocation] - appFrameOffset) * multiplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current app window breakpoint (by device).
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
get breakpoint() {
|
||||
return states.breakpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get builder zoomed status.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @returns {bool}
|
||||
*/
|
||||
get isBuilderZoomed() {
|
||||
return states.isBuilderZoomed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current window vertical scrollbar width.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
get verticalScrollBar() {
|
||||
return states.verticalScrollBar[this.scrollLocation];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get builder scroll location of builder context + preview mode.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param {string} previewMode Desktop|tablet|phone|zoom|wireframe.
|
||||
*
|
||||
* @returns {string} App|top.
|
||||
*/
|
||||
getBuilderScrollLocation = previewMode => get(builderScrollLocations, previewMode, 'app')
|
||||
|
||||
/**
|
||||
* Add width change event listener.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @returns {Window}
|
||||
*/
|
||||
addWidthChangeListener = callback => {
|
||||
maybeIncreaseEmitterMaxListeners(this, WIDTH_CHANGE);
|
||||
this.on(WIDTH_CHANGE, callback);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove width change event listener.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @returns {Window}
|
||||
*/
|
||||
removeWidthChangeListener = callback => {
|
||||
this.removeListener(WIDTH_CHANGE, callback);
|
||||
maybeDecreaseEmitterMaxListeners(this, WIDTH_CHANGE);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add height change event listener.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @returns {Window}
|
||||
*/
|
||||
addHeightChangeListener = callback => {
|
||||
maybeIncreaseEmitterMaxListeners(this, HEIGHT_CHANGE);
|
||||
this.on(HEIGHT_CHANGE, callback);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove height change event listener.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @returns {Window}
|
||||
*/
|
||||
removeHeightChangeListener = callback => {
|
||||
this.removeListener(HEIGHT_CHANGE, callback);
|
||||
maybeDecreaseEmitterMaxListeners(this, HEIGHT_CHANGE);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add scroll location change event listener.
|
||||
*
|
||||
* @param callback
|
||||
* @since 4.6.0
|
||||
* @returns {ETScriptWindowStore}
|
||||
*/
|
||||
addScrollLocationChangeListener = callback => {
|
||||
maybeIncreaseEmitterMaxListeners(this, SCROLL_LOCATION_CHANGE);
|
||||
this.on(SCROLL_LOCATION_CHANGE, callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove scroll location change event listener.
|
||||
*
|
||||
* @param callback
|
||||
* @since 4.6.0
|
||||
* @returns {ETScriptWindowStore}
|
||||
*/
|
||||
removeScrollLocationChangeListener = callback => {
|
||||
this.removeListener(SCROLL_LOCATION_CHANGE, callback);
|
||||
maybeDecreaseEmitterMaxListeners(this, SCROLL_LOCATION_CHANGE);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add scroll top change event listener.
|
||||
*
|
||||
* @param callback
|
||||
* @since 4.6.0
|
||||
* @returns {ETScriptWindowStore}
|
||||
*/
|
||||
addScrollTopChangeListener = callback => {
|
||||
maybeIncreaseEmitterMaxListeners(this, SCROLL_TOP_CHANGE);
|
||||
this.on(SCROLL_TOP_CHANGE, callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove scroll top change event listener.
|
||||
*
|
||||
* @param callback
|
||||
* @since 4.6.0
|
||||
* @returns {ETScriptWindowStore}
|
||||
*/
|
||||
removeScrollTopChangeListener = callback => {
|
||||
this.removeListener(SCROLL_TOP_CHANGE, callback);
|
||||
maybeDecreaseEmitterMaxListeners(this, SCROLL_TOP_CHANGE);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set breakpoint (by device) based on window width.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @todo Update breakpoint setting mechanic so this won't need to define another screen size definition
|
||||
* and able to reuse (et_screen_size()).
|
||||
*
|
||||
* @param {number} windowWidth
|
||||
*
|
||||
* @returns {ETScriptWindowStore}
|
||||
*/
|
||||
setBreakpoint = windowWidth => {
|
||||
let newBreakpoint = '';
|
||||
|
||||
forEach(deviceMinimumBreakpoints, (minWidth, device) => {
|
||||
if (windowWidth > minWidth) {
|
||||
newBreakpoint = device;
|
||||
|
||||
// equals to "break"
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// No need to update breakpoint property if it is unchanged
|
||||
if (this.breakpoint === newBreakpoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
states.breakpoint = newBreakpoint;
|
||||
|
||||
this.emit(BREAKPOINT_CHANGE);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add breakpoint change event listener.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param {Function} callback
|
||||
*/
|
||||
addBreakpointChangeListener = callback => {
|
||||
maybeIncreaseEmitterMaxListeners(this, BREAKPOINT_CHANGE);
|
||||
this.on(BREAKPOINT_CHANGE, callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove breakpoint change event listener.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param {Function} callback
|
||||
*/
|
||||
removeBreakpointChangeListener = callback => {
|
||||
this.removeListener(BREAKPOINT_CHANGE, callback);
|
||||
maybeDecreaseEmitterMaxListeners(this, BREAKPOINT_CHANGE);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
// initiate window store instance
|
||||
const windowStoreInstance = new ETScriptWindowStore();
|
||||
|
||||
|
||||
/**
|
||||
* Listen for (app/top) window events, and update store's value
|
||||
* store is listener free; it only hold / set / get values.
|
||||
*/
|
||||
forEach(windowLocations, windowLocation => {
|
||||
const isTop = 'top' === windowLocation;
|
||||
const isApp = 'app' === windowLocation;
|
||||
const currentWindow = isApp ? window : top_window;
|
||||
const $currentWindow = currentWindow.jQuery(currentWindow);
|
||||
|
||||
// Scroll in Theme Builder & Layout Block Builder happens on element; adjustment needed
|
||||
// const scrollWindow = isTop && (isTB || isLBB) ? currentWindow.document.getElementById('et-fb-app') : currentWindow;
|
||||
const scrollWindow = () => {
|
||||
// Theme Builder & Layout Block Builder
|
||||
if (isTop && (isTB || isLBB)) {
|
||||
return currentWindow.document.getElementById('et-fb-app');
|
||||
}
|
||||
|
||||
// Layout Block Preview / Gutenberg
|
||||
if (isTop && isLBP) {
|
||||
return currentWindow.document.getElementsByClassName(getContentAreaSelector(currentWindow, false))[0];
|
||||
}
|
||||
|
||||
return currentWindow;
|
||||
};
|
||||
|
||||
// listen to current (app/top) window resize event
|
||||
currentWindow.addEventListener('resize', () => {
|
||||
const width = currentWindow.jQuery(currentWindow).innerWidth();
|
||||
const height = currentWindow.jQuery(currentWindow).innerHeight();
|
||||
|
||||
windowStoreInstance.setWidth(windowLocation, width).setHeight(windowLocation, height);
|
||||
windowStoreInstance.setVerticalScrollBarWidth(windowLocation, (currentWindow.outerWidth - width));
|
||||
|
||||
if ((windowStoreInstance.width > 782 && height <= 782) || (windowStoreInstance.width <= 782 && height > 782)) {
|
||||
// Wait until admin bar's viewport style kicks in
|
||||
setTimeout(() => {
|
||||
ETScriptStickyStore.setElementHeight('wpAdminBar');
|
||||
|
||||
windowStoreInstance.emit(SCROLL_TOP_CHANGE);
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
|
||||
// listen to current (app/top) window scroll event
|
||||
scrollWindow().addEventListener('scroll', () => {
|
||||
const scrollTop = isTop && (isTB || isLBB || isLBP) ? scrollWindow().scrollTop : scrollWindow().pageYOffset;
|
||||
|
||||
windowStoreInstance.setScrollTop(windowLocation, scrollTop);
|
||||
});
|
||||
|
||||
// Top window listener only
|
||||
if (isTop) {
|
||||
// Listen to builder's preview mode change that is passed via top window event
|
||||
$currentWindow.on('et_fb_preview_mode_changed', (event, screenMode, builderMode) => {
|
||||
const scrollLocation = windowStoreInstance.getBuilderScrollLocation(builderMode);
|
||||
|
||||
windowStoreInstance.setBuilderZoomedStatus(builderMode);
|
||||
windowStoreInstance.setScrollLocation(scrollLocation);
|
||||
});
|
||||
|
||||
// Update iframe offset if any metabox is moved
|
||||
if (isBFB) {
|
||||
currentWindow.addEventListener('ETBFBMetaboxSortStopped', () => {
|
||||
windowStoreInstance.setBfbIframeOffset();
|
||||
});
|
||||
}
|
||||
|
||||
// Gutenberg moves the scroll back to window if window's width is less than 600px
|
||||
if (isLBP) {
|
||||
currentWindow.addEventListener('scroll', () => {
|
||||
if (windowStoreInstance.width > 600) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollTop = currentWindow.pageYOffset;
|
||||
|
||||
windowStoreInstance.setScrollTop(windowLocation, scrollTop);
|
||||
});
|
||||
}
|
||||
|
||||
// When scroll is located on top window, there is a chance that the top window actually scrolls
|
||||
// before the builder is loaded which means initial scroll top value actually has changed
|
||||
// to avoid issue caused by it, when app window that carries this script is loaded, trigger
|
||||
// scroll event on the top window's scrolling element
|
||||
scrollWindow().dispatchEvent(new CustomEvent('scroll'));
|
||||
}
|
||||
|
||||
// App window listener only
|
||||
if (isApp) {
|
||||
// Update known element props when breakpoint changes. Breakpoint change is basically less
|
||||
// aggressive resize event, happened between known window's width
|
||||
if (isFE || isVB) {
|
||||
windowStoreInstance.addBreakpointChangeListener(() => {
|
||||
ETScriptStickyStore.setElementsProps();
|
||||
});
|
||||
}
|
||||
|
||||
// Update iframe offset if layout block is moved
|
||||
if (isLBP) {
|
||||
currentWindow.addEventListener('ETBlockGbBlockOrderChange', () => {
|
||||
// Need to wait at least 300ms until GB animation is done
|
||||
setTimeout(() => {
|
||||
windowStoreInstance.setLayoutBlockPreviewIframeOffset();
|
||||
|
||||
windowStoreInstance.emit(SCROLL_TOP_CHANGE);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Update iframe offset if notice size is changed
|
||||
currentWindow.addEventListener('ETGBNoticeSizeChange', () => {
|
||||
if (ETScriptStickyStore.getElementProp('gbComponentsNoticeList', 'exist', false)) {
|
||||
ETScriptStickyStore.setElementHeight('gbComponentsNoticeList');
|
||||
|
||||
windowStoreInstance.emit(SCROLL_TOP_CHANGE);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Register store instance as component to be exposed via global object
|
||||
registerFrontendComponent('stores', 'window', windowStoreInstance);
|
||||
|
||||
// Export store instance
|
||||
// IMPORTANT: For uniformity, import this as ETScriptWindowStore
|
||||
export default windowStoreInstance;
|
Reference in New Issue
Block a user