diff --git a/packages/child/check-blocking-css.js b/packages/child/check/blocking-css.js similarity index 96% rename from packages/child/check-blocking-css.js rename to packages/child/check/blocking-css.js index c1f521683..f09f759a6 100644 --- a/packages/child/check-blocking-css.js +++ b/packages/child/check/blocking-css.js @@ -1,5 +1,5 @@ -import { AUTO } from '../common/consts' -import { advise, log } from './console' +import { AUTO } from '../../common/consts' +import { advise, log } from '../console' const nodes = () => [document.documentElement, document.body] const properties = ['min-height', 'min-width', 'max-height', 'max-width'] diff --git a/packages/child/check/both.js b/packages/child/check/both.js new file mode 100644 index 000000000..7aba1e748 --- /dev/null +++ b/packages/child/check/both.js @@ -0,0 +1,3 @@ +// checkBoth +export default ({ calculateWidth, calculateHeight }) => + calculateWidth === calculateHeight diff --git a/packages/child/check/calculation-mode.js b/packages/child/check/calculation-mode.js new file mode 100644 index 000000000..8224b6d97 --- /dev/null +++ b/packages/child/check/calculation-mode.js @@ -0,0 +1,78 @@ +import { HIGHLIGHT } from 'auto-console-group' + +import { + HEIGHT_CALC_MODE_DEFAULT, + WIDTH_CALC_MODE_DEFAULT, +} from '../../common/consts' +import { advise, log, warn } from '../console' +import { getHeight, getWidth } from '../size' +import settings from '../values/settings' + +const DEPRECATED_RESIZE_METHODS = { + bodyOffset: 1, + bodyScroll: 1, + offset: 1, + documentElementOffset: 1, + documentElementScroll: 1, + boundingClientRect: 1, + max: 1, + min: 1, + grow: 1, + lowestElement: 1, +} + +const olderVersions = ( + label, +) => `set this option to 'auto' when using an older version of iframe-resizer on the parent page. This can be done on the child page by adding the following code: + +window.iframeResizer = { +license: 'xxxx', +${label}CalculationMethod: AUTO, +} +` + +function showDeprecationWarning(label, calcMode) { + const actionMsg = settings.version + ? 'remove this option.' + : olderVersions(label) + + advise( + `Deprecated ${label}CalculationMethod (${calcMode}) + +This version of iframe-resizer can auto detect the most suitable ${label} calculation method. It is recommended that you ${actionMsg} +`, + ) +} + +export function checkCalcMode(calcMode, calcModeDefault, modes) { + const { label } = modes + + if (calcModeDefault !== calcMode) { + if (!(calcMode in modes)) { + warn(`${calcMode} is not a valid option for ${label}CalculationMethod.`) + calcMode = calcModeDefault + } + + if (calcMode in DEPRECATED_RESIZE_METHODS) + showDeprecationWarning(label, calcMode) + } + + log(`Set ${label} calculation method: %c${calcMode}`, HIGHLIGHT) + return calcMode +} + +export function checkHeightMode() { + settings.heightCalcMode = checkCalcMode( + settings.heightCalcMode, + HEIGHT_CALC_MODE_DEFAULT, + getHeight, + ) +} + +export function checkWidthMode() { + settings.widthCalcMode = checkCalcMode( + settings.widthCalcMode, + WIDTH_CALC_MODE_DEFAULT, + getWidth, + ) +} diff --git a/packages/child/check/cross-domain.js b/packages/child/check/cross-domain.js new file mode 100644 index 000000000..48ba7c2e0 --- /dev/null +++ b/packages/child/check/cross-domain.js @@ -0,0 +1,12 @@ +import { log } from '../console' +import settings from '../values/settings' +import state from '../values/state' + +export default function checkCrossDomain() { + try { + state.sameOrigin = + settings.mode === 1 || 'iframeParentListener' in window.parent + } catch (error) { + log('Cross domain iframe detected') + } +} diff --git a/packages/child/check/deprecated-attributes.js b/packages/child/check/deprecated-attributes.js new file mode 100644 index 000000000..7748191d4 --- /dev/null +++ b/packages/child/check/deprecated-attributes.js @@ -0,0 +1,22 @@ +import { SIZE_ATTR } from '../../common/consts' +import { advise } from '../console' + +const DEPRECATED = `Deprecated Attributes + +The data-iframe-height and data-iframe-width attributes have been deprecated and replaced with the single data-iframe-size attribute. Use of the old attributes will be removed in a future version of iframe-resizer.` + +export default function checkDeprecatedAttrs() { + let found = false + + const checkAttrs = (attr) => + document.querySelectorAll(`[${attr}]`).forEach((el) => { + el.toggleAttribute(SIZE_ATTR, true) + el.removeAttribute(attr) + found = true + }) + + checkAttrs('data-iframe-height') + checkAttrs('data-iframe-width') + + if (found) advise(DEPRECATED) +} diff --git a/packages/child/check/ignored-elements.js b/packages/child/check/ignored-elements.js new file mode 100644 index 000000000..ea1b04ad8 --- /dev/null +++ b/packages/child/check/ignored-elements.js @@ -0,0 +1,30 @@ +import { BOLD, NORMAL } from 'auto-console-group' + +import { IGNORE_ATTR } from '../../common/consts' +import { warn } from '../console' + +let ignoredElementsCount = 0 + +function warnIgnored(ignoredElements) { + const s = ignoredElements.length === 1 ? '' : 's' + + warn( + `%c[${IGNORE_ATTR}]%c found on %c${ignoredElements.length}%c element${s}`, + BOLD, + NORMAL, + BOLD, + NORMAL, + ) +} + +export default function checkIgnoredElements() { + const ignoredElements = document.querySelectorAll(`*[${IGNORE_ATTR}]`) + const hasIgnored = ignoredElements.length > 0 + + if (hasIgnored && ignoredElements.length !== ignoredElementsCount) { + warnIgnored(ignoredElements) + ignoredElementsCount = ignoredElements.length + } + + return hasIgnored +} diff --git a/packages/child/check/mode.js b/packages/child/check/mode.js new file mode 100644 index 000000000..0448cf4ab --- /dev/null +++ b/packages/child/check/mode.js @@ -0,0 +1,28 @@ +import { VERSION } from '../../common/consts' +import setMode, { getModeData, getModeLabel } from '../../common/mode' +import { isDef } from '../../common/utils' +import { advise, purge, vInfo } from '../console' +import settings from '../values/settings' +import state from '../values/state' + +export default function ({ key, key2, mode, version }) { + const oMode = mode + const pMode = setMode({ key }) + const cMode = setMode({ key2 }) + // eslint-disable-next-line no-multi-assign + settings.mode = mode = Math.max(pMode, cMode) + if (mode < 0) { + mode = Math.min(pMode, cMode) + purge() + advise(`${getModeData(mode + 2)}${getModeData(2)}`) + if (isDef(version)) { + state.firstRun = false + throw getModeData(mode + 2).replace(/<\/?[a-z][^>]*>|<\/>/gi, '') + } + } else if (!isDef(version) || (oMode > -1 && mode > oMode)) { + if (sessionStorage.getItem('ifr') !== VERSION) + vInfo(`v${VERSION} (${getModeLabel(mode)})`, mode) + if (mode < 2) advise(getModeData(3)) + sessionStorage.setItem('ifr', VERSION) + } +} diff --git a/packages/child/check/overflow.js b/packages/child/check/overflow.js new file mode 100644 index 000000000..1db40a02e --- /dev/null +++ b/packages/child/check/overflow.js @@ -0,0 +1,46 @@ +import { FUNCTION, IGNORE_ATTR, OVERFLOW_ATTR } from '../../common/consts' +import { endAutoGroup, event as consoleEvent, info } from '../console' +import state from '../values/state' + +let prevOverflowedNodeSet = new Set() + +export function filterIgnoredElements(nodeList) { + const filteredNodeSet = new Set() + const ignoredNodeSet = new Set() + + for (const node of nodeList) { + if (node.closest(`[${IGNORE_ATTR}]`)) { + ignoredNodeSet.add(node) + } else { + filteredNodeSet.add(node) + } + } + + if (ignoredNodeSet.size > 0) { + queueMicrotask(() => { + consoleEvent('overflowIgnored') + info(`Ignoring elements with [data-iframe-ignore] > *:\n`, ignoredNodeSet) + endAutoGroup() + }) + } + + return filteredNodeSet +} + +export default function checkOverflow() { + const allOverflowedNodes = document.querySelectorAll(`[${OVERFLOW_ATTR}]`) + const overflowedNodeSet = filterIgnoredElements(allOverflowedNodes) + let hasOverflowUpdated = false + + // Not supported in Safari 16 (or esLint!!!) + // eslint-disable-next-line no-use-extend-native/no-use-extend-native + if (typeof Set.prototype.symmetricDifference === FUNCTION) + hasOverflowUpdated = + overflowedNodeSet.symmetricDifference(prevOverflowedNodeSet).size > 0 + + prevOverflowedNodeSet = overflowedNodeSet + state.overflowedNodeSet = overflowedNodeSet + state.hasOverflow = overflowedNodeSet.size > 0 + + return { hasOverflowUpdated, overflowedNodeSet } +} diff --git a/packages/child/check/quirks-mode.js b/packages/child/check/quirks-mode.js new file mode 100644 index 000000000..2038e8ecd --- /dev/null +++ b/packages/child/check/quirks-mode.js @@ -0,0 +1,15 @@ +import { advise } from '../console' + +const QUIRKS_MODE = 'BackCompat' + +const DEPRECATED = `Quirks Mode Detected + +This iframe is running in the browser's legacy Quirks Mode, this may cause issues with the correct operation of iframe-resizer. It is recommended that you switch to the modern Standards Mode. + +For more information see https://iframe-resizer.com/quirks-mode. +` + +export default function checkQuirksMode() { + if (document.compatMode !== QUIRKS_MODE) return + advise(DEPRECATED) +} diff --git a/packages/child/check/ready.js b/packages/child/check/ready.js new file mode 100644 index 000000000..8c6239a1c --- /dev/null +++ b/packages/child/check/ready.js @@ -0,0 +1,15 @@ +import { READY_STATE_CHANGE } from '../../common/consts' +import { isolateUserCode } from '../../common/utils' +import { addEventListener } from '../events/listeners' + +const COMPLETE = 'complete' +let readyChecked = false + +export default function checkReadyYet(readyCallback) { + if (document.readyState === COMPLETE) isolateUserCode(readyCallback) + else if (!readyChecked) + addEventListener(document, READY_STATE_CHANGE, () => + checkReadyYet(readyCallback), + ) + readyChecked = true +} diff --git a/packages/child/check/settings.js b/packages/child/check/settings.js new file mode 100644 index 000000000..b2e89fb00 --- /dev/null +++ b/packages/child/check/settings.js @@ -0,0 +1,12 @@ +import { HIGHLIGHT } from 'auto-console-group' + +import { info, log } from '../console' +import settings from '../values/settings' + +export default () => { + info(`Set targetOrigin for parent: %c${settings.targetOrigin}`, HIGHLIGHT) + + if (settings.autoResize !== true) { + log('Auto Resize disabled') + } +} diff --git a/packages/child/check/tags.js b/packages/child/check/tags.js new file mode 100644 index 000000000..7b852582a --- /dev/null +++ b/packages/child/check/tags.js @@ -0,0 +1,11 @@ +import { HIGHLIGHT } from 'auto-console-group' + +import { SIZE_ATTR } from '../../common/consts' +import { log } from '../console' +import state from '../values/state' + +export default function checkAndSetupTags() { + state.taggedElements = document.querySelectorAll(`[${SIZE_ATTR}]`) + state.hasTags = state.taggedElements.length > 0 + log(`Tagged elements found: %c${state.hasTags}`, HIGHLIGHT) +} diff --git a/packages/child/check/tolerance.js b/packages/child/check/tolerance.js new file mode 100644 index 000000000..9ec8c4c9b --- /dev/null +++ b/packages/child/check/tolerance.js @@ -0,0 +1,3 @@ +import settings from '../values/settings' + +export default (a, b) => !(Math.abs(a - b) <= settings.tolerance) diff --git a/packages/child/check/version.js b/packages/child/check/version.js new file mode 100644 index 000000000..14f5f1d40 --- /dev/null +++ b/packages/child/check/version.js @@ -0,0 +1,24 @@ +import { FALSE, VERSION } from '../../common/consts' +import { advise } from '../console' + +const LEGACY = `Legacy version detected on parent page + +Detected legacy version of parent page script. It is recommended to update the parent page to use @iframe-resizer/parent. + +See https://iframe-resizer.com/setup/ for more details. +` + +const mismatch = (version) => `Version mismatch + +The parent and child pages are running different versions of iframe resizer. + +Parent page: ${version} - Child page: ${VERSION}. +` + +export default function checkVersion({ version }) { + if (!version || version === '' || version === FALSE) { + advise(LEGACY) + } else if (version !== VERSION) { + advise(mismatch(version)) + } +} diff --git a/packages/child/listeners.js b/packages/child/events/listeners.js similarity index 94% rename from packages/child/listeners.js rename to packages/child/events/listeners.js index 949c45759..2b5250ab6 100644 --- a/packages/child/listeners.js +++ b/packages/child/events/listeners.js @@ -1,6 +1,6 @@ import { HIGHLIGHT } from 'auto-console-group' -import { log } from './console' +import { log } from '../console' export const tearDownList = [] diff --git a/packages/child/events/mouse.js b/packages/child/events/mouse.js new file mode 100644 index 000000000..459496f30 --- /dev/null +++ b/packages/child/events/mouse.js @@ -0,0 +1,21 @@ +import { HIGHLIGHT } from 'auto-console-group' + +import { MOUSE_ENTER, MOUSE_LEAVE } from '../../common/consts' +import { log } from '../console' +import sendMessage from '../send/message' +import { addEventListener } from './listeners' + +const sendMouse = (evt) => + sendMessage(0, 0, evt.type, `${evt.screenY}:${evt.screenX}`) + +function addMouseListener(evt, name) { + log(`Add event listener: %c${name}`, HIGHLIGHT) + addEventListener(window.document, evt, sendMouse) +} + +export default function setupMouseEvents({ mouseEvents }) { + if (mouseEvents !== true) return + + addMouseListener(MOUSE_ENTER, 'Mouse Enter') + addMouseListener(MOUSE_LEAVE, 'Mouse Leave') +} diff --git a/packages/child/events/page-hide.js b/packages/child/events/page-hide.js new file mode 100644 index 000000000..13b1c4195 --- /dev/null +++ b/packages/child/events/page-hide.js @@ -0,0 +1,18 @@ +import { BEFORE_UNLOAD, PAGE_HIDE } from '../../common/consts' +import { invoke, lower } from '../../common/utils' +import { event as consoleEvent, info } from '../console' +import sendMessage from '../send/message' +import { addEventListener, tearDownList } from './listeners' + +const resetNoResponseTimer = () => sendMessage(0, 0, BEFORE_UNLOAD) + +function onPageHide({ persisted }) { + if (!persisted) resetNoResponseTimer() + consoleEvent(PAGE_HIDE) + info('Page persisted:', persisted) + if (persisted) return + tearDownList.forEach(invoke) +} + +// setupOnPageHide +export default () => addEventListener(window, lower(PAGE_HIDE), onPageHide) diff --git a/packages/child/events/print.js b/packages/child/events/print.js new file mode 100644 index 000000000..f4f6a68e0 --- /dev/null +++ b/packages/child/events/print.js @@ -0,0 +1,19 @@ +import sendSize from '../send/size' +import { addEventListener } from './listeners' + +function add({ eventType, eventName }) { + const handleEvent = () => sendSize(eventName, eventType) + addEventListener(window, eventName, handleEvent, { passive: true }) +} + +export default function setupPrintListeners() { + add({ + eventType: 'After Print', + eventName: 'afterprint', + }) + + add({ + eventType: 'Before Print', + eventName: 'beforeprint', + }) +} diff --git a/packages/child/events/ready.js b/packages/child/events/ready.js new file mode 100644 index 000000000..222330762 --- /dev/null +++ b/packages/child/events/ready.js @@ -0,0 +1,33 @@ +import { FOREGROUND, HIGHLIGHT } from 'auto-console-group' + +import { CHILD_READY_MESSAGE } from '../../common/consts' +import { event as consoleEvent, log } from '../console' +import state from '../values/state' + +// Normally the parent kicks things off when it detects the iframe has loaded. +// If this script is async-loaded, then tell parent page to retry init. +let sent = false +const sendReady = (target) => + target.postMessage( + CHILD_READY_MESSAGE, + window?.iframeResizer?.targetOrigin || '*', + ) + +export default function ready() { + if (document.readyState === 'loading' || !state.firstRun || sent) return + + const { parent, top } = window + + consoleEvent('ready') + log( + 'Sending%c ready%c to parent from', + HIGHLIGHT, + FOREGROUND, + window.location.href, + ) + + sendReady(parent) + if (parent !== top) sendReady(top) + + sent = true +} diff --git a/packages/child/from-string.js b/packages/child/from-string.js deleted file mode 100644 index d910e554b..000000000 --- a/packages/child/from-string.js +++ /dev/null @@ -1,7 +0,0 @@ -const strBool = (str) => str === 'true' - -const castDefined = (cast) => (data) => - undefined === data ? undefined : cast(data) - -export const getBoolean = castDefined(strBool) -export const getNumber = castDefined(Number) diff --git a/packages/child/from-string.test.js b/packages/child/from-string.test.js deleted file mode 100644 index 553f98cd3..000000000 --- a/packages/child/from-string.test.js +++ /dev/null @@ -1,37 +0,0 @@ -import { FALSE } from '../common/consts' -import { getBoolean, getNumber } from './from-string' - -describe('from-string utility functions', () => { - describe('getBoolean', () => { - test('should return true for the string "true"', () => { - expect(getBoolean('true')).toBe(true) - }) - - test('should return false for the string "false"', () => { - expect(getBoolean(FALSE)).toBe(false) - }) - - test('should return undefined for undefined input', () => { - expect(getBoolean()).toBeUndefined() - }) - - test('should return false for non-boolean strings', () => { - expect(getBoolean('random')).toBe(false) - }) - }) - - describe('getNumber', () => { - test('should return a number for valid numeric strings', () => { - expect(getNumber('42')).toBe(42) - expect(getNumber('3.14')).toBe(3.14) - }) - - test('should return NaN for non-numeric strings', () => { - expect(getNumber('random')).toBeNaN() - }) - - test('should return undefined for undefined input', () => { - expect(getNumber()).toBeUndefined() - }) - }) -}) diff --git a/packages/child/index.js b/packages/child/index.js index 7c398c3f4..c94069483 100644 --- a/packages/child/index.js +++ b/packages/child/index.js @@ -1,1783 +1,35 @@ -import { BOLD, FOREGROUND, HIGHLIGHT, ITALIC, NORMAL } from 'auto-console-group' - -import { - AUTO, - AUTO_RESIZE, - BASE, - BEFORE_UNLOAD, - BOOLEAN, - CHILD, - CHILD_READY_MESSAGE, - CLOSE, - ENABLE, - FALSE, - FUNCTION, - HEIGHT, - HEIGHT_EDGE, - IGNORE_ATTR, - IGNORE_DISABLE_RESIZE, - IGNORE_TAGS, - IN_PAGE_LINK, - INIT, - MANUAL_RESIZE_REQUEST, - MESSAGE, - MESSAGE_ID, - MESSAGE_ID_LENGTH, - MIN_SIZE, - MOUSE_ENTER, - MOUSE_LEAVE, - MUTATION_OBSERVER, - NO_CHANGE, - NONE, - NULL, - NUMBER, - OBJECT, - OFFSET, - OFFSET_SIZE, - OVERFLOW_ATTR, - OVERFLOW_OBSERVER, - PAGE_HIDE, - PAGE_INFO, - PAGE_INFO_STOP, - PARENT_INFO, - PARENT_INFO_STOP, - PARENT_RESIZE_REQUEST, - READY_STATE_CHANGE, - RESIZE_OBSERVER, - SCROLL, - SCROLL_BY, - SCROLL_TO, - SCROLL_TO_OFFSET, - SEPARATOR, - SET_OFFSET_SIZE, - SIZE_ATTR, - SIZE_CHANGE_DETECTED, - STRING, - TITLE, - UNDEFINED, - VERSION, - VISIBILITY_OBSERVER, - WIDTH, - WIDTH_EDGE, -} from '../common/consts' -import setMode, { getKey, getModeData, getModeLabel } from '../common/mode' -import { - capitalizeFirstLetter, - getElementName, - id, - invoke, - isDef, - // isElement, - isolateUserCode, - lower, - once, - round, - typeAssert, -} from '../common/utils' -import checkBlockingCSS from './check-blocking-css' -import { - advise, - assert, - debug, - deprecateMethod, - deprecateMethodReplace, - deprecateOption, - endAutoGroup, - error, - errorBoundary, - event as consoleEvent, - info, - label, - log, - purge, - setConsoleOptions, - vInfo, - warn, -} from './console' -import { getBoolean, getNumber } from './from-string' -import { - addEventListener, - removeEventListener, - tearDownList, -} from './listeners' -import createMutationObserver from './observers/mutation' -import createOverflowObserver from './observers/overflow' -import createPerformanceObserver, { - PREF_END, - PREF_START, -} from './observers/perf' -import createResizeObserver from './observers/resize' -import createVisibilityObserver from './observers/visibility' -import { readFunction, readNumber, readString } from './read' +import { MESSAGE, READY_STATE_CHANGE, UNDEFINED } from '../common/consts' +import { event as consoleEvent, warn } from './console' +import { addEventListener, removeEventListener } from './events/listeners' +import ready from './events/ready' +import received from './received' +import state from './values/state' function iframeResizerChild() { - const customCalcMethods = { - height: () => { - warn('Custom height calculation function not defined') - return getHeight.auto() - }, - width: () => { - warn('Custom width calculation function not defined') - return getWidth.auto() - }, - } - const deprecatedResizeMethods = { - bodyOffset: 1, - bodyScroll: 1, - offset: 1, - documentElementOffset: 1, - documentElementScroll: 1, - boundingClientRect: 1, - max: 1, - min: 1, - grow: 1, - lowestElement: 1, - } - const eventCancelTimer = 128 - const eventHandlersByName = {} - const heightCalcModeDefault = AUTO - const widthCalcModeDefault = SCROLL - - let autoResize = true - let bodyBackground = '' - let bodyMargin = 0 - let bodyMarginStr = '' - let bodyPadding = '' - let bothDirections = false - let calculateHeight = true - let calculateWidth = false - let firstRun = true - let hasIgnored = false - let hasOverflow = false - let hasOverflowUpdated = true - let hasTags = false - let height = 1 - let heightCalcMode = heightCalcModeDefault // only applies if not provided by host page (V1 compatibility) - let ignoreSelector = '' - let initLock = true - let inPageLinks = {} - let isHidden = false - let logExpand = false - let logging = false - let key - let key2 - let mode = 0 - let mouseEvents = false - let offsetHeight = 0 - let offsetWidth = 0 - let origin - let overflowedNodeSet = new Set() - let overflowObserver - let parentId = '' - let resizeFrom = CHILD - let resizeObserver - let sameOrigin = false - let sizeSelector = '' - let taggedElements = [] - let target = window.parent - let targetOriginDefault = '*' - let timerActive - let totalTime - let tolerance = 0 - let triggerLocked = false - let version - let width = 1 - let widthCalcMode = widthCalcModeDefault - let win = window - - let onBeforeResize - let onMessage = () => { - warn('onMessage function not defined') - } - let onReady = () => {} - let onPageInfo = null - let onParentInfo = null - - function isolate(funcs) { - funcs.forEach((func) => { - try { - func() - } catch (error_) { - if (mode < 0) throw error_ - advise( - `Error in setup function\niframe-resizer detected an error during setup.\n\nPlease report the following error message at https://github.com/davidjbradshaw/iframe-resizer/issues`, - ) - error(error_) - } - }) - } - - function init(data) { - readDataFromParent(data) - - setConsoleOptions({ id: parentId, enabled: logging, expand: logExpand }) - consoleEvent('initReceived') - log(`Initialising iframe v${VERSION} ${window.location.href}`) - - readDataFromPage() - - const setup = [ - checkVersion, - checkBoth, - checkMode, - checkIgnoredElements, - checkCrossDomain, - checkHeightMode, - checkWidthMode, - checkDeprecatedAttrs, - checkQuirksMode, - checkAndSetupTags, - bothDirections ? id : checkBlockingCSS, - - setMargin, - () => setBodyStyle('background', bodyBackground), - () => setBodyStyle('padding', bodyPadding), - - bothDirections ? id : stopInfiniteResizingOfIframe, - injectClearFixIntoBodyElement, - - applySelectors, - attachObservers, - - setupInPageLinks, - setupEventListeners, - setupMouseEvents, - setupOnPageHide, - setupPublicMethods, - ] - - isolate(setup) - - checkReadyYet(once(onReady)) - log('Initialization complete') - endAutoGroup() - - sendSize( - INIT, - 'Init message from host page', - undefined, - undefined, - `${VERSION}:${mode}`, - ) - - sendTitle() - } - - const resetNoResponseTimer = () => sendMessage(0, 0, BEFORE_UNLOAD) - - function onPageHide({ persisted }) { - if (!persisted) resetNoResponseTimer() - consoleEvent(PAGE_HIDE) - info('Page persisted:', persisted) - if (persisted) return - tearDownList.forEach(invoke) - } - - const setupOnPageHide = () => - addEventListener(window, lower(PAGE_HIDE), onPageHide) - - let readyChecked = false - function checkReadyYet(readyCallback) { - if (document.readyState === 'complete') isolateUserCode(readyCallback) - else if (!readyChecked) - addEventListener(document, READY_STATE_CHANGE, () => - checkReadyYet(readyCallback), - ) - readyChecked = true - } - - function checkAndSetupTags() { - taggedElements = document.querySelectorAll(`[${SIZE_ATTR}]`) - hasTags = taggedElements.length > 0 - log(`Tagged elements found: %c${hasTags}`, HIGHLIGHT) - } - - function sendTitle() { - if (document.title && document.title !== '') { - sendMessage(0, 0, TITLE, document.title) - } - } - - function warnIgnored(ignoredElements) { - const s = ignoredElements.length === 1 ? '' : 's' - - warn( - `%c[${IGNORE_ATTR}]%c found on %c${ignoredElements.length}%c element${s}`, - BOLD, - NORMAL, - BOLD, - NORMAL, - ) - } - - let ignoredElementsCount = 0 - function checkIgnoredElements() { - const ignoredElements = document.querySelectorAll(`*[${IGNORE_ATTR}]`) - hasIgnored = ignoredElements.length > 0 - if (hasIgnored && ignoredElements.length !== ignoredElementsCount) { - warnIgnored(ignoredElements) - ignoredElementsCount = ignoredElements.length - } - return hasIgnored - } - - function checkQuirksMode() { - if (document.compatMode !== 'BackCompat') return - - advise( - `Quirks Mode Detected - -This iframe is running in the browser's legacy Quirks Mode, this may cause issues with the correct operation of iframe-resizer. It is recommended that you switch to the modern Standards Mode. - -For more information see https://iframe-resizer.com/quirks-mode. -`, - ) - } - - function checkVersion() { - if (!version || version === '' || version === FALSE) { - advise( - `Legacy version detected on parent page - -Detected legacy version of parent page script. It is recommended to update the parent page to use @iframe-resizer/parent. - -See https://iframe-resizer.com/setup/ for more details. -`, - ) - return - } - - if (version !== VERSION) { - advise( - `Version mismatch - -The parent and child pages are running different versions of iframe resizer. - -Parent page: ${version} - Child page: ${VERSION}. -`, - ) - } - } - - function checkCrossDomain() { - try { - sameOrigin = mode === 1 || 'iframeParentListener' in window.parent - } catch (error) { - log('Cross domain iframe detected') - } - } - - // eslint-disable-next-line sonarjs/cognitive-complexity - function readDataFromParent(data) { - parentId = data[0] ?? parentId - bodyMargin = getNumber(data[1]) ?? bodyMargin // For V1 compatibility - calculateWidth = getBoolean(data[2]) ?? calculateWidth - logging = getBoolean(data[3]) ?? logging - // data[4] no longer used (was intervalTimer) - autoResize = getBoolean(data[6]) ?? autoResize - bodyMarginStr = data[7] ?? bodyMarginStr - heightCalcMode = data[8] ?? heightCalcMode - bodyBackground = data[9] ?? bodyBackground - bodyPadding = data[10] ?? bodyPadding - tolerance = getNumber(data[11]) ?? tolerance - inPageLinks.enable = getBoolean(data[12]) ?? false - resizeFrom = data[13] ?? resizeFrom - widthCalcMode = data[14] ?? widthCalcMode - mouseEvents = getBoolean(data[15]) ?? mouseEvents - offsetHeight = getNumber(data[16]) ?? offsetHeight - offsetWidth = getNumber(data[17]) ?? offsetWidth - calculateHeight = getBoolean(data[18]) ?? calculateHeight - key = data[19] ?? key - version = data[20] ?? version - mode = getNumber(data[21]) ?? mode - // sizeSelector = data[22] || sizeSelector - logExpand = getBoolean(data[23]) ?? logExpand - } - - function readDataFromPage() { - // eslint-disable-next-line sonarjs/cognitive-complexity - function readData(data) { - log(`Reading data from page:`, Object.keys(data)) - - onBeforeResize = readFunction(data, 'onBeforeResize') ?? onBeforeResize - onMessage = readFunction(data, 'onMessage') ?? onMessage - onReady = readFunction(data, 'onReady') ?? onReady - - if (typeof data?.offset === NUMBER) { - deprecateOption(OFFSET, OFFSET_SIZE) - if (calculateHeight) - offsetHeight = readNumber(data, OFFSET) ?? offsetHeight - if (calculateWidth) - offsetWidth = readNumber(data, OFFSET) ?? offsetWidth - } - - if (typeof data?.offsetSize === NUMBER) { - if (calculateHeight) - offsetHeight = readNumber(data, OFFSET_SIZE) ?? offsetHeight - if (calculateWidth) - offsetWidth = readNumber(data, OFFSET_SIZE) ?? offsetWidth - } - - key2 = readString(data, getKey(0)) ?? key2 - ignoreSelector = readString(data, 'ignoreSelector') ?? ignoreSelector - sizeSelector = readString(data, 'sizeSelector') ?? sizeSelector - targetOriginDefault = - readString(data, 'targetOrigin') ?? targetOriginDefault - - // String or Function - heightCalcMode = data?.heightCalculationMethod || heightCalcMode - widthCalcMode = data?.widthCalculationMethod || widthCalcMode - } - - function setupCustomCalcMethods(calcMode, calcFunc) { - if (typeof calcMode === FUNCTION) { - advise( - `Deprecated Option(${calcFunc}CalculationMethod) - -The use of ${calcFunc}CalculationMethod as a function is deprecated and will be removed in a future version of iframe-resizer. Please use the new onBeforeResize event handler instead. - -See https://iframe-resizer.com/api/child for more details.`, - ) - customCalcMethods[calcFunc] = calcMode - calcMode = 'custom' - } - - return calcMode - } - - if (mode === 1) return - - const data = window.iframeResizer || window.iFrameResizer - - if (typeof data !== OBJECT) return - - readData(data) - heightCalcMode = setupCustomCalcMethods(heightCalcMode, HEIGHT) - widthCalcMode = setupCustomCalcMethods(widthCalcMode, WIDTH) - - info(`Set targetOrigin for parent: %c${targetOriginDefault}`, HIGHLIGHT) - } - - function checkBoth() { - if (calculateWidth === calculateHeight) { - bothDirections = true - } - } - - function chkCSS(attr, value) { - if (value.includes('-')) { - warn(`Negative CSS value ignored for ${attr}`) - value = '' - } - - return value - } - - function setBodyStyle(attr, value) { - if (undefined === value || value === '' || value === NULL) return - - document.body.style.setProperty(attr, value) - info(`Set body ${attr}: %c${value}`, HIGHLIGHT) - } - - function applySelector(name, attribute, selector) { - if (selector === '') return - - log(`${name}: %c${selector}`, HIGHLIGHT) - - for (const el of document.querySelectorAll(selector)) { - log(`Applying ${attribute} to:`, el) - el.toggleAttribute(attribute, true) - } - } - - function applySelectors() { - applySelector('sizeSelector', SIZE_ATTR, sizeSelector) - applySelector('ignoreSelector', IGNORE_ATTR, ignoreSelector) - } - - function setMargin() { - // If called via V1 script, convert bodyMargin from int to str - if (undefined === bodyMarginStr) { - bodyMarginStr = `${bodyMargin}px` - } - - setBodyStyle('margin', chkCSS('margin', bodyMarginStr)) - } - - function stopInfiniteResizingOfIframe() { - const setAutoHeight = (el) => - el.style.setProperty(HEIGHT, AUTO, 'important') - - setAutoHeight(document.documentElement) - setAutoHeight(document.body) - - log('Set HTML & body height: %cauto !important', HIGHLIGHT) - } - - function manageTriggerEvent(options) { - const listener = { - add(eventName) { - function handleEvent() { - sendSize(options.eventName, options.eventType) - } - - eventHandlersByName[eventName] = handleEvent - - addEventListener(window, eventName, handleEvent, { passive: true }) - }, - remove(eventName) { - const handleEvent = eventHandlersByName[eventName] - delete eventHandlersByName[eventName] - - removeEventListener(window, eventName, handleEvent) - }, - } - - listener[options.method](options.eventName) - } - - function manageEventListeners(method) { - manageTriggerEvent({ - method, - eventType: 'After Print', - eventName: 'afterprint', - }) - - manageTriggerEvent({ - method, - eventType: 'Before Print', - eventName: 'beforeprint', - }) - } - - function checkDeprecatedAttrs() { - let found = false - - const checkAttrs = (attr) => - document.querySelectorAll(`[${attr}]`).forEach((el) => { - found = true - el.removeAttribute(attr) - el.toggleAttribute(SIZE_ATTR, true) - }) - - checkAttrs('data-iframe-height') - checkAttrs('data-iframe-width') - - if (found) { - advise( - `Deprecated Attributes - -The data-iframe-height and data-iframe-width attributes have been deprecated and replaced with the single data-iframe-size attribute. Use of the old attributes will be removed in a future version of iframe-resizer.`, - ) - } - } - - function checkCalcMode(calcMode, calcModeDefault, modes) { - const { label } = modes - - if (calcModeDefault !== calcMode) { - if (!(calcMode in modes)) { - warn(`${calcMode} is not a valid option for ${label}CalculationMethod.`) - calcMode = calcModeDefault - } - - if (calcMode in deprecatedResizeMethods) { - const actionMsg = version - ? 'remove this option.' - : `set this option to 'auto' when using an older version of iframe-resizer on the parent page. This can be done on the child page by adding the following code: - -window.iframeResizer = { - license: 'xxxx', - ${label}CalculationMethod: AUTO, -} -` - - advise( - `Deprecated ${label}CalculationMethod (${calcMode}) - -This version of iframe-resizer can auto detect the most suitable ${label} calculation method. It is recommended that you ${actionMsg} -`, - ) - } - } - - log(`Set ${label} calculation method: %c${calcMode}`, HIGHLIGHT) - return calcMode - } - - function checkHeightMode() { - heightCalcMode = checkCalcMode( - heightCalcMode, - heightCalcModeDefault, - getHeight, - ) - } - - function checkWidthMode() { - widthCalcMode = checkCalcMode(widthCalcMode, widthCalcModeDefault, getWidth) - } - - function checkMode() { - const oMode = mode - const pMode = setMode({ key }) - const cMode = setMode({ key: key2 }) - mode = Math.max(pMode, cMode) - if (mode < 0) { - mode = Math.min(pMode, cMode) - purge() - advise(`${getModeData(mode + 2)}${getModeData(2)}`) - if (isDef(version)) - throw getModeData(mode + 2).replace(/<\/?[a-z][^>]*>|<\/>/gi, '') - } else if (!isDef(version) || (oMode > -1 && mode > oMode)) { - if (sessionStorage.getItem('ifr') !== VERSION) - vInfo(`v${VERSION} (${getModeLabel(mode)})`, mode) - if (mode < 2) advise(getModeData(3)) - sessionStorage.setItem('ifr', VERSION) - } - } - - function setupEventListeners() { - if (autoResize !== true) { - log('Auto Resize disabled') - } - - manageEventListeners('add') - } - - function injectClearFixIntoBodyElement() { - const clearFix = document.createElement('div') - - clearFix.style.clear = 'both' - // Guard against the following having been globally redefined in CSS. - clearFix.style.display = 'block' - clearFix.style.height = '0' - - document.body.append(clearFix) - } - - function setupInPageLinks() { - const getPagePosition = () => ({ - x: document.documentElement.scrollLeft, - y: document.documentElement.scrollTop, - }) - - function getElementPosition(el) { - const elPosition = el.getBoundingClientRect() - const pagePosition = getPagePosition() - - return { - x: parseInt(elPosition.left, BASE) + parseInt(pagePosition.x, BASE), - y: parseInt(elPosition.top, BASE) + parseInt(pagePosition.y, BASE), - } - } - - function findTarget(location) { - function jumpToTarget(target) { - const jumpPosition = getElementPosition(target) - - log( - `Moving to in page link (%c#${hash}%c) at x: %c${jumpPosition.x}%c y: %c${jumpPosition.y}`, - HIGHLIGHT, - FOREGROUND, - HIGHLIGHT, - FOREGROUND, - HIGHLIGHT, - ) - - sendMessage(jumpPosition.y, jumpPosition.x, SCROLL_TO_OFFSET) // X&Y reversed at sendMessage uses height/width - } - - const hash = location.split('#')[1] || location // Remove # if present - const hashData = decodeURIComponent(hash) - const target = - document.getElementById(hashData) || - document.getElementsByName(hashData)[0] - - if (target !== undefined) { - jumpToTarget(target) - return - } - - log(`In page link (#${hash}) not found in iframe, so sending to parent`) - sendMessage(0, 0, IN_PAGE_LINK, `#${hash}`) - } - - function checkLocationHash() { - const { hash, href } = window.location - - if (hash !== '' && hash !== '#') { - findTarget(href) - } - } - - function bindAnchors() { - for (const link of document.querySelectorAll('a[href^="#"]')) { - if (link.getAttribute('href') !== '#') { - addEventListener(link, 'click', (e) => { - e.preventDefault() - findTarget(link.getAttribute('href')) - }) - } - } - } - - function bindLocationHash() { - addEventListener(window, 'hashchange', checkLocationHash) - } - - function initCheck() { - // Check if page loaded with location hash after init resize - setTimeout(checkLocationHash, eventCancelTimer) - } - - function enableInPageLinks() { - log('Setting up location.hash handlers') - bindAnchors() - bindLocationHash() - initCheck() - } - - const { enable } = inPageLinks - - if (enable) { - if (mode === 1) { - advise(getModeData(5)) - } else { - enableInPageLinks() - } - } else { - log('In page linking not enabled') - } - - inPageLinks = { - ...inPageLinks, - findTarget, - } - } - - function setupMouseEvents() { - if (mouseEvents !== true) return - - function sendMouse(e) { - sendMessage(0, 0, e.type, `${e.screenY}:${e.screenX}`) - } - - function addMouseListener(evt, name) { - log(`Add event listener: %c${name}`, HIGHLIGHT) - addEventListener(window.document, evt, sendMouse) - } - - addMouseListener(MOUSE_ENTER, 'Mouse Enter') - addMouseListener(MOUSE_LEAVE, 'Mouse Leave') - } - - function setupPublicMethods() { - if (mode === 1) return - - win.parentIframe = Object.freeze({ - autoResize: (enable) => { - typeAssert(enable, BOOLEAN, 'parentIframe.autoResize(enable) enable') - - // if (calculateWidth === calculateHeight) { - if (calculateWidth === false && calculateHeight === false) { - consoleEvent(ENABLE) - advise( - `Auto Resize can not be changed when direction is set to '${NONE}'.`, // or '${BOTH}' - ) - return false - } - - if (enable === true && autoResize === false) { - autoResize = true - queueMicrotask(() => sendSize(ENABLE, 'Auto Resize enabled')) - } else if (enable === false && autoResize === true) { - autoResize = false - } - - sendMessage(0, 0, AUTO_RESIZE, JSON.stringify(autoResize)) - - return autoResize - }, - - close() { - sendMessage(0, 0, CLOSE) - }, - - getId: () => parentId, - - getOrigin: () => { - consoleEvent('getOrigin') - deprecateMethod('getOrigin()', 'getParentOrigin()') - return origin - }, - - getParentOrigin: () => origin, - - getPageInfo(callback) { - if (typeof callback === FUNCTION) { - onPageInfo = callback - sendMessage(0, 0, PAGE_INFO) - deprecateMethodReplace( - 'getPageInfo()', - 'getParentProps()', - 'See https://iframe-resizer.com/upgrade for details. ', - ) - return - } - - onPageInfo = null - sendMessage(0, 0, PAGE_INFO_STOP) - }, - - getParentProps(callback) { - typeAssert( - callback, - FUNCTION, - 'parentIframe.getParentProps(callback) callback', - ) - - onParentInfo = callback - sendMessage(0, 0, PARENT_INFO) - - return () => { - onParentInfo = null - sendMessage(0, 0, PARENT_INFO_STOP) - } - }, - - getParentProperties(callback) { - deprecateMethod('getParentProperties()', 'getParentProps()') - this.getParentProps(callback) - }, - - moveToAnchor(anchor) { - typeAssert(anchor, STRING, 'parentIframe.moveToAnchor(anchor) anchor') - inPageLinks.findTarget(anchor) - }, - - reset() { - resetIframe('parentIframe.reset') - }, - - setOffsetSize(newOffset) { - typeAssert( - newOffset, - NUMBER, - 'parentIframe.setOffsetSize(offset) offset', - ) - offsetHeight = newOffset - offsetWidth = newOffset - sendSize(SET_OFFSET_SIZE, `parentIframe.setOffsetSize(${newOffset})`) - }, - - scrollBy(x, y) { - typeAssert(x, NUMBER, 'parentIframe.scrollBy(x, y) x') - typeAssert(y, NUMBER, 'parentIframe.scrollBy(x, y) y') - sendMessage(y, x, SCROLL_BY) // X&Y reversed at sendMessage uses height/width - }, - - scrollTo(x, y) { - typeAssert(x, NUMBER, 'parentIframe.scrollTo(x, y) x') - typeAssert(y, NUMBER, 'parentIframe.scrollTo(x, y) y') - sendMessage(y, x, SCROLL_TO) // X&Y reversed at sendMessage uses height/width - }, - - scrollToOffset(x, y) { - typeAssert(x, NUMBER, 'parentIframe.scrollToOffset(x, y) x') - typeAssert(y, NUMBER, 'parentIframe.scrollToOffset(x, y) y') - sendMessage(y, x, SCROLL_TO_OFFSET) // X&Y reversed at sendMessage uses height/width - }, - - sendMessage(msg, targetOrigin) { - if (targetOrigin) - typeAssert( - targetOrigin, - STRING, - 'parentIframe.sendMessage(msg, targetOrigin) targetOrigin', - ) - sendMessage(0, 0, MESSAGE, JSON.stringify(msg), targetOrigin) - }, - - setHeightCalculationMethod(heightCalculationMethod) { - heightCalcMode = heightCalculationMethod - checkHeightMode() - }, - - setWidthCalculationMethod(widthCalculationMethod) { - widthCalcMode = widthCalculationMethod - checkWidthMode() - }, - - setTargetOrigin(targetOrigin) { - typeAssert( - targetOrigin, - STRING, - 'parentIframe.setTargetOrigin(targetOrigin) targetOrigin', - ) - - log(`Set targetOrigin: %c${targetOrigin}`, HIGHLIGHT) - targetOriginDefault = targetOrigin - }, - - resize(customHeight, customWidth) { - if (customHeight !== undefined) - typeAssert( - customHeight, - NUMBER, - 'parentIframe.resize(customHeight, customWidth) customHeight', - ) - - if (customWidth !== undefined) - typeAssert( - customWidth, - NUMBER, - 'parentIframe.resize(customHeight, customWidth) customWidth', - ) - - const valString = `${customHeight || ''}${customWidth ? `,${customWidth}` : ''}` - - sendSize( - MANUAL_RESIZE_REQUEST, - `parentIframe.resize(${valString})`, - customHeight, - customWidth, - ) - }, - - size(customHeight, customWidth) { - deprecateMethod('size()', 'resize()') - this.resize(customHeight, customWidth) - }, - }) - - win.parentIFrame = win.parentIframe - } - - function filterIgnoredElements(nodeList) { - const filteredNodeSet = new Set() - const ignoredNodeSet = new Set() - - for (const node of nodeList) { - if (node.closest(`[${IGNORE_ATTR}]`)) { - ignoredNodeSet.add(node) - } else { - filteredNodeSet.add(node) - } - } - - if (ignoredNodeSet.size > 0) { - queueMicrotask(() => { - consoleEvent('overflowIgnored') - info( - `Ignoring elements with [data-iframe-ignore] > *:\n`, - ignoredNodeSet, - ) - endAutoGroup() - }) - } - - return filteredNodeSet - } - - let prevOverflowedNodeSet = new Set() - function checkOverflow() { - const allOverflowedNodes = document.querySelectorAll(`[${OVERFLOW_ATTR}]`) - - overflowedNodeSet = filterIgnoredElements(allOverflowedNodes) - - hasOverflow = overflowedNodeSet.size > 0 - - // Not supported in Safari 16 (or esLint!!!) - // eslint-disable-next-line no-use-extend-native/no-use-extend-native - if (typeof Set.prototype.symmetricDifference === FUNCTION) - hasOverflowUpdated = - overflowedNodeSet.symmetricDifference(prevOverflowedNodeSet).size > 0 - - prevOverflowedNodeSet = overflowedNodeSet - } - - function overflowObserved() { - checkOverflow() - - switch (true) { - case !hasOverflowUpdated: - return - - case overflowedNodeSet.size > 1: - info('Overflowed Elements:', overflowedNodeSet) - break - - case hasOverflow: - break - - default: - info('No overflow detected') - } - - sendSize(OVERFLOW_OBSERVER, 'Overflow updated') - } - - function createOverflowObservers(nodeList) { - const overflowObserverOptions = { - root: document.documentElement, - side: calculateHeight ? HEIGHT_EDGE : WIDTH_EDGE, - } - - overflowObserver = createOverflowObserver( - overflowObserved, - overflowObserverOptions, - ) - - overflowObserver.attachObservers(nodeList) - - return overflowObserver - } - - function resizeObserved(entries) { - if (!Array.isArray(entries) || entries.length === 0) return - const el = entries[0].target - sendSize(RESIZE_OBSERVER, `Element resized <${getElementName(el)}>`) - } - - function createResizeObservers(nodeList) { - resizeObserver = createResizeObserver(resizeObserved) - resizeObserver.attachObserverToNonStaticElements(nodeList) - return resizeObserver - } - - function visibilityChange(isVisible) { - log(`Visible: %c${isVisible}`, HIGHLIGHT) - isHidden = !isVisible - sendSize(VISIBILITY_OBSERVER, 'Visibility changed') - } - - const getCombinedElementLists = (nodeList) => { - const elements = new Set() - - for (const node of nodeList) { - elements.add(node) - for (const element of getAllElements(node)) elements.add(element) - } - - info(`Inspecting:\n`, elements) - return elements - } - - const addObservers = (nodeList) => { - if (nodeList.size === 0) return - - consoleEvent('addObservers') - - const elements = getCombinedElementLists(nodeList) - - overflowObserver.attachObservers(elements) - resizeObserver.attachObserverToNonStaticElements(elements) - - endAutoGroup() - } - - const removeObservers = (nodeList) => { - if (nodeList.size === 0) return - - consoleEvent('removeObservers') - - const elements = getCombinedElementLists(nodeList) - - overflowObserver.detachObservers(elements) - resizeObserver.detachObservers(elements) - - endAutoGroup() - } - - function contentMutated({ addedNodes, removedNodes }) { - consoleEvent('contentMutated') - applySelectors() - checkAndSetupTags() - checkOverflow() - endAutoGroup() - - removeObservers(removedNodes) - addObservers(addedNodes) - } - - function mutationObserved(mutations) { - contentMutated(mutations) - sendSize(MUTATION_OBSERVER, 'Mutation Observed') - } - - function pushDisconnectsOnToTearDown(observers) { - tearDownList.push(...observers.map((observer) => observer.disconnect)) - } - - function attachObservers() { - const nodeList = getAllElements(document.documentElement) - - const observers = [ - createMutationObserver(mutationObserved), - createOverflowObservers(nodeList), - createPerformanceObserver(), - createResizeObservers(nodeList), - createVisibilityObserver(visibilityChange), - ] - - pushDisconnectsOnToTearDown(observers) - } - - function getMaxElement(side) { - performance.mark(PREF_START) - - const Side = capitalizeFirstLetter(side) - - let elVal = MIN_SIZE - let maxEl = document.documentElement - let maxVal = hasTags - ? 0 - : document.documentElement.getBoundingClientRect().bottom - - const targetElements = hasTags - ? taggedElements - : hasOverflow - ? Array.from(overflowedNodeSet) - : getAllElements(document.documentElement) // Width resizing may need to check all elements - - for (const element of targetElements) { - elVal = - element.getBoundingClientRect()[side] + - parseFloat(getComputedStyle(element).getPropertyValue(`margin-${side}`)) - - if (elVal > maxVal) { - maxVal = elVal - maxEl = element - } - } - - info(`${Side} position calculated from:`, maxEl) - info(`Checked %c${targetElements.length}%c elements`, HIGHLIGHT, FOREGROUND) - - performance.mark(PREF_END, { - detail: { - hasTags, - len: targetElements.length, - logging, - Side, - }, - }) - - return maxVal - } - - const getAllMeasurements = (dimension) => [ - dimension.bodyOffset(), - dimension.bodyScroll(), - dimension.documentElementOffset(), - dimension.documentElementScroll(), - dimension.boundingClientRect(), - ] - - const addNot = (tagName) => `:not(${tagName})` - const selector = `* ${Array.from(IGNORE_TAGS).map(addNot).join('')}` - const getAllElements = (node) => node.querySelectorAll(selector) - - function getOffsetSize(getDimension) { - const offset = getDimension.getOffset() - - if (offset !== 0) { - info(`Page offsetSize: %c${offset}px`, HIGHLIGHT) - } - - return offset - } - - const prevScrollSize = { - height: 0, - width: 0, - } - - const prevBoundingSize = { - height: 0, - width: 0, - } - - const getAdjustedScroll = (getDimension) => - getDimension.documentElementScroll() + Math.max(0, getDimension.getOffset()) - - const BOUNDING_FORMAT = [HIGHLIGHT, FOREGROUND, HIGHLIGHT] - - function getAutoSize(getDimension) { - function returnBoundingClientRect() { - prevBoundingSize[dimension] = boundingSize - prevScrollSize[dimension] = scrollSize - return Math.max(boundingSize, MIN_SIZE) - } - - const isHeight = getDimension === getHeight - const dimension = getDimension.label - const boundingSize = getDimension.boundingClientRect() - const ceilBoundingSize = Math.ceil(boundingSize) - const floorBoundingSize = Math.floor(boundingSize) - const scrollSize = getAdjustedScroll(getDimension) - const sizes = `HTML: %c${boundingSize}px %cPage: %c${scrollSize}px` - - let calculatedSize = MIN_SIZE - - switch (true) { - case !getDimension.enabled(): - return Math.max(scrollSize, MIN_SIZE) - - case hasTags: - info(`Found element with data-iframe-size attribute`) - calculatedSize = getDimension.taggedElement() - break - - case !hasOverflow && - firstRun && - prevBoundingSize[dimension] === 0 && - prevScrollSize[dimension] === 0: - info(`Initial page size values: ${sizes}`, ...BOUNDING_FORMAT) - calculatedSize = returnBoundingClientRect() - break - - case triggerLocked && - boundingSize === prevBoundingSize[dimension] && - scrollSize === prevScrollSize[dimension]: - info(`Size unchanged: ${sizes}`, ...BOUNDING_FORMAT) - calculatedSize = Math.max(boundingSize, scrollSize) - break - - case boundingSize === 0 && scrollSize !== 0: - info(`Page is hidden: ${sizes}`, ...BOUNDING_FORMAT) - calculatedSize = scrollSize - break - - case !hasOverflow && - boundingSize !== prevBoundingSize[dimension] && - scrollSize <= prevScrollSize[dimension]: - info(`New size: ${sizes} `, ...BOUNDING_FORMAT) - info( - `Previous size: %c${prevBoundingSize[dimension]}px`, - HIGHLIGHT, - ) - calculatedSize = returnBoundingClientRect() - break - - case !isHeight: - calculatedSize = getDimension.taggedElement() - break - - case !hasOverflow && boundingSize < prevBoundingSize[dimension]: - info(` size decreased: ${sizes}`, ...BOUNDING_FORMAT) - calculatedSize = returnBoundingClientRect() - break - - case scrollSize === floorBoundingSize || scrollSize === ceilBoundingSize: - info(` size equals page size: ${sizes}`, ...BOUNDING_FORMAT) - calculatedSize = returnBoundingClientRect() - break - - case boundingSize > scrollSize: - info(`Page size < size: ${sizes}`, ...BOUNDING_FORMAT) - calculatedSize = returnBoundingClientRect() - break - - case hasOverflow: - info(`Found elements possibly overflowing `) - calculatedSize = getDimension.taggedElement() - break - - default: - info(`Using size: ${sizes}`, ...BOUNDING_FORMAT) - calculatedSize = returnBoundingClientRect() - } - - info(`Content ${dimension}: %c${calculatedSize}px`, HIGHLIGHT) - - calculatedSize += getOffsetSize(getDimension) - - return Math.max(calculatedSize, MIN_SIZE) - } - - const getBodyOffset = () => { - const { body } = document - const style = getComputedStyle(body) - - return ( - body.offsetHeight + - parseInt(style.marginTop, BASE) + - parseInt(style.marginBottom, BASE) - ) - } - - const getHeight = { - label: HEIGHT, - enabled: () => calculateHeight, - getOffset: () => offsetHeight, - auto: () => getAutoSize(getHeight), - bodyOffset: getBodyOffset, - bodyScroll: () => document.body.scrollHeight, - offset: () => getHeight.bodyOffset(), // Backwards compatibility - custom: () => customCalcMethods.height(), - documentElementOffset: () => document.documentElement.offsetHeight, - documentElementScroll: () => document.documentElement.scrollHeight, - boundingClientRect: () => - Math.max( - document.documentElement.getBoundingClientRect().bottom, - document.body.getBoundingClientRect().bottom, - ), - max: () => Math.max(...getAllMeasurements(getHeight)), - min: () => Math.min(...getAllMeasurements(getHeight)), - grow: () => getHeight.max(), - lowestElement: () => getMaxElement(HEIGHT_EDGE), - taggedElement: () => getMaxElement(HEIGHT_EDGE), - } - - const getWidth = { - label: WIDTH, - enabled: () => calculateWidth, - getOffset: () => offsetWidth, - auto: () => getAutoSize(getWidth), - bodyScroll: () => document.body.scrollWidth, - bodyOffset: () => document.body.offsetWidth, - custom: () => customCalcMethods.width(), - documentElementScroll: () => document.documentElement.scrollWidth, - documentElementOffset: () => document.documentElement.offsetWidth, - boundingClientRect: () => - Math.max( - document.documentElement.getBoundingClientRect().right, - document.body.getBoundingClientRect().right, - ), - max: () => Math.max(...getAllMeasurements(getWidth)), - min: () => Math.min(...getAllMeasurements(getWidth)), - rightMostElement: () => getMaxElement(WIDTH_EDGE), - scroll: () => - Math.max(getWidth.bodyScroll(), getWidth.documentElementScroll()), - taggedElement: () => getMaxElement(WIDTH_EDGE), - } - - const checkTolerance = (a, b) => !(Math.abs(a - b) <= tolerance) - - function callOnBeforeResize(newSize) { - const returnedSize = onBeforeResize(newSize) - - if (returnedSize === undefined) { - throw new TypeError( - 'No value returned from onBeforeResize(), expected a numeric value', - ) - } - - if (Number.isNaN(returnedSize)) - throw new TypeError( - `Invalid value returned from onBeforeResize(): ${returnedSize}, expected Number`, - ) - - if (returnedSize < MIN_SIZE) { - throw new RangeError( - `Out of range value returned from onBeforeResize(): ${returnedSize}, must be at least ${MIN_SIZE}`, - ) - } - - return returnedSize - } - - function getNewSize(direction, mode) { - const calculatedSize = direction[mode]() - const newSize = - direction.enabled() && onBeforeResize !== undefined - ? callOnBeforeResize(calculatedSize) - : calculatedSize - - assert( - newSize >= MIN_SIZE, - `New iframe ${direction.label} is too small: ${newSize}, must be at least ${MIN_SIZE}`, - ) - - return newSize - } - - function sizeIframe( - triggerEvent, - triggerEventDesc, - customHeight, - customWidth, - msg, - ) { - const isSizeChangeDetected = () => - (calculateHeight && checkTolerance(height, newHeight)) || - (calculateWidth && checkTolerance(width, newWidth)) - - const newHeight = customHeight ?? getNewSize(getHeight, heightCalcMode) - const newWidth = customWidth ?? getNewSize(getWidth, widthCalcMode) - - const updateEvent = isSizeChangeDetected() - ? SIZE_CHANGE_DETECTED - : triggerEvent - - log(`Resize event: %c${triggerEventDesc}`, HIGHLIGHT) - - switch (updateEvent) { - case INIT: - case ENABLE: - case SIZE_CHANGE_DETECTED: - // lockTrigger() - height = newHeight - width = newWidth - // eslint-disable-next-line no-fallthrough - case SET_OFFSET_SIZE: - dispatchMessage(height, width, triggerEvent, msg) - break - - // the following case needs {} to prevent a compile error - case OVERFLOW_OBSERVER: - case MUTATION_OBSERVER: - case RESIZE_OBSERVER: - case VISIBILITY_OBSERVER: { - log(NO_CHANGE) - purge() - break - } - - default: - purge() - info(NO_CHANGE) - } - - timerActive = false // Reset time for next resize - } - - let sendPending = false - const sendFailed = once(() => advise(getModeData(4))) - let hiddenMessageShown = false - let rafId - - const sendSize = errorBoundary( - (triggerEvent, triggerEventDesc, customHeight, customWidth, msg) => { - consoleEvent(triggerEvent) - - switch (true) { - case isHidden === true: { - if (hiddenMessageShown === true) break - log('Iframe hidden - Ignored resize request') - hiddenMessageShown = true - sendPending = false - cancelAnimationFrame(rafId) - break - } - - // Ignore overflowObserver here, as more efficient than using - // mutationObserver to detect OVERFLOW_ATTR changes - case sendPending === true && triggerEvent !== OVERFLOW_OBSERVER: { - purge() - log('Resize already pending - Ignored resize request') - break // only update once per frame - } - - case !autoResize && !(triggerEvent in IGNORE_DISABLE_RESIZE): { - info('Resizing disabled') - break - } - - default: { - hiddenMessageShown = false - sendPending = true - totalTime = performance.now() - timerActive = true - - if (!rafId) - rafId = requestAnimationFrame(() => { - sendPending = false - rafId = null - consoleEvent('requestAnimationFrame') - debug(`Reset sendPending: %c${triggerEvent}`, HIGHLIGHT) - }) - - sizeIframe( - triggerEvent, - triggerEventDesc, - customHeight, - customWidth, - msg, - ) - } - } - - endAutoGroup() - }, - ) - - function lockTrigger() { - if (triggerLocked) { - log('TriggerLock blocked calculation') - return - } - triggerLocked = true - debug('Trigger event lock on') - - requestAnimationFrame(() => { - triggerLocked = false - debug('Trigger event lock off') - }) - } - - function triggerReset(triggerEvent) { - height = getHeight[heightCalcMode]() - width = getWidth[widthCalcMode]() - - sendMessage(height, width, triggerEvent) - } - - function resetIframe(triggerEventDesc) { - const hcm = heightCalcMode - heightCalcMode = heightCalcModeDefault - - log(`Reset trigger event: %c${triggerEventDesc}`, HIGHLIGHT) - lockTrigger() - triggerReset('reset') - - heightCalcMode = hcm - } - - function dispatchMessage(height, width, triggerEvent, msg, targetOrigin) { - if (mode < -1) return - - function setTargetOrigin() { - if (undefined === targetOrigin) { - targetOrigin = targetOriginDefault - return - } - - log(`Message targetOrigin: %c${targetOrigin}`, HIGHLIGHT) - } - - function displayTimeTaken() { - const timer = round(performance.now() - totalTime) - return triggerEvent === INIT - ? `Initialised iframe in %c${timer}ms` - : `Size calculated in %c${timer}ms` - } - - function dispatchToParent() { - const size = `${height}:${width}` - const message = `${parentId}:${size}:${triggerEvent}${undefined === msg ? '' : `:${msg}`}` - - if (sameOrigin) - try { - window.parent.iframeParentListener(MESSAGE_ID + message) - } catch (error) { - if (mode === 1) sendFailed() - else throw error - return - } - else target.postMessage(MESSAGE_ID + message, targetOrigin) - - if (timerActive) log(displayTimeTaken(), HIGHLIGHT) - - info( - `Sending message to parent page via ${sameOrigin ? 'sameOrigin' : 'postMessage'}: %c%c${message}`, - ITALIC, - HIGHLIGHT, - ) - } - - setTargetOrigin() - dispatchToParent() - } - - const sendMessage = errorBoundary( - (height, width, triggerEvent, message, targetOrigin) => { - consoleEvent(triggerEvent) - dispatchMessage(height, width, triggerEvent, message, targetOrigin) - endAutoGroup() - }, - ) - - function receiver(event) { - consoleEvent('onMessage') - const { freeze } = Object - const { parse } = JSON - const parseFrozen = (data) => freeze(parse(data)) - - const notExpected = (type) => sendMessage(0, 0, `${type}Stop`) - - const processRequestFromParent = { - init: function initFromParent() { - if (document.readyState === 'loading') { - log('Page not ready, ignoring init message') - return - } - - const data = event.data.slice(MESSAGE_ID_LENGTH).split(SEPARATOR) - - target = event.source - origin = event.origin - - init(data) - - firstRun = false - - setTimeout(() => { - initLock = false - }, eventCancelTimer) - }, - - reset() { - if (initLock) { - log('Page reset ignored by init') - return - } - - log('Page size reset by host page') - triggerReset('resetPage') - }, - - resize() { - // This method is used by the tabVisible event on the parent page - log('Resize requested by host page') - sendSize(PARENT_RESIZE_REQUEST, 'Parent window requested size check') - }, - - moveToAnchor() { - inPageLinks.findTarget(getData()) - }, - - inPageLink() { - this.moveToAnchor() - }, // Backward compatibility - - pageInfo() { - const msgBody = getData() - log(`PageInfo received from parent:`, parseFrozen(msgBody)) - if (onPageInfo) { - isolateUserCode(onPageInfo, parse(msgBody)) - } else { - notExpected(PAGE_INFO) - } - }, - - parentInfo() { - const msgBody = parseFrozen(getData()) - log(`ParentInfo received from parent:`, msgBody) - if (onParentInfo) { - isolateUserCode(onParentInfo, msgBody) - } else { - notExpected(PARENT_INFO) - } - }, - - message() { - const msgBody = getData() - log(`onMessage called from parent:%c`, HIGHLIGHT, parseFrozen(msgBody)) - // eslint-disable-next-line sonarjs/no-extra-arguments - isolateUserCode(onMessage, parse(msgBody)) - }, - } - - const isMessageForUs = () => - MESSAGE_ID === `${event.data}`.slice(0, MESSAGE_ID_LENGTH) - - const getMessageType = () => event.data.split(']')[1].split(SEPARATOR)[0] - - const getData = () => event.data.slice(event.data.indexOf(SEPARATOR) + 1) - - const isMiddleTier = () => - 'iframeResize' in window || - (window.jQuery !== undefined && '' in window.jQuery.prototype) - - // Test if this message is from a child below us. This is an ugly test, however, updating - // the message format would break backwards compatibility. - const isInitMsg = () => - event.data.split(SEPARATOR)[2] in { true: 1, false: 1 } - - function callFromParent() { - const messageType = getMessageType() - - consoleEvent(messageType) - - if (messageType in processRequestFromParent) { - processRequestFromParent[messageType]() - return - } - - if (!isMiddleTier() && !isInitMsg()) { - warn(`Unexpected message (${event.data})`) - } - } - - function processMessage() { - if (firstRun === false) { - callFromParent() - return - } - - if (isInitMsg()) { - label(getMessageType()) - consoleEvent(INIT) - processRequestFromParent.init() - return - } - - log( - `Ignored message of type "${getMessageType()}". Received before initialization.`, - ) - } - - if (isMessageForUs()) { - processMessage() - } - } - - const received = errorBoundary(receiver) - - // Normally the parent kicks things off when it detects the iframe has loaded. - // If this script is async-loaded, then tell parent page to retry init. - let sent = false - const sendReady = (target) => - target.postMessage( - CHILD_READY_MESSAGE, - window?.iframeResizer?.targetOrigin || '*', - ) - - function ready() { - if (document.readyState === 'loading' || !firstRun || sent) return - - const { parent, top } = window - - consoleEvent('ready') - log( - 'Sending%c ready%c to parent from', - HIGHLIGHT, - FOREGROUND, - window.location.href, - ) - - sendReady(parent) - if (parent !== top) sendReady(top) - - sent = true - } - if ('iframeChildListener' in window) { warn('Already setup') - } else { - window.iframeChildListener = (data) => - setTimeout(() => received({ data, sameOrigin: true })) + return + } - consoleEvent('listen') - addEventListener(window, MESSAGE, received) - addEventListener(document, READY_STATE_CHANGE, ready) + window.iframeChildListener = (data) => + setTimeout(() => received({ data, sameOrigin: true })) - ready() - } + consoleEvent('listen') + addEventListener(window, MESSAGE, received) + addEventListener(document, READY_STATE_CHANGE, ready) + + ready() /* TEST CODE START */ function mockMsgListener(msgObject) { received(msgObject) - return win + return state.win } try { // eslint-disable-next-line no-restricted-globals if (top?.document?.getElementById('banner')) { - win = {} + state.win = {} // Create test hooks window.mockMsgListener = mockMsgListener diff --git a/packages/child/init.js b/packages/child/init.js new file mode 100644 index 000000000..771b23bc1 --- /dev/null +++ b/packages/child/init.js @@ -0,0 +1,111 @@ +import { INIT, VERSION } from '../common/consts' +import { id, once } from '../common/utils' +import checkBlockingCSS from './check/blocking-css' +import checkBoth from './check/both' +import { checkHeightMode, checkWidthMode } from './check/calculation-mode' +import checkCrossDomain from './check/cross-domain' +import checkDeprecatedAttrs from './check/deprecated-attributes' +import checkIgnoredElements from './check/ignored-elements' +import checkMode from './check/mode' +import checkQuirksMode from './check/quirks-mode' +import checkReadyYet from './check/ready' +import checkSettings from './check/settings' +import checkAndSetupTags from './check/tags' +import checkVersion from './check/version' +import { + endAutoGroup, + event as consoleEvent, + log, + setConsoleOptions, +} from './console' +import setupMouseEvents from './events/mouse' +import setupOnPageHide from './events/page-hide' +import setupPrintListeners from './events/print' +import setupPublicMethods from './methods' +import attachObservers from './observed' +import createApplySelectors from './page/apply-selectors' +import injectClearFixIntoBodyElement from './page/clear-fix' +import { setBodyStyle, setMargin } from './page/css' +import setupInPageLinks from './page/links' +import stopInfiniteResizingOfIframe from './page/stop-infinite-resizing' +import readDataFromPage from './read/from-page' +import readDataFromParent from './read/from-parent' +import sendSize from './send/size' +import sendTitle from './send/title' +import isolate from './utils/isolate' +import map2settings from './utils/map-settings' +import settings from './values/settings' +import state from './values/state' + +function startLogging({ logExpand, logging, parentId }) { + setConsoleOptions({ id: parentId, enabled: logging, expand: logExpand }) + consoleEvent('initReceived') + log(`Initialising iframe v${VERSION} ${window.location.href}`) +} + +function startIframeResizerChild({ + bodyBackground, + bodyPadding, + inPageLinks, + onReady, +}) { + const bothDirections = checkBoth(settings) + + const setup = [ + () => checkVersion(settings), + () => checkMode(settings), + checkIgnoredElements, + checkCrossDomain, + checkHeightMode, + checkWidthMode, + checkDeprecatedAttrs, + checkQuirksMode, + checkAndSetupTags, + checkSettings, + bothDirections ? id : checkBlockingCSS, + + () => setMargin(settings), + () => setBodyStyle('background', bodyBackground), + () => setBodyStyle('padding', bodyPadding), + + bothDirections ? id : stopInfiniteResizingOfIframe, + injectClearFixIntoBodyElement, + + state.applySelectors, + attachObservers, + + () => setupInPageLinks(inPageLinks), + setupPrintListeners, + () => setupMouseEvents(settings), + setupOnPageHide, + () => setupPublicMethods(), + ] + + isolate(setup) + + checkReadyYet(once(onReady)) + log('Initialization complete', settings) + endAutoGroup() + + sendSize( + INIT, + 'Init message from host page', + undefined, + undefined, + `${VERSION}:${settings.mode}`, + ) + + sendTitle() +} + +export default function (data) { + if (!state.firstRun) return + + map2settings(readDataFromParent(data)) + startLogging(settings) + map2settings(readDataFromPage()) + + state.applySelectors = createApplySelectors(settings) + + startIframeResizerChild(settings) +} diff --git a/packages/child/methods/auto-resize.js b/packages/child/methods/auto-resize.js new file mode 100644 index 000000000..8f78d4cbe --- /dev/null +++ b/packages/child/methods/auto-resize.js @@ -0,0 +1,30 @@ +import { AUTO_RESIZE, BOOLEAN, ENABLE, NONE } from '../../common/consts' +import { typeAssert } from '../../common/utils' +import { advise, event as consoleEvent } from '../console' +import sendMessage from '../send/message' +import sendSize from '../send/size' +import settings from '../values/settings' + +export default function autoResize(enable) { + typeAssert(enable, BOOLEAN, 'parentIframe.autoResize(enable) enable') + const { autoResize, calculateHeight, calculateWidth } = settings + + if (calculateWidth === false && calculateHeight === false) { + consoleEvent(ENABLE) + advise( + `Auto Resize can not be changed when direction is set to '${NONE}'.`, // or '${BOTH}' + ) + return false + } + + if (enable === true && autoResize === false) { + settings.autoResize = true + queueMicrotask(() => sendSize(ENABLE, 'Auto Resize enabled')) + } else if (enable === false && autoResize === true) { + settings.autoResize = false + } + + sendMessage(0, 0, AUTO_RESIZE, JSON.stringify(settings.autoResize)) + + return settings.autoResize +} diff --git a/packages/child/methods/calculation-methods.js b/packages/child/methods/calculation-methods.js new file mode 100644 index 000000000..72b172680 --- /dev/null +++ b/packages/child/methods/calculation-methods.js @@ -0,0 +1,12 @@ +import { checkHeightMode, checkWidthMode } from '../check/calculation-mode' +import settings from '../values/settings' + +export function setHeightCalculationMethod(heightCalculationMethod) { + settings.heightCalcMode = heightCalculationMethod + checkHeightMode() +} + +export function setWidthCalculationMethod(widthCalculationMethod) { + settings.widthCalcMode = widthCalculationMethod + checkWidthMode() +} diff --git a/packages/child/methods/index.js b/packages/child/methods/index.js new file mode 100644 index 000000000..9a7942b98 --- /dev/null +++ b/packages/child/methods/index.js @@ -0,0 +1,56 @@ +import { CLOSE } from '../../common/consts' +import { warn } from '../console' +import { resetIframe } from '../page/reset' +import APIsendMessage from '../send/message' +import settings from '../values/settings' +import state from '../values/state' +import autoResize from './auto-resize' +import { + setHeightCalculationMethod, + setWidthCalculationMethod, +} from './calculation-methods' +import moveToAnchor from './move-to-anchor' +import setOffsetSize from './offset-size' +import { getOrigin, getParentOrigin, setTargetOrigin } from './origin' +import getPageInfo from './page-info' +import { getParentProperties, getParentProps } from './parent-props' +import deprecationProxy from './proxy' +import resize from './resize' +import { scrollBy, scrollTo, scrollToOffset } from './scroll' +import sendMessage from './send-message' + +const close = () => APIsendMessage(0, 0, CLOSE) +const getId = () => settings.parentId +const reset = () => resetIframe('parentIframe.reset') +const size = () => + warn('parentIframe.size() has been renamed parentIframe.resize()') + +export default function setupPublicMethods() { + const { win } = state // Required for old Karma tests + if (settings.mode === 1) return + + win.parentIframe = Object.freeze({ + autoResize, + close, + getId, + getOrigin, // TODO Remove in V6 + getParentOrigin, + getPageInfo, // TODO Remove in V6 + getParentProps, + getParentProperties, // TODO Remove in V6 + moveToAnchor, + reset, + setOffsetSize, + scrollBy, + scrollTo, + scrollToOffset, + sendMessage, + setHeightCalculationMethod, + setWidthCalculationMethod, + setTargetOrigin, + resize, + size, // TODO Remove in V6 + }) + + win.parentIFrame = deprecationProxy(win.parentIframe) +} diff --git a/packages/child/methods/move-to-anchor.js b/packages/child/methods/move-to-anchor.js new file mode 100644 index 000000000..f477f7e2f --- /dev/null +++ b/packages/child/methods/move-to-anchor.js @@ -0,0 +1,8 @@ +import { STRING } from '../../common/consts' +import { typeAssert } from '../../common/utils' +import state from '../values/state' + +export default function moveToAnchor(anchor) { + typeAssert(anchor, STRING, 'parentIframe.moveToAnchor(anchor) anchor') + state.inPageLinks.findTarget(anchor) +} diff --git a/packages/child/methods/offset-size.js b/packages/child/methods/offset-size.js new file mode 100644 index 000000000..22d0d2f86 --- /dev/null +++ b/packages/child/methods/offset-size.js @@ -0,0 +1,11 @@ +import { NUMBER, SET_OFFSET_SIZE } from '../../common/consts' +import { typeAssert } from '../../common/utils' +import sendSize from '../send/size' +import settings from '../values/settings' + +export default function setOffsetSize(newOffset) { + typeAssert(newOffset, NUMBER, 'parentIframe.setOffsetSize(offset) offset') + settings.offsetHeight = newOffset + settings.offsetWidth = newOffset + sendSize(SET_OFFSET_SIZE, `parentIframe.setOffsetSize(${newOffset})`) +} diff --git a/packages/child/methods/origin.js b/packages/child/methods/origin.js new file mode 100644 index 000000000..8d9ce3fef --- /dev/null +++ b/packages/child/methods/origin.js @@ -0,0 +1,26 @@ +import { HIGHLIGHT } from 'auto-console-group' + +import { STRING } from '../../common/consts' +import { typeAssert } from '../../common/utils' +import { deprecateMethod, event as consoleEvent, log } from '../console' +import settings from '../values/settings' +import state from '../values/state' + +export function getOrigin() { + consoleEvent('getOrigin') + deprecateMethod('getOrigin()', 'getParentOrigin()') + return state.origin +} + +export const getParentOrigin = () => state.origin + +export function setTargetOrigin(targetOrigin) { + typeAssert( + targetOrigin, + STRING, + 'parentIframe.setTargetOrigin(targetOrigin) targetOrigin', + ) + + log(`Set targetOrigin: %c${targetOrigin}`, HIGHLIGHT) + settings.targetOrigin = targetOrigin +} diff --git a/packages/child/methods/page-info.js b/packages/child/methods/page-info.js new file mode 100644 index 000000000..a20cfc59d --- /dev/null +++ b/packages/child/methods/page-info.js @@ -0,0 +1,20 @@ +import { FUNCTION, PAGE_INFO, PAGE_INFO_STOP } from '../../common/consts' +import { deprecateMethodReplace } from '../console' +import sendMessage from '../send/message' +import state from '../values/state' + +export default function getPageInfo(callback) { + if (typeof callback === FUNCTION) { + state.onPageInfo = callback + sendMessage(0, 0, PAGE_INFO) + deprecateMethodReplace( + 'getPageInfo()', + 'getParentProps()', + 'See https://iframe-resizer.com/upgrade for details. ', + ) + return + } + + state.onPageInfo = null + sendMessage(0, 0, PAGE_INFO_STOP) +} diff --git a/packages/child/methods/parent-props.js b/packages/child/methods/parent-props.js new file mode 100644 index 000000000..932d48a5a --- /dev/null +++ b/packages/child/methods/parent-props.js @@ -0,0 +1,26 @@ +import { FUNCTION, PARENT_INFO, PARENT_INFO_STOP } from '../../common/consts' +import { typeAssert } from '../../common/utils' +import { deprecateMethod } from '../console' +import sendMessage from '../send/message' +import state from '../values/state' + +export function getParentProps(callback) { + typeAssert( + callback, + FUNCTION, + 'parentIframe.getParentProps(callback) callback', + ) + + state.onParentInfo = callback + sendMessage(0, 0, PARENT_INFO) + + return () => { + state.onParentInfo = null + sendMessage(0, 0, PARENT_INFO_STOP) + } +} + +export function getParentProperties(callback) { + deprecateMethod('getParentProperties()', 'getParentProps()') + this.getParentProps(callback) +} diff --git a/packages/child/methods/proxy.js b/packages/child/methods/proxy.js new file mode 100644 index 000000000..13f0cdb31 --- /dev/null +++ b/packages/child/methods/proxy.js @@ -0,0 +1,35 @@ +import { advise } from '../console' + +const oldObjectName = (prop) => + `Deprecated API object name +The window.parentIFrame object has been renamed to window.parentIframe. Please update your code as the old object will be removed in a future version. + +Called property: '${String(prop)}' +` + +export default function deprecationProxy(target) { + const warnedProps = new Set() + + return new Proxy(target, { + get(target, prop) { + if (!warnedProps.has(prop)) { + advise(oldObjectName(prop)) + warnedProps.add(prop) + } + + const value = target[prop] + const descriptor = Object.getOwnPropertyDescriptor(target, prop) + + // If property is non-configurable and non-writable, return the actual value + if (descriptor && !descriptor.configurable && !descriptor.writable) { + return value + } + + if (typeof value === 'function') { + return value.bind(target) + } + + return value + }, + }) +} diff --git a/packages/child/methods/resize.js b/packages/child/methods/resize.js new file mode 100644 index 000000000..a0357b340 --- /dev/null +++ b/packages/child/methods/resize.js @@ -0,0 +1,28 @@ +import { MANUAL_RESIZE_REQUEST, NUMBER } from '../../common/consts' +import { typeAssert } from '../../common/utils' +import sendSize from '../send/size' + +export default function resize(customHeight, customWidth) { + if (customHeight !== undefined) + typeAssert( + customHeight, + NUMBER, + 'parentIframe.resize(customHeight, customWidth) customHeight', + ) + + if (customWidth !== undefined) + typeAssert( + customWidth, + NUMBER, + 'parentIframe.resize(customHeight, customWidth) customWidth', + ) + + const valString = `${customHeight || ''}${customWidth ? `,${customWidth}` : ''}` + + sendSize( + MANUAL_RESIZE_REQUEST, + `parentIframe.resize(${valString})`, + customHeight, + customWidth, + ) +} diff --git a/packages/child/methods/scroll.js b/packages/child/methods/scroll.js new file mode 100644 index 000000000..879c8e3aa --- /dev/null +++ b/packages/child/methods/scroll.js @@ -0,0 +1,18 @@ +import { + NUMBER, + SCROLL_BY, + SCROLL_TO, + SCROLL_TO_OFFSET, +} from '../../common/consts' +import { typeAssert } from '../../common/utils' +import sendMessage from '../send/message' + +const createScrollMethod = (TYPE) => (x, y) => { + typeAssert(x, NUMBER, `parentIframe.${TYPE}(x, y) x`) + typeAssert(y, NUMBER, `parentIframe.${TYPE}(x, y) y`) + sendMessage(y, x, TYPE) // X&Y reversed at sendMessage uses height/width +} + +export const scrollBy = createScrollMethod(SCROLL_BY) +export const scrollTo = createScrollMethod(SCROLL_TO) +export const scrollToOffset = createScrollMethod(SCROLL_TO_OFFSET) diff --git a/packages/child/methods/send-message.js b/packages/child/methods/send-message.js new file mode 100644 index 000000000..2ea648be9 --- /dev/null +++ b/packages/child/methods/send-message.js @@ -0,0 +1,13 @@ +import { MESSAGE, STRING } from '../../common/consts' +import { typeAssert } from '../../common/utils' +import sendMessage from '../send/message' + +export default function (msg, targetOrigin) { + if (targetOrigin) + typeAssert( + targetOrigin, + STRING, + 'parentIframe.sendMessage(msg, targetOrigin) targetOrigin', + ) + sendMessage(0, 0, MESSAGE, JSON.stringify(msg), targetOrigin) +} diff --git a/packages/child/observed/index.js b/packages/child/observed/index.js new file mode 100644 index 000000000..1520d6c53 --- /dev/null +++ b/packages/child/observed/index.js @@ -0,0 +1,27 @@ +import { tearDownList } from '../events/listeners' +import createMutationObserver from '../observers/mutation' +import createPerformanceObserver from '../observers/perf' +import createVisibilityObserver from '../observers/visibility' +import { getAllElements } from '../size/all' +import mutationObserved from './mutation' +import createOverflowObservers from './overflow' +import createResizeObservers from './resize' +import visibilityChange from './visibility' + +function pushDisconnectsOnToTearDown(observers) { + tearDownList.push(...observers.map((observer) => observer.disconnect)) +} + +export default function attachObservers() { + const nodeList = getAllElements(document.documentElement) + + const observers = [ + createMutationObserver(mutationObserved), + createOverflowObservers(nodeList), + createPerformanceObserver(), + createResizeObservers(nodeList), + createVisibilityObserver(visibilityChange), + ] + + pushDisconnectsOnToTearDown(observers) +} diff --git a/packages/child/observed/mutation.js b/packages/child/observed/mutation.js new file mode 100644 index 000000000..b7dbfcb15 --- /dev/null +++ b/packages/child/observed/mutation.js @@ -0,0 +1,63 @@ +import { MUTATION_OBSERVER } from '../../common/consts' +import checkOverflow from '../check/overflow' +import checkAndSetupTags from '../check/tags' +import { endAutoGroup, event as consoleEvent, info } from '../console' +import sendSize from '../send/size' +import { getAllElements } from '../size/all' +import state from '../values/state' +import observers from './observers' + +const getCombinedElementLists = (nodeList) => { + const elements = new Set() + + for (const node of nodeList) { + elements.add(node) + for (const element of getAllElements(node)) elements.add(element) + } + + info(`Inspecting:\n`, elements) + + return elements +} + +const addObservers = (nodeList) => { + if (nodeList.size === 0) return + + consoleEvent('addObservers') + + const elements = getCombinedElementLists(nodeList) + + observers.overflow.attachObservers(elements) + observers.resize.attachObserverToNonStaticElements(elements) + + endAutoGroup() +} + +const removeObservers = (nodeList) => { + if (nodeList.size === 0) return + + consoleEvent('removeObservers') + + const elements = getCombinedElementLists(nodeList) + + observers.overflow.detachObservers(elements) + observers.resize.detachObservers(elements) + + endAutoGroup() +} + +function contentMutated({ addedNodes, removedNodes }) { + consoleEvent('contentMutated') + state.applySelectors() + checkAndSetupTags() + checkOverflow() + endAutoGroup() + + removeObservers(removedNodes) + addObservers(addedNodes) +} + +export default function mutationObserved(mutations) { + contentMutated(mutations) + sendSize(MUTATION_OBSERVER, 'Mutation Observed') +} diff --git a/packages/child/observed/observers.js b/packages/child/observed/observers.js new file mode 100644 index 000000000..b1c6ea436 --- /dev/null +++ b/packages/child/observed/observers.js @@ -0,0 +1 @@ +export default {} diff --git a/packages/child/observed/overflow.js b/packages/child/observed/overflow.js new file mode 100644 index 000000000..fbc855f61 --- /dev/null +++ b/packages/child/observed/overflow.js @@ -0,0 +1,43 @@ +import { HEIGHT_EDGE, OVERFLOW_OBSERVER, WIDTH_EDGE } from '../../common/consts' +import checkOverflow from '../check/overflow' +import { info } from '../console' +import createOverflowObserver from '../observers/overflow' +import sendSize from '../send/size' +import settings from '../values/settings' +import state from '../values/state' +import observers from './observers' + +function overflowObserved() { + const { hasOverflow } = state + const { hasOverflowUpdated, overflowedNodeSet } = checkOverflow() + + switch (true) { + case !hasOverflowUpdated: + return + + case overflowedNodeSet.size > 1: + info('Overflowed Elements:', overflowedNodeSet) + break + + case hasOverflow: + break + + default: + info('No overflow detected') + } + + sendSize(OVERFLOW_OBSERVER, 'Overflow updated') +} + +export default function createOverflowObservers(nodeList) { + const overflowOptions = { + root: document.documentElement, + side: settings.calculateHeight ? HEIGHT_EDGE : WIDTH_EDGE, + } + + observers.overflow = createOverflowObserver(overflowObserved, overflowOptions) + + observers.overflow.attachObservers(nodeList) + + return observers.overflow +} diff --git a/packages/child/observed/resize.js b/packages/child/observed/resize.js new file mode 100644 index 000000000..c172701e9 --- /dev/null +++ b/packages/child/observed/resize.js @@ -0,0 +1,18 @@ +import { RESIZE_OBSERVER } from '../../common/consts' +import { getElementName } from '../../common/utils' +import createResizeObserver from '../observers/resize' +import sendSize from '../send/size' +import observers from './observers' + +function resizeObserved(entries) { + if (!Array.isArray(entries) || entries.length === 0) return + const el = entries[0].target + sendSize(RESIZE_OBSERVER, `Element resized <${getElementName(el)}>`) +} + +export default function createResizeObservers(nodeList) { + observers.resize = createResizeObserver(resizeObserved) + observers.resize.attachObserverToNonStaticElements(nodeList) + + return observers.resize +} diff --git a/packages/child/observed/visibility.js b/packages/child/observed/visibility.js new file mode 100644 index 000000000..70ce87419 --- /dev/null +++ b/packages/child/observed/visibility.js @@ -0,0 +1,12 @@ +import { HIGHLIGHT } from 'auto-console-group' + +import { VISIBILITY_OBSERVER } from '../../common/consts' +import { log } from '../console' +import sendSize from '../send/size' +import state from '../values/state' + +export default function visibilityChange(isVisible) { + log(`Visible: %c${isVisible}`, HIGHLIGHT) + state.isHidden = !isVisible + sendSize(VISIBILITY_OBSERVER, 'Visibility changed') +} diff --git a/packages/child/page/apply-selectors.js b/packages/child/page/apply-selectors.js new file mode 100644 index 000000000..61e1d608d --- /dev/null +++ b/packages/child/page/apply-selectors.js @@ -0,0 +1,22 @@ +import { HIGHLIGHT } from 'auto-console-group' + +import { IGNORE_ATTR, SIZE_ATTR } from '../../common/consts' +import { log } from '../console' + +export const applySelector = (name, attribute, selector) => () => { + if (selector === '') return + + log(`${name}: %c${selector}`, HIGHLIGHT) + + for (const el of document.querySelectorAll(selector)) { + log(`Applying ${attribute} to:`, el) + el.toggleAttribute(attribute, true) + } +} + +export default function ({ sizeSelector, ignoreSelector }) { + return () => { + applySelector('sizeSelector', SIZE_ATTR, sizeSelector) + applySelector('ignoreSelector', IGNORE_ATTR, ignoreSelector) + } +} diff --git a/packages/child/page/clear-fix.js b/packages/child/page/clear-fix.js new file mode 100644 index 000000000..0b5c14738 --- /dev/null +++ b/packages/child/page/clear-fix.js @@ -0,0 +1,10 @@ +export default function injectClearFixIntoBodyElement() { + const clearFix = document.createElement('div') + + clearFix.style.clear = 'both' + // Guard against the following having been globally redefined in CSS. + clearFix.style.display = 'block' + clearFix.style.height = '0' + + document.body.append(clearFix) +} diff --git a/packages/child/page/css.js b/packages/child/page/css.js new file mode 100644 index 000000000..5a644bae3 --- /dev/null +++ b/packages/child/page/css.js @@ -0,0 +1,29 @@ +import { HIGHLIGHT } from 'auto-console-group' + +import { NULL } from '../../common/consts' +import { info, warn } from '../console' + +export function checkCSS(attr, value) { + if (value.includes('-')) { + warn(`Negative CSS value ignored for ${attr}`) + value = '' + } + + return value +} + +export function setBodyStyle(attr, value) { + if (undefined === value || value === '' || value === NULL) return + + document.body.style.setProperty(attr, value) + info(`Set body ${attr}: %c${value}`, HIGHLIGHT) +} + +export function setMargin({ bodyMarginStr, bodyMargin }) { + // If called via V1 script, convert bodyMargin from int to str + if (undefined === bodyMarginStr) { + bodyMarginStr = `${bodyMargin}px` + } + + setBodyStyle('margin', checkCSS('margin', bodyMarginStr)) +} diff --git a/packages/child/page/links.js b/packages/child/page/links.js new file mode 100644 index 000000000..0eb0d4d24 --- /dev/null +++ b/packages/child/page/links.js @@ -0,0 +1,112 @@ +import { FOREGROUND, HIGHLIGHT } from 'auto-console-group' + +import { + BASE, + EVENT_CANCEL_TIMER, + IN_PAGE_LINK, + SCROLL_TO_OFFSET, +} from '../../common/consts' +import { getModeData } from '../../common/mode' +import { advise, log } from '../console' +import { addEventListener } from '../events/listeners' +import sendMessage from '../send/message' +import settings from '../values/settings' +import state from '../values/state' + +const getPagePosition = () => ({ + x: document.documentElement.scrollLeft, + y: document.documentElement.scrollTop, +}) + +function getElementPosition(el) { + const elPosition = el.getBoundingClientRect() + const pagePosition = getPagePosition() + + return { + x: parseInt(elPosition.left, BASE) + parseInt(pagePosition.x, BASE), + y: parseInt(elPosition.top, BASE) + parseInt(pagePosition.y, BASE), + } +} + +function jumpToTarget(hash, target) { + const jumpPosition = getElementPosition(target) + + log( + `Moving to in page link (%c#${hash}%c) at x: %c${jumpPosition.x}%c y: %c${jumpPosition.y}`, + HIGHLIGHT, + FOREGROUND, + HIGHLIGHT, + FOREGROUND, + HIGHLIGHT, + ) + + sendMessage(jumpPosition.y, jumpPosition.x, SCROLL_TO_OFFSET) // X&Y reversed at sendMessage uses height/width +} + +function findTarget(location) { + const hash = location.split('#')[1] || location // Remove # if present + const hashData = decodeURIComponent(hash) + const target = + document.getElementById(hashData) || document.getElementsByName(hashData)[0] + + if (target !== undefined) { + jumpToTarget(hash, target) + return + } + + log(`In page link (#${hash}) not found in iframe, so sending to parent`) + sendMessage(0, 0, IN_PAGE_LINK, `#${hash}`) +} + +function checkLocationHash() { + const { hash, href } = window.location + + if (hash !== '' && hash !== '#') { + findTarget(href) + } +} + +function bindAnchors() { + for (const link of document.querySelectorAll('a[href^="#"]')) { + if (link.getAttribute('href') !== '#') { + addEventListener(link, 'click', (e) => { + e.preventDefault() + findTarget(link.getAttribute('href')) + }) + } + } +} + +function bindLocationHash() { + addEventListener(window, 'hashchange', checkLocationHash) +} + +function initCheck() { + // Check if page loaded with location hash after init resize + setTimeout(checkLocationHash, EVENT_CANCEL_TIMER) +} + +function enableInPageLinks() { + log('Setting up location.hash handlers') + bindAnchors() + bindLocationHash() + initCheck() + + state.inPageLinks = { + findTarget, + } +} + +export default function setupInPageLinks(enabled) { + const { mode } = settings + + if (enabled) { + if (mode === 1) { + advise(getModeData(5)) + } else { + enableInPageLinks() + } + } else { + log('In page linking not enabled') + } +} diff --git a/packages/child/page/reset.js b/packages/child/page/reset.js new file mode 100644 index 000000000..80b949b49 --- /dev/null +++ b/packages/child/page/reset.js @@ -0,0 +1,42 @@ +import { HIGHLIGHT } from 'auto-console-group' + +import { HEIGHT_CALC_MODE_DEFAULT } from '../../common/consts' +import { debug, log } from '../console' +import sendMessage from '../send/message' +import { getHeight, getWidth } from '../size' +import settings from '../values/settings' +import state from '../values/state' + +function lockTrigger() { + if (state.triggerLocked) { + log('TriggerLock blocked calculation') + return + } + + state.triggerLocked = true + debug('Trigger event lock on') + + requestAnimationFrame(() => { + state.triggerLocked = false + debug('Trigger event lock off') + }) +} + +export function triggerReset(triggerEvent) { + const { heightCalcMode, widthCalcMode } = settings + const height = getHeight[heightCalcMode]() + const width = getWidth[widthCalcMode]() + + log(`Reset trigger event: %c${triggerEvent}`, HIGHLIGHT) + sendMessage(height, width, triggerEvent) +} + +export function resetIframe(triggerEventDesc) { + const hcm = settings.heightCalcMode + + log(`Reset trigger event: %c${triggerEventDesc}`, HIGHLIGHT) + settings.heightCalcMode = HEIGHT_CALC_MODE_DEFAULT + lockTrigger() + triggerReset('reset') + settings.heightCalcMode = hcm +} diff --git a/packages/child/page/stop-infinite-resizing.js b/packages/child/page/stop-infinite-resizing.js new file mode 100644 index 000000000..30615d404 --- /dev/null +++ b/packages/child/page/stop-infinite-resizing.js @@ -0,0 +1,15 @@ +import { HIGHLIGHT } from 'auto-console-group' + +import { AUTO, HEIGHT } from '../../common/consts' +import { log } from '../console' + +const IMPORTANT = 'important' + +export default function stopInfiniteResizingOfIframe() { + const setAutoHeight = (el) => el.style.setProperty(HEIGHT, AUTO, IMPORTANT) + + setAutoHeight(document.documentElement) + setAutoHeight(document.body) + + log('Set HTML & body height: %cauto !important', HIGHLIGHT) +} diff --git a/packages/child/read.js b/packages/child/read.js deleted file mode 100644 index 8cbba7312..000000000 --- a/packages/child/read.js +++ /dev/null @@ -1,14 +0,0 @@ -import { BOOLEAN, FUNCTION, NUMBER, STRING } from '../common/consts' - -const read = (type) => (data, key) => { - if (!(key in data)) return - // eslint-disable-next-line valid-typeof, consistent-return - if (typeof data[key] === type) return data[key] - - throw new TypeError(`${key} is not a ${type}.`) -} - -export const readFunction = read(FUNCTION) -export const readBoolean = read(BOOLEAN) -export const readNumber = read(NUMBER) -export const readString = read(STRING) diff --git a/packages/child/read.test.js b/packages/child/read.test.js deleted file mode 100644 index 87417ed68..000000000 --- a/packages/child/read.test.js +++ /dev/null @@ -1,49 +0,0 @@ -import { readBoolean, readFunction, readNumber, readString } from './read' - -describe('read utility functions', () => { - const mockData = { - funcKey: () => {}, - boolKey: true, - numKey: 42, - strKey: 'test', - } - - test('readFunction should return the function if the key exists and is a function', () => { - expect(readFunction(mockData, 'funcKey')).toBe(mockData.funcKey) - }) - - test('readFunction should throw a TypeError if the key is not a function', () => { - expect(() => readFunction(mockData, 'boolKey')).toThrow(TypeError) - }) - - test('readBoolean should return the boolean if the key exists and is a boolean', () => { - expect(readBoolean(mockData, 'boolKey')).toBe(true) - }) - - test('readBoolean should throw a TypeError if the key is not a boolean', () => { - expect(() => readBoolean(mockData, 'numKey')).toThrow(TypeError) - }) - - test('readNumber should return the number if the key exists and is a number', () => { - expect(readNumber(mockData, 'numKey')).toBe(42) - }) - - test('readNumber should throw a TypeError if the key is not a number', () => { - expect(() => readNumber(mockData, 'strKey')).toThrow(TypeError) - }) - - test('readString should return the string if the key exists and is a string', () => { - expect(readString(mockData, 'strKey')).toBe('test') - }) - - test('readString should throw a TypeError if the key is not a string', () => { - expect(() => readString(mockData, 'funcKey')).toThrow(TypeError) - }) - - test('read functions should return undefined if the key does not exist', () => { - expect(readFunction(mockData, 'missingKey')).toBeUndefined() - expect(readBoolean(mockData, 'missingKey')).toBeUndefined() - expect(readNumber(mockData, 'missingKey')).toBeUndefined() - expect(readString(mockData, 'missingKey')).toBeUndefined() - }) -}) diff --git a/packages/child/read/from-page.js b/packages/child/read/from-page.js new file mode 100644 index 000000000..31d79cb24 --- /dev/null +++ b/packages/child/read/from-page.js @@ -0,0 +1,81 @@ +import { + FUNCTION, + HEIGHT, + NUMBER, + OBJECT, + OFFSET, + OFFSET_SIZE, + STRING, + WIDTH, +} from '../../common/consts' +import { getKey } from '../../common/mode' +import { deprecateOption, log } from '../console' +import setupCustomCalcMethod from '../size/custom' +import settings from '../values/settings' + +const read = (type) => (data, key) => { + if (!(key in data)) return + // eslint-disable-next-line valid-typeof, consistent-return + if (typeof data[key] === type) return data[key] + + throw new TypeError(`${key} is not a ${type}.`) +} + +export const readFunction = read(FUNCTION) +export const readNumber = read(NUMBER) +export const readString = read(STRING) + +const isObject = (obj) => + obj !== null && typeof obj === OBJECT && !Array.isArray(obj) + +function readOffsetSize(data) { + const { calculateHeight, calculateWidth } = settings + let offsetHeight + let offsetWidth + + if (typeof data?.offset === NUMBER) { + deprecateOption(OFFSET, OFFSET_SIZE) + if (calculateHeight) offsetHeight = readNumber(data, OFFSET) + if (calculateWidth) offsetWidth = readNumber(data, OFFSET) + } + + if (typeof data?.offsetSize === NUMBER) { + if (calculateHeight) offsetHeight = readNumber(data, OFFSET_SIZE) + if (calculateWidth) offsetWidth = readNumber(data, OFFSET_SIZE) + } + + return { offsetHeight, offsetWidth } +} + +function readData(data) { + log(`Reading data from page:`, Object.keys(data)) + + const { offsetHeight, offsetWidth } = readOffsetSize(data) + + return { + offsetHeight, + offsetWidth, + ignoreSelector: readString(data, 'ignoreSelector'), + key2: readString(data, getKey(0)), + sizeSelector: readString(data, 'sizeSelector'), + targetOrigin: readString(data, 'targetOrigin'), + + heightCalcMode: setupCustomCalcMethod( + data?.heightCalculationMethod, + HEIGHT, + ), + widthCalcMode: setupCustomCalcMethod(data?.widthCalculationMethod, WIDTH), + + onBeforeResize: readFunction(data, 'onBeforeResize'), + onMessage: readFunction(data, 'onMessage'), + onReady: readFunction(data, 'onReady'), + } +} + +export default function readDataFromPage() { + const { mode } = settings + if (mode === 1) return {} + + const data = window.iframeResizer || window.iFrameResizer + return isObject(data) ? readData(data) : {} +} diff --git a/packages/child/read/from-parent.js b/packages/child/read/from-parent.js new file mode 100644 index 000000000..8485e60de --- /dev/null +++ b/packages/child/read/from-parent.js @@ -0,0 +1,33 @@ +const strBool = (str) => str === 'true' + +const castDefined = (cast) => (data) => + undefined === data ? undefined : cast(data) + +const getBoolean = castDefined(strBool) +const getNumber = castDefined(Number) + +export default (data) => ({ + parentId: data[0], + bodyMargin: getNumber(data[1]), + calculateWidth: getBoolean(data[2]), + logging: getBoolean(data[3]), + // data[4] no longer used (was intervalTimer) + autoResize: getBoolean(data[6]), + bodyMarginStr: data[7], + heightCalcMode: data[8], + bodyBackground: data[9], + bodyPadding: data[10], + tolerance: getNumber(data[11]), + inPageLinks: getBoolean(data[12]), + // data[13] no longer used (was resizeFrom) + widthCalcMode: data[14], + mouseEvents: getBoolean(data[15]), + offsetHeight: getNumber(data[16]), + offsetWidth: getNumber(data[17]), + calculateHeight: getBoolean(data[18]), + key: data[19], + version: data[20], + mode: getNumber(data[21]), + // sizeSelector: data[22] // Now only available via page settings + logExpand: getBoolean(data[23]), +}) diff --git a/packages/child/received/index.js b/packages/child/received/index.js new file mode 100644 index 000000000..93b3cfe8b --- /dev/null +++ b/packages/child/received/index.js @@ -0,0 +1,33 @@ +import { errorBoundary, event as consoleEvent, warn } from '../console' +import state from '../values/state' +import { isMessageForUs, isMiddleTier } from './is' +import processRequest from './process-request' +import { getMessageType } from './utils' + +function receiver(event) { + const { firstRun } = state + const messageType = getMessageType(event) + + consoleEvent(messageType) + + switch (true) { + case messageType in processRequest: + processRequest[messageType](event) + break + + case firstRun && isMiddleTier(): + warn( + `Ignored message of type "${messageType}". Received before initialization.`, + ) + break + + default: + warn( + `Unexpected message (${event.data}), this is likely due to a newer version on iframe-resizer running on the parent page.`, + ) + } +} + +export default errorBoundary((event) => { + if (isMessageForUs(event)) receiver(event) +}) diff --git a/packages/child/received/init.js b/packages/child/received/init.js new file mode 100644 index 000000000..26a480178 --- /dev/null +++ b/packages/child/received/init.js @@ -0,0 +1,28 @@ +import { + EVENT_CANCEL_TIMER, + MESSAGE_ID_LENGTH, + SEPARATOR, +} from '../../common/consts' +import { log } from '../console' +import init from '../init' +import state from '../values/state' + +export default function initFromParent(event) { + if (document.readyState === 'loading') { + log('Page not ready, ignoring init message') + return + } + + const data = event.data.slice(MESSAGE_ID_LENGTH).split(SEPARATOR) + + state.target = event.source + state.origin = event.origin + + init(data) + + state.firstRun = false + + setTimeout(() => { + state.initLock = false + }, EVENT_CANCEL_TIMER) +} diff --git a/packages/child/received/is.js b/packages/child/received/is.js new file mode 100644 index 000000000..a1c2d8025 --- /dev/null +++ b/packages/child/received/is.js @@ -0,0 +1,15 @@ +import { MESSAGE_ID, MESSAGE_ID_LENGTH, SEPARATOR } from '../../common/consts' + +const IFRAME_RESIZE = 'iframeResize' + +export const isMessageForUs = (event) => + MESSAGE_ID === `${event.data}`.slice(0, MESSAGE_ID_LENGTH) + +export const isMiddleTier = () => + IFRAME_RESIZE in window || + (window.jQuery !== undefined && IFRAME_RESIZE in window.jQuery.prototype) + +// Test if this message is from a child below us. This is an ugly test, +// however, updating the message format would break backwards compatibility. +export const isInitMessage = (event) => + event.data.split(SEPARATOR)[2] in { true: 1, false: 1 } diff --git a/packages/child/received/message.js b/packages/child/received/message.js new file mode 100644 index 000000000..107c0db43 --- /dev/null +++ b/packages/child/received/message.js @@ -0,0 +1,15 @@ +import { HIGHLIGHT } from 'auto-console-group' + +import { isolateUserCode } from '../../common/utils' +import { log } from '../console' +import settings from '../values/settings' +import { getData, parse, parseFrozen } from './utils' + +export default function message(event) { + const msgBody = getData(event) + + log(`onMessage called from parent:%c`, HIGHLIGHT, parseFrozen(msgBody)) + + // eslint-disable-next-line sonarjs/no-extra-arguments + isolateUserCode(settings.onMessage, parse(msgBody)) +} diff --git a/packages/child/received/page-info.js b/packages/child/received/page-info.js new file mode 100644 index 000000000..11a7a0a52 --- /dev/null +++ b/packages/child/received/page-info.js @@ -0,0 +1,18 @@ +import { PAGE_INFO } from '../../common/consts' +import { isolateUserCode } from '../../common/utils' +import { log } from '../console' +import state from '../values/state' +import { getData, notExpected, parse, parseFrozen } from './utils' + +export default function pageInfo(event) { + const { onPageInfo } = state + const messageBody = getData(event) + + log(`PageInfo received from parent:`, parseFrozen(messageBody)) + + if (onPageInfo) { + isolateUserCode(onPageInfo, parse(messageBody)) + } else { + notExpected(PAGE_INFO) + } +} diff --git a/packages/child/received/parent-info.js b/packages/child/received/parent-info.js new file mode 100644 index 000000000..31a62c71a --- /dev/null +++ b/packages/child/received/parent-info.js @@ -0,0 +1,18 @@ +import { PARENT_INFO } from '../../common/consts' +import { isolateUserCode } from '../../common/utils' +import { log } from '../console' +import state from '../values/state' +import { getData, notExpected, parseFrozen } from './utils' + +export default function parentInfo(event) { + const { onParentInfo } = state + const messageBody = parseFrozen(getData(event)) + + log(`ParentInfo received from parent:`, messageBody) + + if (onParentInfo) { + isolateUserCode(onParentInfo, messageBody) + } else { + notExpected(PARENT_INFO) + } +} diff --git a/packages/child/received/process-request.js b/packages/child/received/process-request.js new file mode 100644 index 000000000..502f4124b --- /dev/null +++ b/packages/child/received/process-request.js @@ -0,0 +1,21 @@ +import state from '../values/state' +import init from './init' +import message from './message' +import pageInfo from './page-info' +import parentInfo from './parent-info' +import reset from './reset' +import resize from './resize' +import { getData } from './utils' + +const moveToAnchor = (event) => state.inPageLinks.findTarget(getData(event)) + +export default { + init, + reset, + resize, + moveToAnchor, + inPageLink: moveToAnchor, // Backward compatibility + pageInfo, + parentInfo, + message, +} diff --git a/packages/child/received/reset.js b/packages/child/received/reset.js new file mode 100644 index 000000000..6197a247d --- /dev/null +++ b/packages/child/received/reset.js @@ -0,0 +1,13 @@ +import { log } from '../console' +import { triggerReset } from '../page/reset' +import state from '../values/state' + +export default function reset() { + if (state.initLock) { + log('Page reset ignored by init') + return + } + + log('Page size reset by host page') + triggerReset('resetPage') +} diff --git a/packages/child/received/resize.js b/packages/child/received/resize.js new file mode 100644 index 000000000..d65477c09 --- /dev/null +++ b/packages/child/received/resize.js @@ -0,0 +1,9 @@ +import { PARENT_RESIZE_REQUEST } from '../../common/consts' +import { log } from '../console' +import sendSize from '../send/size' + +// This method is used by the tabVisible event on the parent page +export default function resize() { + log('Resize requested by host page') + sendSize(PARENT_RESIZE_REQUEST, 'Parent window requested size check') +} diff --git a/packages/child/received/utils.js b/packages/child/received/utils.js new file mode 100644 index 000000000..c47f6f0bb --- /dev/null +++ b/packages/child/received/utils.js @@ -0,0 +1,15 @@ +import { INIT, SEPARATOR } from '../../common/consts' +import sendMessage from '../send/message' +import { isInitMessage } from './is' + +const { freeze } = Object +export const { parse } = JSON +export const parseFrozen = (data) => freeze(parse(data)) + +export const notExpected = (type) => sendMessage(0, 0, `${type}Stop`) + +export const getData = (event) => + event.data.slice(event.data.indexOf(SEPARATOR) + 1) + +export const getMessageType = (event) => + isInitMessage(event) ? INIT : event.data.split(']')[1].split(SEPARATOR)[0] diff --git a/packages/child/send/dispatch.js b/packages/child/send/dispatch.js new file mode 100644 index 000000000..e227812d5 --- /dev/null +++ b/packages/child/send/dispatch.js @@ -0,0 +1,72 @@ +import { HIGHLIGHT, ITALIC } from 'auto-console-group' + +import { INIT, MESSAGE_ID } from '../../common/consts' +import { getModeData } from '../../common/mode' +import { once, round } from '../../common/utils' +import { advise, info, log } from '../console' +import settings from '../values/settings' +import state from '../values/state' + +const sendFailed = once(() => advise(getModeData(4))) + +export function displayTimeTaken(triggerEvent) { + if (!state.timerActive) return + + const timer = round(performance.now() - state.totalTime) + const timeTaken = + triggerEvent === INIT + ? `Initialised iframe in %c${timer}ms` + : `Size calculated in %c${timer}ms` + + log(timeTaken, HIGHLIGHT) +} + +export function setTargetOrigin(targetOrigin) { + if (undefined === targetOrigin) targetOrigin = settings.targetOrigin + else log(`Message targetOrigin: %c${targetOrigin}`, HIGHLIGHT) + return targetOrigin +} + +export function dispatchToParent(message, targetOrigin) { + const { mode } = settings + const { sameOrigin, target } = state + + if (sameOrigin) + try { + window.parent.iframeParentListener(MESSAGE_ID + message) + } catch (error) { + if (mode === 1) sendFailed() + else throw error + return false + } + else target.postMessage(MESSAGE_ID + message, setTargetOrigin(targetOrigin)) + + return true +} + +export default function dispatch( + height, + width, + triggerEvent, + msg, + targetOrigin, +) { + const { parentId } = settings + const { sameOrigin } = state + const size = `${height}:${width}` + const message = `${parentId}:${size}:${triggerEvent}${undefined === msg ? '' : `:${msg}`}` + + if (settings.mode < -1) return + + const success = dispatchToParent(message, targetOrigin) + + if (!success) return + + displayTimeTaken(triggerEvent) + + info( + `Sending message to parent page via ${sameOrigin ? 'sameOrigin' : 'postMessage'}: %c%c${message}`, + ITALIC, + HIGHLIGHT, + ) +} diff --git a/packages/child/send/message.js b/packages/child/send/message.js new file mode 100644 index 000000000..5eadfea05 --- /dev/null +++ b/packages/child/send/message.js @@ -0,0 +1,10 @@ +import { endAutoGroup, errorBoundary, event as consoleEvent } from '../console' +import dispatch from './dispatch' + +export default errorBoundary( + (height, width, triggerEvent, message, targetOrigin) => { + consoleEvent(triggerEvent) + dispatch(height, width, triggerEvent, message, targetOrigin) + endAutoGroup() + }, +) diff --git a/packages/child/send/size.js b/packages/child/send/size.js new file mode 100644 index 000000000..c863f1c5f --- /dev/null +++ b/packages/child/send/size.js @@ -0,0 +1,87 @@ +import { HIGHLIGHT } from 'auto-console-group' + +import { IGNORE_DISABLE_RESIZE, OVERFLOW_OBSERVER } from '../../common/consts' +import { + debug, + endAutoGroup, + errorBoundary, + event as consoleEvent, + info, + log, + purge, +} from '../console' +import getContentSize from '../size/content' +import settings from '../values/settings' +import state from '../values/state' +import dispatch from './dispatch' + +let sendPending = false +let hiddenMessageShown = false +let rafId + +function sendSize( + triggerEvent, + triggerEventDesc, + customHeight, + customWidth, + msg, +) { + const { autoResize } = settings + const { isHidden } = state + + consoleEvent(triggerEvent) + + switch (true) { + case isHidden === true: { + if (hiddenMessageShown === true) break + log('Iframe hidden - Ignored resize request') + hiddenMessageShown = true + sendPending = false + cancelAnimationFrame(rafId) + break + } + + // Ignore overflowObserver here, as more efficient than using + // mutationObserver to detect OVERFLOW_ATTR changes + case sendPending === true && triggerEvent !== OVERFLOW_OBSERVER: { + purge() + log('Resize already pending - Ignored resize request') + break // only update once per frame + } + + case !autoResize && !(triggerEvent in IGNORE_DISABLE_RESIZE): { + info('Resizing disabled') + break + } + + default: { + hiddenMessageShown = false + sendPending = true + state.totalTime = performance.now() + state.timerActive = true + + const newSize = getContentSize( + triggerEvent, + triggerEventDesc, + customHeight, + customWidth, + ) + + if (newSize) dispatch(newSize.height, newSize.width, triggerEvent, msg) + + if (!rafId) + rafId = requestAnimationFrame(() => { + sendPending = false + rafId = null + consoleEvent('requestAnimationFrame') + debug(`Reset sendPending: %c${triggerEvent}`, HIGHLIGHT) + }) + + state.timerActive = false // Reset time for next resize + } + } + + endAutoGroup() +} + +export default errorBoundary(sendSize) diff --git a/packages/child/send/title.js b/packages/child/send/title.js new file mode 100644 index 000000000..8113e27e1 --- /dev/null +++ b/packages/child/send/title.js @@ -0,0 +1,7 @@ +import { TITLE } from '../../common/consts' +import sendMessage from './message' + +export default function sendTitle() { + const { title } = document + if (title && title !== '') sendMessage(0, 0, TITLE, title) +} diff --git a/packages/child/size/all.js b/packages/child/size/all.js new file mode 100644 index 000000000..3d97b1044 --- /dev/null +++ b/packages/child/size/all.js @@ -0,0 +1,14 @@ +import { IGNORE_TAGS } from '../../common/consts' + +const addNot = (tagName) => `:not(${tagName})` +const selector = `* ${Array.from(IGNORE_TAGS).map(addNot).join('')}` + +export const getAllElements = (node) => node.querySelectorAll(selector) + +export const getAllMeasurements = (dimension) => [ + dimension.bodyOffset(), + dimension.bodyScroll(), + dimension.documentElementOffset(), + dimension.documentElementScroll(), + dimension.boundingClientRect(), +] diff --git a/packages/child/size/auto.js b/packages/child/size/auto.js new file mode 100644 index 000000000..ebc61069e --- /dev/null +++ b/packages/child/size/auto.js @@ -0,0 +1,144 @@ +import { FOREGROUND, HIGHLIGHT } from 'auto-console-group' + +import { HEIGHT, MIN_SIZE } from '../../common/consts' +import { info } from '../console' +import state from '../values/state' + +const BOUNDING_FORMAT = [HIGHLIGHT, FOREGROUND, HIGHLIGHT] + +const prevScrollSize = { + height: 0, + width: 0, +} + +const prevBoundingSize = { + height: 0, + width: 0, +} + +function getBoundingClientRect(dimension, boundingSize, scrollSize) { + prevBoundingSize[dimension] = boundingSize + prevScrollSize[dimension] = scrollSize + return boundingSize +} + +function getOffset(getDimension) { + const offset = getDimension.getOffset() + if (offset !== 0) info(`Page offsetSize: %c${offset}px`, HIGHLIGHT) + return offset +} + +const getAdjustedScroll = (getDimension) => + getDimension.documentElementScroll() + Math.max(0, getDimension.getOffset()) + +export default function getAutoSize(getDimension) { + const { hasOverflow, hasTags, triggerLocked } = state + const dimension = getDimension.label + const isHeight = dimension === HEIGHT + const boundingSize = getDimension.boundingClientRect() + const ceilBoundingSize = Math.ceil(boundingSize) + const floorBoundingSize = Math.floor(boundingSize) + const scrollSize = getAdjustedScroll(getDimension) + const sizes = `HTML: %c${boundingSize}px %cPage: %c${scrollSize}px` + + let calculatedSize = MIN_SIZE + + switch (true) { + case !getDimension.enabled(): + return Math.max(scrollSize, MIN_SIZE) + + case hasTags: + info(`Found element with data-iframe-size attribute`) + calculatedSize = getDimension.taggedElement() + break + + case !hasOverflow && + state.firstRun && + prevBoundingSize[dimension] === 0 && + prevScrollSize[dimension] === 0: + info(`Initial page size values: ${sizes}`, ...BOUNDING_FORMAT) + calculatedSize = getBoundingClientRect( + dimension, + boundingSize, + scrollSize, + ) + break + + case triggerLocked && + boundingSize === prevBoundingSize[dimension] && + scrollSize === prevScrollSize[dimension]: + info(`Size unchanged: ${sizes}`, ...BOUNDING_FORMAT) + calculatedSize = Math.max(boundingSize, scrollSize) + break + + case boundingSize === 0 && scrollSize !== 0: + info(`Page is hidden: ${sizes}`, ...BOUNDING_FORMAT) + calculatedSize = scrollSize + break + + case !hasOverflow && + boundingSize !== prevBoundingSize[dimension] && + scrollSize <= prevScrollSize[dimension]: + info(`New size: ${sizes} `, ...BOUNDING_FORMAT) + info( + `Previous size: %c${prevBoundingSize[dimension]}px`, + HIGHLIGHT, + ) + calculatedSize = getBoundingClientRect( + dimension, + boundingSize, + scrollSize, + ) + break + + case !isHeight: + calculatedSize = getDimension.taggedElement() + break + + case !hasOverflow && boundingSize < prevBoundingSize[dimension]: + info(` size decreased: ${sizes}`, ...BOUNDING_FORMAT) + calculatedSize = getBoundingClientRect( + dimension, + boundingSize, + scrollSize, + ) + break + + case scrollSize === floorBoundingSize || scrollSize === ceilBoundingSize: + info(` size equals page size: ${sizes}`, ...BOUNDING_FORMAT) + calculatedSize = getBoundingClientRect( + dimension, + boundingSize, + scrollSize, + ) + break + + case boundingSize > scrollSize: + info(`Page size < size: ${sizes}`, ...BOUNDING_FORMAT) + calculatedSize = getBoundingClientRect( + dimension, + boundingSize, + scrollSize, + ) + break + + case hasOverflow: + info(`Found elements possibly overflowing `) + calculatedSize = getDimension.taggedElement() + break + + default: + info(`Using size: ${sizes}`, ...BOUNDING_FORMAT) + calculatedSize = getBoundingClientRect( + dimension, + boundingSize, + scrollSize, + ) + } + + info(`Content ${dimension}: %c${calculatedSize}px`, HIGHLIGHT) + + calculatedSize += getOffset(getDimension) + + return Math.max(calculatedSize, MIN_SIZE) +} diff --git a/packages/child/size/body-offset.js b/packages/child/size/body-offset.js new file mode 100644 index 000000000..c4babb7ef --- /dev/null +++ b/packages/child/size/body-offset.js @@ -0,0 +1,13 @@ +import { BASE } from '../../common/consts' + +// getBodyOffset +export default () => { + const { body } = document + const style = getComputedStyle(body) + + return ( + body.offsetHeight + + parseInt(style.marginTop, BASE) + + parseInt(style.marginBottom, BASE) + ) +} diff --git a/packages/child/size/change-detected.js b/packages/child/size/change-detected.js new file mode 100644 index 000000000..5cb25558f --- /dev/null +++ b/packages/child/size/change-detected.js @@ -0,0 +1,13 @@ +import checkTolerance from '../check/tolerance' +import settings from '../values/settings' +import state from '../values/state' + +export default function isSizeChangeDetected(newHeight, newWidth) { + const { calculateHeight, calculateWidth } = settings + const { height, width } = state + + return ( + (calculateHeight && checkTolerance(height, newHeight)) || + (calculateWidth && checkTolerance(width, newWidth)) + ) +} diff --git a/packages/child/size/content.js b/packages/child/size/content.js new file mode 100644 index 000000000..12e7c1b70 --- /dev/null +++ b/packages/child/size/content.js @@ -0,0 +1,63 @@ +import { HIGHLIGHT } from 'auto-console-group' + +import { + ENABLE, + INIT, + MUTATION_OBSERVER, + NO_CHANGE, + OVERFLOW_OBSERVER, + RESIZE_OBSERVER, + SET_OFFSET_SIZE, + SIZE_CHANGE_DETECTED, + VISIBILITY_OBSERVER, +} from '../../common/consts' +import { info, log, purge } from '../console' +import settings from '../values/settings' +import state from '../values/state' +import isSizeChangeDetected from './change-detected' +import { getNewHeight, getNewWidth } from './get-new' + +export default function getContentSize( + triggerEvent, + triggerEventDesc, + customHeight, + customWidth, +) { + const { heightCalcMode, widthCalcMode } = settings + + const newHeight = customHeight ?? getNewHeight(heightCalcMode) + const newWidth = customWidth ?? getNewWidth(widthCalcMode) + + const updateEvent = isSizeChangeDetected(newHeight, newWidth) + ? SIZE_CHANGE_DETECTED + : triggerEvent + + log(`Resize event: %c${triggerEventDesc}`, HIGHLIGHT) + + switch (updateEvent) { + case INIT: + case ENABLE: + case SIZE_CHANGE_DETECTED: + state.height = newHeight + state.width = newWidth + // eslint-disable-next-line no-fallthrough + case SET_OFFSET_SIZE: + return state + + // the following case needs {} to prevent a compile error on Next.js + case OVERFLOW_OBSERVER: + case MUTATION_OBSERVER: + case RESIZE_OBSERVER: + case VISIBILITY_OBSERVER: { + log(NO_CHANGE) + purge() + break + } + + default: + purge() + info(NO_CHANGE) + } + + return null +} diff --git a/packages/child/size/custom.js b/packages/child/size/custom.js new file mode 100644 index 000000000..0d368bc6d --- /dev/null +++ b/packages/child/size/custom.js @@ -0,0 +1,28 @@ +import { FUNCTION, HEIGHT } from '../../common/consts' +import { advise } from '../console' +import { getHeight, getWidth } from './index' + +const CUSTOM = 'custom' + +const deprecated = ( + calcFunc, +) => `Deprecated Option(${calcFunc}CalculationMethod) + +The use of ${calcFunc}CalculationMethod as a function is deprecated and will be removed in a future version of iframe-resizer. Please use the new onBeforeResize event handler instead. + +See https://iframe-resizer.com/api/child for more details. +` + +export default function setupCustomCalcMethod(calcMode, calcFunc) { + if (typeof calcMode !== FUNCTION) return calcMode + + advise(deprecated(calcFunc)) + + if (calcFunc === HEIGHT) { + getHeight.custom = calcMode + } else { + getWidth.custom = calcMode + } + + return CUSTOM +} diff --git a/packages/child/size/get-height.js b/packages/child/size/get-height.js new file mode 100644 index 000000000..3d652e598 --- /dev/null +++ b/packages/child/size/get-height.js @@ -0,0 +1,35 @@ +import { HEIGHT, HEIGHT_EDGE } from '../../common/consts' +import { warn } from '../console' +import settings from '../values/settings' +import { getAllMeasurements } from './all' +import getAutoSize from './auto' +import getBodyOffset from './body-offset' +import getMaxElement from './max-element' + +const getHeight = { + label: HEIGHT, + enabled: () => settings.calculateHeight, + getOffset: () => settings.offsetHeight, + auto: () => getAutoSize(getHeight), + bodyOffset: getBodyOffset, + bodyScroll: () => document.body.scrollHeight, + offset: () => getHeight.bodyOffset(), // Backwards compatibility + documentElementOffset: () => document.documentElement.offsetHeight, + documentElementScroll: () => document.documentElement.scrollHeight, + boundingClientRect: () => + Math.max( + document.documentElement.getBoundingClientRect().bottom, + document.body.getBoundingClientRect().bottom, + ), + max: () => Math.max(...getAllMeasurements(getHeight)), + min: () => Math.min(...getAllMeasurements(getHeight)), + grow: () => getHeight.max(), + lowestElement: () => getMaxElement(HEIGHT_EDGE), + taggedElement: () => getMaxElement(HEIGHT_EDGE), + custom: () => { + warn('Custom height calculation function not defined') + return getHeight.auto() + }, +} + +export default getHeight diff --git a/packages/child/size/get-new.js b/packages/child/size/get-new.js new file mode 100644 index 000000000..2a1b64909 --- /dev/null +++ b/packages/child/size/get-new.js @@ -0,0 +1,38 @@ +import { MIN_SIZE } from '../../common/consts' +import settings from '../values/settings' +import getHeight from './get-height' +import getWidth from './get-width' + +function callOnBeforeResize(newSize) { + const returnedSize = settings.onBeforeResize(newSize) + + if (returnedSize === undefined) { + throw new TypeError( + 'No value returned from onBeforeResize(), expected a numeric value', + ) + } + + if (Number.isNaN(returnedSize)) + throw new TypeError( + `Invalid value returned from onBeforeResize(): ${returnedSize}, expected Number`, + ) + + if (returnedSize < MIN_SIZE) { + throw new RangeError( + `Out of range value returned from onBeforeResize(): ${returnedSize}, must be at least ${MIN_SIZE}`, + ) + } + + return returnedSize +} + +const createGetNewSize = (direction) => (mode) => { + const calculatedSize = direction[mode]() + + return direction.enabled() && settings.onBeforeResize !== undefined + ? callOnBeforeResize(calculatedSize) + : calculatedSize +} + +export const getNewHeight = createGetNewSize(getHeight) +export const getNewWidth = createGetNewSize(getWidth) diff --git a/packages/child/size/get-width.js b/packages/child/size/get-width.js new file mode 100644 index 000000000..72f715e35 --- /dev/null +++ b/packages/child/size/get-width.js @@ -0,0 +1,34 @@ +import { WIDTH, WIDTH_EDGE } from '../../common/consts' +import { warn } from '../console' +import settings from '../values/settings' +import { getAllMeasurements } from './all' +import getAutoSize from './auto' +import getMaxElement from './max-element' + +const getWidth = { + label: WIDTH, + enabled: () => settings.calculateWidth, + getOffset: () => settings.offsetWidth, + auto: () => getAutoSize(getWidth), + bodyScroll: () => document.body.scrollWidth, + bodyOffset: () => document.body.offsetWidth, + documentElementScroll: () => document.documentElement.scrollWidth, + documentElementOffset: () => document.documentElement.offsetWidth, + boundingClientRect: () => + Math.max( + document.documentElement.getBoundingClientRect().right, + document.body.getBoundingClientRect().right, + ), + max: () => Math.max(...getAllMeasurements(getWidth)), + min: () => Math.min(...getAllMeasurements(getWidth)), + rightMostElement: () => getMaxElement(WIDTH_EDGE), + scroll: () => + Math.max(getWidth.bodyScroll(), getWidth.documentElementScroll()), + taggedElement: () => getMaxElement(WIDTH_EDGE), + custom: () => { + warn('Custom width calculation function not defined') + return getWidth.auto() + }, +} + +export default getWidth diff --git a/packages/child/size/index.js b/packages/child/size/index.js new file mode 100644 index 000000000..f5ee4ca42 --- /dev/null +++ b/packages/child/size/index.js @@ -0,0 +1,2 @@ +export { default as getHeight } from './get-height' +export { default as getWidth } from './get-width' diff --git a/packages/child/size/max-element.js b/packages/child/size/max-element.js new file mode 100644 index 000000000..eea0cc4da --- /dev/null +++ b/packages/child/size/max-element.js @@ -0,0 +1,66 @@ +import { FOREGROUND, HIGHLIGHT } from 'auto-console-group' + +import { MIN_SIZE } from '../../common/consts' +import { capitalizeFirstLetter } from '../../common/utils' +import { info } from '../console' +import { PREF_END, PREF_START } from '../observers/perf' +import settings from '../values/settings' +import state from '../values/state' +import { getAllElements } from './all' + +function getSelectedElements() { + const { hasOverflow, hasTags, overflowedNodeSet, taggedElements } = state + + return hasTags + ? taggedElements + : hasOverflow + ? Array.from(overflowedNodeSet) + : getAllElements(document.documentElement) // Width resizing may need to check all elements +} + +function findMaxElement(targetElements, side) { + const marginSide = `margin-${side}` + + let elVal = MIN_SIZE + let maxEl = document.documentElement + let maxVal = state.hasTags + ? MIN_SIZE + : document.documentElement.getBoundingClientRect().bottom + + for (const element of targetElements) { + elVal = + element.getBoundingClientRect()[side] + + parseFloat(getComputedStyle(element).getPropertyValue(marginSide)) + + if (elVal > maxVal) { + maxVal = elVal + maxEl = element + } + } + return { maxEl, maxVal } +} + +export default function getMaxElement(side) { + performance.mark(PREF_START) + + const Side = capitalizeFirstLetter(side) + const { logging } = settings + const { hasTags } = state + + const targetElements = getSelectedElements() + const { maxEl, maxVal } = findMaxElement(targetElements, side) + + info(`${Side} position calculated from:`, maxEl) + info(`Checked %c${targetElements.length}%c elements`, HIGHLIGHT, FOREGROUND) + + performance.mark(PREF_END, { + detail: { + hasTags, + len: targetElements.length, + logging, + Side, + }, + }) + + return maxVal +} diff --git a/packages/child/utils/isolate.js b/packages/child/utils/isolate.js new file mode 100644 index 000000000..74ba9c1e1 --- /dev/null +++ b/packages/child/utils/isolate.js @@ -0,0 +1,17 @@ +import { advise, error } from '../console' +import settings from '../values/settings' + +export default function isolate(funcs) { + const { mode } = settings + funcs.forEach((func) => { + try { + func() + } catch (error_) { + if (mode < 0) throw error_ + advise( + `Error in setup function\niframe-resizer detected an error during setup.\n\nPlease report the following error message at https://github.com/davidjbradshaw/iframe-resizer/issues`, + ) + error(error_) + } + }) +} diff --git a/packages/child/utils/map-settings.js b/packages/child/utils/map-settings.js new file mode 100644 index 000000000..34c2f7316 --- /dev/null +++ b/packages/child/utils/map-settings.js @@ -0,0 +1,7 @@ +import settings from '../values/settings' + +export default function map2settings(data) { + for (const [key, value] of Object.entries(data)) { + if (value) settings[key] = value + } +} diff --git a/packages/child/values/settings.js b/packages/child/values/settings.js new file mode 100644 index 000000000..4fdbc3cb7 --- /dev/null +++ b/packages/child/values/settings.js @@ -0,0 +1,33 @@ +import { + HEIGHT_CALC_MODE_DEFAULT, + WIDTH_CALC_MODE_DEFAULT, +} from '../../common/consts' +import { warn } from '../console' + +export default { + bodyMargin: 0, // For V1 compatibility + calculateWidth: false, + logging: false, + autoResize: true, + bodyMarginStr: '', + heightCalcMode: HEIGHT_CALC_MODE_DEFAULT, + bodyBackground: '', + bodyPadding: '', + tolerance: 0, + inPageLinks: false, + widthCalcMode: WIDTH_CALC_MODE_DEFAULT, + mouseEvents: false, + offsetHeight: 0, + offsetWidth: 0, + calculateHeight: true, + mode: 0, + logExpand: false, + ignoreSelector: '', + sizeSelector: '', + targetOrigin: '*', + onBeforeResize: undefined, + onMessage: () => { + warn('onMessage function not defined') + }, + onReady: () => {}, +} diff --git a/packages/child/values/state.js b/packages/child/values/state.js new file mode 100644 index 000000000..5960198b6 --- /dev/null +++ b/packages/child/values/state.js @@ -0,0 +1,18 @@ +export default { + applySelectors: null, + firstRun: true, + hasTags: false, + hasOverflow: false, + isHidden: false, + initLock: true, + inPageLinks: {}, + origin: undefined, + overflowedNodeSet: new Set(), + sameOrigin: false, + taggedElements: [], + target: window?.parent, + triggerLocked: false, + win: window, + onPageInfo: null, + onParentInfo: null, +} diff --git a/packages/common/consts.js b/packages/common/consts.js index dded1e5af..918ed65cf 100644 --- a/packages/common/consts.js +++ b/packages/common/consts.js @@ -120,6 +120,11 @@ export const INIT_EVENTS = Object.freeze({ export const EXPAND = 'expanded' export const COLLAPSE = 'collapsed' +export const HEIGHT_CALC_MODE_DEFAULT = AUTO +export const WIDTH_CALC_MODE_DEFAULT = SCROLL + +export const EVENT_CANCEL_TIMER = 128 + export const LOG_OPTIONS = Object.freeze({ [EXPAND]: 1, [COLLAPSE]: 1, diff --git a/packages/common/mode.js b/packages/common/mode.js index 6814ef013..cf6c38d54 100644 --- a/packages/common/mode.js +++ b/packages/common/mode.js @@ -20,7 +20,7 @@ const l = (l) => { (l <= 'Z' ? 90 : 122) >= (l = l.codePointAt(0) + 19) ? l : l - 26, ), ), - x = ['spjluzl', 'rlf', 'clyzpvu'], + x = ['spjluzl', 'rlf', 'clyzpvu', 'rlf2'], y = [ 'Puchspk Spjluzl Rlf', 'Tpzzpun Spjluzl Rlf', @@ -44,7 +44,7 @@ export const getModeData = (l) => p(y[l]) export const getModeLabel = (l) => p(z[l]) export const getKey = (l) => p(x[l]) export default (y) => { - const z = y[p(x[0])] || y[p(x[1])] || y[p(x[2])] + const z = y[p(x[0])] || y[p(x[1])] || y[p(x[2])] || y[p(x[3])] if (!z) return -1 const u = z.split('-') let v = (function (y = '') { diff --git a/spec/childSpec.js b/spec/childSpec.js index 92faba194..c78b2fa56 100644 --- a/spec/childSpec.js +++ b/spec/childSpec.js @@ -287,7 +287,7 @@ define(['iframeResizerChild', 'jquery'], (mockMsgListener, $) => { setTimeout(() => { expect(console.warn).toHaveBeenCalledWith( - 'Unexpected message ([iFrameSizer]foo)', + 'Unexpected message ([iFrameSizer]foo), this is likely due to a newer version on iframe-resizer running on the parent page.', ) done() })