import { RunnerObject, Layout, TransformValue, LayoutMetadata } from 'types' import { LIST, responsivePositioningOptions } from '@adalo/constants' import { Dimensions, Platform } from 'react-native' import { IApp } from 'interfaces' import { getDeviceType, DEVICE_TYPE_MOBILE, DEVICE_TYPE_TABLET, DEVICE_TYPE_DESKTOP, } from './device' const { FIXED_ON_SCROLL } = responsivePositioningOptions enum Anchoring { LEFT_AND_RIGHT = 'LEFT_AND_RIGHT', CENTER = 'CENTER', } export const getScreenSize = () => { const responsiveWrapper = Platform.OS === 'web' ? (document.getElementById('responsive-preview-frame') as Element) : null const { width, height } = responsiveWrapper ? window.getComputedStyle(responsiveWrapper) : Dimensions.get('window') return { width: typeof width === 'string' ? parseFloat(width) : width, height: typeof height === 'string' ? parseFloat(height) : height, } } const getAdjustedLayoutForObject = (obj: any, deviceType: string) => { switch (obj.attributes?.libraryName) { case '@adalo/navigation': if ( (obj.attributes?.onDesktop === 'top' && deviceType === DEVICE_TYPE_DESKTOP) || (obj.attributes?.onTablet === 'top' && deviceType === DEVICE_TYPE_TABLET) || (obj.attributes?.onMobile === 'top' && deviceType === DEVICE_TYPE_MOBILE) ) { return { width: '100%', height: 66, } } return {} default: return {} } } export const getResponsiveObject = (obj: any, width?: number, app?: IApp) => { if (!obj) { return } const deviceType: string = getDeviceType(width, app) as string if (obj.attributes?.shared?.[deviceType] !== false) { return { ...obj, layout: { ...obj.layout, ...getAdjustedLayoutForObject(obj, deviceType), }, } } const deviceObj = obj[deviceType] const responsivity = deviceObj.responsivity || obj.responsivity const attributes = deviceObj.attributes ? { ...obj.attributes, ...deviceObj.attributes } : obj.attributes const layout = deviceObj.layout || obj.layout const buildLayoutMetadata = deviceObj.buildLayoutMetadata || obj.buildLayoutMetadata return { // unique ...obj, responsivity, attributes, layout: { ...layout, ...getAdjustedLayoutForObject(obj, deviceType), }, buildLayoutMetadata, } } export const getRenderObject = (object: RunnerObject): RunnerObject => { const top = object.layout.top ? 0 : undefined const bottom = object.layout.bottom ? 0 : undefined const newObject = { ...object, layout: { ...object.layout, marginTop: 0, marginLeft: 0, left: 0, right: 0, top, bottom, width: '100%', }, } return newObject } const getDerivedWidth = ( layout: Layout, parentLayoutMetadata: LayoutMetadata ) => { const calculatedWidth = () => { const { width, left, right } = layout if (typeof width === 'number') { return width } if (typeof width === 'string' && width.endsWith('%')) { if ( typeof parentLayoutMetadata !== 'undefined' && typeof parentLayoutMetadata.derivedWidth === 'number' ) { return parentLayoutMetadata.derivedWidth * (parseFloat(width) / 100) } return getScreenSize().width * (parseFloat(width) / 100) } if (typeof left === 'number' && typeof right === 'number') { if ( typeof parentLayoutMetadata !== 'undefined' && typeof parentLayoutMetadata.derivedWidth === 'number' ) { return parentLayoutMetadata.derivedWidth - right - left } return getScreenSize().width - right - left } return undefined } const derivedWidth = calculatedWidth() if ( parentLayoutMetadata && derivedWidth && parentLayoutMetadata.type === LIST && parentLayoutMetadata.columnCount ) { const parentListHasMasonry = parentLayoutMetadata.masonry ?? false const parentColumnCount = parentLayoutMetadata.columnCount const parentRowMargin = parentLayoutMetadata.rowMargin ?? 0 if (parentListHasMasonry === true) { return ( (derivedWidth - parentRowMargin * (parentColumnCount - 1)) / parentColumnCount ) } // for non-masonry lists, we need the full column width and apply our left/right modifications return derivedWidth / parentColumnCount } return derivedWidth } const getAnchoring = (object: RunnerObject): Anchoring | undefined => { const { left, right, width } = object.layout const isAnchorLeftAndRight = width === undefined && typeof right === 'number' && typeof left === 'number' const isAnchorCenter = typeof width === 'string' && width.endsWith('%') && typeof left === 'string' && left.endsWith('%') && typeof right === 'undefined' if (isAnchorLeftAndRight) { return Anchoring.LEFT_AND_RIGHT } if (isAnchorCenter) { return Anchoring.CENTER } return undefined } const getLeftRightMarginModifier = ( parentLayoutMetadata: LayoutMetadata, anchoring: Anchoring | undefined, derivedWidth: number | undefined ): { leftModifier: number | undefined rightModifier: number | undefined derivedWidth: number | undefined } => { let leftModifier let rightModifier if ( parentLayoutMetadata && derivedWidth && parentLayoutMetadata.type === LIST && parentLayoutMetadata.columnCount ) { const parentListHasMasonry = parentLayoutMetadata.masonry ?? false const parentColumnCount = parentLayoutMetadata.columnCount ?? 1 const parentRowMargin = parentLayoutMetadata.rowMargin ?? 0 if ( parentListHasMasonry === false && parentColumnCount > 1 && parentRowMargin !== 0 && typeof parentLayoutMetadata.index === 'number' && (anchoring === Anchoring.LEFT_AND_RIGHT || anchoring === Anchoring.CENTER) ) { const normalisedIndex = parentLayoutMetadata.index % parentLayoutMetadata.columnCount leftModifier = (normalisedIndex / parentColumnCount) * parentRowMargin rightModifier = ((parentColumnCount - normalisedIndex - 1) / parentColumnCount) * parentRowMargin // prettier-ignore derivedWidth -= leftModifier derivedWidth -= rightModifier } } return { leftModifier, rightModifier, derivedWidth, } } export const isPercentage = (a: number | string): a is string => typeof a === 'string' export const constrainedLayoutAdjustments = ( object: RunnerObject, layout: Layout, parentLayoutMetadata: LayoutMetadata, layoutMetadata: LayoutMetadata, translateY: number | string ): { layout: Layout layoutMetadata: LayoutMetadata } => { const { minwidth, maxwidth, minwidthLeft, minwidthMarginLeft, maxwidthLeft, maxwidthMarginLeft, } = object.buildLayoutMetadata ?? {} const supportsMinwidth = typeof minwidth === 'number' && typeof minwidthLeft === 'string' && typeof minwidthMarginLeft === 'number' && typeof layoutMetadata.derivedWidth === 'number' && layoutMetadata.derivedWidth < minwidth const supportsMaxwidth = typeof maxwidth === 'number' && typeof maxwidthLeft === 'string' && typeof maxwidthMarginLeft === 'number' && typeof layoutMetadata.derivedWidth === 'number' && layoutMetadata.derivedWidth > maxwidth && typeof parentLayoutMetadata !== 'undefined' && typeof parentLayoutMetadata.maxwidthConstrained === 'boolean' && parentLayoutMetadata.maxwidthConstrained === false if (supportsMinwidth) { const transform: TransformValue[] = [ { translateX: minwidthMarginLeft }, ...(isPercentage(translateY) ? [] : [{ translateY }]), ] return { layout: { ...layout, transform, left: minwidthLeft, right: undefined, width: minwidth, }, layoutMetadata: { ...layoutMetadata, minwidthConstrained: true, }, } } if (supportsMaxwidth) { const transform: TransformValue[] = [ { translateX: maxwidthMarginLeft }, ...(isPercentage(translateY) ? [] : [{ translateY }]), ] return { layout: { ...layout, transform, left: maxwidthLeft, right: undefined, width: maxwidth, }, layoutMetadata: { ...layoutMetadata, maxwidthConstrained: true, }, } } return { layout, layoutMetadata, } } export const getPositioningLayout = ( object: RunnerObject, newY: number, parentLayoutMetadata: LayoutMetadata ): { layout: Layout layoutMetadata: LayoutMetadata } => { const translateX = object.layout.marginLeft || 0 const translateY = object.layout.marginTop || 0 const { left, right, bottom, width, height, zIndex } = object.layout const transform: TransformValue[] = isPercentage(translateX) ? [] : [{ translateX }] let { top } = object.layout if (object.responsivity?.verticalPositioning !== FIXED_ON_SCROLL) { top = newY || object.layout.top } if (!isPercentage(translateY)) { transform.push({ translateY }) } const anchoring = getAnchoring(object) const { leftModifier, rightModifier, derivedWidth } = getLeftRightMarginModifier( parentLayoutMetadata, anchoring, getDerivedWidth(object.layout, parentLayoutMetadata) ) const layout: Layout = { position: 'absolute', top, transform, left, right, bottom, width, height, zIndex, } const layoutMetadata: LayoutMetadata = { derivedWidth, minwidthConstrained: false, maxwidthConstrained: false, type: object.type, columnCount: object.attributes.columnCount, rowMargin: object.attributes.rowMargin, masonry: object.attributes.masonry, } if (anchoring === Anchoring.LEFT_AND_RIGHT) { if ( typeof left === 'number' && typeof right === 'number' && typeof leftModifier === 'number' && typeof rightModifier === 'number' ) { layout.left = left + leftModifier layout.right = right + rightModifier } return constrainedLayoutAdjustments( object, layout, parentLayoutMetadata, layoutMetadata, translateY ) } if (anchoring === Anchoring.CENTER) { if ( typeof left === 'string' && typeof width === 'string' && typeof leftModifier === 'number' && typeof rightModifier === 'number' && typeof derivedWidth === 'number' ) { const leftModifierPercentage = (leftModifier / derivedWidth) * 100 const rightModifierPercentage = (rightModifier / derivedWidth) * 100 layout.left = `${parseFloat(left) + leftModifierPercentage}%` layout.width = `${parseFloat(width) - leftModifierPercentage - rightModifierPercentage}%` // prettier-ignore } return constrainedLayoutAdjustments( object, layout, parentLayoutMetadata, layoutMetadata, translateY ) } return { layout, layoutMetadata, } } export const getPushId = ( listItemId: string, childId: string, itemId: number, parentPushId: string ): string => { let pushId = childId // inside a direct non list parent (so that we use the list item push Id, // as the base and not the groups one, so elements don't go unecessarily huge, if (listItemId && !itemId) { const lastIndex = parentPushId.lastIndexOf(' -- ') const parentPushIdWithoutGroupId = parentPushId.substring(0, lastIndex) pushId = `${parentPushIdWithoutGroupId} -- ${childId}` // inside a direct list parent } else if (itemId) { pushId = `${parentPushId} - ${itemId} -- ${childId}` } return pushId }