diff --git a/apps/admin/app/jcr_root/apps/dx/admin/clientlibs/author/src/js/app.js b/apps/admin/app/jcr_root/apps/dx/admin/clientlibs/author/src/js/app.js index e50c30ed..c50caf3f 100755 --- a/apps/admin/app/jcr_root/apps/dx/admin/clientlibs/author/src/js/app.js +++ b/apps/admin/app/jcr_root/apps/dx/admin/clientlibs/author/src/js/app.js @@ -1,51 +1,8 @@ -window.dx = window.dx || {}; -window.dx.author = { watch: { functions: [] } }; +// Ignore for code coverage +/* istanbul ignore file */ -window.dx.author.watch.registerFunction = (func) => { - window.dx.author.watch.functions.push(func); -}; - -const TAG_SCRIPT = 'SCRIPT'; -const WATCH_CONFIG = { - childList: true, - subtree: true, -}; - -/** - * Watch for Author mutations and run functions when they meet node type criteria. - * - * @param {Array} apps The functions or classes to instantiate when a mutation occurs - * @param {DOMElment} parent The top level parent to start the observation - */ -const watch = (document) => { - const parentToWatch = document.querySelector('body'); - - const callback = (mutationsList) => { - mutationsList.forEach((mutation) => { - // Attempt to cut down on noise from all mutations. - // An AEM component mutation will have only one added node. No more. No less. - if (window.dx.author.watch.functions.length > 0 && mutation.addedNodes.length === 1) { - const addedNode = mutation.addedNodes[0]; - if (addedNode.nodeType === 1 && addedNode.tagName !== TAG_SCRIPT) { - // Loop through each function and instantiate - // it with the added node. - window.dx.author.watch.functions.forEach((app) => { - app(addedNode); - }); - } - } - }); - }; - - // Create an observer instance linked to the callback function - const observer = new MutationObserver(callback); - observer.observe(parentToWatch, WATCH_CONFIG); -}; - -const authorWatch = (document) => { - if (typeof CQ !== 'undefined' && document) { - watch(document); - } -}; +import { initAuthorVh } from './utils/authorVh'; +import authorWatch from './utils/authorWatch'; authorWatch(document); +initAuthorVh(document); diff --git a/apps/admin/app/jcr_root/apps/dx/admin/clientlibs/author/src/js/utils/__mocks__/vh.html b/apps/admin/app/jcr_root/apps/dx/admin/clientlibs/author/src/js/utils/__mocks__/vh.html new file mode 100644 index 00000000..9b12de8a --- /dev/null +++ b/apps/admin/app/jcr_root/apps/dx/admin/clientlibs/author/src/js/utils/__mocks__/vh.html @@ -0,0 +1,28 @@ + +
+ +
+ +
+ +
+ +
+ + + +
+ +
+ +
+
i1
+
i2
+
i3
+
i4
+
+ +
+ diff --git a/apps/admin/app/jcr_root/apps/dx/admin/clientlibs/author/src/js/utils/__tests__/authorVh.test.js b/apps/admin/app/jcr_root/apps/dx/admin/clientlibs/author/src/js/utils/__tests__/authorVh.test.js new file mode 100644 index 00000000..5e68d8b5 --- /dev/null +++ b/apps/admin/app/jcr_root/apps/dx/admin/clientlibs/author/src/js/utils/__tests__/authorVh.test.js @@ -0,0 +1,163 @@ +import vhHtml from '../__mocks__/vh.html'; +import { + initAuthorVh, + findAuthorVhElements, + getEditorHeight, + setVh, + getInitialWidth, + getVhAsPx, + getBreakpointVh, +} from '../authorVh'; + +// Define our innerWidth +Object.defineProperties(window, { + innerWidth: { value: 300, configurable: true, writable: true }, + innerHeight: { value: 720 }, +}); + +document.body.innerHTML = vhHtml; + +let elements; +let editorHeight; + +beforeAll(() => { + elements = findAuthorVhElements(document); +}); + +describe('count elements with VH', () => { + test('four total elements', () => { + expect(window.innerWidth).toEqual(300); + expect(elements).toHaveLength(8); + }); +}); + +describe('get editor height', () => { + test('height is height minus AEM toolbar', () => { + editorHeight = getEditorHeight(); + expect(editorHeight).toEqual(610); + }); +}); + +describe('setting VH', () => { + test('VH is correctly set on first element', () => { + const mobileElement = setVh(elements[0], 'mobile', editorHeight); + expect(mobileElement.style.minHeight).toEqual('122px'); + }); + test('bad data is not set on element', () => { + const badElement = document.querySelector('.bad-element'); + expect(badElement.style.minHeight).toBeFalsy(); + }); +}); + +describe('get initial width', () => { + test('width should be mobile', () => { + const initialWidth = getInitialWidth(); + expect(initialWidth).toEqual('mobile'); + }); + + test('width should be tablet', () => { + Object.defineProperties(window, { + innerWidth: { value: 768, configurable: true, writable: true }, + }); + const initialWidth = getInitialWidth(); + expect(initialWidth).toEqual('tablet'); + }); + test('width should be desktop', () => { + Object.defineProperties(window, { + innerWidth: { value: 1366, configurable: true, writable: true }, + }); + const initialWidth = getInitialWidth(); + expect(initialWidth).toEqual('desktop'); + }); +}); + +describe('get vh to px', () => { + test('vh should not convert to px', () => { + const pxValue = getVhAsPx(900, 'foo'); + expect(pxValue).toBeNull(); + }); + test('vh should convert to px', () => { + const pxValue = getVhAsPx(900, 66); + expect(pxValue).toEqual(594); + }); +}); + +describe('get breakpoint vh as px', () => { + test('breakpoint vh should be mobile', () => { + const el = elements[2]; + const pxValueMobile = getBreakpointVh('mobile', window.innerHeight, el.dataset); + expect(pxValueMobile.flexVh).toEqual(144); + }); + + test('breakpoint vh should be tablet', () => { + const el = elements[2]; + const pxValueMobile = getBreakpointVh('tablet', window.innerHeight, el.dataset); + expect(pxValueMobile.flexVh).toEqual(288); + }); + + test('breakpoint vh should be desktop', () => { + const el = elements[2]; + const pxValueMobile = getBreakpointVh('desktop', window.innerHeight, el.dataset); + expect(pxValueMobile.flexVh).toEqual(432); + }); +}); + +describe('breakpoint vh with items defined', () => { + test('mobile only', () => { + const el = elements[4]; + const { flexVh, itemVhs } = getBreakpointVh('mobile', window.innerHeight, el.dataset); + expect(flexVh).toEqual(144); + expect(itemVhs).toEqual([720]); + }); + + test('tablet override', () => { + const el = elements[5]; + const { flexVh, itemVhs } = getBreakpointVh('mobile', window.innerHeight, el.dataset); + expect(flexVh).toEqual(144); + expect(itemVhs).toEqual([144, null, 288]); + + const { flexVh: flexVh1, itemVhs: itemVhs1 } = getBreakpointVh( + 'tablet', + window.innerHeight, + el.dataset + ); + expect(flexVh1).toEqual(288); + expect(itemVhs1).toEqual([null, 108, 648]); + }); + + test('desktop override + item inheritance', () => { + const el = elements[6]; + + const item1 = el.querySelector('#i1'); + const item2 = el.querySelector('#i2'); + const item3 = el.querySelector('#i3'); + const item4 = el.querySelector('#i4'); + + setVh(el, 'mobile', window.innerHeight, el.dataset); + expect(item1.style.minHeight).toBe(''); + expect(item2.style.minHeight).toBe(''); + expect(item3.style.minHeight).toBe('712.8px'); + expect(item4.style.minHeight).toBe('216px'); + + setVh(el, 'tablet', window.innerHeight, el.dataset); + expect(item1.style.minHeight).toBe(''); + expect(item2.style.minHeight).toBe('108px'); + expect(item3.style.minHeight).toBe('712.8px'); + expect(item4.style.minHeight).toBe('288px'); + + setVh(el, 'desktop', window.innerHeight, el.dataset); + expect(item1.style.minHeight).toBe('72px'); + expect(item2.style.minHeight).toBe('108px'); + expect(item3.style.minHeight).toBe('712.8px'); + expect(item4.style.minHeight).toBe('288px'); + }); +}); + +describe('initAuthorVh', () => { + test('four total elements', () => { + const addListener = jest.fn(); + global.matchMedia = () => ({ addListener }); + initAuthorVh(document); + expect(addListener).toHaveBeenCalled(); + }); +}); diff --git a/apps/admin/app/jcr_root/apps/dx/admin/clientlibs/author/src/js/__tests__/authorWatch.test.js b/apps/admin/app/jcr_root/apps/dx/admin/clientlibs/author/src/js/utils/__tests__/authorWatch.test.js similarity index 97% rename from apps/admin/app/jcr_root/apps/dx/admin/clientlibs/author/src/js/__tests__/authorWatch.test.js rename to apps/admin/app/jcr_root/apps/dx/admin/clientlibs/author/src/js/utils/__tests__/authorWatch.test.js index 8eb5deff..1645f5a2 100644 --- a/apps/admin/app/jcr_root/apps/dx/admin/clientlibs/author/src/js/__tests__/authorWatch.test.js +++ b/apps/admin/app/jcr_root/apps/dx/admin/clientlibs/author/src/js/utils/__tests__/authorWatch.test.js @@ -35,7 +35,8 @@ window.MutationObserver = SimulateMutationObserver; window.CQ = {}; // use require as import is hoisted -require('../app'); +const authorWatch = require('../authorWatch.js').default; +authorWatch(document); describe('authorWatch', () => { beforeEach(() => { diff --git a/apps/admin/app/jcr_root/apps/dx/admin/clientlibs/author/src/js/utils/authorVh.js b/apps/admin/app/jcr_root/apps/dx/admin/clientlibs/author/src/js/utils/authorVh.js new file mode 100644 index 00000000..6c461c4f --- /dev/null +++ b/apps/admin/app/jcr_root/apps/dx/admin/clientlibs/author/src/js/utils/authorVh.js @@ -0,0 +1,225 @@ +/** + * Author VH + * A utility to set VH values as pixels in authoring mode (editor.html) + * This is needed because editor.html's iframe cannot handle VH. + * + * This utility supports: + * 1) Initial load of page + * 2) Changing the mobile emulator width + * 3) Saving dialog and adding new components (through AuthorWatch) + */ + +// Setup Size Names +const DESKTOP = 'desktop'; +const TABLET = 'tablet'; +const MOBILE = 'mobile'; + +// Setup Sizing +const MOBILE_MAX = 599; +const TABLET_MIN = 600; +const TABLET_MAX = 1199; +const DESKTOP_MIN = 1200; + +// Setup Media Queries +const MOBILE_QUERY = `(max-width: ${MOBILE_MAX}px)`; +const TABLET_QUERY = `(min-width: ${TABLET_MIN}px) and (max-width: ${TABLET_MAX}px)`; +const DESKTOP_QUERY = `(min-width: ${DESKTOP_MIN}px)`; + +const AUTHOR_VH_SELECTOR = '.has-AuthorVh'; +const FLEX_CLASS = 'flex'; +const FLEX_CONTAINER_SELECTOR = '.dexter-FlexContainer'; +const AEM_TOOLBAR_HEIGHT = 110; + +/** + * Get the equivalent px value for a vh value + * @param {number} viewHeight - The current window height in px + * @param {number} vhValue - the vh height to convert to px + * @returns {number} vhValue in px + */ +const getVhAsPx = (viewHeight, vhValue) => { + if (!(vhValue > 0)) return null; + const vhDecimal = vhValue / 100; + const pixelHeight = viewHeight * vhDecimal; + return pixelHeight; +}; + +const mergeArrays = (arr, mergeArr) => { + if (!arr || arr.length === 0) return mergeArr ? [...mergeArr] : []; + if (!mergeArr || mergeArr.length === 0) return arr ? [...arr] : []; + + let merged = [...arr].map((val, i) => { + if (!(val || val === 0) && mergeArr[i]) { + return mergeArr[i]; + } + return val; + }); + + if (merged.length < mergeArr.length) { + // add addtional mergeArr entries to the end of merged + merged = [...merged, ...mergeArr.slice(merged.length)]; + } + + return merged; +}; + +/** + * Get the equivalent px value for a vh value + * Note that this is a curried function requiring viewHeight first + * @param {number} viewHeight - The current window height in px + * @param {string]} vhs - A comma delimited string of vhs to convert to px + * @returns {array[number]} array of vhs in px + */ +const getVhPxArray = (mediaName, viewHeight, dataset) => { + // If any array slots are empty, need to inherit the values from smaller screen size if defined + const mobile = dataset.authorMobileItemsVh && dataset.authorMobileItemsVh.split(','); + const tablet = dataset.authorTabletItemsVh && dataset.authorTabletItemsVh.split(','); + const desktop = dataset.authorDesktopItemsVh && dataset.authorDesktopItemsVh.split(','); + + let vhValues = []; + switch (mediaName) { + case DESKTOP: + vhValues = mergeArrays(desktop, mergeArrays(tablet, mobile)); + break; + case TABLET: + vhValues = mergeArrays(tablet, mobile); + break; + default: + vhValues = mobile; + } + + return vhValues && vhValues.map((val) => getVhAsPx(viewHeight, val)); +}; + +const breakpointSwitch = (mediaName, viewHeight, dataset) => { + let vhValue; + switch (mediaName) { + case DESKTOP: + vhValue = + getVhAsPx(viewHeight, dataset.authorDesktopVh) || + getVhAsPx(viewHeight, dataset.authorTabletVh) || + getVhAsPx(viewHeight, dataset.authorMobileVh); + break; + case TABLET: + vhValue = + getVhAsPx(viewHeight, dataset.authorTabletVh) || + getVhAsPx(viewHeight, dataset.authorMobileVh); + break; + default: + vhValue = getVhAsPx(viewHeight, dataset.authorMobileVh); + } + return vhValue; +}; + +const getBreakpointVh = (mediaName, viewHeight, dataset) => ({ + flexVh: breakpointSwitch(mediaName, viewHeight, dataset), + itemVhs: getVhPxArray(mediaName, viewHeight, dataset), +}); + +const getInitialWidth = () => { + const { innerWidth } = window; + if (innerWidth >= DESKTOP_MIN) { + return DESKTOP; + } + if (innerWidth >= TABLET_MIN && innerWidth <= TABLET_MAX) { + return TABLET; + } + return MOBILE; +}; + +/** + * Get the parent window height because AEM's content editor + * iframe height can change often. + */ +const getEditorHeight = () => window.parent.innerHeight - AEM_TOOLBAR_HEIGHT; + +const findAuthorVhElements = (el) => { + if (el) { + const flexContainer = + el === document || el.classList.contains(FLEX_CLASS) + ? el + : el.closest(FLEX_CONTAINER_SELECTOR); + return flexContainer ? flexContainer.querySelectorAll(AUTHOR_VH_SELECTOR) : []; + } + return []; +}; + +/** + * Set the element VH + * @param {HTMLElement} element The element you want to change. + * @param {String} mediaName the media breakpoint name. + * @param {Number} viewHeight the view height. + */ +const setVh = (element, mediaName, viewHeight) => { + const { flexVh, itemVhs } = getBreakpointVh(mediaName, viewHeight, element.dataset); + + if (flexVh) { + element.style.minHeight = `${flexVh}px`; + } + + if (itemVhs && itemVhs.length) { + const children = [...element.children]; + children.forEach((el, i) => { + if (el.nodeName === 'CQ') return; + + if (el.classList.contains('newpar')) { + el.style.minHeight = '0'; + } else if (itemVhs[i]) { + el.style.minHeight = `${itemVhs[i]}px`; + } else { + el.style.minHeight = ''; + } + }); + } + return element; +}; + +const loopThroughVhElements = (elements, mediaName) => { + const viewHeight = getEditorHeight(); + elements.forEach((element) => { + setVh(element, mediaName, viewHeight); + }); +}; + +const setupMediaListeners = () => { + const mediaMatches = [ + { mediaName: MOBILE, mediaQuery: window.matchMedia(MOBILE_QUERY) }, + { mediaName: TABLET, mediaQuery: window.matchMedia(TABLET_QUERY) }, + { mediaName: DESKTOP, mediaQuery: window.matchMedia(DESKTOP_QUERY) }, + ]; + + mediaMatches.forEach((match) => { + match.mediaQuery.addListener((event) => { + if (event.matches) { + // Re-find any elments that may have been added + // since we initially setup the listener. + const elements = findAuthorVhElements(document); + loopThroughVhElements(elements, match.mediaName); + } + }); + }); +}; + +const initAuthorVh = (el) => { + const elements = findAuthorVhElements(el); + if (elements.length > 0) { + // Run on first load of page / element + loopThroughVhElements(elements, getInitialWidth()); + + // AuthorWatch may pass in a single component as the parent so only setup + // media query listeners if the passed in element is a document. + if (el === document) { + setupMediaListeners(); + } + } +}; + +export { + initAuthorVh, + findAuthorVhElements, + setVh, + getEditorHeight, + getInitialWidth, + getBreakpointVh, + getVhAsPx, + getVhPxArray, +}; diff --git a/apps/admin/app/jcr_root/apps/dx/admin/clientlibs/author/src/js/utils/authorWatch.js b/apps/admin/app/jcr_root/apps/dx/admin/clientlibs/author/src/js/utils/authorWatch.js new file mode 100644 index 00000000..1ef58491 --- /dev/null +++ b/apps/admin/app/jcr_root/apps/dx/admin/clientlibs/author/src/js/utils/authorWatch.js @@ -0,0 +1,51 @@ +window.dx = window.dx || {}; +window.dx.author = { watch: { functions: [] } }; + +window.dx.author.watch.registerFunction = (func) => { + window.dx.author.watch.functions.push(func); +}; + +const TAG_SCRIPT = 'SCRIPT'; +const WATCH_CONFIG = { + childList: true, + subtree: true, +}; + +/** + * Watch for Author mutations and run functions when they meet node type criteria. + * + * @param {Array} apps The functions or classes to instantiate when a mutation occurs + * @param {DOMElment} parent The top level parent to start the observation + */ +const watch = (document) => { + const parentToWatch = document.querySelector('body'); + + const callback = (mutationsList) => { + mutationsList.forEach((mutation) => { + // Attempt to cut down on noise from all mutations. + // An AEM component mutation will have only one added node. No more. No less. + if (window.dx.author.watch.functions.length > 0 && mutation.addedNodes.length === 1) { + const addedNode = mutation.addedNodes[0]; + if (addedNode.nodeType === 1 && addedNode.tagName !== TAG_SCRIPT) { + // Loop through each function and instantiate + // it with the added node. + window.dx.author.watch.functions.forEach((app) => { + app(addedNode); + }); + } + } + }); + }; + + // Create an observer instance linked to the callback function + const observer = new MutationObserver(callback); + observer.observe(parentToWatch, WATCH_CONFIG); +}; + +const authorWatch = (document) => { + if (typeof CQ !== 'undefined' && document) { + watch(document); + } +}; + +export default authorWatch;