{"version":3,"file":"index-03597b74.js","sources":["../../../src/state/actionConstants.js","../../../src/constants.js","../../../src/util/aactools/urlUtility.js","../../../src/util/dexf.ts","../../../src/state/translations/translationsSelectors.ts","../../../src/state/dexfSettings/dexfSettingsSelectors.ts","../../../src/settings/application.ts","../../../src/services/ServiceHandler.ts","../../../src/util/abTestUtility.ts","../../../src/services/statistics/insights/insightsReporter.js","../../../src/util/platform.js","../../../src/services/statistics/sentry/sentryMonitoring.js","../../../src/services/statistics/analytics.js","../../../src/services/statistics/utils/object.ts","../../../src/services/statistics/insights/custom/redux/reduxStatisticsEvents.ts","../../../src/services/statistics/insights/custom/redux/reduxStatisticsEventBuilder.js","../../../src/state/sentryRedux.ts","../../../src/state/createStore.js","../../../src/state/translations/translationsDefaultState.ts","../../../src/state/translations/translationsReducer.ts","../../../src/state/dexfSettings/dexfSettingsDefaultState.ts","../../../src/state/dexfSettings/dexfSettingsReducer.ts","../../../src/state/dialog/dialogDefaultState.ts","../../../src/state/dialog/dialogReducer.ts","../../../src/state/init/initDefaultState.ts","../../../src/state/init/initReducer.ts","../../../src/settings/masterConstants.js","../../../src/settings/settingsHelpers.js","../../../src/settings/rangeSettings/AURDAL.js","../../../src/settings/rangeSettings/BOAXEL.js","../../../src/settings/rangeSettings/BROR.js","../../../src/settings/rangeSettings/IVAR.js","../../../src/settings/rangeSettings/JONAXEL.js","../../../src/settings/rangeSettings/ELVARLI.js","../../../src/settings/rangeSettings/index.js","../../../src/settings/constants.js","../../../src/services/history/storage.js","../../../src/state/navigation/navigationDefaultState.ts","../../../src/state/navigation/navigationReducer.ts","../../../src/state/navigation/navigationSelectors.ts","../../../src/services/FixVPC.js","../../../src/translations.ts","../../../src/services/L10n.ts","../../../src/services/products/articles.js","../../../src/util/measures.js","../../../src/services/products/productHandler.js","../../../src/util/propFilter.js","../../../src/util/articleNo.js","../../../src/util/array.js","../../../src/util/round.js","../../../src/util/mergePolygons.js","../../../src/scene/util/geometry.js","../../../src/util/aactools/idGenerator.js","../../../src/state/tac/range/bror/index.js","../../../src/scene/jonaxel/ClothesRailConfig.js","../../../src/state/tac/range/jonaxel/index.js","../../../src/scene/boaxel/AdjustableConfig.js","../../../src/util/room.js","../../../src/state/tac/range/common.js","../../../src/state/tac/range/boaxel/mountingRail.js","../../../src/state/tac/tacReducer/makeSpaceForChild.js","../../../src/state/tac/tacReducer/sorter.js","../../../src/state/tac/replace.js","../../../src/state/rangeData/rangeDataSelectors.ts","../../../src/state/scene/sceneSelectors.ts","../../../src/state/scene/sceneDefaultState.ts","../../../src/state/scene/sceneReducer.ts","../../../src/state/scene/sceneActions.ts","../../../src/state/rangeData/rangeDataActions.ts","../../../src/state/productMenu/productMenuActions.ts","../../../src/util/supportedEvents.js","../../../src/state/userAgent/userAgentSelectors.ts","../../../src/state/popups/popupsSelectors.ts","../../../src/state/tac/tacSelectors.ts","../../../src/state/popups/popupsActions.ts","../../../src/state/tac/tacReducer/removeItem.js","../../../src/state/tac/tacReducer/updateItem.js","../../../src/state/popups/extendableConfItem.js","../../../src/components/Popup/alignments.js","../../../src/state/popups/popupsThunks.ts","../../../src/state/products/productsHelpers.ts","../../../src/state/products/productsSelectors.ts","../../../src/state/tac/tacReducer/tacSelectors.js","../../../src/services/swiper/common.js","../../../src/services/swiper/bror/index.js","../../../src/services/swiper/jonaxel/index.js","../../../src/services/swiper/boaxel/index.js","../../../src/services/swiper/aurdal/index.js","../../../src/services/swiper/ivar/index.jsx","../../../src/services/statistics/insights/custom/local/localStatisticsActions.ts","../../../src/services/statistics/insights/custom/local/localStatisticsEvents.ts","../../../src/services/statistics/insights/custom/local/localStatisticsReporter.ts","../../../src/util/deviceTypes.js","../../../src/util/screenSizes.js","../../../src/util/userAgent.js","../../../src/util/aactools/getDefaultPAC.js","../../../src/services/history/index.js","../../../src/util/events.js","../../../src/components/utils/StopPropagation.jsx","../../../src/state/dialog/dialogSelectors.ts","../../../src/state/init/initSelectors.ts","../../../src/components/Lightbox/DeeplinkChoice.tsx","../../../src/state/summary/summaryActions.ts","../../../src/state/init/initThunks.ts","../../../src/util/aactools/conversionMiddleware/middlewares/bulkArticles.js","../../../src/util/aactools/conversionMiddleware/ranges/BROR.js","../../../src/util/aactools/conversionMiddleware/index.js","../../../src/state/ymal/ymalSelectors.ts","../../../src/util/aactools/kompisConvert.ts","../../../src/services/statistics/insights/mandatory/mandatoryStatisticsActions.ts","../../../src/services/statistics/insights/mandatory/mandatoryStatisticsEvents.ts","../../../src/services/statistics/insights/mandatory/mandatoryStatisticsReporter.ts","../../../src/components/SeriesGallery.jsx","../../../src/components/Lightbox/PacMissing.tsx","../../../src/hooks/useKioskIntegration.ts","../../../src/components/Lightbox/IpexGalleryWarning.tsx","../../../src/services/toastMaster.ts","../../../src/services/amelioration/ameliorationTypes.ts","../../../src/services/amelioration/amelioration.ts","../../../src/components/Lightbox/AmeliorationDialog.tsx","../../../src/state/dialog/dialogVariations.ts","../../../src/state/dialog/dialogActions.ts","../../../src/state/vpc/vpcActions.ts","../../../src/state/tac/tacActions.js","../../../src/state/popups/popupsDefaultState.ts","../../../src/state/popups/popupsReducer.ts","../../../src/state/tac/tacReducer/getWallPoints.js","../../../src/state/tac/tacReducer/addMultipleItems.js","../../../src/state/tac/tacReducer/updateMultipleItems.js","../../../src/state/tac/tacReducer/updateDependentItems.js","../../../src/util/aactools/convert.js","../../../src/state/sprs/sprsSelectors.ts","../../../src/state/scene/sceneThunks.js","../../../src/state/tac/tacReducer/validate/elvarli/index.js","../../../src/services/amelioration/range/elvarli/elvarliStabilityAmelioration.ts","../../../src/state/tac/tacReducer/validate/validationErrors.ts","../../../src/state/tac/tacReducer/validate/aurdal/index.js","../../../src/state/tac/tacReducer/validate/boaxel/index.js","../../../src/state/tac/tacReducer/validate/bror/index.js","../../../src/state/tac/tacReducer/validate/jonaxel/index.js","../../../src/state/tac/tacReducer/validate/ivar/index.js","../../../src/state/tac/tacReducer/validate/meansOfNotification.ts","../../../src/state/tac/tacReducer/validate/index.js","../../../src/state/tac/tacReducer/hideItem.js","../../../src/state/draft/ceilingHeight/ceilingHeightActions.ts","../../../src/state/toasts/toastsSelectors.ts","../../../src/state/toasts/toastsActions.ts","../../../src/state/toasts/toastsThunks.ts","../../../src/state/tac/tacThunks.js","../../../src/state/draft/draftKeys.ts","../../../src/state/draft/draftSelectors.ts","../../../src/state/draft/ceilingHeight/ceilingHeightSelectors.ts","../../../src/state/draft/ceilingHeight/ceilingHeightThunks.ts","../../../src/util/useKeyboard.ts","../../../src/components/Input/LabeledUnitInput/LabeledUnitInput.tsx","../../../src/services/swiper/elvarli/index.ts","../../../src/services/util.ts","../../../src/services/swiper/index.ts","../../../src/state/rawData/rawDataSelectors.ts","../../../src/state/productMenu/productMenuSelectors.ts","../../../src/services/scene/elvarli/index.ts","../../../src/components/Sheets/SelectElvarliVariantSheet/SelectElvarliVariantSheet.tsx","../../../src/components/ProductMenu/ProductMenuSubFilter.tsx","../../../src/components/utils/ClassComponent.jsx","../../../src/emitter.js","../../../src/settings/events.js","../../../src/util/dom.js","../../../src/components/utils/Portal.js","../../../src/components/Transition/transitionStates.js","../../../src/components/Transition/TransitionTarget.jsx","../../../src/components/Popup/setPosition.js","../../../src/components/Outline/Outline.jsx","../../../src/state/screensaver/screensaverModes.js","../../../src/state/screensaver/screensaverSelectors.ts","../../../src/components/Popup/Popup.jsx","../../../src/components/Transition/Transition.jsx","../../../src/components/Popup/IntroPopup.jsx","../../../src/components/Popup/TooltipTarget.jsx","../../../src/components/Popup/Tooltip.jsx","../../../src/components/CircleOption/CircleOption.jsx","../../../src/state/productMenu/productMenuThunks.ts","../../../src/components/ProductMenu/ProductMenuColorFilter.tsx","../../../src/services/products/itemsFilters.ts","../../../src/services/products/elvarli/index.js","../../../src/state/tac/tacReducer/connectParts.js","../../../src/state/tac/tacReducer/addItem.js","../../../src/state/tac/range/boaxel/brackets.js","../../../src/state/tac/range/boaxel/dependentItems.js","../../../src/state/tac/range/boaxel/supersection.js","../../../src/state/tac/range/boaxel/index.js","../../../src/scene/aurdal/getClothesRailConfig.js","../../../src/scene/ivar/TableConfig.js","../../../src/scene/util/getItemConfig.js","../../../src/state/tac/range/aurdal/mountingRail.js","../../../src/state/tac/range/aurdal/dependentItems.js","../../../src/state/tac/range/aurdal/index.js","../../../src/state/tac/range/ivar/dependentItems.js","../../../src/state/tac/range/ivar/index.js","../../../src/state/tac/range/elvarli/dependentItems.js","../../../src/state/tac/range/elvarli/index.js","../../../src/state/tac/range/index.js","../../../src/state/tac/tacHelpers.js","../../../src/services/products/models.js","../../../src/services/products/bror/index.js","../../../src/services/products/jonaxel/index.js","../../../src/services/products/boaxel/index.js","../../../src/services/products/aurdal/index.js","../../../src/services/products/ivar/index.js","../../../src/services/products/index.js","../../../src/services/PacValidator.js","../../../src/hooks/useVpc.js","../../../src/icons/custom/CoatHangerIcon.ts","../../../src/icons/custom/CoverIcon.ts","../../../src/icons/custom/DoorsIcon.ts","../../../src/icons/custom/FramesIcon.ts","../../../src/icons/custom/HandPointIcon.ts","../../../src/icons/custom/InteriorsIcon.ts","../../../src/icons/custom/SectionsIcon.ts","../../../src/icons/custom/ShelvingUnitsIcon.ts","../../../src/icons/custom/TablesIcon.ts","../../../src/icons/custom/TrashcanIcon.ts","../../../src/icons/custom/UprightsIcon.ts","../../../src/icons/custom/WallMeasurementIcon.ts","../../../src/icons/custom/WheelsIcon.ts","../../../src/icons/custom/getCustomIcon.ts","../../../src/components/utils/kompis/getKompisIconData.ts","../../../src/components/IconButton.tsx","../../../src/state/vpc/vpcSelectors.ts","../../../src/components/OpenSeriesGalleryButton.tsx","../../../src/components/utils/seriesGallery.js","../../../src/util/locale.ts","../../../src/state/vpc/vpcTypes.ts","../../../src/state/summary/summarySelectors.ts","../../../src/components/TopBar/LanguageSelector.tsx","../../../src/views/StartView/StartView.tsx","../../../src/components/IconPill.tsx","../../../src/components/ActionButtons/Measurements/Measurements.tsx","../../../src/components/UndoRedo/UndoRedo.jsx","../../../src/components/ActionButtons/WallResizerIcon/WallMeasurementsLegend.tsx","../../../src/components/ActionButtons/WallResizerIcon/WallResizerIcon.tsx","../../../src/state/sheets/sheetActions.ts","../../../src/components/ProductList/ProductList.jsx","../../../src/components/Sheets/WhatsIncludedSheet.tsx","../../../src/state/sheets/sheetSelectors.ts","../../../src/util/plannerVersion.ts","../../../src/util/aactools/ICF.ts","../../../src/util/wait.ts","../../../src/state/stores/storesActions.js","../../../src/state/stores/storeThunk.js","../../../src/state/summary/summaryThunks.ts","../../../src/state/vpc/vpcThunks.ts","../../../src/components/Footer/Footer.tsx","../../../src/state/products/productsActions.ts","../../../src/services/products/productMeasurementsFormatter.js","../../../src/services/products/articleMeasurementsGatherer.js","../../../src/services/products/productMeasurementsGatherer.js","../../../src/state/products/productsThunks.ts","../../../src/state/rawData/rawDataActions.ts","../../../src/state/init/loadProductMenu.js","../../../src/components/TopBar/TopBar.tsx","../../../src/components/Icon.tsx","../../../src/components/ProductMenu/ProductMenuFilter.tsx","../../../src/components/ProductMenu/ProductSwiperIndicator.jsx","../../../src/components/ProductMenu/ProductSwiperItem.jsx","../../../src/components/ProductMenu/SwiperSettings.js","../../../src/components/ProductMenu/MountingRailSwitch.jsx","../../../src/components/ProductMenu/ProductSwiper.tsx","../../../src/components/ProductMenu/ProductMenu.tsx","../../../src/components/ConfMenu/range/bror/index.js","../../../src/components/ConfMenu/range/jonaxel/index.js","../../../src/components/ConfMenu/range/boaxel/index.js","../../../src/components/ConfMenu/range/aurdal/index.js","../../../src/components/ConfMenu/range/ivar/index.js","../../../src/components/ConfMenu/range/elvarli/index.js","../../../src/components/ConfMenu/range/index.js","../../../src/components/ConfMenu/AbstractPicker.jsx","../../../src/components/ConfMenu/ConfColorPicker/ConfColorPicker.jsx","../../../src/scene/util/dimensionDisplay.js","../../../src/components/ConfMenu/ConfSizePicker/ConfSizePicker.jsx","../../../src/components/ConfMenu/ConfVariantPicker/ConfVariantPicker.jsx","../../../src/hooks/useHover.js","../../../src/components/Slider/Slider.jsx","../../../src/components/ConfMenu/ConfExtendableSlider/ConfExtendableSlider.jsx","../../../src/components/ConfMenu/ConfVariantToggler.jsx","../../../src/components/ConfMenu/AddonPicker/AddonPicker.jsx","../../../src/components/ConfMenu/generateOptions.jsx","../../../src/components/ConfMenu/getMenuItems.js","../../../src/components/Sheets/AdditionalInfoSheet.tsx","../../../src/state/sheets/sheetThunks.ts","../../../src/components/Sheets/CombinedProductsSheet.tsx","../../../src/components/ConfMenu/ConfMenu.tsx","../../../src/components/Modal/Modal.jsx","../../../src/components/ScenePopupManager.jsx","../../../src/scene/util/snapPadding.ts","../../../src/util/offset.js","../../../src/scene/util/draw.js","../../../src/scene/util/project.js","../../../src/scene/Base.js","../../../src/scene/InnerDropArea.js","../../../src/services/propping/bror/index.js","../../../src/scene/ItemContainer.js","../../../src/services/propping/common.js","../../../src/services/propping/jonaxel/index.js","../../../src/services/propping/boaxel/index.js","../../../src/services/propping/aurdal/index.js","../../../src/services/propping/ivar/index.js","../../../src/services/propping/elvarli/index.js","../../../src/services/propping/index.js","../../../src/scene/util/getDepthOffset.js","../../../src/scene/Item.js","../../../src/scene/boaxel/bracketsMixin.js","../../../src/scene/boaxel/BoaxelItem.js","../../../src/scene/OuterDropArea.js","../../../src/scene/bror/Section.js","../../../src/scene/DynamicProppingAncestor.js","../../../src/scene/boaxel/Upright.js","../../../src/scene/boaxel/Section.js","../../../src/scene/aurdal/Section.js","../../../src/scene/ivar/Section.js","../../../src/scene/elvarli/Section.js","../../../src/scene/util/adjustableContainer.js","../../../src/scene/Shelf.js","../../../src/scene/jonaxel/Legs.js","../../../src/scene/jonaxel/Frame.js","../../../src/scene/jonaxel/TopShelf.js","../../../src/scene/jonaxel/ShelvingUnit.js","../../../src/scene/ProppingItem.js","../../../src/scene/MultiParentClothesRail.js","../../../src/scene/jonaxel/ClothesRail.js","../../../src/scene/boaxel/ClothesRail.js","../../../src/scene/aurdal/ClothesRail.js","../../../src/scene/jonaxel/Cover.js","../../../src/scene/boaxel/MountingRail.js","../../../src/scene/aurdal/MountingRail.js","../../../src/scene/boaxel/BoaxelProppingItem.js","../../../src/scene/boaxel/AdjustableSection.ts","../../../src/scene/aurdal/Sidewall.js","../../../src/scene/boaxel/BoaxelShelf.js","../../../src/scene/boaxel/Table.js","../../../src/scene/ivar/IvarShelf.js","../../../src/scene/ivar/Drawer.js","../../../src/scene/ivar/Doors.js","../../../src/scene/elvarli/bracketsMixin.js","../../../src/scene/elvarli/ElvarliShelf.js","../../../src/scene/elvarli/ElvarliDrawer.js","../../../src/scene/elvarli/ElvarliShelfDrawer.js","../../../src/scene/elvarli/ElvarliProppingItem.js","../../../src/scene/elvarli/ElvarliShelfClothesRail.js","../../../src/scene/elvarli/ElvarliClothesRail.js","../../../src/scene/util/getComponent.js","../../../src/scene/Rect.js","../../../src/scene/Room.js","../../../src/scene/DynamicRoom.js","../../../src/scene/boaxel/DraggableWallResizer.js","../../../src/scene/boaxel/DragDot.js","../../../src/scene/boaxel/DragLine.js","../../../src/scene/FixedRoom.js","../../../src/scene/MeasurementLine.js","../../../src/scene/util/slowDevice.js","../../../src/scene/Measurements.js","../../../src/scene/MeasurementLayer.js","../../../src/scene/SceneBase.js","../../../src/scene/util/supersection.js","../../../src/scene/Scene.js","../../../src/components/ProductMenuCover/ProductMenuCover.tsx","../../../src/components/WallSizeInput/WallSizeInput.jsx","../../../src/util/dragging.js","../../../src/components/SupplyBanner/SupplyBanner.jsx","../../../src/components/JoyRide/JoyRide.tsx","../../../src/views/SceneView/SceneView.tsx","../../../src/state/stores/storesSelectors.js","../../../src/components/LoadingSpinner/LoadingSpinner.jsx","../../../src/views/SummaryView/Ymal.tsx","../../../src/components/GoodToKnowTab/GoodToKnowTab.tsx","../../../src/views/SummaryView/SummaryPage.tsx","../../../src/views/SummaryView/SummaryView.tsx","../../../src/views/StartView/init.ts","../../../src/services/scene/bror/index.ts","../../../src/services/scene/jonaxel/index.ts","../../../src/services/scene/boaxel/index.ts","../../../src/services/scene/aurdal/index.ts","../../../src/services/scene/ivar/index.ts","../../../src/services/scene/index.ts","../../../src/views/SceneView/init.ts","../../../src/views/SummaryView/init.ts","../../../src/settings/views/views.ts","../../../src/state/navigation/navigationThunks.ts","../../../src/state/sprs/sprsDefaultState.ts","../../../src/state/sprs/sprsReducer.ts","../../../src/state/tac/tacReducer/filterActions.js","../../../src/state/tac/tacReducer/index.js","../../../src/state/userAgent/userAgentDefaultState.ts","../../../src/state/userAgent/userAgentReducer.ts","../../../src/state/userAgent/userAgentActions.ts","../../../src/state/vpc/vpcDefaultState.ts","../../../src/state/vpc/vpcReducer.ts","../../../src/state/screensaver/screensaverDefaultState.ts","../../../src/state/screensaver/screensaverReducer.ts","../../../src/state/screensaver/screensaverActions.ts","../../../src/state/productMenu/productMenuDefaultState.ts","../../../src/state/productMenu/productMenuReducer.ts","../../../src/state/ymal/ymalDefaultState.ts","../../../src/state/ymal/ymalReducer.ts","../../../src/state/products/productsDefaultState.ts","../../../src/state/products/productsReducer.ts","../../../src/state/stores/storesReducer.js","../../../src/state/summary/summaryDefaultState.ts","../../../src/state/summary/summaryReducer.ts","../../../src/state/rawData/rawDataDefaultState.ts","../../../src/state/rawData/rawDataReducer.ts","../../../src/state/rangeData/rangeDataDefaultState.ts","../../../src/state/rangeData/rangeDataReducer.ts","../../../src/state/draft/reducerCreater.ts","../../../src/state/draft/ceilingHeight/ceilingHeightReducer.ts","../../../src/state/draft/ceilingHeight/ceilingHeightDefaultState.ts","../../../src/state/draft/filterActions.ts","../../../src/state/draft/draftReducer.ts","../../../src/state/sheets/sheetDefaultState.ts","../../../src/state/sheets/sheetReducer.ts","../../../src/state/toasts/toastsDefaultState.ts","../../../src/state/toasts/toastsReducer.ts","../../../src/state/reducer.ts","../../../src/state/index.js","../../../src/state/translations/translationsActions.ts","../../../src/state/init/loadTranslations.ts","../../../src/state/dexfSettings/dexfSettingsActions.ts","../../../src/state/init/loadDexfSettings.ts","../../../src/state/sprs/sprsActions.ts","../../../src/state/ymal/ymalActions.ts","../../../src/state/init/loadCmsData.js","../../../src/components/FakeData.jsx","../../../src/services/products/loadProducts.jsx","../../../src/services/products/loadImages.js","../../../src/components/ScreenSaver/timerUtils.js","../../../src/state/init/SetUpScreensaverTimers.ts","../../../src/state/ymal/ymalThunks.ts","../../../src/state/init/thunkSetupDerivedDefaultState.ts","../../../src/state/init/initActions.ts","../../../src/components/BodyAttributes.jsx","../../../src/components/Lightbox/LoadingError.jsx","../../../src/services/iframe.js","../../../src/state/userAgent/userAgentThunks.ts","../../../src/components/ResizeHandler/ResizeHandler.tsx","../../../src/hooks/useCountdown.ts","../../../src/components/ScreenSaver/useScreensaverTimer.ts","../../../src/components/ScreenSaver/useModeHandler.ts","../../../src/components/ScreenSaver/getVideoSrc.js","../../../src/components/ScreenSaver/closeComponents.js","../../../src/components/VideoPlayer.tsx","../../../src/components/ScreenSaver/SlideShow/SlideShow.tsx","../../../src/components/ScreenSaver/ScreenSaver.tsx","../../../src/components/Survey/Survey.tsx","../../../src/components/Sheets/index.ts","../../../src/components/Sheets/SheetLoader.tsx","../../../src/App.jsx","../../../src/main.jsx"],"sourcesContent":["export const DIALOG_CLOSE = 'DIALOG: CLOSE';\nexport const DIALOG_OPEN = 'DIALOG: OPEN';\n\nexport const INIT_DONE = 'INIT: DONE';\nexport const INIT_ERROR = 'INIT: ERROR';\n\nexport const TRANSLATIONS_SET = 'TRANSLATIONS: SET';\n\nexport const DEXF_SETTINGS_SET = 'DEXF_SETTINGS_SET';\n\nexport const SET_VIEW = 'NAVIGATION: SET_VIEW';\n\nexport const POPUPS_HIDE_INTRO_POPUPS = 'POPUPS: HIDE_INTRO_POPUPS';\nexport const POPUPS_HIDE_SCENE_ERRORS = 'POPUPS: HIDE_SCENE_ERRORS';\nexport const POPUPS_SHOW_SCENE_ERRORS = 'POPUPS: SHOW_SCENE_ERRORS';\nexport const POPUPS_CONF_OPEN = 'POPUPS: CONF_OPEN';\nexport const POPUPS_CONF_CLOSE = 'POPUPS: CONF_CLOSE';\nexport const POPUPS_CONF_CLEAR = 'POPUPS: CONF_CLEAR';\nexport const POPUPS_RESET_INTRO = 'POPUPS: RESET_INTRO';\nexport const POPUPS_SET_HAS_SHOWN_INTRO_POPUPS =\n 'POPUPS: SET_HAS_SHOWN_INTRO_POPUPS';\nexport const POPUPS_MOUNTING_RAIL_SHEET_OPEN =\n 'POPUPS: MOUNTING_RAIL_SHEET_OPEN';\nexport const POPUPS_SUPPLY_BANNER_CLOSE = 'POPUPS: SUPPLY_BANNER_CLOSE';\nexport const POPUPS_SET_HAS_SHOWN_FILTER_INTRO_POPUP =\n 'POPUPS_SET_HAS_SHOWN_FILTER_INTRO_POPUP';\nexport const POPUPS_SET_FILTER_INTRO_POPUP_VISIBLE =\n 'POPUPS_SET_FILTER_INTRO_POPUP_VISIBLE';\nexport const POPUPS_OVERLAY_ACTIVE = 'POPUPS_OVERLAY_ACTIVE';\nexport const POPUPS_SCROLL_EXCEPTION = 'POPUPS_SCROLL_EXCEPTION';\nexport const POPUPS_HANDLE_POPUPS_ON_ADD_ITEM =\n 'POPUPS_HANDLE_POPUPS_ON_ADD_ITEM';\nexport const POPUPS_HANDLE_POPUPS_ON_UPDATE_ITEM =\n 'POPUPS_HANDLE_POPUPS_ON_UPDATE_ITEM';\nexport const POPUPS_SET_INTRO_POPUPS_VISIBLE =\n 'POPUPS_SET_INTRO_POPUPS_VISIBLE';\nexport const POPUPS_SET_CUTTABLE_MOUNTING_RAIL_HINT_CHECK_PENDING =\n 'POPUPS_SET_CUTTABLE_MOUNTING_RAIL_HINT_CHECK_PENDING';\nexport const POPUPS_SET_SURVEY_VISIBLE = 'POPUPS_SET_SURVEY_VISIBLE';\nexport const POPUPS_SET_SURVEY_STATE = 'POPUPS_SET_SURVEY_STATE';\nexport const POPUPS_SET_ERROR_HANDLING_PENDING_TIMESTAMP =\n 'POPUPS_SET_ERROR_HANDLING_PENDING_TIMESTAMP';\n\nexport const SCENE_ITEM_PICKED_UP = 'SCENE: ITEM_PICKED_UP';\nexport const SCENE_HIDE_MEASUREMENTS = 'SCENE: HIDE_MEASUREMENTS';\nexport const SCENE_SET_ROOM = 'SCENE: SET_ROOM';\nexport const SCENE_SET_RECT = 'SCENE: SET_RECT';\nexport const SCENE_SET_MARGINS = 'SCENE: SET_MARGINS';\nexport const SCENE_SHOW_MEASUREMENTS = 'SCENE: SHOW_MEASUREMENTS';\nexport const SCENE_SET_PROPPING_VISIBILITY = 'SCENE: TOGGLE_PROPPING';\nexport const SCENE_TOGGLE_WALL_RESIZER = 'SCENE: TOGGLE_WALL_RESIZER';\nexport const SCENE_SET_WALL_RESIZER_ACTIVE = 'SCENE: SET_WALL_RESIZER_ACTIVE';\nexport const SCENE_SET_PRELOAD_STATE = 'SCENE_SET_PRELOAD_STATE';\nexport const SCENE_SHOW_SHEET = 'SCENE_SHOW_SHEET';\nexport const SCENE_HIDE_SHEET = 'SCENE_HIDE_SHEET';\nexport const SCENE_SET_WALL_RESIZER_INACTIVE =\n 'SCENE: SET_WALL_RESIZER_INACTIVE';\nexport const SCENE_SET_WALL_RESIZER_DRAGGING =\n 'SCENE: SET_WALL_RESIZER_DRAGGING';\n\nexport const SURVEY_SET_VISIBLE = 'SURVEY_SET_VISIBLE';\nexport const SURVEY_SET_STATE = 'SURVEY_SET_STATE';\n\nexport const VIEW_SET_VIEW_RECT = 'VIEW: SET_VIEW_RECT';\n\nexport const SCREENSAVER_MODE = 'SCREENSAVER: MODE';\n\nexport const FEATURE_FLAGS_SET = 'FEATURE_FLAGS: SET';\n\nexport const STORES_SET = 'STORES: SET';\n\nexport const SPRS_SET = 'SPRS: SET';\nexport const SPRS_SET_RIC = 'SPRS: SET_RIC';\n\nexport const PRODUCT_MENU_LOAD_FILTERS = 'PRODUCT_MENU: LOAD_FILTERS';\nexport const PRODUCT_MENU_SET_FILTER = 'PRODUCT_MENU: SET_FILTER';\nexport const PRODUCT_MENU_SET_SUB_FILTER = 'PRODUCT_MENU: SET_SUB_FILTER';\nexport const PRODUCT_MENU_SET_COLOR = 'PRODUCT_MENU: SET_COLOR';\nexport const PRODUCT_MENU_SET_SUB_FILTER_ITEMS =\n 'PRODUCT_MENU: SET_SUB_FILTER_ITEMS';\nexport const PRODUCT_MENU_CLEAR_FILTER = 'PRODUCT_MENU_CLEAR_FILTER';\nexport const PRODUCT_MENU_SET_SUB_FILTER_OPTIONS =\n 'PRODUCT_MENU_SET_SUB_FILTER_OPTIONS';\nexport const PRODUCT_MENU_SET_SELECTABLE_COLORS =\n 'PRODUCT_MENU_SET_SELECTABLE_COLORS';\n\nexport const TAC_ADD_ITEM = 'TAC: ADD_ITEM';\nexport const TAC_ADD_MULTIPLE = 'TAC: ADD_MULTIPLE';\nexport const TAC_LOAD = 'TAC: LOAD';\nexport const TAC_RESET = 'TAC_RESET';\nexport const TAC_REMOVE_ITEM = 'TAC: REMOVE_ITEM';\nexport const TAC_HIDE_ITEM = 'TAC: HIDE_ITEM';\nexport const TAC_UPDATE_ITEM = 'TAC: UPDATE_ITEM';\nexport const TAC_UPDATE_MULTIPLE = 'TAC: UPDATE_MULTIPLE';\nexport const TAC_SET_WALL = 'TAC: TAC_SET_WALL';\nexport const TAC_SET_USE_MOUNTING_RAIL = 'TAC: SET_USE_MOUNTING_RAIL';\nexport const TAC_REMOVE_TOAST_FLAGS = 'TAC: REMOVE_TOAST_FLAGS';\nexport const TAC_SET_TOAST_FLAGS = 'TAC: SET_TOAST_FLAGS';\nexport const TAC_UPDATE_WALL_SIZE = 'TAC: UPDATE_WALL_SIZE';\n\nexport const USER_AGENT_UPDATE = 'USER_AGENT_UPDATE';\nexport const USER_AGENT_UPDATE_HEIGHT = 'USER_AGENT_UPDATE_HEIGHT';\nexport const USER_AGENT_SHOULD_UPDATE_HEIGHT =\n 'USER_AGENT_SHOULD_UPDATE_HEIGHT';\n\nexport const VPC_LOADED = 'VPC: VPC_LOADED';\nexport const VPC_ERROR = 'VPC: ERROR';\nexport const VPC_CLEAR = 'VPC: CLEAR';\nexport const VPC_SET_SAVE_STATE = 'VPC_SET_SAVE_STATE';\nexport const VPC_SET_SAVE_CODE = 'VPC_SET_SAVE_CODE';\nexport const VPC_DIRTY_CONFIGURATION = 'VPC_DIRTY_CONFIGURATION';\nexport const VPC_SET_SAVE_PROGRESS_STATUS = 'VPC_SET_SAVE_PROGRESS_STATUS';\n\nexport const YMAL_SET = 'YMAL: SET';\nexport const YMAL_SET_DATA = 'YMAL_SET_DATA';\n\nexport const SUMMARY_SET_STORE_AVAILABILITIES =\n 'SUMMARY_SET_STORE_AVAILABILITIES';\nexport const SUMMARY_SET_STORE_ID = 'SUMMARY_SET_STORE_ID';\nexport const SUMMARY_SET_SHOPPING_ITEMS = 'SUMMARY_SET_SHOPPING_ITEMS';\nexport const SUMMARY_SET_VPC_CODE = 'SUMMARY_SET_VPC_CODE';\nexport const SUMMARY_SET_DESIGN_LINK = 'SUMMARY_SET_DESIGN_LINK';\nexport const SUMMARY_SET_CURRENT_CONFIGURATION =\n 'SUMMARY_SET_CURRENT_CONFIGURATION';\nexport const SUMMARY_SET_STATE = 'SUMMARY_SET_STATE';\nexport const SUMMARY_SET_SHARE_DESIGN_CARD = 'SUMMARY_SET_SHARE_DESIGN_CARD';\nexport const SUMMARY_SET_HOME_DELIVERY = 'SUMMARY_SET_HOME_DELIVERY';\nexport const SUMMARY_SET_FINANCING_OPTIONS = 'SUMMARY_SET_FINANCING_OPTIONS';\nexport const SUMMARY_SET_MODAL_TYPE = 'SUMMARY_SET_MODAL_TYPE';\nexport const SUMMARY_SET_SCENE_IMAGE = 'SUMMARY_SET_SCENE_IMAGE';\nexport const SUMMARY_ADD_UNAVAILABLE_PRODUCTS_TO_LIST =\n 'SUMMARY_ADD_UNAVAILABLE_PRODUCTS_TO_LIST';\nexport const SUMMARY_SET_CONFIRMATION_CARD_TYPE =\n 'SUMMARY_SET_CONFIRMATION_CARD_TYPE';\nexport const SUMMARY_SET_MODAL_STATE = 'SUMMARY_SET_MODAL_STATE';\nexport const SUMMARY_CLEAR_BUTTON = 'SUMMARY_CLEAR_BUTTON';\nexport const SUMMARY_SET_COPY_DESIGN_CODE_STATE =\n 'SUMMARY_SET_COPY_DESIGN_CODE_STATE';\nexport const SUMMARY_SET_COPY_DESIGN_LINK_STATE =\n 'SUMMARY_SET_COPY_DESIGN_LINK_STATE';\nexport const SUMMARY_SET_SHOULD_SEND_EVENT_STATE =\n 'SUMMARY_SET_SHOULD_SEND_EVENT_STATE';\nexport const SUMMARY_SET_FAILED_SHOPPING_ITEMS =\n 'SUMMARY_SET_FAILED_SHOPPING_ITEMS';\n\nexport const SET_RAW_DATA = 'SET_RAW_DATA';\n\nexport const SET_RANGE_DATA = 'SET_RANGE_DATA';\nexport const SET_RANGE_PRODUCT_MENU = 'SET_RANGE_PRODUCT_MENU';\n\nexport const SET_DRAFT_CEILING_HEIGHT = 'SET_DRAFT_CEILING_HEIGHT';\n\nexport const ENQUEUE_SHEET = 'ENQUEUE_SHEET';\nexport const DEQUEUE_SHEET = 'DEQUEUE_SHEET';\nexport const DEQUEUE_ALL_SHEETS = 'DEQUEUE_ALL_SHEETS';\n\nexport const TOASTS_SET_HAS_SHOWN_MULTIPACK_MESSAGE_FOR =\n 'TOASTS_SET_HAS_SHOWN_MULTIPACK_MESSAGE_FOR';\n","export const ITEMS = {\n DOOR: 'door',\n SHELF_DRAWER: 'shelf-drawer',\n DRAWER: 'drawer',\n SECTION: 'section',\n SECTION_SIDE_UNITS: 'section-side-units',\n SECTION_POSTS: 'section-posts',\n CABINET: 'cabinet',\n CLOTHES_RAIL: 'clothes-rail',\n SHELF: 'shelf',\n METAL_SHELF: 'metal-shelf',\n WIRE_SHELF: 'wire-shelf',\n FELT_SHELF: 'felt-shelf',\n BASKET: 'basket',\n UPRIGHT: 'upright',\n SHOE_SHELF: 'shoe-shelf',\n DRYING_RACK: 'drying-rack',\n TROUSER_HANGER: 'trouser-hanger',\n POST: 'post',\n PEGBOARD: 'pegboard',\n ADD_ON: 'add-on',\n DIVIDER: 'divider',\n WORKBENCH: 'workbench',\n TROLLEY: 'trolley',\n BOTTLE_RACK: 'bottle-rack',\n ITEM: 'item',\n CHEST: 'chest',\n TABLE: 'table',\n FRAME: 'frame',\n SHELVING_UNIT: 'shelving-unit',\n TOP_SHELF: 'top-shelf',\n BOX: 'box',\n BRACKET: 'bracket',\n MOUNTING_RAIL: 'mounting-rail',\n LEG: 'leg',\n COVER: 'cover',\n SIDEWALL: 'sidewall',\n DUMMY: 'dummy',\n SIDE_PANEL: 'side-panel',\n CROSS_BRACE: 'cross-brace',\n END_SHELF: 'end-shelf',\n SHELF_CLOTHES_RAIL: 'shelf-clothes-rail',\n};\n\nexport const FILTERS = {\n SECTIONS: 'sections',\n INSERTS: 'inserts',\n PARTS: 'parts',\n UPRIGHTS: 'uprights',\n SHELVES_BASKETS: 'shelves-baskets',\n ACCESSORIES: 'accessories',\n TABLES: 'tables',\n MODULES: 'modules',\n FRAMES: 'frames',\n SHELVING_UNITS: 'shelving_units',\n};\n\nexport const MISSING_PRODUCT_CATEGORIES = {\n SECTION: 'section',\n MOUNTING_RAIL_SHORT: 'mounting_rail_short',\n MOUNTING_RAIL_LONG: 'mounting_rail_long',\n SIDE_PANEL: 'side_panel',\n END_SHELF: 'end_shelf',\n MOUNTING_RAILS_SIDE_PANEL_END_SHELF_COLOR_MISMATCH:\n 'mounting_rails_side_panel_end_shelf_color_mismatch',\n UPRIGHT: 'upright',\n BRACKET: 'bracket',\n};\n\nexport const ART = 'ART';\n\nexport const RANGES = {\n BROR: 'BROR',\n IVAR: 'IVAR',\n JONAXEL: 'JONAXEL',\n BOAXEL: 'BOAXEL',\n AURDAL: 'AURDAL',\n ELVARLI: 'ELVARLI',\n};\n\nexport const DIMENSIONS = {\n width: 'width',\n depth: 'depth',\n height: 'height',\n};\n\nexport const UNITS = {\n mm: 'mm',\n cm: 'cm',\n dm: 'dm',\n m: 'm',\n};\n\nexport const UNIT_CONVERSIONS = {\n [UNITS.mm]: 1,\n [UNITS.cm]: 10,\n [UNITS.dm]: 100,\n [UNITS.m]: 1000,\n};\n\nexport const KEYS = {\n ENTER: 'Enter',\n BACKSPACE: 'backspace',\n};\n\nexport const MS = 'MEASURE_VALUE';\nexport const TS = 'TIMES_SYMBOL';\nexport const SI = 'SI';\n\nexport const SCENE_VIEW_ID = 'scene-view';\n\nexport const ADD = 'ADD';\nexport const REMOVE = 'REMOVE';\nexport const KIOSK = 'kiosk';\n","import { RANGES } from '../../constants';\n\n/**\n * This utility has been taken/borrowed out of the K1 url utility package because we\n * want to remove all of our dependencies to K1.\n */\nexport class UrlUtility {\n constructor() {\n this.DEFAULT_COUNTRY = 'se';\n this.DEFAULT_LANGUAGE = 'sv';\n this.DEFAULT_UI_PLATFORM = 'irw';\n }\n\n getAppInfo = () => {\n if (this._appInfo) {\n return this._appInfo;\n }\n\n const query = this.getCombinedQuery();\n\n let country = this.DEFAULT_COUNTRY;\n let language = this.DEFAULT_LANGUAGE;\n\n if (query.locale) {\n const locale = query.locale.split('_');\n\n if (\n locale.length > 1 &&\n locale[0].length === 2 &&\n locale[1].length === 2\n ) {\n country = locale[1].toLowerCase();\n language = locale[0].toLowerCase();\n }\n }\n\n this._appInfo = {\n country: country,\n language: language,\n uiPlatform: query.uiPlatform || this.DEFAULT_UI_PLATFORM,\n storeId: query.storeId || null,\n applicationName: this.getApplicationName() || null,\n seriesGallery: {\n applications: query.applications ? query.applications.split(',') : [],\n defaultApplication: query.defaultApplication || null,\n },\n };\n return this._appInfo;\n };\n\n getCombinedQuery = () => {\n return Object.assign(this.getSearchQuery(), this.getHashQuery());\n };\n\n getApplicationName = () => {\n const defaultApp = RANGES.BROR.toLowerCase();\n\n const match = document.location.href.match(\n /\\/(bror|jonaxel|boaxel|aurdal|ivar|elvarli)\\//i\n );\n return match ? match[1] : defaultApp;\n };\n\n getSearchQuery = () => {\n const search = window.location.search\n ? window.location.search.substring(1)\n : '';\n\n return this.parseQueryString(search);\n };\n\n getHashQuery = () => {\n const hash = window.location.hash\n ? window.location.hash.split('?').reverse()[0]\n : '';\n return this.parseQueryString(hash);\n };\n\n parseQueryString = queryString => {\n const variables = queryString.split('&');\n const query = {};\n\n const iterator = this.createForOfIteratorHelper(variables);\n let step;\n\n try {\n for (iterator.s(); !(step = iterator.n()).done; ) {\n const variable = step.value;\n\n if (variable) {\n const pair = variable.split('=');\n query[pair[0]] = pair[1];\n }\n }\n } catch (err) {\n iterator.e(err);\n } finally {\n iterator.f();\n }\n\n return query;\n };\n\n createForOfIteratorHelper = (o, allowArrayLike) => {\n var it;\n if (typeof Symbol === 'undefined' || o[Symbol.iterator] == null) {\n if (\n Array.isArray(o) ||\n (it = this.unsupportedIterableToArray(o)) ||\n (allowArrayLike && o && typeof o.length === 'number')\n ) {\n if (it) o = it;\n let i = 0;\n const F = function F() {};\n return {\n s: F,\n n: function n() {\n if (i >= o.length) return { done: true };\n return { done: false, value: o[i++] };\n },\n e: function e(e) {\n throw e;\n },\n f: F,\n };\n }\n throw new TypeError(\n 'Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.'\n );\n }\n let normalCompletion = true,\n didErr = false,\n err;\n return {\n s: function s() {\n it = o[Symbol.iterator]();\n },\n n: function n() {\n const step = it.next();\n normalCompletion = step.done;\n return step;\n },\n e: function e(e2) {\n didErr = true;\n err = e2;\n },\n f: function f() {\n try {\n if (!normalCompletion && it.return != null) it.return();\n } finally {\n if (didErr) throw err;\n }\n },\n };\n };\n\n unsupportedIterableToArray = (o, minLen) => {\n if (!o) return;\n if (typeof o === 'string') return this.arrayLikeToArray(o, minLen);\n let n = Object.prototype.toString.call(o).slice(8, -1);\n if (n === 'Object' && o.constructor) n = o.constructor.name;\n if (n === 'Map' || n === 'Set') return Array.from(o);\n if (n === 'Arguments' || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))\n return this.arrayLikeToArray(o, minLen);\n };\n}\n","import { RANGES } from '../constants';\nimport { applicationSettings } from '../settings/application';\n\nexport const dexfProdKeys = {\n aurdal: 'f0b44905-880e-4737-b252-f45dfe760ceb',\n boaxel: '9b8125dd-7d31-4a73-9a79-dcfd462bc749',\n bror: '7247c3db-9664-4fd1-a9eb-b4d90938a37f',\n elvarli: '411b3ce1-2f68-46e8-886f-4f08c63ecfcc',\n ivar: '285253d8-2ec3-4a18-84d7-42b1678607fe',\n jonaxel: '094b01b5-b441-4ed2-9142-1d84d47f8949',\n};\n\nexport const dexfQaKey = {\n shared: '829e294e-1e25-4f81-975e-ad53ac4a3682',\n};\n\nexport const getDexfApiKey = (range: string) => {\n if (!applicationSettings.isProd) return dexfQaKey.shared;\n\n switch (range) {\n case RANGES.AURDAL:\n return dexfProdKeys.aurdal;\n case RANGES.BOAXEL:\n return dexfProdKeys.boaxel;\n case RANGES.BROR:\n return dexfProdKeys.bror;\n case RANGES.ELVARLI:\n return dexfProdKeys.elvarli;\n case RANGES.IVAR:\n return dexfProdKeys.ivar;\n case RANGES.JONAXEL:\n return dexfProdKeys.jonaxel;\n default:\n throw new Error('Could not find dexf api key for range');\n }\n};\n","import type { State } from '../StateTypes';\nimport type { IKompisTranslations } from '@inter-ikea-kompis/types';\nimport type {\n IExtendedTranslations,\n IStorageTwoTranslations,\n} from './translationsTypes';\n\n/**\n * Select translations\n * @returns {IExtendedTranslations} The translations\n */\nexport const selectTranslations = (state: State): IExtendedTranslations =>\n state.translations;\n\n/**\n * Select Kompis translations\n * @returns {IKompisTranslations} The Kompis translations\n */\nexport const selectKompisTranslations = (state: State): IKompisTranslations =>\n selectTranslations(state).kompis;\n\n/**\n * Select StorageTwo translations\n * @returns {IStorageTwoTranslations} The StorageTwo translations\n */\nexport const selectStorageTwoTranslations = (\n state: State\n): IStorageTwoTranslations => selectTranslations(state).storagetwo;\n\n/**\n * Select translations flattened and including both Kompis and StorageTwo translations\n * @returns {IKompisTranslations & IStorageTwoTranslations} The flattened translations\n */\nexport const selectFlattenedTranslations = (\n state: State\n): IKompisTranslations & IStorageTwoTranslations => ({\n ...selectKompisTranslations(state),\n ...selectStorageTwoTranslations(state),\n});\n","import type { State } from '../StateTypes';\nimport type {\n IDexfSettingsKompis,\n IDexfSettingsLocalisation,\n} from '@inter-ikea-kompis/types';\nimport type {\n IExtendedDexfSettings,\n IDexfSettingsAppsettings,\n IDexfSettingsVpc,\n IDexfSettingsGallery,\n} from './dexfSettingsTypes';\n\n/**\n * Select Dexf settings\n *\n * @param state\n * @returns {IExtendedDexfSettings} The Dexf settings\n */\nexport const selectDexfSettings = (state: State): IExtendedDexfSettings =>\n state.dexfSettings;\n\n/**\n * Select Dexf settings have been loaded\n *\n * @param state\n * @returns {boolean} Whether the Dexf settings have been loaded\n */\nexport const selectDexfSettingsHaveBeenLoaded = (state: State): boolean =>\n Object.keys(state.dexfSettings).length > 0;\n\n/**\n * Select localisation Dexf settings\n *\n * @param state\n * @returns {IDexfSettingsLocalisation} The localisation Dexf settings\n */\nexport const selectLocalisationDexfSettings = (\n state: State\n): IDexfSettingsLocalisation => selectDexfSettings(state).localisation;\n\n/**\n * Select Kompis Dexf settings\n *\n * @param state\n * @returns {IDexfSettingsKompis} The Kompis Dexf settings\n */\nexport const selectKompisDexfSettings = (state: State): IDexfSettingsKompis =>\n selectDexfSettings(state).kompis;\n\n/**\n * Select appsettings Dexf settings\n *\n * @param state\n * @returns {IDexfSettingsAppsettings} The appsettings Dexf settings\n */\nexport const selectAppsettingsDexfSettings = (\n state: State\n): IDexfSettingsAppsettings => selectDexfSettings(state).appsettings;\n\n/**\n * Select VPC Dexf settings\n *\n * @param state\n * @returns {IDexfSettingsVpc} The VPC Dexf settings\n */\nexport const selectVpcDexfSettings = (state: State): IDexfSettingsVpc =>\n selectDexfSettings(state).vpc;\n\n/**\n * Select Gallery Dexf settings\n *\n * @param state\n * @returns {IDexfSettingsGallery} The Gallery Dexf settings\n */\nexport const selectGalleryDexfSettings = (state: State): IDexfSettingsGallery =>\n selectDexfSettings(state).gallery;\n\n/**\n * Select has tip over warning\n *\n * @param state\n * @returns {boolean}\n */\nexport const selectHasTipOverWarning = (state: State): boolean =>\n selectKompisDexfSettings(state).localization.hasTipOverWarning;\n\n/**\n * Select tip over warning URL\n *\n * @param state\n * @returns {string | undefined}\n */\nexport const selectTipOverWarningUrl = (state: State): string | undefined =>\n selectKompisDexfSettings(state).urls.tipOverWarningUrl;\n\n/**\n * Select use metric\n *\n * @param state\n * @returns {boolean}\n */\nexport const selectUseMetric = (state: State): boolean =>\n selectLocalisationDexfSettings(state).useMetricMeasures;\n\n/**\n * Select write direction\n *\n * @param state\n * @returns {string}\n */\nexport const selectWriteDirection = (state: State): string =>\n selectLocalisationDexfSettings(state).writeDirection;\n\n/**\n * Checks whether market uses right to left text\n *\n * @param state\n * @returns {boolean}\n */\nexport const selectIsRtl = (state: State): boolean =>\n selectWriteDirection(state) === 'rtl';\n\n/**\n * Checks whether the supply banner is enabled for the market\n *\n * @param state\n * @returns {boolean}\n */\nexport const selectIsSupplyBannedEnabled = (state: State): boolean =>\n selectAppsettingsDexfSettings(state).plannerBannerMessageEnabled;\n\n/**\n * Select message type for the supply banner\n *\n * @param state\n * @returns {string}\n */\nexport const selectSupplyBannerMessageType = (state: State): string =>\n selectAppsettingsDexfSettings(state).plannerBannerMessageType;\n\n/**\n * Select link URL for the supply banner\n *\n * @param state\n * @returns {string}\n */\nexport const selectSupplyBannerLinkURL = (state: State): string =>\n selectAppsettingsDexfSettings(state).plannerBannerMessageLinkUrl;\n\n/**\n * Select currency code of the market\n *\n * @param state\n * @returns {string}\n */\nexport const selectCurrencyCode = (state: State): string =>\n selectLocalisationDexfSettings(state).currencyCode;\n\n/**\n * Select experience gallery back URL\n *\n * @param state\n * @returns {string}\n */\nexport const selectExperienceGalleryBackUrl = (state: State): string =>\n selectGalleryDexfSettings(state).experienceGalleryBackURL;\n\n/**\n * Select CTE gallery back URL\n *\n * @param state\n * @returns {string}\n */\nexport const selectCteGalleryBackUrl = (state: State): string =>\n selectGalleryDexfSettings(state).CTE_GalleryBackURL;\n","/*global APP_VERSION */\nimport { DexfSettingsEnvironmentEnum } from '@inter-ikea-kompis/enums/';\nimport type {\n IShoppingCartServiceOptions,\n IShoppingListServiceOptions,\n} from '@inter-ikea-kompis/services';\nimport store from '../state';\nimport { getDexfApiKey } from '../util/dexf';\nimport { selectKompisTranslations } from '../state/translations/translationsSelectors';\nimport {\n selectDexfSettings,\n selectExperienceGalleryBackUrl,\n selectCteGalleryBackUrl,\n} from '../state/dexfSettings/dexfSettingsSelectors';\nimport { UrlUtility } from '../util/aactools/urlUtility';\n\nconst urlUtility = new UrlUtility();\nconst appInfo = urlUtility.getAppInfo();\n// @ts-ignore\nlet uiPlatformParam = urlUtility.getCombinedQuery().uiPlatform;\n\n(function setDocumentTitle() {\n document.title = urlUtility.getApplicationName().toUpperCase();\n})();\n\n// look at containing directory for dev builds\nif (!uiPlatformParam && import.meta.env.MODE !== 'production') {\n const match = window.location.pathname.match(\n /(kiosk|irw|m2|nwp|mobile)(?=\\/?)/\n );\n uiPlatformParam = match && match[0];\n}\n\nconst consumer = 'ALGOT';\nconst applicationName = urlUtility.getApplicationName().toUpperCase();\nconst applicationVersion = APP_VERSION;\nconst uiPlatform = uiPlatformParam || 'kiosk';\nconst storeId = appInfo.storeId;\nconst contract = '36400';\nconst locale = `${appInfo.language.toLowerCase()}-${appInfo.country.toUpperCase()}`;\nconst country = appInfo.country;\nconst dexfVpcConfigurationVersion = '1.1';\nconst surveyWaitTimeSeconds = 180;\nconst surveyWaitTimeSecondsKiosk = 120;\nconst surveyWaitTimeSecondsOnSucceed = 4;\nconst isProd = import.meta.env.MODE === 'production';\n\nexport const applicationSettings = {\n applicationName,\n applicationVersion,\n uiPlatform,\n storeId,\n locale,\n country,\n consumer,\n contract,\n dexfVpcConfigurationVersion,\n surveyWaitTimeSeconds,\n surveyWaitTimeSecondsKiosk,\n surveyWaitTimeSecondsOnSucceed,\n isProd,\n};\n\nexport const getIpexGalleryUrl = () => {\n const state = store.getState();\n\n return isProd\n ? selectExperienceGalleryBackUrl(state)\n : selectCteGalleryBackUrl(state);\n};\n\nexport const getFrequentlyUsedServiceSettingsOptions = () => ({\n dexfApiKey: getDexfApiKey(applicationName),\n dexfEnvironment: DexfSettingsEnvironmentEnum.production,\n dexfApplicationId: applicationName,\n locale: locale,\n settings: selectDexfSettings(store.getState()),\n});\n\nexport const translationSettings = {\n locale,\n translationPrefixApplication: applicationName,\n translationEndpoints: [\n '/addon-app/translations/kompis/en-XZ.json',\n '/addon-app/translations/kompis/{language}-{country}.json',\n '/addon-app/storagetwo/translations/en-XZ.json',\n '/addon-app/storagetwo/translations/{language}-{country}.json',\n ],\n};\n\nexport const getShoppingListServiceOptions =\n (): IShoppingListServiceOptions => {\n const { consumer, contract, locale } = applicationSettings;\n const settings = selectDexfSettings(store.getState());\n const translations = selectKompisTranslations(store.getState());\n\n return {\n consumer,\n contract,\n locale,\n settings,\n translations,\n };\n };\n\nexport const getShoppingCartServiceOptions =\n (): IShoppingCartServiceOptions => {\n const { consumer, contract, applicationName, locale } = applicationSettings;\n const settings = selectDexfSettings(store.getState());\n\n return {\n consumer,\n contract,\n applicationName,\n locale,\n settings,\n };\n };\n","import {\n CheckoutAvailabilityService,\n FinancingService,\n NotificationService,\n SettingsService,\n ShoppingCartService,\n ShoppingListService,\n TranslationsService,\n ZipValidationService,\n PlatformService,\n ProductService,\n CookieConsentService,\n StoreService,\n VpcService,\n} from '@inter-ikea-kompis/services';\nimport {\n getShoppingListServiceOptions,\n getShoppingCartServiceOptions,\n getFrequentlyUsedServiceSettingsOptions,\n translationSettings,\n applicationSettings,\n} from '../settings/application';\n\nconst serviceNames = {\n NotificationService: 'NotificationService',\n SettingsService: 'SettingsService',\n ZipValidationService: 'ZipValidationService',\n ShoppingListService: 'ShoppingListService',\n ShoppingCartService: 'ShoppingCartService',\n CheckoutAvailabilityService: 'CheckoutAvailabilityService',\n FinancingService: 'FinancingService',\n TranslationService: 'TranslationService',\n PlatformService: 'PlatformService',\n ProductService: 'ProductService',\n CookieConsentService: 'CookieConsentService',\n StoreService: 'StoreService',\n VpcService: 'VpcService',\n};\n\nconst services: { [key: string]: object } = {};\n\nconst serviceStarter = (serviceName: string, service: any, args: object) => {\n if (services[serviceName]) return services[serviceName];\n\n const newService = new service(args);\n services[serviceName] = new service(args);\n return newService;\n};\n\nexport const getDexfSettingsService = () => {\n const { dexfApiKey, dexfEnvironment, dexfApplicationId, locale } =\n getFrequentlyUsedServiceSettingsOptions();\n return serviceStarter(serviceNames.SettingsService, SettingsService, {\n dexfApiKey,\n dexfEnvironment,\n dexfApplicationId,\n locale,\n });\n};\n\nexport const getNotificationService = () => {\n return serviceStarter(\n serviceNames.NotificationService,\n NotificationService,\n getFrequentlyUsedServiceSettingsOptions()\n );\n};\n\nexport const getZipValidationService = () => {\n return serviceStarter(\n serviceNames.ZipValidationService,\n ZipValidationService,\n getFrequentlyUsedServiceSettingsOptions()\n );\n};\n\nexport const getShoppingListService = () => {\n return serviceStarter(\n serviceNames.ShoppingListService,\n ShoppingListService,\n getShoppingListServiceOptions()\n );\n};\n\nexport const getShoppingCartService = () => {\n return serviceStarter(\n serviceNames.ShoppingCartService,\n ShoppingCartService,\n getShoppingCartServiceOptions()\n );\n};\n\nexport const getCheckoutAvailabilityService = () => {\n return serviceStarter(\n serviceNames.CheckoutAvailabilityService,\n CheckoutAvailabilityService,\n getFrequentlyUsedServiceSettingsOptions()\n );\n};\n\nexport const getFinancingService = () => {\n return serviceStarter(serviceNames.FinancingService, FinancingService, {\n settings: getFrequentlyUsedServiceSettingsOptions().settings,\n });\n};\n\nexport const getTranslationsService = () => {\n return serviceStarter(\n serviceNames.TranslationService,\n TranslationsService,\n translationSettings\n );\n};\n\nexport const getPlatformService = () => {\n return serviceStarter(serviceNames.PlatformService, PlatformService, {\n applicationName: applicationSettings.applicationName,\n settings: getFrequentlyUsedServiceSettingsOptions().settings,\n });\n};\n\nexport const getProductService = () => {\n return serviceStarter(\n serviceNames.ProductService,\n ProductService,\n getFrequentlyUsedServiceSettingsOptions()\n );\n};\n\nexport const getCookieConsentService = () => {\n return serviceStarter(\n serviceNames.CookieConsentService,\n CookieConsentService,\n getFrequentlyUsedServiceSettingsOptions()\n );\n};\n\nexport const getStoreService = () => {\n return serviceStarter(\n serviceNames.StoreService,\n StoreService,\n getFrequentlyUsedServiceSettingsOptions()\n );\n};\n\nexport const getVpcService = () => {\n const { dexfApiKey, settings } = getFrequentlyUsedServiceSettingsOptions();\n const {\n applicationName,\n dexfVpcConfigurationVersion: dexfConfigurationVersion,\n } = applicationSettings;\n return serviceStarter(serviceNames.VpcService, VpcService, {\n dexfApiKey,\n applicationName,\n dexfConfigurationVersion,\n settings,\n });\n};\n","import { ABTestVariationEnum } from '@inter-ikea-kompis/enums';\nimport { applicationSettings } from '../settings/application';\nimport { getCookieConsentService } from '../services/ServiceHandler';\nimport { selectDexfSettings } from '../state/dexfSettings/dexfSettingsSelectors';\nimport store from '../state';\n\n/**\n * Initialize AB test from DEXF-settings\n *\n */\nexport const initializeDexfABTests = async () => {\n try {\n const consentLevel = 2;\n const analyticsCookieConsentService = getCookieConsentService();\n if (!(await analyticsCookieConsentService.hasCookieConsent(consentLevel)))\n return;\n\n try {\n const dexfSettings = selectDexfSettings(store.getState());\n if (!dexfSettings?.abtesting?.abTestingId) {\n return null;\n }\n\n const abTestId = dexfSettings.abtesting.abTestingId;\n handleAbVariation(abTestId);\n } catch (error) {\n // Handle error.\n console.log(error);\n }\n } catch (error) {\n console.warn('Cant fetch consent, assume no consent');\n }\n};\n\n/**\n * Returns AB test variation if it's in local storage.\n *\n * @param abTestId\n */\nexport const getLocalStorageAbVariation = (abTestId: string) => {\n const localStorageVariation = localStorage.getItem(abTestId);\n if (localStorageVariation) return localStorageVariation;\n return null;\n};\n\n/**\n * Check if AB test exist in local storage, else create new.\n *\n * @param abTestId\n */\nconst handleAbVariation = (abTestId: string) => {\n const plannerAbTestId = abTestId.concat(\n '_',\n applicationSettings.applicationName\n );\n\n const variationFound = getLocalStorageAbVariation(plannerAbTestId);\n if (variationFound) {\n return variationFound;\n }\n\n return setTestVariation(plannerAbTestId);\n};\n\n/**\n * Set AB test name with a random variation between A/B\n *\n * @param abTestId\n */\nconst setTestVariation = (plannerAbTestId: string) => {\n const variation =\n Math.random() < 0.5 ? ABTestVariationEnum.A : ABTestVariationEnum.B;\n\n localStorage.setItem(plannerAbTestId, variation);\n return variation;\n};\n","import { UrlUtility } from '../../../util/aactools/urlUtility';\nimport { applicationSettings } from '../../../settings/application';\nimport { getCookieConsentService } from '../../ServiceHandler';\nimport { InsightsApi, IpexMomentEnum } from '@insights/insights-data-provider';\nimport { AnalyticsManager } from '@inter-ikea-kompis/analytics';\nimport { getLocalStorageAbVariation } from '../../../util/abTestUtility';\n\n/**\n * This class is used to report analytics to the Insikt analytics component\n */\nexport default class InsightsReporter {\n constructor() {\n this.urlUtility = new UrlUtility();\n this.hasHadFirstInteraction = false;\n this.insightsApi = new InsightsApi();\n }\n async hasConsent() {\n try {\n const consentLevel = 2;\n if (applicationSettings.uiPlatform === 'kiosk') {\n return true;\n }\n const hasConsent = await getCookieConsentService().hasCookieConsent(\n consentLevel\n );\n return hasConsent;\n } catch (error) {\n console.warn('Cant fetch consent, assume no consent');\n return false;\n }\n }\n\n async connect() {\n if (this._isConnected || !(await this.hasConsent())) {\n return;\n }\n\n const appInfo = this.urlUtility.getAppInfo();\n const versionMatch = document.location.href.match(\n /\\/(\\d+\\.\\d+\\.\\d+\\.\\d+)\\//\n );\n const plannerVersion = versionMatch ? versionMatch[1] : '0.1';\n const applicationName = applicationSettings.applicationName;\n const SPR_ORDER = 'SPR_ORDER';\n const abVersionName = SPR_ORDER.concat('_', applicationName);\n const abVersionVariation = getLocalStorageAbVariation(abVersionName);\n\n await this.insightsApi.connectApplication({\n applicationId: applicationName.toLowerCase(),\n applicationVersion: plannerVersion,\n platform: 'owfe',\n countryCode: appInfo.country.toLowerCase(),\n languageCode: appInfo.language.toLowerCase(),\n storeId: appInfo.storeId,\n kiosk: applicationSettings.uiPlatform === 'kiosk',\n dev: !applicationSettings.isProd,\n isPlanner: true,\n enableGa: true,\n abVersionName: abVersionVariation ? abVersionName : undefined,\n abVersionVariation: abVersionVariation,\n });\n\n this._isConnected = true;\n AnalyticsManager.setInsightsApi(this.insightsApi);\n }\n\n /**\n * Sends event data to IPEX Insights.\n *\n * @param {string} ipexMoment IPEX moment string.\n * @param {string} eventString Event string.\n * @param {object} payload Optional payload.\n */\n async reportCustomEvent(event) {\n if (!(await this.canTransmit())) {\n return;\n }\n const isNonInteractionEvent =\n event.ipexMoment === IpexMomentEnum.nonInteraction;\n if (!this.hasHadFirstInteraction && !isNonInteractionEvent) {\n event.payload.firstInteraction = true;\n this.hasHadFirstInteraction = true;\n }\n this.insightsApi.sendEvent(event);\n }\n\n getInsightsApi() {\n return this.insightsApi;\n }\n\n /**\n * Function to set the AB Testing params by name and variation\n *\n * @param {String} abVersionName AB testing name param\n * @param {String} abVersionVariation AB testing variation param\n */\n async setAbVersion(abVersionName, abVersionVariation) {\n if (!(await this.canTransmit())) {\n return;\n }\n\n this.insightsApi.setAbVersion(abVersionName, abVersionVariation);\n }\n\n async canTransmit() {\n if (this._isConnected && !(await this.hasConsent())) {\n this.insightsApi.disconnect();\n this._isConnected = false;\n }\n\n return this._isConnected;\n }\n}\n","import { applicationSettings } from '../settings/application';\nexport default {\n isKiosk: applicationSettings.uiPlatform === 'kiosk',\n};\n","import * as Sentry from '@sentry/react';\nimport { applicationSettings } from '../../../settings/application';\nimport platform from '../../../util/platform';\n\nfunction getLongTermId() {\n const longTermId = localStorage.getItem('long_term_id');\n\n if (!longTermId) {\n return null;\n }\n const { id } = JSON.parse(longTermId);\n return id;\n}\n\nfunction create_UUID() {\n var dt = new Date().getTime();\n var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(\n /[xy]/g,\n function (c) {\n var r = (dt + Math.random() * 16) % 16 | 0;\n dt = Math.floor(dt / 16);\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);\n }\n );\n return uuid;\n}\n\nfunction getCachedPac() {\n if (\n localStorage.getItem('cachedPac-' + applicationSettings.applicationName) !==\n null\n ) {\n return localStorage.getItem(\n 'cachedPac-' + applicationSettings.applicationName\n );\n }\n return null;\n}\n\nfunction getComputerId() {\n const url = window.location.href;\n const query = url.split('&');\n const computerIdQuery = query.find(string => string.includes('computerId='));\n\n if (!computerIdQuery) return;\n\n const computerId = computerIdQuery.split('=');\n return computerId[1];\n}\n\nfunction init() {\n const locale = applicationSettings.locale;\n // We aren't allowed to send Sentry data when application has a chinese locale\n if (locale === 'en-CN' || locale === 'zh-CN') {\n return false;\n }\n\n const isProduction = applicationSettings.isProd;\n Sentry.setUser({ id: getLongTermId() || create_UUID() });\n Sentry.setTag('application', applicationSettings.applicationName);\n if (getComputerId()) {\n Sentry.setTag('computerId', getComputerId());\n }\n Sentry.init({\n dsn: 'https://487b61e0c18344ed840ec085946bfa10@o514642.ingest.sentry.io/5944662',\n integrations: [\n Sentry.browserTracingIntegration(),\n Sentry.captureConsoleIntegration({ levels: ['error'] }),\n ],\n beforeSend: (event, hint) => {\n const cachedPac = getCachedPac();\n\n // This part is used to remove certain errors that occur on kiosk\n // Where they haven't stopped using the old gallery, even if it doesn't work anymore.\n if (platform.isKiosk) {\n if (\n event.message?.includes(\n 'Invalid prop `type` supplied to `KompisIconButton`'\n )\n )\n return null;\n const values = event.exception?.values;\n const url = event.request?.url;\n if (values && url) {\n const serviceExceptionError = values.find(error => {\n const value = error.value;\n if (value) {\n return (\n value.includes(\n 'Uncaught ServiceException: Server responded with 404'\n ) ||\n value.includes(\n 'Failed to get app localizations. \"ServiceSettings.applicationName\"'\n )\n );\n }\n return false;\n });\n const hascComputerId = url.includes('computerId=RETCA313-KW');\n\n if (serviceExceptionError && hascComputerId) {\n return null;\n }\n }\n }\n\n if (cachedPac !== null) {\n hint.attachments = [\n {\n filename: 'cachedLocalstorage.txt',\n data: cachedPac,\n },\n ];\n }\n return event;\n },\n\n ignoreErrors: [\n /Cannot tween a null target./, // This error sometimes appear after screensaver ends and it doesn't affect the user\n /ResizeObserver loop limit exceeded/, // This error can occur when changing iframe size but it doesn't affect the user\n /Skapa Web Component!/, // This error is not something we can solve and doesn't seem to affect the user\n /^TypeError: Failed to fetch$/, // This error is not something we can solve from our end, this seems to only affect the insights data we try to send\n /Failed to send \\w+ event/, // This error is not something we can solve from our end, this seems to only affect the insights data we try to send\n ],\n // Set tracesSampleRate to 1.0 to capture 100%\n // of transactions for performance monitoring.\n environment: isProduction ? 'production' : 'development',\n tracesSampleRate: isProduction ? 0.3 : 1.0,\n release: `${applicationSettings.applicationVersion}`,\n normalizeDepth: 10,\n });\n}\n\nexport default {\n init,\n};\n","import InsightsReporter from './insights/insightsReporter';\nimport sentryMonitoring from './sentry/sentryMonitoring';\n\nconst insiktReporter = new InsightsReporter();\n\nexport async function init() {\n try {\n await insiktReporter.connect();\n } catch (error) {\n console.warn(error);\n }\n\n try {\n await sentryMonitoring.init();\n } catch (error) {\n console.warn(error);\n }\n}\n\nexport const getInsiktReporter = () => {\n return insiktReporter;\n};\n","const removeNonInteractionKeyValuePair = (object: any) => {\n if (!object) return;\n return Object.keys(object).reduce((acc: any, key) => {\n return {\n ...acc,\n ...(key !== 'nonInteraction' && { [`${key}`]: object[key] }),\n };\n }, {});\n};\n\n/**\n * Checks if an object is empty.\n * @param object\n * @returns {boolean}\n */\n\nexport const objectEmpty = (object: Object) => {\n return (\n object &&\n Object.keys(object).length === 0 &&\n Object.getPrototypeOf(object) === Object.prototype\n );\n};\n\n/**\n * Removes all undefined key value pairs from an object.\n * @param object\n * @returns {object}\n */\n\nexport const removeUndefinedKeyValuePairs = (object: any): any => {\n if (!object) return;\n const objectWithoutUndefinedValues: any = {};\n Object.keys(object).forEach((key: string) => {\n const value: any = object[key];\n if (value !== undefined && value !== null && !objectEmpty(value))\n objectWithoutUndefinedValues[key] = value;\n });\n return objectWithoutUndefinedValues;\n};\n\n/**\n * Finds a value by key in an object.\n * @param object\n * @param keyToFind\n * @returns\n */\nexport const findValueByKeyInObject = (object: any, keyToFind: string): any => {\n if (!object) return;\n const objectWhereKeyResides = findObjectWhereKeyResides(object, keyToFind);\n if (!objectWhereKeyResides) return;\n return objectWhereKeyResides[keyToFind];\n};\n\n/**\n * Recursively searches an object hierarchy until it finds the object that contains the specific primitive value.\n * @param object\n * @param keyToFind\n * @returns\n */\nexport const findObjectWhereKeyResides = (\n object: any,\n keyToFind: string\n): any => {\n if (isObject(object)) {\n const keys = Object.keys(object);\n if (keys.includes(keyToFind)) {\n return object;\n }\n for (const key of keys) {\n const result = findObjectWhereKeyResides(object[key], keyToFind);\n if (result) return result;\n }\n return;\n } else if (Array.isArray(object)) {\n for (const entry of object) {\n findObjectWhereKeyResides(entry, keyToFind);\n }\n }\n return;\n};\n\n/**\n * Flattens an array with objects or an object with objects to a single-depth object hierarchy.\n * Used to flatten for example a payload that contains an array of item objects.\n * This is used because the insightsApi only accepts a single-depth object hierarchy with primitives.\n * @example [{itemId: '8080830'}, itemId: {itemId: '9093020'}] becomes {itemId_1: '8080830', itemId_2: '9093020'}\n * @param object\n * @param parent\n * @param result\n * @returns A single depth object hierarchy with primitives.\n */\n\nexport const flattenObject = (\n object: any,\n parent: string = '',\n result: any = {}\n) => {\n if (!object) return result;\n\n const isArray = (value: unknown) => {\n return Array.isArray(value);\n };\n\n const getReturn = (key: string): any => {\n const propName = parent ? `${parent}_${key}` : key;\n const value: any = object[key];\n\n if (isObject(value)) return flattenObject(value, propName, result);\n\n return {\n [propName]: isArray(value)\n ? value.map((item: any) => flattenObject(item, propName, result))\n : value,\n };\n };\n\n return Object.keys(object).reduce(\n (acc: any, curr: string) => ({ ...acc, ...getReturn(curr) }),\n result\n );\n};\n/**\n * Removes all undefined key value pairs.\n * @param object\n * @returns\n */\nexport const sanitizeObject = (object: any) => {\n if (!object) return;\n return removeNonInteractionKeyValuePair(removeUndefinedKeyValuePairs(object));\n};\n\n/**\n * Checks to see if the meta object contains a property nonInteraction which is true.\n * @param meta\n * @returns {boolean}\n */\nexport const isNonInteractionEvent = (object: any) => {\n return object?.nonInteraction;\n};\n\nconst isObject = (value: unknown) => {\n return typeof value === 'object' && !Array.isArray(value) && value !== null;\n};\n","import * as action from '../../../../../state/actionConstants';\nimport { ActionTypes } from 'redux-undo';\nimport { IpexMomentEnum } from '@insights/insights-data-provider';\nimport { getInsiktReporter } from '../../../analytics';\nimport {\n sanitizeObject,\n findValueByKeyInObject,\n isNonInteractionEvent,\n} from '../../../utils/object';\ninterface CustomReduxEvent {\n ipexMoment: IpexMomentEnum;\n event: string;\n payload?: any;\n keys?: Array | undefined;\n reportEvent: Function;\n}\n\ninterface CustomReduxEvents {\n [key: string]: CustomReduxEvent;\n}\n\n/**\n * Calls the insightsApi to report the custom event.\n * @param event\n * @param payload\n * @param meta\n */\nconst reportCustomReduxEvent = (\n actionType: string,\n payload: any,\n meta: any\n) => {\n const eventToReport = buildCustomReduxStatisticsEvent(\n actionType,\n payload,\n meta\n );\n getInsiktReporter().reportCustomEvent(eventToReport);\n};\n\nexport const customReduxEvents: CustomReduxEvents = {\n [action.SCENE_SHOW_MEASUREMENTS]: {\n ipexMoment: IpexMomentEnum.showMe,\n event: 'scene_show_measurements',\n payload: {},\n keys: ['nonInteraction'],\n reportEvent: reportCustomReduxEvent,\n },\n [action.SCENE_HIDE_MEASUREMENTS]: {\n ipexMoment: IpexMomentEnum.showMe,\n event: 'scene_hide_measurements',\n payload: {},\n keys: ['nonInteraction'],\n reportEvent: reportCustomReduxEvent,\n },\n [action.SCENE_SET_WALL_RESIZER_ACTIVE]: {\n ipexMoment: IpexMomentEnum.makeItMe,\n event: 'scene_set_wall_resizer_active',\n payload: {},\n keys: ['nonInteraction'],\n reportEvent: reportCustomReduxEvent,\n },\n [action.SCENE_SET_WALL_RESIZER_INACTIVE]: {\n ipexMoment: IpexMomentEnum.makeItMe,\n event: 'scene_set_wall_resizer_inactive',\n payload: {},\n keys: ['nonInteraction'],\n reportEvent: reportCustomReduxEvent,\n },\n [action.SCENE_ITEM_PICKED_UP]: {\n ipexMoment: IpexMomentEnum.makeItMe,\n event: 'scene_item_picked_up',\n payload: {},\n keys: ['item', 'xPosition', 'yPosition', 'zPosition'],\n reportEvent: reportCustomReduxEvent,\n },\n [action.TAC_SET_WALL]: {\n ipexMoment: IpexMomentEnum.makeItMe,\n event: 'tac_set_wall',\n payload: {},\n keys: ['origin', 'height', 'width'],\n reportEvent: reportCustomReduxEvent,\n },\n [action.TAC_SET_USE_MOUNTING_RAIL]: {\n ipexMoment: IpexMomentEnum.makeItMe,\n event: 'tac_set_use_mounting_rails',\n payload: {},\n keys: ['enableMountingRails'],\n reportEvent: reportCustomReduxEvent,\n },\n [action.PRODUCT_MENU_SET_FILTER]: {\n ipexMoment: IpexMomentEnum.whatIsMe,\n event: 'product_menu_set_filter',\n payload: {},\n keys: [\n 'filter',\n 'nonInteraction',\n 'subFilterColor',\n 'subFilterDepth',\n 'nonInteraction',\n ],\n reportEvent: reportCustomReduxEvent,\n },\n [action.TAC_ADD_ITEM]: {\n ipexMoment: IpexMomentEnum.makeItMe,\n event: 'tac_add_item',\n payload: {},\n keys: ['item', 'parent', 'connectsTo', 'origin'],\n reportEvent: reportCustomReduxEvent,\n },\n [action.TAC_REMOVE_ITEM]: {\n ipexMoment: IpexMomentEnum.makeItMe,\n event: 'tac_remove_item',\n payload: {},\n keys: ['item', 'optionType', 'origin'],\n reportEvent: reportCustomReduxEvent,\n },\n [action.TAC_UPDATE_ITEM]: {\n ipexMoment: IpexMomentEnum.makeItMe,\n event: 'tac_update_item',\n payload: {},\n keys: [\n 'item',\n 'parent',\n 'connectsTo',\n 'optionValue',\n 'optionType',\n 'origin',\n 'xPosition',\n 'yPosition',\n 'zPosition',\n ],\n reportEvent: reportCustomReduxEvent,\n },\n [action.SET_VIEW]: {\n ipexMoment: IpexMomentEnum.showMe,\n event: 'navigation_set_view',\n payload: {},\n keys: ['view', 'nonInteraction'],\n reportEvent: reportCustomReduxEvent,\n },\n [action.DIALOG_OPEN]: {\n ipexMoment: IpexMomentEnum.nonInteraction,\n event: 'dialog_open',\n payload: {},\n keys: ['origin', 'dialogType'],\n reportEvent: reportCustomReduxEvent,\n },\n [ActionTypes.UNDO]: {\n ipexMoment: IpexMomentEnum.makeItMe,\n event: 'scene_undo',\n payload: {},\n keys: [],\n reportEvent: reportCustomReduxEvent,\n },\n [ActionTypes.REDO]: {\n ipexMoment: IpexMomentEnum.makeItMe,\n event: 'scene_redo',\n payload: {},\n keys: [],\n reportEvent: reportCustomReduxEvent,\n },\n};\n\n/**\n * Used by other files/modules to access the customReduxEvents object.\n * @param reduxEventType\n * @returns ICustomReduxEvent\n */\nexport const getReduxEvent = (reduxEventType: string) => {\n return customReduxEvents[reduxEventType];\n};\n\n/**\n * Builds the custom event by formatting it according to insights specifications.\n * Insights specification for events:\n * ipexMoment: 1/5 enum choices.\n * event: snake_case.\n * payload: single-depth object hierarchy with only primitives.\n * @param actionType\n * @param payload\n * @param meta\n * @returns {object}\n */\nexport const buildCustomReduxStatisticsEvent = (\n actionType: string,\n payload: any = {},\n meta: any = {}\n) => {\n const sanitizedPayload = sanitizeObject(payload);\n const sanitizedMeta = sanitizeObject(meta);\n\n const customReduxEvent = customReduxEvents[actionType];\n\n const { keys, reportEvent, ...event } = {\n ...customReduxEvent,\n ipexMoment: isNonInteractionEvent(meta)\n ? IpexMomentEnum.nonInteraction\n : customReduxEvent.ipexMoment,\n payload: {\n ...buildPayloadBasedOnKeys(\n customReduxEvents[actionType].keys,\n sanitizedPayload,\n sanitizedMeta\n ),\n },\n };\n return event;\n};\n\n/**\n * Builds a payload object based on the keys defined in the customReduxEvents objects.\n * The keys are used to filtrate through all of the possible values we have \"caught\" in the reduxEventBuilder.js file.\n * @param keys\n * @param payload\n * @param meta\n * @returns {object}\n */\nconst buildPayloadBasedOnKeys = (\n keys: string[] | undefined,\n payload: any,\n meta: any\n): any => {\n if (!keys) return;\n const builtPayload: any = {};\n keys.forEach((key: string) => {\n const value = findValueByKeyInObject(payload, key);\n builtPayload[key] = value;\n });\n keys.forEach((key: string) => {\n const value = findValueByKeyInObject(meta, key);\n if (builtPayload[key] === undefined) builtPayload[key] = value;\n });\n return sanitizeObject(builtPayload);\n};\n","import { customReduxEvents, getReduxEvent } from './reduxStatisticsEvents';\n\n/**\n * Checks if the specific action has a corresponding custom redux event defined.\n * @param {*} action\n * @returns {boolean}\n */\nconst existsInCustomReduxEvents = action => {\n return Object.keys(customReduxEvents).includes(action.type);\n};\n/**\n\n * This object has all the properties that we are interested in \"catching\" in relation to our redux events, in order to report them.\n * @param {*} object\n * @param {*} state\n * @returns {object}\n */\nconst buildPayload = object => {\n if (!object) return;\n return {\n enableMountingRails: !object?.disableMountingRails,\n view: object?.view,\n dialogType: object?.dialogType,\n filter: object?.filter,\n item: object?.item?.id,\n parent: object?.parent?.id,\n connectsTo: object?.item?.connectsTo?.id,\n extended: object?.extended,\n origin: object?.origin,\n optionValue: object?.optionValue,\n optionType: object?.optionType,\n width: object?.width,\n height: object?.height,\n subFilterDepth: object?.subFilter,\n subFilterColor: object?.colorFilter?.name,\n nonInteraction: object?.nonInteraction,\n xPosition: object?.item?.x,\n yPosition: object?.item?.y,\n zPosition: object?.item?.z,\n };\n};\n\n/**\n * Checks if the action has a corresponding custom redux events or if we have\n * specified in the meta-data that a specific action should not be reported.\n * @param {*} action\n * @returns {boolean}\n */\nconst shouldNotBeReported = action => {\n return (\n !action ||\n action.meta?.shouldReport === false ||\n !existsInCustomReduxEvents(action)\n );\n};\n\n/**\n * This function executes as part of the redux middleware.\n * Every action dispatched in the codebase enters this function.\n * @param {*} action\n * @param {*} state\n * @returns {undefined}\n */\nexport function reduxEventReporter(action) {\n if (shouldNotBeReported(action)) return;\n const event = getReduxEvent(action.type);\n event.reportEvent(\n action.type,\n buildPayload(action.payload),\n buildPayload(action.meta)\n );\n}\n","import * as Sentry from '@sentry/react';\n\nexport function sentryRedux() {\n return Sentry.createReduxEnhancer({\n actionTransformer: action => {\n const actionType = action.type;\n var payload = {};\n const typeList = [\n 'PRODUCT_MENU: SET_FILTER',\n 'PRODUCT_MENU: SET_SUB_FILTER',\n 'PRODUCT_MENU: SET_COLOR',\n 'SCENE: ITEM_PICKED_UP',\n 'TAC: UPDATE_ITEM',\n 'TAC: ADD_ITEM',\n 'TAC: REMOVE_ITEM',\n 'TAC: TAC_SET_WALL',\n 'NAVIGATION: SET_VIEW',\n ];\n if (typeList.find(type => type === actionType)) {\n payload = action.payload;\n if (actionType === 'TAC: UPDATE_ITEM') {\n payload = action.payload.model.wall;\n } else if (actionType === 'SCENE: ITEM_PICKED_UP') {\n payload = action.payload.item.filter;\n }\n }\n\n return {\n type: actionType,\n payload: payload,\n };\n },\n });\n}\n","import { createStore, compose, applyMiddleware, combineReducers } from 'redux';\nimport { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';\nimport thunk from 'redux-thunk';\nimport { reduxEventReporter } from '../services/statistics/insights/custom/redux/reduxStatisticsEventBuilder';\nimport { applicationSettings } from '../settings/application';\nimport { sentryRedux } from './sentryRedux';\n\nexport function getDefaultMiddleware(thunkInjection) {\n return [\n thunk.withExtraArgument(thunkInjection),\n store => next => action => {\n reduxEventReporter(action, store.getState(), next);\n return next(action);\n },\n ];\n}\n\n/**\n * @param {any} obj The object to inspect.\n * @returns {boolean} True if the argument appears to be a plain object.\n */\nfunction isPlainObject(obj) {\n if (typeof obj !== 'object' || obj === null) return false;\n\n let proto = obj;\n while (Object.getPrototypeOf(proto) !== null) {\n proto = Object.getPrototypeOf(proto);\n }\n\n return Object.getPrototypeOf(obj) === proto;\n}\n\nexport default function create(options = {}) {\n const sentryReduxEnhancer = sentryRedux();\n\n const {\n reducer,\n thunkInjection,\n middleware = getDefaultMiddleware(thunkInjection),\n devTools = true,\n preloadedState,\n enhancers = [sentryReduxEnhancer],\n } = options;\n\n let rootReducer;\n\n if (typeof reducer === 'function') {\n rootReducer = reducer;\n } else if (isPlainObject(reducer)) {\n rootReducer = combineReducers(reducer);\n } else {\n throw new Error(\n 'Reducer argument must be a function or an object of functions that can be passed to combineReducers'\n );\n }\n\n const middlewareEnhancer = applyMiddleware(...middleware);\n\n const storeEnhancers = [middlewareEnhancer, ...enhancers];\n\n const finalCompose = devTools ? composeWithDevTools : compose;\n\n const composedEnhancer = finalCompose(...storeEnhancers);\n\n const store = createStore(rootReducer, preloadedState, composedEnhancer);\n\n // Expose the store to enable white-box testing\n if (!applicationSettings.isProd) {\n window.reduxStore = store;\n }\n\n return store;\n}\n","import type { IExtendedTranslations } from './translationsTypes';\n\nconst defaultState: IExtendedTranslations | {} = {};\nexport default defaultState;\n","import { TRANSLATIONS_SET } from '../actionConstants';\nimport { Action } from '../../generalTypes';\nimport defaultState from '././translationsDefaultState';\nimport type { IExtendedTranslations } from './translationsTypes';\n\nexport default (\n state = defaultState,\n action: Action\n) => {\n switch (action.type) {\n case TRANSLATIONS_SET:\n return action.payload;\n default:\n return state;\n }\n};\n","import type { IExtendedDexfSettings } from './dexfSettingsTypes';\n\nconst defaultState: IExtendedDexfSettings | {} = {};\nexport default defaultState;\n","import { DEXF_SETTINGS_SET } from '../actionConstants';\nimport { Action } from '../../generalTypes';\nimport defaultState from '././dexfSettingsDefaultState';\nimport type { IExtendedDexfSettings } from './dexfSettingsTypes';\n\nexport default (\n state = defaultState,\n action: Action\n) => {\n switch (action.type) {\n case DEXF_SETTINGS_SET:\n return action.payload;\n default:\n return state;\n }\n};\n","import { Dialog } from './dialogTypes';\n\nconst defaultState: Dialog = {};\n\nexport default defaultState;\n","import { DIALOG_OPEN, DIALOG_CLOSE } from '../actionConstants';\nimport defaultState from './dialogDefaultState';\nimport { Dialog } from './dialogTypes';\n\nexport default function (state: Dialog = defaultState, action: any) {\n const options = action.payload?.options || {};\n switch (action.type) {\n case DIALOG_OPEN:\n return {\n type: action.payload.dialog,\n closable: options.closable, // TODO jost keep options object?\n options: options,\n };\n case DIALOG_CLOSE:\n return {};\n default:\n return state;\n }\n}\n","import { Init } from './initTypes';\n\nconst defaultState: Init = {\n loading: true,\n error: false,\n};\n\nexport default defaultState;\n","import { INIT_DONE, INIT_ERROR } from '../actionConstants';\nimport defaultState from './initDefaultState';\nimport { Init } from './initTypes';\n\nexport default function (state: Init = defaultState, action: any): Init {\n switch (action.type) {\n case INIT_DONE:\n return {\n loading: false,\n error: false,\n };\n case INIT_ERROR:\n return {\n loading: false,\n error: action.payload || true,\n };\n default:\n return state;\n }\n}\n","// unless specified, all values are in mm\nimport { applicationSettings } from './application';\n\nexport default {\n ROOM_DEPTH: 1000,\n IRW_IFRAME_HEIGHT: 640, //pixels\n\n KIOSK_SCALING_PERCENTAGE: 125,\n\n INITIAL_VIEW: 'INITIAL_VIEW',\n VIEW_NAMES: {\n START: 'START',\n SCENE: 'SCENE',\n SUMMARY: 'SUMMARY',\n },\n\n OFFSET_BOTTOM_ATTACHMENT: 50,\n OFFSET_TOP_ATTACHMENT: 50,\n\n NBR_OF_ATTACHMENTS: {\n 1100: 6,\n 1900: 10,\n },\n SKIRT_HEIGHT: 50,\n\n OBLIQUE_ANGLE: 27 * (Math.PI / 180),\n\n MEASUREMENTS_OFFSET: 4, //pixels\n MEASUREMENTS_OFFSET_KIOSK: 8, //pixels\n MEASUREMENTS_OFFSET_MOBILE: 4, //pixels\n MEASUREMENTS_LINE_SERIF: 50, //mm, I think.\n\n CROSS_BRACE_DIMENSIONS: {\n 650: {\n height: 820,\n width: 630,\n },\n\n 850: {\n height: 625,\n width: 825,\n },\n },\n\n /*\nNB don't just add next drag mode as 5\nsince we rely on bitwise operations here, so MIXED is INSERT | FLOAT\n*/\n DRAG_MODE: {\n INSERT: 1,\n FLOAT: 2,\n MIXED: 3,\n MULTI: 4,\n },\n\n SCREENSAVER_MODE: {\n idle: 'idle',\n slideshow: 'slideshow',\n countdown: 'countdown',\n },\n\n SESSION_STORAGE: {\n LANGUAGE_PICKER_ACTIVE_VIEW_KEY: 'LANGUAGE_PICKER_ACTIVE_VIEW_KEY',\n CHECKOUT_IMAGE_CACHE_KEY: 'CHECKOUT_IMAGE_CACHE_KEY',\n },\n\n // TODO this should roughly be 20 pixels (from UX designs) in IRW\n DROP_AREA_MAX_HEIGHT: 107,\n // and this would be half of that.\n DROP_AREA_MIN_HEIGHT: 64,\n DROP_AREA_SEPARATION: 0.1,\n\n IMAGE_ROOT: `./images/${applicationSettings.applicationName.toLowerCase()}/`,\n START_FROM_SCRATCH_IMAGE_FILENAME: 'custom_start_scratch.jpg',\n MOUNTING_RAIL_SAFETY_INSTRUCTIONS: 'mounting_rail_safety_instructions.png',\n AURDAL_GOOD_TO_KNOW_DRAWERS_IMAGE: 'good_to_know_drawers_aurdal.jpg',\n\n SERIES_GALLERY_ACTIVE_KEY: 'SERIES_GALLERY_ACTIVE_KEY',\n\n DEFAULT_TRANSPARENCY_ALPHA: 0.4,\n\n VERTEX_ROUNDING_PRECISION: 0.0001,\n\n OUTLINE_PADDING: 10,\n OUTLINE_PADDING_MOBILE_PORTRAIT: 5,\n\n // Thickness (before scaling) of any skirt edge regardless of which wall/sidewall it is.\n SKIRT_EDGE_THICKNESS: 8,\n\n SKIRT_COLOR: 0xebebeb,\n SKIRT_EDGE_COLOR: 0xcbcbc2,\n GRADIENT_LIGHT_COLOR: 0xccccc6,\n GRADIENT_DARK_COLOR: 0xb9b9b1,\n SIDEWALL_SKIRT_COLOR: 0xc3c3c3,\n SIDEWALL_SKIRT_EDGE_COLOR: 0xd6d6d6,\n SIDEWALL_GRADIENT_LIGHT_COLOR: 0xc4c4bd,\n SIDEWALL_GRADIENT_DARK_COLOR: 0xb9b9b1,\n RIGHT_SIDEWALL_SKIRT_COLOR: 0xebebeb,\n RIGHT_SIDEWALL_SKIRT_EDGE_COLOR: 0xd6d6d6,\n\n WALL_GRADIENT_ANGLE: 11,\n SIDEWALL_GRADIENT_ANGLE: 0,\n};\n","import constants from './masterConstants';\n\nexport function uniformGrid(size) {\n return {\n x: { step: size, offset: 0 },\n y: { step: size, offset: 0 },\n };\n}\n\nexport const composeBoaGrid = () => {\n const sizes = {};\n sizes[constants.DRAG_MODE.INSERT] = uniformGrid(10);\n sizes[constants.DRAG_MODE.FLOAT] = {\n x: { step: 200, offset: 0, activationDistance: 1250 },\n y: { step: 100, offset: constants.SKIRT_HEIGHT },\n };\n sizes[constants.DRAG_MODE.MIXED] = uniformGrid(10);\n return sizes;\n};\n","import { applicationSettings } from '../application';\nimport { uniformGrid } from '../settingsHelpers';\nimport { DIMENSIONS, ITEMS, MS, TS, UNITS } from '../../constants';\nconst { width, depth, height } = DIMENSIONS;\n\nexport default {\n grid: uniformGrid(1),\n defaultFilter: 'sections',\n roomMinDesktop: {\n width: 4600,\n height: 3100,\n },\n roomMinSummary: {\n width: 1000,\n height: 2200,\n },\n roomMinMobile: {\n width: 2200,\n height: 2200,\n },\n distanceBetweenAttachments: 75,\n roomMax: {\n width: 5700,\n height: 3250,\n },\n zoomableScene: false,\n sideWall: true,\n postWidth: 20, // side-panels\n wall: {\n width: {\n min: 1000,\n max: 5700,\n },\n height: {\n min: 2200,\n max: 3250,\n },\n points:\n applicationSettings.uiPlatform === 'kiosk'\n ? [\n { x: 0, y: 0 },\n { x: 4600, y: 0 },\n { x: 4600, y: 2400 },\n { x: 0, y: 2400 },\n ]\n : [\n { x: 0, y: 0 },\n { x: 4000, y: 0 },\n { x: 4000, y: 2650 },\n { x: 0, y: 2650 },\n ],\n pointsMobilePortrait: [\n { x: 0, y: 0 },\n { x: 2400, y: 0 },\n { x: 2400, y: 2650 },\n { x: 0, y: 2650 },\n ],\n pointsMobileLandscape: [\n { x: 0, y: 0 },\n { x: 5700, y: 0 },\n { x: 5700, y: 2650 },\n { x: 0, y: 2650 },\n ],\n pointsTablet: [\n { x: 0, y: 0 },\n { x: 3900, y: 0 },\n { x: 3900, y: 2650 },\n { x: 0, y: 2650 },\n ],\n },\n canvasMargins: { top: 0, right: 0 },\n canvasMarginsKiosk: { top: 0, right: 0 },\n canvasMarginsMobile: {\n top: 0,\n right: 0,\n },\n canvasMarginsPortraitPercentual: {\n mobile: 0,\n tablet: 0,\n },\n displaySectionMeasurements: false,\n mountingRails: {\n white: {\n 1250: '50459208',\n 650: '70459207',\n },\n darkGrey: {\n 1250: '80460955',\n 650: '60460956',\n },\n },\n sectionSnappingDistance: 50,\n showMultipleErrors: true,\n dynamicWallLimits: true,\n measurements: {\n productsRemap: [],\n displayMeasurement: [\n {\n affects: [ITEMS.SECTION],\n rules: {\n measure: false,\n display: true,\n measurementPrefix: false,\n format: `${width}${TS}${depth}${TS}${height} ${MS}`,\n unit: UNITS.cm,\n },\n },\n {\n affects: [\n ITEMS.SECTION,\n ITEMS.CLOTHES_RAIL,\n ITEMS.SHELF,\n ITEMS.DRAWER,\n ITEMS.BASKET,\n ITEMS.SHOE_SHELF,\n ],\n rules: {\n measure: true,\n display: true,\n measurementPrefix: true,\n format: `${width} ${MS}, ${height} ${MS}, ${depth} ${MS}`,\n unit: UNITS.cm,\n },\n },\n ],\n },\n hideInfoIconForType: [],\n bulkArticles: {},\n colorCodes: {\n white: '#F8F8F7',\n dark_grey: '#696664',\n },\n};\n","import { applicationSettings } from '../application';\nimport { composeBoaGrid, uniformGrid } from '../settingsHelpers';\nimport { DIMENSIONS, ITEMS, MS, UNITS } from '../../constants';\nconst { width, height } = DIMENSIONS;\n\nexport default {\n grid: uniformGrid(10),\n dynamicGrid: composeBoaGrid(),\n defaultFilter: 'uprights',\n postWidth: 20,\n padHeight: 0,\n roomMinDesktop: {\n width: 4600,\n height: 3100,\n },\n roomMinSummary: {\n width: 1000,\n height: 1000,\n },\n roomMinMobile: {\n width: 2200,\n height: 2200,\n },\n distanceBetweenAttachments: 100,\n roomMax: {\n width: 5700,\n height: 3250,\n },\n zoomableScene: false,\n sideWall: true,\n wall: {\n width: {\n min: 1000,\n max: 5700,\n },\n height: {\n min: 1250,\n max: 3250,\n },\n points:\n applicationSettings.uiPlatform === 'kiosk'\n ? [\n { x: 0, y: 0 },\n { x: 4600, y: 0 },\n { x: 4600, y: 2400 },\n { x: 0, y: 2400 },\n ]\n : [\n { x: 0, y: 0 },\n { x: 4000, y: 0 },\n { x: 4000, y: 2650 },\n { x: 0, y: 2650 },\n ],\n pointsMobilePortrait: [\n { x: 0, y: 0 },\n { x: 2400, y: 0 },\n { x: 2400, y: 2650 },\n { x: 0, y: 2650 },\n ],\n pointsMobileLandscape: [\n { x: 0, y: 0 },\n { x: 5700, y: 0 },\n { x: 5700, y: 2650 },\n { x: 0, y: 2650 },\n ],\n pointsTablet: [\n { x: 0, y: 0 },\n { x: 3900, y: 0 },\n { x: 3900, y: 2650 },\n { x: 0, y: 2650 },\n ],\n },\n canvasMargins: { top: 50, right: 50 },\n canvasMarginsKiosk: { top: 50, right: 50 },\n canvasMarginsMobile: {\n top: 50,\n right: 50,\n },\n canvasMarginsPortraitPercentual: {\n mobile: 0.075,\n tablet: 0.05,\n },\n displaySectionMeasurements: true,\n displayTableMeasurements: true,\n /* TODO: Logic for automatic choice of mounting rails will be implemented in STOR2-3816.\n Until then, comment out the set of mounting rails that you don't want to use. */\n mountingRails: {\n white: {\n 1800: '60474270',\n 800: '10448740',\n 600: '30448739',\n },\n anthracite: {\n 1800: '00575597',\n 800: '20575596',\n 600: '70575594',\n },\n },\n measurements: {\n productsRemap: [],\n displayMeasurement: [\n {\n affects: [ITEMS.UPRIGHT],\n rules: {\n display: true,\n measure: false,\n measurementPrefix: false,\n format: `${height} ${MS}`,\n unit: UNITS.cm,\n },\n },\n {\n affects: [\n ITEMS.SHELF,\n ITEMS.METAL_SHELF,\n ITEMS.WIRE_SHELF,\n ITEMS.BASKET,\n ITEMS.CLOTHES_RAIL,\n ITEMS.TROUSER_HANGER,\n ITEMS.DRYING_RACK,\n ITEMS.SHOE_SHELF,\n ],\n rules: {\n measure: false,\n display: false,\n measurementPrefix: false,\n format: `${width} ${MS}`,\n unit: UNITS.cm,\n },\n },\n ],\n },\n hideInfoIconForType: [],\n bulkArticles: {},\n colorCodes: {\n white: '#F8F8F7',\n metal_white: '#F3F3F3',\n oak: '#D8B47E',\n wire_white: '#F8F8F7',\n black_brown: '#464541',\n anthracite: '#383E42',\n wire_anthracite: '#383E42',\n metal_anthracite: '#5F6367',\n grey: '#ABABAB',\n dark_grey: '#696766',\n light_blue: '#9BBBCB',\n white_stained_oak_effect: '#B6A189',\n },\n cuttableMountingRailProtrusionThreshold: 30,\n linkCardSkyttaImageFilename: 'link_card_skytta.jpg',\n};\n","import { uniformGrid } from '../settingsHelpers';\nimport { DIMENSIONS, ITEMS, MS, TS, UNITS } from '../../constants';\nconst { width, depth, height } = DIMENSIONS;\n\nexport default {\n grid: uniformGrid(10),\n defaultFilter: 'sections',\n postWidth: 42,\n padHeight: 0,\n roomMinDesktop: {\n width: 4650,\n height: 2200,\n },\n roomMinSummary: {\n width: 1000,\n height: 1000,\n },\n roomMinMobile: {\n width: 2200,\n height: 2200,\n },\n distanceBetweenAttachments: 200,\n roomMax: {\n width: 8000,\n height: 5000,\n },\n zoomableScene: true,\n canvasMargins: { top: 30, right: 60 },\n canvasMarginsKiosk: { top: 30, right: 100 },\n canvasMarginsMobile: {\n top: 50,\n right: 40,\n },\n displaySectionMeasurements: false,\n measurements: {\n productsRemap: [\n {\n affects: [\n '30333286',\n '10547387',\n '60333850',\n '80547384',\n '00452619',\n '80452620',\n 'S49275286',\n ],\n keys: {\n [DIMENSIONS.width]: DIMENSIONS.depth,\n [DIMENSIONS.height]: DIMENSIONS.height,\n Length: DIMENSIONS.width,\n },\n },\n ],\n displayMeasurement: [\n {\n affects: [ITEMS.SECTION, ITEMS.POST],\n rules: {\n display: true,\n measure: false,\n measurementPrefix: false,\n format: `${height}${TS}${width} ${MS}`,\n unit: UNITS.cm,\n },\n },\n {\n affects: [ITEMS.SHELF, ITEMS.PEGBOARD],\n rules: {\n display: true,\n measure: true,\n measurementPrefix: true,\n format: `${width} ${MS}, ${height} ${MS}`,\n unit: UNITS.cm,\n },\n },\n {\n affects: [ITEMS.ADD_ON],\n rules: {\n display: false,\n measure: false,\n measurementPrefix: true,\n format: `${width} ${MS}, ${height} ${MS}`,\n unit: UNITS.cm,\n },\n },\n {\n affects: [ITEMS.DIVIDER],\n rules: {\n display: true,\n measure: false,\n measurementPrefix: true,\n format: `${width} ${MS}, ${height} ${MS}`,\n unit: UNITS.cm,\n },\n },\n {\n affects: [ITEMS.CABINET, ITEMS.WORKBENCH, ITEMS.TROLLEY],\n rules: {\n display: true,\n measure: false,\n measurementPrefix: true,\n format: `${width} ${MS}, ${depth} ${MS}, ${height} ${MS}`,\n unit: UNITS.cm,\n },\n },\n ],\n },\n hideInfoIconForType: ['post'],\n bulkArticles: {\n 80333284: {\n id: '30512283',\n bulkSize: 4,\n },\n 30333842: {\n id: '00512289',\n bulkSize: 4,\n },\n 90333288: {\n id: '10512279',\n bulkSize: 4,\n },\n 40382785: {\n id: '90512280',\n bulkSize: 4,\n },\n },\n colorCodes: {\n white: '#F8F8F7',\n black: '#383E42',\n wood: '#F4D8A8',\n grey_green: '#738273',\n },\n};\n","import { applicationSettings } from '../application';\nimport { uniformGrid } from '../settingsHelpers';\nimport { DIMENSIONS, ITEMS, MS, TS, UNITS } from '../../constants';\nconst { width, depth, height } = DIMENSIONS;\n\nexport default {\n postWidth: 45,\n distanceBetweenAttachments: 32,\n grid: uniformGrid(1),\n defaultFilter: 'sections',\n roomMinDesktop: {\n width: 1000,\n height: 1000,\n },\n roomMinSummary: {\n width: 1000,\n height: 1000,\n },\n roomMinMobile: {\n width: 2200,\n height: 2200,\n },\n zoomableScene: false,\n sideWall: true,\n wall: {\n width: {\n min: 1000,\n max: 5700,\n },\n height: {\n min: 1000,\n max: 3250,\n },\n points:\n applicationSettings.uiPlatform === 'kiosk'\n ? [\n { x: 0, y: 0 },\n { x: 4600, y: 0 },\n { x: 4600, y: 2400 },\n { x: 0, y: 2400 },\n ]\n : [\n { x: 0, y: 0 },\n { x: 4000, y: 0 },\n { x: 4000, y: 2650 },\n { x: 0, y: 2650 },\n ],\n pointsMobilePortrait: [\n { x: 0, y: 0 },\n { x: 2400, y: 0 },\n { x: 2400, y: 2650 },\n { x: 0, y: 2650 },\n ],\n pointsMobileLandscape: [\n { x: 0, y: 0 },\n { x: 5700, y: 0 },\n { x: 5700, y: 2650 },\n { x: 0, y: 2650 },\n ],\n pointsTablet: [\n { x: 0, y: 0 },\n { x: 3900, y: 0 },\n { x: 3900, y: 2650 },\n { x: 0, y: 2650 },\n ],\n },\n canvasMargins: { top: 50, right: 50 },\n canvasMarginsKiosk: { top: 50, right: 50 },\n canvasMarginsMobile: {\n top: 50,\n right: 50,\n },\n canvasMarginsPortraitPercentual: {\n mobile: 0.075,\n tablet: 0.05,\n },\n sectionSnappingDistance: 50,\n showMultipleErrors: true,\n measurements: {\n productsRemap: [\n {\n affects: ['10453072'],\n keys: { 'system, width:': DIMENSIONS.width },\n },\n ],\n displayMeasurement: [\n {\n affects: [ITEMS.SECTION],\n rules: {\n display: true,\n measure: false,\n measurementPrefix: false,\n format: `${width}${TS}${height} ${MS}`,\n unit: UNITS.cm,\n },\n },\n {\n affects: [\n ITEMS.SHELF,\n ITEMS.FELT_SHELF,\n ITEMS.BOTTLE_RACK,\n ITEMS.DRAWER,\n ITEMS.ITEM,\n ITEMS.CHEST,\n ITEMS.CABINET,\n ITEMS.TABLE,\n ITEMS.SHELVING_UNIT,\n ITEMS.DOOR,\n ],\n rules: {\n measure: true,\n display: true,\n measurementPrefix: true,\n format: `${width} ${MS}, ${depth} ${MS}, ${height} ${MS}`,\n unit: UNITS.cm,\n },\n },\n {\n affects: [ITEMS.SHELF_DRAWER],\n rules: {\n measure: false,\n display: false,\n measurementPrefix: true,\n format: `${width} ${MS}, ${height} ${MS}, ${depth} ${MS}`,\n unit: UNITS.cm,\n },\n },\n {\n affects: [ITEMS.BOX],\n rules: {\n measure: false,\n display: true,\n measurementPrefix: true,\n format: `${width} ${MS}, ${depth} ${MS}, ${height} ${MS}`,\n unit: UNITS.cm,\n },\n },\n ],\n },\n hideInfoIconForType: [],\n bulkArticles: {},\n colorCodes: {\n pine: '#F4D8A8',\n mesh_white: '#F8F8F7',\n white: '#F8F8F7',\n grey: '#FBFBFB',\n mesh_green: '#3D6443',\n mesh_grey: '#736E6B',\n red: '#BA3531',\n },\n};\n","import { uniformGrid } from '../settingsHelpers';\nimport { DIMENSIONS, ITEMS, MS, TS, UNITS } from '../../constants';\nconst { width, depth, height } = DIMENSIONS;\n\nexport default {\n grid: uniformGrid(1),\n defaultFilter: 'frames',\n postWidth: 11,\n padHeight: 5,\n roomMinDesktop: {\n width: 0,\n height: 2500,\n },\n roomMinSummary: {\n width: 1000,\n height: 1000,\n },\n roomMinMobile: {\n width: 2200,\n height: 2200,\n },\n distanceBetweenAttachments: 162,\n roomMax: {\n width: 4500,\n height: 3000,\n },\n zoomableScene: true,\n canvasMargins: { top: 30, right: 60 },\n canvasMarginsKiosk: { top: 30, right: 100 },\n canvasMarginsMobile: {\n top: 50,\n right: 40,\n },\n displaySectionMeasurements: false,\n measurements: {\n productsRemap: [],\n displayMeasurement: [\n {\n affects: [ITEMS.FRAME, ITEMS.SHELVING_UNIT],\n rules: {\n display: true,\n measure: false,\n measurementPrefix: false,\n format: `${width}${TS}${depth}${TS}${height} ${MS}`,\n unit: UNITS.cm,\n },\n },\n {\n affects: [ITEMS.CLOTHES_RAIL, ITEMS.BASKET, ITEMS.TOP_SHELF],\n rules: {\n display: false,\n measure: false,\n measurementPrefix: false,\n format: `${height}${TS}${width} ${MS}`,\n unit: UNITS.cm,\n },\n },\n ],\n },\n hideInfoIconForType: [],\n bulkArticles: {},\n colorCodes: {\n white: '#F8F8F7',\n anthracite: '#383E42',\n grey: '#929191',\n },\n};\n","import { applicationSettings } from '../application';\nimport { uniformGrid } from '../settingsHelpers';\nimport { DIMENSIONS, ITEMS, MS, UNITS } from '../../constants';\nconst { width, depth, height } = DIMENSIONS;\n\n// TODO: Currently copied from IVAR. Adjust according to the needs in ELVARLI.\nexport default {\n postWidth: {\n [ITEMS.SECTION_POSTS]: 82,\n [ITEMS.SECTION_SIDE_UNITS]: 42,\n },\n distanceBetweenAttachments: 32,\n grid: uniformGrid(1),\n defaultFilter: 'sections',\n roomMin: {\n width: 1000,\n height: 1000,\n },\n roomMinCheckout: {\n width: 1000,\n height: 1000,\n },\n zoomableScene: false,\n sideWall: true,\n wall: {\n width: {\n min: 1000,\n max: 5700,\n },\n height: {\n min: 2220,\n max: 3500,\n },\n points:\n applicationSettings.uiPlatform === 'kiosk'\n ? [\n { x: 0, y: 0 },\n { x: 4600, y: 0 },\n { x: 4600, y: 2400 },\n { x: 0, y: 2400 },\n ]\n : [\n { x: 0, y: 0 },\n { x: 4000, y: 0 },\n { x: 4000, y: 2400 },\n { x: 0, y: 2400 },\n ],\n pointsMobilePortrait: [\n { x: 0, y: 0 },\n { x: 2400, y: 0 },\n { x: 2400, y: 2400 },\n { x: 0, y: 2400 },\n ],\n pointsMobileLandscape: [\n { x: 0, y: 0 },\n { x: 5700, y: 0 },\n { x: 5700, y: 2400 },\n { x: 0, y: 2400 },\n ],\n pointsTablet: [\n { x: 0, y: 0 },\n { x: 3900, y: 0 },\n { x: 3900, y: 2400 },\n { x: 0, y: 2400 },\n ],\n },\n canvasMargins: { top: 0, right: 0 },\n canvasMarginsKiosk: { top: 0, right: 0 },\n canvasMarginsMobile: {\n top: 0,\n right: 0,\n },\n canvasMarginsPortraitPercentual: {\n mobile: 0,\n tablet: 0,\n },\n sectionSnappingDistance: 50,\n showMultipleErrors: true,\n measurements: {\n productsRemap: [],\n displayMeasurement: [\n {\n affects: [ITEMS.SECTION_POSTS, ITEMS.SECTION_SIDE_UNITS],\n rules: {\n display: true,\n measure: false,\n measurementPrefix: false,\n format: `${width} ${MS}`,\n unit: UNITS.cm,\n },\n },\n {\n affects: [ITEMS.SHELF, ITEMS.DRAWER, ITEMS.ITEM],\n rules: {\n measure: true,\n display: false,\n measurementPrefix: true,\n format: `${width} ${MS}, ${depth} ${MS}, ${height} ${MS}`,\n unit: UNITS.cm,\n },\n },\n ],\n },\n hideInfoIconForType: [],\n bulkArticles: {},\n colorCodes: {\n white: '#F8F8F7',\n bamboo: '#D6BDA1',\n },\n};\n","import AURDAL from './AURDAL';\nimport BOAXEL from './BOAXEL';\nimport BROR from './BROR';\nimport IVAR from './IVAR';\nimport JONAXEL from './JONAXEL';\nimport ELVARLI from './ELVARLI';\n\nexport default {\n AURDAL,\n BOAXEL,\n BROR,\n IVAR,\n JONAXEL,\n ELVARLI,\n};\n","import { applicationSettings } from './application';\nimport rangeSettings from './rangeSettings';\nimport constants from './masterConstants';\n\nconst getRangeConstants = () => {\n if (applicationSettings.applicationName in rangeSettings)\n return rangeSettings[applicationSettings.applicationName];\n\n throw new Error('Range specific constants missing');\n};\n\nconst rangeConstants = getRangeConstants();\n\nexport default Object.assign(constants, {\n GRID: rangeConstants.grid,\n DYNAMIC_GRID: rangeConstants.dynamicGrid,\n DEFAULT_FILTER: rangeConstants.defaultFilter,\n POST_WIDTH: rangeConstants.postWidth,\n PAD_HEIGHT: rangeConstants.padHeight,\n ROOM_MAX: rangeConstants.roomMax,\n ROOM_MIN_DESKTOP: rangeConstants.roomMinDesktop,\n ROOM_MIN_MOBILE: rangeConstants.roomMinMobile,\n ROOM_MIN_SUMMARY: rangeConstants.roomMinSummary,\n DISTANCE_BETWEEN_ATTACHMENTS: rangeConstants.distanceBetweenAttachments,\n ZOOMABLE_SCENE: rangeConstants.zoomableScene,\n WALL: rangeConstants.wall,\n SIDEWALL: rangeConstants.sideWall,\n CANVAS_MARGINS: rangeConstants.canvasMargins,\n CANVAS_MARGINS_KIOSK: rangeConstants.canvasMarginsKiosk,\n CANVAS_MARGINS_MOBILE: rangeConstants.canvasMarginsMobile,\n CANVAS_MARGINS_PORTRAIT_PERCENTUAL:\n rangeConstants.canvasMarginsPortraitPercentual,\n CANVAS_MARGINS_SUMMARY: rangeConstants.canvasMarginsSummary,\n APPLICATION_NAME: applicationSettings.applicationName,\n DISPLAY_SECTION_MEASUREMENTS: rangeConstants.displaySectionMeasurements,\n MOUNTING_RAILS: rangeConstants.mountingRails,\n SECTION_SNAPPING_DISTANCE: rangeConstants.sectionSnappingDistance,\n SHOW_MULTIPLE_ERRORS: rangeConstants.showMultipleErrors,\n DYNAMIC_WALL_LIMITS: rangeConstants.dynamicWallLimits,\n DISPLAY_TABLE_MEASUREMENTS: rangeConstants.displayTableMeasurements,\n DISPLAY_MEASUREMENT: rangeConstants.displayMeasurement,\n MEASUREMENTS_SETTINGS: rangeConstants.measurements,\n HIDE_INFO_ICON_FOR_TYPE: rangeConstants.hideInfoIconForType,\n BULK_ARTICLES: rangeConstants.bulkArticles,\n COLOR_CODES: rangeConstants.colorCodes,\n SCENE: rangeConstants.scene,\n CUTTABLE_MOUNTING_RAIL_PROTRUSION_THRESHOLD:\n rangeConstants.cuttableMountingRailProtrusionThreshold,\n LINK_CARD_SKYTTA_IMAGE_FILENAME: rangeConstants.linkCardSkyttaImageFilename,\n});\n","import { applicationSettings } from '../../settings/application';\n\nfunction FakeStorage() {\n this.length = 0;\n}\n\nFakeStorage.prototype = {\n setItem: function (key, val) {\n this[key] = val;\n this.length = Object.keys(this).length - 1;\n },\n getItem: function (key) {\n return this[key] || null;\n },\n removeItem: function (key) {\n delete this[key];\n this.length = Object.keys(this).length - 1;\n },\n clear: function () {\n Object.keys(this).forEach(function (key) {\n if (this.hasOwnProperty(key)) {\n delete this[key];\n }\n }, this);\n this.length = Object.keys(this).length - 1;\n },\n key: function (index) {\n return Object.keys(this)[index + 1] || null; // + 1 since this.length\n },\n};\n\nconst uid = Date();\nconst hasTag = new RegExp(applicationSettings.applicationName + '$');\n\nfunction decideStorage(type) {\n let storage;\n let variant;\n try {\n storage = localStorage;\n variant = 'localStorage';\n storage.setItem(uid, uid);\n if (storage.getItem(uid) !== uid || type === 'session') {\n throw new Error(\"localStorage doesn't work\");\n }\n storage.removeItem(uid);\n } catch (error) {\n try {\n storage = sessionStorage;\n variant = 'sessionStorage';\n storage.setItem(uid, uid);\n if (storage.getItem(uid) !== uid) {\n throw new Error('sessionStorage doesnt work');\n }\n storage.removeItem(uid);\n } catch (error) {\n variant = 'appStorage';\n storage = new FakeStorage();\n }\n }\n return {\n storage: storage,\n variant: variant,\n };\n}\n\nfunction tag(key) {\n return key + '-' + applicationSettings.applicationName;\n}\n\nfunction wrapStorage(storageStuff) {\n const storage = storageStuff.storage;\n const variant = storageStuff.variant;\n return {\n /*\n need to know what type of storage we're using.\n keep this in wrapper so we dont pollute the real storage\n */\n variant: variant,\n setItem: function (key, val) {\n return storage.setItem(tag(key), val);\n },\n getItem: function (key) {\n return storage.getItem(tag(key));\n },\n length: storage.length,\n removeItem: function (key) {\n return storage.removeItem(tag(key));\n },\n clear: function () {\n for (const key in storage) {\n if (hasTag.test(key) && storage.hasOwnProperty(key)) {\n storage.removeItem(key);\n }\n }\n },\n key: function (key) {\n return storage.key(tag(key));\n },\n };\n}\n\nfunction getLocalStorage() {\n return wrapStorage(decideStorage('local'));\n}\n\nfunction getSessionStorage() {\n return wrapStorage(decideStorage('session'));\n}\n\nconst api = {\n session: getSessionStorage(),\n local: getLocalStorage(),\n};\n\nexport default api;\n","import constants from '../../settings/constants';\nimport storage from '../../services/history/storage';\n// @ts-ignore // No types found\nimport { UrlUtility } from '@ikea-aoa/ikea-shared-utils';\nimport { Navigation } from './navigationTypes';\n\nconst {\n SESSION_STORAGE: { LANGUAGE_PICKER_ACTIVE_VIEW_KEY },\n VIEW_NAMES,\n INITIAL_VIEW,\n} = constants;\n\nexport const getInitialView = () => {\n const initialView = UrlUtility.getHashQuery()[INITIAL_VIEW];\n const storageItem = storage.session.getItem(LANGUAGE_PICKER_ACTIVE_VIEW_KEY);\n\n if (!initialView) return VIEW_NAMES.SCENE;\n if (storageItem) return storageItem;\n if (!VIEW_NAMES.hasOwnProperty(initialView.toUpperCase()))\n throw new Error(`View '${initialView}' does not exist.`);\n\n return initialView.toUpperCase();\n};\n\nconst defaultState: Navigation = {\n currentView: getInitialView(),\n views: Object.keys(VIEW_NAMES),\n history: [],\n};\n\nexport default defaultState;\n","import { SET_VIEW } from '../actionConstants';\nimport defaultState from './navigationDefaultState';\n\nexport default (state = defaultState, action: any) => {\n switch (action.type) {\n case SET_VIEW:\n return {\n ...state,\n currentView: action.payload.view,\n history: state.currentView ? [state.currentView, ...state.history] : [],\n };\n\n default:\n return state;\n }\n};\n","import { State } from '../StateTypes';\nimport { Navigation } from './navigationTypes';\nimport constants from '../../settings/constants';\n/**\n * Select navigation slice state\n *\n * @param navigation\n */\nexport const selectNavigation = ({ navigation }: State): Navigation =>\n navigation;\n\n/**\n * Select current view\n *\n * @param state\n */\nexport const selectCurrentView = (state: State): string =>\n selectNavigation(state).currentView;\n\n/**\n * Selects first index of history\n *\n * @param state\n */\nexport const selectMostRecentHistory = (state: State) =>\n selectNavigation(state).history.length\n ? selectNavigation(state).history?.[0]\n : false;\n\n/**\n * Returns true if current view matches param view\n *\n * @param view\n */\nexport const selectIsView = (view: string) => (state: State) =>\n selectCurrentView(state) === view;\n\nexport const selectIsCurrentViewStartView = (state: State) =>\n selectCurrentView(state) === constants.VIEW_NAMES.START;\n","/**\n * @param {Object} item\n * @returns {Object | undefined} An object containing the changed props\n */\nconst getChangedProps = item => {\n if (item.id === '60333850b') {\n // replace any old trolley w doors with correct id\n return {\n id: 'S49275286',\n };\n } else if (\n [\n '40033763_insert',\n '70033766_insert',\n '10482946_insert',\n '70482948_insert',\n '50482949_insert',\n '30381593_insert',\n '50381592_insert',\n '40483973_insert',\n '60483972_insert',\n ].some(id => item.id === id)\n ) {\n // IVAR cabinets are now the same product as inserts and non-inserts\n // and their position in the section have corrected slightly\n return {\n id: item.id.replace('_insert', ''),\n x: item.x + 15,\n };\n }\n};\n\n/**\n * @param {Object} item\n * @returns {Object} A corrected version of the item\n */\nconst getCorrectedItem = item => {\n return {\n ...item,\n ...getChangedProps(item),\n items: [...(item.items?.map(getCorrectedItem) || [])],\n };\n};\n\n/**\n * Goes through a PAC coming from a VPC and replaces any products that have\n * changed since its creation.\n * @param {Object} pac\n * @returns {Object} A corrected version of the PAC\n */\nexport default function fixVPC(pac) {\n const adjustedPAC = {\n ...pac,\n model: {\n ...pac.model,\n items: [...pac.model.items.map(getCorrectedItem)],\n },\n isVpc: true,\n };\n\n return adjustedPAC;\n}\n","import type { IKompisTranslations } from '@inter-ikea-kompis/types';\nimport type { IStorageTwoTranslations } from './state/translations/translationsTypes';\n\ninterface IKompisTranslationMappings {\n [key: string]: keyof IKompisTranslations;\n}\n\ninterface IStorageTwoTranslationMappings {\n [key: string]: keyof IStorageTwoTranslations;\n}\n\nexport interface ITranslationMappings {\n [key: string]: keyof (IKompisTranslations & IStorageTwoTranslations);\n}\n\nconst kompisTranslationMappings: IKompisTranslationMappings = {\n BACK: 'back',\n DONE: 'done',\n INFORMATION_VIEW_MORE_INFORMATION: 'informationViewMoreInformation',\n INVALID_CONFIGURATION_DESCRIPTION: 'invalidConfigurationDescription',\n INVALID_CONFIGURATION_MESSAGE: 'invalidConfigurationMessage',\n IPEX_GALLERY_EXIT_DIALOGUE_BODY: 'ipexGalleryExitDialogueBody',\n IPEX_GALLERY_EXIT_DIALOGUE_CONTINUE_BUTTON:\n 'ipexGalleryExitDialogueContinueButton',\n IPEX_GALLERY_EXIT_DIALOGUE_EXIT_BUTTON: 'ipexGalleryExitDialogueExitButton',\n IPEX_GALLERY_EXIT_DIALOGUE_HEADLINE: 'ipexGalleryExitDialogueHeadline',\n LABEL_DEPTH: 'labelDepth',\n LABEL_HEIGHT: 'labelHeight',\n LABEL_WIDTH: 'labelWidth',\n LOAD_VPC_DIALOG_HEADING: 'loadVpcDialogHeading',\n MEASURE_VALUE_CM: 'measureValueCm',\n MEASURE_VALUE_IN: 'measureValueIn',\n MEASURE_VALUE_FEET: 'measureValueFeet',\n PLANNER_BANNER_MESSAGE_BODY: 'plannerBannerMessageBody',\n PLANNER_BANNER_MESSAGE_BODY_KIOSK: 'plannerBannerMessageBodyKiosk',\n PLANNER_BANNER_MESSAGE_LINK_TEXT: 'plannerBannerMessageLinkText',\n PLANNER_BANNER_MESSAGE_TITLE: 'plannerBannerMessageTitle',\n PLANNER_BANNER_MESSAGE_TITLE_KIOSK: 'plannerBannerMessageTitleKiosk',\n PLANNER_SURVEY_DECLINE: 'plannerSurveyDecline',\n PRODUCT_DETAILS_GOOD_TO_KNOW_HEADER: 'productDetailsGoodToKnowHeader',\n PRODUCT_DETAILS_SAFETY_AND_COMPLIANCE_HEADER:\n 'productDetailsSafetyAndComplianceHeader',\n PRODUCT_LIST_HEADLINE: 'productListHeadline',\n SERIES_GALLERY_LINK: 'seriesGalleryLink',\n SHORT_LABEL_DEPTH: 'shortLabelDepth',\n SHORT_LABEL_HEIGHT: 'shortLabelHeight',\n SHORT_LABEL_WIDTH: 'shortLabelWidth',\n SPR_GALLERY_HEADLINE: 'sprGalleryHeadline',\n SPR_GALLERY_OPEN_SAVED_SPR: 'sprGalleryOpenSavedSpr',\n START_AGAIN: 'startAgain',\n START_FROM_SCRATCH: 'startFromScratch',\n TIMES_SYMBOL: 'timesSymbol',\n TIP_OVER_WARNING: 'tipOverWarning',\n TIP_OVER_WARNING_LINK: 'tipOverWarningLink',\n TOOLTIP_DELETE: 'tooltipDelete',\n TOOLTIP_DELETE_SECTION: 'tooltipDeleteSection',\n TOOLTIP_INFO: 'tooltipInfo',\n TOOLTIP_ROTATE: 'tooltipRotate',\n YES_PLEASE: 'yesPlease',\n YMAL_HEADING: 'ymalHeading',\n};\n\nconst storageTwoTranslationMappings: IStorageTwoTranslationMappings = {\n ALERT_INFORMATION_CLOTHES_RAIL: 'alertInformationClothesRail',\n AURDAL_DRAWERS_GOOD_TO_KNOW_DESCRIPTION: 'aurdalDrawersGoodToKnowDescription',\n AURDAL_DRAWERS_GOOD_TO_KNOW_HEADING: 'aurdalDrawersGoodToKnowHeading',\n BACK_TO_PRODUCT_GALLERY: 'backToProductGallery',\n BALLOON_HINT_NO_PLACE: 'balloonHintNoPlace',\n BALLOON_HINT_STEP_THREE_UNIT: 'balloonHintStepThreeUnit',\n BALLOON_HINT_STEP_THREE_WALL_INTRO: 'balloonHintStepThreeWallIntro',\n BRACKETS_INFO: 'bracketsInfo',\n BUTTON_HIDE_MEASURES: 'buttonHideMeasures',\n BUTTON_SHOW_MEASURES: 'buttonShowMeasures',\n CANNOT_FIT_CABINET: 'cannotFitCabinet',\n CANNOT_FIT_SHELF_DIVIDER: 'cannotFitShelfDivider',\n CEILING_HEIGHT: 'ceilingHeight',\n CHOICE_INFORMATION: 'choiceInformation',\n CLOTHES_RAIL_ADJACENT: 'clothesRailAdjacent',\n COLOUR: 'colour',\n FILTER_DEPTH_LABEL: 'filterDepthLabel',\n GALLERY_RETURN_INFORMATION: 'galleryReturnInformation',\n LINK_CARD_SKYTTA_BUTTON: 'linkCardSkyttaButton',\n LINK_CARD_SKYTTA_DESCRIPTION: 'linkCardSkyttaDescription',\n LINK_CARD_SKYTTA_HEADING: 'linkCardSkyttaHeading',\n MENU_FILTER_ACCESSORIES: 'menuFilterAccessories',\n MENU_FILTER_CABINETS: 'menuFilterCabinets',\n MENU_FILTER_INSERTS: 'menuFilterInserts',\n MENU_FILTER_ONE: 'menuFilterOne',\n MENU_FILTER_TABLETOP: 'menuFilterTabletop',\n MENU_FILTER_THREE: 'menuFilterThree',\n MENU_FILTER_TWO: 'menuFilterTwo',\n MENU_FILTER_UPRIGHTS: 'menuFilterUprights',\n MENU_PRODUCT_DOORS: 'menuProductDoors',\n MENU_SECTION_FRAMES: 'menuSectionFrames',\n MENU_SECTION_SHELVING_UNITS: 'menuSectionShelvingUnits',\n MINI_SURVEY_BODY: 'miniSurveyBody',\n MOUNTING_RAIL: 'mountingRail',\n MOUNTING_RAIL_COMES_IN_THREE_DIFFERENT_LENGTHS:\n 'mountingRailComesInThreeDifferentLengths',\n MOUNTING_RAIL_CUT: 'mountingRailCut',\n MOUNTING_RAIL_HELPS_YOU_HANG_STORAGE_RAILS_EVENLY:\n 'mountingRailHelpsYouHangStorageRailsEvenly',\n POPUP_ADD_SHELVES_BODY: 'popupAddShelvesBody',\n POPUP_ADD_SHELVES_HEADER: 'popupAddShelvesHeader',\n POPUP_ALERT_DUPLICATE_SIDEPANEL: 'popupAlertDuplicateSidepanel',\n POPUP_ALERT_INVALID_CONFIG_ADD_PART: 'popupAlertInvalidConfigAddPart',\n POPUP_ALERT_INVALID_CONFIG_HIGH_CABINET: 'popupAlertInvalidConfigHighCabinet',\n POPUP_ALERT_INVALID_CONFIG_LOW_SECTION: 'popupAlertInvalidConfigLowSection',\n POPUP_ALERT_INVALID_CONFIG_MISPLACED: 'popupAlertInvalidConfigMisplaced',\n POPUP_ALERT_INVALID_CONFIG_PLACEMENT: 'popupAlertInvalidConfigPlacement',\n POPUP_ALERT_INVALID_CONFIG_WEIGHT: 'popupAlertInvalidConfigWeight',\n POPUP_ALERT_SUSPENSION_RAIL: 'popupAlertSuspensionRail',\n POPUP_INFORMATION_DOORS: 'popupInformationDoors',\n POPUP_MENU_HEADER_BASKETS: 'popupMenuHeaderBaskets',\n POPUP_MENU_HEADER_FRAME: 'popupMenuHeaderFrame',\n POPUP_MENU_HEADER_INSERTS: 'popupMenuHeaderInserts',\n POPUP_MENU_HEADER_SECTION: 'popupMenuHeaderSection',\n POPUP_PEGBOARD_HINT: 'popupPegboardHint',\n POSTS: 'posts',\n POST_MOUNTING_INFO: 'postMountingInfo',\n PRODUCT_INFO_SHEET: 'productInfoSheet',\n PRODUCT_INFO_SHEET_DESCRIPTION: 'productInfoSheetDescription',\n REPLACE_PREVIOUS_BODY: 'replacePreviousBody',\n REPLACE_PREVIOUS_HEADING: 'replacePreviousHeading',\n REPLACE_PREVIOUS_PRIMARY: 'replacePreviousPrimary',\n REPLACE_PREVIOUS_SECONDARY: 'replacePreviousSecondary',\n SAFETY_AND_COMPLIANCE_DESCRIPTION: 'safetyAndComplianceDescription',\n SCREENSAVER_TEXT: 'screensaverText',\n SELECT_BASE: 'selectBase',\n SIDE_UNITS: 'sideUnits',\n SIDE_UNIT_INFO: 'sideUnitInfo',\n SPLASH_SCREEN_STEP_FOUR_DESCRIPTION: 'splashScreenStepFourDescription',\n TOASTER_ARTICLE_EXCHANGED: 'toasterArticleExchanged',\n TOASTER_PEGBOARD_INFO: 'toasterPegboardInfo',\n TOASTER_PEGBOARD_KIOSK: 'toasterPegboardKiosk',\n TOASTER_RESET_WALL: 'toasterResetWall',\n TOAST_ADD_SHELVES: 'toastAddShelves',\n TOAST_MULTIPACK_INFO: 'toastMultipackInfo',\n TOOLTIP_ADD_CABINET: 'tooltipAddCabinet',\n TOOLTIP_ADD_CASTORS: 'tooltipAddCastors',\n TOOLTIP_ADD_COVER: 'tooltipAddCover',\n TOOLTIP_ANTHRACITE: 'tooltipAnthracite',\n TOOLTIP_BAMBOO: 'tooltipBamboo',\n TOOLTIP_BLACK: 'tooltipBlack',\n TOOLTIP_BLACKBROWN: 'tooltipBlackbrown',\n TOOLTIP_BLACK_MESH: 'tooltipBlackMesh',\n TOOLTIP_DARK_GREY: 'tooltipDarkGrey',\n TOOLTIP_FILTER_HINT: 'tooltipFilterHint',\n TOOLTIP_GREEN_MESH: 'tooltipGreenMesh',\n TOOLTIP_GREY: 'tooltipGrey',\n TOOLTIP_GREY_GREEN: 'tooltipGreyGreen',\n TOOLTIP_GREY_GREEN_MESH: 'tooltipGreyGreenMesh',\n TOOLTIP_GREY_MESH: 'tooltipGreyMesh',\n TOOLTIP_LIGHT_BLUE: 'tooltipLightBlue',\n TOOLTIP_MESH: 'tooltipMesh',\n TOOLTIP_MESH_ANTHRACITE: 'tooltipMeshAnthracite',\n TOOLTIP_METAL: 'tooltipMetal',\n TOOLTIP_OAK_PATTERNED: 'tooltipOakPatterned',\n TOOLTIP_PINE: 'tooltipPine',\n TOOLTIP_RED: 'tooltipRed',\n TOOLTIP_REDO: 'tooltipRedo',\n TOOLTIP_REMOVE_CABINET: 'tooltipRemoveCabinet',\n TOOLTIP_REMOVE_CASTORS: 'tooltipRemoveCastors',\n TOOLTIP_REMOVE_COVER: 'tooltipRemoveCover',\n TOOLTIP_TEXTILE: 'tooltipTextile',\n TOOLTIP_TYPE_MESH: 'tooltipTypeMesh',\n TOOLTIP_TYPE_PARTICLEBOARD: 'tooltipTypeParticleboard',\n TOOLTIP_TYPE_WIRE: 'tooltipTypeWire',\n TOOLTIP_UNDO: 'tooltipUndo',\n TOOLTIP_WALL_MEASUREMENTS: 'tooltipWallMeasurements',\n TOOLTIP_WHITE: 'tooltipWhite',\n TOOLTIP_WHITE_OAK: 'tooltipWhiteOak',\n TOOLTIP_WIRE_SHELF: 'tooltipWireShelf',\n TOOLTIP_WOOD: 'tooltipWood',\n TYPE: 'type',\n USE_POSTS: 'usePosts',\n USE_SIDE_UNITS: 'useSideUnits',\n};\n\nexport const t: ITranslationMappings = {\n ...kompisTranslationMappings,\n ...storageTwoTranslationMappings,\n};\n","import type { IKompisTranslations } from '@inter-ikea-kompis/types';\nimport { applicationSettings } from '../settings/application';\nimport { t } from '../translations';\nimport store from '../state';\nimport { selectFlattenedTranslations } from '../state/translations/translationsSelectors';\nimport type { IStorageTwoTranslations } from '../state/translations/translationsTypes';\n\nconst replaceVariables = (translation: string): string =>\n translation\n .replaceAll(\n '{{applicationname}}',\n () => applicationSettings.applicationName\n )\n .replaceAll('{{tip_over_warning_link}}', () =>\n translate(t.TIP_OVER_WARNING_LINK)\n );\n\nexport const translate = (\n key: keyof (IKompisTranslations & IStorageTwoTranslations)\n): string => {\n const translation = selectFlattenedTranslations(store.getState())?.[key];\n if (!translation) return key;\n\n return replaceVariables(translation);\n};\n","import { translate } from '../L10n';\nimport { t } from '../../translations';\nimport productService from './index';\nconst articles = {};\n\nfunction getSections() {\n const cm = translate(t.MEASURE_VALUE_CM);\n return {\n section_84x54x190: {\n measureReference: {\n textMetric: `190x85 ${cm}`,\n textImperial: '74 3/4x33 1/2 \"',\n },\n },\n section_84x39x190: {\n measureReference: {\n textMetric: `190x85 ${cm}`,\n textImperial: '74 3/4x33 1/2 \"',\n },\n },\n section_64x54x190: {\n measureReference: {\n textMetric: `190x65 ${cm}`,\n textImperial: '74 3/4x25 5/8 \"',\n },\n },\n section_64x39x190: {\n measureReference: {\n textMetric: `190x65 ${cm}`,\n textImperial: '74 3/4x25 5/8 \"',\n },\n },\n section_84x54x110: {\n measureReference: {\n textMetric: `110x85 ${cm}`,\n textImperial: '43 1/4x33 1/2 \"',\n },\n },\n section_84x39x110: {\n measureReference: {\n textMetric: `110x85 ${cm}`,\n textImperial: '43 1/4x33 1/2 \"',\n },\n },\n section_64x54x110: {\n measureReference: {\n textMetric: `110x65 ${cm}`,\n textImperial: '43 1/4x25 5/8 \"',\n },\n },\n section_64x39x110: {\n measureReference: {\n textMetric: `110x65 ${cm}`,\n textImperial: '43 1/4x25 5/8 \"',\n },\n },\n section_84x54x190_white: {\n measureReference: {\n textMetric: `190x85 ${cm}`,\n textImperial: '74 3/4x33 1/2 \"',\n },\n },\n section_84x39x190_white: {\n measureReference: {\n textMetric: `190x85 ${cm}`,\n textImperial: '74 3/4x33 1/2 \"',\n },\n },\n section_64x54x190_white: {\n measureReference: {\n textMetric: `190x65 ${cm}`,\n textImperial: '74 3/4x25 5/8 \"',\n },\n },\n section_64x39x190_white: {\n measureReference: {\n textMetric: `190x65 ${cm}`,\n textImperial: '74 3/4x25 5/8 \"',\n },\n },\n section_84x54x110_white: {\n measureReference: {\n textMetric: `110x85 ${cm}`,\n textImperial: '43 1/4x33 1/2 \"',\n },\n },\n section_84x39x110_white: {\n measureReference: {\n textMetric: `110x85 ${cm}`,\n textImperial: '43 1/4x33 1/2 \"',\n },\n },\n section_64x54x110_white: {\n measureReference: {\n textMetric: `110x65 ${cm}`,\n textImperial: '43 1/4x25 5/8 \"',\n },\n },\n section_64x39x110_white: {\n measureReference: {\n textMetric: `110x65 ${cm}`,\n textImperial: '43 1/4x25 5/8 \"',\n },\n },\n };\n}\n\nexport function setArticles(newArticles) {\n for (const article of newArticles) {\n articles[article.content.ruItemNo] = article;\n articles[article.content.itemNoGlobal] = article;\n }\n}\n\nfunction getArticle(id) {\n // drop trailing 'b' from product variants to enable article matching\n if (/^\\d+\\w$/.test(id)) {\n id = id.replace(/b$/, '');\n }\n\n // drop leading 'S' from product ids to enable spr matching\n id = id.replace(/^S(?=\\d)/i, '');\n\n if (articles[id]) {\n return articles[id];\n }\n\n const sections = getSections();\n if (sections[id]) {\n return sections[id];\n }\n\n return {};\n}\n\nexport const getArticleContent = id => {\n return getArticle(id)?.content;\n};\n\nexport const findArticle = id => {\n let art = getArticleContent(id);\n const includedArticles = productService.getIncludedArticles(id);\n while (includedArticles.length && (!art || !art.measureReference)) {\n art = getArticleContent(includedArticles.shift());\n }\n\n return art;\n};\n\nexport default {\n getArticle,\n getArticleContent,\n};\n","export const MM_PER_INCH = 25.4;\n\nexport const fractionLigatures = {\n '': '', // not a real ligature, used for conversion (implicit round down)\n '1/8': ' ⅛',\n '1/4': ' ¼',\n '3/8': ' ⅜',\n '1/2': ' ½',\n '5/8': ' ⅝',\n '3/4': ' ¾',\n '7/8': ' ⅞',\n 'round up!': 'round up!', // not a real ligature, used for conversion\n};\n\nexport function metricToImperial(mm) {\n const out = {\n feet: '0',\n inches: '0',\n onlyInches: '0',\n };\n\n let i_wholeInches = (mm / MM_PER_INCH) << 0;\n let m_inchRest = mm - i_wholeInches * MM_PER_INCH;\n\n let i_inchRestAsFraction =\n Object.values(fractionLigatures)[\n Math.round((m_inchRest / MM_PER_INCH) * 8)\n ];\n\n if (i_inchRestAsFraction === 'round up!') {\n m_inchRest = 0; // discard since we rounded up\n i_inchRestAsFraction = '';\n i_wholeInches++;\n }\n\n const i_feet = (i_wholeInches / 12) << 0;\n const i_feetRest = i_wholeInches % 12;\n\n out.feet = '' + i_feet;\n out.inches = '' + i_feetRest + i_inchRestAsFraction;\n out.onlyInches = '' + i_wholeInches + i_inchRestAsFraction;\n\n return out;\n}\n\nexport function withLigatures(inches) {\n return inches.replace(/\\d+\\/\\d+/g, fraction => fractionLigatures[fraction]);\n}\n\nexport function asMm(values) {\n if (values.imp) {\n return MM_PER_INCH * (12 * values.major + 1 * values.minor);\n }\n return 10 * values.minor;\n}\n\nexport const mm = {\n toCm: val => val / 10,\n toDm: val => val / 100,\n toM: val => val / 1000,\n toInches: val => Math.round(val * 0.0393700787),\n};\n\nexport const cm = {\n toMm: val => val * 10,\n toDm: val => val / 10,\n toM: val => val / 100,\n toInches: val => Math.round(val * 0.393700787),\n};\n\nexport const dm = {\n toMm: val => val * 100,\n toCm: val => val * 10,\n toM: val => val / 10,\n toInches: val => Math.round(val * 3.93700787),\n};\n\nexport const m = {\n toMm: val => val * 1000,\n toCm: val => val * 100,\n toDm: val => val * 10,\n toInches: val => Math.round(val * 39.3700787),\n};\n\nexport const inches = {\n toMm: val => Math.round(val * 25.4),\n toCm: val => Math.round(val * 2.54),\n toDm: val => Math.round(val * 0.254),\n toM: val => Math.round(val * 0.0254),\n};\n","import productService from '.';\nimport articles from './articles';\nimport constants from '../../settings/constants';\nimport { ITEMS, RANGES, UNIT_CONVERSIONS, UNITS } from '../../constants';\nimport { metricToImperial } from '../../util/measures';\n\nconst products = [];\n\n/**\n * Gets the sprite item type for a specific product\n * @param {*} product\n * @returns {String}\n */\nfunction calcType(product) {\n if (product.id.indexOf(ITEMS.SECTION) === 0) {\n return ITEMS.SECTION;\n }\n if (\n [\n ITEMS.SHELF,\n ITEMS.METAL_SHELF,\n ITEMS.WIRE_SHELF,\n ITEMS.FELT_SHELF,\n ITEMS.ADD_ON,\n ITEMS.DIVIDER,\n ITEMS.FRAME,\n ITEMS.SHELVING_UNIT,\n ITEMS.LEG,\n ITEMS.CLOTHES_RAIL,\n ITEMS.COVER,\n ITEMS.TOP_SHELF,\n ITEMS.UPRIGHT,\n ITEMS.DRYING_RACK,\n ITEMS.SHOE_SHELF,\n ITEMS.TROUSER_HANGER,\n ITEMS.MOUNTING_RAIL,\n ITEMS.SIDEWALL,\n ITEMS.DUMMY,\n ITEMS.TABLE,\n ITEMS.BOTTLE_RACK,\n ITEMS.SHELF_DRAWER,\n ITEMS.DRAWER,\n ITEMS.DOOR,\n ITEMS.SHELF_CLOTHES_RAIL,\n ].some(type => type === product.filter.type)\n ) {\n return product.filter.type;\n }\n return ITEMS.ITEM;\n}\n\nfunction addModelData(products, models) {\n function roundBounds(product) {\n if (models[product.modelid]) {\n const bounds = models[product.modelid].bounds.default.size;\n return Object.entries(bounds).reduce((out, [dim, val]) => {\n out[dim] = Math.round(val);\n return out;\n }, {});\n }\n return {\n depth: 0,\n height: 0,\n width: 0,\n };\n }\n\n function getLogic(product) {\n if (models[product.modelid]) {\n return models[product.modelid].logic;\n }\n return {};\n }\n return products.map(function (prod) {\n return {\n ...prod,\n bounds: roundBounds(prod),\n logic: getLogic(prod),\n };\n });\n}\n\nexport function setProducts(loadedFiles) {\n const productData = loadedFiles.find(chunk =>\n chunk.hasOwnProperty('products')\n );\n const modelData = loadedFiles.find(chunk => chunk.hasOwnProperty('models'));\n\n const modelEnrichedProducts = addModelData(\n productData.products,\n modelData.models\n );\n\n products.length = 0;\n products.push(\n ...modelEnrichedProducts.map(function (prod) {\n return {\n depth: prod.bounds.depth,\n height: prod.bounds.height,\n width: prod.bounds.width,\n id: prod.id,\n modelid: prod.modelid,\n type: calcType(prod),\n iows: prod.iows,\n name: prod.itemname,\n logic: prod.logic,\n filter: prod.filter,\n parts: prod.parts,\n sprno: prod.sprno,\n };\n })\n );\n productService.init();\n}\n\nfunction validateSections() {\n // ditch any sections that don't have matching shelves\n productService.getSections().forEach(function hasShelves(section) {\n const valid = productService\n .getSectionInserts()\n .some(function sameFootprint(shelf) {\n const offset = productService.getSectionOffset(shelf);\n return (\n shelf.filter.depth + offset.x * 2 === section.filter.depth &&\n shelf.filter.width + offset.z * 2 === section.filter.width\n );\n });\n section.valid = valid;\n if (!valid) {\n console.log('invalidated section', section, 'due to missing shelves');\n }\n });\n}\n\n/**\n * Returns measurement according to (S)ystem (o)f (M)easurement\n *\n * @param useMetricMeasures {boolean}\n * @param val {number}\n * @param unit {string}\n * @returns {number|string}\n */\nexport const getMeasurementBySoM = (useMetricMeasures, val, unit = UNITS.cm) =>\n useMetricMeasures\n ? Math.ceil(val / UNIT_CONVERSIONS[unit])\n : metricToImperial(val).onlyInches;\n\nexport function validateProducts() {\n // first flag anything missing parts as invalid\n products.forEach(function validate(product) {\n product.valid =\n product.iows\n .map(iow => iow.itemno)\n .every(itemno => articles.getArticleContent(itemno)?.ruItemNo) &&\n (!product.sprno || articles.getArticleContent(product.sprno)?.ruItemNo);\n });\n\n function requireAllSectionPartsToBeValid() {\n return productService.getSections().forEach(section => {\n section.valid = Object.values(section.parts).every(\n part => productService.getProduct(part)?.valid\n );\n });\n }\n\n function requireAllShelfDrawerPartsToBeValid() {\n const isShelfDrawer = item =>\n productService.isType(item, ITEMS.SHELF_DRAWER);\n const getParts = item => Object.values(item.parts);\n return productService\n .getAll()\n .filter(isShelfDrawer)\n .forEach(shelfDrawer => {\n shelfDrawer.valid = getParts(shelfDrawer).every(\n part => productService.getProduct(part)?.valid\n );\n });\n }\n\n switch (constants.APPLICATION_NAME) {\n case 'BROR':\n validateSections();\n\n // remove cabinet if nowhere to fit\n const cabinets = productService\n .getAll()\n .filter(productService.isCabinet)\n .filter(productService.isInsert);\n\n cabinets.forEach(function (cabinet) {\n cabinet.valid = productService\n .getSections()\n .some(function fitsCabinet(section) {\n const offset = productService.getSectionOffset(cabinet);\n\n return (\n cabinet.filter.depth + offset.z * 2 === section.filter.depth &&\n cabinet.filter.width + offset.x * 2 === section.filter.width\n );\n });\n });\n break;\n case RANGES.BOAXEL:\n validateSections();\n break;\n case RANGES.AURDAL:\n //Require all parts for a section to be valid\n requireAllSectionPartsToBeValid();\n break;\n case RANGES.IVAR:\n const regularAndKnockdownSideUnitPairs = [\n {\n regular: 'side_unit_5x30x124_back_main', // 73755709\n knockdown: 'side_unit_5x30x124_back_main_knock_down', // 90484649\n },\n {\n regular: 'side_unit_5x50x124_back_main', // 53755809\n knockdown: 'side_unit_5x50x124_back_main_knock_down', // 10484653\n },\n {\n regular: 'side_unit_5x30x179_back_main', // 83756609\n knockdown: 'side_unit_5x30x179_back_main_knock_down', // 50484651\n },\n {\n regular: 'side_unit_5x50x179_back_main', // 63756709\n knockdown: 'side_unit_5x50x179_back_main_knock_down', // 90484654\n },\n {\n regular: 'side_unit_5x30x226_back_main', // 57489509\n knockdown: 'side_unit_5x30x226_back_main_knock_down', // 30484652\n },\n {\n regular: 'side_unit_5x50x226_back_main', // 87489409\n knockdown: 'side_unit_5x50x226_back_main_knock_down', // 60484655\n },\n ];\n regularAndKnockdownSideUnitPairs.forEach(({ regular, knockdown }) => {\n const regularProduct = productService.getProduct(regular);\n const knockdownProduct = productService.getProduct(knockdown);\n\n // If both variants are available, we use the knockdown one.\n if (regularProduct) regularProduct.valid = !knockdownProduct?.valid;\n });\n\n productService.getSections().forEach(section => {\n // Filter out only the parts available on the market\n section.parts = Object.fromEntries(\n Object.entries(section.parts).filter(\n ([key, value]) => productService.getProduct(value)?.valid\n )\n );\n\n // An IVAR section needs two side-panels and one cross-brace to be valid\n section.valid =\n Object.values(section.parts).filter(part => {\n const product = productService.getProduct(part);\n return (\n product?.iows.length &&\n productService.isType(product, [\n ITEMS.SIDE_PANEL,\n ITEMS.CROSS_BRACE,\n ])\n );\n }).length === 3;\n });\n break;\n case RANGES.ELVARLI:\n requireAllSectionPartsToBeValid();\n requireAllShelfDrawerPartsToBeValid();\n break;\n default:\n break;\n }\n}\n\nexport function flagSuperfluousProductsAsInvalid() {\n switch (constants.APPLICATION_NAME) {\n case RANGES.BOAXEL:\n const mountingRails = productService.getFilteredItems(item =>\n productService.isType(item, ITEMS.MOUNTING_RAIL)\n );\n const mountingRailColors =\n productService.getColorsOfProducts(mountingRails);\n\n mountingRailColors.forEach(mountingRailColor => {\n if (\n !productService.areMountingRailsOfSpecificColorValid(\n mountingRailColor\n )\n )\n mountingRails.forEach(mountingRail => {\n if (mountingRail.filter.color === mountingRailColor)\n mountingRail.valid = false;\n });\n });\n\n const oakShelf60Old = productService.getProduct('20448754');\n const oakShelf80Old = productService.getProduct('50448757');\n const oakShelf60New = productService.getProduct('80576079');\n const oakShelf80New = productService.getProduct('60576080');\n\n const invalidateOldOakShelves = () => {\n oakShelf60Old && (oakShelf60Old.valid = false);\n oakShelf80Old && (oakShelf80Old.valid = false);\n };\n\n const invalidateNewOakShelves = () => {\n oakShelf60New && (oakShelf60New.valid = false);\n oakShelf80New && (oakShelf80New.valid = false);\n };\n\n if (oakShelf60New && oakShelf80New) {\n invalidateOldOakShelves();\n } else if (oakShelf60Old && oakShelf80Old) {\n invalidateNewOakShelves();\n } else if (oakShelf60New || oakShelf80New) {\n /* Even if the market has a \"complete\" set of oak colored shelves consisting\n of an old shelf of one size and a new shelf of the other size, mixing them\n might not be a good idea, since they look the same in the planner, but\n seem to differ slightly in color in reality. */\n invalidateOldOakShelves();\n }\n\n break;\n case RANGES.AURDAL:\n // If one of our mandatory products are missing for one of the colors,\n // mark all products of that color as invalid\n ['white', 'dark_grey'].forEach(color => {\n if (!productService.hasAllMandatoryProductsOfColor(color)) {\n const productsOfColor = productService.getFilteredItems(Boolean, {\n color,\n });\n productsOfColor.forEach(product => {\n product.valid = false;\n });\n }\n });\n break;\n case RANGES.ELVARLI:\n const getSectionsPostsOfDepth = depth =>\n productService\n .getSections()\n .filter(\n section =>\n productService.isType(section, ITEMS.SECTION_POSTS) &&\n section.depth === depth\n );\n const filterOnPresentAndValid = productIds => {\n const allProducts = productService.getAll();\n const presentAndValidIds = productIds.filter(id =>\n allProducts.map(product => product.id).includes(id)\n );\n return presentAndValidIds;\n };\n const invalidateProducts = products =>\n products.forEach(product => (product.valid = false));\n\n const insertsRequiring360 = [\n '60313282', // Shelf, white, 40x36\n '20313284', // Shelf, white, 80x36\n '90319268', // Shelf, bamboo, 40x36\n '70319269', // Shelf, bamboo, 80x36\n 'shelf_drawer_40_36_white',\n 'shelf_drawer_80_36_white',\n 'shelf_drawer_40_36_bamboo',\n 'shelf_drawer_80_36_bamboo',\n '60313282_shelf_clothes_rail_40_36',\n '20313284_shelf_clothes_rail_80_36',\n '90319268_shelf_clothes_rail_40_36',\n '70319269_shelf_clothes_rail_80_36',\n ];\n\n const insertsRequiring510 = [\n '60296174', // Shelf, white, 40x51\n '10296176', // Shelf, white, 80x51\n '20296289', // Shelf, bamboo, 40x51\n '80296291', // Shelf, bamboo, 80x51\n 'shelf_drawer_40_51_white',\n 'shelf_drawer_80_51_white',\n 'shelf_drawer_40_51_bamboo',\n 'shelf_drawer_80_51_bamboo',\n '60296174_shelf_clothes_rail_40_51',\n '10296176_shelf_clothes_rail_80_51',\n '20296289_shelf_clothes_rail_40_51',\n '80296291_shelf_clothes_rail_80_51',\n ];\n\n const insertsFittingBoth = [\n '10317292', // Shoe shelf, white, 40x36\n '50317290', // Shoe shelf, white, 80x36\n '40296212', // Clothes rail, white, 40\n '00296214', // Clothes rail, white, 80\n ];\n\n /* Only need to consider left brackets,\n since IOWS doesn't distinguish between left/right brackets. */\n const bracket360 = '00317527_L';\n const bracket510 = '00296172_L';\n\n const sectionsPosts360 = getSectionsPostsOfDepth(360);\n const sectionsPosts510 = getSectionsPostsOfDepth(510);\n const foundAnInsertRequiring360 =\n !!filterOnPresentAndValid(insertsRequiring360).length;\n const foundAnInsertRequiring510 =\n !!filterOnPresentAndValid(insertsRequiring510).length;\n const foundAnInsertFittingBoth =\n !!filterOnPresentAndValid(insertsFittingBoth).length;\n const foundBracket360 = !!filterOnPresentAndValid([bracket360]).length;\n const foundBracket510 = !!filterOnPresentAndValid([bracket510]).length;\n\n let wantToInvalidateSectionsPosts360 =\n !foundAnInsertRequiring360 || !foundBracket360;\n const wantToInvalidateSectionsPosts510 =\n !foundAnInsertRequiring510 || !foundBracket510;\n\n /* Corner case: If sections of both depths are to be invalidated and we have\n an insert without depth preference, we wouldn't have any post sections left\n where this insert can be placed, so we keep the 360 sections after all. */\n if (\n wantToInvalidateSectionsPosts360 &&\n wantToInvalidateSectionsPosts510 &&\n foundAnInsertFittingBoth\n )\n wantToInvalidateSectionsPosts360 = false;\n\n wantToInvalidateSectionsPosts360 && invalidateProducts(sectionsPosts360);\n wantToInvalidateSectionsPosts510 && invalidateProducts(sectionsPosts510);\n break;\n default:\n break;\n }\n}\n\nexport function getMissingMandatoryProductCategories() {\n return productService.getMissingMandatoryProductCategories();\n}\n\nexport function __test__flagAllAsValid() {\n products.forEach(function validate(product) {\n product.valid = true;\n });\n}\n\nexport default products;\n","function match(item, key, value) {\n if (value === undefined) {\n return true;\n }\n if (value.min && value.max) {\n // supplied value is a range\n return item.filter[key] <= value.max && item.filter[key] >= value.min;\n }\n\n if (item.logic[key] !== undefined) {\n return item.logic[key] === value;\n }\n\n return item.filter[key] === value;\n}\n\nexport function filter(array, ...filters) {\n if (!Array.isArray(filters) || filters.length === 0) {\n return array.slice();\n }\n\n return array.filter(item => {\n return filters.some(filter => {\n const entries = Object.entries(filter);\n\n return entries.every(([key, value]) => match(item, key, value));\n });\n });\n}\n","export function articleNo(str) {\n const match = str.match(/^S?\\d+/);\n if (match && match[0]) {\n return match[0];\n }\n}\n","export function unique(value, index, self) {\n return self.indexOf(value) === index;\n}\n\nexport function flatten(array) {\n return Array.prototype.concat.apply([], array);\n}\n","function round(num, nearest) {\n if (nearest < 1) {\n const decimals = 1 / nearest;\n\n return Math.round(num * decimals) / decimals;\n }\n return Math.round(num / nearest) * nearest;\n}\n\nfunction floor(num, nearest) {\n if (nearest < 1) {\n const decimals = 1 / nearest;\n\n return Math.floor(num * decimals) / decimals;\n }\n return Math.floor(num / nearest) * nearest;\n}\n\nfunction ceil(num, nearest) {\n if (nearest < 1) {\n const decimals = 1 / nearest;\n\n return Math.ceil(num * decimals) / decimals;\n }\n\n return Math.ceil(num / nearest) * nearest;\n}\n\nexport { round, floor, ceil };\n","import polybool from 'polybooljs';\n\nimport { round } from './round';\nimport constants from '../settings/constants';\n\nexport function roundVertices(polygon) {\n return {\n ...polygon,\n regions: polygon.regions.map(region =>\n region.map(vertex =>\n vertex.map(point => round(point, constants.VERTEX_ROUNDING_PRECISION))\n )\n ),\n };\n}\n\nexport function mergePolygons(poly1, poly2) {\n poly1 = roundVertices(poly1);\n poly2 = roundVertices(poly2);\n return polybool.union(poly1, poly2);\n}\n","import polybool from 'polybooljs';\nimport classifyPoint from 'robust-point-in-polygon';\nimport { roundVertices } from '../../util/mergePolygons';\n\nfunction distance(p1, p2) {\n return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));\n}\n\nfunction inflate(obj, size = 0) {\n const inflated = { ...obj };\n inflated.height = inflated.height || size;\n inflated.width = inflated.width || size;\n inflated.depth = inflated.depth || size;\n return inflated;\n}\n\nfunction as3d(obj) {\n return Object.assign(\n { x: 0, y: 0, z: 0, height: 0, width: 0, depth: 0 },\n obj\n );\n}\n\nfunction getCenter(box) {\n return {\n x: box.x + box.width / 2,\n y: box.y + box.height / 2,\n z: box.z + box.depth / 2,\n };\n}\n\nfunction nanTo0(obj) {\n const nonans = Object.keys(obj).reduce((out, key) => {\n out[key] = Number.isFinite(obj[key]) ? obj[key] : 0;\n return out;\n }, {});\n return Object.assign({}, obj, nonans);\n}\n\nfunction fixPadding(padding) {\n const defaultPadding = {\n left: 0,\n right: 0,\n top: 0,\n bottom: 0,\n front: 0,\n back: 0,\n };\n return nanTo0(Object.assign(defaultPadding, padding));\n}\n\nfunction pad(box, padding) {\n padding = fixPadding(padding);\n\n const padded = {\n x: box.x - padding.left,\n y: box.y - padding.bottom,\n z: box.z - padding.back,\n width: box.width + padding.left + padding.right,\n height: box.height + padding.bottom + padding.top,\n depth: box.depth + padding.back + padding.front,\n };\n return nanTo0(padded);\n}\n\nfunction collides(r1, r2, padding) {\n if (padding) {\n r1 = pad(r1, padding);\n }\n\n const collides2d =\n r1.x < r2.x + r2.width &&\n r1.x + r1.width > r2.x &&\n r1.y < r2.y + r2.height &&\n r1.y + r1.height > r2.y;\n\n // only look at depth when all the input seems to support it\n // if (r1.z || r2.z) is false, then both have z=0 and the 2d collision has caught it anyway\n if ((r1.z || r2.z) && r1.depth && r2.depth) {\n return collides2d && r1.z < r2.z + r2.depth && r1.z + r1.depth > r2.z;\n }\n return collides2d;\n}\n\nfunction getCollidingRects(currentRect, rects, padding) {\n return rects.filter(rect => collides(currentRect, rect, padding));\n}\n\n// check if container contains rect\n// rect can also be a point or line\nfunction contains(container, box, padding) {\n container = pad(container, padding);\n container = as3d(container);\n box = as3d(box);\n const result =\n box.x >= container.x &&\n box.x + (box.width || 0) <= container.x + container.width &&\n box.y >= container.y &&\n box.y + (box.height || 0) <= container.y + container.height &&\n box.z >= container.z &&\n box.z + (box.depth || 0) <= container.z + container.depth;\n\n return result;\n}\n\nfunction fits(slot, item) {\n return (\n (!item.logic.floor || item.logic.wall || slot.y <= 0) &&\n slot.width >= item.width &&\n slot.height >= item.height\n );\n}\n\nfunction splitRectangle(outer, inner) {\n const rectangles = [];\n\n if (inner.y + inner.height < outer.y + outer.height) {\n // top rectangle\n rectangles.push({\n width: outer.width,\n height: outer.y + outer.height - (inner.y + inner.height),\n x: outer.x,\n y: inner.y + inner.height,\n });\n }\n\n if (inner.x + inner.width < outer.x + outer.width) {\n // right rectangle\n rectangles.push({\n width: outer.x + outer.width - (inner.x + inner.width),\n height: outer.height,\n x: inner.x + inner.width,\n y: outer.y,\n });\n }\n\n if (inner.y > outer.y) {\n // bottom rectangle\n rectangles.push({\n width: outer.width,\n height: inner.y - outer.y,\n x: outer.x,\n y: outer.y,\n });\n }\n\n if (inner.x > outer.x) {\n // left rectangle\n rectangles.push({\n width: inner.x - outer.x,\n height: outer.height,\n x: outer.x,\n y: outer.y,\n });\n }\n\n return rectangles;\n}\n\nfunction sliceRectangle(space, rects) {\n return rects.reduce(\n (slots, rect) => {\n const collidingRects = getCollidingRects(rect, slots);\n\n slots = slots.filter(slot => !collidingRects.includes(slot));\n\n collidingRects.forEach(rectangle => {\n let newSlots = splitRectangle(rectangle, rect);\n\n newSlots = newSlots.filter(newSlot =>\n slots.every(slot => !contains(slot, newSlot))\n );\n\n slots.push(...newSlots);\n });\n\n return slots;\n },\n [space]\n );\n}\n\n/*\n since some colliding areas don't actually collide,\n but the snap padding makes it appear so,\n any bad collisions yield a negative closeness\n*/\nfunction weightedCloseness(intersection, rect, target) {\n let intersectionArea = Math.abs(intersection.height * intersection.width);\n if (intersection.height < 0 || intersection.width < 0) {\n intersectionArea *= -1;\n }\n const rectArea = rect.height * rect.width;\n const targetArea = target.height * target.width;\n // care mainly about how much of rect that intersects with target,\n // and secondly give a slight weight to how much of target that intersects with rect\n const intersectionRatio =\n (intersectionArea / rectArea) * (1 + rectArea / (targetArea * 10000));\n return intersectionRatio;\n}\n\nfunction connectingRect(r1, r2) {\n const x = Math.min(r1.x, r2.x);\n const y = Math.max(r1.y, r2.y);\n const xx = Math.max(r1.x + r1.width, r2.x + r2.width);\n const yy = Math.min(r1.y + r1.height, r2.y + r2.height);\n return {\n x: x,\n y: y,\n width: xx - x,\n height: yy - y,\n };\n}\n\nfunction intersectingRect(r1, r2) {\n const x = Math.max(r1.x, r2.x);\n const y = Math.max(r1.y, r2.y);\n const xx = Math.min(r1.x + r1.width, r2.x + r2.width);\n const yy = Math.min(r1.y + r1.height, r2.y + r2.height);\n return {\n x: x,\n y: y,\n width: xx - x,\n height: yy - y,\n };\n}\n\nfunction closer(cand, curr) {\n return curr < cand;\n}\n\n/*\n when determining what rect is most colliding,\n consider the overlapping area of two rects a measure of collision\n*/\nfunction closestCollidingRect(target, rects, fill = 0) {\n if (rects.length === 1) {\n return rects[0];\n }\n let closest = null;\n return rects.reduce((result, rect) => {\n const intersection = intersectingRect(target, inflate(rect, fill));\n const closeness = weightedCloseness(intersection, rect, target);\n if (!result || closer(closeness, closest)) {\n closest = closeness;\n return rect;\n }\n return result;\n }, null);\n}\n\nfunction closestPositionInSlot(rect, slot) {\n let x, y;\n\n if (slot.x >= rect.x) {\n x = slot.x;\n } else if (rect.x + rect.width > slot.x + slot.width) {\n x = slot.x + slot.width - rect.width;\n } else {\n x = rect.x;\n }\n\n if (slot.y > rect.y) {\n y = slot.y;\n } else if (rect.y + rect.height > slot.y + slot.height) {\n y = slot.y + slot.height - rect.height;\n } else {\n y = rect.y;\n }\n\n return { x, y };\n}\n\nfunction closestPosition(rect, slots) {\n let shortestDistance;\n\n return slots.reduce((result, slot) => {\n const position = closestPositionInSlot(rect, slot);\n const dist = distance(position, rect);\n\n if (!result || dist < shortestDistance) {\n shortestDistance = dist;\n\n return position;\n }\n\n return result;\n }, null);\n}\n\nfunction extendsOutside(child, parent, axis) {\n if (axis) {\n const sizeProperty = axis === 'x' ? 'width' : 'height';\n return (\n child[axis] < 0 ||\n child[axis] + child[sizeProperty] > parent[sizeProperty]\n );\n }\n\n return (\n child.x < 0 ||\n child.x + child.width > parent.width ||\n child.y < 0 ||\n child.y + child.height > parent.height\n );\n}\n\nfunction surround(boxes) {\n const boxes3d = boxes.map(as3d);\n const minX = Math.min(...boxes3d.map(box => box.x));\n const minY = Math.min(...boxes3d.map(box => box.y));\n const minZ = Math.min(...boxes3d.map(box => box.z));\n return {\n x: minX,\n y: minY,\n z: minZ,\n width: Math.max(...boxes3d.map(box => box.x + (box.width || 0))) - minX,\n height: Math.max(...boxes3d.map(box => box.y + (box.height || 0))) - minY,\n depth: Math.max(...boxes3d.map(box => box.z + box.depth)) - minZ,\n };\n}\n\nfunction mergeKids(parent, children) {\n // fake parent at 0,0\n const fake = {\n x: 0,\n y: 0,\n z: 0,\n width: parent.width,\n height: parent.height,\n depth: parent.depth,\n };\n\n const all = [fake, ...children.filter(child => !contains(parent, child))];\n const out = surround(all);\n\n // if parent isn't placed, assume {0,0}\n out.x += parent.x || 0;\n out.y += parent.y || 0;\n out.z += parent.z || 0;\n return out;\n}\n\nfunction mergeSiblings(s1, s2) {\n const all = [s1, s2];\n return surround(all);\n}\n\n// +-------------------+ +-------------------+\n// | r2 | | r2 |\n// +-------------------+ === true +-------------------+ === false\n// |---------------| |---------------|\n// | r1 | | r1 |\n// +---------------+ +---------------+\nfunction eclipsed(r1, r2) {\n return (\n r1.y + r1.height < r2.y &&\n r1.x >= r2.x &&\n r1.x + r1.width <= r2.x + r2.width\n );\n}\n\nfunction closest(items, direction = 'right') {\n function closer(a, b) {\n return a.x > b.x ? 1 : a.x < b.x ? -1 : 0;\n }\n if (direction === 'right') {\n return items.sort(closer)[0];\n }\n return items.sort(closer).reverse()[0];\n}\n\nfunction uniformPadding(size) {\n return {\n top: size,\n bottom: size,\n left: size,\n right: size,\n front: size,\n back: size,\n };\n}\n\nfunction rect2poly(rect) {\n return {\n inverted: false,\n regions: [\n [\n [rect.x, rect.y],\n [rect.x, rect.y + rect.height],\n [rect.x + rect.width, rect.y + rect.height],\n [rect.x + rect.width, rect.y],\n ],\n ],\n };\n}\n\nfunction rects2poly(rects) {\n const polygons = rects.map(rect2poly).map(roundVertices);\n let segments = polybool.segments(polygons[0]);\n for (let i = 1; i < polygons.length; i++) {\n const seg2 = polybool.segments(polygons[i]);\n const comb = polybool.combine(segments, seg2);\n segments = polybool.selectUnion(comb);\n }\n return polybool.polygon(segments);\n}\n\nfunction obscured(item, observer, items) {\n /*\n if there are items in items that obscure item from observers pov, it cannot be observed\n */\n\n const sharedYs = connectingRect(item, observer);\n const ignored = [item.itemid, observer.itemid];\n const obscurers = getCollidingRects(sharedYs, items).filter(\n rect => !ignored.includes(rect.itemid)\n );\n if (!obscurers.length) {\n //nothings in our path\n return false;\n }\n\n // something is between us. now we figure out if they cover our field of view or not\n let next = sharedYs.y;\n const end = next + sharedYs.height;\n obscurers.sort((a, b) => b.y - a.y); // top to bottom so we can pop\n while (obscurers.length) {\n const curr = obscurers.pop();\n if (curr.y > next) {\n // we found a hole\n return false;\n }\n const reached = curr.y + curr.height;\n if (reached > next) {\n next = reached;\n }\n if (next >= end) {\n // we've escaped our interval without holes\n return true;\n }\n }\n\n return false;\n}\n\nfunction isInsidePolygon(polygon, point) {\n return classifyPoint(polygon, point) < 1;\n}\n\nexport default {\n collides,\n contains,\n closest,\n closestCollidingRect,\n closestPosition,\n distance,\n fits,\n getCenter,\n getCollidingRects,\n connectingRect,\n intersectingRect,\n rects2poly,\n sliceRectangle,\n splitRectangle,\n extendsOutside,\n mergeKids,\n mergeSiblings,\n pad,\n surround,\n eclipsed,\n uniformPadding,\n obscured,\n isInsidePolygon,\n __test__: {\n closer,\n },\n};\n","let id = 1;\nlet fakeId = 1;\n\nexport default {\n id: () => id++,\n fakeId: () => `f_${fakeId++}`,\n hasRealId: item => Number.isInteger(item.itemid),\n};\n","import { ITEMS } from '../../../../constants';\nimport { isType } from '../../../../services/products';\nimport geometry from '../../../../scene/util/geometry';\nimport productService from '../../../../services/products';\n\nconst itemShouldIgnoreDrawer = (item, drawer, parent) => {\n const shelfCollisions = geometry.getCollidingRects(\n drawer,\n parent.items.filter(item => productService.isType(item, ITEMS.SHELF))\n );\n\n if (shelfCollisions.length) {\n return false;\n }\n\n const drawersColliding = parent.items.filter(\n item =>\n item.itemid !== drawer.itemid &&\n productService.isType(item, ITEMS.DRAWER) &&\n geometry.collides(drawer, item)\n );\n\n if (!drawersColliding.length) {\n return true;\n }\n\n const drawersCollidingAbove = drawersColliding.filter(\n other => other.y > drawer.y\n );\n const drawersCollidingBelow = drawersColliding.filter(\n other => other.y < drawer.y\n );\n\n if (\n productService.isType(item, ITEMS.SHELF) &&\n drawersCollidingAbove.length &&\n drawersCollidingBelow.length\n ) {\n return false;\n }\n\n if (drawersCollidingAbove.length && item.y < drawer.y) {\n return drawersCollidingAbove.every(drawer =>\n itemShouldIgnoreDrawer(item, drawer, {\n items: parent.items.filter(item => item.y > drawer.y),\n })\n );\n }\n\n if (drawersCollidingBelow.length && item.y > drawer.y) {\n return drawersCollidingBelow.every(drawer =>\n itemShouldIgnoreDrawer(item, drawer, {\n items: parent.items.filter(item => item.y < drawer.y),\n })\n );\n }\n\n return false;\n};\n\nconst drawerShouldIgnoreShelf = (drawer, shelf, parent) => {\n const shelfCollidingDrawers = geometry.getCollidingRects(\n shelf,\n parent.items.filter(item => productService.isType(item, ITEMS.DRAWER))\n );\n\n if (shelfCollidingDrawers.length) {\n // Shelf already collides with another drawer.\n return false;\n }\n\n const drawerCollidingShelves = geometry.getCollidingRects(\n drawer,\n parent.items.filter(item => productService.isType(item, ITEMS.SHELF))\n );\n\n if (\n drawerCollidingShelves.length > 1 &&\n drawerCollidingShelves.some(rect => rect === shelf)\n ) {\n // Drawer would collide with two shelves.\n return false;\n }\n\n return true;\n};\n\nconst drawerShouldIgnoreDrawer = (drawer, drawerSibling, parent) => {\n if (drawer.y === drawerSibling.y) {\n return false;\n }\n\n const allColliding = geometry.getCollidingRects(drawer, parent.items);\n const shelvesColliding = allColliding.filter(item =>\n productService.isType(item, ITEMS.SHELF)\n );\n const drawersColliding = allColliding.filter(item =>\n productService.isType(item, ITEMS.DRAWER)\n );\n\n if (shelvesColliding.length) {\n // Drawer collides with one shelf,\n // meaning all other colliding drawers needs to be free from shelves\n return drawersColliding.every(other =>\n itemShouldIgnoreDrawer(drawer, other, parent)\n );\n }\n\n if (drawersColliding.length > 1) {\n // Drawer collides with other drawers on both sides,\n // meaning at least one of them needs to be free of shelves.\n return drawersColliding.some(other =>\n itemShouldIgnoreDrawer(drawer, other, parent)\n );\n }\n\n return true;\n};\n\nconst shouldIgnoreCollision = (item, parent, candidate) => {\n if (\n productService.isType(item, ITEMS.SHELF) &&\n productService.isType(candidate, ITEMS.DRAWER)\n ) {\n return itemShouldIgnoreDrawer(item, candidate, parent);\n }\n\n if (\n productService.isType(item, ITEMS.DRAWER) &&\n productService.isType(candidate, ITEMS.SHELF)\n ) {\n return drawerShouldIgnoreShelf(item, candidate, parent);\n }\n\n if (\n [item, candidate].every(item => productService.isType(item, ITEMS.DRAWER))\n ) {\n return drawerShouldIgnoreDrawer(item, candidate, parent);\n }\n\n return false;\n};\n\nfunction filterSlots(slots) {\n const DRAWER_MAX_Y = 1400;\n\n return slots.filter(slot => {\n if (isType(slot, ITEMS.DRAWER)) {\n return slot.y + slot.height <= DRAWER_MAX_Y;\n }\n return true;\n });\n}\n\nfunction getDependentItems(item, tac) {\n //Not implemented for bror\n return [];\n}\n\nfunction getSlotSources(item, tac) {\n //Not implemented for bror\n return [];\n}\n\nexport default {\n filterSlots,\n getDependentItems,\n getSlotSources,\n shouldIgnoreCollision,\n};\n","const config = {\n innerId: 'clothes_rail_50_51_4_white_in',\n outerId: 'clothes_rail_50_51_4_white_out',\n fullId: 'clothes_rail_50_51_4_white',\n maxWidth: 820,\n minWidth: 460,\n mountOffset: 17,\n};\n\nexport { config };\n","import constants from '../../../../settings/constants';\nimport tacHelpers from '../../tacHelpers';\nimport { isType, isExtendable } from '../../../../services/products';\nimport { config } from '../../../../scene/jonaxel/ClothesRailConfig';\nimport productService from '../../../../services/products';\nimport { isStandAlone } from '../../../../services/products/models';\nimport { ITEMS } from '../../../../constants';\n\nfunction isBelowParent(slot) {\n return slot.y < slot.parent.y;\n}\n\nfunction isBlockedByClothesRail(slot, tac) {\n function isBlockingClothesRail(item, slot) {\n return (\n isType(item, ITEMS.CLOTHES_RAIL) &&\n isExtendable(item) &&\n ((isType(slot, 'leg') && slot.height > constants.PAD_HEIGHT) ||\n (isType(slot, 'cover') &&\n item.y < slot.parent.height - constants.DISTANCE_BETWEEN_ATTACHMENTS))\n );\n }\n\n return (\n (slot.parent.items &&\n slot.parent.items.find(item => isBlockingClothesRail(item, slot))) ||\n tacHelpers\n .getAllItems(tac.items)\n .find(\n item =>\n item.connectsTo &&\n item.connectsTo.itemid === slot.parent.itemid &&\n isBlockingClothesRail(item, slot)\n )\n );\n}\n\nfunction onWheels(tac, item) {\n return tacHelpers\n .getTopAncestor(tac, item)\n .items?.some(\n item => productService.isLeg(item) && item.filter.variant === 'castors'\n );\n}\n\n/**\n * Decides whether a given slot is to be considered valid or not\n * based on a number of rules.\n * @param {*} slot The slot to validate.\n * @param {*} tac The TAC.\n * @returns {boolean} The validness of the slot.\n */\nfunction valid(slot, tac) {\n // a stack is invalid if it's three or more parts tall\n // so either the parent is more than one part tall.\n // Protective pads should not be counted as a part in this case.\n if (\n productService.isStackable(slot.parent) &&\n productService.isStackable(slot) &&\n slot.parent.y > constants.PAD_HEIGHT\n ) {\n return false;\n }\n //.. or the fitted stack is\n if (\n productService.isStackable(slot) &&\n (onWheels(tac, slot) || (slot.items || []).some(productService.isStackable))\n ) {\n return false;\n }\n\n // Stacking on top of a covered parent is also invalid\n if (\n productService.isStackable(slot) &&\n slot.parent.items?.some(item => productService.isType(item, 'cover'))\n ) {\n return false;\n }\n\n //.. or we are trying to place an item below an already stacked parent,\n // and the item is not listed as a part on the parent\n if (\n isBelowParent(slot) &&\n productService.isStackable(slot.parent) &&\n (slot.parent.items || []).some(productService.isStackable) &&\n !(Object.values(slot.parent.parts) || []).some(part => part === slot.id)\n ) {\n return false;\n }\n // Covers and legs are possibly blocked by extendable clothes rail depending on pos.\n if (isType(slot, ['cover', 'leg']) && isBlockedByClothesRail(slot, tac)) {\n return false;\n }\n\n // It should not be possible to add another cover if a cover is already present.\n if (\n isType(slot, ['cover']) &&\n slot.parent.items?.some(item => productService.isType(item, 'cover'))\n ) {\n return false;\n }\n return true;\n}\n\nfunction filterSlots(slots, tac) {\n return slots.filter(slot => valid(slot, tac));\n}\n\nfunction hasBlockingCover(tac, slot) {\n return (\n slot.local &&\n slot.local.y <\n slot.parent.height - constants.DISTANCE_BETWEEN_ATTACHMENTS &&\n slot.parent.items &&\n slot.parent.items.find(item => isType(item, 'cover'))\n );\n}\n\n/*\nIf either the left or the right frame is connected to a clothes rail with a different partner,\nthe slot is deemed invalid.\n*/\nfunction hasConflictingClothesRail(left, right, tac) {\n const existingExtCrs = tacHelpers\n .getClothesRails(tac)\n .filter(cr => productService.isExtendable(cr));\n\n if (!existingExtCrs.length) {\n return false;\n }\n\n const topAncestorLeft = tacHelpers.getTopAncestor(tac, left.parent);\n const topAncestorRight = tacHelpers.getTopAncestor(tac, right.parent);\n\n return existingExtCrs.find(clothesRail => {\n const clothesRailTopAncestor = tacHelpers.getTopAncestor(tac, clothesRail);\n const connectsToTopAncestor =\n clothesRail.connectsTo &&\n tacHelpers.getTopAncestor(\n tac,\n tacHelpers.getItem(tac, clothesRail.connectsTo.itemid)\n );\n return (\n connectsToTopAncestor &&\n ((clothesRailTopAncestor.itemid === topAncestorLeft.itemid &&\n connectsToTopAncestor.itemid !== topAncestorRight.itemid) ||\n (clothesRailTopAncestor.itemid !== topAncestorLeft.itemid &&\n connectsToTopAncestor.itemid === topAncestorRight.itemid))\n );\n });\n}\n\nfunction getPartnerConfig(partner) {\n return config;\n}\n\nfunction partnerSlots(slots, otherSlots, tac) {\n // add complement slot for other half of extendable clothes rod\n const insideSlots = slots\n .filter(slot => slot.id === config.innerId)\n .filter(\n slot => !onWheels(tac, slot.parent) && !hasBlockingCover(tac, slot)\n );\n if (insideSlots.length) {\n const outsideSlots = otherSlots.filter(\n slot => !onWheels(tac, slot.parent) && !hasBlockingCover(tac, slot)\n );\n insideSlots.forEach(left => {\n const fits = outsideSlots.filter(\n right =>\n right.x > left.x &&\n right.y === left.y &&\n right.x + right.width <= left.x + config.maxWidth &&\n !hasConflictingClothesRail(left, right, tac)\n );\n if (fits.length === 1) {\n left.partnerSlot = fits[0];\n }\n if (fits.length > 1) {\n throw new Error('found more than one fit for outside clothes rail');\n }\n });\n }\n return slots.filter(slot => slot.id !== config.innerId || slot.partnerSlot);\n}\n\nfunction getDependentItems(item, tac) {\n //Not implemented for jonaxel\n return [];\n}\n\nfunction getProppingItemsToAdapt(tac, movingItem) {\n if (!movingItem || !isStandAlone(movingItem)) {\n return [];\n }\n\n const connectedClothesRails = [\n ...tacHelpers\n .getClothesRails({ items: [movingItem] })\n .filter(cr => productService.isExtendable(cr)),\n ...tacHelpers.getConnectedClothesRails(tac, movingItem),\n ];\n\n const otherExtCrs = tacHelpers\n .getClothesRails(tac)\n .filter(\n cr =>\n productService.isExtendable(cr) &&\n !connectedClothesRails.some(connected => connected.itemid === cr.itemid)\n );\n\n return otherExtCrs;\n}\n\nfunction getSlotSources(item, tac) {\n //Not implemented for jonaxel\n return [];\n}\n\nfunction moveWithCr(item) {\n return tacHelpers.hasExtClothesRail(item);\n}\n\nexport default {\n filterSlots,\n getDependentItems,\n getPartnerConfig,\n getProppingItemsToAdapt,\n getSlotSources,\n moveWithCr,\n partnerSlots,\n};\n","const config = {\n section: {\n maxWidth: 300,\n minWidth: 200,\n },\n shelf: {\n 30463744: {\n leftId: 'BOAXEL_30463744_adj_metal_shelf_thin_20_30_2',\n rightId: 'BOAXEL_30463744_adj_metal_shelf_thick_20_30_2',\n },\n 30575586: {\n leftId: 'BOAXEL_30575586_adj_metal_shelf_thin_20_30_2',\n rightId: 'BOAXEL_30575586_adj_metal_shelf_thick_20_30_2',\n },\n },\n clothesrail: {\n 70463742: {\n leftId: 'BOAXEL_70463742_adj_clothes_rail_thin_20_30_2',\n rightId: 'BOAXEL_70463742_adj_clothes_rail_thick_20_30_2',\n },\n 50575585: {\n leftId: 'BOAXEL_50575585_adj_clothes_rail_thin_20_30_2',\n rightId: 'BOAXEL_50575585_adj_clothes_rail_thick_20_30_2',\n },\n },\n legs: {\n topId: 'pair_of_legs_thick_60_10_120',\n bottomId: 'pair_of_legs_thin_60_10_120',\n maxHeight: 840,\n minHeight: 665,\n },\n};\n\nexport { config };\n","import constants from '../settings/constants';\n\nfunction isFixedRoom() {\n return !!constants.WALL;\n}\n\nexport { isFixedRoom };\n","import geometry from '../../../scene/util/geometry';\nimport { isFixedRoom } from '../../../util/room';\n\nexport function adjustMountingRailChainPos(chain, newX, wallWidth) {\n chain[0].x = isFixedRoom() ? Math.max(0, newX) : newX;\n for (let i = 1; i < chain.length; i++) {\n chain[i].x = chain[i - 1].x + chain[i - 1].width;\n }\n\n if (\n isFixedRoom() &&\n chain[chain.length - 1].x + chain[chain.length - 1].width > wallWidth\n ) {\n for (let i = chain.length - 1; i >= 0; i--) {\n if (i === chain.length - 1) {\n chain[i].x = wallWidth - chain[i].width;\n } else {\n chain[i].x = chain[i + 1].x - chain[i].width;\n }\n }\n }\n}\n\nfunction getMountingRailChain(current, mountingRails, padding, chain = []) {\n for (let i = 0; i < mountingRails.length; i++) {\n if (geometry.collides(current, mountingRails[i], padding)) {\n const newCurrent = mountingRails[i];\n chain.push(newCurrent);\n mountingRails.splice(i, 1);\n\n return getMountingRailChain(newCurrent, mountingRails, padding, chain);\n }\n }\n\n return chain;\n}\n\nexport function getMountingRailChains(mountingRails, padding = {}, done = []) {\n mountingRails = mountingRails.slice();\n if (!mountingRails.length) return done;\n\n const group = [];\n\n const current = mountingRails.pop();\n group.push(current);\n\n const mountingRailChain = getMountingRailChain(\n current,\n mountingRails,\n padding\n );\n group.push(...mountingRailChain);\n done.push(group);\n\n return getMountingRailChains(mountingRails, padding, done);\n}\n","import productService, {\n areMountingRailsOfSpecificColorValid,\n getValidMountingRailColors,\n} from '../../../../services/products';\nimport geometry from '../../../../scene/util/geometry';\nimport tacHelpers from '../../tacHelpers';\nimport constants from '../../../../settings/constants';\nimport { adjustMountingRailChainPos } from '../common';\nimport { ITEMS } from '../../../../constants';\n\n/**\n * Divides sections into groups (a Map) in such a way that sections that are\n * top aligned with each other belong to the same group. Note that \"skewed\"\n * sections (left upright higher/lower than the right upright) will not be\n * included in any group.\n * @param {Array} sections The sections to be divided.\n * @param {object} tac The tac containing the sections and their uprights.\n * @returns {object} A Map with the groups' \"top y\" coordinates as keys\n * and arrays of grouped sections as values.\n */\nfunction getTopAlignedSectionGroups(sections, tac) {\n const sectionGroupsVertical = new Map();\n\n sections.forEach(section => {\n const topmostUprights = section.uprights\n .map(upright => tacHelpers.getItem(tac, upright.itemid))\n .sort((a, b) => b.y - a.y)\n .slice(0, 2);\n\n if (\n topmostUprights[0].y + topmostUprights[0].height ===\n topmostUprights[1].y + topmostUprights[1].height &&\n topmostUprights[1].y + topmostUprights[1].height ===\n section.y + section.height\n ) {\n if (sectionGroupsVertical.get(section.y + section.height)) {\n sectionGroupsVertical.get(section.y + section.height).push(section);\n } else {\n sectionGroupsVertical.set(section.y + section.height, [section]);\n }\n }\n });\n\n return sectionGroupsVertical;\n}\n\n/**\n * Takes a map of \"key -> section arrays\" and further splits the groups\n * belonging to each key in such a way that each group only contain\n * horizontally coherent sections (no horizontal \"holes\").\n * @param {Map} sectionGroupsToSplit A Map with arrays of sections as values.\n * @returns {object} A Map contining the split groups with keys based on the\n * keys of the input Map, but with an appended comma-separated\n * counter index depending on which consecutive section group\n * the key denotes (e.g., old key \"120\" would result in the new\n * keys \"120,0\", \"120,1\", ..., \"120,n\".\n */\nfunction splitIntoCoherentSectionGroups(sectionGroupsToSplit) {\n const coherentSectionGroups = new Map();\n\n sectionGroupsToSplit.forEach((sections, prevKey) => {\n let coherentGroupCounter = 0;\n let prevSection = null;\n sections\n .sort((a, b) => a.x - b.x)\n .forEach(section => {\n prevSection &&\n prevSection.x + prevSection.width !== section.x &&\n coherentGroupCounter++;\n const updatedKey = `${prevKey},${coherentGroupCounter}`;\n if (coherentSectionGroups.get(updatedKey)) {\n coherentSectionGroups.get(updatedKey).push(section);\n } else {\n coherentSectionGroups.set(updatedKey, [section]);\n }\n prevSection = section;\n });\n });\n\n return coherentSectionGroups;\n}\n\n/**\n * Calculates the width of a coherent (no \"holes\") group of sections.\n * The width also includes the \"overshooting\" part of the group's uprights.\n * @param {Array} sections The sections beloning to a coherent group.\n * @returns {number} The width of the group.\n */\nfunction widthOfSectionGroup(sectionsInGroup) {\n const width =\n sectionsInGroup.reduce((accWidth, section) => accWidth + section.width, 0) +\n 2 * (productService.getPostWidth() / 2);\n\n return width;\n}\n\n/**\n * Gets the rail product corresponding to a specific approximate length\n * (=length as given in the range constants, e.g., 600).\n * @param {number | string} approxLength The approximate length.\n * @returns {object} The corresponding rail product.\n */\nfunction getRailProductByApproxLength(approxLength, color) {\n let product = null;\n const productId = constants.MOUNTING_RAILS[color][approxLength];\n if (productId) {\n product = productService.getProduct(productId) || null;\n }\n\n return product;\n}\n\n/**\n * Gets the corresponding precise length (e.g, 624) of a rail,\n * given an approximate length (=length as listed in the range\n * constants, e.g., 600).\n * @param {number | string} approxLength The approximate length.\n * @returns {number} The corresponding precise length.\n */\nfunction getPreciseRailLength(approxLength, color) {\n const preciseLength =\n getRailProductByApproxLength(approxLength, color)?.width || null;\n\n return preciseLength;\n}\n\n/**\n * Calculates the total length of a number of rail lengths.\n * @param {Array} rails The individual lengths of the rails.\n * @returns {number} The total length of all the rails.\n */\nfunction lengthOfRails(rails) {\n const length = rails.reduce(\n (totalLengthAcc, railLength) => totalLengthAcc + railLength,\n 0\n );\n return length;\n}\n\n/**\n * Given available rail lengths, calculates the optimal rail lengths\n * to use in order to cover a specific distance in such a way that the\n * total length is long enough to cover the distance, yet there should\n * be as little overflow as possible.\n * @param {Array} availableLengths The available rail lengths to be\n * used in the calculations.\n * @param {number} lengthToCover The distance that needs to covered.\n * @returns {Array} The optimal rail lengths to cover the distance.\n * The same length might occur more than once.\n */\nfunction getOptimallyCoveringRails(availableLengths, lengthToCover) {\n let optimalRails = [];\n if (lengthToCover > 0) {\n const candidates = [];\n availableLengths.forEach(availableLength => {\n const theRest = getOptimallyCoveringRails(\n availableLengths,\n lengthToCover - availableLength\n );\n availableLength + lengthOfRails(theRest) >= lengthToCover &&\n candidates.push([availableLength, ...theRest]);\n });\n optimalRails = candidates.reduce(\n (acc, curr) => (lengthOfRails(curr) < lengthOfRails(acc) ? curr : acc),\n [Infinity]\n );\n }\n return optimalRails;\n}\n\nfunction getY(section, mountingRailHeight) {\n return section.y + section.height - mountingRailHeight / 2;\n}\n\n/**\n * @returns {number} Intermediate x pos. This is only valid if there are no other\n * mounting rails in the vicinity, otherwise it needs later adjusting.\n */\nfunction getX(section) {\n return section.x - productService.getPostWidth() / 2;\n}\n\n/**\n * Takes a rail product and adds a position based on a starting section\n * and a provided horizontal offset.\n * @param {object} section The section from which the positioning starts.\n * @param {number} xOffset The horizontal offset from the start of the chain.\n * @param {object} railProduct The original, unpositioned rail product.\n */\nfunction getPositionedMountingRail(section, xOffset, railProduct) {\n return {\n ...railProduct,\n x: getX(section) + xOffset,\n y: getY(section, railProduct.filter.height),\n z: 0,\n };\n}\n\n/**\n * Gets the rail product corresponding to a specific precise length\n * (e.g., 624).\n * @param {number | string} approxLength The approximate length.\n * @returns {object} The corresponding rail product.\n */\nfunction getRailProductByPreciseLength(preciseLength, color) {\n let product = null;\n const matchingProducts = Object.values(constants.MOUNTING_RAILS[color])\n .map(productId => productService.getProduct(productId))\n .filter(\n product =>\n product &&\n product.width &&\n product.width.toString() === preciseLength.toString()\n );\n matchingProducts.length && (product = matchingProducts[0]);\n\n return product;\n}\n\nfunction getCorrectlyPositionedMountingRails(mountingRailChain, wallWidth) {\n const chainCopy = mountingRailChain.slice();\n chainCopy.sort((a, b) => a.x - b.x);\n const newX = chainCopy[0].x;\n adjustMountingRailChainPos(chainCopy, newX, wallWidth);\n\n return chainCopy;\n}\n\nfunction decideColorOfMountingRailGroup(sectionsInGroup) {\n const uprights = sectionsInGroup.reduce((uprightsThisFar, section) => {\n const updatedUprightsThisFar = [...uprightsThisFar];\n section.uprights.forEach(uprightInSection => {\n if (\n !updatedUprightsThisFar.find(\n upright => upright.itemid === uprightInSection.itemid\n )\n )\n updatedUprightsThisFar.push(uprightInSection);\n });\n return updatedUprightsThisFar;\n }, []);\n const colorsOfUprights = uprights.map(upright => upright.filter.color);\n const countedColors = colorsOfUprights.reduce((resultThisFar, color) => {\n const updatedResultThisFar = { ...resultThisFar };\n updatedResultThisFar[color] = resultThisFar[color]\n ? resultThisFar[color] + 1\n : 1;\n return updatedResultThisFar;\n }, {});\n const colorsSortedDescending = Object.keys(countedColors).sort(\n (color1, color2) => countedColors[color2] - countedColors[color1]\n );\n const chosenColor =\n colorsSortedDescending.find(color =>\n areMountingRailsOfSpecificColorValid(color)\n ) || getValidMountingRailColors()[0];\n\n return chosenColor;\n}\n\nfunction getGroupedMountingRails(tac) {\n const groupedMountingRails = [];\n\n const postWidth = productService.getPostWidth();\n\n const mountingSections = tac.items.filter(\n item =>\n item.type === ITEMS.SECTION && !item.keepDuringDrag && item.width < 1000\n );\n\n const mountingSectionGroupsVertical = getTopAlignedSectionGroups(\n mountingSections,\n tac\n );\n const mountingSectionGroups = splitIntoCoherentSectionGroups(\n mountingSectionGroupsVertical\n );\n\n mountingSectionGroups.forEach(sectionsInGroup => {\n const color = decideColorOfMountingRailGroup(sectionsInGroup);\n const groupWidth = widthOfSectionGroup(sectionsInGroup);\n\n const preciseRailLengthsAvailableDescending = Object.keys(\n constants.MOUNTING_RAILS[color]\n )\n .map(approxLength => getPreciseRailLength(approxLength, color))\n .filter(preciseLength => preciseLength)\n .sort((length1, length2) => length2 - length1);\n\n const optimalRails = getOptimallyCoveringRails(\n preciseRailLengthsAvailableDescending,\n groupWidth\n );\n const optimalRailsTotalLength = lengthOfRails(optimalRails);\n const overflow = optimalRailsTotalLength - groupWidth;\n const firstSection = sectionsInGroup[0];\n\n const mountingRailsInGroup = [];\n let xOffset = -overflow / 2;\n optimalRails.forEach(preciseRailLength => {\n mountingRailsInGroup.push(\n getPositionedMountingRail(\n firstSection,\n xOffset,\n getRailProductByPreciseLength(preciseRailLength, color)\n )\n );\n xOffset += preciseRailLength;\n });\n\n const firstSectionInGroup = sectionsInGroup[0];\n const lastSectionInGroup = sectionsInGroup[sectionsInGroup.length - 1];\n groupedMountingRails.push({\n mountingRails: mountingRailsInGroup,\n areaToCoverStartX: firstSectionInGroup.x - postWidth / 2,\n areaToCoverEndX:\n lastSectionInGroup.x + lastSectionInGroup.width + postWidth / 2,\n });\n });\n\n const wallWidth = geometry.surround(tac.wall.points).width;\n\n groupedMountingRails.forEach(group => {\n group.mountingRails = getCorrectlyPositionedMountingRails(\n group.mountingRails,\n wallWidth\n );\n });\n\n return groupedMountingRails;\n}\n\n/**\n * Decides what mounting rails to use for a given configuration.\n * @param {object} tac The TAC representing the configuration.\n * @returns {Array} The mounting rails to use (including their positions).\n */\nfunction getMountingRails(tac) {\n const groupedMountingRails = getGroupedMountingRails(tac);\n const mountingRails = [];\n groupedMountingRails.forEach(group => {\n mountingRails.push(...group.mountingRails);\n });\n\n return mountingRails;\n}\n\nexport { getGroupedMountingRails as getCuttableMountingRailData };\nexport default getMountingRails;\n","import tacHelpers from '../tacHelpers';\n\nfunction getOffset(parent, child, model) {\n const leggedParent = parent.logic && parent.logic.floor && child.y < 0;\n\n if (!leggedParent) {\n return false;\n }\n\n const grandParent = model && tacHelpers.getParent(model, parent);\n const flooredGrandParent =\n !grandParent || !grandParent.logic || !grandParent.logic.floor;\n\n return flooredGrandParent;\n}\n\nexport default function makeSpaceForChild(child, parent, model) {\n const offset = getOffset(parent, child, model);\n\n if (offset) {\n parent.y = -child.y;\n }\n\n if (child.items) {\n child.items.forEach(item => {\n makeSpaceForChild(item, child, model);\n });\n }\n}\n","import _ from 'lodash';\nimport tacHelpers from '../tacHelpers';\nimport geometry from '../../../scene/util/geometry';\nimport { isSection } from '../../../services/products';\nimport { isStandAlone } from '../../../services/products/models';\n\nfunction leftOf(them, me) {\n return me.x + me.width > them.x;\n}\n\nfunction below(me, them) {\n return them.y >= me.y + me.height;\n}\n\nfunction behind(me, them) {\n return them.z >= me.z + me.depth;\n}\n\nfunction isInvisible(them) {\n return !them.width || !them.height || !them.depth;\n}\n\nfunction outsideWall(them) {\n return isStandAlone(them) && !tacHelpers.isWithinWall(them);\n}\n\nfunction intersects(itemOne, itemTwo) {\n const intersection = geometry.intersectingRect(itemOne, itemTwo);\n return intersection.height > 0 && intersection.width > 0;\n}\n\nfunction map2dot(map) {\n return `\n digraph {\n ${map\n .map(dep =>\n ` ${dep.item.itemid} [label=${dep.item.id}_${dep.item.type.replace(\n '-',\n '_'\n )}] -> {${dep.dependsOn.join(';')}};`.replace(' -> {}', '')\n )\n .join('\\n')}\n }\n `;\n}\n\nfunction dependsOn(me, them) {\n if (outsideWall(me)) {\n // Always draw items outside the wall limits last\n return true;\n }\n\n /*\n Never depend if:\n 1. We're behind, no matter where.\n 2. We're left of something that is invisible\n 3. They're outside the limits of the wall\n */\n if (\n behind(me, them) ||\n (isInvisible(them) && leftOf(me, them)) ||\n outsideWall(them)\n ) {\n return false;\n }\n\n if (intersects(me, them)) {\n //if both are sections and me has item(s), draw the smaller one first\n if (\n isSection(me) &&\n isSection(them) &&\n me.items?.length &&\n (geometry.contains(me, them) || geometry.contains(them, me))\n ) {\n return geometry.contains(me, them);\n }\n\n const allowedOverlap = tacHelpers.range.getAllowedOverlap(\n { items: [them, me] },\n them,\n me\n );\n\n // if we intersect, depend if they are behind or left of and is not contained by the other one\n return (\n behind(them, me) ||\n (them.x < me.x &&\n geometry.intersectingRect(them, me).width === allowedOverlap?.x &&\n !geometry.contains(them, me))\n );\n }\n\n // depend on stuff on the left, if we are not below it\n if (leftOf(them, me) && !below(me, them)) {\n return true;\n }\n\n // if we don't intersect, then depend on them if they're left and below\n return leftOf(them, me) && below(them, me);\n}\n\nfunction buildDependencies(items) {\n const dependencies = items.map(item => ({\n item: item,\n dependsOn: null,\n }));\n\n dependencies.forEach(function (dep) {\n dep.dependsOn = items\n .filter(item => item !== dep.item)\n .filter(item => dependsOn(dep.item, item))\n .map(item => item.itemid);\n });\n\n return dependencies;\n}\n\nfunction sortDependencies(nodes) {\n // This is an implementation of Kahn's algorithm for sorting a dependency graph\n // https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm\n const out = [];\n const metDeps = nodes.filter(node => node.dependsOn.length === 0);\n const unmetDeps = nodes.filter(node => node.dependsOn.length !== 0);\n while (metDeps.length) {\n var done = metDeps.pop();\n out.push(done.item.itemid);\n //eslint-disable-next-line no-loop-func\n unmetDeps.forEach(function (node) {\n const index = node.dependsOn.indexOf(done.item.itemid);\n if (index !== -1) {\n node.dependsOn.splice(index, 1);\n if (node.dependsOn.length === 0) {\n metDeps.push(node);\n }\n }\n });\n }\n\n const left = unmetDeps.filter(node => node.dependsOn.length !== 0);\n if (left.length) {\n //https://en.wikipedia.org/wiki/DOT_(graph_description_language)#Directed_graphs\n const mapAsDot = `\n paste this into https://codepen.io/ledhund/pen/qBZMVwQ\n\n ${map2dot(left)}\n `;\n console.log(mapAsDot);\n throw new Error('Bad dependency graph');\n }\n return out;\n}\n\nfunction inDrawOrder(items) {\n const arr = items.slice();\n const deps = buildDependencies(arr);\n // filter deps to avoid any duplicate sprites without itemid\n const sortedDeps = sortDependencies(deps);\n const sortedItems = sortedDeps.map(itemid =>\n items.find(item => itemid === item.itemid)\n );\n\n // if temps have kids, keep them in regular draw order\n const tempsFirst = _.partition(sortedItems, function drawAtBottom(item) {\n return item.isTempItem && !item.items?.length;\n });\n return Array.prototype.concat.apply([], tempsFirst);\n}\n\nexport default {\n inDrawOrder,\n};\n","import idGenerator from '../../util/aactools/idGenerator';\n\nexport function replace(items = [], replacement, options = {}) {\n const pos =\n options.replaceFakes && !idGenerator.hasRealId(replacement)\n ? items.findIndex(item => !idGenerator.hasRealId(item))\n : items.findIndex(item => item.itemid === replacement.itemid);\n if (pos !== -1) {\n items[pos] = replacement;\n return;\n }\n items.forEach(item => replace(item.items, replacement, options));\n}\n","import { State } from '../StateTypes';\n\nexport const selectRangeData = (state: State) => state.rangeData;\n\nexport const selectRangeDataSlice = (key: string) => (state: State) =>\n selectRangeData(state)[key];\n","import { State } from '../StateTypes';\nimport {\n DraggingWallResizer,\n MeasurementsActive,\n Room,\n Scene,\n SceneRect,\n WallResizerActive,\n} from './sceneTypes';\n\n/**\n * Select scene state slice\n *\n * @param scene\n */\nexport const selectScene = ({ scene }: State): Scene => scene;\n\n/**\n * Select is wall resizer active\n *\n * @param state\n * @returns {*}\n */\nexport const selectIsWallResizerActive = (state: State): WallResizerActive =>\n selectScene(state).wallResizerActive;\n\n/**\n * Select is measurements active\n *\n * @param state\n * @returns {*}\n */\nexport const selectIsMeasurementsActive = (state: State): MeasurementsActive =>\n selectScene(state).measurementsActive;\n\n/**\n * Select dragging wall resizer\n *\n * @param state\n * @returns {Requireable | boolean}\n */\nexport const selectDraggingWallResizer = (state: State): DraggingWallResizer =>\n selectScene(state).draggingWallResizer;\n\n/**\n * Select room\n *\n * @param state\n * @returns {*}\n */\nexport const selectRoom = (state: State): Room => selectScene(state).room;\n\nexport const selectMinRoom = (state: State): Room => selectRoom(state).minRoom;\n\nexport const selectSceneRect = (state: State): SceneRect =>\n selectRoom(state).sceneRect;\n\nexport const selectSceneMargins = (state: State): SceneRect =>\n selectRoom(state).margins;\n/**\n * Select preloadDone state slice\n *\n * @param state\n */\nexport const selectHasScenePreloadFinished = (state: State) =>\n selectScene(state).preloadDone;\n\n/**\n * Select show scene sheet\n *\n * @param state\n */\nexport const selectShowSceneSheet = (state: State) =>\n selectScene(state).sceneSheet.showSceneSheet;\n\n/**\n * Select scene sheet variant\n * @param state\n */\nexport const selectSceneSheetVariant = (state: State) =>\n selectScene(state).sceneSheet.sceneSheetModel;\n","import { Scene } from './sceneTypes';\n\nconst defaultState: Scene = {\n sceneSheet: {\n showSceneSheet: false,\n sceneSheetModel: '',\n },\n preloadDone: false,\n measurementsActive: false,\n wallResizerActive: false,\n draggingWallResizer: false,\n room: null,\n showPropping: true,\n};\n\nexport default defaultState;\n","import {\n SCENE_HIDE_MEASUREMENTS,\n SCENE_SHOW_MEASUREMENTS,\n SCENE_SET_PROPPING_VISIBILITY,\n SCENE_SET_ROOM,\n SCENE_SET_WALL_RESIZER_ACTIVE,\n SCENE_SET_WALL_RESIZER_INACTIVE,\n SCENE_SET_WALL_RESIZER_DRAGGING,\n SCENE_SET_RECT,\n SCENE_SET_MARGINS,\n PRODUCT_MENU_SET_FILTER,\n TAC_LOAD,\n SCENE_SHOW_SHEET,\n SCENE_HIDE_SHEET,\n SCENE_SET_PRELOAD_STATE,\n} from '../actionConstants';\nimport defaultState from './sceneDefaultState';\nimport { Scene } from './sceneTypes';\n\nexport default (state: Scene = defaultState, action: any) => {\n switch (action.type) {\n case SCENE_SET_ROOM:\n return {\n ...state,\n room: { ...state.room, ...action.payload },\n };\n case SCENE_SET_RECT:\n return {\n ...state,\n room: { ...state.room, sceneRect: action.payload },\n };\n case SCENE_SET_MARGINS:\n return {\n ...state,\n room: { ...state.room, margins: action.payload },\n };\n case SCENE_HIDE_MEASUREMENTS:\n return {\n ...state,\n measurementsActive: false,\n };\n case SCENE_SHOW_MEASUREMENTS:\n return {\n ...state,\n measurementsActive: true,\n };\n case SCENE_SET_PROPPING_VISIBILITY:\n return {\n ...state,\n showPropping: action.payload.showPropping,\n };\n case SCENE_SET_WALL_RESIZER_ACTIVE:\n return {\n ...state,\n wallResizerActive: true,\n };\n case SCENE_SET_WALL_RESIZER_INACTIVE: {\n return {\n ...state,\n wallResizerActive: false,\n };\n }\n case SCENE_SET_WALL_RESIZER_DRAGGING:\n return {\n ...state,\n draggingWallResizer: action.payload.draggingWallResizer,\n };\n case PRODUCT_MENU_SET_FILTER: {\n const manualPortraitChange =\n !action.meta?.nonInteraction && action.meta?.isPortrait;\n return {\n ...state,\n measurementsActive: manualPortraitChange\n ? false\n : state.measurementsActive,\n wallResizerActive: manualPortraitChange\n ? false\n : state.wallResizerActive,\n };\n }\n case TAC_LOAD:\n return {\n ...state,\n showPropping: true,\n };\n\n case SCENE_SHOW_SHEET:\n return {\n ...state,\n sceneSheet: {\n showSceneSheet: true,\n sceneSheetModel: action.payload.sheetType,\n },\n };\n\n case SCENE_SET_PRELOAD_STATE:\n return {\n ...state,\n preloadDone: action.payload.preloadState,\n };\n\n case SCENE_HIDE_SHEET:\n return {\n ...state,\n sceneSheet: {\n showSceneSheet: false,\n sceneSheetModel: '',\n },\n };\n\n default:\n return state;\n }\n};\n","import {\n SCENE_HIDE_MEASUREMENTS,\n SCENE_SHOW_MEASUREMENTS,\n SCENE_SET_PROPPING_VISIBILITY,\n SCENE_SET_ROOM,\n SCENE_SET_WALL_RESIZER_ACTIVE,\n SCENE_SET_WALL_RESIZER_DRAGGING,\n SCENE_SET_WALL_RESIZER_INACTIVE,\n SCENE_SET_RECT,\n SCENE_SET_MARGINS,\n SCENE_SHOW_SHEET,\n SCENE_HIDE_SHEET,\n SCENE_SET_PRELOAD_STATE,\n} from '../actionConstants';\nimport { Action, CustomAction, RootAction } from '../../generalTypes';\nimport {\n DraggingWallResizer,\n NonInteraction,\n Room,\n ShowPropping,\n SceneRect,\n Margins,\n} from './sceneTypes';\n\nexport const actionShowMeasurements = (): RootAction => ({\n type: SCENE_SHOW_MEASUREMENTS,\n});\n\nexport const actionHideMeasurements =\n (meta: {}): CustomAction => ({\n type: SCENE_HIDE_MEASUREMENTS,\n meta,\n });\n\nexport const actionSetRoom = (room: Room): Action => ({\n type: SCENE_SET_ROOM,\n payload: room,\n});\n\nexport const actionSetSceneRect = (\n sceneRect: SceneRect\n): Action => ({\n type: SCENE_SET_RECT,\n payload: sceneRect,\n});\n\nexport const actionSetMargins = (margins: Margins): Action => ({\n type: SCENE_SET_MARGINS,\n payload: margins,\n});\n\nexport const actionSetProppingVisibility = (showPropping: ShowPropping) => ({\n type: SCENE_SET_PROPPING_VISIBILITY,\n payload: { showPropping },\n});\n\nexport const actionSetWallResizerActive = (meta: {}): CustomAction<{}, {}> => ({\n type: SCENE_SET_WALL_RESIZER_ACTIVE,\n payload: {\n active: true,\n },\n meta,\n});\n\nexport const actionSetWallResizerInactive = (meta: {}): CustomAction<\n {},\n {}\n> => ({\n type: SCENE_SET_WALL_RESIZER_INACTIVE,\n payload: {\n active: false,\n },\n meta,\n});\n\nexport const setDraggingWallResizer = (\n draggingWallResizer: DraggingWallResizer\n): Action => ({\n type: SCENE_SET_WALL_RESIZER_DRAGGING,\n payload: draggingWallResizer,\n});\n\nexport const actionSetScenePreloadState = (preloadState: boolean) => ({\n type: SCENE_SET_PRELOAD_STATE,\n payload: { preloadState },\n});\n\nexport const actionShowSceneSheet = (sheetType: string) => ({\n type: SCENE_SHOW_SHEET,\n payload: { sheetType },\n});\n\nexport const actionHideSceneSheet = () => ({\n type: SCENE_HIDE_SHEET,\n});\n","import { Action } from '../../generalTypes';\nimport { SET_RANGE_DATA } from '../actionConstants';\n\nexport const actionSetRangeData = (key: string, value: any): Action => ({\n type: SET_RANGE_DATA,\n payload: {\n key,\n value,\n },\n});\n","import {\n PRODUCT_MENU_CLEAR_FILTER,\n PRODUCT_MENU_LOAD_FILTERS,\n PRODUCT_MENU_SET_COLOR,\n PRODUCT_MENU_SET_FILTER,\n PRODUCT_MENU_SET_SELECTABLE_COLORS,\n PRODUCT_MENU_SET_SUB_FILTER,\n PRODUCT_MENU_SET_SUB_FILTER_ITEMS,\n PRODUCT_MENU_SET_SUB_FILTER_OPTIONS,\n} from '../actionConstants';\nimport { Filter, SubFilter } from './productMenuTypes';\nimport { Action } from '../../generalTypes';\n\nexport const setFilters = (\n filters: Filter[]\n): Action<{ filters: Filter[] }> => ({\n type: PRODUCT_MENU_LOAD_FILTERS,\n payload: { filters },\n});\n\nexport const actionSetSwiperSubFilter = (subFilter: SubFilter) => ({\n type: PRODUCT_MENU_SET_SUB_FILTER,\n payload: subFilter,\n});\n\nexport const actionSetSwiperSubFilterItems = (subFilterItems: any) => ({\n type: PRODUCT_MENU_SET_SUB_FILTER_ITEMS,\n payload: subFilterItems,\n});\n\nexport const actionSetColor = (color: string, filterName: string) => ({\n type: PRODUCT_MENU_SET_COLOR,\n payload: {\n filterName,\n color,\n },\n});\n\nexport const actionSetFilter = (filter: string) => ({\n type: PRODUCT_MENU_SET_FILTER,\n payload: filter,\n});\n\nexport const actionClearFilter = () => ({\n type: PRODUCT_MENU_CLEAR_FILTER,\n});\n\nexport const actionSetSelectableColors = (\n selectableColors: any,\n filterName: string\n) => ({\n type: PRODUCT_MENU_SET_SELECTABLE_COLORS,\n payload: {\n selectableColors,\n filterName,\n },\n});\n\nexport const actionSetSubFilterOptions = (filter: string, options: any) => ({\n type: PRODUCT_MENU_SET_SUB_FILTER_OPTIONS,\n payload: {\n filter,\n options,\n },\n});\n","function getTransitionEnd() {\n const transitions = {\n transition: 'transitionend',\n OTransition: 'otransitionend',\n MozTransition: 'transitionend',\n WebkitTransition: 'webkitTransitionEnd',\n };\n\n for (const t in transitions) {\n if (document.body.style[t] !== undefined) {\n return transitions[t];\n }\n }\n}\n\nconst supportedEvents = {\n pointerDown: ['pointerdown', 'touchstart', 'mousedown'],\n pointerUp: ['pointerup', 'touchend', 'mouseup'],\n pointerMove: ['pointermove', 'touchmove', 'mousemove'],\n pointerLeave: ['pointerleave', 'touchcancel', 'mouseleave'],\n keyDown: ['keydown'],\n close: ['close'],\n click: ['click'],\n};\n\nconst getSupportedEvent = eventArray =>\n eventArray.find(eventName => `on${eventName}` in window);\n\nexport const POINTER_DOWN = getSupportedEvent(supportedEvents.pointerDown);\nexport const POINTER_UP = getSupportedEvent(supportedEvents.pointerUp);\n// outside event is specific to pixi sprites\nexport const POINTER_UP_OUTSIDE = POINTER_UP + 'outside';\nexport const POINTER_MOVE = getSupportedEvent(supportedEvents.pointerMove);\nexport const POINTER_LEAVE = getSupportedEvent(supportedEvents.pointerLeave);\nexport const KEY_DOWN = getSupportedEvent(supportedEvents.keyDown);\nexport const CLOSE = getSupportedEvent(supportedEvents.close);\nexport const CLICK = getSupportedEvent(supportedEvents.click);\nexport const TRANSITION_END = getTransitionEnd();\n\nexport const POINTER_SUPPORT = 'onpointermove' in window;\nexport const TOUCH_SUPPORT = 'ontouchmove' in window;\nexport const MOUSE_SUPPORT = 'onmousedown' in window;\n","import platform from '../../util/platform';\nimport { TOUCH_SUPPORT } from '../../util/supportedEvents';\nimport { State } from '../StateTypes';\nimport { UserAgent } from './userAgentTypes';\n\n/**\n * Selects user agent state slice\n *\n * @param state\n */\nexport const selectUserAgent = (state: State): UserAgent => state.userAgent;\n\n/**\n * Returns true if unit is mobile in portrait mode\n *\n * @param state\n */\nexport const selectIsMobilePortrait = (state: State): boolean => {\n const { isMobile, isPortrait } = selectUserAgent(state);\n return isMobile && isPortrait;\n};\n\n/**\n * Returns true if unit is mobile landscape\n *\n * @param state\n */\nexport const selectIsMobileLandscape = (state: State): boolean => {\n const { isMobile, isLandscape } = selectUserAgent(state);\n return isMobile && isLandscape;\n};\n\n/**\n * Returns true if static tool tip should be used\n *\n * @param state\n */\nexport const selectUseStaticToolTip = (state: State): boolean => {\n const { isMobile, isTablet, isDesktop } = selectUserAgent(state);\n return (\n platform.isKiosk || isMobile || isTablet || (isDesktop && TOUCH_SUPPORT)\n );\n};\n\n/**\n * Select Device type\n *\n * @param state\n */\nexport const selectDeviceType = (state: State): string =>\n selectUserAgent(state).deviceType;\n\n/**\n * Select if mobile or tablet\n *\n * @param state\n */\nexport const selectIsMobileOrTablet = (state: State) => {\n const { isMobile, isTablet } = selectUserAgent(state);\n return isMobile || isTablet;\n};\n\n/**\n * Select is tablet portrait\n *\n * @param state\n * @returns {*}\n */\nexport const selectIsTabletPortrait = (state: State) => {\n const { isTablet, isPortrait } = selectUserAgent(state);\n return isTablet && isPortrait;\n};\n\n/**\n * Select is portrait\n *\n * @param state\n */\nexport const selectIsPortrait = (state: State) =>\n selectUserAgent(state).isPortrait;\n\n/**\n * Select is landscape\n *\n * @param state\n */\nexport const selectIsLandscape = (state: State) =>\n selectUserAgent(state).isLandscape;\n\n/**\n * Select is Mobile\n *\n * @param state\n */\nexport const selectIsMobile = (state: State) => selectUserAgent(state).isMobile;\n\nexport const selectIsKiosk = (state: State) =>\n selectUserAgent(state).deviceType === 'kiosk';\n\n/**\n * Select is tablet\n *\n * @param state\n */\nexport const selectIsTablet = (state: State) => selectUserAgent(state).isTablet;\n\n/**\n * Select orientation\n *\n * @param state\n */\nexport const selectOrientation = (state: State) =>\n selectUserAgent(state).orientation;\n\n/**\n * Selects necessary data to determine that this was rendered on a larger display\n * in landscape mode\n */\nexport const selectIsWidescreen = (state: State) =>\n !selectIsMobile(state) &&\n (!selectIsTablet(state) || !selectIsPortrait(state));\n","import { State } from '../StateTypes';\nimport {\n ConfDialog,\n HasHadUserInteraction,\n HasShownExtendableConf,\n HasShownMountingRailSheet,\n HasShownPegboardHint,\n HasShownCuttableMountingRailHint,\n IntroPopupsVisible,\n Popups,\n ShowDoorsHint,\n ShowExtendableConf,\n ShowPegboardHint,\n ShowCuttableMountingRailHint,\n CuttableMountingRailHintAlignment,\n CuttableMountingRailHintCheckPending,\n ErrorHandlingPendingTimestamp,\n} from './popupsTypes';\nimport { selectIsMeasurementsActive } from '../scene/sceneSelectors';\n\n/**\n * Select popups state slice\n *\n * @param popups\n */\nexport const selectPopups = ({ popups }: State): Popups => popups;\n\n/**\n * Select has had user interaction\n *\n * @param state\n * @returns {Validator> | boolean}\n */\nexport const selectHasHadUserInteraction = (\n state: State\n): HasHadUserInteraction => selectPopups(state).hasHadUserInteraction;\n\n/**\n * Select has shown mounting rail sheet\n *\n * @param state\n * @returns {boolean}\n */\nexport const selectHasShownMountingRailSheet = (\n state: State\n): HasShownMountingRailSheet => selectPopups(state).hasShownMountingRailSheet;\n\n/**\n * Select conf dialog\n *\n * @param state\n * @returns {Requireable | {} | {open: boolean} | {closedId: *, open: boolean} | {closedId: null}}\n */\nexport const selectConfDialog = (state: State): ConfDialog =>\n selectPopups(state).confDialog;\n\n/**\n * Select has shown extendable conf\n *\n * @param state\n * @returns {boolean}\n */\nexport const selectHasShownExtendableConf = (\n state: State\n): HasShownExtendableConf => selectPopups(state).hasShownExtendableConf;\n\n/**\n * Select show extendable conf\n *\n * @param state\n * @returns {null|*}\n */\nexport const selectShowExtendableConf = (state: State): ShowExtendableConf =>\n selectPopups(state).showExtendableConf;\n\n/**\n * Select has shown pegboard hint\n *\n * @param state\n * @returns {boolean}\n */\nexport const selectHasShownPegboardHint = (\n state: State\n): HasShownPegboardHint => selectPopups(state).hasShownPegboardHint;\n\n/**\n * Select pegboard hint\n *\n * @param state\n * @returns {null | TacItem}\n */\nexport const selectPegboardHint = (state: State): ShowPegboardHint =>\n selectPopups(state).showPegboardHint;\n\n/**\n * Select has shown cuttable mounting rail hint\n *\n * @param state\n * @returns {boolean}\n */\nexport const selectHasShownCuttableMountingRailHint = (\n state: State\n): HasShownCuttableMountingRailHint =>\n selectPopups(state).hasShownCuttableMountingRailHint;\n\n/**\n * Select cuttable mounting rail hint\n *\n * @param state\n * @returns {null | TacItem}\n */\nexport const selectCuttableMountingRailHint = (\n state: State\n): ShowCuttableMountingRailHint =>\n selectPopups(state).showCuttableMountingRailHint;\n\n/**\n * Select cuttable mounting rail hint alignment\n *\n * @param state\n * @returns {null | string}\n */\nexport const selectCuttableMountingRailHintAlignment = (\n state: State\n): CuttableMountingRailHintAlignment =>\n selectPopups(state).cuttableMountingRailHintAlignment;\n\n/**\n * Select cuttable mounting rail hint check pending\n *\n * @param state\n * @returns {null | string}\n */\nexport const selectCuttableMountingRailHintCheckPending = (\n state: State\n): CuttableMountingRailHintCheckPending =>\n selectPopups(state).cuttableMountingRailHintCheckPending;\n\n/**\n * Select intro popups visible\n *\n * @param state\n * @returns {Requireable | boolean}\n */\nexport const selectIntroPopupsVisible = (state: State): IntroPopupsVisible =>\n selectPopups(state).introPopupsVisible;\n\n/**\n * Select doors popups visible\n *\n * @param state\n * @returns {Requireable | boolean}\n */\nexport const selectShowDoorsPopupHint = (state: State): ShowDoorsHint =>\n selectPopups(state).showDoorsHint;\n\n/**\n * Select has shown filter intro popup\n *\n * @param state\n */\nexport const selectHasShownFilterIntroPopup = (state: State) =>\n selectPopups(state).hasShownFilterIntroPopup;\n\n/**\n * Select filter intro popup visible\n *\n * @param state\n */\nexport const selectFilterIntroPopupVisible = (state: State) =>\n selectPopups(state).filterIntroPopupVisible;\n\n/**\n * Select should filter intro popup be visible\n *\n * @param state\n */\nexport const selectShouldFilterIntroPopupBeVisible = (state: State) =>\n !selectConfDialog(state).open &&\n !selectIsMeasurementsActive(state) &&\n !selectHasShownFilterIntroPopup(state);\n\n/** Select overlay active eg. if any element using an overlay is active\n *\n * @param state\n */\nexport const selectOverlayActive = (state: State) =>\n selectPopups(state).overlayActive;\n\n/** Select survey visible state\n *\n * @param state\n */\nexport const selectSurveyVisible = (state: State) =>\n selectPopups(state).surveyVisible;\n\n/** Select survey visible state\n *\n * @param state\n */\nexport const selectSurveyState = (state: State) =>\n selectPopups(state).surveyState;\n\n/** Select scroll exception eg. if we need to scroll in mobile\n *\n * @param state\n */\nexport const selectScrollException = (state: State) =>\n selectPopups(state).scrollException;\n\n/* Select has shown intro popups\n *\n * @param state\n */\nexport const selectHasShownIntroPopups = (state: State) =>\n selectPopups(state).hasShownIntroPopups;\n\n/* Select error handling pending timestamp\n *\n * @param state\n */\nexport const selectErrorHandlingPendingTimestamp = (\n state: State\n): ErrorHandlingPendingTimestamp =>\n selectPopups(state).errorHandlingPendingTimestamp;\n","import tacHelpers from './tacHelpers';\nimport { TacHistoryObject, TacModel } from './tacTypes';\nimport { State } from '../StateTypes';\nimport { Wall } from '../../generalTypes';\nimport { selectUseMetric } from '../dexfSettings/dexfSettingsSelectors';\nimport { mm } from '../../util/measures';\n\n/**\n * Selects current tac\n *\n * @param state\n * @returns TacModel | null\n */\nexport const selectTac = ({ tac: { present } }: State): TacModel | null =>\n present;\n\n/**\n * Select past tac\n *\n * @param past\n * @returns TacHistoryObject\n */\nexport const selectPastTac = ({ tac: { past } }: State): TacHistoryObject =>\n past;\n\n/**\n * Select future tac\n * @param future\n * @returns TacHistoryObject\n */\nexport const selectFutureTac = ({ tac: { future } }: State): TacHistoryObject =>\n future;\n\n/**\n * Returns true if tac scene is empty\n *\n * @param state\n * @returns {boolean}\n */\nexport const selectTacIsEmpty = (state: State): boolean =>\n !selectTac(state)?.items.some(item => tacHelpers.hasRealArticle(item));\n\n/**\n * Select \"disableMountingRails\" setting\n *\n * @param state\n * @returns {boolean}\n */\nexport const selectDisableMountingRails = (state: State): boolean =>\n selectTac(state)?.settings?.disableMountingRails;\n\n/**\n * Select has undo history\n *\n * @param state\n * @returns {*}\n */\nexport const selectHasUndoHistory = (state: State): boolean =>\n !!selectPastTac(state).length;\n\n/**\n * Select has redo history\n *\n * @param state\n * @returns {*}\n */\nexport const selectHasRedoHistory = (state: State): boolean =>\n !!selectFutureTac(state).length;\n\nexport const selectPresentWall = (state: State): Wall | undefined =>\n selectTac(state)?.wall;\n\nexport const selectWallPoints = (state: State): any =>\n selectPresentWall(state)?.points;\n\nexport const selectWallSize = (state: State): any =>\n selectPresentWall(state)?.size;\n\nexport const selectCeilingHeightBasedOnMarket = (\n state: State,\n height: number\n) => (selectUseMetric(state) ? mm.toCm(height) : mm.toInches(height));\n","import {\n POPUPS_HIDE_INTRO_POPUPS,\n POPUPS_SHOW_SCENE_ERRORS,\n POPUPS_HIDE_SCENE_ERRORS,\n POPUPS_CONF_OPEN,\n POPUPS_CONF_CLOSE,\n POPUPS_CONF_CLEAR,\n POPUPS_RESET_INTRO,\n SCENE_ITEM_PICKED_UP,\n POPUPS_MOUNTING_RAIL_SHEET_OPEN,\n POPUPS_SUPPLY_BANNER_CLOSE,\n POPUPS_SET_HAS_SHOWN_FILTER_INTRO_POPUP,\n POPUPS_SET_FILTER_INTRO_POPUP_VISIBLE,\n POPUPS_OVERLAY_ACTIVE,\n POPUPS_SET_INTRO_POPUPS_VISIBLE,\n POPUPS_SET_CUTTABLE_MOUNTING_RAIL_HINT_CHECK_PENDING,\n POPUPS_SET_HAS_SHOWN_INTRO_POPUPS,\n POPUPS_SCROLL_EXCEPTION,\n POPUPS_HANDLE_POPUPS_ON_ADD_ITEM,\n POPUPS_HANDLE_POPUPS_ON_UPDATE_ITEM,\n POPUPS_SET_SURVEY_VISIBLE,\n POPUPS_SET_SURVEY_STATE,\n POPUPS_SET_ERROR_HANDLING_PENDING_TIMESTAMP,\n} from '../actionConstants';\nimport { CustomAction, TacItem, RootAction } from '../../generalTypes';\nimport { SceneError } from './popupsTypes';\n\nexport const actionHideIntroPopups = (): RootAction => ({\n type: POPUPS_HIDE_INTRO_POPUPS,\n});\n\nexport const actionShowSceneErrors = (\n errors: SceneError[]\n): CustomAction<{ errors: SceneError[] }> => ({\n type: POPUPS_SHOW_SCENE_ERRORS,\n payload: {\n errors,\n },\n});\n\nexport const actionHideSceneErrors = (): RootAction => ({\n type: POPUPS_HIDE_SCENE_ERRORS,\n});\n\nexport const actionConfOpened = (): RootAction => ({ type: POPUPS_CONF_OPEN });\n\nexport const actionConfClosed = (\n closedId: number | undefined\n): CustomAction<{ closedId: number | undefined }> => ({\n type: POPUPS_CONF_CLOSE,\n payload: {\n closedId,\n },\n});\n\nexport const actionClearPreviousConf = (): RootAction => ({\n type: POPUPS_CONF_CLEAR,\n});\n\nexport const resetIntroPopups = (): RootAction => ({\n type: POPUPS_RESET_INTRO,\n});\n\nexport const actionItemPickedUp = (\n item: TacItem\n): CustomAction<{ item: TacItem }> => ({\n type: SCENE_ITEM_PICKED_UP,\n payload: {\n item: item,\n },\n});\n\nexport const mountingRailSheetOpened = (): RootAction => ({\n type: POPUPS_MOUNTING_RAIL_SHEET_OPEN,\n});\n\nexport const actionSetSurveyVisible = (surveyVisible: boolean) => ({\n type: POPUPS_SET_SURVEY_VISIBLE,\n payload: { surveyVisible },\n});\n\nexport const actionSetSurveyState = (surveyState: string) => ({\n type: POPUPS_SET_SURVEY_STATE,\n payload: { surveyState },\n});\n\nexport const actionSetErrorHandlingPendingTimestamp = (\n errorHandlingPendingTimestamp: null | number\n) => ({\n type: POPUPS_SET_ERROR_HANDLING_PENDING_TIMESTAMP,\n payload: { errorHandlingPendingTimestamp },\n});\n\nexport const closeSupplyBanner = (): RootAction => ({\n type: POPUPS_SUPPLY_BANNER_CLOSE,\n});\n\nexport const actionSetHasShownFilterIntroPopup = (\n hasShownFilterIntroPopup: boolean\n) => ({\n type: POPUPS_SET_HAS_SHOWN_FILTER_INTRO_POPUP,\n payload: { hasShownFilterIntroPopup },\n});\n\nexport const actionSetFilterIntroPopupBeVisible = (\n filterIntroPopupVisible: boolean\n) => ({\n type: POPUPS_SET_FILTER_INTRO_POPUP_VISIBLE,\n payload: { filterIntroPopupVisible },\n});\n\nexport const actionSetOverlayState = (\n state: boolean\n): CustomAction<{ state: boolean }> => ({\n type: POPUPS_OVERLAY_ACTIVE,\n payload: {\n state,\n },\n});\n\nexport const actionSetScrollException = (\n state: boolean\n): CustomAction<{ state: boolean }> => ({\n type: POPUPS_SCROLL_EXCEPTION,\n payload: {\n state,\n },\n});\n\nexport const actionSetIntroPopupsVisible = (introPopupsVisible: boolean) => ({\n type: POPUPS_SET_INTRO_POPUPS_VISIBLE,\n payload: {\n introPopupsVisible,\n },\n});\n\nexport const actionSetCuttableMountingRailHintCheckPending = (\n cuttableMountingRailHintCheckPending: boolean\n) => ({\n type: POPUPS_SET_CUTTABLE_MOUNTING_RAIL_HINT_CHECK_PENDING,\n payload: {\n cuttableMountingRailHintCheckPending,\n },\n});\n\nexport const actionSetHasShownIntroPopups = () => ({\n type: POPUPS_SET_HAS_SHOWN_INTRO_POPUPS,\n});\n\nexport const actionHandlePopupOnAddItem = (newValues: any) => ({\n type: POPUPS_HANDLE_POPUPS_ON_ADD_ITEM,\n payload: newValues,\n});\n\nexport const actionHandlePopupOnUpdateItem = (newValues: any) => ({\n type: POPUPS_HANDLE_POPUPS_ON_UPDATE_ITEM,\n payload: newValues,\n});\n","import helpers from '../tacHelpers';\nimport _ from 'lodash';\nimport { replace } from '../replace';\n\nexport default function removeItem(model, item) {\n const parent = helpers.getParent(model, item);\n\n if (!parent) {\n return false;\n }\n\n const newParent = {\n ...parent,\n items: parent.items.filter(i => i.itemid !== item.itemid),\n };\n\n if (parent === model) {\n return newParent;\n } else {\n const out = _.cloneDeep(model);\n replace(out.items, newParent);\n return out;\n }\n}\n","import geometry from '../../../scene/util/geometry';\nimport tacHelpers from '../tacHelpers';\nimport sorter from './sorter';\nimport removeItem from './removeItem';\nimport _ from 'lodash';\nimport { replace } from '../replace';\nimport makeSpaceForChild from './makeSpaceForChild';\nimport connectParts from './connectParts';\nimport constants from '../../../settings/constants';\nimport productService from '../../../services/products';\nimport { isFixedRoom } from '../../../util/room';\n\nfunction closer(a, b) {\n return a.x > b.x ? 1 : a.x < b.x ? -1 : 0;\n}\n\nfunction getMovedX(oldItem, newItem) {\n return oldItem.x - newItem.x && oldItem.x - newItem.x;\n}\n\nfunction closest(items, direction = 'right') {\n if (direction === 'right') {\n return items.sort(closer)[0];\n }\n return items.sort(closer).reverse()[0];\n}\n\nfunction moveRight(item, originalItem, newItems, tac, initial = false) {\n const fullSizeItem = tacHelpers.getFullSize(item);\n const originalFullSizeItem = tacHelpers.getFullSize(originalItem);\n const skippedItems = !initial\n ? newItems.filter(item => item.x <= originalItem.x)\n : [];\n const items = newItems.filter(right => right.x > originalItem.x);\n\n const collidingItems = geometry.getCollidingRects(\n fullSizeItem,\n items.map(item => Object.assign({}, item, tacHelpers.getFullSize(item)))\n );\n\n if (collidingItems.length === 0) {\n return [...items, ...skippedItems];\n }\n\n const closestItem = closest(collidingItems, 'right');\n\n let distance;\n if (\n originalFullSizeItem.verticalExtensionPoint &&\n originalFullSizeItem.verticalExtensionPoint >\n closestItem.y + closestItem.height\n ) {\n /*\n Current case being a smaller frame/shelving unit placed under a clothes rail.\n */\n if (item.x + item.width - closestItem.x > 0) {\n distance = item.x + item.width - closestItem.x;\n } else {\n // No need to move.\n distance = 0;\n }\n } else {\n const overlappingItem =\n tacHelpers.hasExtClothesRail(fullSizeItem) ||\n !geometry.collides(originalItem, closestItem)\n ? fullSizeItem\n : tacHelpers.withAllowedOverlaps(fullSizeItem, tac, closestItem);\n distance = overlappingItem.x + overlappingItem.width - closestItem.x;\n }\n\n const nonFullSizeClosestItem = items.find(\n item => item.itemid === closestItem.itemid\n );\n const movedItem = {\n ...nonFullSizeClosestItem,\n x: closestItem.x + distance,\n };\n\n const otherItems = items.filter(i => i.itemid !== movedItem.itemid);\n\n const remaining = moveRight(\n movedItem,\n nonFullSizeClosestItem,\n otherItems,\n tac\n );\n const reaminingCollisions = geometry.getCollidingRects(\n fullSizeItem,\n remaining.map(item => Object.assign({}, item, tacHelpers.getFullSize(item)))\n );\n\n if (reaminingCollisions.length === 0) {\n return [movedItem, ...skippedItems].concat(remaining);\n }\n\n const retry = moveRight(item, originalItem, remaining, tac);\n\n return [movedItem, ...skippedItems].concat(retry);\n}\n\nfunction moveLeft(item, originalItem, newItems, tac, initial = false) {\n const fullSizeItem = tacHelpers.getFullSize(item);\n const skippedItems = !initial\n ? newItems.filter(item => item.x >= originalItem.x)\n : [];\n const items = newItems.filter(left => left.x < originalItem.x);\n\n const collidingItems = geometry.getCollidingRects(\n fullSizeItem,\n items.map(item => Object.assign({}, item, tacHelpers.getFullSize(item)))\n );\n if (collidingItems.length === 0) {\n return [...items, ...skippedItems];\n }\n const closestItem = closest(collidingItems, 'left');\n\n const nonFullSizeClosestItem = items.find(\n item => item.itemid === closestItem.itemid\n );\n\n let distance;\n if (\n closestItem.verticalExtensionPoint &&\n closestItem.verticalExtensionPoint > fullSizeItem.y + fullSizeItem.height\n ) {\n /*\n Current case being a smaller frame/shelving unit placed under a clothes rail.\n */\n if (nonFullSizeClosestItem.x + nonFullSizeClosestItem.width > item.x) {\n distance =\n item.x - (nonFullSizeClosestItem.x + nonFullSizeClosestItem.width);\n } else {\n // No need to move.\n distance = 0;\n }\n } else {\n const overlappingItem =\n tacHelpers.hasExtClothesRail(closestItem) ||\n !geometry.collides(originalItem, closestItem)\n ? fullSizeItem\n : tacHelpers.withAllowedOverlaps(fullSizeItem, tac, closestItem);\n distance = overlappingItem.x - closestItem.x - closestItem.width;\n }\n\n const movedItem = {\n ...nonFullSizeClosestItem,\n x: closestItem.x + distance,\n };\n\n const otherItems = items.filter(i => i.itemid !== movedItem.itemid);\n\n const remaining = moveLeft(\n movedItem,\n nonFullSizeClosestItem,\n otherItems,\n tac\n );\n\n const reaminingCollisions = geometry.getCollidingRects(\n fullSizeItem,\n remaining.map(item => Object.assign({}, item, tacHelpers.getFullSize(item)))\n );\n\n if (reaminingCollisions.length === 0) {\n return [movedItem, ...skippedItems].concat(remaining);\n }\n\n const retry = moveLeft(item, originalItem, remaining, tac);\n\n return [movedItem, ...skippedItems].concat(retry);\n}\n\nfunction tryMoveItems(\n item,\n oldItem,\n moveNewItemX,\n otherItems,\n potentialBlockers,\n tac\n) {\n const recenteredItem = { ...item, x: item.x + moveNewItemX };\n const groupedItems = _.partition(otherItems, productService.isDependentItem);\n otherItems = groupedItems[1];\n\n const rightItems = moveRight(recenteredItem, oldItem, otherItems, tac, true);\n const leftItems = moveLeft(recenteredItem, oldItem, otherItems, tac, true);\n const centerItems = otherItems.filter(center => center.x === item.x);\n const allItems = [\n recenteredItem,\n ...leftItems,\n ...rightItems,\n ...centerItems,\n ...groupedItems[0],\n ];\n\n const uniqueItems = _.uniqBy(allItems, 'itemid');\n if (uniqueItems.length !== allItems.length) {\n throw new Error('Duplicate items after move');\n }\n\n let newSize;\n let newLimits;\n let newMaxX;\n const isFixedContext = isFixedRoom();\n\n if (isFixedContext) {\n newLimits = tacHelpers.getLimits({\n items: allItems,\n });\n newMaxX = newLimits.max.x;\n } else {\n newSize = tacHelpers.getSize({\n items: allItems,\n });\n newMaxX = newSize.width;\n }\n\n // Assume move validity until proven guilty\n let result = { valid: true, recenteredItem, allItems };\n const wallLimits = tacHelpers.getWallLimits(tac);\n\n if (newMaxX > constants.ROOM_MAX.width) {\n return {\n ...result,\n valid: false,\n invalidityMargin: newMaxX - constants.ROOM_MAX.width,\n };\n } else if (isFixedContext && newLimits.max.x > wallLimits.max.x) {\n return {\n ...result,\n valid: false,\n invalidityMargin: newLimits.max.x - wallLimits.max.x,\n };\n } else if (isFixedContext && newLimits.min.x < wallLimits.min.x) {\n return {\n ...result,\n valid: false,\n invalidityMargin: -newLimits.min.x,\n };\n } else if (!potentialBlockers || !potentialBlockers.length) {\n return result;\n } else {\n // ...and we also won't allow neither one of the blockers two top ancestors to move at all\n const allItemsGlobalPos = tacHelpers.getAllItems(allItems).map(item => {\n return {\n ...item,\n ...tacHelpers.getGlobalCoords(item, { items: allItems }),\n };\n });\n\n potentialBlockers.forEach(blocker => {\n const topAncestorGlobal = allItemsGlobalPos.find(\n item => item.itemid === blocker.topAncestor.itemid\n );\n const connectedGlobal = allItemsGlobalPos.find(\n item => item.itemid === blocker.connectsTo.itemid\n );\n const invalidityMargin =\n (topAncestorGlobal &&\n getMovedX(blocker.topAncestor, topAncestorGlobal)) ||\n (connectedGlobal && getMovedX(blocker.connectsTo, connectedGlobal));\n\n if (invalidityMargin) {\n result = {\n ...result,\n valid: false,\n invalidityMargin,\n };\n }\n });\n }\n\n return result;\n}\n\n/**\n * Gets a new version of the TAC with a specific item updated\n * @param {*} model\n * @param {*} item\n * @param {*} parent\n * @param {*} options\n * @returns {Object}\n */\nexport default function updateItem(model, item, parent = model, options = {}) {\n if (!item.itemid) {\n throw new Error('Cannot update an item that is not already in the model');\n }\n\n if (parent !== model) {\n parent = tacHelpers.getItem(model, parent.itemid);\n }\n\n let previousParent;\n const oldItem = tacHelpers.getItem(model, item.itemid);\n const moveNewItemX =\n options.moveOthers && parent === model\n ? Math.ceil((oldItem.width - item.width) / 2)\n : 0;\n\n if (\n !parent.items ||\n !parent.items.find(child => child.itemid === item.itemid)\n ) {\n previousParent = tacHelpers.getParent(model, item);\n\n if (previousParent) {\n const newModel = removeItem(model, item, previousParent);\n\n if (model === parent) {\n parent = newModel;\n } else {\n // update parent if item was moved here from a descendant\n parent = tacHelpers.getItem(newModel, parent.itemid);\n }\n\n model = newModel;\n }\n }\n\n const otherItems = parent.items\n ? parent.items.filter(i => i.itemid !== item.itemid)\n : [];\n\n let updatedItem;\n let allItems;\n if (!options.moveOthers) {\n updatedItem = { ...item, x: item.x + moveNewItemX };\n allItems = [updatedItem, ...otherItems];\n } else {\n const potentialBlockers = tacHelpers.getPotentialBlockers(\n { items: parent.items || [] },\n oldItem\n );\n let moveResult = tryMoveItems(\n item,\n oldItem,\n moveNewItemX,\n [...otherItems],\n potentialBlockers,\n model\n );\n\n if (!moveResult.valid) {\n /*\n We could not rotate item around it's own axis without exceeding max width,\n but let's see if there is enough space to rotate at all.\n */\n const originalMargin = moveResult.invalidityMargin;\n\n //First, let's try to the right...\n let newMoveDistance = moveNewItemX + moveResult.invalidityMargin;\n\n moveResult = tryMoveItems(\n item,\n oldItem,\n newMoveDistance,\n [...otherItems],\n potentialBlockers,\n model\n );\n\n if (!moveResult.valid) {\n // ...and then to the left.\n newMoveDistance = moveNewItemX - originalMargin;\n\n moveResult = tryMoveItems(\n item,\n oldItem,\n newMoveDistance,\n [...otherItems],\n potentialBlockers,\n model\n );\n\n if (!moveResult.valid) {\n // Item cannot be rotated so update is invalid.\n return false;\n }\n }\n }\n\n updatedItem = moveResult.recenteredItem;\n allItems = moveResult.allItems;\n }\n\n const sortedItems = options.postponeSort\n ? allItems\n : sorter.inDrawOrder(allItems);\n let newParent = {\n ...parent,\n items: sortedItems,\n };\n\n // When updating we need to reconnect all our parts to see if they still fit\n if (item.parts && !options.keepParts) {\n const newItem = newParent.items.find(\n parentItem => parentItem.itemid === item.itemid\n );\n // Remove old parts from items\n newItem.items = newItem.items\n ? newItem.items.filter(\n i =>\n ![\n ...Object.values(newItem.parts),\n ...Object.values(oldItem.parts),\n ].some(part => part === i.id)\n )\n : [];\n\n newParent = connectParts(newItem, newParent, parent, model, options);\n }\n\n makeSpaceForChild(updatedItem, newParent, model);\n\n if (parent === model) {\n return newParent;\n } else {\n const out = _.cloneDeep(model);\n replace(out.items, newParent);\n return out;\n }\n}\n","import { isExtendable, isUpright } from '../../services/products';\nimport { applicationSettings } from '../../settings/application';\nimport { getDependencyDiff } from '../tac/range/boaxel/dependentItems';\nimport updateItem from '../tac/tacReducer/updateItem';\nimport addItem from '../tac/tacReducer/addItem';\n\nfunction ignoredAutoUpdate(meta) {\n return (\n ['JONAXEL', 'AURDAL'].includes(applicationSettings.applicationName) &&\n meta?.automatedUpdate\n );\n}\n\nfunction adjustableItem(item, meta) {\n if (\n ['JONAXEL', 'AURDAL'].includes(applicationSettings.applicationName) &&\n isExtendable(item.id)\n ) {\n return item;\n }\n if (applicationSettings.applicationName === 'BOAXEL') {\n if (!isUpright(item.id) || meta.origin === 'dialog') {\n return false;\n }\n const updatedTac = item.itemid\n ? updateItem(meta.tac, item)\n : addItem(meta.tac, item);\n\n const diff = getDependencyDiff(updatedTac, { triggerItem: item });\n return [...diff.added, ...diff.updated].find(isExtendable);\n }\n}\n\nexport function extendableConfItem(item, meta) {\n return !ignoredAutoUpdate(meta) && adjustableItem(item, meta);\n}\n","export const TOP = 'TOP';\nexport const LEFT = 'LEFT';\nexport const RIGHT = 'RIGHT';\nexport const BOTTOM = 'BOTTOM';\n\nexport const all = [TOP, LEFT, RIGHT, BOTTOM];\n","import {\n selectHasShownIntroPopups,\n selectPopups,\n selectConfDialog,\n selectCuttableMountingRailHintCheckPending,\n} from './popupsSelectors';\nimport { selectTac } from '../tac/tacSelectors';\nimport {\n actionConfClosed,\n actionHandlePopupOnAddItem,\n actionHandlePopupOnUpdateItem,\n actionSetHasShownIntroPopups,\n actionSetIntroPopupsVisible,\n actionSetCuttableMountingRailHintCheckPending,\n} from './popupsActions';\nimport { Thunk } from '../../generalTypes';\nimport { MountingRailThunkParams } from './popupsTypes';\nimport productsServiceCommon, {\n isPegboard,\n shouldShowDoorHint,\n} from '../../services/products';\nimport { extendableConfItem } from './extendableConfItem';\nimport { range } from '../tac/range';\nimport { ITEMS } from '../../constants';\nimport { LEFT, RIGHT } from '../../components/Popup/alignments';\nimport constants from '../../settings/constants';\n\nexport const thunkConditionalDisplayIntroPopups: Thunk =\n () => (dispatch, getState) => {\n const hasShownIntroPopups = selectHasShownIntroPopups(getState());\n if (!hasShownIntroPopups) {\n dispatch(actionSetIntroPopupsVisible(true));\n dispatch(actionSetHasShownIntroPopups());\n }\n };\n\nexport const thunkConfClosed: Thunk =\n (closedId: number | undefined) => (dispatch, getState) => {\n dispatch(actionConfClosed(closedId));\n closedId && dispatch(actionSetCuttableMountingRailHintCheckPending(true));\n };\n\nexport const thunkHandlePopupOnAddItem: Thunk =\n (item, meta) => (dispatch, getState) => {\n const popUpsState = selectPopups(getState());\n\n if (!popUpsState.hasShownPegboardHint && isPegboard(item.id)) {\n return dispatch(\n actionHandlePopupOnAddItem({\n showPegboardHint: item,\n hasShownPegboardHint: true,\n hasHadUserInteraction: true,\n })\n );\n }\n\n if (shouldShowDoorHint(item.id)) {\n return dispatch(\n actionHandlePopupOnAddItem({\n showDoorsHint: item,\n hasHadUserInteraction: true,\n })\n );\n }\n\n if (!popUpsState.hasShownExtendableConf) {\n const confItem = extendableConfItem(item, meta);\n if (confItem) {\n return dispatch(\n actionHandlePopupOnAddItem({\n showExtendableConf: confItem,\n hasShownExtendableConf: true,\n hasHadUserInteraction: true,\n })\n );\n }\n }\n\n dispatch(actionHandlePopupOnAddItem({ hasHadUserInteraction: true }));\n };\n\nexport const thunkHandlePopupOnUpdateItem: Thunk =\n (item, meta) => (dispatch, getState) => {\n const popUpsState = selectPopups(getState());\n\n if (!popUpsState.hasShownPegboardHint && isPegboard(item.id)) {\n return dispatch(\n actionHandlePopupOnUpdateItem({\n showPegboardHint: item,\n hasShownPegboardHint: true,\n hasHadUserInteraction:\n popUpsState.hasHadUserInteraction || !meta?.automatedUpdate,\n })\n );\n }\n\n if (!popUpsState.hasShownExtendableConf) {\n const confItem = extendableConfItem(item, meta);\n if (confItem) {\n return dispatch(\n actionHandlePopupOnUpdateItem({\n showExtendableConf: confItem,\n hasShownExtendableConf: true,\n hasHadUserInteraction:\n popUpsState.hasHadUserInteraction || !meta?.automatedUpdate,\n })\n );\n }\n }\n\n if (popUpsState.hasShownExtendableConf) {\n return dispatch(\n actionHandlePopupOnUpdateItem({\n showExtendableConf: null,\n hasShownExtendableConf: true,\n hasHadUserInteraction:\n popUpsState.hasHadUserInteraction || !meta?.automatedUpdate,\n })\n );\n }\n\n dispatch(\n actionHandlePopupOnUpdateItem({\n hasHadUserInteraction:\n popUpsState.hasHadUserInteraction || !meta?.automatedUpdate,\n })\n );\n };\n\nexport const thunkHandlePopupOnRemoveItem: Thunk =\n () => (dispatch, getState) => {\n const popUpsState = selectPopups(getState());\n\n if (popUpsState.hasShownExtendableConf) {\n dispatch(\n actionHandlePopupOnUpdateItem({\n showExtendableConf: null,\n hasShownExtendableConf: true,\n })\n );\n }\n };\n\nexport const thunkHandlePopupOnTacLoad: Thunk = () => dispatch => {\n dispatch(\n actionHandlePopupOnUpdateItem({\n showExtendableConf: null,\n hasShownExtendableConf: false,\n })\n );\n};\n\nexport const thunkHandleMountingRailPopup: Thunk =\n (params: MountingRailThunkParams) => (dispatch, getState) => {\n const wasPending = selectCuttableMountingRailHintCheckPending(getState());\n wasPending &&\n dispatch(actionSetCuttableMountingRailHintCheckPending(false));\n\n const protrusionThreshold =\n constants.CUTTABLE_MOUNTING_RAIL_PROTRUSION_THRESHOLD || Infinity;\n\n const tac = params?.tac || selectTac(getState());\n const popUpsState = selectPopups(getState());\n const confDialog = selectConfDialog(getState());\n const confDialogIsClosed = !confDialog.open;\n if (\n tac &&\n !popUpsState.hasShownCuttableMountingRailHint &&\n (params?.omitExtendedConfClosedCheck || confDialogIsClosed)\n ) {\n const cuttableMountingRailData = range.getCuttableMountingRailData(tac);\n\n const sumOfWidthsOf = (itemsArray: any) =>\n itemsArray.reduce(\n (sum: number, currentItem: any) => (sum += currentItem.width),\n 0\n );\n\n const candidates: any = [];\n cuttableMountingRailData.forEach((group: any) => {\n const { mountingRails, areaToCoverStartX, areaToCoverEndX } = group;\n const lengthOfMountingRailGroup = sumOfWidthsOf(mountingRails);\n const leftmostRailInGroup = mountingRails[0];\n const rightmostRailInGroup = mountingRails[mountingRails.length - 1];\n const mountingRailsStartX = mountingRails[0].x;\n const mountingRailsEndX =\n mountingRailsStartX + lengthOfMountingRailGroup;\n const protrusionLeft = areaToCoverStartX - mountingRailsStartX;\n const protrusionRight = mountingRailsEndX - areaToCoverEndX;\n const canCutLeft = protrusionLeft > protrusionThreshold;\n const canCutRight = protrusionRight > protrusionThreshold;\n const distanceToLeftWall = mountingRailsStartX;\n const distanceToRightWall = tac.wall?.size?.width\n ? tac.wall.size.width - mountingRailsEndX\n : 0;\n\n canCutLeft &&\n candidates.push({\n railItem: leftmostRailInGroup,\n distanceToWall: distanceToLeftWall,\n hintAlignment: LEFT,\n });\n canCutRight &&\n candidates.push({\n railItem: rightmostRailInGroup,\n distanceToWall: distanceToRightWall,\n hintAlignment: RIGHT,\n });\n });\n\n const chosenCandidate = candidates.reduce(\n (bestSoFar: any, candidate: any) =>\n candidate.distanceToWall > (bestSoFar?.distanceToWall || -Infinity)\n ? candidate\n : bestSoFar,\n null\n );\n\n const chosenMountingRail = chosenCandidate?.railItem;\n const hintAlignmentOfChosenMountingRail = chosenCandidate?.hintAlignment;\n\n /* The mounting rail object of the candidate is newly generated and doesn't\n have the itemid set, since it's not in the TAC. We need to find the\n corresponding mounting rail in the TAC before we proceed. */\n const chosenMountingRailInTac = chosenMountingRail\n ? tac.items.find(\n (tacItem: any) =>\n productsServiceCommon.isType(tacItem.id, ITEMS.MOUNTING_RAIL) &&\n tacItem.x === chosenMountingRail.x &&\n tacItem.y === chosenMountingRail.y &&\n tacItem.z === chosenMountingRail.z\n )\n : null;\n\n if (chosenMountingRailInTac) {\n dispatch(\n actionHandlePopupOnAddItem({\n showCuttableMountingRailHint: chosenMountingRailInTac,\n cuttableMountingRailHintAlignment:\n hintAlignmentOfChosenMountingRail,\n hasShownCuttableMountingRailHint: true,\n hasHadUserInteraction: true,\n })\n );\n }\n }\n };\n","import DexfTypeCodeToGlobalTypeMap from '../../services/products/DexfTypeCodeToGlobalTypeMap.json';\nimport productService, { getProduct } from '../../services/products';\nimport constants from '../../settings/constants';\nimport articles, { getArticleContent } from '../../services/products/articles';\nimport {\n DimensionRemapObject,\n MeasurementsRule,\n MeasurementsSetting,\n MinMaxObject,\n ProductsByIdObject,\n} from './productsTypes';\nimport { TacItem, Itemid, Itemids, IowsArticle } from '../../generalTypes';\nimport { TacItems } from '../rawData/RawDataTypes';\nimport { digAllItems } from '../tac/tacHelpers';\n\n/**\n * Returns the global type name based on type code.\n *\n * @param {string} typeCode Type code.\n * @returns {string} Global type name.\n */\nexport const getGlobalType = (typeCode: string): string | null =>\n // @ts-ignore\n DexfTypeCodeToGlobalTypeMap[typeCode] || null;\n\n/**\n * Returns all products as an object with productID as key\n *\n * @returns {object}\n */\nexport const getProductsAsObject = (): ProductsByIdObject =>\n productService\n .getAll()\n .reduce((acc, curr) => ({ ...acc, [curr.id]: curr }), {});\n\n/**\n * Returns a set of the different dimensions for all products\n * @returns Set\n */\nexport const getAllDimensionsKeys = () =>\n productService.getAll().reduce((acc, { id }) => {\n const article = articles.getArticle(id);\n\n if (article?.content?.measure)\n article.content.measure.forEach(({ typeCode }: any) => {\n const globalType = getGlobalType(typeCode);\n globalType && acc.add(globalType.toLowerCase());\n });\n\n return acc;\n }, new Set());\n\n/**\n * Checks that we have a product and that settings are declared for said product\n *\n * @param product {object}\n * @returns {*}\n */\nexport const productSettingsExists = (product: TacItem): MeasurementsRule =>\n product && getProductMeasurementSettings(product.filter.type);\n\n/**\n * Derives the specific product settings from the ranges settings file based on product type\n *\n * @param productType {string}\n * @returns {*}\n */\nexport const getProductMeasurementSettings = (\n productType: string\n): MeasurementsRule =>\n constants.MEASUREMENTS_SETTINGS.displayMeasurement.find(\n ({ affects }: MeasurementsSetting) => affects?.includes(productType)\n )?.rules;\n\n/**\n * Retrieves the index of a remap by product ids\n *\n * @param productIds {array}\n * @returns {*}\n */\nconst getIndexOfRemap = (productIds: Itemids): number =>\n productIds.findIndex((id: Itemid) =>\n constants.MEASUREMENTS_SETTINGS.productsRemap.find(\n ({ affects }: DimensionRemapObject) => affects.includes(id)\n )\n );\n\n/**\n * Returns remap keys for a product if there are any\n *\n * @param productIds {array}\n * @returns {*}\n */\nexport const getProductMeasurementRemapKeys = (\n productIds: Itemids\n): DimensionRemapObject => {\n const index: number = getIndexOfRemap(productIds);\n\n return index > -1\n ? constants.MEASUREMENTS_SETTINGS.productsRemap[index]\n : false;\n};\n\n/**\n * Returns smallest and largest value from an array as an object\n *\n * @param values {array}\n * @returns {{min: number, max: number}}\n */\nexport const getSmallestAndLargest = (values: number[]): MinMaxObject => ({\n min: Math.min.apply(Math, values),\n max: Math.max.apply(Math, values),\n});\n\n/**\n * Used to get parts that are in the\n * iows object but not in the parts object.\n * @example shelf-clothes-rail Elvarli\n */\nconst getIowsItemsAsIds = (iows: IowsArticle[]) =>\n iows.reduce((acc: string[], curr: IowsArticle) => [...acc, curr.itemno], []);\n\nexport const getAllPartsAsIdsFromItem = (item: TacItem): Itemid[] => {\n const { parts, iows } = item;\n // If the original item id is not removed, the recursive function will call itself for ever.\n const removeIowsIdSameAsOriginalItemId = (iowsIds: string[]) =>\n iowsIds.filter(iowsId => iowsId !== item.id);\n\n const partsArray = parts ? Object.values(parts) : [];\n const iowsArray = iows\n ? removeIowsIdSameAsOriginalItemId(getIowsItemsAsIds(iows))\n : [];\n return [...partsArray, ...iowsArray].reduce((acc: any[], currPart) => {\n const product = getProduct(currPart, true);\n return product && product.parts\n ? [...acc, currPart, ...getAllPartsAsIdsFromItem(product)]\n : [...acc, currPart];\n }, []);\n};\n\nexport const getAllPartsAsIdsFromItems = (items: TacItems): Itemid[][] => {\n const iterableRawData: TacItem[] = Object.values(items);\n return [\n ...iterableRawData.reduce((acc: Itemid[][], curr: TacItem) => {\n return curr.parts || curr.iows.length\n ? [...acc, getAllPartsAsIdsFromItem(curr)]\n : [...acc];\n }, []),\n ];\n};\nconst removeDuplicates = (array: any[]) => [...new Set(array)];\n\nexport const getAllPartsAsProductsFromItem = (item: TacItem): TacItem[] => {\n return removeDuplicates(\n getAllPartsAsIdsFromItem(item).reduce((acc: TacItem[], part: string) => {\n return [...acc, getProduct(part, true)];\n }, [])\n );\n};\n\n/**\n * Finds and returns all parts that are also part of the item in its current state.\n * @example shelf-drawer sometimes has brackets and sometimes not depending\n * on whether its attached to side-units or posts.\n * @param parts\n * @returns\n */\nexport const getAllPartsThatAreAlsoAttachedAsProducts = (item: TacItem) => {\n const allItems = digAllItems([item]);\n const partsIds = getAllPartsAsIdsFromItem(item);\n const iowsIds = getIowsItemsAsIds(item.iows);\n return removeDuplicates(\n [...partsIds, ...iowsIds]\n .filter(id =>\n allItems.find(\n (item: TacItem) =>\n item.id === id ||\n getIowsItemsAsIds(item.iows).some(iowsId => iowsId === id)\n )\n )\n .reduce(\n (acc: TacItem[], curr: string) => [...acc, getProduct(curr, true)],\n []\n )\n );\n};\n\nconst getProductIowsId = (product: TacItem) => product.iows[0]?.itemno;\n\nexport const getCommunicatedItemName = (item: TacItem): string => {\n const itemId = getProductIowsId(item);\n if (!itemId) return '';\n const article = getArticleContent(itemId);\n\n const capitalizeFirstLetter = (name: string) =>\n name.charAt(0).toUpperCase() + name.slice(1);\n return `${capitalizeFirstLetter(article.typeName)}`;\n};\n\nexport const getCommunicatedItemImageUrl = (item: TacItem): any => {\n const itemId = getProductIowsId(item);\n if (!itemId) return '';\n const article = getArticleContent(itemId);\n return article.image[0].url || '';\n};\n\nexport const isCombinedItem = (item: TacItem): boolean =>\n getAllPartsThatAreAlsoAttachedAsProducts(item).length > 1;\n","import { State } from '../StateTypes';\nimport { Itemid } from '../../generalTypes';\nimport { Product } from './productsTypes';\n\n/**\n * Selects related ids by id\n *\n * @param state {object}\n * @param id {string}\n * @returns {*}\n */\nexport const selectRelatedIdsById = (\n state: State,\n id: Itemid\n): Product | undefined =>\n state.products.find(({ products }) => products.includes(id));\n","import tacHelpers from '../tacHelpers';\n\n/**\n * Selects current tac\n * @param state\n * @returns {(function(*=): boolean)|{settings: {disableMountingRails: boolean}}|{items: [], wall: {points: *}}|State|(function(*=): boolean)}\n */\nexport const selectPresentTac = state => state.tac.present;\n\n/**\n * Returns true if tac scene is empty\n * @param state\n * @returns {boolean}\n */\nexport const selectTacIsEmpty = state => {\n const tac = selectPresentTac(state);\n return !tac?.items.some(item => tacHelpers.hasRealArticle(item));\n};\n","import tacHelpers from '../../state/tac/tacHelpers';\nimport { range } from '../../state/tac/range';\nimport {\n selectCombinedAvailableColors,\n selectFiltersWithColorOptions,\n} from '../../state/productMenu/productMenuSelectors';\nimport { actionSetSelectableColors } from '../../state/productMenu/productMenuActions';\n\nexport function hasRoomSlots(tac, space, item, extraFilter) {\n const itemSpace = tacHelpers.getSpaceForItem(space, item);\n const dependentItems = range.getDependentItems(item, tac);\n const fittingTac = tacHelpers.filterTac(\n tac,\n dependentItems.map(item => item.itemid)\n );\n const rects = tacHelpers.getRects(fittingTac, item, tac);\n const roomSlots = tacHelpers.getRoomSlots(itemSpace, rects, item, item, tac);\n\n if (extraFilter) {\n return roomSlots.filter(extraFilter).length;\n }\n return roomSlots.length;\n}\n\nexport const thunkSetSelectableColors = () => (dispatch, getState) => {\n const filters = selectFiltersWithColorOptions(getState());\n\n filters.forEach(({ name: filterName }) => {\n const availableColors = selectCombinedAvailableColors(\n getState(),\n filterName\n );\n dispatch(actionSetSelectableColors(availableColors, filterName));\n });\n};\n","import productService from '../../products';\nimport tacHelpers from '../../../state/tac/tacHelpers';\nimport { translate } from '../../L10n';\nimport { t } from '../../../translations';\nimport { hasRoomSlots } from '../common';\nimport { ITEMS } from '../../../constants';\nimport _ from 'lodash';\n\nfunction getWontFitMsg(product) {\n if (productService.isCabinet(product)) {\n return translate(t.CANNOT_FIT_CABINET);\n }\n if (productService.isType(product, ITEMS.DIVIDER)) {\n return translate(t.CANNOT_FIT_SHELF_DIVIDER);\n }\n\n return translate(t.BALLOON_HINT_NO_PLACE);\n}\n\nfunction couldItemFit(tac, product) {\n const fakeTac = _.cloneDeep(tac);\n const offset = productService.getSectionOffset(product);\n\n const tacSections = tacHelpers.getSections(tac, {});\n const nonMatchingTacSections = tacSections.filter(\n section =>\n product.filter.depth + offset.z * 2 !== section.filter.depth ||\n product.filter.width + offset.x * 2 !== section.filter.width\n );\n\n nonMatchingTacSections.forEach(section => {\n const matchingSection = productService.getSections(fakeTac, {\n width: product.filter.width + offset.x * 2,\n depth: product.filter.depth + offset.z * 2,\n height: section.filter.height,\n })[0];\n\n const newSection = tacHelpers.getSwitchableItem(section, matchingSection, {\n switchingProp: 'depth',\n });\n\n const replaceIndex = fakeTac.items.findIndex(\n item => item.itemid === newSection.itemid\n );\n\n fakeTac.items[replaceIndex] = newSection;\n });\n\n return !!tacHelpers.getOpenSlots(fakeTac, product).length;\n}\n\nfunction getDisabledStatus(tac, product, space, rebased) {\n if (productService.isInsert(product)) {\n if (!tacHelpers.hasSection(tac)) {\n return getWontFitMsg(product);\n }\n\n const hasOpenConnectionSlot = tacHelpers.hasOpenSlot(tac, product, rebased);\n\n if (\n !hasOpenConnectionSlot &&\n productService.isType(product, [ITEMS.CABINET, ITEMS.DIVIDER]) &&\n couldItemFit(tac, product)\n ) {\n return getWontFitMsg(product);\n }\n\n if (!hasOpenConnectionSlot) {\n return translate(t.BALLOON_HINT_NO_PLACE);\n }\n } else if (!hasRoomSlots(tac, space, product)) {\n return translate(t.BALLOON_HINT_NO_PLACE);\n }\n return false;\n}\n\nfunction shouldDisplayProductType(product) {\n return !productService.isSection(product);\n}\n\nfunction getTrashcanZoomLevel(product) {\n return productService.isAddOnShelf(product)\n ? 0.5 // looks good\n : 1.16; // default\n}\nconst getSceneInitFunctions = () => [];\n\nexport default {\n getDisabledStatus,\n shouldDisplayProductType,\n getTrashcanZoomLevel,\n getSceneInitFunctions,\n};\n","import productService from '../../products';\nimport tacHelpers from '../../../state/tac/tacHelpers';\nimport { translate } from '../../L10n';\nimport { t } from '../../../translations';\nimport { hasRoomSlots } from '../common';\nimport { ITEMS } from '../../../constants';\n\nfunction getDisabledStatus(tac, product, space, rebased) {\n const hasOpenConnectionSlot = tacHelpers.hasOpenSlot(tac, product, rebased);\n if (\n productService.isType(product, ITEMS.FRAME) ||\n productService.isType(product, ITEMS.SHELVING_UNIT)\n ) {\n if (!hasRoomSlots(tac, space, product) && !hasOpenConnectionSlot) {\n return translate(t.BALLOON_HINT_NO_PLACE);\n }\n } else {\n if (!hasOpenConnectionSlot) {\n return translate(t.BALLOON_HINT_NO_PLACE);\n }\n }\n return false;\n}\n\nfunction shouldDisplayProductType(product) {\n return !productService.isType(product, [ITEMS.FRAME, ITEMS.SHELVING_UNIT]);\n}\n\nexport { getDisabledStatus };\n\nexport default {\n getDisabledStatus,\n shouldDisplayProductType,\n};\n","import productService from '../../products';\nimport { hasRoomSlots } from '../common';\nimport { translate } from '../../L10n';\nimport { t } from '../../../translations';\nimport tacHelpers from '../../../state/tac/tacHelpers';\nimport constants from '../../../settings/constants';\nimport { DIMENSIONS, ITEMS } from '../../../constants';\nimport { getMeasurementBySoM } from '../../products/productHandler';\nimport { config as asConfig } from '../../../scene/boaxel/AdjustableConfig';\nimport { withLigatures } from '../../../util/measures';\n\nfunction uprightFilter(slot) {\n return slot.width > constants.DYNAMIC_GRID[constants.DRAG_MODE.FLOAT].x.step;\n}\n\nfunction getDisabledStatus(tac, product, space, rebased) {\n if (productService.isType(product, ITEMS.UPRIGHT)) {\n if (!hasRoomSlots(tac, space, product, uprightFilter)) {\n return translate(t.BALLOON_HINT_NO_PLACE);\n }\n } else {\n const hasOpenConnectionSlot = tacHelpers.hasOpenSlot(tac, product, rebased);\n\n if (!hasOpenConnectionSlot) {\n if (productService.isType(product, ITEMS.CLOTHES_RAIL)) {\n return translate(t.ALERT_INFORMATION_CLOTHES_RAIL);\n }\n return translate(t.BALLOON_HINT_NO_PLACE);\n }\n }\n return false;\n}\n\nfunction displayColor(product) {\n return productService.isType(product, [\n ITEMS.SHELF,\n ITEMS.METAL_SHELF,\n ITEMS.WIRE_SHELF,\n ITEMS.SHOE_SHELF,\n ITEMS.BASKET,\n ITEMS.DRYING_RACK,\n ITEMS.CLOTHES_RAIL,\n ]);\n}\n\nfunction shouldDisplayProductType(product) {\n return !productService.isUpright(product);\n}\n\nfunction getTableDisplayText(\n article,\n product,\n useMetricMeasures,\n dimension = DIMENSIONS.height\n) {\n if (dimension !== DIMENSIONS.height)\n return `${getMeasurementBySoM(useMetricMeasures, product[dimension])} ${\n useMetricMeasures\n ? translate(t.MEASURE_VALUE_CM)\n : translate(t.MEASURE_VALUE_IN)\n }`;\n\n const { minHeight, maxHeight } = asConfig.legs;\n const minValue = getMeasurementBySoM(\n useMetricMeasures,\n product.height + minHeight\n );\n const maxValue = getMeasurementBySoM(\n useMetricMeasures,\n product.height + maxHeight\n );\n\n return withLigatures(\n `${minValue}-${maxValue} ${\n useMetricMeasures\n ? translate(t.MEASURE_VALUE_CM)\n : translate(t.MEASURE_VALUE_IN)\n }`\n );\n}\n\nexport default {\n getDisabledStatus,\n displayColor,\n shouldDisplayProductType,\n getTableDisplayText,\n};\n","import { isSection } from '../../products';\nimport { hasRoomSlots } from '../common';\nimport { translate } from '../../L10n';\nimport { t } from '../../../translations';\nimport tacHelpers from '../../../state/tac/tacHelpers';\nimport { ceil } from '../../../util/round';\nimport productService from '../../products';\n\nfunction shouldDisplayProductType(product) {\n return !isSection(product);\n}\n\nfunction getDisabledStatus(tac, product, space, rebased) {\n if (isSection(product)) {\n if (hasRoomSlots(tac, space, product)) {\n const xMargin = tacHelpers.getXmargin({\n ...tac,\n items: tac.items.filter(item =>\n productService.isType(item, 'mounting-rail')\n ),\n });\n\n const singleRailWidth = 650;\n const savedWidthOnMerge = 50;\n\n if (xMargin >= singleRailWidth) {\n // if the xMargin is above or equal to the width of the 'single' mounting rail, we can always fit\n return false;\n } else if (\n xMargin +\n savedWidthOnMerge *\n ceil(\n tac.items.filter(\n item =>\n productService.isType(item, 'mounting-rail') &&\n item.width === singleRailWidth\n ).length / 2,\n 1\n ) >=\n singleRailWidth\n ) {\n // if the xMargin is below 650 we could still fit,\n // but only if there are enough 'single' rails on the scene\n // that the merges would free up the space needed\n return false;\n }\n return translate(t.POPUP_ALERT_SUSPENSION_RAIL);\n }\n return translate(t.BALLOON_HINT_NO_PLACE);\n }\n\n const hasOpenConnectionSlot = tacHelpers.hasOpenSlot(tac, product, rebased);\n if (!hasOpenConnectionSlot) {\n return translate(t.BALLOON_HINT_NO_PLACE);\n }\n\n return false;\n}\n\nfunction displayColor(product) {\n return isSection(product);\n}\n\nconst getSceneInitFunctions = () => [];\n\nexport default {\n shouldDisplayProductType,\n getDisabledStatus,\n displayColor,\n getSceneInitFunctions,\n};\n","import tacHelpers from '../../../state/tac/tacHelpers';\nimport { translate } from '../../L10n';\nimport { t } from '../../../translations';\nimport productService from '../../products';\nimport { isStandAlone } from '../../products/models';\nimport { hasRoomSlots } from '../common';\n\nfunction getDisabledStatus(tac, product, space, rebased) {\n if (isStandAlone(product)) {\n if (\n !hasRoomSlots(tac, space, product) &&\n !tacHelpers.hasOpenSlot(tac, product, rebased)\n ) {\n return translate(t.BALLOON_HINT_NO_PLACE);\n }\n } else {\n if (!tacHelpers.hasOpenSlot(tac, product, rebased)) {\n return translate(t.BALLOON_HINT_NO_PLACE);\n }\n }\n\n return false;\n}\n\nfunction shouldDisplayProductType(product) {\n return !productService.isSection(product);\n}\n\nconst getSwiperConfigSectionItems = () => [];\nconst getSceneInitFunctions = () => [];\n\nexport { getDisabledStatus };\n\nexport default {\n getDisabledStatus,\n shouldDisplayProductType,\n getSceneInitFunctions,\n getSwiperConfigSectionItems,\n};\n","const localStatisticsActions = {\n ADD_TO_BAG_ERROR: 'add_to_bag_error',\n ADD_TO_LIST_ERROR: 'add_to_list_error',\n ADD_UNAVAILABLE_TO_CART: 'add_unavailable_to_cart',\n ADD_UNAVAILABLE_TO_CART_AND_LIST: 'add_unavailable_to_cart_and_list',\n TAC_ARTICLE_CLICKED: 'tac_article_clicked',\n CONF_MENU_SLIDER: 'conf_menu_slider',\n CONF_MENU_INFO_BUTTON_CLICKED: 'conf_menu_info_button_clicked',\n PRODUCT_MENU_MOUNTING_RAIL_INFO_CLICKED:\n 'product_menu_mounting_rail_info_clicked',\n NO_SAVE: 'no_save',\n SAVE: 'save',\n PRODUCT_MENU_SWIPER_INTERACTION: 'product_menu_swiper_interaction',\n PRODUCT_MENU_SUB_FILTER_INTERACTION: 'product_menu_sub_filter_interaction',\n VALIDATION_ERROR: 'validation_error',\n OPEN_PLANNER: 'open_planner',\n START_VIEW_SPR_CLICKED: 'start_view_spr_clicked',\n START_VIEW_VPC_ENTER_CODE_SUCCESS: 'start_view_vpc_enter_code_success',\n SPR_DEEPLINK: 'open_planner_spr_deeplink',\n OPEN_PLANNER_SPR_DEEPLINK_NOT_SUPPORTED:\n 'open_planner_spr_deeplink_not_supported',\n OPEN_PLANNER_VPC_DEEPLINK_NOT_SUPPORTED:\n 'open_planner_vpc_deeplink_not_supported',\n VPC_DEEPLINK: 'open_planner_vpc_deeplink',\n NOT_LOADED: 'not_loaded',\n SURVEY_FEEDBACK: 'submit_survey_feedback',\n SHOW_SURVEY: 'show_survey',\n TERMINATE_SURVEY: 'terminate_survey',\n SUB_PLANNER_USER_CHOICE: 'sub_planner_user_choice',\n SUB_PLANNER_AUTOMATIC_CHOICE: 'sub_planner_automatic_choice',\n LANGUAGE_SELECTOR_USER_CHOICE: 'language_selector_user_choice',\n SERIES_GALLERY_USAGE: 'series_gallery_usage',\n AMELIORATION_DIALOG_SHOWN: 'amelioration_dialog_shown',\n AMELIORATION_DIALOG_ACCEPTED: 'amelioration_dialog_accepted',\n AMELIORATION_DIALOG_DECLINED: 'amelioration_dialog_declined',\n AMELIORATION_ATTEMPT: 'amelioration_attempt',\n AMELIORATION_CAN_NOT_BE_SOLVED: 'amelioration_can_not_be_solved',\n AMELIORATION_SOLVED: 'amelioration_solved',\n AMELIORATION_DECLINED: 'amelioration_declined',\n};\n\nexport default localStatisticsActions;\n","import localStatisticsActions from './localStatisticsActions';\nimport { getInsiktReporter } from '../../../analytics';\nimport { IpexMomentEnum } from '@insights/insights-data-provider';\nimport { isNonInteractionEvent, sanitizeObject } from '../../../utils/object';\n\ninterface CustomLocalEvent {\n ipexMoment: IpexMomentEnum;\n event: string;\n payload?: any;\n reportEvent: Function;\n}\n\ninterface CustomLocalEvents {\n [key: string]: CustomLocalEvent;\n}\n/**\n * Builds the custom event by formatting it according to insights specifications.\n * Insights specification for events:\n * ipexMoment: 1/5 enum choices.\n * event: snake_case.\n * payload: single-depth object hierarchy with only primitives.\n * @param actionType\n * @param payload\n * @param meta\n * @returns {object}\n */\nexport const buildCustomLocalStatisticsEvent = (\n actionType: string,\n payload: any,\n meta: any\n) => {\n const sanitizedPayload = sanitizeObject(payload);\n const sanitizedMeta = sanitizeObject(meta);\n\n const customLocalEvent = customLocalEvents[actionType];\n\n const { reportEvent, ...event } = {\n ...customLocalEvent,\n ipexMoment: isNonInteractionEvent(meta)\n ? IpexMomentEnum.nonInteraction\n : customLocalEvent.ipexMoment,\n payload: {\n ...sanitizedPayload,\n ...sanitizedMeta,\n },\n };\n return event;\n};\n\n/**\n * Used by other files/modules to access the customLocalEvents object.\n * @param localEventType\n * @returns ICustomLocalEvent\n */\nexport const getLocalEvent = (localEventType: string) => {\n return customLocalEvents[localEventType];\n};\n\n/**\n * Calls the insightsApi to report the custom event.\n * @param event\n * @param payload\n * @param meta\n */\nconst reportCustomLocalEvent = (event: string, payload: any, meta: any) => {\n const eventToReport = buildCustomLocalStatisticsEvent(event, payload, meta);\n getInsiktReporter().reportCustomEvent(eventToReport);\n};\n\n/**\n * Contains all custom local events that can be reported at any place in our codebase.\n * Needs to contain:\n * @property ipexMoment The type of event according to ikea specifications, determined by ourselves.\n * @property event The name of the event, always in snake case.\n * @property reportEvent All events have access to a reportEvent function.\n */\nexport const customLocalEvents: CustomLocalEvents = {\n [localStatisticsActions.CONF_MENU_INFO_BUTTON_CLICKED]: {\n ipexMoment: IpexMomentEnum.helpMe,\n event: localStatisticsActions.CONF_MENU_INFO_BUTTON_CLICKED,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.CONF_MENU_SLIDER]: {\n ipexMoment: IpexMomentEnum.makeItMe,\n event: localStatisticsActions.CONF_MENU_SLIDER,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.TAC_ARTICLE_CLICKED]: {\n ipexMoment: IpexMomentEnum.makeItMe,\n event: localStatisticsActions.TAC_ARTICLE_CLICKED,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.PRODUCT_MENU_MOUNTING_RAIL_INFO_CLICKED]: {\n ipexMoment: IpexMomentEnum.helpMe,\n event: localStatisticsActions.PRODUCT_MENU_MOUNTING_RAIL_INFO_CLICKED,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.PRODUCT_MENU_SWIPER_INTERACTION]: {\n ipexMoment: IpexMomentEnum.whatIsMe,\n event: localStatisticsActions.PRODUCT_MENU_SWIPER_INTERACTION,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.VALIDATION_ERROR]: {\n ipexMoment: IpexMomentEnum.helpMe,\n event: localStatisticsActions.VALIDATION_ERROR,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.OPEN_PLANNER]: {\n ipexMoment: IpexMomentEnum.giveMe,\n event: localStatisticsActions.OPEN_PLANNER,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.START_VIEW_SPR_CLICKED]: {\n ipexMoment: IpexMomentEnum.makeItMe,\n event: localStatisticsActions.START_VIEW_SPR_CLICKED,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.ADD_UNAVAILABLE_TO_CART_AND_LIST]: {\n ipexMoment: IpexMomentEnum.giveMe,\n event: localStatisticsActions.ADD_UNAVAILABLE_TO_CART_AND_LIST,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.ADD_UNAVAILABLE_TO_CART]: {\n ipexMoment: IpexMomentEnum.giveMe,\n event: localStatisticsActions.ADD_UNAVAILABLE_TO_CART,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.ADD_TO_BAG_ERROR]: {\n ipexMoment: IpexMomentEnum.giveMe,\n event: localStatisticsActions.ADD_TO_BAG_ERROR,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.ADD_TO_LIST_ERROR]: {\n ipexMoment: IpexMomentEnum.giveMe,\n event: localStatisticsActions.ADD_TO_LIST_ERROR,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.START_VIEW_VPC_ENTER_CODE_SUCCESS]: {\n ipexMoment: IpexMomentEnum.giveMe,\n event: localStatisticsActions.START_VIEW_VPC_ENTER_CODE_SUCCESS,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.OPEN_PLANNER_SPR_DEEPLINK_NOT_SUPPORTED]: {\n ipexMoment: IpexMomentEnum.giveMe,\n event: localStatisticsActions.OPEN_PLANNER_SPR_DEEPLINK_NOT_SUPPORTED,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.OPEN_PLANNER_VPC_DEEPLINK_NOT_SUPPORTED]: {\n ipexMoment: IpexMomentEnum.giveMe,\n event: localStatisticsActions.OPEN_PLANNER_VPC_DEEPLINK_NOT_SUPPORTED,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.SURVEY_FEEDBACK]: {\n ipexMoment: IpexMomentEnum.nonInteraction,\n event: localStatisticsActions.SURVEY_FEEDBACK,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.SHOW_SURVEY]: {\n ipexMoment: IpexMomentEnum.nonInteraction,\n event: localStatisticsActions.SHOW_SURVEY,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.TERMINATE_SURVEY]: {\n ipexMoment: IpexMomentEnum.nonInteraction,\n event: localStatisticsActions.TERMINATE_SURVEY,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.NOT_LOADED]: {\n ipexMoment: IpexMomentEnum.nonInteraction,\n event: localStatisticsActions.NOT_LOADED,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.SUB_PLANNER_USER_CHOICE]: {\n ipexMoment: IpexMomentEnum.makeItMe,\n event: localStatisticsActions.SUB_PLANNER_USER_CHOICE,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.SUB_PLANNER_AUTOMATIC_CHOICE]: {\n ipexMoment: IpexMomentEnum.nonInteraction,\n event: localStatisticsActions.SUB_PLANNER_AUTOMATIC_CHOICE,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.LANGUAGE_SELECTOR_USER_CHOICE]: {\n ipexMoment: IpexMomentEnum.makeItMe,\n event: localStatisticsActions.LANGUAGE_SELECTOR_USER_CHOICE,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.PRODUCT_MENU_SUB_FILTER_INTERACTION]: {\n ipexMoment: IpexMomentEnum.showMe,\n event: localStatisticsActions.PRODUCT_MENU_SUB_FILTER_INTERACTION,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.SERIES_GALLERY_USAGE]: {\n ipexMoment: IpexMomentEnum.makeItMe,\n event: localStatisticsActions.SERIES_GALLERY_USAGE,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.AMELIORATION_DIALOG_SHOWN]: {\n ipexMoment: IpexMomentEnum.helpMe,\n event: localStatisticsActions.AMELIORATION_DIALOG_SHOWN,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.AMELIORATION_DIALOG_ACCEPTED]: {\n ipexMoment: IpexMomentEnum.makeItMe,\n event: localStatisticsActions.AMELIORATION_DIALOG_ACCEPTED,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.AMELIORATION_DIALOG_DECLINED]: {\n ipexMoment: IpexMomentEnum.makeItMe,\n event: localStatisticsActions.AMELIORATION_DIALOG_DECLINED,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.AMELIORATION_ATTEMPT]: {\n ipexMoment: IpexMomentEnum.nonInteraction,\n event: localStatisticsActions.AMELIORATION_ATTEMPT,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.AMELIORATION_CAN_NOT_BE_SOLVED]: {\n ipexMoment: IpexMomentEnum.nonInteraction,\n event: localStatisticsActions.AMELIORATION_CAN_NOT_BE_SOLVED,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.AMELIORATION_SOLVED]: {\n ipexMoment: IpexMomentEnum.nonInteraction,\n event: localStatisticsActions.AMELIORATION_SOLVED,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n [localStatisticsActions.AMELIORATION_DECLINED]: {\n ipexMoment: IpexMomentEnum.nonInteraction,\n event: localStatisticsActions.AMELIORATION_DECLINED,\n reportEvent: function (payload: any, meta: any) {\n reportCustomLocalEvent(this.event, payload, meta);\n },\n },\n};\n","import { getLocalEvent } from './localStatisticsEvents';\nimport localStatisticsActions from './localStatisticsActions';\nimport { IShoppingItem } from '@inter-ikea-kompis/types/lib';\n\nconst localStatisticsReporter = {\n reportAddToBagError: (shoppingItems?: IShoppingItem[]) => {\n const event = getLocalEvent(localStatisticsActions.ADD_TO_BAG_ERROR);\n event.reportEvent(shoppingItems);\n },\n reportAddToListError: (shoppingItems?: IShoppingItem[]) => {\n const event = getLocalEvent(localStatisticsActions.ADD_TO_LIST_ERROR);\n event.reportEvent(shoppingItems);\n },\n reportProductMenuMountingRailInfoClicked: () => {\n const event = getLocalEvent(\n localStatisticsActions.PRODUCT_MENU_MOUNTING_RAIL_INFO_CLICKED\n );\n event.reportEvent();\n },\n\n reportStartViewSprClick: (sprId: string) => {\n const event = getLocalEvent(localStatisticsActions.START_VIEW_SPR_CLICKED);\n event.reportEvent({ sprId: sprId });\n },\n reportTacArticleClicked: (itemId: string) => {\n const event = getLocalEvent(localStatisticsActions.TAC_ARTICLE_CLICKED);\n event.reportEvent({ item: itemId });\n },\n reportConfMenuInfoButtonClicked: (itemId: string) => {\n const event = getLocalEvent(\n localStatisticsActions.CONF_MENU_INFO_BUTTON_CLICKED\n );\n event.reportEvent({ item: itemId });\n },\n reportConfMenuSliderChange: (width: any, itemId: any) => {\n const event = getLocalEvent(localStatisticsActions.CONF_MENU_SLIDER);\n event.reportEvent({ width: width, item: itemId });\n },\n reportProductMenuSwiperInteraction: (\n { status, item }: any,\n interaction: string\n ) => {\n const event = getLocalEvent(\n localStatisticsActions.PRODUCT_MENU_SWIPER_INTERACTION\n );\n event.reportEvent({ status, interaction, item });\n },\n reportSurveyFeedback: (grade: number, currentView: string) => {\n const event = getLocalEvent(localStatisticsActions.SURVEY_FEEDBACK);\n event.reportEvent({ grade, currentView });\n },\n reportShowSurvey: () => {\n const event = getLocalEvent(localStatisticsActions.SHOW_SURVEY);\n event.reportEvent();\n },\n reportTerminateSurvey: () => {\n const event = getLocalEvent(localStatisticsActions.TERMINATE_SURVEY);\n event.reportEvent();\n },\n reportStartViewVpcEnterCodeSuccess: (configurationId: string) => {\n const event = getLocalEvent(\n localStatisticsActions.START_VIEW_VPC_ENTER_CODE_SUCCESS\n );\n event.reportEvent({ vpcCode: configurationId });\n },\n reportValidationError: (\n statisticsLabel: string,\n meansOfNotification: string\n ) => {\n const event = getLocalEvent(localStatisticsActions.VALIDATION_ERROR);\n event.reportEvent({ status: statisticsLabel, meansOfNotification });\n },\n reportNotLoaded: (reason: string, missingProductCategories: any) => {\n const event = getLocalEvent(localStatisticsActions.NOT_LOADED);\n event.reportEvent({ reason, missingProductCategories });\n },\n reportOpenPlanner: (entry: string, ...rest: Object[]) => {\n const event = getLocalEvent(localStatisticsActions.OPEN_PLANNER);\n event.reportEvent({ entry, ...rest }, { nonInteraction: true });\n },\n reportOpenPlannerVpcDeeplinkNotSupported: (vpcCode: Object) => {\n const event = getLocalEvent(\n localStatisticsActions.OPEN_PLANNER_VPC_DEEPLINK_NOT_SUPPORTED\n );\n event.reportEvent({ vpcCode }, { nonInteraction: true });\n },\n reportOpenPlannerSprDeeplinkNotSupported: (sprId: Object) => {\n const event = getLocalEvent(\n localStatisticsActions.OPEN_PLANNER_SPR_DEEPLINK_NOT_SUPPORTED\n );\n event.reportEvent({ sprId }, { nonInteraction: true });\n },\n reportSubPlannerUserChoice: (subPlanner: string) => {\n const event = getLocalEvent(localStatisticsActions.SUB_PLANNER_USER_CHOICE);\n event.reportEvent({ subPlanner });\n },\n reportSubPlannerAutomaticChoice: (subPlanner: string) => {\n const event = getLocalEvent(\n localStatisticsActions.SUB_PLANNER_AUTOMATIC_CHOICE\n );\n event.reportEvent({ subPlanner });\n },\n reportLanguageSelectorUserChoice: (\n fromLocale: string,\n toLocale: string,\n currentView: string\n ) => {\n const event = getLocalEvent(\n localStatisticsActions.LANGUAGE_SELECTOR_USER_CHOICE\n );\n event.reportEvent({ fromLocale, toLocale, currentView });\n },\n reportProductMenuSubFilterInteraction: (\n filter: string,\n subFilter: string,\n option: string\n ) => {\n const event = getLocalEvent(\n localStatisticsActions.PRODUCT_MENU_SUB_FILTER_INTERACTION\n );\n event.reportEvent({ filter, subFilter, option });\n },\n reportSeriesGalleryUsage: () => {\n const event = getLocalEvent(localStatisticsActions.SERIES_GALLERY_USAGE);\n event.reportEvent();\n },\n reportAmeliorationDialogShown: () => {\n const event = getLocalEvent(\n localStatisticsActions.AMELIORATION_DIALOG_SHOWN\n );\n event.reportEvent();\n },\n reportAmeliorationDialogAccepted: () => {\n const event = getLocalEvent(\n localStatisticsActions.AMELIORATION_DIALOG_ACCEPTED\n );\n event.reportEvent();\n },\n reportAmeliorationDialogDeclined: () => {\n const event = getLocalEvent(\n localStatisticsActions.AMELIORATION_DIALOG_DECLINED\n );\n event.reportEvent();\n },\n reportAmeliorationAttempt: (error: string) => {\n const event = getLocalEvent(localStatisticsActions.AMELIORATION_ATTEMPT);\n event.reportEvent({ error });\n },\n reportAmeliorationCanNotBeSolved: (error: string) => {\n const event = getLocalEvent(\n localStatisticsActions.AMELIORATION_CAN_NOT_BE_SOLVED\n );\n event.reportEvent({ error });\n },\n reportAmeliorationSolved: (error: string) => {\n const event = getLocalEvent(localStatisticsActions.AMELIORATION_SOLVED);\n event.reportEvent({ error });\n },\n reportAmeliorationDeclined: (error: string) => {\n const event = getLocalEvent(localStatisticsActions.AMELIORATION_DECLINED);\n event.reportEvent({ error });\n },\n};\nexport default localStatisticsReporter;\n","export default {\n MOBILE: 'mobile',\n TABLET: 'tablet',\n DESKTOP: 'desktop',\n};\n","import screen from '@ikea-aoa/ikea-shared-styles/lib/variables/Screen.json';\n\n/*\n * \"ikea-screen-xx-small\": \"320px\"\n * \"ikea-screen-x-small\": \"480px\"\n * \"ikea-screen-small\": \"768px\"\n * \"ikea-screen-medium\": \"1024px\"\n * \"ikea-screen-large\": \"1440px\"\n * \"ikea-screen-x-large\": \"1920px\"\n */\n\nexport default {\n xxSmall: parseInt(screen['ikea-screen-xx-small'], 10),\n xSmall: parseInt(screen['ikea-screen-x-small'], 10),\n small: parseInt(screen['ikea-screen-small'], 10),\n medium: parseInt(screen['ikea-screen-medium'], 10),\n large: parseInt(screen['ikea-screen-large'], 10),\n xLarge: parseInt(screen['ikea-screen-x-large'], 10),\n};\n","import deviceTypes from './deviceTypes';\nimport screenSizes from './screenSizes';\nimport { IframeUtility } from '@inter-ikea-kompis/utilities';\nimport platform from './platform';\nimport { KIOSK } from '../constants';\n\nfunction getOrientation() {\n return window.innerWidth > IframeUtility.calculateHeight(window.innerWidth)\n ? 'landscape'\n : 'portrait';\n}\n\nfunction getDeviceType(appWidth, orientation) {\n const deviceType = IframeUtility.getDeviceType();\n\n // The app should make no difference between tablet and desktop when in landscape\n if (deviceType === deviceTypes.TABLET && orientation === 'landscape') {\n return deviceTypes.DESKTOP;\n }\n\n // Overriding calculation from KOMPIS when changing browser size\n if (deviceType === deviceTypes.DESKTOP) {\n if (orientation === 'portrait') {\n if (appWidth <= screenSizes.xSmall) {\n return deviceTypes.MOBILE;\n } else if (appWidth <= screenSizes.medium) {\n return deviceTypes.TABLET;\n } else {\n return deviceTypes.DESKTOP;\n }\n }\n\n if (IframeUtility.calculateHeight(appWidth) <= screenSizes.xSmall) {\n return deviceTypes.MOBILE;\n } else {\n return deviceTypes.DESKTOP;\n }\n }\n\n return deviceType;\n}\n\nfunction getUserAgent() {\n const orientation = getOrientation();\n const deviceType = platform.isKiosk\n ? KIOSK\n : getDeviceType(window.innerWidth, orientation);\n\n return { orientation, deviceType };\n}\n\nfunction isMobile() {\n return getUserAgent().deviceType === deviceTypes.MOBILE;\n}\n\nexport { getUserAgent, getDeviceType, isMobile };\n","import geometry from '../../scene/util/geometry';\nimport constants from '../../settings/constants';\nimport { getUserAgent } from '../userAgent';\n\nfunction getDefaultPac() {\n const defaultPAC = {\n model: {\n items: [],\n },\n\n version: '1.1',\n };\n\n function getPoints() {\n const userAgent = getUserAgent();\n if (\n userAgent.deviceType === 'mobile' &&\n userAgent.orientation === 'portrait'\n ) {\n return constants.WALL.pointsMobilePortrait;\n } else if (\n userAgent.deviceType === 'mobile' &&\n userAgent.orientation === 'landscape'\n ) {\n return constants.WALL.pointsMobileLandscape;\n } else if (userAgent.deviceType === 'tablet') {\n return constants.WALL.pointsTablet;\n } else {\n return constants.WALL.points;\n }\n }\n\n if (constants.WALL) {\n const { height, width } = geometry.surround(getPoints());\n defaultPAC.wall = {\n points: getPoints(),\n size: {\n height,\n width,\n },\n };\n }\n\n return defaultPAC;\n}\n\nexport default getDefaultPac;\n","import storage from './storage';\n\nconst STORAGE_KEY = 'cachedPac';\n\nfunction load() {\n try {\n const PAC = JSON.parse(storage.local.getItem(STORAGE_KEY));\n\n if (PAC && PAC.model) {\n return PAC;\n }\n } catch (err) {\n console.error(err);\n }\n\n storage.local.removeItem(STORAGE_KEY);\n\n return null;\n}\n\nfunction save(PAC) {\n storage.local.setItem(STORAGE_KEY, JSON.stringify(PAC));\n}\n\nfunction clear() {\n storage.local.clear();\n}\n\nexport default {\n load,\n save,\n clear,\n};\n","export function preventDefault(e) {\n e.preventDefault();\n}\n\nexport function stopPropagation(e) {\n e.stopPropagation();\n}\n","import React from 'react';\nimport PropTypes from 'prop-types';\nimport * as supportedEvents from '../../util/supportedEvents';\nimport { stopPropagation } from '../../util/events';\n\nexport default class StopPropagation extends React.Component {\n static propTypes = {\n className: PropTypes.string,\n children: PropTypes.oneOfType([\n PropTypes.arrayOf(PropTypes.node),\n PropTypes.node,\n ]).isRequired,\n style: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),\n };\n\n render() {\n const { className, children, style } = this.props;\n\n return (\n \n {children}\n \n );\n }\n}\n","import { State } from '../StateTypes';\nimport { Dialog } from './dialogTypes';\n\nexport const selectDialog = ({ dialog }: State): Dialog => dialog;\n\nexport const selectDialogExists = ({ dialog }: State): Dialog => dialog.type;\nexport const selectDialogOptions = ({ dialog }: State): Dialog => dialog.options;","import { State } from '../StateTypes';\n\n/**\n * Select init state slice\n * @param state\n */\nexport const selectInit = (state: State) => state.init;\n\n/**\n * Select init is loading\n * @param state\n */\nexport const selectInitLoading = (state: State) => selectInit(state).loading;\n\n/**\n * Select init is done loading\n * @param state\n */\nexport const selectInitDone = (state: State) => !selectInitLoading(state);\n\n/**\n * Select init failed\n * @param state\n */\nexport const selectInitFailed = (state: State) => selectInit(state).error;\n\n/**\n * Export const select init successful\n * @param state\n */\nexport const selectInitSuccessful = (state: State) =>\n selectInitDone(state) && !selectInitFailed(state);\n","import { useDispatch, useSelector } from 'react-redux';\nimport React from 'react';\nimport { PromptButtonTypeEnum } from '@inter-ikea-kompis/component-prompt';\nimport { KompisPrompt, KompisText } from '@inter-ikea-kompis/react-components';\nimport { SkapaTheme } from '@inter-ikea-kompis/themes';\nimport { actionCloseDialog } from '../../state/dialog/dialogActions';\nimport { translate } from '../../services/L10n';\nimport StopPropagation from '../utils/StopPropagation';\nimport { selectDialog } from '../../state/dialog/dialogSelectors';\nimport { thunkLoadPac } from '../../state/tac/tacThunks';\nimport { selectInitLoading } from '../../state/init/initSelectors';\nimport { t } from '../../translations';\n\nconst DeeplinkChoice = () => {\n const loading = useSelector(selectInitLoading);\n const dialog = useSelector(selectDialog);\n const dispatch = useDispatch();\n\n /**\n * Handle on return click\n */\n const returnClick = () => dispatch(actionCloseDialog());\n\n /**\n * Handle on continue click\n */\n const continueClick = () => {\n dispatch(thunkLoadPac(dialog.options.spr));\n dispatch(actionCloseDialog());\n };\n\n const isPrimaryButtonEvent = (type: PromptButtonTypeEnum) =>\n type === PromptButtonTypeEnum.primaryButton;\n\n const handleButtonClick = ({ detail: { type } }: any) =>\n isPrimaryButtonEvent(type) ? continueClick() : returnClick();\n\n return loading ? null : (\n \n \n \n {translate(t.REPLACE_PREVIOUS_BODY)}\n \n \n \n );\n};\n\nexport default DeeplinkChoice;\n","import {\n SUMMARY_ADD_UNAVAILABLE_PRODUCTS_TO_LIST,\n SUMMARY_SET_MODAL_TYPE,\n SUMMARY_SET_CURRENT_CONFIGURATION,\n SUMMARY_SET_CONFIRMATION_CARD_TYPE,\n SUMMARY_SET_MODAL_STATE,\n SUMMARY_SET_SHOPPING_ITEMS,\n SUMMARY_SET_STATE,\n SUMMARY_SET_STORE_AVAILABILITIES,\n SUMMARY_SET_STORE_ID,\n SUMMARY_SET_VPC_CODE,\n SUMMARY_SET_DESIGN_LINK,\n SUMMARY_CLEAR_BUTTON,\n SUMMARY_SET_COPY_DESIGN_CODE_STATE,\n SUMMARY_SET_COPY_DESIGN_LINK_STATE,\n SUMMARY_SET_FAILED_SHOPPING_ITEMS,\n SUMMARY_SET_SHARE_DESIGN_CARD,\n SUMMARY_SET_HOME_DELIVERY,\n SUMMARY_SET_SCENE_IMAGE,\n SUMMARY_SET_FINANCING_OPTIONS,\n} from '../actionConstants';\nimport { AddToBagStateEnum } from '@inter-ikea-kompis/component-add-to-bag';\nimport { SummaryPageConfirmationCardTypeEnum } from '@inter-ikea-kompis/component-summary-page';\nimport { ConfirmationSummaryShareDesignStateEnum } from '@inter-ikea-kompis/component-configuration-summary';\nimport { SendByEmailStateEnum } from '@inter-ikea-kompis/component-send-by-email';\nimport { IFinancingOption, IZipAvailability } from '@inter-ikea-kompis/types';\n\nexport const ADD_TO_LIST = 'addToList';\nexport const ADD_TO_CART = 'addToCart';\nexport const SEND_BY_EMAIL = 'sendByEmailState';\nexport const SEND_BY_EMAIL_VALUE = 'sendByEmailValue';\nexport const SEND_BY_EMAIL_INVALID_INPUT = 'sendByEmailInvalidInput';\nexport const SET_SHARE_DESIGN_CARD_STATE = 'state';\nexport const SET_HOME_DELIVERY_STATE = 'state';\nexport const SET_HOME_DELIVERY_ZIP_CODE = 'zipCode';\nexport const SET_HOME_DELIVERY_INPUT_VALUE = 'inputValue';\nexport const SET_HOME_DELIVERY_ZIP_AVAILABILITIES = 'zipAvailabilities';\n\nexport const SAVE_VPC = 'saveVpc';\n\nexport const actionSetStoreId = (storeId: string) => ({\n type: SUMMARY_SET_STORE_ID,\n payload: { storeId },\n});\n\nexport const actionSetStoreAvailabilities = (\n specificStoreAvailabilities: any\n) => ({\n type: SUMMARY_SET_STORE_AVAILABILITIES,\n payload: {\n specificStoreAvailabilities,\n },\n});\n\nexport const actionSetShoppingItems = (shoppingItems: any) => ({\n type: SUMMARY_SET_SHOPPING_ITEMS,\n payload: { shoppingItems },\n});\n\nexport const actionSetVpcCode = (vpcCode: string) => ({\n type: SUMMARY_SET_VPC_CODE,\n payload: { vpcCode },\n});\n\nexport const actionSetDesignLink = (designLink: string) => ({\n type: SUMMARY_SET_DESIGN_LINK,\n payload: { designLink },\n});\n\nexport const actionSetSceneImage = (sceneImage: string) => ({\n type: SUMMARY_SET_SCENE_IMAGE,\n payload: { sceneImage },\n});\n\nconst actionSetSummaryPageState =\n (target: string) =>\n (state: AddToBagStateEnum | ConfirmationSummaryShareDesignStateEnum) => ({\n type: SUMMARY_SET_STATE,\n payload: { target, state },\n });\n\nexport const actionSetAddToListState = actionSetSummaryPageState(ADD_TO_LIST);\nexport const actionSetAddToCartState = actionSetSummaryPageState(ADD_TO_CART);\n\nconst actionSetShareDesignCard =\n (target: string) => (state: SendByEmailStateEnum | boolean | string) => ({\n type: SUMMARY_SET_SHARE_DESIGN_CARD,\n payload: { target, state },\n });\n\nexport const actionSetSendByEmailState =\n actionSetShareDesignCard(SEND_BY_EMAIL);\n\nexport const actionSetShareDesignCardState = actionSetShareDesignCard(\n SET_SHARE_DESIGN_CARD_STATE\n);\n\nconst actionSetHomeDelivery =\n (target: string) => (state: IZipAvailability[] | string | undefined) => ({\n type: SUMMARY_SET_HOME_DELIVERY,\n payload: { target, state },\n });\n\nexport const actionSetHomeDeliveryState = actionSetHomeDelivery(\n SET_HOME_DELIVERY_STATE\n);\nexport const actionSetHomeDeliveryZipCode = actionSetHomeDelivery(\n SET_HOME_DELIVERY_ZIP_CODE\n);\nexport const actionSetHomeDeliveryInputValue = actionSetHomeDelivery(\n SET_HOME_DELIVERY_INPUT_VALUE\n);\nexport const actionSetHomeDeliveryZipAvailabilities = actionSetHomeDelivery(\n SET_HOME_DELIVERY_ZIP_AVAILABILITIES\n);\n\nexport const actionSetConfirmationCardType = (\n confirmationCardType: SummaryPageConfirmationCardTypeEnum\n) => ({\n type: SUMMARY_SET_CONFIRMATION_CARD_TYPE,\n payload: { confirmationCardType },\n});\n\nexport const actionSetShowModal = () => ({\n type: SUMMARY_SET_MODAL_STATE,\n payload: { modalState: true },\n});\n\nexport const actionSetHideModal = () => ({\n type: SUMMARY_SET_MODAL_STATE,\n payload: { modalState: false },\n});\n\nexport const actionClearButtons = () => ({\n type: SUMMARY_CLEAR_BUTTON,\n});\n\nexport const actionSetModalType = (modalType: any) => ({\n type: SUMMARY_SET_MODAL_TYPE,\n payload: { modalType },\n});\n\nexport const actionSetAddUnavailableProductsToList = (\n addUnavailableProductsToList: boolean\n) => ({\n type: SUMMARY_ADD_UNAVAILABLE_PRODUCTS_TO_LIST,\n payload: { addUnavailableProductsToList },\n});\n\nexport const actionSetCurrentConfiguration = (currentConfiguration: any) => ({\n type: SUMMARY_SET_CURRENT_CONFIGURATION,\n payload: { currentConfiguration },\n});\n\nexport const actionSetCopyDesignCodeState = (\n shareDesignCardCopyState: any\n) => ({\n type: SUMMARY_SET_COPY_DESIGN_CODE_STATE,\n payload: { shareDesignCardCopyState },\n});\n\nexport const actionSetCopyDesignLinkState = (\n shareDesignCardLinkState: any\n) => ({\n type: SUMMARY_SET_COPY_DESIGN_LINK_STATE,\n payload: { shareDesignCardLinkState },\n});\n\nexport const actionSetFailedShoppingItems = (failedShoppingItems: any) => ({\n type: SUMMARY_SET_FAILED_SHOPPING_ITEMS,\n payload: { failedShoppingItems },\n});\n\nexport const actionSetFinancingOptions = (\n financingOptions: IFinancingOption | undefined\n) => ({\n type: SUMMARY_SET_FINANCING_OPTIONS,\n payload: { financingOptions },\n});\n","import { resetIntroPopups } from '../popups/popupsActions';\nimport { actionHideMeasurements } from '../scene/sceneActions';\nimport history from '../../services/history';\nimport { actionSetHideModal } from '../summary/summaryActions';\nimport { Thunk } from '../../generalTypes';\n\nexport const thunkResetApp: Thunk = () => dispatch => {\n dispatch(resetIntroPopups());\n dispatch(actionHideMeasurements({}));\n dispatch(actionSetHideModal());\n\n history.clear();\n};\n","import productsService from '../../../../services/products';\n\nconst bulkArticles = idMap => articles => {\n /**\n * @param id\n * @returns {{iows: {qty: number, itemno: *, type: string}, id: *}}\n */\n const createCollectionArticleObject = id =>\n productsService.getProduct(idMap[id].id);\n\n const itemIds = Object.keys(idMap)\n .filter(id => createCollectionArticleObject(id))\n .map(id => id, 10);\n\n const itemsIds = itemIds.reduce(\n (acc, curr) => ({\n ...acc,\n [curr]: [],\n }),\n {}\n );\n\n /**\n * Derive articles from array based on idMap\n * @param reversed\n * @returns {function({id?: *}): boolean}\n */\n const collectArticles =\n (reversed = false) =>\n ({ id }) =>\n reversed ? !itemIds.includes(id) : itemIds.includes(id);\n\n /**\n * Sorts all shelves into arrays, by id\n * @param acc\n * @param curr\n */\n const sortArticles = (acc, curr) => ({\n ...acc,\n [curr.id]: [...acc[curr.id], curr],\n });\n\n /**\n * Splits items into smaller given chunks\n * @param items\n * @param obj\n * @returns {*[]|*}\n */\n const itemSplitter = (items, obj = []) => {\n const itemId = items[0]?.id || null;\n const multipackItem = itemId ? idMap[itemId] : null;\n const isMultipackOnly = productsService.isOnlyAvailableInMultipack(itemId);\n const bulkSize = multipackItem?.bulkSize || null;\n\n if (!items.length || (!isMultipackOnly && items.length < bulkSize))\n return [...obj, ...items];\n\n const bulk = items.slice(0, bulkSize);\n const rest = items.slice(bulkSize, items.length);\n return itemSplitter(rest, [\n ...obj,\n createCollectionArticleObject(bulk[0].id),\n ]);\n };\n\n /**\n * Assembles the 4 packs, and rest of shelves\n * @returns {*[]}\n */\n const assembleShelvesArray = () => {\n const itemsExcludingShelves = articles.filter(collectArticles(true));\n\n const shelves = articles\n .filter(collectArticles())\n .reduce(sortArticles, itemsIds);\n\n const groupedShelves = Object.values(shelves)\n .map(art => itemSplitter(art))\n .flat();\n\n return [...itemsExcludingShelves, ...groupedShelves];\n };\n\n return assembleShelvesArray();\n};\n\nexport default bulkArticles;\n","import bulkArticles from '../middlewares/bulkArticles';\nimport constants from '../../../../settings/constants';\n\nexport default [bulkArticles(constants.BULK_ARTICLES)];\n","import { applicationSettings } from '../../../settings/application';\nimport { RANGES } from '../../../constants';\nimport BROR from './ranges/BROR';\n\nconst ranges = { [RANGES.BROR]: BROR };\n\n/**\n * As a general rule, this function will need no manipulation. Yes, I know, bold claim,\n * but bare with me! This one simply runs other functions and if such exist for the given\n * range, and makes sure that they returns arrays, simple as that!\n *\n * Any manipulation goes in ranges and middlewares folder. The concept is, we have loosely\n * coupled middleware functions, stored in the \"middlewares\"-folder that we use to combine\n * for a given range in the ranges folder. The ranges files should ALWAYS return an array\n * including prepped functions ready to run.\n *\n * @param items\n * @returns {*}\n */\nexport const tacMiddleware = items => {\n return ranges?.[applicationSettings.applicationName]\n ? ranges[applicationSettings.applicationName].reduce((acc, curr) => {\n const itemsArray = curr(acc);\n\n if (!Array.isArray(itemsArray))\n throw new Error(\n 'Did not return array. Items thingy functions must return one array'\n );\n\n return itemsArray;\n }, items)\n : items;\n};\n","import { State } from '../StateTypes';\nimport { ArticleGroup, Product, Ymal } from './ymalTypes';\nimport { ART } from '../../constants';\n\n/**\n * Select ymal\n * @param ymal\n * @returns {*}\n */\nexport const selectYmal = ({ ymal }: State): Ymal => ymal;\n\n/**\n * Select ymal article groups\n * @param state\n * @returns {Validator> | {regular: boolean, pegboard: boolean, pegboard_black: boolean}}\n */\nexport const selectYmalArticleGroups = (state: State): ArticleGroup[] =>\n selectYmal(state).articleGroups;\n\n/**\n * Select ymal articles\n * @param state\n * @returns {[string, string, string, string]|[string, string, string]|[string]|[string]|[string]|[string]|*|IArticleData[]}\n */\nexport const selectYmalArticles = (state: State): Product[] =>\n selectYmal(state).articles;\n\n/**\n * Select YMALs cms object\n * @param state\n */\nexport const selectYmalCmsObjects = (state: State) =>\n selectYmalArticles(state).map(({ id }) => ({ id, type: ART }));\n\n/**\n * Select YMALs data\n * @param state\n */\nexport const selectYMALsData = (state: State) => selectYmal(state).ymal;\n\n/**\n * Select YMAL content\n * @param state\n * @param id\n */\nexport const selectYMALContent = (state: State, id: any) =>\n selectYMALsData(state).find(ymal => id === ymal.content.ruItemNo).content;\n","import { ART } from '../../constants';\nimport { IShoppingItem, IShoppingProduct } from '@inter-ikea-kompis/types';\nimport { tacMiddleware } from './conversionMiddleware';\nimport { unique } from '../array';\nimport articles from '../../services/products/articles';\nimport productService from '../../services/products';\nimport tacHelpers from '../../state/tac/tacHelpers';\nimport { TacItem, IowsArticle } from '../../generalTypes';\nimport { TacModel } from '../../state/tac/tacTypes';\n// TODO: IArticleData is expected to officially exported in the next version of the DPC.\n// When this happens, change so that we import IArticleData the same way as we currently import IItems.\nimport IArticleData from '@insights/insights-data-provider/lib/types/IArticleData';\nimport { IItems } from '@insights/insights-data-provider';\nimport store from '../../state';\nimport { selectYMALContent } from '../../state/ymal/ymalSelectors';\nimport { selectCurrencyCode } from '../../state/dexfSettings/dexfSettingsSelectors';\n\n/**\n *\n * @param tac\n * @returns Array of all valid products in TAC\n */\nfunction getAllValidProducts(tac: TacModel): TacItem[] {\n return tacHelpers\n .getAllItems(tac.items)\n .map((item: TacItem) => productService.getProduct(item))\n .filter(Boolean);\n}\n\n/**\n *\n * @param tac\n * @returns Array of Kompis2 Shopping Products\n */\nexport function getShoppingProductsFromTAC(tac: TacModel): IShoppingProduct[] {\n return getShoppingItemsFromTAC(tac).map(item => ({\n quantity: item.quantity,\n product: { ...articles.getArticle(item.id) },\n }));\n}\n\n/**\n *\n * @param tac\n * @param preferLocalIds\n * @returns Array of Kompis2 Shopping Items\n */\nexport function getShoppingItemsFromTAC(\n tac: TacModel,\n preferLocalIds: boolean = false\n): IShoppingItem[] {\n const items = getAllValidProducts(tac);\n return getShoppingItems(items, preferLocalIds);\n}\n\n/**\n *\n * @param tacItems\n * @param preferLocalIds\n * @returns Array of Kompis2 Shopping Items\n */\nexport function getShoppingItems(\n tacItems: TacItem[],\n preferLocalIds: boolean = false\n): IShoppingItem[] {\n const allIows = tacMiddleware(tacItems).flatMap((prod: TacItem) => prod.iows);\n\n return allIows\n .map((iows: IowsArticle) => iows.itemno)\n .filter(unique)\n .map((itemno: string) => ({\n id: preferLocalIds\n ? articles.getArticle(itemno)?.content?.ruItemNo\n : itemno,\n type: ART,\n quantity: allIows\n .filter((iows: IowsArticle) => iows.itemno === itemno)\n .reduce((acc: number, curr: IowsArticle) => acc + curr.qty, 0),\n }));\n}\n\nexport function getInsightsArticleData(\n shoppingItems: IShoppingItem[]\n): IArticleData[] {\n const articleData = shoppingItems.map(item => {\n const content =\n articles.getArticleContent(item.id) ||\n selectYMALContent(store.getState(), item.id);\n return {\n id: item.id,\n price: content.priceInformation.salesPrice[0].priceInclTax,\n qty: item.quantity,\n };\n });\n return articleData;\n}\n\nexport function getInsightsItems(shoppingItems: IShoppingItem[]): IItems {\n const articleData = getInsightsArticleData(shoppingItems);\n const currency = selectCurrencyCode(store.getState());\n return { currency: currency, items: articleData };\n}\n","const mandatoryStatisticsActions = {\n ADD_TO_CART: 'ADD_TO_CART',\n ADD_TO_WISHLIST: 'ADD_TO_WISHLIST',\n INIT_PLANNING_SESSION: 'INIT_PLANNING_SESSION',\n SEND_DESIGN_INTERACTION_EVENT: 'SEND_DESIGN_INTERACTION_EVENT',\n};\n\nexport default mandatoryStatisticsActions;\n","import { IShoppingItem } from '@inter-ikea-kompis/types/lib';\nimport { getInsightsItems } from '../../../../util/aactools/kompisConvert';\nimport { getInsiktReporter } from '../../analytics';\nimport mandatoryStatisticsActions from './mandatoryStatisticsActions';\nimport {\n DesignInteractionEnum,\n DesignSourceEnum,\n} from '@insights/insights-data-provider';\ninterface MandatoryEvent {\n reportEvent: Function;\n}\ninterface MandatoryEvents {\n [key: string]: MandatoryEvent;\n}\nconst insiktReporter = getInsiktReporter();\nconst getInsightsApi = () => insiktReporter.getInsightsApi();\n\n/**\n * The mandatory events that are required to be reported according to the insights documentation.\n * @link https://i-p-e-x.atlassian.net/wiki/spaces/IPEX/pages/890536089/Data+Provider+Component\n */\nexport const mandatoryEvents: MandatoryEvents = {\n [mandatoryStatisticsActions.ADD_TO_CART]: {\n reportEvent: async (payload: IShoppingItem[]) => {\n if (await insiktReporter.canTransmit()) {\n const items = getInsightsItems(payload);\n getInsightsApi().addToCart(items);\n }\n },\n },\n [mandatoryStatisticsActions.ADD_TO_WISHLIST]: {\n reportEvent: async (payload: IShoppingItem[]) => {\n if (await insiktReporter.canTransmit()) {\n const items = getInsightsItems(payload);\n getInsightsApi().addToWishList(items);\n }\n },\n },\n [mandatoryStatisticsActions.INIT_PLANNING_SESSION]: {\n reportEvent: async (vpcCode: string, vpcSource: DesignSourceEnum) => {\n (await insiktReporter.canTransmit()) &&\n getInsightsApi().initPlanningSession(vpcCode, vpcSource);\n },\n },\n [mandatoryStatisticsActions.SEND_DESIGN_INTERACTION_EVENT]: {\n reportEvent: async (\n designId: string,\n vpcInteraction: DesignInteractionEnum\n ) => {\n (await insiktReporter.canTransmit()) &&\n getInsightsApi().sendDesignInteractionEvent(designId, vpcInteraction);\n },\n },\n};\n\nexport const getMandatoryEvent = (mandatoryEventType: string) => {\n return mandatoryEvents[mandatoryEventType];\n};\n","import { getMandatoryEvent } from './mandatoryStatisticsEvents';\nimport mandatoryStatisticsActions from './mandatoryStatisticsActions';\nimport {\n DesignInteractionEnum,\n DesignSourceEnum,\n} from '@insights/insights-data-provider';\nimport { IShoppingItem } from '@inter-ikea-kompis/types/lib';\nconst mandatoryStatisticsReporter = {\n reportAddToBagSuccess: (shoppingItems: IShoppingItem[]) => {\n const event = getMandatoryEvent(mandatoryStatisticsActions.ADD_TO_CART);\n event.reportEvent(shoppingItems);\n },\n reportAddToListSuccess: (shoppingItems: IShoppingItem[]) => {\n const event = getMandatoryEvent(mandatoryStatisticsActions.ADD_TO_WISHLIST);\n event.reportEvent(shoppingItems);\n },\n reportInitialPlanningSession: (\n vpcCode: string,\n vpcSource: DesignSourceEnum\n ) => {\n const event = getMandatoryEvent(\n mandatoryStatisticsActions.INIT_PLANNING_SESSION\n );\n event.reportEvent(vpcCode, vpcSource);\n },\n reportDesignInteractionEvent: (\n designCode: string,\n vpcInteraction: DesignInteractionEnum\n ) => {\n const event = getMandatoryEvent(\n mandatoryStatisticsActions.SEND_DESIGN_INTERACTION_EVENT\n );\n event.reportEvent(designCode, vpcInteraction);\n },\n};\nexport default mandatoryStatisticsReporter;\n","import React from 'react';\nimport PropTypes from 'prop-types';\nimport { connect } from 'react-redux';\nimport { ComponentManager } from '@ikea-aoa/ikea-shared-component';\nimport StopPropagation from './utils/StopPropagation';\nimport { default as _SeriesGallery } from '@ikea-aoa/ikea-component-series-gallery';\nimport isValid from '../services/PacValidator';\nimport fixOldVpc from '../services/FixVPC';\nimport storage from '../services/history/storage';\nimport { applicationSettings } from '../settings/application';\nimport { DesignSourceEnum } from '@insights/insights-data-provider';\nimport { thunkSetView } from '../state/navigation/navigationThunks';\nimport masterConstants from '../settings/masterConstants';\nimport { thunkResetApp } from '../state/init/initThunks';\nimport { closeSeriesGallery } from '../state/dialog/dialogActions.ts';\nimport constants from '../settings/constants';\nimport { selectDialog } from '../state/dialog/dialogSelectors.ts';\nimport { thunkLoadPac } from '../state/tac/tacThunks';\nimport mandatoryStatisticsReporter from '../services/statistics/insights/mandatory/mandatoryStatisticsReporter';\n\nclass SeriesGallery extends React.Component {\n static propTypes = {\n goToStart: PropTypes.func.isRequired,\n closeSeriesGallery: PropTypes.func.isRequired,\n resetApp: PropTypes.func.isRequired,\n };\n\n getLocalhostUrlWithNewPlanner = (currentPlanner, plannerToNavigateTo) => {\n const currentUrl = window.location.href;\n return currentUrl.replace(currentPlanner, plannerToNavigateTo);\n };\n\n isLocalhostEnvironment = () => {\n const url = window.location.href;\n return url.includes('localhost');\n };\n\n closeSeriesGalleryAndGoToStart = () => {\n this.props.closeSeriesGallery();\n this.props.goToStart();\n };\n\n constructor() {\n super();\n const componentManager = new ComponentManager();\n componentManager.register(_SeriesGallery);\n componentManager.connectObserver();\n\n this.events = _SeriesGallery.events({\n vpcDone: vpcData => {\n const pac = fixOldVpc(vpcData.configuration);\n if (isValid(pac)) {\n this.props.loadPAC(pac);\n mandatoryStatisticsReporter.reportInitialPlanningSession(\n vpcData.vpcCode,\n DesignSourceEnum.gallery\n );\n }\n },\n show: () => {\n storage.session.setItem(constants.SERIES_GALLERY_ACTIVE_KEY, true);\n },\n close: () => {\n storage.session.removeItem(constants.SERIES_GALLERY_ACTIVE_KEY);\n },\n applicationClick: click => {\n storage.session.removeItem(constants.SERIES_GALLERY_ACTIVE_KEY);\n const applicationClicked = click.application;\n\n const currentPlanner =\n applicationSettings.applicationName.toLocaleLowerCase();\n const plannerToNavigateTo = applicationClicked.id.toLowerCase();\n\n if (this.isLocalhostEnvironment()) {\n if (currentPlanner === plannerToNavigateTo) {\n this.closeSeriesGalleryAndGoToStart();\n return;\n }\n this.props.resetApp();\n window.location.href = this.getLocalhostUrlWithNewPlanner(\n currentPlanner,\n plannerToNavigateTo\n );\n window.location.reload();\n return;\n }\n\n if (currentPlanner === plannerToNavigateTo) {\n this.closeSeriesGalleryAndGoToStart();\n } else {\n this.props.resetApp();\n window.location.href = applicationClicked.url;\n }\n },\n });\n }\n\n render() {\n return (\n \n \n \n );\n }\n}\n\nexport default connect(\n state => ({\n dialog: selectDialog(state),\n }),\n dispatch => ({\n goToStart: () => dispatch(thunkSetView(masterConstants.VIEW_NAMES.START)),\n closeSeriesGallery: () => dispatch(closeSeriesGallery()),\n resetApp: () => dispatch(thunkResetApp()),\n loadPAC: pac => dispatch(thunkLoadPac(pac)),\n })\n)(SeriesGallery);\n","import React from 'react';\nimport { useDispatch } from 'react-redux';\nimport { translate } from '../../services/L10n';\nimport { previousView } from '../../state/navigation';\nimport StopPropagation from '../utils/StopPropagation';\nimport { KompisPrompt } from '@inter-ikea-kompis/react-components';\nimport { SkapaTheme } from '@inter-ikea-kompis/themes';\nimport { actionCloseDialog } from '../../state/dialog/dialogActions';\nimport { actionSetOverlayState } from '../../state/popups/popupsActions';\nimport { t } from '../../translations';\n\nconst PacMissing = () => {\n const dispatch = useDispatch();\n\n /**\n * Close modal and navigate to start view\n */\n const close = (): void => {\n dispatch(actionCloseDialog());\n dispatch(previousView());\n dispatch(actionSetOverlayState(false));\n };\n\n return (\n \n \n \n );\n};\n\nexport default PacMissing;\n","import {\n KioskIntegration,\n KioskPlatform,\n} from '@inter-ikea-gallery/integration';\n\nexport const useKioskIntegration = () => {\n const params = new URLSearchParams(window.location.href);\n let integration: KioskIntegration | null = null;\n let mode: KioskPlatform | undefined = undefined;\n\n if (\n params.get('ipexGallery') === 'true' ||\n params.get('origin') === 'upptacka'\n ) {\n mode = params.get('ipexGallery') === 'true' ? 'gallery' : 'upptacka';\n integration = new KioskIntegration({ debug: true, platform: mode });\n }\n\n const isUpptacka = mode === 'upptacka';\n const isGallery = mode === 'gallery';\n const isUpptackaOrIpexGallery = isGallery || isUpptacka;\n\n return { integration, isUpptacka, isGallery, isUpptackaOrIpexGallery };\n};\n","import React from 'react';\nimport { useDispatch } from 'react-redux';\nimport { translate } from '../../services/L10n';\nimport StopPropagation from '../utils/StopPropagation';\nimport { KompisPrompt, KompisText } from '@inter-ikea-kompis/react-components';\nimport { SkapaTheme } from '@inter-ikea-kompis/themes';\nimport { actionCloseDialog } from '../../state/dialog/dialogActions';\nimport { actionSetOverlayState } from '../../state/popups/popupsActions';\nimport { PromptButtonTypeEnum } from '@inter-ikea-kompis/component-prompt';\nimport { useKioskIntegration } from '../../hooks/useKioskIntegration';\nimport { t } from '../../translations';\n\nconst IpexGalleryWarning = () => {\n const dispatch = useDispatch();\n const { integration } = useKioskIntegration();\n const [showPrompt, setShowPrompt] = React.useState(true);\n\n const onButtonClick = (event: any) => {\n setShowPrompt(false);\n if (event.detail.type === PromptButtonTypeEnum.primaryButton) {\n integration?.exitClicked();\n } else {\n close();\n }\n };\n\n /**\n * Close modal\n */\n const close = (): void => {\n dispatch(actionCloseDialog());\n dispatch(actionSetOverlayState(false));\n };\n\n return (\n \n \n \n {translate(t.IPEX_GALLERY_EXIT_DIALOGUE_BODY)}\n \n \n \n );\n};\n\nexport default IpexGalleryWarning;\n","import { ToastManager } from '@inter-ikea-kompis/component-toast';\nimport { ConfigurationSummaryToastMessage } from '@inter-ikea-kompis/component-configuration-summary';\nimport { AddToBagToastMessage } from '@inter-ikea-kompis/component-add-to-bag';\nimport { AddToListToastMessage } from '@inter-ikea-kompis/component-add-to-list';\nimport { SkapaTheme } from '@inter-ikea-kompis/themes';\nimport { translate } from '../services/L10n';\nimport { t } from '../translations';\nimport { selectKompisTranslations } from '../state/translations/translationsSelectors';\nimport { selectKompisDexfSettings } from '../state/dexfSettings/dexfSettingsSelectors';\nimport store from '../state';\n\nconst toastManager = new ToastManager({ theme: SkapaTheme });\n\ntype Toast = {\n label: string;\n actionHref?: string;\n actionLabel?: string;\n};\n\nconst toastQueue: Array = [];\nlet queueIsRunning: boolean = false;\nlet runQueuePromise: Promise | null = null;\n\n/**\n * Queues the TAC toast and returns a promise, to handle\n * several toasts being shown sequentially.\n * @param {string} translationKey The translation key for the message in the toast.\n */\nexport function showTacToast(translationKey: string): void {\n const toast: Toast = { label: translate(t[translationKey]) };\n queueToast(toast);\n}\n\n/**\n * Queues the amelioration toast and returns a promise, to handle\n * several toasts being shown sequentially.\n * @param {string} message The message to be shown in the toast.\n */\nexport function showAmeliorationToast(message: string): void {\n const toast: Toast = { label: message };\n queueToast(toast);\n}\n\n/**\n * Queues the toast and returns a promise, to handle\n * several toasts being shown sequentially.\n * @param {number} bulkSize The number of pieces in the multipack.\n * @returns {Promise} A promise that is resolved when the toast is dismissed.\n */\nexport function showMultipackToast(bulkSize: number): Promise {\n const parameterName = 'nr_of_articles';\n const message = translate(t.TOAST_MULTIPACK_INFO).replaceAll(\n '{{' + parameterName + '}}',\n bulkSize.toString()\n );\n const toast: Toast = { label: message };\n return queueToast(toast);\n}\n\n/**\n * Displays a toast message depending on if;\n * 1) An product was added to the bag\n * 2) An product was added to the list\n * and whether the addition of the product was succesful.\n * @param {string} destination Either cart or list.\n * @param {boolean} success If the product was successfully added to the bag/list.\n * @param {boolean} ymal If we're coming from ymal.\n * @returns {void}\n */\nexport async function renderToast(\n destination: string,\n success: boolean,\n ymal: boolean\n) {\n const kompisTranslations = selectKompisTranslations(store.getState());\n const kompisUrls = selectKompisDexfSettings(store.getState()).urls;\n\n const addToCartError =\n ConfigurationSummaryToastMessage.getAddToCartFail(kompisTranslations);\n const addToListError =\n ConfigurationSummaryToastMessage.getAddToListFail(kompisTranslations);\n const addToCartYmalSuccessMessage = AddToBagToastMessage.getAddToCartSuccess(\n kompisTranslations,\n kompisUrls\n );\n const addToCartYmalErrorMessage = AddToBagToastMessage.getAddToCartFailure(\n kompisTranslations,\n kompisUrls\n );\n const addToListYmalSuccessMessage = AddToListToastMessage.getAddToListSuccess(\n kompisTranslations,\n kompisUrls\n );\n const addToListYmalErrorMessage = AddToListToastMessage.getAddToListFailure(\n kompisTranslations,\n kompisUrls\n );\n\n let toast: Toast = { label: '', actionHref: '', actionLabel: '' };\n if (destination === 'cart') {\n ymal\n ? (toast = success\n ? addToCartYmalSuccessMessage\n : addToCartYmalErrorMessage)\n : !success && (toast = addToCartError);\n return showToast(toast);\n }\n ymal\n ? (toast = success\n ? addToListYmalSuccessMessage\n : addToListYmalErrorMessage)\n : !success && (toast = addToListError);\n return showToast(toast);\n}\n\n/**\n * Queues the toast.\n * If the queue is already running it only pushes the the toast into the queue.\n * If the queue is not running it pushes the toast to the queue and runs the queue.\n * @param {Toast} toast\n * @returns {Promise}\n */\nasync function queueToast(toast: Toast): Promise {\n toastQueue.push(toast);\n if (queueIsRunning) {\n return;\n } else {\n runQueuePromise = runQueue();\n }\n}\n\n/**\n * Asynchronously runs a queue until the queue is empty.\n * Awaits the resolving of one toast until it shows the next toast.\n */\nasync function runQueue() {\n queueIsRunning = true;\n while (!!toastQueue.length) {\n const toastToRun = toastQueue.shift();\n await showToast(toastToRun);\n }\n queueIsRunning = false;\n}\n\n/**\n * Returns a promise that resolves when all queued toasts have\n * been shown (and the last one has timed out and closed).\n * @returns {Promise}\n */\nasync function noMorePendingToasts(): Promise {\n if (queueIsRunning && runQueuePromise) {\n await runQueuePromise;\n }\n}\n\n/**\n * Calculates the lifespan-time of the toast to be shown, shows the toast, and then sets a timeout\n * that resolves once that lifespan-time has passed.\n * @param {Toast} toast\n * @returns {Promise}\n */\nasync function showToast(toast: Toast | undefined): Promise {\n if (!toast) return Promise.resolve();\n const toastLifespan = calculateToastLifespan(toast.label);\n return new Promise((resolve, reject) => {\n toastManager.showToast(toast);\n setTimeout(async () => {\n resolve();\n }, toastLifespan);\n });\n}\n\n/**\n * Resets all toasts. For example when going from scene to start view.\n */\nexport function stopAndResetQueuedToasts() {\n dismissCurrentToast();\n queueIsRunning = false;\n while (toastQueue.length) {\n toastQueue.pop();\n }\n}\n\n/**\n * Calculate the lifespan of the toast based on the amount of characters in the toast label,\n * and a default max and min value.\n * Values retrieved from {@link https://skapa.ikea.net/components/messages/toast}\n * @param toastLabel\n * @returns {number} toastLifespan in milliseconds.\n */\nfunction calculateToastLifespan(toastLabel: string): number {\n const defaultMax: number = 10000;\n const defaultMin: number = 5000;\n const readTimeOneChar: number = 50;\n\n let toastLifespan: number = readTimeOneChar * toastLabel.length;\n\n if (toastLifespan < defaultMin) toastLifespan = defaultMin;\n if (toastLifespan > defaultMax) toastLifespan = defaultMax;\n\n return toastLifespan;\n}\n\n/**\n * Hides the toast that is currently shown.\n */\nexport function dismissCurrentToast(): void {\n toastManager.hideToast();\n}\n\nexport default {\n showTacToast,\n dismissCurrentToast,\n noMorePendingToasts,\n};\n","import { TacModel } from \"../../state/tac/tacTypes\";\n\n// Should be an enum, but cannot use it due to the ancient, pre-historic version of Typescript.\nexport type AmeliorationOutcomeType = string;\nexport const AmeliorationOutcome = {\n Success: 'success',\n Failure: 'failure',\n Declined: 'declined',\n Busy: 'busy',\n};\n\nexport type AmeliorationDialogCallbacks = {\n onAcceptClicked?: () => void,\n onDeclineClicked?: () => void,\n};\n\nexport type AmeliorationDialogContent = {\n headerText: string,\n bodyText: string,\n acceptText: string,\n declineText: string,\n};\n\nexport type TransformationResult = {\n success: boolean,\n tacOut?: TacModel,\n successMessage?: string,\n};\n\nexport type AmeliorationResult = {\n outcome: AmeliorationOutcomeType,\n tacOut?: TacModel,\n successMessage?: string,\n};\n\nexport type AmeliorationArgs = {\n transform: (tacIn: TacModel) => TransformationResult,\n fastCheck?: (tacIn: TacModel) => boolean,\n dialogContent: AmeliorationDialogContent,\n};\n\nexport type Amelioration = {\n start: (tacIn: TacModel) => Promise,\n checkWhetherCanAmeliorate: (tacIn: TacModel) => boolean | null,\n};\n\nexport type AmeliorationCache = {\n tacIn: TacModel | null;\n canAmeliorate: boolean | null;\n tacOut?: TacModel | null;\n successMessage?: string | null,\n};","import store from '../../state/';\nimport { TacModel } from '../../state/tac/tacTypes';\nimport { Dialog } from '../../state/dialog/dialogTypes';\nimport { selectDialogOptions } from '../../state/dialog/dialogSelectors';\nimport { openAmeliorationDialog } from '../../state/dialog/dialogActions';\nimport { showAmeliorationToast } from '../toastMaster';\nimport {\n Amelioration,\n AmeliorationArgs,\n AmeliorationCache,\n AmeliorationDialogCallbacks,\n AmeliorationOutcome,\n AmeliorationResult,\n TransformationResult,\n} from './ameliorationTypes';\nimport _ from 'lodash';\n\nexport const ameliorationDialogCallbacks: AmeliorationDialogCallbacks = {};\nlet busy: boolean = false;\n\nexport const setupAmelioration = (args: AmeliorationArgs): Amelioration => {\n const cache: AmeliorationCache = {\n tacIn: null,\n canAmeliorate: null,\n tacOut: null,\n successMessage: null,\n };\n\n const clearCacheIfNeeded = (tacIn: TacModel | undefined): boolean => {\n if (tacIn && JSON.stringify(tacIn) === JSON.stringify(cache.tacIn))\n return false;\n\n cache.tacIn = tacIn ? _.cloneDeep(tacIn) : null;\n cache.canAmeliorate = null;\n cache.tacOut = null;\n cache.successMessage = null;\n\n return true;\n };\n\n let userResponsePromise: any;\n let userHasAccepted: any;\n const setupUserResponsePromise = () => {\n userResponsePromise = new Promise((resolve, reject) => {\n userHasAccepted = resolve;\n });\n };\n\n const onAcceptClicked = (): void => {\n cache.successMessage && showAmeliorationToast(cache.successMessage);\n userHasAccepted(true);\n };\n\n const onDeclineClicked = (): void => {\n userHasAccepted(false);\n };\n\n const transformAndSetCache = (tacIn: TacModel): void => {\n const transformationResult: TransformationResult = args.transform(tacIn);\n cache.canAmeliorate = transformationResult.success;\n if (transformationResult.success) {\n cache.tacOut = _.cloneDeep(transformationResult.tacOut);\n cache.successMessage = transformationResult.successMessage;\n }\n };\n\n const checkWhetherCanAmeliorate = (tacIn: TacModel): boolean | null => {\n if (busy) return null;\n\n clearCacheIfNeeded(tacIn);\n\n if (cache.canAmeliorate !== null) return cache.canAmeliorate;\n\n if (args.fastCheck) {\n const canAmeliorate = args.fastCheck(tacIn);\n cache.canAmeliorate = canAmeliorate;\n return canAmeliorate;\n }\n\n transformAndSetCache(tacIn);\n\n return !!cache.canAmeliorate;\n };\n\n const start = async (tacIn: TacModel): Promise => {\n if (busy) {\n const busyAmeliorationResult: AmeliorationResult = {\n outcome: AmeliorationOutcome.Busy,\n };\n\n return busyAmeliorationResult;\n }\n\n clearCacheIfNeeded(tacIn);\n\n if (!(cache.canAmeliorate === false) && !cache.tacOut) {\n transformAndSetCache(tacIn);\n }\n\n if (!cache.canAmeliorate) {\n const failureAmeliorationResult: AmeliorationResult = {\n outcome: AmeliorationOutcome.Failure,\n };\n\n return failureAmeliorationResult;\n }\n\n ameliorationDialogCallbacks.onAcceptClicked = onAcceptClicked;\n ameliorationDialogCallbacks.onDeclineClicked = onDeclineClicked;\n\n let options: Dialog = selectDialogOptions(store.getState());\n if (!options) options = {};\n if (!options.options) options.options = {};\n if (!options.options.ameliorationData)\n options.options.ameliorationData = {};\n const { ameliorationData } = options.options;\n ameliorationData.dialogContent = args.dialogContent;\n\n busy = true;\n setupUserResponsePromise();\n store.dispatch(openAmeliorationDialog(options));\n const acceptedByUser = await userResponsePromise;\n busy = false;\n\n if (acceptedByUser) {\n const successAmeliorationResult: AmeliorationResult = {\n outcome: AmeliorationOutcome.Success,\n tacOut: _.cloneDeep(cache.tacOut) || undefined,\n successMessage: cache.successMessage || undefined,\n };\n\n return successAmeliorationResult;\n }\n\n // If we get here, the amelioration has been declined by the user.\n const declinedAmeliorationResult: AmeliorationResult = {\n outcome: AmeliorationOutcome.Declined,\n };\n\n return declinedAmeliorationResult;\n };\n\n const amelioration: Amelioration = {\n start,\n checkWhetherCanAmeliorate,\n };\n\n return amelioration;\n};\n","import { useDispatch, useSelector } from 'react-redux';\nimport React, { useEffect } from 'react';\nimport { PromptButtonTypeEnum } from '@inter-ikea-kompis/component-prompt';\nimport { KompisPrompt, KompisText } from '@inter-ikea-kompis/react-components';\nimport { SkapaTheme } from '@inter-ikea-kompis/themes';\nimport { actionCloseDialog } from '../../state/dialog/dialogActions';\nimport StopPropagation from '../utils/StopPropagation';\nimport { selectIsMobilePortrait } from '../../state/userAgent/userAgentSelectors';\nimport { selectDialogOptions } from '../../state/dialog/dialogSelectors';\nimport { ameliorationDialogCallbacks } from '../../services/amelioration';\nimport { AmeliorationDialogContent } from '../../services/amelioration/ameliorationTypes';\nimport localStatisticsReporter from '../../services/statistics/insights/custom/local/localStatisticsReporter';\n\nconst AmeliorationDialog = () => {\n const isMobilePortrait = useSelector(selectIsMobilePortrait);\n const dialogOptions = useSelector(selectDialogOptions);\n const dispatch = useDispatch();\n\n const dialogContent: AmeliorationDialogContent | null =\n dialogOptions?.options?.ameliorationData?.dialogContent || null;\n\n useEffect(() => {\n localStatisticsReporter.reportAmeliorationDialogShown();\n }, []);\n\n /**\n * Handle on decline\n */\n const declineClick = () => {\n localStatisticsReporter.reportAmeliorationDialogDeclined();\n dispatch(actionCloseDialog());\n if (ameliorationDialogCallbacks?.onDeclineClicked) {\n ameliorationDialogCallbacks.onDeclineClicked();\n } else {\n throw new Error('No decline callback found for amelioration dialog.');\n }\n };\n\n /**\n * Handle on accept\n */\n const acceptClick = () => {\n localStatisticsReporter.reportAmeliorationDialogAccepted();\n dispatch(actionCloseDialog());\n if (ameliorationDialogCallbacks?.onAcceptClicked) {\n ameliorationDialogCallbacks.onAcceptClicked();\n } else {\n throw new Error('No accept callback found for amelioration dialog.');\n }\n };\n\n const isPrimaryButtonEvent = (type: PromptButtonTypeEnum) =>\n type === PromptButtonTypeEnum.primaryButton;\n\n const handleButtonClick = ({ detail: { type } }: any) =>\n isPrimaryButtonEvent(type) ? acceptClick() : declineClick();\n\n return dialogContent ? (\n \n
\n \n {dialogContent.bodyText}\n \n
\n
\n ) : null;\n};\n\nexport default AmeliorationDialog;\n","import DeeplinkChoice from '../../components/Lightbox/DeeplinkChoice';\nimport SeriesGallery from '../../components/SeriesGallery';\nimport PacMissing from '../../components/Lightbox/PacMissing';\nimport IpexGalleryWarning from '../../components/Lightbox/IpexGalleryWarning';\nimport AmeliorationDialog from '../../components/Lightbox/AmeliorationDialog';\n\nexport default {\n DEEPLINK_CHOICE: DeeplinkChoice,\n PAC_MISSING: PacMissing,\n SERIES_GALLERY: SeriesGallery,\n IPEX_GALLERY_WARNING: IpexGalleryWarning,\n AMELIORATION_DIALOG: AmeliorationDialog,\n};\n","import { DIALOG_OPEN, DIALOG_CLOSE } from '../actionConstants';\nimport dialogVariants from './dialogVariations';\nimport { Dialog } from './dialogTypes';\nimport { CustomAction } from '../../generalTypes';\nimport { Action } from 'redux';\n\nexport const actionOpenSeriesGallery = (\n options: Dialog\n): CustomAction<{ options: Dialog; dialog: any }> => ({\n type: DIALOG_OPEN,\n payload: {\n dialog: dialogVariants.SERIES_GALLERY,\n options,\n },\n});\n\nexport const closeSeriesGallery = (): CustomAction<{ dialog: any }> => ({\n type: DIALOG_CLOSE,\n payload: {\n dialog: dialogVariants.SERIES_GALLERY,\n },\n});\n\nexport const openDeeplinkChoice = (\n options: Dialog\n): CustomAction<{ options: Dialog; dialog: any }> => ({\n type: DIALOG_OPEN,\n payload: {\n dialog: dialogVariants.DEEPLINK_CHOICE,\n options,\n },\n});\n\nexport const openPacMissing = (\n options: Dialog\n): CustomAction<{ options: Dialog; dialog: any }> => ({\n type: DIALOG_OPEN,\n payload: {\n dialog: dialogVariants.PAC_MISSING,\n options,\n },\n});\n\nexport const openIpexGalleryWarning = (\n options: Dialog\n): CustomAction<{ options: Dialog; dialog: any }> => ({\n type: DIALOG_OPEN,\n payload: {\n dialog: dialogVariants.IPEX_GALLERY_WARNING,\n options,\n },\n});\n\nexport const openAmeliorationDialog = (\n options: Dialog\n): CustomAction<{ options: Dialog; dialog: any }> => ({\n type: DIALOG_OPEN,\n payload: {\n dialog: dialogVariants.AMELIORATION_DIALOG,\n options,\n },\n});\n\nexport const actionCloseDialog = (): Action => ({\n type: DIALOG_CLOSE,\n});\n","import {\n VPC_LOADED,\n VPC_ERROR,\n VPC_CLEAR,\n VPC_SET_SAVE_STATE,\n VPC_DIRTY_CONFIGURATION,\n VPC_SET_SAVE_CODE,\n VPC_SET_SAVE_PROGRESS_STATUS,\n} from '../actionConstants';\nimport { Action, RootAction } from '../../generalTypes';\nimport { VpcLoadedAction, VpcSaveProgressStatusEnumType } from './vpcTypes';\nimport { ConfirmationSummaryShareDesignStateEnum } from '@inter-ikea-kompis/component-configuration-summary';\n\nexport const actionVpcLoaded = (\n code: string,\n currentView: string\n): Action => ({\n type: VPC_LOADED,\n payload: {\n currentView: currentView,\n code: code,\n },\n});\n\nexport const actionVpcError = (): RootAction => ({\n type: VPC_ERROR,\n});\n\nexport const actionVpcClear = (): RootAction => ({\n type: VPC_CLEAR,\n});\n\nexport const actionSetSaveVpcState = (\n state: ConfirmationSummaryShareDesignStateEnum\n) => ({\n type: VPC_SET_SAVE_STATE,\n payload: { state },\n});\n\nexport const actionSetSavedVpcCode = (vpcCode: string) => ({\n type: VPC_SET_SAVE_CODE,\n payload: { vpcCode },\n});\n\nexport const actionSetDirtyConfiguration = (dirtyConfiguration: boolean) => ({\n type: VPC_DIRTY_CONFIGURATION,\n payload: { dirtyConfiguration },\n});\n\nexport const actionSetVpcSaveProgressStatus = (\n progressStatus: VpcSaveProgressStatusEnumType\n) => ({\n type: VPC_SET_SAVE_PROGRESS_STATUS,\n payload: { progressStatus },\n});\n","import { UrlUtility } from '@ikea-aoa/ikea-shared-utils';\nimport {\n TAC_ADD_ITEM,\n TAC_ADD_MULTIPLE,\n TAC_REMOVE_ITEM,\n TAC_LOAD,\n TAC_UPDATE_ITEM,\n TAC_HIDE_ITEM,\n TAC_UPDATE_MULTIPLE,\n TAC_SET_WALL,\n TAC_SET_USE_MOUNTING_RAIL,\n TAC_REMOVE_TOAST_FLAGS,\n TAC_SET_TOAST_FLAGS,\n TAC_UPDATE_WALL_SIZE,\n} from '../actionConstants';\nimport history from '../../services/history';\nimport { openDeeplinkChoice } from '../dialog/dialogActions';\nimport { actionVpcLoaded } from '../vpc/vpcActions';\nimport store from '..';\nimport pacValid from '../../services/PacValidator';\nimport fixOldVpc from '../../services/FixVPC';\nimport platform from '../../util/platform';\nimport constants from '../../settings/constants';\nimport storage from '../../services/history/storage';\nimport localStatisticsActions from '../../services/statistics/insights/custom/local/localStatisticsActions';\nimport { DesignSourceEnum } from '@insights/insights-data-provider';\nimport { thunkLoadPac, thunkOpenPacMissing } from './tacThunks';\nimport { thunkSetView } from '../navigation/navigationThunks';\nimport localStatisticsReporter from '../../services/statistics/insights/custom/local/localStatisticsReporter';\nimport mandatoryStatisticsReporter from '../../services/statistics/insights/mandatory/mandatoryStatisticsReporter';\nimport { getVpcService } from '../../services/ServiceHandler';\n\nexport const thunkNavigateWithMissingPac = () => dispatch => {\n dispatch(thunkSetView(constants.VIEW_NAMES.START));\n dispatch(thunkOpenPacMissing());\n};\n\nconst reportMandatoryInitPlanningSession = (vpcCode, vpcSource) => {\n mandatoryStatisticsReporter.reportInitialPlanningSession(vpcCode, vpcSource);\n};\n\nconst reportLocalOpenPlannerEvent = (entry, ...rest) => {\n localStatisticsReporter.reportOpenPlanner(entry, ...rest);\n if (UrlUtility.getCombinedQuery().applications) {\n localStatisticsReporter.reportSeriesGalleryUsage();\n }\n};\n\nconst thunkHandleVpcCase = (vpcCode, saveGame) => dispatch => {\n // Check if \"vpcSource=skytta\" is present in url\n const hashQueryVpcSource = UrlUtility.getCombinedQuery().vpcSource;\n const designSource =\n hashQueryVpcSource && hashQueryVpcSource.includes(DesignSourceEnum.skytta)\n ? DesignSourceEnum.skytta\n : DesignSourceEnum.gallery;\n\n // Clean url from vpc code\n document.location.hash = document.location.hash.replace(\n /(.*)(?:#)((?:\\/U\\/)?.{5,6})(?:[&?]|$)(.*)/i,\n '$1#?$3'\n );\n /* // Clean url from initial view\n document.location.hash = document.location.hash.replace(\n /&[a-zA-Z]+_[a-zA-Z]+=[a-zA-Z]+/i,\n ''\n ); */\n return getVpcService()\n .getConfiguration(vpcCode)\n .then(data => {\n reportMandatoryInitPlanningSession(vpcCode, designSource);\n reportLocalOpenPlannerEvent(localStatisticsActions.VPC_DEEPLINK, vpcCode);\n\n dispatch(\n actionVpcLoaded(vpcCode, store.getState().navigation.currentView)\n );\n return fixOldVpc(data.configuration);\n })\n .catch(error => {\n localStatisticsReporter.reportOpenPlannerVpcDeeplinkNotSupported(vpcCode);\n // didnt find vpc, so if savegame present,\n // let user close gallery and resume\n dispatch(thunkNavigateWithMissingPac());\n return saveGame;\n });\n};\n\nconst thunkHandleSprCase = (sprId, saveGame) => dispatch => {\n const sprList = store.getState().sprs;\n const spr = sprList.find(\n spr => spr.id === sprId || spr.kompisSPR.content?.ruItemNo === sprId\n );\n\n if (!spr) {\n localStatisticsReporter.reportOpenPlannerSprDeeplinkNotSupported(sprId);\n } else {\n reportMandatoryInitPlanningSession(spr?.vpcCode, DesignSourceEnum.spr);\n reportLocalOpenPlannerEvent(localStatisticsActions.SPR_DEEPLINK, sprId);\n }\n\n if (spr && (!saveGame || !pacValid(saveGame))) {\n // Deeplinked SPR and nothing in storage so go with the SPR.\n return Promise.resolve(spr);\n } else if (spr && saveGame) {\n // We have something stored so user must decide if this is to be\n // discarded and use deeplinked SPR. We ask the user, but load the\n // stored configuration for now.\n dispatch(openDeeplinkChoice({ spr }));\n return Promise.resolve(saveGame);\n } else if (!spr && saveGame) {\n // SPR not found. Inform the user, go to the closable SPR gallery and\n // load the \"save game\" so that something is there if the usr closes\n // the SPR gallery.\n dispatch(thunkNavigateWithMissingPac());\n return Promise.resolve(saveGame);\n } else {\n dispatch(thunkNavigateWithMissingPac());\n return Promise.resolve();\n }\n};\n\nconst thunkHandleSaveGameCase = saveGame => dispatch => {\n reportMandatoryInitPlanningSession('', DesignSourceEnum.startFromTemplate);\n reportLocalOpenPlannerEvent(localStatisticsActions.SAVE);\n\n const promise = Promise.resolve(saveGame);\n const isLangPickerReload = storage.session.getItem(\n constants.SESSION_STORAGE.LANGUAGE_PICKER_ACTIVE_VIEW_KEY\n );\n\n if (platform.isKiosk && !isLangPickerReload) {\n dispatch(thunkSetView(constants.VIEW_NAMES.START));\n }\n\n return promise;\n};\n\nexport function findPAC() {\n return dispatch => {\n const vpcMatch = /(?:(?:#|\\/U\\/))(\\w{5,6})(?=[&?]|$)/i.exec(\n document.location.hash\n );\n const vpcCode = vpcMatch && vpcMatch[1];\n\n // example spr id 79272677\n const sprMatch = /(?:#S)(\\d{8})(?=[&?]|$)/.exec(document.location.hash);\n const sprId = sprMatch && sprMatch[1];\n const saveGame = history.load();\n\n const handleCase = () => {\n if (vpcCode) return dispatch(thunkHandleVpcCase(vpcCode));\n else if (sprId) return dispatch(thunkHandleSprCase(sprId, saveGame));\n else if (saveGame) return dispatch(thunkHandleSaveGameCase(saveGame));\n else {\n reportMandatoryInitPlanningSession(\n '',\n DesignSourceEnum.startFromScratch\n );\n reportLocalOpenPlannerEvent(localStatisticsActions.NO_SAVE);\n\n return Promise.reject(new Error('No pacs found'));\n }\n };\n\n return handleCase()\n .then(pac => {\n if (!pac) {\n // a dialog took care of it\n return;\n } else if (pacValid(pac)) {\n dispatch(thunkLoadPac(pac));\n } else {\n dispatch(thunkNavigateWithMissingPac());\n }\n })\n .catch(e => {\n dispatch(thunkSetView(constants.VIEW_NAMES.START));\n });\n };\n}\n\nexport const actionLoadPac = (model, errors, centerConfiguration) => ({\n type: TAC_LOAD,\n payload: {\n model,\n centerConfiguration,\n errors,\n },\n});\n\nconst genericTacUpdaterAction =\n type =>\n (model, errors, meta = {}) => ({\n type,\n payload: {\n model,\n errors,\n },\n meta,\n });\n\nexport const actionAddItem = genericTacUpdaterAction(TAC_ADD_ITEM);\nexport const actionAddMultiple = genericTacUpdaterAction(TAC_ADD_MULTIPLE);\nexport const actionRemoveItem = genericTacUpdaterAction(TAC_REMOVE_ITEM);\nexport const actionHideItem = genericTacUpdaterAction(TAC_HIDE_ITEM);\nexport const actionUpdateItem = genericTacUpdaterAction(TAC_UPDATE_ITEM);\nexport const actionUpdateMultiple =\n genericTacUpdaterAction(TAC_UPDATE_MULTIPLE);\nexport const actionSetWall = genericTacUpdaterAction(TAC_SET_WALL);\n\nexport function tacSetToastFlags(toastFlags) {\n return {\n type: TAC_SET_TOAST_FLAGS,\n payload: {\n toastFlags,\n },\n };\n}\n\nexport function actionSetUseMountingRails(model) {\n return {\n type: TAC_SET_USE_MOUNTING_RAIL,\n payload: {\n model,\n },\n };\n}\n\nexport function removeToastFlags() {\n return {\n type: TAC_REMOVE_TOAST_FLAGS,\n };\n}\n\nexport const actionSetWallSize = wallSize => ({\n type: TAC_UPDATE_WALL_SIZE,\n payload: wallSize,\n});\n","import { Popups } from './popupsTypes';\nimport { MiniSurveyStateEnum } from '@inter-ikea-kompis/component-mini-survey';\n\nconst defaultState: Popups = {\n sceneErrorVisible: false,\n introPopupsVisible: false,\n hasShownIntroPopups: false,\n hasShownPegboardHint: false,\n showPegboardHint: null,\n cuttableMountingRailHintCheckPending: false,\n hasShownCuttableMountingRailHint: false,\n showCuttableMountingRailHint: null,\n cuttableMountingRailHintAlignment: null,\n showDoorsHint: null,\n hasShownExtendableConf: false,\n showExtendableConf: null,\n hasHadUserInteraction: false,\n supplyBannerClosed: false,\n hasShownFilterIntroPopup: false,\n filterIntroPopupVisible: false,\n hasShownMountingRailSheet: false,\n confDialog: {},\n overlayActive: false,\n scrollException: false,\n surveyVisible: false,\n surveyState: MiniSurveyStateEnum.default,\n errorHandlingPendingTimestamp: null,\n};\n\nexport default defaultState;\n","import {\n POPUPS_HIDE_INTRO_POPUPS,\n POPUPS_SHOW_SCENE_ERRORS,\n POPUPS_HIDE_SCENE_ERRORS,\n POPUPS_CONF_OPEN,\n POPUPS_CONF_CLOSE,\n SCENE_ITEM_PICKED_UP,\n POPUPS_RESET_INTRO,\n POPUPS_CONF_CLEAR,\n TAC_REMOVE_ITEM,\n TAC_SET_USE_MOUNTING_RAIL,\n POPUPS_MOUNTING_RAIL_SHEET_OPEN,\n POPUPS_SET_HAS_SHOWN_FILTER_INTRO_POPUP,\n POPUPS_SUPPLY_BANNER_CLOSE,\n POPUPS_SET_FILTER_INTRO_POPUP_VISIBLE,\n POPUPS_OVERLAY_ACTIVE,\n POPUPS_SET_INTRO_POPUPS_VISIBLE,\n POPUPS_SET_HAS_SHOWN_INTRO_POPUPS,\n POPUPS_SCROLL_EXCEPTION,\n POPUPS_HANDLE_POPUPS_ON_ADD_ITEM,\n POPUPS_HANDLE_POPUPS_ON_UPDATE_ITEM,\n POPUPS_SET_CUTTABLE_MOUNTING_RAIL_HINT_CHECK_PENDING,\n POPUPS_SET_SURVEY_VISIBLE,\n POPUPS_SET_SURVEY_STATE,\n POPUPS_SET_ERROR_HANDLING_PENDING_TIMESTAMP,\n} from '../actionConstants';\nimport defaultState from './popupsDefaultState';\nimport { Popups } from './popupsTypes';\n\nexport default (state: Popups = defaultState, action: any) => {\n switch (action.type) {\n case POPUPS_HANDLE_POPUPS_ON_ADD_ITEM:\n return {\n ...state,\n ...action.payload,\n };\n\n case POPUPS_HANDLE_POPUPS_ON_UPDATE_ITEM:\n return {\n ...state,\n ...action.payload,\n };\n case POPUPS_SET_CUTTABLE_MOUNTING_RAIL_HINT_CHECK_PENDING:\n return {\n ...state,\n ...action.payload,\n };\n case TAC_REMOVE_ITEM:\n case TAC_SET_USE_MOUNTING_RAIL:\n return {\n ...state,\n hasHadUserInteraction: true,\n };\n case SCENE_ITEM_PICKED_UP:\n if (\n state.showPegboardHint &&\n state.showPegboardHint.itemid === action.payload.item.itemid\n ) {\n return {\n ...state,\n showPegboardHint: null,\n hasShownPegboardHint: true,\n showDoorsHint: null,\n };\n }\n if (state.showDoorsHint) {\n return {\n ...state,\n showDoorsHint: null,\n };\n }\n return state;\n case POPUPS_SHOW_SCENE_ERRORS:\n return {\n ...state,\n sceneErrorVisible: true,\n };\n case POPUPS_HIDE_SCENE_ERRORS:\n return {\n ...state,\n sceneErrorVisible: false,\n };\n case POPUPS_HIDE_INTRO_POPUPS:\n if (state.introPopupsVisible) {\n return {\n ...state,\n introPopupsVisible: false,\n };\n }\n return state;\n case POPUPS_CONF_OPEN:\n return {\n ...state,\n confDialog: {\n ...state.confDialog,\n open: true,\n },\n };\n case POPUPS_CONF_CLOSE:\n return {\n ...state,\n confDialog: {\n open: false,\n closedId: action.payload.closedId,\n },\n };\n case POPUPS_CONF_CLEAR:\n return {\n ...state,\n confDialog: {\n ...state.confDialog,\n closedId: null,\n },\n };\n case POPUPS_SUPPLY_BANNER_CLOSE:\n return {\n ...state,\n supplyBannerClosed: true,\n };\n case POPUPS_MOUNTING_RAIL_SHEET_OPEN: {\n return {\n ...state,\n hasShownMountingRailSheet: true,\n };\n }\n\n case POPUPS_RESET_INTRO:\n return defaultState;\n\n case POPUPS_SET_HAS_SHOWN_FILTER_INTRO_POPUP:\n return {\n ...state,\n hasShownFilterIntroPopup: action.payload.hasShownFilterIntroPopup,\n };\n\n case POPUPS_SET_FILTER_INTRO_POPUP_VISIBLE:\n return {\n ...state,\n filterIntroPopupVisible: action.payload.filterIntroPopupVisible,\n };\n\n case POPUPS_OVERLAY_ACTIVE:\n return {\n ...state,\n overlayActive: action.payload.state,\n };\n\n case POPUPS_SCROLL_EXCEPTION:\n return {\n ...state,\n scrollException: action.payload.state,\n };\n\n case POPUPS_SET_INTRO_POPUPS_VISIBLE:\n return {\n ...state,\n introPopupsVisible: action.payload.introPopupsVisible,\n };\n\n case POPUPS_SET_HAS_SHOWN_INTRO_POPUPS:\n return {\n ...state,\n hasShownIntroPopups: true,\n };\n\n case POPUPS_SET_SURVEY_VISIBLE:\n return {\n ...state,\n surveyVisible: action.payload.surveyVisible,\n };\n\n case POPUPS_SET_SURVEY_STATE:\n return {\n ...state,\n surveyState: action.payload.surveyState,\n };\n\n case POPUPS_SET_ERROR_HANDLING_PENDING_TIMESTAMP:\n return {\n ...state,\n errorHandlingPendingTimestamp:\n action.payload.errorHandlingPendingTimestamp,\n };\n\n default:\n return state;\n }\n};\n","import { cloneDeep } from 'lodash';\n\nexport default function getWallPoints(oldPoints, width, height) {\n const points = cloneDeep(oldPoints);\n const rightPointsIndex = [0, 1];\n const topPointsIndex = [0, 1];\n for (let i = 2; i < points.length; i++) {\n if (points[i].x > points[rightPointsIndex[0]].x) {\n rightPointsIndex[0] = i;\n } else if (points[i].x > points[rightPointsIndex[1]].x) {\n rightPointsIndex[1] = i;\n }\n\n if (points[i].y > points[topPointsIndex[0]].y) {\n topPointsIndex[0] = i;\n } else if (points[i].y > points[topPointsIndex[1]].y) {\n topPointsIndex[1] = i;\n }\n }\n\n if (width) {\n points[rightPointsIndex[0]].x = parseFloat(width);\n points[rightPointsIndex[1]].x = parseFloat(width);\n }\n\n if (height) {\n points[topPointsIndex[0]].y = parseFloat(height);\n points[topPointsIndex[1]].y = parseFloat(height);\n }\n\n return points;\n}\n","import tacHelpers from '../tacHelpers';\nimport addItem from './addItem';\n\n// ITEMS\nexport function addMultipleItems(state, items, parent, options) {\n let model = state;\n\n items.forEach(item => {\n let updParent;\n if (item.parentRef) {\n updParent = tacHelpers.getItem(model, item.parentRef);\n delete item.parentRef;\n } else if (parent) {\n updParent = tacHelpers.getItem(model, parent.itemid);\n }\n model = addItem(model, item, updParent, options);\n });\n\n return model;\n}\n","import tacHelpers from '../tacHelpers';\nimport updateItem from './updateItem';\n\nexport function updateMultipleItems(state, items, parent, options) {\n let model = state;\n\n items.forEach((item, index, arr) => {\n let updParent;\n if (item.parentRef) {\n updParent = tacHelpers.getItem(model, item.parentRef);\n delete item.parentRef;\n } else if (parent) {\n updParent = tacHelpers.getItem(model, parent.itemid);\n }\n\n const localOptions = { ...options, ...item.localOptions };\n item.localOptions && delete item.localOptions;\n\n model = updateItem(model, item, updParent, {\n keepParts: localOptions?.keepParts,\n moveOthers: localOptions?.moveOthers,\n postponeSort: localOptions?.hasDependencies || index < arr.length - 1,\n forceParts:\n localOptions?.forceParts ||\n (localOptions?.forcePartsOnFirstItem && index === 0),\n partsToSkip: localOptions?.partsToSkip || [],\n isPersistent: localOptions?.isPersistent,\n });\n });\n\n return model;\n}\n","import { range } from '../range';\nimport removeItem from './removeItem';\nimport { addMultipleItems } from './addMultipleItems';\nimport { updateMultipleItems } from './updateMultipleItems';\n\nexport default function updateDependentItems(model, options = {}) {\n const diff = range.getDependencyDiff(model, options);\n\n if (diff) {\n const { added, updated, removed } = diff;\n\n if (removed.length) {\n if (!options.isPersistent) {\n removed\n .filter(item => !item.keepDuringDrag)\n .forEach(item => {\n model = removeItem(model, item);\n });\n model = updateMultipleItems(\n model,\n removed.filter(item => item.keepDuringDrag),\n model,\n { ...options, moveOthers: false }\n );\n } else {\n removed.forEach(item => {\n model = removeItem(model, item);\n });\n }\n }\n\n if (updated.length) {\n if (options.flagTempItems) {\n updated.forEach(item => (item.isTempItem = true));\n }\n model = updateMultipleItems(model, updated, model, {\n ...options,\n moveOthers: false,\n });\n }\n\n if (added.length) {\n if (options.flagTempItems) {\n added.forEach(item => (item.isTempItem = true));\n }\n model = addMultipleItems(model, added, model, options);\n }\n }\n\n return model;\n}\n","import _ from 'lodash';\n\nimport products from '../../services/products/productHandler';\nimport idGenerator from './idGenerator';\nimport getDefaultPac from './getDefaultPAC';\nimport tacHelpers from '../../state/tac/tacHelpers';\nimport productService from '../../services/products';\nimport geometry from '../../scene/util/geometry';\nimport getWallPoints from '../../state/tac/tacReducer/getWallPoints';\nimport constants from '../../settings/constants';\nimport { floor } from '../round';\nimport updateDependentItems from '../../state/tac/tacReducer/updateDependentItems';\nimport { applicationSettings } from '../../settings/application';\nimport platform from '../platform';\nimport { RANGES } from '../../constants';\n\nfunction parseItems(items) {\n const isElvarli = applicationSettings.applicationName === RANGES.ELVARLI;\n return items.map(item => {\n const product = products.find(product => product.id === item.id);\n let parsedItem = {};\n\n Object.assign(parsedItem, product);\n parsedItem = {\n ...parsedItem,\n itemid: idGenerator.id(),\n id: item.id,\n ...(isElvarli && item.height && { height: item.height }),\n x: item.x,\n y: item.y,\n z: item.z || 0,\n };\n\n if (item.items) {\n parsedItem.items = parseItems(item.items);\n }\n return parsedItem;\n });\n}\n\n/**\n * Gets the items with only the props relevant for the PAC left\n * @param {*} items\n * @returns {Array}\n */\nfunction compressItems(items) {\n return items.map(({ id, itemid, x, y, z, height, items: itemsItems }) => ({\n id,\n itemid,\n x,\n y,\n z,\n height,\n ...(itemsItems && { items: compressItems(itemsItems) }),\n }));\n}\n\nfunction resetX(items) {\n const minX = Math.min(...items.map(item => item.x));\n return items.map(item => ({ ...item, x: item.x - minX }));\n}\n\nfunction updateSidewalls(tac, wallWidth) {\n const rightSidewall = tac.items.find(\n item => item.x > 0 && productService.isType(item, 'sidewall')\n );\n\n if (rightSidewall) {\n rightSidewall.x = wallWidth;\n }\n}\n\nfunction adjustWallAgnosticTac(tac) {\n const getSizeBasedOnPoints = points => {\n const { height, width } = geometry.surround(points);\n return { height, width };\n };\n\n const FREE_SPACE = 650; // we want these tacs to load with at least 65cm of free space to each side\n let wallSize = geometry.surround(tac.wall.points);\n\n const lockedLeft = tac.items.find(\n item =>\n productService.isType(item, 'sidewall') &&\n tacHelpers.hasExtClothesRail(item)\n );\n\n let rightSidewall = tac.items.find(\n item => item.x > 0 && productService.isType(item, 'sidewall')\n );\n const allSections = tacHelpers.getSections(tac).sort((a, b) => b.x - a.x);\n const rightmostSection = allSections[0];\n // normally we would just do tacHelpers.isClothesRailConnected() on the right sidewall,\n // but here we don't have that information yet\n const lockedRight =\n rightSidewall && tacHelpers.hasExtClothesRail(rightmostSection);\n\n if (lockedLeft && lockedRight) {\n const points = getWallPoints(\n tac.wall.points,\n rightSidewall.x,\n wallSize.height\n );\n // tac goes wall-to-wall, adjust wall according to position of the right sidewall\n tac.wall = {\n points,\n size: getSizeBasedOnPoints(points),\n };\n return;\n }\n\n const size = tacHelpers.getSize(tac);\n const limits = tacHelpers.getLimits(tac);\n\n if (lockedLeft) {\n // tac is connected to only the left sidewall so move nothing,\n // but make sure there is some extra space on the right\n\n const actualWidth = limits.max.x;\n\n const newWidth = Math.max(wallSize.width, actualWidth + FREE_SPACE);\n if (newWidth !== wallSize.width) {\n const points = getWallPoints(\n tac.wall.points,\n size.width + FREE_SPACE,\n wallSize.height\n );\n tac.wall = {\n points,\n size: getSizeBasedOnPoints(points),\n };\n updateSidewalls(tac, newWidth);\n }\n return;\n }\n\n const minX = Math.min(\n ...tac.items\n .filter(item => !productService.isType(item, 'sidewall'))\n .map(item => item.x)\n );\n\n if (lockedRight) {\n // tac is connected to only the right sidewall\n const actualWidth = rightSidewall.x - limits.min.x;\n\n let move;\n if (actualWidth + FREE_SPACE > wallSize.width) {\n // move stuff to create some space on the left\n move = minX - FREE_SPACE;\n } else {\n // just move everything to the right end of the wall\n move = -(wallSize.width - rightSidewall.x);\n }\n\n tac.items = tac.items.map(item => ({\n ...item,\n x: item.x === 0 ? 0 : item.x - move,\n }));\n\n rightSidewall = tac.items.find(\n item => item.x > 0 && productService.isType(item, 'sidewall')\n );\n const points = getWallPoints(\n tac.wall.points,\n rightSidewall.x,\n wallSize.height\n );\n tac.wall = {\n points,\n size: getSizeBasedOnPoints(points),\n };\n return;\n }\n\n // if we reach here, this is a \"free-standing\" spr, not connected to any walls\n\n // let's first make sure that the wall is big enough\n const newWidth = Math.max(wallSize.width, size.width + FREE_SPACE * 2);\n if (newWidth !== wallSize.width) {\n const points = getWallPoints(tac.wall.points, newWidth, wallSize.height);\n tac.wall = {\n points,\n size: getSizeBasedOnPoints(points),\n };\n wallSize = geometry.surround(tac.wall.points);\n }\n\n // and then center the configuration\n const move = minX - floor((wallSize.width - size.width) / 2, 1);\n tac.items = tac.items.map(item => ({\n ...item,\n x: item.x === 0 ? 0 : item.x - move,\n }));\n\n updateSidewalls(tac, newWidth);\n}\n\n/**\n * Checks whether a conversion from PAC to TAC is correct according to range specific rules\n * @param {*} pac\n * @param {*} tac\n * @returns {Boolean}\n */\nconst isInvalidConversion = (pac, tac) => {\n switch (applicationSettings.applicationName) {\n case 'BOAXEL':\n if (tac.items.length) {\n const wallRect = {\n ...geometry.surround(tac.wall.points),\n depth: constants.ROOM_DEPTH,\n };\n const tacRect = geometry.surround(tac.items);\n\n if (!geometry.contains(wallRect, tacRect)) {\n return true;\n }\n }\n break;\n default:\n break;\n }\n\n return false;\n};\n\n/**\n * Gets the settings from a PAC, stripped of outdated properties\n * @param {*} pac\n * @returns {Object}\n */\nconst getPacSettings = pac => {\n if (!pac.settings) {\n return {};\n }\n const { filter, ...settingsWithoutFilter } = pac.settings;\n return settingsWithoutFilter;\n};\n\n/**\n * Gets a new version of the TAC, with the wall points set to their max values\n * @param {*} tac\n * @returns {Object}\n */\nconst getMaxWalledTac = tac => {\n const points = getWallPoints(\n tac.wall.points,\n constants.WALL.width.max,\n constants.WALL.height.max\n );\n const { width, height } = geometry.surround(points);\n return {\n ...tac,\n wall: {\n ...tac.wall,\n points,\n size: {\n width,\n height,\n },\n },\n };\n};\n\n/**\n * Gets a new version of the TAC, with range specific adjustments made\n * @param {*} pac\n * @param {*} tac\n * @returns {Object|undefined}\n */\nconst getRangeAdjustedTac = (pac, tac) => {\n let newTac = { ...tac };\n switch (applicationSettings.applicationName) {\n case 'BOAXEL':\n if (isInvalidConversion(pac, newTac)) {\n newTac = getMaxWalledTac(newTac);\n newTac = updateDependentItems(newTac, { isPersistent: true });\n\n if (isInvalidConversion(pac, newTac)) {\n return;\n }\n }\n return newTac;\n default:\n return newTac;\n }\n};\n\n/**\n * Gets a new version of the TAC with flags set for which (if any) toasts to display on load\n * @param {*} tac\n * @param {*} pac\n * @returns {Object}\n */\nexport const getToastFlaggedTac = (tac, pac, extraItemsExist) => {\n const getArticlesExchangedToast = () => {\n const articlesExchangedForBetterSuitedOption = !tacHelpers.itemsUnchanged(\n tac.items,\n pac.model.items,\n {\n props: ['id', 'items'],\n realArticles: true,\n }\n );\n return (\n articlesExchangedForBetterSuitedOption && ['TOASTER_ARTICLE_EXCHANGED']\n );\n };\n\n const getResetWallToast = () => {\n const configurationNoLongerFitsInTheRoom =\n pac.wall && tac.wall && !_.isEqual(pac.wall, tac.wall);\n return configurationNoLongerFitsInTheRoom && ['TOASTER_RESET_WALL'];\n };\n\n const getExtraItemsToast = () => {\n return (\n extraItemsExist && [\n platform.isKiosk ? 'TOASTER_PEGBOARD_KIOSK' : 'TOASTER_PEGBOARD_INFO',\n ]\n );\n };\n\n return [\n getExtraItemsToast(),\n getResetWallToast(),\n getArticlesExchangedToast(),\n ].filter(Boolean);\n};\n\n/**\n * Converts a PAC (Persistent Article Configuration, i.e. what is saved in VPC/save game)\n * to a TAC (Temporary Article Configuration, i.e. what the planner uses in Redux)\n * @param {*} pac\n * @returns {Object}\n */\nfunction PACtoTAC(pac) {\n const rawItems = parseItems(pac.model.items);\n const defaultPAC = getDefaultPac();\n const wall = pac.wall || defaultPAC.wall;\n let tac = {\n items: rawItems,\n settings: { ...defaultPAC.settings, ...getPacSettings(pac) },\n ...(defaultPAC.wall && { wall }),\n };\n\n if (pac.wall && !pac.wall.size) {\n /* Backward compatibility for PACs without the size object in their schema.\n Old VPC codes, in particular, can lack the size object. */\n tac.wall.size = {\n width: pac.wall.points?.[2]?.x || constants.WALL.width.max,\n height: pac.wall.points?.[2]?.y || constants.WALL.height.max,\n };\n }\n\n if (defaultPAC.wall && !pac.wall) {\n //this is wall-less PAC in a wall-full range, probably an SPR\n adjustWallAgnosticTac(tac);\n tac = updateDependentItems(tac, { isPersistent: true });\n adjustWallAgnosticTac(tac);\n } else {\n tac = updateDependentItems(tac, { isPersistent: true });\n }\n\n tac = getRangeAdjustedTac(pac, tac);\n\n if (!tac) {\n return null;\n }\n\n return tac;\n}\n\nfunction TACtoPAC(tac) {\n const defaultPAC = getDefaultPac();\n return {\n model: {\n items: compressItems(tac.items),\n },\n version: defaultPAC.version,\n settings: Object.assign({}, defaultPAC.settings, tac.settings),\n wall: tac.wall,\n };\n}\n\nexport default {\n PACtoTAC,\n TACtoPAC,\n resetX,\n getToastFlaggedTac,\n};\n","import { State } from '../StateTypes';\n\n/**\n * Select sprs\n * @param sprs\n */\nexport const selectSprs = ({ sprs }: State) => (sprs ? sprs : []);\n\n/**\n * Select sprs to display\n * @param state\n */\nexport const selectSprsToDisplay = (state: State) =>\n selectSprs(state)\n .filter(({ showInGallery }) => !!showInGallery)\n .sort((a, b) => a.sort - b.sort);\n\n/**\n * Select sprs to display - abTest purposes only\n * @param state\n */\nexport const selectSprsSortedByItemQuantity = (state: State) => {\n const sprs = selectSprs(state).filter(({ showInGallery }) => !!showInGallery);\n const sortByFalling = sprs.slice(0);\n\n sortByFalling.sort(function (a, b) {\n return (\n (b.kompisSPR.content?.child || []).reduce(\n (sum, item) => sum + item.quantity,\n 0\n ) -\n (a.kompisSPR.content?.child || []).reduce(\n (sum, item) => sum + item.quantity,\n 0\n )\n );\n });\n return sortByFalling;\n};\n","import {\n actionHideMeasurements,\n actionSetSceneRect,\n actionSetWallResizerActive,\n actionSetWallResizerInactive,\n actionShowMeasurements,\n} from './sceneActions';\nimport {\n selectIsMeasurementsActive,\n selectIsWallResizerActive,\n} from './sceneSelectors';\nimport {\n selectIsKiosk,\n selectIsMobile,\n selectIsPortrait,\n selectUserAgent,\n} from '../userAgent/userAgentSelectors';\n\nimport constants from '../../settings/constants';\nimport { selectCurrentView } from '../navigation/navigationSelectors';\nimport { isFixedRoom } from '../../util/room';\nimport { selectWallPoints, selectWallSize } from '../tac/tacSelectors';\nimport { actionSetMargins, actionSetRoom } from './sceneActions';\nimport { selectSceneRect } from './sceneSelectors';\nimport _ from 'lodash';\n\nexport const thunkToggleMeasurements = meta => (dispatch, getState) => {\n selectIsMeasurementsActive(getState())\n ? dispatch(actionHideMeasurements(meta))\n : dispatch(actionShowMeasurements(meta));\n};\n\nexport const thunkSetWallResizerActive = meta => (dispatch, getState) => {\n selectIsMeasurementsActive(getState()) &&\n dispatch(actionHideMeasurements({ nonInteraction: true }));\n dispatch(actionSetWallResizerActive(meta));\n};\n\nexport const thunkToggleWallResizer = meta => (dispatch, getState) => {\n const wallResizerActive = selectIsWallResizerActive(getState());\n const measurementsActive = selectIsMeasurementsActive(getState());\n if (wallResizerActive) {\n return dispatch(actionSetWallResizerInactive(meta));\n }\n measurementsActive && dispatch(actionHideMeasurements(meta));\n return dispatch(actionSetWallResizerActive(meta));\n};\n\nexport const thunkSetMargins = () => (dispatch, getState) => {\n const getMobilePortraitPercentual = () => {\n return constants.CANVAS_MARGINS_PORTRAIT_PERCENTUAL['mobile'];\n };\n const getTabletPortraitPercentual = () => {\n return constants.CANVAS_MARGINS_PORTRAIT_PERCENTUAL['tablet'];\n };\n const getPortraitPercentual = () => {\n return selectIsMobile(getState())\n ? getMobilePortraitPercentual()\n : getTabletPortraitPercentual();\n };\n const getWallHeight = () => {\n return selectIsWallResizerActive(getState())\n ? constants.WALL.height.max\n : selectWallSize(getState()).height;\n };\n const getWallWidth = () => {\n return selectIsWallResizerActive(getState())\n ? constants.WALL.width.max\n : selectWallSize(getState()).width;\n };\n\n const wallPointsExist = () => {\n return selectWallPoints(getState()) && selectWallPoints(getState()).length;\n };\n\n const getMargins = () => {\n if (selectCurrentView(getState()) === constants.VIEW_NAMES.SUMMARY) {\n return {\n top: selectSceneRect(getState()).height * 0.2,\n right: 0,\n };\n }\n if (selectIsPortrait(getState()) && isFixedRoom() && wallPointsExist()) {\n return {\n top: getPortraitPercentual() * getWallHeight(),\n right: getPortraitPercentual() * getWallWidth(),\n };\n } else {\n return selectIsKiosk(getState())\n ? constants.CANVAS_MARGINS_KIOSK\n : selectIsMobile(getState())\n ? constants.CANVAS_MARGINS_MOBILE\n : constants.CANVAS_MARGINS;\n }\n };\n dispatch(actionSetMargins(getMargins()));\n};\n\nexport const thunkSetMinRoom = () => (dispatch, getState) => {\n const getMinRoom = () => {\n const currentView = selectCurrentView(getState());\n const isMobile = selectUserAgent(getState()).isMobile;\n if (currentView === constants.VIEW_NAMES.SUMMARY)\n return constants.ROOM_MIN_SUMMARY;\n return isMobile ? constants.ROOM_MIN_MOBILE : constants.ROOM_MIN_DESKTOP;\n };\n dispatch(actionSetRoom({ minRoom: getMinRoom() }));\n};\n\nexport const thunkSetSceneRect = sceneRect => (dispatch, getState) => {\n const currentSceneRect = selectSceneRect(getState());\n if (!_.isEqual(currentSceneRect, sceneRect)) {\n dispatch(actionSetSceneRect(sceneRect));\n }\n};\n","import _ from 'lodash';\nimport { ITEMS } from '../../../../../constants';\nimport geometry from '../../../../../scene/util/geometry';\nimport productService from '../../../../../services/products';\nimport tacHelpers, { digAllItems } from '../../../tacHelpers';\nimport validationErrors from '../validationErrors';\n\n/**\n * Gets no of points a specific insert should add to the section total\n *\n * @param {Object} item The insert to look up\n * @returns {Number} The no of points the insert should add\n */\nexport const getInsertPoints = item => {\n switch (item.filter.type) {\n case ITEMS.DRAWER:\n return 1;\n case ITEMS.SHELF:\n return 1;\n case ITEMS.SHELF_CLOTHES_RAIL:\n return 1;\n default:\n return 0;\n }\n};\n\n/**\n * Gets the dimensions and coordinates of the three \"zones of interest\"\n * (top, bottom, middle) needed for the stability validation\n *\n * @param {Object} section The section to get zones for\n * @returns {Array} A list of the two zones in the requested section\n */\nconst getStabilityZones = section => {\n const { width, height, depth } = section;\n const localSection = { x: 0, y: 0, z: 0, width, height, depth };\n\n const bottom = {\n ...localSection,\n y: 0,\n height: 0.3 * section.height,\n };\n\n const top = {\n ...localSection,\n y: section.height - 0.3 * section.height,\n height: 0.3 * section.height,\n };\n\n return [bottom, top];\n};\n\nexport const getAllSubItemInserts = item => {\n if (!item.items.length) return item;\n // Reusing recursive search algorithm\n return digAllItems(item.items).filter(productService.isInsert);\n};\n\n/**\n * Repositions an item so that its coordinates are relative to the section that it is\n * a descendand of, instead of just relative to the parent of the item. This is useful\n * for collision detection when the item is not a direct child of the section, but a\n * grandchild or further descendant.\n *\n * @param {Object} item The item of which we want to get the position relative to the section.\n * @param {Object} section The section of which the item is a descendant.\n * @returns {Object} An object corresponding to the item but with coordinates relative to the section.\n */\nconst getRepositionedItem = (item, section) => {\n const parent = tacHelpers.getParent(section, item);\n const hasReachedSection = parent.itemid === section.itemid;\n\n const itemRepositioned = _.clone(item);\n if (!hasReachedSection) {\n const parentRepositioned = getRepositionedItem(parent, section);\n itemRepositioned.x += parentRepositioned.x;\n itemRepositioned.y += parentRepositioned.y;\n itemRepositioned.z += parentRepositioned.z;\n }\n\n return itemRepositioned;\n};\n\n/**\n * Gets the (eventual) stability errors for a section\n *\n * @param {Object} section The section to validate\n * @param {Object} inserts A list fo all the inserts in the section\n * @returns {Array} A list of all stability errors found on the section\n */\nexport const getStabilityErrors = (section, inserts) => {\n const stabilityErrors = [];\n const isSectionSideUnits = productService.isType(\n section,\n ITEMS.SECTION_SIDE_UNITS\n );\n\n if (isSectionSideUnits) {\n const insertsWithPoints = inserts.map(insert => ({\n ...insert,\n stabilityPoints: getInsertPoints(insert, 'stability', section),\n }));\n\n const insertsWithPointsRepositioned = insertsWithPoints.map(\n insertWithPoints => getRepositionedItem(insertWithPoints, section)\n );\n\n const isValidZone = area =>\n insertsWithPointsRepositioned.some(\n insert => insert.stabilityPoints && geometry.collides(insert, area)\n );\n\n const enoughInsertPointsInRelevantAreas =\n getStabilityZones(section).every(isValidZone);\n\n if (!enoughInsertPointsInRelevantAreas) {\n const unstableZones = getStabilityZones(section)\n .map(zone => {\n return {\n min: zone.y,\n max: zone.height + zone.y,\n valid: isValidZone(zone),\n };\n })\n .filter(zone => !zone.valid);\n stabilityErrors.push({\n ...validationErrors.STABILITY_ZONES_UNCOVERED_ELVARLI,\n itemid: section.itemid,\n zones: unstableZones,\n });\n }\n }\n\n return stabilityErrors;\n};\n\n/**\n * Takes two sections, one to the left and one to the right, and decides\n * whether they are merged or not.\n * If either leftSection or rightSection is null/undefined, the sections\n * are considered not to be merged.\n *\n * @param {Object} leftSection The section to the left (or null/undefined).\n * @param {Object} rightSection The section to the right (or null/undefined).\n * @returns {Boolean} Whether the two sections are merged or not.\n */\nconst areSectionsMerged = (leftSection, rightSection) => {\n if (!leftSection || !rightSection) return false;\n\n const sectionsOverlap = rightSection.x < leftSection.x + leftSection.width;\n return sectionsOverlap;\n};\n\n/**\n * Takes two sections, one to the left and one to the right, and returns the\n * \"middle\" item of their shared side unit. The left / right sections are also\n * allowed to be null/undefined.\n * Important rules:\n * 1) If both left and right sections are defined, but they are not merged, they\n * don't have a shared side unit, so null is returned.\n * 2) If the left section is defined, but the right section is null/undefined, the\n * shared side unit is considered to be the right side unit of the left section.\n * 3) If the right section is defined, but the left section is null/undefined, the\n * shared side unit is considered to be left side unit of the right section.\n *\n * @param {Object} leftSection The section to the left (or null/undefined).\n * @param {Object} rightSection The section to the right (or null/undefined).\n * @returns {Object} The \"middle\" item of the shared side unit (or null).\n */\nconst getSharedSideUnitMiddleItemOfSections = (leftSection, rightSection) => {\n if (\n leftSection &&\n rightSection &&\n !areSectionsMerged(leftSection, rightSection)\n )\n return null;\n\n const sideUnitMiddleItemFilter = item =>\n productService.isType(item, ITEMS.SIDE_PANEL) &&\n item.id.includes('_middle');\n const getLeftmostOf = items =>\n items ? items.sort((item1, item2) => item1.x - item2.x)[0] : null;\n const getRightmostOf = items =>\n items ? items.sort((item1, item2) => item2.x - item1.x)[0] : null;\n const getLeftSideUnitMiddleItemOfSection = section =>\n getLeftmostOf(section?.items?.filter(sideUnitMiddleItemFilter));\n const getRightSideUnitMiddleItemOfSection = section =>\n getRightmostOf(section?.items?.filter(sideUnitMiddleItemFilter));\n\n let sharedSideUnitMiddleItem = null;\n if (rightSection) {\n // Doesn't matter in this case whether left section is null/undefined or not.\n sharedSideUnitMiddleItem = getLeftSideUnitMiddleItemOfSection(rightSection);\n } else if (leftSection) {\n // We have a left section but the right section is null/undefined.\n sharedSideUnitMiddleItem = getRightSideUnitMiddleItemOfSection(leftSection);\n } // If we have neither left nor right section, we'll keep the shared side unit middle item as null.\n\n return sharedSideUnitMiddleItem;\n};\n\n/**\n * Checks whether there are enough brackets with holes to accommodate the \"shelf + clothes rail\" items\n * in the configuration. The validation intentionally assumes that the user won't \"borrow\" brackets with\n * holes from other side units and only assembles each side unit using the parts delivered in that package.\n * This check is only relevant when building with side unit sections, and will never trigger a validation\n * error for a post sections configuration.\n *\n * @param {Object} tac The TAC to analyze.\n * @returns {Array} The validation errors.\n */\nconst getNotEnoughBracketsWithClothesRailHolesErrors = tac => {\n const NUMBER_OF_BRACKETS_WITH_HOLES_PROVIDED_PER_SIDE_UNIT = 2;\n\n const sectionsSideUnits = tac.items\n .filter(item => productService.isType(item, ITEMS.SECTION_SIDE_UNITS))\n .sort((s1, s2) => s1.x - s2.x);\n\n // We want \"null sections\" where there are \"holes\" in the configuration, i.e., where the sections\n // aren't merged, and also to the left of the first section, and to the right of the last section.\n const sectionsSideUnitsWithNullSections = [];\n for (let i = 0; i < sectionsSideUnits.length; i++) {\n const thisSection = sectionsSideUnits[i];\n const sectionToTheLeft = i > 0 ? sectionsSideUnits[i - 1] : null;\n if (!areSectionsMerged(sectionToTheLeft, thisSection))\n sectionsSideUnitsWithNullSections.push(null);\n sectionsSideUnitsWithNullSections.push(thisSection);\n }\n sectionsSideUnitsWithNullSections.push(null); // \"null section\" to the right of the last real section.\n\n const pairsOfSections = [];\n for (let i = 0; i < sectionsSideUnitsWithNullSections.length - 1; i++) {\n const leftSection = sectionsSideUnitsWithNullSections[i];\n const rightSection = sectionsSideUnitsWithNullSections[i + 1];\n pairsOfSections.push({\n leftSection,\n rightSection,\n });\n }\n\n const shelvesClothesRailPerSection = new Map();\n sectionsSideUnits.forEach(section => {\n shelvesClothesRailPerSection.set(\n section,\n section.items\n .filter(item => productService.isType(item, ITEMS.SHELF_CLOTHES_RAIL))\n .sort((scr1, scr2) => scr2.y - scr1.y)\n );\n });\n shelvesClothesRailPerSection.set(null, []); // \"null sections\" don't have any shelves clothes rail.\n\n const sideUnitsWithoutEnoughBracketsWithHoles = [];\n pairsOfSections.forEach(({ leftSection, rightSection }) => {\n const scrsInLeft = shelvesClothesRailPerSection.get(leftSection);\n const scrsInRight = shelvesClothesRailPerSection.get(rightSection);\n const numberOfBracketsWithHolesNeededOnSharedSideUnit =\n scrsInLeft.length + // One for each sh. cl. rail in left section...\n scrsInRight.filter(scrInRight => {\n return !scrsInLeft.some(scrInLeft => scrInLeft.y === scrInRight.y);\n })\n .length; /* ...but only count those sh. cl. rail in the right section for which there\n isn't a sh. cl. rail in the left section at the same height. */\n\n if (\n numberOfBracketsWithHolesNeededOnSharedSideUnit >\n NUMBER_OF_BRACKETS_WITH_HOLES_PROVIDED_PER_SIDE_UNIT\n )\n sideUnitsWithoutEnoughBracketsWithHoles.push(\n getSharedSideUnitMiddleItemOfSections(leftSection, rightSection)\n );\n });\n\n const notEnoughBracketsWithHolesErrors = [];\n sideUnitsWithoutEnoughBracketsWithHoles.forEach(sideUnit => {\n notEnoughBracketsWithHolesErrors.push({\n ...validationErrors.NOT_ENOUGH_BRACKETS_WITH_HOLES_FOR_CLOTHES_RAIL_ELVARLI,\n itemid: sideUnit,\n });\n });\n\n return notEnoughBracketsWithHolesErrors;\n};\n\n/**\n * Validates an ELVARLI TAC according to the stability rules\n *\n * @param {Object} tac The complete TAC to validate\n * @returns {Array} A list of all stability errors found in the TAC\n */\nconst validate = tac => {\n const allErrors = tac.items.reduce((errors, item) => {\n if (productService.isSection(item)) {\n const inserts = item.items.filter(productService.isInsert);\n const insertsIncludingSubInserts = inserts\n .reduce((acc, insert) => [...acc, getAllSubItemInserts(insert)], [])\n .flat();\n errors.push(...getStabilityErrors(item, insertsIncludingSubInserts));\n }\n return errors;\n }, []);\n allErrors.push(...getNotEnoughBracketsWithClothesRailHolesErrors(tac));\n\n return allErrors;\n};\n\nexport default {\n validate,\n};\n","import { setupAmelioration } from '../..';\nimport {\n Amelioration,\n AmeliorationArgs,\n AmeliorationDialogContent,\n TransformationResult,\n} from '../../ameliorationTypes';\nimport { TacModel } from '../../../../state/tac/tacTypes';\nimport { translate } from '../../../L10n';\nimport { t } from '../../../../translations';\nimport _ from 'lodash';\nimport { isInsert, isSection } from '../../../products';\nimport { getCheapestFittingShelf } from '../../../products/elvarli';\nimport {\n getAllSubItemInserts,\n getStabilityErrors,\n} from '../../../../state/tac/tacReducer/validate/elvarli';\nimport productService from '../../../../services/products';\nimport tacHelpers from '../../../../state/tac/tacHelpers';\nimport addItem from '../../../../state/tac/tacReducer/addItem';\nimport { TacItem } from '../../../../generalTypes';\nimport removeItem from '../../../../state/tac/tacReducer/removeItem';\n\nlet elvarliStabilityAmelioration: Amelioration | null = null;\n\ntype Zone = {\n min: number;\n max: number;\n valid: boolean;\n openSlots: any[];\n};\n\nconst getOpenSlotsBasedOnZone = (zone: Zone, openSlots: any) => {\n const withinZone = (slot: { y: number }, zone: Zone) =>\n slot.y >= zone.min && slot.y <= zone.max;\n return openSlots.filter((slot: { y: number }) => withinZone(slot, zone));\n};\n\nconst getOpenSlotsBasedOnSection = (\n tac: TacModel,\n section: TacItem,\n insert: TacItem\n) => {\n const tacWithOnlyRelevantSection = {\n ...tac,\n items: tac.items.filter(item => item.itemid === section.itemid),\n };\n return tacHelpers.getOpenSlots(tacWithOnlyRelevantSection, insert);\n};\n\nconst getPreferredOpenSlot = (section: TacItem, openSlots: any) => {\n if (!openSlots.length) return null;\n\n const shortestDistanceToTopOrBottom = (slot: any) => {\n const distanceToTop = section.height - (slot.y + slot.height);\n const distanceToBottom = slot.y;\n\n return Math.min(distanceToTop, distanceToBottom);\n };\n const slotsSortedByPreference = openSlots.sort(\n (slot1: any, slot2: any) =>\n shortestDistanceToTopOrBottom(slot1) -\n shortestDistanceToTopOrBottom(slot2)\n );\n\n return slotsSortedByPreference[0];\n};\n\nconst addStabilityInsertToOneOfOpenSlots = (\n tacModel: TacModel,\n section: TacItem,\n insert: TacItem,\n openSlots: any\n) => {\n if (!openSlots.length) return false;\n const chosenSlot = getPreferredOpenSlot(section, openSlots);\n const shelfWithModifiedPos = { ...chosenSlot.local, ...insert };\n return addItem(tacModel, shelfWithModifiedPos, section, {\n isPersistent: true,\n });\n};\n\nconst findUnstableZonesFrom = (stabilityErrors: any) => {\n return stabilityErrors.flatMap((error: any) =>\n error.zones.flatMap((zone: any) => zone)\n );\n};\n\nconst findSectionInsertsWithinZone = (section: TacItem, zone: Zone) => {\n const inserts = section.items.filter(isInsert);\n const withinZone = (insert: TacItem) =>\n insert.y >= zone.min && insert.y <= zone.max;\n return inserts.filter(withinZone);\n};\n\nconst fixElvarliSectionStability = (tacIn: TacModel): TransformationResult => {\n let missingReplacementShelvesEntirely = false;\n let tacOut = _.cloneDeep(tacIn);\n\n const sections = tacOut.items.filter(item => isSection(item));\n\n sections.forEach(section => {\n const inserts = section.items.filter(productService.isInsert);\n const insertsIncludingSubInserts = inserts\n .reduce(\n (acc: any, insert: any) => [...acc, getAllSubItemInserts(insert)],\n []\n )\n .flat();\n const stabilityErrors = getStabilityErrors(\n section,\n insertsIncludingSubInserts\n );\n const insert = getCheapestFittingShelf(section) as TacItem;\n if (!insert) {\n missingReplacementShelvesEntirely = true;\n return;\n }\n if (stabilityErrors.length) {\n const unstableZones = findUnstableZonesFrom(stabilityErrors);\n\n const openSlotsBasedOnSection = getOpenSlotsBasedOnSection(\n tacOut,\n section,\n insert\n );\n const openSlotsBasedOnUnstableZone = unstableZones.flatMap(\n (zone: Zone) => {\n return {\n ...zone,\n openSlots: getOpenSlotsBasedOnZone(zone, openSlotsBasedOnSection),\n };\n }\n );\n // Iterates through each unstable zone and adds a stability insert to one of the open slots\n // so that the section becomes stable.\n\n tacOut = openSlotsBasedOnUnstableZone.reduce(\n (acc: TacModel, zone: Zone) => {\n const modifiedSection = acc.items.find(\n (item: TacItem) => item.itemid === section.itemid\n ) as TacItem;\n let modifiedTac = addStabilityInsertToOneOfOpenSlots(\n acc,\n modifiedSection,\n insert,\n zone.openSlots\n );\n if (!modifiedTac) {\n // The zone is still unstable and no insert was possible to add to the zone.\n // This means that the zone is occupied with inserts that do not give stability points.\n // Find the inserts covering that specific zone.\n // eslint-disable-next-line no-unused-vars\n const inserts = findSectionInsertsWithinZone(\n section,\n zone\n ).reverse();\n const firstInsert = inserts[0];\n const replacingInsert = getCheapestFittingShelf(section) as TacItem;\n // Remove that insert from the tac.\n tacOut = removeItem(tacOut, firstInsert);\n const modifiedSection = tacOut.items.find(\n item => item.itemid === section.itemid\n ) as TacItem;\n // Get all open slots in the section\n const openSlots = getOpenSlotsBasedOnSection(\n tacOut,\n modifiedSection,\n replacingInsert\n );\n // Now there should be an open slot for the replacing insert\n const relatedOpenSlots = getOpenSlotsBasedOnZone(zone, openSlots);\n // Add the replacing insert to the now open slot\n modifiedTac = addStabilityInsertToOneOfOpenSlots(\n acc,\n modifiedSection,\n replacingInsert,\n relatedOpenSlots\n );\n }\n return modifiedTac ? modifiedTac : acc;\n },\n tacOut\n );\n }\n });\n\n if (missingReplacementShelvesEntirely) {\n return {\n success: false,\n };\n }\n\n const successMessage = translate(t.TOAST_ADD_SHELVES);\n\n const successResult: TransformationResult = {\n success: true,\n tacOut: tacOut,\n successMessage,\n };\n\n return successResult;\n};\n\nexport const getElvarliStabilityAmelioration = (): Amelioration => {\n if (elvarliStabilityAmelioration) return elvarliStabilityAmelioration;\n\n const dialogContent: AmeliorationDialogContent = {\n headerText: translate(t.POPUP_ADD_SHELVES_HEADER),\n bodyText: translate(t.POPUP_ADD_SHELVES_BODY),\n acceptText: translate(t.YES_PLEASE),\n declineText: translate(t.PLANNER_SURVEY_DECLINE),\n };\n\n const args: AmeliorationArgs = {\n transform: fixElvarliSectionStability,\n dialogContent,\n };\n\n elvarliStabilityAmelioration = setupAmelioration(args);\n\n return elvarliStabilityAmelioration;\n};\n","import { getElvarliStabilityAmelioration } from '../../../../services/amelioration/range/elvarli';\n\nexport default {\n DUPLICATE_SIDEPANELS: {\n options: {},\n statisticsLabel: 'duplicate_sidepanels',\n translationKey: 'POPUP_ALERT_DUPLICATE_SIDEPANEL',\n },\n HIGH_CABINET_UNSTABLE: {\n options: {\n priority: 4,\n },\n statisticsLabel: 'high_cabinet_unstable',\n translationKey: 'POPUP_ALERT_INVALID_CONFIG_HIGH_CABINET',\n },\n INSERTS_TOO_FEW: {\n options: {\n priority: 2,\n },\n statisticsLabel: 'inserts_too_few',\n translationKey: 'POPUP_ALERT_INVALID_CONFIG_ADD_PART',\n },\n STABILITY_ZONES_UNCOVERED_BROR: {\n options: {},\n statisticsLabel: 'stability_zones_uncovered',\n translationKey: 'POPUP_ALERT_INVALID_CONFIG_MISPLACED',\n },\n STABILITY_ZONES_UNCOVERED_IVAR: {\n options: {\n priority: 3,\n },\n statisticsLabel: 'stability_zones_uncovered',\n translationKey: 'POPUP_ALERT_INVALID_CONFIG_PLACEMENT',\n },\n STABILITY_ZONES_UNCOVERED_IVAR_LOW_SECTION: {\n options: {\n priority: 3,\n },\n statisticsLabel: 'stability_zones_uncovered_low_section',\n translationKey: 'POPUP_ALERT_INVALID_CONFIG_LOW_SECTION',\n },\n NOT_ENOUGH_BRACKETS_WITH_HOLES_FOR_CLOTHES_RAIL_ELVARLI: {\n options: {\n priority: 1,\n },\n statisticsLabel: 'not_enough_brackets_with_holes_for_clothes_rail',\n translationKey: 'CLOTHES_RAIL_ADJACENT',\n },\n SUSPENSION_RAIL_TOO_WIDE: {\n options: {\n showInstantly: true,\n },\n statisticsLabel: 'suspension_rail_too_wide',\n translationKey: 'POPUP_ALERT_SUSPENSION_RAIL',\n },\n WEIGHT_LIMIT_EXCEEDED: {\n options: {\n priority: 1,\n },\n statisticsLabel: 'weight_limit_exceeded',\n translationKey: 'POPUP_ALERT_INVALID_CONFIG_WEIGHT',\n },\n STABILITY_ZONES_UNCOVERED_ELVARLI: {\n options: {\n priority: 2,\n },\n statisticsLabel: 'stability_zones_uncovered_elvarli',\n translationKey: 'POPUP_ALERT_INVALID_CONFIG_PLACEMENT',\n getAmelioration: getElvarliStabilityAmelioration,\n },\n};\n","import productService from '../../../../../services/products';\nimport constants from '../../../../../settings/constants';\nimport tacHelpers from '../../../tacHelpers';\nimport validationErrors from '../validationErrors';\nimport * as Sentry from '@sentry/browser';\n\nfunction tooCloseToSibling(section, allSections) {\n return allSections.some(\n otherSection =>\n otherSection.itemid !== section.itemid &&\n otherSection.x - (section.x + section.width) <\n constants.SECTION_SNAPPING_DISTANCE &&\n otherSection.x - (section.x + section.width) >= 0\n );\n}\n\nfunction validate(tac) {\n const allSections = tac.items.filter(productService.isSection);\n\n return tac.items.reduce((errors, item) => {\n if (productService.isSection(item)) {\n if (tooCloseToSibling(item, allSections)) {\n const sidePanelId = item.items.find(\n child => productService.isType(child, 'side-panel') && child.x > 0\n ).itemid;\n\n errors.push({\n ...validationErrors.DUPLICATE_SIDEPANELS,\n itemid: sidePanelId,\n });\n }\n } else if (productService.isType(item, 'mounting-rail')) {\n if (!tacHelpers.isWithinWall(item, tac)) {\n Sentry.captureMessage('AURDAL_SUSPENSION_RAIL_TOO_WIDE');\n errors.push({\n ...validationErrors.SUSPENSION_RAIL_TOO_WIDE,\n itemid: item.itemid,\n });\n }\n }\n return errors;\n }, []);\n}\n\nexport default { validate };\n","export default { validate: tac => [] };\n","import constants from '../../../../../settings/constants';\nimport products from '../../../../../services/products';\nimport productService from '../../../../../services/products';\nimport geometry from '../../../../../scene/util/geometry';\nimport validationErrors from '../validationErrors';\nimport { ITEMS } from '../../../../../constants';\n\nconst bottom = [0, 1, 2];\n\n/**\n * Items that should not count towards stability in a section.\n */\nconst nonStabilityItems = [ITEMS.DRAWER];\n\n/**\n * Checks if an item is not considered to contribute towards stability in a section.\n * @param {object} item\n * @returns {boolean}\n */\nconst isPartOfNonStabilityItems = item => {\n return nonStabilityItems.some(nonStabilityItem =>\n productService.isType(item, nonStabilityItem)\n );\n};\n\n/**\n * Removes the items that do not contribute towards stability in a section.\n * @param {array} items\n * @returns {Array}\n */\nconst filterOutNonStabilityItems = items => {\n return items.filter(item => !isPartOfNonStabilityItems(item));\n};\n\n/**\n * The amount of items that contribute to stability, that need to be inserted into a section\n * for it to be stable.\n */\nconst stabilityItemsRequired = {\n sections: {\n 1900: 4,\n 1100: 3,\n },\n};\n\n/**\n * Gets the section height.\n * @param {object} section\n * @returns {number}\n */\nconst getSectionHeight = section => {\n return section.filter.height;\n};\n\n/**\n * Retrieves the amount of required items that contribute to stability, that is\n * required to be inserted into the section.\n * @param {object} section\n * @returns {boolean}\n */\nconst getRequiredAmountOfStabilityItems = section => {\n const height = getSectionHeight(section);\n return stabilityItemsRequired.sections[`${height}`];\n};\n\nfunction validate(tac) {\n // All sections must have a shelf at the top, middle and bottom to ensure stability\n return tac.items.reduce((errors, item) => {\n if (products.isSection(item)) {\n const stabilityItems = filterOutNonStabilityItems(item.items);\n\n const amountOfStabilityItemsInSection = (stabilityItems || []).reduce(\n (indices, childItem) => {\n if (!geometry.extendsOutside(childItem, item, 'x')) {\n indices.push(\n Math.round(childItem.y / constants.DISTANCE_BETWEEN_ATTACHMENTS)\n );\n }\n\n if (products.isCabinet(childItem)) {\n indices.push(\n Math.round(\n (childItem.y + childItem.height) /\n constants.DISTANCE_BETWEEN_ATTACHMENTS\n )\n );\n }\n\n return indices;\n },\n []\n );\n const topIndex = constants.NBR_OF_ATTACHMENTS[item.height] - 1;\n const top = bottom.map(index => topIndex - index);\n\n if (\n amountOfStabilityItemsInSection.length <\n getRequiredAmountOfStabilityItems(item)\n ) {\n errors.push({\n ...validationErrors.INSERTS_TOO_FEW,\n itemid: item.itemid,\n });\n } else if (\n bottom.every(\n index => !amountOfStabilityItemsInSection.includes(index)\n ) ||\n top.every(index => !amountOfStabilityItemsInSection.includes(index))\n ) {\n errors.push({\n ...validationErrors.STABILITY_ZONES_UNCOVERED_BROR,\n itemid: item.itemid,\n });\n }\n }\n\n return errors;\n }, []);\n}\n\nexport default { validate };\n","export default { validate: tac => [] };\n","import geometry from '../../../../../scene/util/geometry';\nimport productService from '../../../../../services/products';\nimport validationErrors from '../validationErrors';\nimport { ITEMS } from '../../../../../constants';\n\nconst SECTION_HEIGHT_73 = 730;\nconst SECTION_HEIGHT_124 = 1240;\nconst SECTION_HEIGHT_179 = 1785;\nconst SECTION_HEIGHT_226 = 2265;\nconst MAX_NO_OF_BOTTLE_RACKS_OR_FELT_SHELVES_PER_SECTION = 6;\n\n/**\n * Gets the weight or stability limits for a specific section\n *\n * @param {Object} section The section to look up\n * @param {String} limitType The type of limits requested ('weight' or 'stability')\n * @returns {Number} The max/min points limit for the section\n */\nconst getSectionLimit = (section, limitType) => {\n if (section.height === SECTION_HEIGHT_73)\n return limitType === 'weight' ? 4 : 2;\n if (section.height === SECTION_HEIGHT_124)\n return limitType === 'weight' ? 7 : 3;\n if (section.height === SECTION_HEIGHT_179)\n return limitType === 'weight' ? 8 : 4;\n if (section.height === SECTION_HEIGHT_226)\n return limitType === 'weight' ? 9 : 5;\n return 0;\n};\n\n/**\n * Gets no of points a specific insert should add to the section total\n *\n * @param {Object} item The insert to look up\n * @param {String} type The type of points requested ('weight' or 'stability')\n * @param {Object} [parent] The section containing the insert\n * @returns {Number} The no of points the insert should add\n */\nconst getInsertPoints = (item, type, parent) => {\n switch (item.filter.type) {\n case ITEMS.DRAWER:\n case ITEMS.SHELF:\n return 1;\n case ITEMS.BOTTLE_RACK:\n case ITEMS.FELT_SHELF:\n return type === 'weight' ? 1 : 0.5;\n case ITEMS.CHEST:\n case ITEMS.TABLE:\n return type === 'weight' ? 3 : 2;\n case ITEMS.CABINET:\n return type === 'weight'\n ? item.height > 1000\n ? 6\n : 3\n : item.height > 1000\n ? parent.height > 2000\n ? 3\n : 4\n : 2;\n default:\n return 0;\n }\n};\n\n/**\n * Checks whether a section has exceeded the max limit for number of bottle racks / felt shelves\n *\n * @param {Object} inserts All inserts in the section\n * @returns {Boolean} True if there are too many bottle racks or felt shelves in total\n */\nconst tooManyBottleRacksOrFeltShelves = inserts => {\n const noOfBottleRacksAndFeltShelves = inserts.filter(insert =>\n productService.isType(insert, [ITEMS.BOTTLE_RACK, ITEMS.FELT_SHELF])\n ).length;\n\n return (\n noOfBottleRacksAndFeltShelves >\n MAX_NO_OF_BOTTLE_RACKS_OR_FELT_SHELVES_PER_SECTION\n );\n};\n\n/**\n * Gets the (eventual) weight errors for a section\n *\n * @param {Object} section The section to validate\n * @returns {Array} A list of all weight errors found on the section\n */\nconst getWeightErrors = section => {\n const weightErrors = [];\n\n const inserts = section.items.filter(productService.isInsert);\n\n const totalWeightPoints = inserts.reduce((total, item) => {\n return total + (getInsertPoints(item, 'weight', section) || 0);\n }, 0);\n\n const maxWeightPointsInSection = getSectionLimit(section, 'weight');\n\n if (\n totalWeightPoints > maxWeightPointsInSection ||\n tooManyBottleRacksOrFeltShelves(inserts)\n ) {\n weightErrors.push({\n ...validationErrors.WEIGHT_LIMIT_EXCEEDED,\n itemid: section.itemid,\n });\n }\n\n return weightErrors;\n};\n\n/**\n * Gets the dimensions and coordinates of the three \"zones of interest\"\n * (top, bottom, middle) needed for the stability validation\n *\n * @param {Object} section The section to get zones for\n * @returns {Array} A list of the three zones in the requested section\n */\nconst getStabilityZones = section => {\n const { width, height, depth } = section;\n const localSection = { x: 0, y: 0, z: 0, width, height, depth };\n\n if (section.height === SECTION_HEIGHT_73) {\n const bottom = {\n ...localSection,\n y: 0,\n height: 0.475 * section.height,\n };\n\n const top = {\n ...localSection,\n y: section.height - 0.475 * section.height,\n height: 0.475 * section.height,\n };\n\n return [bottom, top];\n }\n\n // 4% gap between zones to prevent shelves and bottle racks\n // to reach into more than one\n const bottom = {\n ...localSection,\n y: 0,\n height: 0.31 * section.height,\n };\n\n const middle = {\n ...localSection,\n y: section.height / 2 - 0.15 * section.height,\n height: 0.4 * section.height,\n };\n\n const top = {\n ...localSection,\n y: section.height - 0.21 * section.height,\n height: 0.21 * section.height,\n };\n\n return [bottom, middle, top];\n};\n\n/**\n * Gets the (eventual) stability errors for a section\n *\n * @param {Object} section The section to validate\n * @param {Object} inserts A list fo all the inserts in the section\n * @param {Object} tac The complete TAC\n * @returns {Array} A list of all stability errors found on the section\n */\nconst getStabilityErrors = (section, inserts, tac) => {\n const stabilityErrors = [];\n\n const insertsWithPoints = inserts.map(insert => ({\n ...insert,\n stabilityPoints: getInsertPoints(insert, 'stability', section),\n }));\n\n const totalStabilityPoints = insertsWithPoints.reduce((total, insert) => {\n return total + (insert.stabilityPoints || 0);\n }, 0);\n\n const sectionLimit = getSectionLimit(section, 'stability');\n if (totalStabilityPoints < sectionLimit) {\n stabilityErrors.push({\n ...validationErrors.INSERTS_TOO_FEW,\n itemid: section.itemid,\n });\n }\n\n if (\n !getStabilityZones(section).every(area =>\n insertsWithPoints.some(\n insert => insert.stabilityPoints && geometry.collides(insert, area)\n )\n )\n ) {\n stabilityErrors.push({\n ...(section.height === SECTION_HEIGHT_73\n ? validationErrors.STABILITY_ZONES_UNCOVERED_IVAR_LOW_SECTION\n : validationErrors.STABILITY_ZONES_UNCOVERED_IVAR),\n itemid: section.itemid,\n });\n }\n\n // SPECIAL CASE:\n // The 179cm high section with only a high cabinet inside is stable\n // only if merged with at least one other section\n if (\n section.height === SECTION_HEIGHT_179 &&\n inserts.length === 1 &&\n totalStabilityPoints === 4 &&\n tac.items.filter(\n item => productService.isSection(item) && geometry.collides(section, item)\n ).length < 2\n ) {\n stabilityErrors.push({\n ...validationErrors.HIGH_CABINET_UNSTABLE,\n itemid: section.itemid,\n });\n }\n\n return stabilityErrors;\n};\n\n/**\n * Validates an IVAR TAC according to the weight and stability rules\n *\n * @param {Object} tac The complete TAC to validate\n * @returns {Array} A list of all weight and stability errors found in the TAC\n */\nconst validate = tac => {\n const allErrors = tac.items.reduce((errors, item) => {\n if (productService.isSection(item)) {\n const inserts = item.items.filter(productService.isInsert);\n errors.push(\n ...getWeightErrors(item, inserts),\n ...getStabilityErrors(item, inserts, tac)\n );\n }\n return errors;\n }, []);\n\n return allErrors;\n};\n\nexport default {\n validate,\n};\n","// Should be an enum, but cannot use it due to the ancient, pre-historic version of Typescript.\nexport const MeansOfNotification = {\n Amelioration: 'amelioration',\n Popup: 'popup',\n};\n","import { applicationSettings } from '../../../../settings/application';\n\nimport Aurdal from './aurdal';\nimport Boaxel from './boaxel';\nimport Bror from './bror';\nimport Jonaxel from './jonaxel';\nimport Ivar from './ivar';\nimport Elvarli from './elvarli';\n\nimport { MeansOfNotification } from './meansOfNotification';\n\nexport default function validate(tac) {\n switch (applicationSettings.applicationName) {\n case 'AURDAL':\n return Aurdal.validate(tac);\n case 'BROR':\n return Bror.validate(tac);\n case 'BOAXEL':\n return Boaxel.validate(tac);\n case 'JONAXEL':\n return Jonaxel.validate(tac);\n case 'IVAR':\n return Ivar.validate(tac);\n case 'ELVARLI':\n return Elvarli.validate(tac);\n default:\n throw new Error(\n 'NotImplementedError',\n 'Range-specific tac validation service needed'\n );\n }\n}\n\nexport { MeansOfNotification };\n","import helpers from '../tacHelpers';\nimport _ from 'lodash';\nimport { replace } from '../replace';\n\nfunction hide(item) {\n return { ...item, hidden: true, items: (item.items || []).map(hide) };\n}\n\nexport default function hideItem(model, item) {\n const parent = helpers.getParent(model, item);\n\n if (!parent) {\n return false;\n }\n\n const newParent = {\n ...parent,\n items: parent.items.filter(i => i.itemid !== item.itemid),\n };\n const hiddenItem = hide(item);\n\n newParent.items.push(hiddenItem);\n\n if (parent === model) {\n return newParent;\n } else {\n const out = _.cloneDeep(model);\n replace(out.items, newParent);\n return out;\n }\n}\n","import { CustomAction } from '../../../generalTypes';\nimport { SET_DRAFT_CEILING_HEIGHT } from '../../actionConstants';\n\nexport const actionSetDraftCeilingHeight = (\n value: number,\n meta = {}\n): CustomAction<{ value: number }> => ({\n type: SET_DRAFT_CEILING_HEIGHT,\n payload: {\n value,\n },\n meta,\n});\n","import { State } from '../StateTypes';\nimport { Toasts, HasShownMultipackMessageFor } from './toastsTypes';\n\n/**\n * Select toasts state slice\n *\n * @param toasts\n */\nexport const selectToasts = ({ toasts }: State): Toasts => toasts;\n\n/**\n * Select has shown multipack message for\n *\n * @param state\n * @returns {string[]}\n */\nexport const selectHasShownMultipackMessageFor = (\n state: State\n): HasShownMultipackMessageFor =>\n selectToasts(state).hasShownMultipackMessageFor;\n","import { TOASTS_SET_HAS_SHOWN_MULTIPACK_MESSAGE_FOR } from '../actionConstants';\n\nexport const actionSetHasShownMultipackMessageFor = (itemIds: string[]) => ({\n type: TOASTS_SET_HAS_SHOWN_MULTIPACK_MESSAGE_FOR,\n payload: { hasShownMultipackMessageFor: itemIds },\n});\n\nexport const clearHasShownMultipackMessageFor = () => ({\n type: TOASTS_SET_HAS_SHOWN_MULTIPACK_MESSAGE_FOR,\n payload: { hasShownMultipackMessageFor: [] },\n});\n","import { selectHasShownMultipackMessageFor } from './toastsSelectors';\nimport { actionSetHasShownMultipackMessageFor } from './toastsActions';\nimport { Thunk } from '../../generalTypes';\nimport { isOnlyAvailableInMultipack } from '../../services/products';\nimport { showMultipackToast } from '../../services/toastMaster';\nimport constants from '../../settings/constants';\n\nexport const thunkShowMultipackMessageIfNeededFor: Thunk =\n (itemId: string) => (dispatch, getState) => {\n if (!isOnlyAvailableInMultipack(itemId)) return;\n\n const idsOfItemsAlreadyShownFor = selectHasShownMultipackMessageFor(\n getState()\n );\n if (idsOfItemsAlreadyShownFor.includes(itemId)) return;\n\n dispatch(\n actionSetHasShownMultipackMessageFor([\n ...idsOfItemsAlreadyShownFor,\n itemId,\n ])\n );\n\n /* TODO: It would be nice nice to do some refactoring, so that we don't just call\n functions in toastMaster when we want to show/queue toasts. The solution should\n probably be based on state, similarly to how we handle popups and sheets. */\n showMultipackToast(constants.BULK_ARTICLES[itemId].bulkSize);\n };\n","import getDefaultPac from '../../util/aactools/getDefaultPAC';\nimport { ActionCreators as UndoActionCreators } from 'redux-undo';\nimport {\n actionAddItem,\n actionAddMultiple,\n actionHideItem,\n actionLoadPac,\n actionRemoveItem,\n actionSetUseMountingRails,\n actionSetWall,\n actionUpdateItem,\n actionUpdateMultiple,\n} from './tacActions';\nimport constants from '../../settings/constants';\nimport {\n selectCeilingHeightBasedOnMarket,\n selectTac,\n selectWallPoints,\n selectWallSize,\n} from './tacSelectors';\nimport {\n selectShouldFilterIntroPopupBeVisible,\n selectErrorHandlingPendingTimestamp,\n} from '../popups/popupsSelectors';\nimport {\n actionSetFilterIntroPopupBeVisible,\n actionSetHasShownFilterIntroPopup,\n actionShowSceneErrors,\n} from '../popups';\nimport { actionSetDirtyConfiguration } from '../vpc/vpcActions';\nimport convert from '../../util/aactools/convert';\nimport { tacSetToastFlags } from './tacActions';\nimport { selectSprs } from '../sprs/sprsSelectors.ts';\nimport { openPacMissing } from '../dialog/dialogActions';\nimport {\n actionSetOverlayState,\n actionSetErrorHandlingPendingTimestamp,\n} from '../popups/popupsActions';\nimport getWallPoints from './tacReducer/getWallPoints';\nimport { thunkSetMargins } from '../scene/sceneThunks';\nimport { getWallHeightDependentItems } from '../../services/products';\nimport { thunkSetView } from '../navigation/navigationThunks';\nimport { selectCurrentView } from '../navigation/navigationSelectors';\nimport { actionClearFilter } from '../productMenu/productMenuActions';\nimport history from '../../services/history';\nimport { isFixedRoom } from '../../util/room';\nimport validate, { MeansOfNotification } from './tacReducer/validate';\nimport addItem from './tacReducer/addItem';\nimport updateDependentItems from './tacReducer/updateDependentItems';\nimport { addMultipleItems } from './tacReducer/addMultipleItems';\nimport tacHelpers from './tacHelpers';\nimport updateItem from './tacReducer/updateItem';\nimport { updateMultipleItems } from './tacReducer/updateMultipleItems';\nimport removeItem from './tacReducer/removeItem';\nimport hideItem from './tacReducer/hideItem';\nimport {\n thunkHandlePopupOnAddItem,\n thunkHandlePopupOnUpdateItem,\n thunkHandlePopupOnRemoveItem,\n thunkHandlePopupOnTacLoad,\n thunkHandleMountingRailPopup,\n} from '../popups/popupsThunks';\nimport { actionSetDraftCeilingHeight } from '../draft/ceilingHeight/ceilingHeightActions';\nimport { getCuredHeight } from '../draft/ceilingHeight/ceilingHeightThunks';\nimport { thunkShowMultipackMessageIfNeededFor } from '../toasts/toastsThunks';\nimport { clearHasShownMultipackMessageFor } from '../toasts/toastsActions';\nimport toastMaster from '../../services/toastMaster';\nimport { AmeliorationOutcome } from '../../services/amelioration/ameliorationTypes';\nimport { POINTER_DOWN, KEY_DOWN } from '../../util/supportedEvents';\nimport localStatisticsReporter from '../../services/statistics/insights/custom/local/localStatisticsReporter';\n\nconst thunkSetToastFlags = pac => (dispatch, getState) => {\n /**\n * If the pac has an id it means that its actually an spr.\n * @returns {string | undefined}\n */\n const pacIsSpr = () => {\n return pac.id;\n };\n\n /**\n * Extra items are only possible to have in the app when they are loaded from the cms.\n * This function checks if there is an spr that has an extra item.\n * @returns\n */\n const sprContainsExtraItems = () => {\n if (!pacIsSpr()) return;\n\n const sprs = selectSprs(getState());\n const spr = sprs.find(spr => spr.id === pac.id);\n return spr.model?.extraItems?.length;\n };\n\n const tac = convert.PACtoTAC(pac);\n const toastFlags = convert.getToastFlaggedTac(\n tac,\n pac,\n sprContainsExtraItems()\n );\n dispatch(tacSetToastFlags(toastFlags));\n};\n\n/**\n * Thunk for loading pac\n * @param pac\n * @returns {function(*): void}\n */\nexport const thunkLoadPac = pac => (dispatch, getState) => {\n const tac = convert.PACtoTAC(pac);\n\n const getDraftHeight = () =>\n selectCeilingHeightBasedOnMarket(getState(), tac.wall.size.height);\n\n const draftHeight = tac.wall && getDraftHeight();\n\n tac && history.save(convert.TACtoPAC(tac));\n\n dispatch(actionLoadPac(tac, validate(tac), !isFixedRoom()));\n tac.wall && dispatch(actionSetDraftCeilingHeight(draftHeight));\n dispatch(thunkSetToastFlags(pac));\n dispatch(UndoActionCreators.clearHistory());\n dispatch(thunkHandlePopupOnTacLoad());\n dispatch(clearHasShownMultipackMessageFor());\n const currentView = selectCurrentView(getState());\n const newView =\n currentView === constants.VIEW_NAMES.START\n ? constants.VIEW_NAMES.SCENE\n : currentView;\n dispatch(thunkSetView(newView, { nonInteraction: true }));\n dispatch(actionSetDirtyConfiguration(true));\n dispatch(thunkHandleMountingRailPopup({ tac }));\n};\n\n/**\n * Thunk for resetting scene\n *\n * @returns {function(*): void}\n */\nexport const thunkResetScene = () => dispatch => {\n dispatch(actionClearFilter());\n dispatch(thunkLoadPac(getDefaultPac()));\n};\n\n/**\n * Thunk to handle missing pac\n *\n * @returns {function(*): void}\n */\n\nexport const thunkOpenPacMissing = () => dispatch => {\n dispatch(thunkSetView(constants.VIEW_NAMES.START, { nonInteraction: true }));\n dispatch(actionSetOverlayState(true));\n dispatch(openPacMissing());\n};\n\n/**\n * Thunk for loading spr\n *\n * @param spr\n * @returns {function(*): *}\n */\nexport const thunkLoadSpr = spr => dispatch => {\n dispatch(actionClearFilter());\n dispatch(thunkLoadPac(spr));\n};\n\n/**\n * Generic tac updater\n * @returns {function(...[*]): function(*, *): void}\n */\nexport const genericTacUpdaterThunk = () => (dispatch, getState) => {\n dispatch(actionSetDirtyConfiguration(true));\n\n if (selectShouldFilterIntroPopupBeVisible(getState())) {\n dispatch(actionSetFilterIntroPopupBeVisible(true));\n dispatch(actionSetHasShownFilterIntroPopup(true));\n }\n};\n\n/**\n * Add item\n * @param item\n * @param parent\n * @param meta\n * @returns {(function(*, *): void)|*}\n */\nexport const thunkAddItem =\n (item, parent, meta = {}) =>\n (dispatch, getState) => {\n const tac = selectTac(getState());\n const model = addItem(tac, item, parent, { isPersistent: true });\n const newModel =\n model &&\n updateDependentItems(model, {\n isPersistent: true,\n triggerItem: item,\n });\n const errors = validate(newModel);\n\n newModel && history.save(convert.TACtoPAC(newModel));\n\n dispatch(actionAddItem(newModel, errors, meta));\n dispatch(thunkShowMultipackMessageIfNeededFor(item.id));\n dispatch(genericTacUpdaterThunk());\n const openedExtendableConf = !!dispatch(\n thunkHandlePopupOnAddItem(item, meta)\n );\n !openedExtendableConf &&\n dispatch(thunkHandleMountingRailPopup({ tac: newModel }));\n };\n\n/**\n * Add multiple\n * @param items\n * @param parent\n * @param meta\n * @returns {(function(*, *): void)|*}\n */\nexport const thunkAddMultiple =\n (items, parent, meta = {}) =>\n (dispatch, getState) => {\n const tac = selectTac(getState());\n const model = addMultipleItems(tac, items, parent, {\n isPersistent: true,\n });\n const errors = validate(model);\n\n model && history.save(convert.TACtoPAC(model));\n\n dispatch(actionAddMultiple(model, errors, meta));\n items.forEach(item =>\n dispatch(thunkShowMultipackMessageIfNeededFor(item.id))\n );\n dispatch(genericTacUpdaterThunk());\n };\n\n/**\n * Update item\n * @param item\n * @param parent\n * @param meta\n * @returns {(function(*, *): void)|*}\n */\nexport const thunkUpdateItem = (item, parent, meta) => (dispatch, getState) => {\n const tac = selectTac(getState());\n const originalItem = tacHelpers.getItem(tac, item.itemid);\n\n const model = updateItem(tac, item, parent, {\n moveOthers: meta && meta.moveOthers,\n isPersistent: true,\n });\n\n const newModel =\n model &&\n updateDependentItems(model, {\n isPersistent: true,\n priorityItem: meta && meta.priorityItem,\n triggerItem: originalItem,\n });\n const errors = validate(newModel);\n\n newModel && history.save(convert.TACtoPAC(newModel));\n\n dispatch(actionUpdateItem(newModel, errors, meta));\n dispatch(thunkShowMultipackMessageIfNeededFor(item.id));\n dispatch(genericTacUpdaterThunk());\n const openedExtendableConf = !!dispatch(\n thunkHandlePopupOnUpdateItem(item, meta)\n );\n !openedExtendableConf &&\n dispatch(thunkHandleMountingRailPopup({ tac: newModel }));\n};\n\n/**\n * Update multiple\n * @param items\n * @param parent\n * @param meta\n * @returns {(function(*, *): void)|*}\n */\nexport const thunkUpdateMultiple =\n (items, parent, meta = {}) =>\n (dispatch, getState) => {\n const tac = selectTac(getState());\n const model = updateMultipleItems(tac, items, parent, meta);\n\n const newModel =\n model &&\n updateDependentItems(model, {\n isPersistent: true,\n priorityItem: meta && meta.priorityItem,\n triggerItem: meta?.triggerItem,\n });\n const errors = validate(newModel);\n\n newModel && history.save(convert.TACtoPAC(newModel));\n\n dispatch(actionUpdateMultiple(newModel, errors, meta));\n items.forEach(item =>\n dispatch(thunkShowMultipackMessageIfNeededFor(item.id))\n );\n dispatch(thunkHandleMountingRailPopup({ tac: newModel }));\n dispatch(genericTacUpdaterThunk());\n };\n\n/**\n * Remove item\n * @param item\n * @returns {(function(*, *): void)|*}\n */\nexport const thunkRemoveItem =\n (item, meta = {}) =>\n (dispatch, getState) => {\n const tac = selectTac(getState());\n let model = removeItem(tac, item);\n\n model =\n model &&\n updateDependentItems(model, {\n isPersistent: true,\n triggerItem: item,\n });\n\n if (model) {\n tacHelpers\n .getAllItems(model.items, item)\n .filter(item => item.connectsTo === item.itemid)\n .forEach(dependency => {\n model = removeItem(model, dependency);\n });\n }\n\n model && history.save(convert.TACtoPAC(model));\n const errors = validate(model);\n\n dispatch(actionRemoveItem(model, errors, meta));\n dispatch(thunkHandleMountingRailPopup({ tac: model }));\n dispatch(thunkHandlePopupOnRemoveItem());\n dispatch(genericTacUpdaterThunk());\n };\n\n/**\n * Hide item\n * @param item\n * @param parent\n * @returns {(function(*, *): void)|*}\n */\nexport const thunkHideItem =\n (item, parent, meta = {}) =>\n (dispatch, getState) => {\n const tac = selectTac(getState());\n const model = hideItem(tac, item, parent);\n const errors = validate(model);\n\n model && history.save(convert.TACtoPAC(model));\n\n dispatch(actionHideItem(model, errors, meta));\n dispatch(genericTacUpdaterThunk());\n };\n\n/**\n * Thunk set using mounting rail\n * @param disableMountingRails\n * @returns {(function(*, *): void)|*}\n */\nexport const thunkSetUsingMountingRail =\n disableMountingRails => (dispatch, getState) => {\n const tac = selectTac(getState());\n\n const model = {\n ...tac,\n settings: {\n ...tac.settings,\n disableMountingRails: disableMountingRails,\n },\n };\n\n const newModel = updateDependentItems(model, {\n isPersistent: true,\n });\n\n newModel && history.save(convert.TACtoPAC(newModel));\n dispatch(actionSetUseMountingRails(newModel));\n dispatch(thunkHandleMountingRailPopup({ tac: newModel }));\n };\n\n/**\n * Converts the state Tac to Pac and saves it to local storage.\n * @definition Tac | Temporary article configuration - Saved in redux\n * @definition Pac | Permanent article configuration - Saved in local storage\n * @param {*} state\n */\nconst saveStateToLocalStorage = state => {\n const tac = selectTac(state);\n history.save(convert.TACtoPAC(tac));\n};\n\nexport const thunkRedo = () => (dispatch, getState) => {\n dispatch(UndoActionCreators.redo());\n saveStateToLocalStorage(getState());\n dispatch(actionSetDirtyConfiguration(true));\n};\n\nexport const thunkUndo = () => (dispatch, getState) => {\n dispatch(UndoActionCreators.undo());\n saveStateToLocalStorage(getState());\n dispatch(actionSetDirtyConfiguration(true));\n};\n\n/**\n * Thunk for replacing the current tac with a new one\n * @param tac The new tac\n * @returns {function(*): void}\n */\nexport const thunkReplaceTac = tac => (dispatch, getState) => {\n dispatch(actionLoadPac(tac, validate(tac), !isFixedRoom()));\n dispatch(genericTacUpdaterThunk());\n saveStateToLocalStorage(getState());\n};\n\nconst thunkMoveWallHeightDependentItems = moveInYDirection => dispatch => {\n const getMovedWallHeightDependingItem = item => {\n return {\n ...item,\n y: item.y + moveInYDirection,\n };\n };\n const replaceMovedPartsInSection = (section, movedParts) => {\n return section.items.reduce((acc, curr) => {\n const findPart = movedParts.find(part => part.itemid === curr.itemid);\n return [...acc, findPart ? findPart : curr];\n }, []);\n };\n const getWallHeightUpdatedSections = (sections, movedParts) => {\n return sections.reduce((acc, curr) => {\n return [\n ...acc,\n {\n ...curr,\n items: replaceMovedPartsInSection(curr, movedParts),\n height: curr.height + moveInYDirection,\n localOptions: { keepParts: true },\n },\n ];\n }, []);\n };\n const wallHeightDependentItems = getWallHeightDependentItems();\n const movedWallHeightDependentItems = wallHeightDependentItems.parts.reduce(\n (acc, curr) => {\n return [...acc, getMovedWallHeightDependingItem(curr)];\n },\n []\n );\n const wallHeightDependentSectionsUpdate = getWallHeightUpdatedSections(\n wallHeightDependentItems.sections,\n movedWallHeightDependentItems\n );\n dispatch(\n thunkUpdateMultiple(wallHeightDependentSectionsUpdate, undefined, {\n automatedUpdate: true,\n })\n );\n};\n\nexport const thunkSetWall =\n ({ width, height }, meta = {}) =>\n (dispatch, getState) => {\n const getCurrentWallHeight = () => {\n return selectWallSize(getState())?.height;\n };\n const getCurrentWallWidth = () => {\n return selectWallSize(getState())?.width;\n };\n const getNewWallWidth = () => {\n return width ? width : getCurrentWallWidth();\n };\n const getNewWallHeight = () => {\n return height ? height : getCurrentWallHeight();\n };\n const ceilingDidMove = () => {\n return getNewWallHeight() !== getCurrentWallHeight();\n };\n const getDifferenceInHeight = () => {\n return getNewWallHeight() - getCurrentWallHeight();\n };\n\n const getNewModel = model =>\n constants.DYNAMIC_WALL_LIMITS\n ? updateDependentItems(model, {\n isPersistent: meta?.isPersistent,\n wallUpdate: true,\n })\n : model;\n\n const currentPoints = selectWallPoints(getState());\n const newPoints = getWallPoints(currentPoints, width, height);\n const wall = {\n points: newPoints,\n size: {\n width: getNewWallWidth(),\n height: getNewWallHeight(),\n },\n };\n if (ceilingDidMove()) {\n const tac = selectTac(getState());\n const curedHeight = getCuredHeight(getNewWallHeight(), tac);\n const marketRelativeHeight = selectCeilingHeightBasedOnMarket(\n getState(),\n curedHeight\n );\n\n dispatch(actionSetDraftCeilingHeight(marketRelativeHeight));\n dispatch(thunkMoveWallHeightDependentItems(getDifferenceInHeight()));\n }\n\n const model = {\n ...selectTac(getState()),\n wall,\n };\n\n const newModel = getNewModel(model);\n const errors = validate(newModel);\n\n history.save(convert.TACtoPAC(newModel));\n\n dispatch(actionSetWall(newModel, errors, meta));\n meta?.isPersistent && dispatch(thunkSetMargins());\n };\n\nexport const thunkHandleErrors = () => async (dispatch, getState) => {\n const getHighestPriorityErrors = errors => {\n const priorityOfError = error => error.options.priority ?? -Infinity;\n return errors\n .sort((a, b) => priorityOfError(a) - priorityOfError(b))\n .filter(\n (error, index, allErrors) =>\n priorityOfError(error) === priorityOfError(allErrors[0])\n );\n };\n\n const onInteraction = () =>\n dispatch(actionSetErrorHandlingPendingTimestamp(null));\n\n const tac = selectTac(getState());\n const errors = tac?.errors;\n\n if (!errors || !errors.length) return;\n\n const highestPriorityErrors = getHighestPriorityErrors(errors);\n const highestPriorityErrorsAreAllOfTheSameType = highestPriorityErrors.every(\n error => error.statisticsLabel === highestPriorityErrors[0].statisticsLabel\n );\n const highestPriorityErrorsWithAmelioration = highestPriorityErrors.filter(\n error => error.getAmelioration\n );\n if (\n highestPriorityErrorsWithAmelioration.length &&\n !highestPriorityErrorsAreAllOfTheSameType\n ) {\n throw new Error(\n 'Internal error. Multiple types of validation errors are listed at the same priority level as an error associated with an amelioration.'\n );\n }\n\n const errorWithAmelioration = highestPriorityErrorsWithAmelioration.length\n ? highestPriorityErrorsWithAmelioration[0]\n : null;\n if (errorWithAmelioration) {\n localStatisticsReporter.reportAmeliorationAttempt(\n errorWithAmelioration.statisticsLabel\n );\n }\n\n const solvableAmelioration = errorWithAmelioration\n ?.getAmelioration()\n .checkWhetherCanAmeliorate(tac)\n ? errorWithAmelioration.getAmelioration()\n : null;\n if (errorWithAmelioration && !solvableAmelioration) {\n localStatisticsReporter.reportAmeliorationCanNotBeSolved(\n errorWithAmelioration.statisticsLabel\n );\n }\n\n if (solvableAmelioration) {\n localStatisticsReporter.reportValidationError(\n errorWithAmelioration.statisticsLabel,\n MeansOfNotification.Amelioration\n );\n\n const ameliorationResult = await solvableAmelioration.start(tac);\n if (ameliorationResult.outcome === AmeliorationOutcome.Success) {\n const updatedTac = ameliorationResult.tacOut;\n dispatch(thunkReplaceTac(updatedTac));\n localStatisticsReporter.reportAmeliorationSolved(\n errorWithAmelioration.statisticsLabel\n );\n\n const timestamp = Date.now();\n dispatch(actionSetErrorHandlingPendingTimestamp(timestamp));\n window.addEventListener(POINTER_DOWN, onInteraction);\n window.addEventListener(KEY_DOWN, onInteraction);\n\n await toastMaster.noMorePendingToasts();\n window.removeEventListener(POINTER_DOWN, onInteraction);\n window.removeEventListener(KEY_DOWN, onInteraction);\n\n const selectedTimestamp = selectErrorHandlingPendingTimestamp(getState());\n\n if (selectedTimestamp === timestamp) {\n await dispatch(thunkHandleErrors());\n } else {\n /* Do nothing.\n When the timestamps don't match, user interaction while the amelioration\n toast was being displayed has cancelled the pending error handling process. */\n }\n } else if (ameliorationResult.outcome === AmeliorationOutcome.Declined) {\n /* Do nothing, except sending a statistics event.\n Just let the user fix the error on their own. */\n localStatisticsReporter.reportAmeliorationDeclined(\n errorWithAmelioration.statisticsLabel\n );\n } else {\n /* If we get here, it's likely that we have a bug in the amelioration\n or in this function.\n Are we sure that the amelioration's fastCheck function doesn't\n return true in scenarios where the transform fails? */\n throw new Error(\n 'Internal error. Abnormal amelioration behaviour encountered.'\n );\n }\n } else {\n dispatch(actionShowSceneErrors(highestPriorityErrors));\n }\n};\n","export const CEILING_HEIGHT = 'CEILING_HEIGHT';\n","import { State } from '../StateTypes';\n\nexport const selectDraftData = (state: State) => state.draft;\n\nexport const selectDraftDataSlice = (key: string) => (state: State) =>\n // @ts-ignore LIMITATION OF ANCIENT, PRE-HISTORIC TS-VERSION NOT SUPPORTING ENUMS\n state.draft.present[key];\n","import { CEILING_HEIGHT } from '../draftKeys';\nimport { selectDraftDataSlice } from '../draftSelectors';\n\nexport const selectDraftCeilingHeight = selectDraftDataSlice(CEILING_HEIGHT);\n","import { thunkSetWall } from '../../tac/tacThunks';\nimport {\n selectCeilingHeightBasedOnMarket,\n selectWallSize,\n selectTac,\n} from '../../tac/tacSelectors';\nimport { TacModel } from '../../tac/tacTypes';\nimport tacHelpers from '../../tac/tacHelpers';\nimport { selectDraftCeilingHeight } from './ceilingHeightSelectors';\nimport { selectUseMetric } from '../../dexfSettings/dexfSettingsSelectors';\nimport { actionSetDraftCeilingHeight } from './ceilingHeightActions';\nimport { parseInt } from 'lodash';\nimport { KEYS } from '../../../constants';\nimport { cm, inches } from '../../../util/measures';\nimport { GetState } from '../../../generalTypes';\nimport constants from '../../../settings/constants';\n\nexport const thunkRecountAsMm =\n (value: number) => (dispatch: any, getState: GetState) => {\n const shouldUseMetric = selectUseMetric(getState());\n return shouldUseMetric ? cm.toMm(value) : inches.toMm(value);\n };\n\nexport const getCuredHeight = (height: number, tac: TacModel | null) => {\n const wallResizingLimits = tacHelpers.getWallResizingLimits(tac);\n const maxHeight = constants.WALL.height.max;\n const minHeight = Math.max(\n wallResizingLimits.max.y,\n constants.WALL.height.min\n );\n\n if (height < minHeight) return minHeight;\n if (height > maxHeight) return maxHeight;\n\n return height;\n};\n\nexport const thunkSaveWallHeight =\n () => (dispatch: any, getState: GetState) => {\n const { width } = selectWallSize(getState());\n const draftHeight = selectDraftCeilingHeight(getState());\n const valueAsMm = dispatch(thunkRecountAsMm(draftHeight));\n const { height: currentHeight } = selectWallSize(getState());\n const tac = selectTac(getState());\n const curedHeight = getCuredHeight(valueAsMm, tac);\n const newDraftHeight = selectCeilingHeightBasedOnMarket(\n getState(),\n curedHeight\n );\n const wallHeightChanged = () => currentHeight !== curedHeight;\n dispatch(\n actionSetDraftCeilingHeight(newDraftHeight, {\n isPersistent: wallHeightChanged(),\n })\n );\n dispatch(\n thunkSetWall(\n {\n width,\n height: curedHeight,\n },\n { isPersistent: wallHeightChanged() }\n )\n );\n };\n\nexport const thunkSetVirtualKeyboardHeightInput =\n (keyCode: string) => (dispatch: any, getState: GetState) => {\n const currentHeight = selectDraftCeilingHeight(getState());\n const parsedInputValue = parseInt(keyCode, 10);\n const addingNumber = !isNaN(parsedInputValue);\n const currentHeightIsZero = currentHeight === 0 && addingNumber;\n const allCharsRemoved =\n `${currentHeight}`.length - 1 <= 0 && keyCode === KEYS.BACKSPACE;\n const removingCharacter = keyCode === KEYS.BACKSPACE;\n\n const handleBackspaceKey = () => {\n const newHeight = parseInt(`${currentHeight}`.slice(0, -1));\n dispatch(actionSetDraftCeilingHeight(newHeight));\n };\n\n const handleNumberKey = () => {\n const newHeight = parseInt(`${currentHeight}${keyCode}`);\n dispatch(actionSetDraftCeilingHeight(newHeight));\n };\n\n const handleZero = () => dispatch(actionSetDraftCeilingHeight(0));\n\n const handleStartingFromZero = () =>\n dispatch(actionSetDraftCeilingHeight(parsedInputValue));\n\n if (currentHeightIsZero) return handleStartingFromZero();\n if (allCharsRemoved) return handleZero();\n if (addingNumber) return handleNumberKey();\n if (removingCharacter) return handleBackspaceKey();\n };\n","import { KeyboardTypeEnum } from '@inter-ikea-kompis/keyboard-manager';\n\ninterface ICallbacks {\n onDone?: () => any;\n onKeyPress?: (keyCode: string) => any;\n}\n\ninterface IRootFunctions {\n setInputType: (type: string) => void;\n keyboardToggle: (state: boolean) => void;\n}\n\nconst useKeyboard = () => {\n const dummyFunc = () => {};\n let _onDoneCallback: () => any = dummyFunc;\n let _onKeyPressCallback: (keyCode: string) => any = dummyFunc;\n let _keyboardTypeSetter: ((type: KeyboardTypeEnum) => any) | null = null;\n let _keyboardToggle: ((state: boolean) => any) | null = null;\n\n const onDone = () => {\n _onDoneCallback();\n _onDoneCallback = dummyFunc;\n _onKeyPressCallback = dummyFunc;\n };\n\n const registerCallbacks = ({ onDone, onKeyPress }: ICallbacks) => {\n if (onDone) _onDoneCallback = onDone;\n if (onKeyPress) _onKeyPressCallback = onKeyPress;\n };\n\n const onKeyPress = (keyCode: string) => _onKeyPressCallback(keyCode);\n\n const showKeyboard = () => _keyboardToggle && _keyboardToggle(true);\n\n const hideKeyboard = () => _keyboardToggle && _keyboardToggle(false);\n\n const setInputType = (type: KeyboardTypeEnum) =>\n _keyboardTypeSetter && _keyboardTypeSetter(type);\n\n const registerRootFunctions = ({\n setInputType,\n keyboardToggle,\n }: IRootFunctions) => {\n if (_keyboardToggle || _keyboardTypeSetter) return;\n\n _keyboardTypeSetter = setInputType;\n _keyboardToggle = keyboardToggle;\n };\n\n return {\n showKeyboard,\n hideKeyboard,\n registerCallbacks,\n onDone,\n onKeyPress,\n setInputType,\n registerRootFunctions,\n };\n};\n\nexport default useKeyboard();\n","import React from 'react';\nimport styles from './LabeledUnitInput.module.less';\nimport { KompisText } from '@inter-ikea-kompis/react-components';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { SkapaTheme } from '@inter-ikea-kompis/themes';\nimport {\n thunkSaveWallHeight,\n thunkSetVirtualKeyboardHeightInput,\n} from '../../../state/draft/ceilingHeight/ceilingHeightThunks';\nimport { selectDraftCeilingHeight } from '../../../state/draft/ceilingHeight/ceilingHeightSelectors';\nimport { selectUseMetric } from '../../../state/dexfSettings/dexfSettingsSelectors';\nimport { actionSetDraftCeilingHeight } from '../../../state/draft/ceilingHeight/ceilingHeightActions';\nimport useKeyboard from '../../../util/useKeyboard';\nimport { KEYS } from '../../../constants';\nimport { translate } from '../../../services/L10n';\nimport { t } from '../../../translations';\nimport { KeyboardTypeEnum } from '@inter-ikea-kompis/keyboard-manager';\n\nconst CeilingInput = () => {\n const dispatch = useDispatch();\n const saveWallHeight = () => dispatch(thunkSaveWallHeight());\n const inputElement = React.useRef(null);\n\n const draftHeight = useSelector(selectDraftCeilingHeight);\n const useMetricMeasures = useSelector(selectUseMetric);\n\n const setDraftHeight = (event: any) => {\n const draftHeightRoundedUp =\n event.target.value && Math.ceil(event.target.value);\n dispatch(actionSetDraftCeilingHeight(draftHeightRoundedUp));\n };\n\n const onBlur = () => {\n inputElement?.current?.blur && inputElement.current.blur();\n saveWallHeight();\n hideKeyboard();\n };\n\n const onFocus = (e: React.FocusEvent) => {\n registerCallbacks({ onDone: onBlur, onKeyPress: onVirtualKeyPress });\n setInputType(KeyboardTypeEnum.number);\n showKeyboard();\n e.target.select();\n };\n\n const { showKeyboard, hideKeyboard, registerCallbacks, setInputType } =\n useKeyboard;\n\n const onVirtualKeyPress = (keyCode: string) =>\n dispatch(thunkSetVirtualKeyboardHeightInput(keyCode));\n\n const handleOnEnter = (e: React.KeyboardEvent) =>\n e.key === KEYS.ENTER &&\n // @ts-ignore This works, since element is an input. For some reason, typescript disagrees.\n e.target.blur();\n\n return (\n
\n \n {translate(t.CEILING_HEIGHT)}\n \n\n
\n
\n \n\n \n {useMetricMeasures\n ? translate(t.MEASURE_VALUE_CM)\n : translate(t.MEASURE_VALUE_IN)}\n \n
\n
\n
\n );\n};\n\nexport default CeilingInput;\n","import tacHelpers from '../../../state/tac/tacHelpers';\nimport { translate } from '../../L10n';\nimport { t } from '../../../translations';\nimport productService from '../../products';\nimport { isStandAlone } from '../../products/models';\nimport { hasRoomSlots } from '../common';\nimport { selectTac, selectTacIsEmpty } from '../../../state/tac/tacSelectors';\nimport { actionShowSceneSheet } from '../../../state/scene';\nimport {\n selectAvailableElvarliVariants,\n selectHasPostsAndSections,\n} from '../../products/elvarli';\nimport {\n ELVARLI_VARIANT,\n SELECT_ELVARLI_VARIANT,\n} from '../../../components/Sheets/SelectElvarliVariantSheet/SelectElvarliVariantSheet';\nimport { actionSetRangeData } from '../../../state/rangeData/rangeDataActions';\nimport { State } from '../../../state/StateTypes';\nimport { Thunk } from '../../../generalTypes';\nimport {\n selectCurrentFilterName,\n selectFilterText,\n} from '../../../state/productMenu/productMenuSelectors';\nimport { FILTERS, ITEMS } from '../../../constants';\nimport { thunkConditionalDisplayIntroPopups } from '../../../state/popups/popupsThunks';\nimport { thunkRemoveUnwantedSectionSubFilterOptions } from '../../scene/elvarli';\nimport { selectRangeDataSlice } from '../../../state/rangeData/rangeDataSelectors';\nimport store from '../../../state';\nimport {\n selectIsMobile,\n selectIsTablet,\n} from '../../../state/userAgent/userAgentSelectors';\nimport localStatisticsReporter from '../../statistics/insights/custom/local/localStatisticsReporter';\nimport CeilingInput from '../../../components/Input/LabeledUnitInput';\n\nfunction getDisabledStatus(tac: any, product: any, space: any, rebased: any) {\n if (isStandAlone(product)) {\n if (\n !hasRoomSlots(tac, space, product) &&\n !tacHelpers.hasOpenSlot(tac, product, rebased)\n ) {\n return translate(t.BALLOON_HINT_NO_PLACE);\n }\n } else {\n if (!tacHelpers.hasOpenSlot(tac, product, rebased)) {\n return translate(t.BALLOON_HINT_NO_PLACE);\n }\n }\n\n return false;\n}\n\nconst handleLackOfElvarliVariant: Thunk = () => (dispatch, getState) => {\n const state = getState();\n const [elvarliVariant] = selectAvailableElvarliVariants(state);\n localStatisticsReporter.reportSubPlannerAutomaticChoice(elvarliVariant);\n dispatch(actionSetRangeData(ELVARLI_VARIANT, elvarliVariant));\n};\n\nfunction shouldDisplayProductType(product: any) {\n return !productService.isSection(product);\n}\n\nexport const selectSelectedElvarliVariant = (state: State) =>\n selectRangeDataSlice(ELVARLI_VARIANT)(state);\n\nexport const selectCurrentlyUsedElvarliVariant = (state: State) =>\n // @ts-ignore\n selectTac(state).items[0].filter.type;\n\nconst thunkUseSceneSheet: Thunk = () => (dispatch, getState) => {\n const state = getState();\n const tacIsEmpty = selectTacIsEmpty(state);\n const hasBothSectionTypes = selectHasPostsAndSections(getState());\n\n if (tacIsEmpty && hasBothSectionTypes)\n return dispatch(actionShowSceneSheet(SELECT_ELVARLI_VARIANT));\n\n if (!hasBothSectionTypes) dispatch(handleLackOfElvarliVariant());\n\n if (!tacIsEmpty) {\n const currentVariant = selectCurrentlyUsedElvarliVariant(state);\n localStatisticsReporter.reportSubPlannerAutomaticChoice(currentVariant);\n dispatch(actionSetRangeData(ELVARLI_VARIANT, currentVariant));\n }\n\n dispatch(thunkConditionalDisplayIntroPopups());\n dispatch(thunkRemoveUnwantedSectionSubFilterOptions());\n};\n\nconst getSceneInitFunctions = () => [thunkUseSceneSheet];\n\n/**\n * Returns filter object for sub filter\n */\nexport const getCeilingHeightInputObject = (\n conditionExtension: any = () => true\n) => ({\n Component: CeilingInput,\n appliesToFilter: FILTERS.SECTIONS,\n condition: () => {\n const state = store.getState();\n const variant = selectRangeDataSlice(ELVARLI_VARIANT)(state);\n const isMobile = selectIsMobile(state);\n const isTablet = selectIsTablet(state);\n\n return variant === ITEMS.SECTION_POSTS && !isMobile && !isTablet;\n },\n});\n\nconst getSwiperConfigSectionItems = () => [getCeilingHeightInputObject()];\n\nconst getSwiperFilterText = (state: State) => {\n const variant = selectRangeDataSlice(ELVARLI_VARIANT)(store.getState());\n\n const handleSection = () => {\n const text = selectFilterText(state)?.[variant];\n\n return text ? text : null;\n };\n\n const handleParts = () => {\n return variant === ITEMS.SECTION_POSTS\n ? selectFilterText(state).brackets\n : null;\n };\n\n return selectCurrentFilterName(state) === FILTERS.SECTIONS\n ? handleSection()\n : handleParts();\n};\n\nexport { getDisabledStatus };\n\nexport default {\n getDisabledStatus,\n shouldDisplayProductType,\n getSceneInitFunctions,\n getSwiperConfigSectionItems,\n getSwiperFilterText,\n};\n","export const NotImplementedError = () => {\n throw new Error(`NotImplementedError\\nRange-specific implementation needed`);\n};\n","import { applicationSettings } from '../../settings/application';\nimport Bror from './bror';\nimport Jonaxel from './jonaxel';\nimport Boaxel from './boaxel';\nimport Aurdal from './aurdal';\nimport Ivar from './ivar';\nimport Elvarli from './elvarli';\nimport productService from '../products';\nimport articles from '../products/articles';\nimport { metricToImperial, MM_PER_INCH } from '../../util/measures';\nimport { translate } from '../L10n';\nimport { t } from '../../translations';\nimport { round } from '../../util/round';\nimport constants from '../../settings/constants';\nimport { RANGES } from '../../constants';\nimport React from 'react';\nimport { Space, TacItem, Thumbnail } from '../../generalTypes';\nimport { TacModel } from '../../state/tac/tacTypes';\nimport { IProduct } from '@inter-ikea-kompis/types/lib';\nimport { NotImplementedError } from '../util';\nimport {\n ColorCombination,\n Filter,\n} from '../../state/productMenu/productMenuTypes';\n\nlet thumbnailData: Thumbnail[];\n\nfunction init(thumbnails: Thumbnail[]) {\n thumbnailData = thumbnails;\n}\n\nfunction getThumbnailData(fileName: string) {\n return thumbnailData.find(thumb => thumb.id === fileName.substring(1));\n}\n\nconst getDisabledStatus = (\n tac: TacModel,\n product: TacItem,\n space: Space | undefined,\n rebased: TacItem\n) => NotImplementedError();\n\nfunction getTypeText(article: IProduct) {\n let articleWithData;\n if (productService.isOnlyAvailableInMultipack(article.itemId)) {\n const multipackItemId = constants.BULK_ARTICLES[article.itemId].id;\n articleWithData = articles.getArticle(multipackItemId);\n } else {\n articleWithData = article;\n }\n\n // @ts-ignore According to kompis docs 'IProduct' contains typeName. According to our typescript, it does not..\n return articleWithData.typeName;\n}\n\nconst getTableDisplayText = (\n article: IProduct,\n product: TacItem,\n useMetricMeasures: boolean,\n dimension: string\n) => '';\n\nconst shouldDisplayProductType = (product: TacItem) => NotImplementedError();\n\nfunction getTrashcanZoomLevel() {\n return 1.16;\n}\n\nfunction displayColor(\n product: TacItem,\n filter: Filter,\n currentColorFilter: ColorCombination\n) {\n const noColorItems = filter.designTextBlacklist || [];\n if (noColorItems.includes(product.id)) {\n return false;\n }\n const ids = filter.products.flat();\n const swaps = productService.getSwappables(product, {\n width: product.filter.width,\n depth: product.filter.depth,\n height: product.filter.height,\n });\n return swaps\n .filter(\n swap =>\n ((!currentColorFilter && filter.colors) ||\n (filter.colors &&\n currentColorFilter.colors?.some(\n color => color === swap.filter.color\n ))) &&\n swap.filter.color !== product.filter.color\n )\n .map(swap => swap.id)\n .filter(id => id !== product.id)\n .filter(id => !noColorItems.includes(id))\n .some(id => ids.indexOf(id) !== -1);\n}\n\nfunction getWallMeasurementText(value: number, useMetricMeasures: boolean) {\n const imp = metricToImperial(round(value, MM_PER_INCH));\n return useMetricMeasures\n ? `${value / 10} ${translate(t.MEASURE_VALUE_CM)}`\n : `${imp.feet} ${translate(t.MEASURE_VALUE_FEET)} ${imp.inches} ${translate(\n t.MEASURE_VALUE_IN\n )}`;\n}\n\nexport type SwiperConfigSectionItem = {\n Component: React.FunctionComponent;\n appliesToFilter: string;\n condition: () => boolean;\n};\n\nconst getSceneInitFunctions = () => [];\nconst getSwiperConfigSectionItems = (): SwiperConfigSectionItem[] => [];\nconst getSwiperFilterText = () => null;\n\nconst base = {\n init,\n getDisabledStatus,\n getTypeText,\n getTableDisplayText,\n displayColor,\n shouldDisplayProductType,\n getTrashcanZoomLevel,\n getWallMeasurementText,\n getThumbnailData,\n getSceneInitFunctions,\n getSwiperConfigSectionItems,\n getSwiperFilterText,\n};\n\nexport const getRange = () => {\n switch (applicationSettings.applicationName) {\n case RANGES.BROR:\n return Bror;\n case RANGES.JONAXEL:\n return Jonaxel;\n case RANGES.BOAXEL:\n return Boaxel;\n case RANGES.AURDAL:\n return Aurdal;\n case RANGES.IVAR:\n return Ivar;\n case RANGES.ELVARLI:\n return Elvarli;\n default:\n console.error('Missing range-specific implementation of swiper service');\n return base;\n }\n};\n\nconst api = {\n ...base,\n ...getRange(),\n};\n\nexport default api;\n","import { State } from '../StateTypes';\n\n/**\n * Select raw data state slice - a slice intended for helping normalizing state.\n * See https://redux.js.org/usage/structuring-reducers/normalizing-state-shape\n * for info on normalized redux structure.\n *\n * @param state\n */\nexport const selectRawData = (state: State) => state.rawData;\n\n/**\n * Select raw items state slice\n * @param state\n */\nexport const selectRawItems = (state: State) => selectRawData(state).products;\n\n/**\n * Select raw item by id\n * @param state\n * @param id\n */\nexport const selectRawItem = (state: State, id: number) =>\n selectRawItems(state)[id];\n\n/**\n * Select raw items by array of ids\n * @param state\n * @param itemIds\n */\nexport const selectRawItemsByIds = (state: State, itemIds: number[]) => {\n const rawItems = selectRawItems(state);\n return itemIds.map(itemId => rawItems[itemId]);\n};\n","import { getSections, deriveMeasurements } from '../../services/products';\nimport { getProductMeasurementSettings } from '../products/productsHelpers';\nimport { State } from '../StateTypes';\nimport {\n ColorObject,\n Item,\n Itemid,\n Itemids,\n TacItem,\n} from '../../generalTypes';\nimport {\n ColorCombination,\n ColorCombinations,\n Filter,\n ProductMenuItem,\n SubfilterOption,\n Swiper,\n} from './productMenuTypes';\nimport { selectRelatedIdsById } from '../products/productsSelectors';\nimport _ from 'lodash';\nimport { selectPresentTac } from '../tac/tacReducer/tacSelectors';\nimport tacHelpers from '../tac/tacHelpers';\nimport { rebasedItems } from '../../services/products/models';\nimport productService from '../../services/products';\nimport SwiperService, { getRange } from '../../services/swiper';\nimport { selectRawItems } from '../rawData/rawDataSelectors';\nimport {\n selectIsWallResizerActive,\n selectShowSceneSheet,\n} from '../scene/sceneSelectors';\nimport { createSelector } from 'reselect';\nimport store from '../';\nimport { selectRangeData } from '../rangeData/rangeDataSelectors';\nimport { selectTac } from '../tac/tacSelectors';\nimport { TacModel } from '../tac/tacTypes';\nimport {\n FilterFunction,\n FilterObject,\n} from '../../services/products/productsServiceTypes';\n\n/**\n * Select sections sizes\n *\n * @returns {{depth: *[], width: *[], height: *[]}}\n */\nexport const selectSectionSizes = () => deriveMeasurements(getSections());\n\n/**\n * Selects product menu\n *\n * @param productMenu\n * @returns {*}\n */\nexport const selectProductMenu = ({ productMenu }: State) => productMenu;\n\n/**\n * Selects filters\n *\n * @param state\n * @returns {*}\n */\nexport const selectFiltersAsArray = (state: State): Filter[] =>\n Object.values(selectProductMenu(state).filters);\n\nexport const selectFiltersWithColorOptions = (state: State) =>\n selectFiltersAsArray(state).filter(({ colors }) => !!colors);\n\n/**\n * Selects swiper\n *\n * @param state\n * @returns {*}\n */\nexport const selectSwiper = (state: State): Swiper =>\n selectProductMenu(state).swiper;\n\n/**\n * Returns a given filter\n *\n * @param state {object}\n * @param filter {string}\n * @returns {*}\n */\nexport const selectFilter = (state: State, filter: string) =>\n selectProductMenuFilters(state)[filter];\n\n/**\n * Select current filter selectable colors\n *\n * @param state\n */\nexport const selectCurrentFilterSelectableColors = (state: State) =>\n selectProductMenuFilter(state).selectableColors;\n\n/**\n * Select current color filter\n *\n * @param state\n * @returns {{}}\n */\nexport const selectCurrentColorFilter = (state: State) => {\n return selectCurrentFilterSelectableColors(state)?.[\n selectProductMenuFilter(state).currentColorFilter\n ];\n};\n\nexport const selectCurrentColorFilterName = (state: State) =>\n selectProductMenuFilter(state).currentColorFilter;\n\n/**\n * Selects current sub filter\n *\n * @param state\n * @returns {Requireable}\n */\nexport const selectCurrentSubFilterIndex = (state: State) =>\n selectSwiper(state).currentSubFilter;\n\nexport const selectDefaultSubFilterIndex = (state: State) => {\n const filters = selectProductMenuFilters(state);\n const relevantFilter = Object.values(filters).find(\n filter => filter.subFilter?.defaultSubFilter\n );\n return relevantFilter?.subFilter?.defaultSubFilter || 0;\n};\n\n/**\n * Select current sub filter value\n * @param state\n */\nexport const selectCurrentSubFilterValue = (state: State) => {\n return selectProductMenuFilter(state).subFilter?.options?.[\n selectCurrentSubFilterIndex(state)\n ]?.value;\n};\n\n/**\n * Select current sub filter key\n * @param state\n */\nexport const selectCurrentSubFilterKey = (state: State) =>\n selectProductMenuFilter(state).subFilter?.key;\n\n/**\n * Select sub filter options\n * @param state\n */\nexport const selectCurrentSubFilterOptions = (state: State) =>\n selectProductMenuFilter(state).subFilter.options || [];\n\n/**\n * Select sub filter options based on filter\n * @param filter\n */\nexport const selectSubFilterOptionsBasedOnFilter = (filter: Filter) =>\n filter.subFilter.options || [];\n\n/**\n * Select all sub filter options\n * @param state\n */\nexport const selectAllSubFilterOptions = (state: State) => {\n return selectFiltersAsArray(state).reduce(\n (acc: Array, filter: Filter) => [\n ...acc,\n ...selectSubFilterOptionsBasedOnFilter(filter),\n ],\n []\n );\n};\n\n/**\n * Returns formatted and ready to render measurements string\n *\n * @param state {object}\n * @param id {string}\n * @returns {*}\n */\nexport const selectMeasurementString = (\n state: State,\n id: Itemid\n): string | undefined => selectRelatedIdsById(state, id)?.measurementString;\n\n/**\n * Returns boolean for whether product measurements display\n * should override settings\n *\n * @param state {object}\n * @param id {string}\n * @returns {boolean}\n */\nexport const selectForceDisplayMeasurement = (\n state: State,\n id: Itemid\n): boolean => !!selectRelatedIdsById(state, id)?.forceDisplay;\n\n/**\n * Returns boolean according to settings if a measurement\n * should be displayed for product\n *\n * @param state {object}\n * @param product {object}\n * @returns {boolean|*}\n */\nexport const selectShouldDisplayMeasurements = (\n state: State,\n product: TacItem\n) =>\n selectForceDisplayMeasurement(state, product?.id) ||\n getProductMeasurementSettings(product.filter.type)?.display ||\n false;\n\n/**\n * Select product menu filters\n *\n * @param state\n */\nexport const selectProductMenuFilters = (state: State) =>\n selectProductMenu(state).filters;\n\n/**\n * Returns current product menu filter\n *\n * @param state\n * @returns {*}\n */\nexport const selectProductMenuFilter = (state: State) => {\n const currentFilter = selectSwiper(state)?.currentFilter;\n\n if (currentFilter) {\n return selectProductMenuFilters(state)[currentFilter];\n }\n return Object.values(selectProductMenuFilters(state))[0];\n};\n\n/**\n * Select filter items\n */\nexport const selectFilterItems = (state: State, filterName: string) => {\n const filter = selectFilter(state, filterName);\n return filter.products.flat().reduce((acc, id) => {\n const item = productService.getProduct(id);\n return item ? [...acc, item] : acc;\n }, []);\n};\n\n/**\n * Returns swiper layout\n * @param state\n * @returns {*}\n */\nexport const selectProductMenuLayout = (state: State) =>\n selectProductMenuFilter(state).swiperLayout.name;\n\n/**\n * Select product menu item\n * @param product\n * @param tac\n */\nexport const selectProductMenuItem = (product: TacItem, tac: TacModel) => {\n const rangeApi = getRange();\n\n const { space, rebased } = tac\n ? { space: tacHelpers.getSpace(tac), rebased: rebasedItems(tac.items) }\n : { space: undefined, rebased: undefined };\n\n /**\n * Get disabled status\n * @returns {*|boolean}\n */\n const disabledStatus = () =>\n tac\n ? rangeApi.hasOwnProperty('getDisabledStatus')\n ? rangeApi.getDisabledStatus(tac, product, space, rebased)\n : SwiperService.getDisabledStatus(tac, product, space, rebased)\n : '...';\n\n /**\n * Create items object\n * @param product\n */\n return {\n product,\n disabled: disabledStatus(),\n };\n};\n\n/**\n * Has selectable colors\n * @param state\n */\nexport const selectFilterHasSelectableColors = (state: State) =>\n selectSelectableColorsAsArray(state)?.length > 1;\n\n/**\n * Sub filter has items\n * @param state\n * @param filterName\n * @param filterOption\n */\nexport const selectSubFilterHasItems = (\n state: State,\n filterName: string,\n filterOption: any\n) => {\n const filter = selectFilter(state, filterName);\n const subFilterKey = filter.subFilter.key;\n const rawItems = selectRawItems(state);\n const items = filter.products.reduce((acc: TacItem[], [id]) => {\n if (!id) return acc;\n\n return id && productService.getProduct(id) ? [...acc, rawItems[id]] : acc;\n }, []);\n\n return !!items.filter(product => {\n // @ts-ignore LIMITATION OF ANCIENT, PRE-HISTORIC TS-VERSION NOT SUPPORTING ENUMS\n return product[subFilterKey] === filterOption.value;\n }).length;\n};\n\n/**\n * Select filtered swiper items\n */\nexport const selectFilteredSwiperItems = createSelector(\n [\n selectPresentTac,\n selectProductMenuFilter,\n selectIsWallResizerActive,\n selectRawItems,\n selectRangeData,\n selectSwiper,\n selectShowSceneSheet,\n ],\n (tac, productMenuFilter, wallResizerActive, rawItems, rangedData, swiper) => {\n /**\n * Runs filters on items, including range specific filters\n */\n const runFilters = (\n filteredItems: ProductMenuItem[],\n filter: FilterFunction\n ) => filteredItems.filter(filter(store.getState()));\n\n /**\n * Returns products\n * @param itemsIds\n */\n const selectProducts = (itemsIds: Itemids) =>\n itemsIds.reduce(\n (acc, id: Itemid) =>\n id && productService.getProduct(id)\n ? [...acc, selectProductMenuItem(rawItems[id], tac)]\n : acc,\n []\n );\n\n /**\n * Returns filtered items in sorted array\n * @param itemsIds\n */\n const handleItemsCluster = (itemsIds: Itemids) =>\n productService\n .getFilters()\n .map(({ filter }: FilterObject) => filter)\n .reduce(runFilters, selectProducts(itemsIds));\n\n return productMenuFilter.products.reduce(\n (acc, ids) => {\n if (!ids.length) return acc;\n\n const products = handleItemsCluster(ids);\n return products.length ? [...acc, products] : acc;\n },\n []\n );\n },\n {\n memoizeOptions: {\n equalityCheck: (a, b) =>\n selectIsWallResizerActive(store.getState()) ? true : a === b,\n maxSize: 1,\n },\n }\n);\n\n/**\n * Select name of current filter\n * @param state\n */\nexport const selectCurrentFilterName = (state: State) =>\n selectSwiper(state).currentFilter;\n\n/**\n * Select unique colors\n * @param state\n * @param filterName\n * @returns {*[]}\n */\nexport const selectUniqueColors = (state: State, filterName: string) => {\n const rawItems = selectRawItems(state);\n const tac = selectTac(state);\n const filter = selectFilter(state, filterName);\n\n if (!tac) return [];\n\n const items = filter.products.reduce(\n (acc: ProductMenuItem[], [id]) => {\n const product = rawItems[id];\n if (id && productService.getProduct(id))\n return [...acc, selectProductMenuItem(product, tac)];\n\n return acc;\n },\n []\n );\n\n const colorCanBeAdded = (\n colorArr: string[],\n ProductSwiperItem: ProductMenuItem\n ) => {\n const color = ProductSwiperItem?.product?.filter?.color;\n return filter?.colors?.[color] && !colorArr.includes(color);\n };\n\n return items.reduce(\n (acc: string[], productSwiperItem: ProductMenuItem) =>\n colorCanBeAdded(acc, productSwiperItem)\n ? [...acc, productSwiperItem.product.filter.color]\n : acc,\n []\n );\n};\n\n/**\n * Select filter text key\n * @param state\n */\nexport const selectFilterText = (state: State) =>\n selectProductMenuFilter(state).filterTextKeys;\n\n/**\n * Select is selected filter\n * @param filter\n * @returns {boolean}\n */\nexport const selectIsSelectedFilter = (filter: Filter) => (state: State) =>\n selectProductMenuFilter(state).name === filter.name;\n\n/**\n * Select available colors\n * @param state\n * @returns {null|*[]|*[]}\n */\nexport const selectAvailableColors = (state: State) =>\n selectProductMenuFilter(state).selectableColors;\n\n/**\n * Select selectable colors\n * @param state\n * @param filterName\n */\nexport const selectSelectableColors = (\n state: State,\n filterName: string = selectCurrentFilterName(state)\n) => {\n return selectFilter(state, filterName)?.selectableColors;\n};\n\n/**\n * Select selectable colors as array\n * @param state\n * @param filterName\n */\nexport const selectSelectableColorsAsArray = (\n state: State,\n filterName: string = selectCurrentFilterName(state)\n) => {\n const selectableColorsArray = selectSelectableColors(state, filterName);\n return selectableColorsArray && Object.values(selectableColorsArray);\n};\n\n/**\n * Select selected color for filter\n * @param state\n * @param filterName\n */\nexport const selectFilterSelectedColor = (state: State, filterName: string) => {\n return selectSelectableColors(state)?.[\n selectFilter(state, filterName).currentColorFilter\n ];\n};\n\n/**\n * Select combined available colors\n * @param state\n * @param filterName\n * @returns {unknown[] | any[]}\n */\nexport const selectCombinedAvailableColors = (\n state: State,\n filterName: string\n) => {\n type Keeps = { name: string; colors: string[] };\n type Discards = string;\n\n type sorterObject = {\n keeps: Keeps[];\n discards: Discards[];\n };\n\n const uniqueColors = selectUniqueColors(state, filterName);\n const filter = selectFilter(state, filterName);\n\n /**\n * Return combinable colors\n * @param combinables\n */\n const getCombinableColors = (combinables: string[]) =>\n combinables.filter(combinable => uniqueColors.includes(combinable));\n\n /**\n * Builds a color object\n * @param colors\n */\n const generateColorObject = (\n colors: string[],\n silentlyIncludeColors: string[] = []\n ) => ({\n name: colors.sort().join('_'),\n colors: [...colors, ...silentlyIncludeColors],\n });\n\n /**\n * Calculates color object\n * @param discards\n * @param currentColor\n */\n const calculateColorObject = (\n discards: Discards[],\n currentColor: ColorObject\n ) => {\n if (discards.includes(currentColor.value)) return [];\n\n const combinableColors = getCombinableColors(currentColor.combinables);\n const silentlyIncludeColors = currentColor.silentlyInclude;\n\n return combinableColors.length\n ? combinableColors.map((combinable: string) =>\n generateColorObject([currentColor.value, combinable])\n )\n : [generateColorObject([currentColor.value], silentlyIncludeColors)];\n };\n\n /**\n * Filters what to colors to keep and which to discard\n */\n const filterUniqueColors = () => {\n return uniqueColors.reduce(\n (acc: sorterObject, curr: string) => {\n const currentColor = filter.colors[curr];\n return {\n keeps: [\n ...acc.keeps,\n ...calculateColorObject(acc.discards, currentColor),\n ],\n discards: [...acc.discards, ...currentColor.combinables],\n };\n },\n { keeps: [], discards: [] }\n ).keeps;\n };\n\n /**\n * Sort colors\n * @param aColor\n * @param bColor\n */\n const sortColors = (\n { colors: [aColor] }: ColorCombination,\n { colors: [bColor] }: ColorCombination\n ) => _.clamp(filter.colors[aColor].sort - filter.colors[bColor].sort, -1, 1);\n\n return filterUniqueColors()\n .sort(sortColors)\n .reduce(\n (acc: ColorCombinations, curr: ColorCombination) => ({\n ...acc,\n [curr.name]: curr,\n }),\n {}\n );\n};\n","import { Thunk } from '../../../generalTypes';\nimport { selectAllSubFilterOptions } from '../../../state/productMenu/productMenuSelectors';\nimport { actionSetSubFilterOptions } from '../../../state/productMenu/productMenuActions';\nimport { FILTERS } from '../../../constants';\nimport { SubfilterOption } from '../../../state/productMenu/productMenuTypes';\nimport { selectRangeDataSlice } from '../../../state/rangeData/rangeDataSelectors';\nimport { ELVARLI_VARIANT } from '../../../components/Sheets/SelectElvarliVariantSheet/SelectElvarliVariantSheet';\n\nexport const thunkRemoveUnwantedSectionSubFilterOptions: Thunk =\n () => (dispatch, getState) => {\n const state = getState();\n const selectedElvarliVariant = selectRangeDataSlice(ELVARLI_VARIANT)(state);\n const filterOptions = selectAllSubFilterOptions(state).filter(\n (filterOption: SubfilterOption) =>\n // @ts-ignore LIMITATION OF ANCIENT, PRE-HISTORIC TS-VERSION NOT SUPPORTING TYPE CASTING, SHOULD BE CAST AS STRING,\n // SINCE THIS ALWAYS RUN AFTER ELVARLI VARIANT IS SELECTED.\n filterOption.sectionType === selectedElvarliVariant\n );\n\n dispatch(actionSetSubFilterOptions(FILTERS.SECTIONS, filterOptions));\n };\n\nexport default {\n sceneReady: () => () => {},\n};\n","import React from 'react';\nimport { SkapaTheme } from '@inter-ikea-kompis/themes';\nimport {\n KompisSheet,\n KompisText,\n KompisToggle,\n KompisButton,\n} from '@inter-ikea-kompis/react-components';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { selectShowSceneSheet } from '../../../state/scene/sceneSelectors';\nimport { actionHideSceneSheet } from '../../../state/scene';\nimport { actionSetRangeData } from '../../../state/rangeData/rangeDataActions';\nimport { actionSetFilter } from '../../../state/productMenu/productMenuActions';\nimport { ThemeFontStyleTypeEnum } from '@inter-ikea-kompis/enums';\nimport { ButtonTypeEnum } from '@inter-ikea-kompis/component-button';\nimport { previousView } from '../../../state/navigation';\nimport { ITEMS } from '../../../constants';\nimport styles from './SelectElvarliVariantSheet.module.less';\nimport { translate } from '../../../services/L10n';\nimport { t } from '../../../translations';\nimport { selectIsMobileLandscape } from '../../../state/userAgent/userAgentSelectors';\nimport { SheetSizeEnum } from '@inter-ikea-kompis/component-sheet';\nimport { thunkConditionalDisplayIntroPopups } from '../../../state/popups/popupsThunks';\nimport constants from '../../../settings/constants';\nimport { thunkRemoveUnwantedSectionSubFilterOptions } from '../../../services/scene/elvarli';\nimport localStatisticsReporter from '../../../services/statistics/insights/custom/local/localStatisticsReporter';\n\nexport const SELECT_ELVARLI_VARIANT = 'SELECT_ELVARLI_VARIANT';\nexport const ELVARLI_VARIANT = 'elvarliVariant';\n\nexport const SelectElvarliVariantSheet = () => {\n const showSheet = useSelector(selectShowSceneSheet);\n const isMobileLandscape = useSelector(selectIsMobileLandscape);\n\n const [selectedIndex, setSelectedIndex] = React.useState(0);\n const options = [\n {\n buttonLabel: translate(t.USE_POSTS),\n toggleOption: { label: translate(t.POSTS), icon: '' },\n type: ITEMS.SECTION_POSTS,\n img: './images/elvarli/posts.png',\n },\n {\n buttonLabel: translate(t.USE_SIDE_UNITS),\n toggleOption: { label: translate(t.SIDE_UNITS), icon: '' },\n type: ITEMS.SECTION_SIDE_UNITS,\n img: './images/elvarli/side_units.png',\n },\n ];\n\n const dispatch = useDispatch();\n\n /**\n * Returns user to start page\n */\n const goToStart = () => dispatch(previousView());\n\n /**\n * Handles selection of a variant\n */\n const selectVariant = () => {\n const selectedVariant = options[selectedIndex].type;\n localStatisticsReporter.reportSubPlannerUserChoice(selectedVariant);\n dispatch(actionSetRangeData(ELVARLI_VARIANT, selectedVariant));\n dispatch(actionHideSceneSheet());\n dispatch(actionSetFilter(constants.DEFAULT_FILTER));\n\n dispatch(thunkConditionalDisplayIntroPopups());\n dispatch(thunkRemoveUnwantedSectionSubFilterOptions());\n };\n\n /**\n * Returns toggle data\n */\n const getToggleOptions = () =>\n options.map(({ toggleOption }) => toggleOption);\n\n /**\n * Handle on select\n * @param selectedIndex\n */\n const _onSelect = ({ detail: { selectedIndex } }: any) =>\n setSelectedIndex(selectedIndex);\n\n /**\n * Render sheet header\n */\n const renderHeader = () => (\n
\n \n {translate(t.SELECT_BASE)}\n \n
\n \n {translate(t.CHOICE_INFORMATION)}\n \n
\n
\n \n
\n
\n );\n\n /**\n * Render sheet body\n */\n const renderBody = () => (\n
\n \n
\n );\n\n /**\n * Render sheet footer\n */\n const renderFooter = () => (\n
\n \n \n
\n );\n\n /**\n * Render on top of each other\n */\n const renderRegular = () => (\n
\n {renderHeader()}\n {renderBody()}\n {renderFooter()}\n
\n );\n\n /**\n * Renders body alongside rest of content\n */\n const renderLandscape = () => (\n
\n {renderBody()}\n
\n {renderHeader()}\n {renderFooter()}\n
\n
\n );\n\n /**\n * Renders content based on orientation\n */\n const renderContent = () =>\n isMobileLandscape ? renderLandscape() : renderRegular();\n\n /**\n * Returns sheet size based on orientation\n */\n const getSheetSize = () =>\n isMobileLandscape ? SheetSizeEnum.medium : SheetSizeEnum.small;\n\n return (\n \n {renderContent()}\n \n );\n};\n","import React, { useEffect } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { SkapaTheme } from '@inter-ikea-kompis/themes';\nimport { KompisText, KompisToggle } from '@inter-ikea-kompis/react-components';\nimport styles from './ProductMenuSubFilter.module.less';\nimport { translate } from '../../services/L10n';\nimport { t } from '../../translations';\nimport { metricToImperial } from '../../util/measures';\nimport { selectUseMetric } from '../../state/dexfSettings/dexfSettingsSelectors';\nimport {\n selectIsMobileOrTablet,\n selectIsPortrait,\n selectIsLandscape,\n} from '../../state/userAgent/userAgentSelectors';\nimport {\n selectCurrentSubFilterIndex,\n selectProductMenuFilter,\n} from '../../state/productMenu/productMenuSelectors';\nimport { SubfilterOption } from '../../state/productMenu/productMenuTypes';\nimport IToggleItem from '@inter-ikea-kompis/component-toggle/lib/types/IToggleItem';\nimport { actionSetSwiperSubFilter } from '../../state/productMenu/productMenuActions';\nimport localStatisticsReporter from '../../services/statistics/insights/custom/local/localStatisticsReporter';\n\nconst ProductMenuSubFilter = () => {\n const dispatch = useDispatch();\n\n const setSwiperSubFilter = (subFilter: number) =>\n dispatch(actionSetSwiperSubFilter(subFilter));\n\n const currentFilter = useSelector(selectProductMenuFilter);\n const currentSubFilter = useSelector(selectCurrentSubFilterIndex);\n const shouldUseMetric = useSelector(selectUseMetric);\n const isMobileOrTablet = useSelector(selectIsMobileOrTablet);\n const isPortrait = useSelector(selectIsPortrait);\n const isLandscape = useSelector(selectIsLandscape);\n\n const { key, options, preferToDisableRatherThanHideEmpty } =\n currentFilter.subFilter;\n const optionsWithItems = options.filter(option => option.hasItems);\n\n useEffect(() => {\n if (isInDisabledMode() && !options[currentSubFilter]?.hasItems) {\n const optionToSelect = optionsWithItems[0];\n const indexOfOptionToSelect = options.indexOf(optionToSelect);\n setSwiperSubFilter(indexOfOptionToSelect);\n }\n }, [currentSubFilter]);\n\n /**\n * Checks criteria to decide whether currently in disabled mode.\n * @returns {boolean} Whether currently in disabled mode.\n */\n const isInDisabledMode = (): boolean =>\n /* Disabled mode is currently not supported for more than two options,\n since KompisToggle cannot handle scenarios where there are two or\n more enabled options while some option is disabled. */\n !!preferToDisableRatherThanHideEmpty &&\n options.length === 2 &&\n optionsWithItems.length === 1;\n\n /**\n * Get options to display\n * @returns {SubfilterOption[]} The options to display depending on mode.\n */\n const getOptionsToDisplay = (): SubfilterOption[] =>\n isInDisabledMode() ? options : optionsWithItems;\n\n /**\n * Get metric\n * @returns {number|string}\n */\n const getMetric = (value: number) =>\n shouldUseMetric ? value / 10 : metricToImperial(value).onlyInches;\n\n /**\n * Get unit\n * @param option\n * @returns {string|*}\n */\n const getUnit = (option: SubfilterOption) =>\n `${getMetric(getLabel(option))} ${\n shouldUseMetric\n ? translate(t.MEASURE_VALUE_CM)\n : translate(t.MEASURE_VALUE_IN)\n }`;\n\n /**\n * Allows for override label\n * @param overrideLabel\n * @param name\n */\n const getLabel = ({ overrideLabel, value }: SubfilterOption) =>\n overrideLabel ? overrideLabel : value;\n\n /**\n * Get label\n * @returns {*}\n */\n const getLabels = (): IToggleItem[] =>\n getOptionsToDisplay().map((option: SubfilterOption) => ({\n label: `${option.name && getUnit(option)}`,\n icon: '',\n }));\n\n /**\n * Set sub filter\n * @param selectedIndex\n * @private\n */\n const _setSubFilter = ({\n detail: { selectedIndex },\n }: CustomEvent<{ selectedIndex: number }>) => {\n setSwiperSubFilter(selectedIndex);\n const filter = currentFilter.name;\n const subFilter = key;\n const option = options[selectedIndex].value?.toString();\n option &&\n localStatisticsReporter.reportProductMenuSubFilterInteraction(\n filter,\n subFilter,\n option\n );\n };\n\n /**\n * Render header label\n * @returns {false|JSX.Element}\n */\n const renderHeaderLabel = () =>\n isLandscape && (\n
\n \n {`${translate(t[currentFilter.subFilter.labelTranslationKey])}:`}\n \n
\n );\n\n return getOptionsToDisplay().length > 1 ? (\n
\n {renderHeaderLabel()}\n \n
\n ) : null;\n};\n\nexport default ProductMenuSubFilter;\n","import React from 'react';\nimport PropTypes from 'prop-types';\n\nexport default class ClassComponent extends React.Component {\n static propTypes = {\n children: PropTypes.oneOfType([\n PropTypes.arrayOf(PropTypes.node),\n PropTypes.node,\n ]).isRequired,\n };\n\n render() {\n return this.props.children;\n }\n}\n","import EventEmitter from 'eventemitter3';\n\nclass CustomEventEmitter extends EventEmitter {\n emit(event, ...args) {\n if (!event) {\n throw new Error('Tried to emit undefined event');\n }\n\n super.emit(event, ...args);\n }\n}\n\nexport default new CustomEventEmitter();\n","export const GRAB_ITEM = 'GRAB_ITEM';\nexport const DROP_ITEM = 'DROP_ITEM';\nexport const PICKUP_ITEM = 'PICKUP_ITEM';\nexport const ITEM_MOVING = 'ITEM_MOVING';\n\nexport const TRASH_CAN_HOVER = 'TRASH_CAN_HOVER';\nexport const SCENE_REDRAWN = 'SCENE_REDRAWN';\n\nexport const SHOW_CONF = 'SHOW_CONF';\nexport const SHOW_POPUP_ERROR = 'SHOW_POPUP_ERROR';\nexport const SHOW_POPUP_INTRO = 'SHOW_POPUP_INTRO';\nexport const SHOW_POPUP_INFO = 'SHOW_POPUP_INFO';\nexport const DISMISS_POPUP = 'DISMISS_POPUP';\n\nexport const IFRAME_RESIZED = 'IFRAME_RESIZED';\n\nexport const CONF_MENU_CHANGE = 'CONF_MENU_CHANGE';\n\nexport const SCREENSAVER_STARTED = 'SCREENSAVER_STARTED';\n\nexport const WALL_RESIZED = 'WALL_RESIZED';\n\nexport default {\n DISMISS_POPUP,\n GRAB_ITEM,\n DROP_ITEM,\n PICKUP_ITEM,\n ITEM_MOVING,\n TRASH_CAN_HOVER,\n SCENE_REDRAWN,\n SHOW_CONF,\n SHOW_POPUP_ERROR,\n SHOW_POPUP_INTRO,\n SHOW_POPUP_INFO,\n IFRAME_RESIZED,\n SCREENSAVER_STARTED,\n WALL_RESIZED,\n};\n","export function innerWidth(el) {\n if (el.children.length === 0) {\n return el.offsetWidth;\n }\n\n return Array.from(el.children).reduce((width, element) => {\n return width + element.offsetWidth;\n }, 0);\n}\n","import React from 'react';\nimport { createPortal } from 'react-dom';\nimport PropTypes from 'prop-types';\nimport { SCENE_VIEW_ID } from '../../constants';\n\nclass Portal extends React.Component {\n //\n // PropTypes\n\n static propTypes = {\n children: PropTypes.node.isRequired,\n container: PropTypes.oneOfType([\n PropTypes.string,\n PropTypes.instanceOf(Element),\n ]),\n };\n\n _getContainer() {\n const container = this.props.container;\n\n if (typeof container === 'string') {\n return (\n document.querySelector(container) ||\n document.querySelector(`#${SCENE_VIEW_ID}`) ||\n document.body\n );\n }\n\n if (container instanceof Element) {\n return container;\n }\n\n return document.querySelector(`#${SCENE_VIEW_ID}`) || document.body;\n }\n\n //\n // Render\n render() {\n return createPortal(this.props.children, this._getContainer());\n }\n}\n\nexport default Portal;\n","export const states = {\n enter: 'enter',\n entering: 'entering',\n entered: 'entered',\n exit: 'exit',\n exiting: 'exiting',\n exited: 'exited',\n};\n\nexport const stateClassNames = {\n enter: ['enter'],\n entering: ['enter', 'enterActive'],\n entered: ['enterDone'],\n exit: ['exit'],\n exiting: ['exit', 'exitActive'],\n exited: ['exitDone'],\n};\n","import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { states, stateClassNames } from './transitionStates';\n\nexport default class BrorTransitionTarget extends React.Component {\n static propTypes = {\n transitionState: PropTypes.arrayOf(PropTypes.oneOf(Object.values(states))),\n };\n\n getTransitionClassNames() {\n if (!this.classNames) {\n throw new Error(\n 'TransitionTarget instances must have a classNames property'\n );\n }\n\n const { transitionState } = this.props;\n\n return (\n (transitionState &&\n stateClassNames[transitionState]\n .map(className => this.classNames[className])\n .filter(Boolean)) ||\n null\n );\n }\n}\n","import * as popupAlignments from './alignments';\n\n/*\n * functions used to return the top and left position based on\n * certain alignments. if `test` is not true, the functions will\n * return false if the balloon tip would not fit. if test is true\n * the positioning will be pushed so the balloon tip is visible inside\n * the iframe, but not pointing exactly where requested\n * (note this only pushes top/bottom alignment up or down, and left/right\n * alignment left or right. further translation of the balloon tip content is\n * done further down if parts are still sticking out)\n */\nfunction getAlignment(\n alignment,\n { arrow = 6, target, size, spacing, margin, force }\n) {\n let top, left;\n\n switch (alignment) {\n case popupAlignments.TOP:\n // center the balloon tip horizontally relative to the target element\n left = target.left + target.width / 2 - size.width / 2;\n top = target.top - size.height - arrow - spacing;\n\n if (top < margin) {\n if (!force) {\n return false;\n }\n\n top = margin;\n }\n\n return { top, left };\n\n case popupAlignments.BOTTOM:\n // center the balloon tip horizontally relative to the target element\n left = target.left + target.width / 2 - size.width / 2;\n // place the balloon tip under the target element\n top = target.top + target.height + spacing;\n\n if (window.innerHeight - (top + size.height + arrow) < margin) {\n if (!force) {\n return false;\n }\n\n top = window.innerHeight - (size.height + arrow + spacing + margin);\n }\n\n return { top, left };\n\n case popupAlignments.LEFT:\n left = target.left - size.width - spacing - arrow;\n // center the balloon tip vertically relative to the target element\n top = target.top + target.height / 2 - size.height / 2;\n\n if (left < margin) {\n if (!force) {\n return false;\n }\n\n left = margin;\n }\n\n return { top, left };\n\n case popupAlignments.RIGHT:\n // place the balloon tip to the rightof the target element\n left = target.left + target.width + spacing;\n // center the balloon tip vertically relative to the target element\n top = target.top + target.height / 2 - size.height / 2;\n\n if (window.innerWidth - (left + size.width + arrow) < margin) {\n if (!force) {\n return false;\n }\n\n left = window.innerWidth - (size.width + arrow + spacing + margin);\n }\n\n return { top, left };\n\n default:\n throw new Error(`Unknown alignment '${alignment}'`);\n }\n}\n\nfunction setPosition(size, target, options) {\n const margin = options.margin || 0;\n const spacing = options.spacing || 0;\n\n /*\n * the order which to try different alignments, depending on what\n * alignment was passed as option\n * FIXME shorter name\n */\n const defaultAlignmentAttemptSequence = {\n [popupAlignments.TOP]: [\n popupAlignments.TOP,\n popupAlignments.BOTTOM,\n popupAlignments.RIGHT,\n popupAlignments.LEFT,\n ],\n [popupAlignments.BOTTOM]: [\n popupAlignments.BOTTOM,\n popupAlignments.TOP,\n popupAlignments.RIGHT,\n popupAlignments.LEFT,\n ],\n [popupAlignments.LEFT]: [\n popupAlignments.LEFT,\n popupAlignments.RIGHT,\n popupAlignments.TOP,\n popupAlignments.BOTTOM,\n ],\n [popupAlignments.RIGHT]: [\n popupAlignments.RIGHT,\n popupAlignments.LEFT,\n popupAlignments.TOP,\n popupAlignments.BOTTOM,\n ],\n };\n\n const sequence = Array.isArray(options.alignment)\n ? options.alignment\n : defaultAlignmentAttemptSequence[options.alignment];\n\n /*\n * reducer that loops through the alignments and returns the first alignment and top/left\n * position where the balloon tip fits. it will return false if no alignments would fit,\n * and thus the destructuring leads to alignment, top and left being undefined\n */\n let { alignment, top, left } = sequence.reduce((result, alignment) => {\n if (result) {\n return result;\n }\n\n const position = getAlignment(alignment, {\n target,\n size,\n spacing,\n margin,\n force: false,\n });\n\n return position && Object.assign({ alignment }, position);\n }, false);\n\n if (!alignment) {\n /*\n * if alignment is not set, no alignment that fits has been found.\n * force alignment of requested `options.aligment`\n */\n ({ top, left } = getAlignment(options.alignment, {\n target,\n size,\n spacing,\n margin,\n force: true,\n }));\n\n alignment = options.alignment;\n }\n\n /*\n * `options.offset` is only used by the vertical measure input balloon tip... since it flips\n * using css transform it's visual position is the same as its DOM position.\n * TODO make this account for aligment fixes and such\n */\n if (options.offset) {\n left += options.offset.left || 0;\n top += options.offset.top || 0;\n }\n\n const adjust = {\n top: '',\n left: '',\n };\n\n /*\n * the following if/else statement translates the .content-holder element inside\n * the balloon tip if it is still outside the iframe. this leaves the arrow in place.\n * translates top/bottom alignment left and right, and left/right alignment\n * up and down.\n */\n if (\n alignment === popupAlignments.TOP ||\n alignment === popupAlignments.BOTTOM\n ) {\n if (left < margin) {\n adjust.left = margin - left;\n } else if (window.innerWidth - (left + size.width) < margin) {\n adjust.left = window.innerWidth - (left + size.width) - margin;\n }\n } else {\n if (top < margin) {\n adjust.top = margin - top;\n } else if (window.innerHeight - (top + size.height) < margin) {\n adjust.top = window.innerHeight - (top + size.height) - margin;\n }\n }\n\n return {\n alignment,\n left,\n top,\n adjust,\n };\n}\n\nexport default setPosition;\n","import React, { createRef } from 'react';\nimport { connect } from 'react-redux';\nimport PropTypes from 'prop-types';\nimport classNames from 'classnames';\nimport Portal from '../utils/Portal';\nimport styles from './Outline.module.less';\nimport { POINTER_DOWN } from '../../util/supportedEvents';\nimport TransitionTarget from '../Transition/TransitionTarget';\nimport constants from '../../settings/constants';\nimport { selectIsMobilePortrait } from '../../state/userAgent/userAgentSelectors';\n\nclass Outline extends TransitionTarget {\n static propTypes = {\n className: PropTypes.string,\n isMobilePortrait: PropTypes.bool,\n targetRect: PropTypes.object,\n target: PropTypes.any,\n onClose: PropTypes.func,\n screensaverMode: PropTypes.string,\n };\n\n _root = createRef();\n classNames = styles;\n\n componentDidMount() {\n window.addEventListener(POINTER_DOWN, this.onPointerDown);\n }\n\n componentWillUnmount() {\n window.removeEventListener(POINTER_DOWN, this.onPointerDown);\n }\n\n onPointerDown = event => {\n if (this.props.screensaverMode !== constants.SCREENSAVER_MODE.countdown) {\n this.props.onClose && this.props.onClose();\n }\n };\n\n _getTargetPos = () =>\n this.props.targetRect || this.props.target.getBoundingClientRect();\n // eslint-disable-next-line no-dupe-class-members\n static propTypes = {};\n\n render() {\n const targetRect = this._getTargetPos();\n\n const padding = this.props.isMobilePortrait\n ? constants.OUTLINE_PADDING_MOBILE_PORTRAIT\n : constants.OUTLINE_PADDING;\n\n return (\n \n \n \n );\n }\n}\n\nexport default connect(state => ({\n screensaverMode: state.screensaver.mode,\n isMobilePortrait: selectIsMobilePortrait(state),\n}))(Outline);\n","export default {\n IDLE: 'idle',\n SLIDESHOW: 'slideshow',\n};\n","import { State } from '../StateTypes';\nimport { ScreensaverMode } from './screensaverTypes';\nimport screensaverModes from './screensaverModes';\n\n/**\n * Select screensaver state slice\n * @param screensaver\n */\nexport const selectScreensaverMode = ({\n screensaver,\n}: State): ScreensaverMode => screensaver.mode;\n\n/**\n * Checks if any screensaver is active\n *\n * @param state\n * @returns {boolean}\n */\nexport const selectScreensaverIsActive = (state: State): boolean =>\n selectScreensaverMode(state) !== screensaverModes.IDLE;\n","import React, { createRef } from 'react';\nimport { findDOMNode } from 'react-dom';\nimport PropTypes from 'prop-types';\nimport classNames from 'classnames';\nimport { connect } from 'react-redux';\nimport _ from 'lodash';\n\nimport emitter from '../../emitter';\nimport {\n IFRAME_RESIZED,\n CONF_MENU_CHANGE,\n SCREENSAVER_STARTED,\n} from '../../settings/events';\nimport { POINTER_DOWN } from '../../util/supportedEvents';\nimport { innerWidth } from '../../util/dom';\nimport Portal from '../utils/Portal';\nimport StopPropagation from '../utils/StopPropagation';\nimport TransitionTarget from '../Transition/TransitionTarget';\nimport { states } from '../Transition/transitionStates';\nimport platform from '../../util/platform';\n\nimport setPosition from './setPosition';\nimport * as alignments from './alignments';\nimport styles from './Popup.module.less';\nimport Outline from '../Outline';\nimport constants from '../../settings/constants';\nimport { selectScreensaverMode } from '../../state/screensaver/screensaverSelectors';\n\nconst supportsMaxContentWidth =\n typeof CSS === 'undefined' ? false : CSS.supports('width', 'max-content');\n\nclass Popup extends TransitionTarget {\n static propTypes = {\n alignment: PropTypes.oneOf(alignments.all),\n autoWidth: PropTypes.bool,\n calculateTargetRect: PropTypes.func,\n children: PropTypes.node.isRequired,\n className: PropTypes.string,\n dismissible: PropTypes.bool,\n autoUpdate: PropTypes.bool,\n margin: PropTypes.number,\n onClose: PropTypes.func,\n outlineTarget: PropTypes.bool,\n spacing: PropTypes.number,\n target: PropTypes.any,\n targetRect: PropTypes.object,\n timeout: PropTypes.number,\n wiggle: PropTypes.bool,\n maxLines: PropTypes.number,\n screensaverMode: PropTypes.string,\n ignorePadding: PropTypes.bool,\n arrowAlignment: PropTypes.string,\n };\n\n static defaultProps = {\n alignment: alignments.LEFT,\n dismissible: true,\n margin: 5,\n spacing: 10,\n autoWidth: false,\n };\n\n //\n // Local variables\n\n _root = createRef();\n _container = createRef();\n _contentHolder = createRef();\n\n classNames = styles;\n state = {\n shownOnce: false,\n hasCalculatedPosition: false,\n hasCalculatedOutline: false,\n alignment: null,\n };\n\n constructor(...args) {\n super(...args);\n\n this._calculateStyles = this._calculateStyles.bind(this);\n this._calculateOutline = this._calculateOutline.bind(this);\n this._onConfChanged = this._onConfChanged.bind(this);\n }\n\n componentDidMount() {\n // TODO set in proptypes that onClose needs to be set if timeout is set\n if (this.props.timeout && this.props.onClose) {\n setTimeout(this.props.onClose, this.props.timeout);\n }\n\n if (this.props.dismissible) {\n window.addEventListener(POINTER_DOWN, this.onPointerDown);\n }\n\n this._calculateStyles();\n\n if (this.props.autoWidth && !supportsMaxContentWidth) {\n this.observer = new MutationObserver(() => this._calculateStyles());\n\n this.observer.observe(this._container.current, {\n childList: true,\n subtree: true,\n });\n }\n\n emitter.on(IFRAME_RESIZED, this._calculateStyles);\n\n // on update give the browser some time to update so we get correct measurments\n emitter.on(CONF_MENU_CHANGE, this._onConfChanged);\n emitter.on(SCREENSAVER_STARTED, this.onPointerDown);\n }\n\n componentWillUnmount() {\n emitter.off(IFRAME_RESIZED, this._calculateStyles);\n emitter.off(CONF_MENU_CHANGE, this._onConfChanged);\n emitter.off(SCREENSAVER_STARTED, this.onPointerDown);\n\n if (this.props.dismissible) {\n window.removeEventListener(POINTER_DOWN, this.onPointerDown);\n }\n\n if (this.observer) {\n this.observer.disconnect();\n }\n }\n\n componentDidUpdate(prevProps) {\n const rect = this._findRect();\n if (!rect && !this.state.destroyed) {\n this.setState({\n destroyed: true,\n });\n }\n\n if (this.props.autoUpdate) {\n if (prevProps.content !== this.props.content) {\n this._calculateStyles();\n } else if (prevProps.transitionState === states.entered) {\n this._calcSize(rect);\n }\n }\n }\n\n //\n // Control\n\n _calcSize(rect) {\n const { alignment, margin, spacing } = this.props;\n\n if (this._container.current) {\n const root = findDOMNode(this._root.current);\n const contentHolder = this._contentHolder.current;\n\n root.style.transform = 'none';\n\n if (!this.props.autoWidth) {\n const startWidth = platform.isKiosk ? 340 : 240;\n const maxLines = this.props.maxLines || 3;\n const computedStyle = window.getComputedStyle(contentHolder);\n const originalPaddingString = computedStyle.getPropertyValue('padding');\n const originalPaddingLeftNum = parseInt(\n computedStyle.getPropertyValue('padding-left')\n );\n const originalPaddingRightNum = parseInt(\n computedStyle.getPropertyValue('padding-right')\n );\n const lineHeightNum = parseInt(\n computedStyle.getPropertyValue('line-height')\n );\n\n /*\n Set padding and icon (if there is one) height to zero,\n so that it won't conflict with the calculation\n */\n\n contentHolder.style.padding = 0;\n\n const iconElement = root.querySelector('.' + styles.icon);\n let originalIconHeight;\n\n if (iconElement) {\n originalIconHeight = iconElement.offsetHeight;\n iconElement.style.height = 0;\n }\n\n contentHolder.style.width = `${startWidth}px`;\n\n //Calculate the smallest width needed to fit the text content.\n const tightedTextContentWidth = this.getTightestPossibleWidth(\n contentHolder,\n lineHeightNum,\n maxLines\n );\n\n //Set new width, with added space for padding\n contentHolder.style.width =\n tightedTextContentWidth +\n originalPaddingLeftNum +\n originalPaddingRightNum +\n 'px';\n\n //Reset padding and icon height to their original values.\n contentHolder.style.padding = originalPaddingString;\n\n if (iconElement) {\n iconElement.style.height = originalIconHeight + 'px';\n }\n } else if (!supportsMaxContentWidth) {\n const computedStyle = window.getComputedStyle(contentHolder);\n const padding =\n parseInt(computedStyle.getPropertyValue('padding-left')) +\n parseInt(computedStyle.getPropertyValue('padding-right'));\n const border =\n parseInt(computedStyle.getPropertyValue('border-left-width')) +\n parseInt(computedStyle.getPropertyValue('border-right-width'));\n contentHolder.style.width = `${\n innerWidth(contentHolder.firstChild) + padding + border\n }px`;\n }\n\n root.style.transform = 'none';\n\n const size = contentHolder.getBoundingClientRect();\n\n root.style.transform = null;\n\n const result = setPosition(size, rect, {\n alignment,\n margin,\n spacing,\n });\n\n return result;\n }\n }\n\n getTightestPossibleWidth(element, lineHeight, maxLines) {\n const maxWidth = Math.round(0.7 * window.innerWidth);\n const minWidth = 10;\n\n function widenToFitAllText() {\n while (\n (element.clientHeight > lineHeight * maxLines ||\n element.scrollWidth > element.offsetWidth) &&\n element.offsetWidth <= maxWidth - 10\n ) {\n element.style.width = element.offsetWidth + 10 + 'px';\n }\n }\n function getTightenedWidth() {\n const currentHeight = element.clientHeight;\n let previousWidth;\n\n do {\n previousWidth = element.offsetWidth;\n element.style.width = element.offsetWidth - 2 + 'px';\n } while (\n element.clientHeight === currentHeight &&\n element.offsetWidth >= element.scrollWidth &&\n element.offsetWidth >= minWidth\n );\n\n return previousWidth;\n }\n\n widenToFitAllText();\n return getTightenedWidth();\n }\n\n _findRect() {\n const { calculateTargetRect, targetRect, target, ignorePadding } =\n this.props;\n\n let rect;\n if (targetRect) {\n rect = targetRect;\n } else if (calculateTargetRect) {\n rect = calculateTargetRect();\n } else if (target) {\n const DOMNode = target instanceof Element ? target : findDOMNode(target);\n rect = this._getBoundingClientRectClone(DOMNode);\n if (ignorePadding) {\n const computedStyle = window.getComputedStyle(DOMNode);\n rect.width -=\n parseInt(computedStyle.getPropertyValue('padding-left')) +\n parseInt(computedStyle.getPropertyValue('padding-right'));\n rect.left += parseInt(computedStyle.getPropertyValue('padding-left'));\n rect.height -=\n parseInt(computedStyle.getPropertyValue('padding-top')) +\n parseInt(computedStyle.getPropertyValue('padding-bottom'));\n rect.top += parseInt(computedStyle.getPropertyValue('padding-top'));\n }\n } else {\n throw Error(\n 'You need to pass target, targetRect or calculateTargetRect to Popup'\n );\n }\n return rect;\n }\n\n _getBoundingClientRectClone(element) {\n const rect = element.getBoundingClientRect();\n return {\n top: rect.top,\n right: rect.right,\n bottom: rect.bottom,\n left: rect.left,\n width: rect.width,\n height: rect.height,\n x: rect.x,\n y: rect.y,\n };\n }\n\n _onConfChanged() {\n this._calculateOutline();\n _.delay(() => this._calculateStyles(), 200);\n }\n\n // this has not been converted to getDerivedStateFromProps because we need reference to the ref\n _calculateStyles() {\n // Calculate the width\n if (this._container.current) {\n const rect = this._findRect();\n // Sprite was destroyed, don't bother\n if (!rect) {\n return;\n }\n const result = this._calcSize(rect);\n const style = {\n position: 'fixed',\n left: Math.round(result.left) + 'px',\n top: Math.round(result.top) + 'px',\n };\n\n const contentStyle = {\n left: result.adjust.left && result.adjust.left + 'px',\n top: result.adjust.top && result.adjust.top + 'px',\n };\n const outlineState = this._calculateOutline(rect);\n this.setState({\n ...outlineState,\n targetRect: rect,\n style,\n contentStyle,\n alignment: result.alignment,\n hasCalculatedPosition: true,\n });\n }\n }\n\n _calculateOutline(rect = null) {\n const state = {\n hasCalculatedOutline: true,\n };\n if (rect) {\n return state;\n }\n if (this._container.current) {\n rect = this._findRect();\n // Sprite was destroyed, don't bother\n if (!rect) {\n return;\n }\n state.targetRect = rect;\n this.setState(state);\n }\n }\n\n onPointerDown = event => {\n if (this.props.screensaverMode !== constants.SCREENSAVER_MODE.countdown) {\n this.props.onClose && this.props.onClose();\n }\n };\n\n render() {\n const {\n className,\n outlineTarget,\n children,\n onClose,\n transitionState,\n wiggle,\n arrowAlignment,\n } = this.props;\n\n const {\n alignment,\n contentStyle,\n hasCalculatedPosition,\n hasCalculatedOutline,\n style,\n targetRect,\n destroyed,\n } = this.state;\n\n if (destroyed) {\n return null;\n }\n\n return (\n \n \n \n \n \n {children}\n \n\n
\n
\n \n
\n\n {hasCalculatedOutline && outlineTarget && (\n \n )}\n
\n );\n }\n}\n\nexport default connect(state => ({\n screensaverMode: selectScreensaverMode(state),\n}))(Popup);\n","import React from 'react';\nimport bowser from 'bowser';\n\nimport { Transition } from 'react-transition-group';\nimport { TRANSITION_END } from '../../util/supportedEvents';\nimport {\n timeoutsShape,\n classNamesShape,\n} from 'react-transition-group/utils/PropTypes';\n\nimport { states, stateClassNames } from './transitionStates';\n\nexport const UNMOUNTED = 'unmounted';\n\nexport default class BrorTransition extends React.Component {\n static propTypes = {\n // eslint-disable-next-line react/forbid-foreign-prop-types\n ...Transition.propTypes,\n timeout: timeoutsShape,\n classNames: classNamesShape,\n };\n\n state = {\n transitionState: states.exited,\n };\n\n _reflowAndSetState = (node, transitionState) => {\n // eslint-disable-next-line no-unused-expressions\n node && node.scrollTop;\n\n this.setState({\n transitionState,\n });\n };\n\n addEndListener = (node, _done) => {\n let timeout;\n\n function done(e) {\n if (timeout) {\n timeout = null;\n\n clearTimeout(timeout);\n\n _done();\n }\n }\n\n const duration = parseFloat(\n window.getComputedStyle(node).getPropertyValue('transition-duration')\n );\n\n timeout = setTimeout(done, duration > 0 ? duration * 1100 : 0);\n\n node.addEventListener(TRANSITION_END, done, false);\n };\n\n onEnter = (node, isAppearing) => {\n this._reflowAndSetState(node, states.enter);\n };\n\n onEntering = (node, isAppearing) => {\n this._reflowAndSetState(node, states.entering);\n };\n\n onEntered = (node, isAppearing) => {\n this._reflowAndSetState(node, states.entered);\n };\n\n onExit = (node, isAppearing) => {\n this._reflowAndSetState(node, states.exit);\n };\n\n onExiting = (node, isAppearing) => {\n this._reflowAndSetState(node, states.exiting);\n };\n\n onExited = (node, isAppearing) => {\n this._reflowAndSetState(node, states.exited);\n };\n\n render() {\n const { children, ...props } = this.props;\n\n // No transitions for IE!\n if (props.in && bowser.msie) {\n return {this.props.children};\n }\n\n const child = React.Children.only(children);\n\n const { transitionState } = this.state;\n\n const childProps = {\n transitionState,\n };\n\n if (this.props.classNames) {\n const classNames = stateClassNames[transitionState]\n .map(state => this.props.classNames[state])\n .filter(Boolean);\n\n if (classNames.length) {\n childProps.className =\n (child.props.className || '') + classNames.join(' ');\n }\n }\n\n const clonedChild = React.cloneElement(child, childProps);\n\n delete props.classNames;\n\n return (\n \n {clonedChild}\n \n );\n }\n}\n","import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport ClassComponent from '../utils/ClassComponent';\n\nimport Popup from './Popup';\nimport BrorTransition from '../Transition';\n\nexport default class IntroPopup extends React.Component {\n static propTypes = {\n showPopup: PropTypes.bool,\n wiggle: PropTypes.bool,\n children: PropTypes.node,\n content: PropTypes.any.isRequired,\n querySelector: PropTypes.string,\n arrowAlignment: PropTypes.string,\n };\n\n _component = React.createRef();\n\n state = {\n hasRendered: false,\n target: null,\n };\n\n componentDidMount() {\n this._setHasRendered();\n }\n\n _getChild() {\n const child = React.Children.only(this.props.children);\n\n const isStateless =\n typeof child.type !== 'string' &&\n !React.Component.isPrototypeOf(child.type);\n\n if (isStateless) {\n return {child};\n }\n\n return React.cloneElement(child, { ref: this._component });\n }\n\n _setHasRendered() {\n const target = document.querySelector(this.props.querySelector);\n\n this.setState({\n hasRendered: true,\n target: target || this._component.current,\n });\n }\n\n _waitForComponent(delay = 200, tries = 20) {\n const component = this._component.current;\n\n let count = 0;\n\n const interval = setInterval(() => {\n if (component.dataset.rendered === 'true' || count > tries) {\n clearInterval(interval);\n\n this._setHasRendered();\n }\n\n count++;\n }, delay);\n }\n\n render() {\n const { showPopup, wiggle } = this.props;\n const { hasRendered, target } = this.state;\n\n return (\n \n {this._getChild()}\n {hasRendered && (\n \n \n {this.props.content}\n \n \n )}\n \n );\n }\n}\n","import React from 'react';\nimport PropTypes from 'prop-types';\nimport classNames from 'classnames';\nimport * as supportedEvents from '../../util/supportedEvents';\n\nexport default class TooltipTarget extends React.Component {\n static propTypes = {\n in: PropTypes.func.isRequired,\n out: PropTypes.func.isRequired,\n children: PropTypes.node,\n hover: PropTypes.bool,\n };\n\n render() {\n return (\n this.props.in()\n : null\n }\n onMouseEnter={\n supportedEvents.MOUSE_SUPPORT && !supportedEvents.TOUCH_SUPPORT\n ? event => this.props.in()\n : null\n }\n onPointerLeave={\n supportedEvents.POINTER_SUPPORT && !supportedEvents.TOUCH_SUPPORT\n ? this.props.out\n : null\n }\n onMouseLeave={\n supportedEvents.MOUSE_SUPPORT && !supportedEvents.TOUCH_SUPPORT\n ? this.props.out\n : null\n }\n >\n {this.props.children}\n \n );\n }\n}\n","import React from 'react';\nimport PropTypes from 'prop-types';\nimport IntroPopup from './IntroPopup';\nimport TooltipTarget from './TooltipTarget';\nimport platform from '../../util/platform';\n\nexport default class Tooltip extends React.Component {\n static propTypes = {\n children: PropTypes.node,\n };\n\n state = {\n hover: false,\n };\n\n constructor() {\n super();\n this.showTooltip = this.showTooltip.bind(this);\n this.hideTooltip = this.hideTooltip.bind(this);\n }\n\n showTooltip() {\n this.setState({ hover: true });\n }\n\n hideTooltip() {\n this.setState({ hover: false });\n }\n\n render() {\n if (platform.isKiosk || !this.props.content) {\n // this isn't a tooltip-target really but this makes the less less messy\n return
{this.props.children}
;\n }\n\n return (\n \n \n {this.props.children}\n \n \n );\n }\n}\n","import React from 'react';\nimport PropTypes from 'prop-types';\nimport styles from './CircleOption.module.less';\nimport classNames from 'classnames';\nimport { TOP } from '../Popup/alignments';\nimport Tooltip from '../Popup/Tooltip';\n\nexport default function CircleOption({\n selected,\n onClick,\n tooltipContent,\n name,\n type,\n disabled,\n dataTestId,\n}) {\n return (\n \n \n \n \n \n );\n}\n\nCircleOption.propTypes = {\n onClick: PropTypes.func.isRequired,\n name: PropTypes.string.isRequired,\n tooltipContent: PropTypes.string,\n selected: PropTypes.bool,\n type: PropTypes.string,\n disabled: PropTypes.bool,\n dataTestId: PropTypes.string,\n};\n","import {\n selectAvailableColors,\n selectCurrentFilterName,\n selectCurrentSubFilterIndex,\n selectFilterSelectedColor,\n} from './productMenuSelectors';\nimport {\n actionSetSwiperSubFilter,\n actionSetColor,\n actionSetFilter,\n} from './productMenuActions';\nimport { Dispatch } from 'redux';\nimport { GetState } from '../../generalTypes';\nimport { ColorCombination } from './productMenuTypes';\n\nexport const thunkSetFilter =\n (filterName: string) => (dispatch: Dispatch, getState: GetState) => {\n const state = getState();\n const previousFilter = selectCurrentFilterName(state);\n const previousColor = selectFilterSelectedColor(state, previousFilter);\n dispatch(actionSetFilter(filterName));\n dispatch(actionSetSwiperSubFilter(selectCurrentSubFilterIndex(state)));\n // @ts-ignore\n dispatch(thunkSetDefaultColorFilter(filterName, previousColor));\n };\n\n/**\n * Sets sub filter and updates swiper items\n *\n * @param subFilterIndex\n * @returns void\n */\nexport const thunkSetSubFilter =\n (subFilterIndex: number) => (dispatch: Dispatch, getState: GetState) => {\n dispatch(actionSetSwiperSubFilter(subFilterIndex));\n };\n\n/**\n * Finds and sets default color filter\n *\n * @param filterName - which filter is being targeted\n * @param previousColor - If there were a previous color selected for perhaps a previous\n * filter and this color is available for this filter as well,\n * said color will be selected as default.\n */\nconst thunkSetDefaultColorFilter =\n (filterName: string, previousColor?: ColorCombination | undefined) =>\n (dispatch: any, getState: GetState) => {\n const availableColors = selectAvailableColors(getState());\n\n if (availableColors && Object.values(availableColors).length) {\n const previousColorIsValid =\n previousColor && availableColors.hasOwnProperty(previousColor.name);\n const [{ name: defaultColor }] = Object.values(availableColors);\n\n previousColorIsValid\n ? dispatch(actionSetColor(previousColor.name, filterName))\n : dispatch(actionSetColor(defaultColor, filterName));\n }\n };\n\n/**\n * Sets color filter and updates swiper items\n * @param color\n * @returns void\n */\nexport const thunkSetColorFilter =\n (color: string) => (dispatch: Dispatch, getState: GetState) => {\n dispatch(actionSetColor(color, selectCurrentFilterName(getState())));\n };\n","import React from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport CircleOption from '../CircleOption';\nimport { translate } from '../../services/L10n';\nimport { t } from '../../translations';\nimport styles from './ProductMenuColorFilter.module.less';\nimport {\n selectCurrentColorFilterName,\n selectProductMenuFilter,\n selectSelectableColorsAsArray,\n} from '../../state/productMenu/productMenuSelectors';\nimport { selectIsLandscape } from '../../state/userAgent/userAgentSelectors';\nimport { thunkSetColorFilter } from '../../state/productMenu/productMenuThunks';\nimport { ColorCombination } from '../../state/productMenu/productMenuTypes';\nimport { KompisText } from '@inter-ikea-kompis/react-components';\nimport { SkapaTheme } from '@inter-ikea-kompis/themes/lib';\nimport localStatisticsReporter from '../../services/statistics/insights/custom/local/localStatisticsReporter';\n\nexport const ProductMenuColorFilter = () => {\n const currentColorFilterName = useSelector(selectCurrentColorFilterName);\n const currentFilter = useSelector(selectProductMenuFilter);\n const selectableColors = useSelector(selectSelectableColorsAsArray);\n const isLandscape = useSelector(selectIsLandscape);\n\n const dispatch = useDispatch();\n const setColorFilter = (color: ColorCombination) => () => {\n dispatch(thunkSetColorFilter(color.name));\n localStatisticsReporter.reportProductMenuSubFilterInteraction(\n currentFilter.name,\n 'color',\n color.name\n );\n };\n\n const tooltips: { [key: string]: string } = {\n white: translate(t.TOOLTIP_WHITE),\n wood: translate(t.TOOLTIP_WOOD),\n oak: translate(t.TOOLTIP_OAK_PATTERNED),\n black: translate(t.TOOLTIP_BLACK),\n anthracite: translate(t.TOOLTIP_ANTHRACITE),\n grey: translate(t.TOOLTIP_GREY),\n grey_oak: `${translate(t.TOOLTIP_GREY)}/${translate(\n t.TOOLTIP_OAK_PATTERNED\n )}`,\n grey_green: translate(t.TOOLTIP_GREY_GREEN),\n black_wood: `${translate(t.TOOLTIP_WOOD)}/${translate(t.TOOLTIP_BLACK)}`,\n dark_grey: translate(t.TOOLTIP_DARK_GREY),\n bamboo: translate(t.TOOLTIP_BAMBOO),\n };\n\n /**\n * Render header label\n * @returns {false|JSX.Element}\n */\n const renderHeaderLabel = () =>\n isLandscape && (\n {`${translate(t.COLOUR)}:`}\n );\n\n /**\n * Render selectable colors\n */\n const renderSelectableColors = () =>\n selectableColors.map(color => (\n \n ));\n\n return (\n
\n {renderHeaderLabel()}\n
\n {renderSelectableColors()}\n
\n
\n );\n};\n\nexport default ProductMenuColorFilter;\n","import { State } from '../../state/StateTypes';\nimport {\n selectCurrentColorFilter,\n selectFilterHasSelectableColors,\n selectCurrentSubFilterKey,\n selectCurrentSubFilterValue,\n selectProductMenuFilter,\n} from '../../state/productMenu/productMenuSelectors';\nimport ProductMenuSubFilter from '../../components/ProductMenu/ProductMenuSubFilter';\nimport store from '../../state';\nimport ProductMenuColorFilter from '../../components/ProductMenu/ProductMenuColorFilter';\nimport { FilterExtension } from './productsServiceTypes';\n\n/**\n * Filters items by sub filter\n * @param state\n */\nexport const subFilter = (state: State) => {\n const subFilterKey = selectCurrentSubFilterKey(state);\n const subFilterValue = selectCurrentSubFilterValue(state);\n\n return ({ product: { filter } }: any) =>\n subFilterKey && subFilterValue > -1\n ? filter?.[subFilterKey] === subFilterValue\n : true;\n};\n\n/**\n * Filters items by color\n * @param state\n */\nexport const filterProductsByColor = (state: State) => {\n const { colors } = selectProductMenuFilter(state);\n const currentColorFilter = selectCurrentColorFilter(state);\n const hasCurrentColorFilter =\n currentColorFilter && Object.keys(currentColorFilter).length > 0;\n\n return ({ product }: any) => {\n return colors && hasCurrentColorFilter\n ? product?.filter &&\n currentColorFilter.colors?.some(\n (color: any) => product.filter.color === color\n )\n : true;\n };\n};\n\n/**\n * Returns filter object for subfilter\n */\nexport const subFilterObject = (filterExtension: FilterExtension) => {\n const { condition, appliesTo } = {\n condition: true,\n appliesTo: [],\n ...filterExtension,\n };\n\n return {\n Component: ProductMenuSubFilter,\n condition: () => {\n const currentFilter = selectProductMenuFilter(store.getState());\n return !!currentFilter.subFilter && condition;\n },\n filter: subFilter,\n appliesTo,\n };\n};\n\n/**\n * Returns filter object for colors\n */\nexport const colorFilterObject = (filterExtension: FilterExtension) => {\n const { condition, appliesTo } = {\n condition: true,\n appliesTo: [],\n ...filterExtension,\n };\n\n return {\n Component: ProductMenuColorFilter,\n condition: () =>\n selectFilterHasSelectableColors(store.getState()) && condition,\n filter: filterProductsByColor,\n appliesTo,\n };\n};\n","import productsServiceCommon, { generateProppingImageResources } from '../';\nimport { getProppingBounds } from '../models';\nimport { FILTERS, ITEMS, MISSING_PRODUCT_CATEGORIES } from '../../../constants';\nimport { selectRangeDataSlice } from '../../../state/rangeData/rangeDataSelectors';\nimport { ELVARLI_VARIANT } from '../../../components/Sheets/SelectElvarliVariantSheet/SelectElvarliVariantSheet';\nimport {\n selectFilterItems,\n selectProductMenuFilter,\n selectFilterHasSelectableColors,\n selectCurrentColorFilter,\n} from '../../../state/productMenu/productMenuSelectors';\nimport store from '../../../state';\nimport { subFilter, subFilterObject } from '../itemsFilters';\nimport settings from '../../../settings/constants';\nimport ProductMenuSubFilter from '../../../components/ProductMenu/ProductMenuSubFilter';\nimport ProductMenuColorFilter from '../../../components/ProductMenu/ProductMenuColorFilter';\nimport { getItemPrice } from '../../../state/tac/tacHelpers';\n\nconst articleImages = [];\n\nconst LEFT = 'left';\nconst RIGHT = 'right';\n\nfunction getArticleImage(id) {\n return articleImages.find(image => image.name === id);\n}\n\nfunction getDragMode(product) {\n return settings.DRAG_MODE.FLOAT;\n}\n\nfunction isClothesRail(id) {\n return productsServiceCommon.isType(id, ITEMS.CLOTHES_RAIL);\n}\n\nfunction isBracket(id) {\n return productsServiceCommon.isType(id, ITEMS.BRACKET);\n}\n\nfunction hasMissingBrackets(product) {\n const elvarliVariant = selectRangeDataSlice(ELVARLI_VARIANT)(\n store.getState()\n );\n if (elvarliVariant === ITEMS.SECTION_SIDE_UNITS) return false;\n\n // We only consider left brackets, since those are the ones connected to IOWS.\n const brackets = ['00317527_L', '00296172_L'];\n\n const getParts = product =>\n product?.parts ? Object.values(product.parts) : [];\n const getPartsRecursively = product => {\n const parts = getParts(product);\n const recursivelyFound = parts.reduce((alreadyFound, part) => {\n const product = productsServiceCommon.getProduct(part);\n return [\n ...alreadyFound,\n ...(product ? getPartsRecursively(product) : []),\n ];\n }, []);\n return [...parts, ...recursivelyFound];\n };\n\n const parts = getPartsRecursively(product);\n const bracketsNeeded = parts.filter(part => brackets.includes(part));\n const bracketsMissing = bracketsNeeded.filter(\n id => !productsServiceCommon.getProduct(id)\n );\n\n return !!bracketsMissing.length;\n}\n\nfunction getFit(item, slot) {\n let fits;\n switch (item.filter.type) {\n case ITEMS.SECTION_SIDE_UNITS:\n case ITEMS.SECTION_POSTS:\n case ITEMS.SIDE_PANEL:\n fits = productsServiceCommon.getFilteredItems(\n product => productsServiceCommon.isType(product, item.filter.type),\n {\n width: item.width,\n depth: item.depth,\n height: slot.height,\n }\n );\n break;\n case ITEMS.SHELF:\n fits = productsServiceCommon.getFilteredItems(\n productsServiceCommon.isShelf,\n {\n width: slot.width,\n depth: slot.depth,\n color: slot.color || item.filter.color,\n }\n );\n break;\n case ITEMS.DRAWER:\n fits = productsServiceCommon.getFilteredItems(\n productsServiceCommon.isDrawer,\n {\n width: slot.width,\n depth: slot.depth,\n }\n );\n break;\n case ITEMS.CLOTHES_RAIL:\n fits = productsServiceCommon.getFilteredItems(isClothesRail, {\n width: slot.width,\n depth: slot.depth,\n });\n break;\n case ITEMS.BRACKET:\n fits = productsServiceCommon.getFilteredItems(isBracket, {\n id: item.id,\n });\n break;\n default:\n fits = productsServiceCommon.getFilteredItems(\n other =>\n productsServiceCommon.isType(\n other,\n slot.filter ? slot.filter.type : item.filter.type\n ),\n {\n color: slot.color || item.filter.color,\n width: slot.width,\n depth: slot.depth,\n }\n );\n break;\n }\n\n if (fits?.length) {\n return Object.assign({}, fits[0]);\n }\n}\n\n/**\n * Gets a matching shelf for a specific section\n * @param {*} section\n * @returns {Object | undefined}\n */\nconst getFittingShelf = section => {\n const WIDTH_DIFF = 44;\n const DEPTH_DIFF = 40;\n const match = productsServiceCommon.getShelves({\n width: section.width - WIDTH_DIFF,\n depth: section.depth - DEPTH_DIFF,\n })?.[0];\n\n return match ? { ...match } : null;\n};\n\n/**\n * Gets a matching shelf for a specific section, the cheapest one.\n * @param {*} section\n * @returns {Object | undefined}\n */\nexport function getCheapestFittingShelf(section) {\n const WIDTH_DIFF = 44;\n const DEPTH_DIFF = 40;\n const match = productsServiceCommon\n .getShelves({\n width: section.width - WIDTH_DIFF,\n depth: section.depth - DEPTH_DIFF,\n })\n ?.reduce(\n (acc, curr) => {\n const itemPrice = getItemPrice(curr);\n if (itemPrice <= acc.price) {\n return {\n item: curr,\n price: itemPrice,\n };\n }\n return acc;\n },\n { item: {}, price: Infinity }\n );\n\n return match.price !== Infinity ? match.item : null;\n}\n\nfunction makeArticleImages() {\n const imageList = productsServiceCommon.getAll().map(product => {\n const { color } = product.filter;\n\n const sceneFileName = `${product.modelid}_${color}_0001.png`;\n const menuFileName = `${product.modelid}_${color}_t_0001.png`;\n\n const hasPortraitSpecificThumb = false;\n return {\n name: product.id,\n color: color,\n type: product.filter.type,\n modelid: product.modelid,\n proppingBounds: getProppingBounds(product),\n url: !productsServiceCommon.isSection(product)\n ? `${settings.IMAGE_ROOT}${sceneFileName}`\n : `${settings.IMAGE_ROOT}thumbnails/${menuFileName}`, // FIXME\n thumbnailUrl: `${settings.IMAGE_ROOT}thumbnails/${menuFileName}`,\n thumbnailUrlPortrait: hasPortraitSpecificThumb\n ? `${settings.IMAGE_ROOT}thumbnails/portrait/${menuFileName}`\n : null,\n };\n });\n\n articleImages.push(...generateProppingImageResources(imageList));\n}\n\nfunction isInsert(item) {\n return productsServiceCommon.isType(item, [\n ITEMS.SHELF,\n ITEMS.DRAWER,\n ITEMS.SHELF_DRAWER,\n ITEMS.SHELF_CLOTHES_RAIL,\n ITEMS.SHOE_SHELF,\n ]);\n}\n\nfunction isSection(item) {\n return productsServiceCommon.isType(item, [\n ITEMS.SECTION_SIDE_UNITS,\n ITEMS.SECTION_POSTS,\n ]);\n}\n\nexport function isSectionPosts(item) {\n return productsServiceCommon.isType(item, ITEMS.SECTION_POSTS);\n}\n\nfunction getFittingBracket(item, side) {\n const bracketItems = [ITEMS.SHELF, ITEMS.SHELF_CLOTHES_RAIL, ITEMS.DRAWER];\n const itemShouldHaveBracket = bracketItems.some(itemType =>\n productsServiceCommon.isType(item, itemType)\n );\n\n let bracketId;\n if (itemShouldHaveBracket && item.depth === 360 && side === LEFT) {\n bracketId = '00317527_L';\n } else if (itemShouldHaveBracket && item.depth === 360 && side === RIGHT) {\n bracketId = '00317527_R';\n } else if (itemShouldHaveBracket && item.depth === 510 && side === LEFT) {\n bracketId = '00296172_L';\n } else if (itemShouldHaveBracket && item.depth === 510 && side === RIGHT) {\n bracketId = '00296172_R';\n } else bracketId = null;\n\n const bracketProduct = bracketId\n ? productsServiceCommon.getProduct(bracketId)\n : null;\n\n return bracketProduct;\n}\n\nfunction getInitialZPos(item) {\n if (isSection(item)) {\n return 5;\n }\n return 0;\n}\n\nfunction shouldHaveColorSelector(item) {\n if (productsServiceCommon.isType(item, ITEMS.SHELF_DRAWER)) {\n return item.items.length === Object.values(item.parts).length;\n }\n return productsServiceCommon.isType(item, [\n ITEMS.SHELF,\n ITEMS.SHELF_CLOTHES_RAIL,\n ]);\n}\n\nconst getPostWidth = () => {\n const state = store.getState();\n const elvarliVariant = selectRangeDataSlice(ELVARLI_VARIANT)(state);\n\n return elvarliVariant\n ? settings.POST_WIDTH[elvarliVariant]\n : settings.POST_WIDTH[ITEMS.SECTION_POSTS];\n};\n\nfunction shouldIgnoreFloorAndCeilingCollision(item) {\n return (\n item.boundsType === 'space' &&\n isInsert(item) &&\n !item.items?.length &&\n !productsServiceCommon.isType(item, ITEMS.SHELF_CLOTHES_RAIL) // Ugly fix for a propping based item (only has item.items as propping and not actual items).\n );\n}\n\nconst sectionPostFilter = state => {\n const elvarliVariant = selectRangeDataSlice(ELVARLI_VARIANT)(state);\n return ({ product }) => product.filter.type === elvarliVariant?.toLowerCase();\n};\n\nexport const selectHasPostsAndSections = state => {\n const filterItems = selectFilterItems(state, FILTERS.SECTIONS).map(\n ({ filter: { type } }) => type\n );\n\n const hasBoth = [ITEMS.SECTION_SIDE_UNITS, ITEMS.SECTION_POSTS].reduce(\n (acc, curr) => acc && filterItems.some(id => id.includes(curr)),\n true\n );\n\n return hasBoth;\n};\n\nexport const selectAvailableElvarliVariants = state => {\n const sections = selectFilterItems(state, FILTERS.SECTIONS).map(\n ({ filter: { type } }) => type\n );\n\n return [...new Set(sections)];\n};\n\nexport const isntPostVariant = () =>\n !(\n selectRangeDataSlice(ELVARLI_VARIANT)(store.getState()) === 'section-posts'\n );\n\nconst sectionPostFilterObject = () => ({\n Component: () => null,\n condition: () => !!selectRangeDataSlice(ELVARLI_VARIANT)(store.getState()),\n filter: sectionPostFilter,\n appliesTo: [FILTERS.SECTIONS],\n});\n\n// SE COMMENTS INSIDE 'getFilters' BELOW\n// eslint-disable-next-line no-unused-vars\nconst sideUnitsSubFilterObject = () => ({\n Component: ProductMenuSubFilter,\n condition: () => {\n const currentFilter = selectProductMenuFilter(store.getState());\n const elvarliVariant = selectRangeDataSlice(ELVARLI_VARIANT)(\n store.getState()\n );\n\n return (\n !!currentFilter.subFilter && elvarliVariant === ITEMS.SECTION_SIDE_UNITS\n );\n },\n filter: subFilter,\n appliesTo: [FILTERS.SECTIONS],\n});\n\n/**\n * Removes freestanding clothes rail\n * See {@link getFilters} for docs on filters structure.\n * @returns object\n */\nconst removeClothesRailFilterObject = () => ({\n Component: () => null,\n condition: () => {\n const elvarliVariant = selectRangeDataSlice(ELVARLI_VARIANT)(\n store.getState()\n );\n return elvarliVariant === ITEMS.SECTION_SIDE_UNITS;\n },\n filter:\n state =>\n ({ product: { id } }) =>\n !['40296212', '00296214'].includes(id),\n appliesTo: [FILTERS.PARTS],\n});\n\n/**\n * Removes articles for which the needed brackets are not available.\n * See {@link getFilters} for docs on filters structure.\n * @returns object\n */\nconst removeArticlesWithUnavailableBracketsFilterObject = () => ({\n Component: () => null,\n condition: () => {\n const elvarliVariant = selectRangeDataSlice(ELVARLI_VARIANT)(\n store.getState()\n );\n return elvarliVariant === ITEMS.SECTION_POSTS;\n },\n filter:\n state =>\n ({ product }) =>\n !hasMissingBrackets(product),\n appliesTo: [FILTERS.PARTS],\n});\n\n/**\n * Include shoeshelf and clothesrail no matter what color is has so it is always included in all color choices\n * @returns object\n */\nconst includeShoeshelfAndClothesrailFilterObject = () => ({\n Component: ProductMenuColorFilter,\n condition: () => selectFilterHasSelectableColors(store.getState()),\n filter: state => {\n const { colors } = selectProductMenuFilter(state);\n const currentColorFilter = selectCurrentColorFilter(state);\n\n const hasCurrentColorFilter =\n currentColorFilter && Object.keys(currentColorFilter).length > 0;\n\n const productIdIsShoeshelfOrClothesrail = productId =>\n ['10317292', '50317290', '40296212', '00296214'].includes(productId);\n\n return ({ product }) => {\n if (productIdIsShoeshelfOrClothesrail(product.id)) return true;\n return colors && hasCurrentColorFilter\n ? product?.filter &&\n currentColorFilter.colors?.some(\n color => product.filter.color === color\n )\n : true;\n };\n },\n appliesTo: [FILTERS.PARTS],\n});\n\n/**\n * See {@link getFilters} for docs on filters structure.\n * @type {[{filter: (function(*=): function({product: *}): boolean), condition: (function(): boolean), Component: () => JSX.Element}]}\n */\nconst getFilters = () => [\n sectionPostFilterObject(),\n removeClothesRailFilterObject(),\n removeArticlesWithUnavailableBracketsFilterObject(),\n /**\n * TODO: THIS SHOULD BE REINSTATED ONCE ELVARLI POST'S DEPTH CAN BE\n * SET BY IT'S INSERTS. 'sideUnitsSubFilterObject', SHOULD BE USED,\n * WHILE 'subFilterObject' SHOULD BE REMOVE AGAIN.\n */\n //sideUnitsSubFilterObject(),\n subFilterObject({ appliesTo: [FILTERS.SECTIONS] }),\n includeShoeshelfAndClothesrailFilterObject(),\n];\n\nconst getMissingMandatoryProductCategories = () => {\n const sections = productsServiceCommon.getFilteredItems(item =>\n isSection(item)\n );\n return sections.length === 0 ? [MISSING_PRODUCT_CATEGORIES.SECTION] : [];\n};\n\nexport default {\n articleImages,\n getArticleImage,\n getDragMode,\n hasMissingBrackets,\n getFit,\n getFittingShelf,\n getInitialZPos,\n isInsert,\n isSection,\n getFittingBracket,\n makeArticleImages,\n shouldHaveColorSelector,\n shouldIgnoreFloorAndCeilingCollision,\n getPostWidth,\n getFilters,\n getMissingMandatoryProductCategories,\n};\n","import _ from 'lodash';\nimport { replace } from '../replace';\nimport products from '../../../services/products';\nimport tacHelpers from '../tacHelpers';\nimport addItem from './addItem';\nimport geometry from '../../../scene/util/geometry';\nimport { applicationSettings } from '../../../settings/application';\nimport { isSectionPosts } from '../../../services/products/elvarli';\nimport { ITEMS, RANGES } from '../../../constants';\n\nfunction getSlotsModel(model, parent, options) {\n if (options.forceParts) {\n // We want to connect parts no matter what, so only include the parent\n return {\n ...model,\n items: model.items.filter(item => item.itemid === parent.itemid),\n };\n }\n\n if (options.isTopLevel) {\n // Only items colliding with our parent are interesting when connecting parts on the top level\n return {\n ...model,\n items: model.items.filter(item => geometry.collides(item, parent)),\n };\n }\n\n return model;\n}\n\nfunction getPartItem(part, parent, model, options) {\n if (options.partsToSkip?.find(partId => partId === part)) {\n return;\n }\n const partProduct = products.getProduct(part);\n if (!partProduct) {\n return;\n }\n const partIsConnectedToCeiling = part => part.logic.ceiling;\n const getSectionHeight = section => {\n return section.height;\n };\n const getSectionHeightAndPartDiff = (partProduct, section) => {\n return getSectionHeight(section) - partProduct.height;\n };\n\n const slotsModel = getSlotsModel(model, parent, options);\n\n const slots = tacHelpers.getOpenSlots(slotsModel, partProduct, {\n noVariants: true,\n proposedParent: parent,\n });\n const currentItemSlot =\n slots && slots.find(slot => slot.parent.itemid === parent.itemid);\n\n const offset =\n currentItemSlot &&\n tacHelpers.getGlobalCoords(currentItemSlot.parent, model);\n return (\n offset && {\n ...partProduct,\n x: currentItemSlot.x - offset.x,\n y: partIsConnectedToCeiling(partProduct)\n ? getSectionHeightAndPartDiff(partProduct, parent)\n : currentItemSlot.y - offset.y,\n z: currentItemSlot.z - offset.z,\n }\n );\n}\n\nexport default function connectParts(\n newItem,\n newParent,\n oldParent,\n model,\n options\n) {\n options = {\n ...options,\n isTopLevel: !tacHelpers.isItem(newParent),\n };\n\n Object.values(newItem.parts).forEach(part => {\n let tmpModel;\n if (oldParent === model) {\n tmpModel = newParent;\n } else {\n const clone = _.cloneDeep(model);\n replace(clone.items, newParent);\n tmpModel = clone;\n }\n newItem = tacHelpers.getItem(tmpModel, newItem.itemid);\n\n if (isSectionPosts(newItem)) {\n const getWallHeight = () => {\n return model.wall.size.height;\n };\n const getNewItemWithWallHeight = () => {\n return {\n ...newItem,\n height: getWallHeight(),\n };\n };\n newItem = getNewItemWithWallHeight();\n }\n\n const partItem = getPartItem(part, newItem, tmpModel, options);\n\n const shouldAddPart = () => {\n if (applicationSettings.applicationName !== RANGES.ELVARLI) return true;\n const isSectionSideUnit = item =>\n products.isType(item, ITEMS.SECTION_SIDE_UNITS);\n const isBracket = item => products.isType(item, ITEMS.BRACKET);\n if (\n isSectionSideUnit(model) ||\n (isSectionSideUnit(newParent) && isBracket(partItem))\n )\n return false;\n return true;\n };\n\n if (partItem && shouldAddPart()) {\n newParent = addItem(newParent, partItem, newItem, options);\n }\n });\n return newParent;\n}\n","import geometry from '../../../scene/util/geometry';\nimport idGenerator from '../../../util/aactools/idGenerator';\nimport makeSpaceForChild from './makeSpaceForChild';\nimport _ from 'lodash';\n\nimport sorter from './sorter';\nimport { replace } from '../replace';\nimport connectParts from './connectParts';\nimport { getBoundedItem } from '../../../services/products/models';\nimport tacHelpers from '../tacHelpers';\n\n// ITEMS\nexport default function addItem(model, item, parent = model, options = {}) {\n if (idGenerator.hasRealId(item)) {\n throw new Error(\n 'You are trying to add an already existing item to the TAC!'\n );\n }\n\n const collidingItems = parent.items\n ? geometry.getCollidingRects(\n getBoundedItem(item, 'collision'),\n parent.items\n .map(sibling => getBoundedItem(sibling, 'collision'))\n .filter(\n sibling =>\n !tacHelpers.shouldIgnoreCollision(item, parent, sibling) &&\n geometry.contains(parent, sibling)\n )\n )\n : [];\n\n if (collidingItems.length > 0) {\n return false;\n }\n\n item.itemid = options.isPersistent ? idGenerator.id() : idGenerator.fakeId();\n\n if (!item.items) {\n item.items = [];\n }\n\n let newParent = {\n ...parent,\n items: parent.items\n ? sorter.inDrawOrder(parent.items.concat(item))\n : [item],\n };\n\n if (item.parts) {\n const newItem = newParent.items.find(\n parentItem => parentItem.itemid === item.itemid\n );\n newParent = connectParts(newItem, newParent, parent, model, options);\n }\n makeSpaceForChild(item, newParent, model);\n if (parent === model) {\n return newParent;\n } else {\n const out = _.cloneDeep(model);\n replace(out.items, newParent);\n return out;\n }\n}\n","import products from '../../../../services/products';\nimport addItem from '../../../tac/tacReducer/addItem';\nimport geometry from '../../../../scene/util/geometry';\nimport { flatten } from '../../../../util/array';\nimport {\n getMatchingConnections,\n fitToPoint,\n} from '../../../../services/products/models';\nimport removeItem from '../../tacReducer/removeItem';\nimport { ITEMS } from '../../../../constants';\n\nfunction bracketsAdded(tac, rebracketedUprights) {\n return rebracketedUprights\n .map(upright => ({\n ...upright,\n items: upright.items.filter(item => !item.itemid),\n }))\n .filter(upright => upright.items.length);\n}\n\nfunction bracketsRemoved(updTac, rebracketedUprights) {\n const oldUprights = updTac.items.filter(item =>\n products.isType(item.id, ITEMS.UPRIGHT)\n );\n const remainingBrackets = flatten(\n rebracketedUprights.map(upright => upright.items)\n ).filter(item => products.isType(item.id, 'bracket'));\n return oldUprights\n .map(upright => ({\n ...upright,\n items: upright.items.filter(\n item =>\n !remainingBrackets.find(bracket => bracket.itemid === item.itemid)\n ),\n }))\n .filter(upright => upright.items.length);\n}\n\nexport function fitBracket(upright, gItem, bracket) {\n const localY =\n gItem.y -\n upright.y +\n (!products.isType(gItem, ITEMS.TABLE) ? gItem.height : 0);\n const conns = getMatchingConnections(bracket, { items: [upright] })[0]\n .connections;\n const fittedBrackets = conns.map(conn => fitToPoint(bracket, conn, 'size'));\n\n const bestBracket = fittedBrackets\n .sort((a, b) => (a.y > b.y ? -1 : a.y < b.y ? 1 : 0))\n .find(bracket => bracket.y < localY);\n\n if (!bestBracket) {\n // nothing remotely usable\n return null;\n }\n\n if (Math.abs(bestBracket.y - localY) > bestBracket.height) {\n // not even the best bracket can be used!\n return null;\n }\n return bestBracket;\n}\n\n// determine if a colliding item should have a bracket on upright\nfunction properBracketPos(item, upright) {\n const closeEnough = 20; //mm\n const uprightx = upright.x + upright.width / 2;\n const leftDiff = Math.abs(item.x - uprightx);\n const rightDiff = Math.abs(item.x + item.width - uprightx);\n if (rightDiff > closeEnough && leftDiff > closeEnough) {\n // this upright is not where a bracket should be.\n return false;\n }\n return true;\n}\n\n/*\nreturns a list of all uprights that changed compared to the ones in tac\n */\nfunction addAllBrackets(tac) {\n const gAllBracketItems = flatten(\n tac.items.map(parent =>\n parent.items.map(child =>\n Object.assign({}, child, {\n x: child.x + parent.x,\n y: child.y + parent.y,\n z: child.z + parent.z,\n })\n )\n )\n ).filter(item => products.getFittingBracket(item));\n const initialUprights = tac.items.filter(item =>\n products.isType(item.id, ITEMS.UPRIGHT)\n );\n const newUprights = initialUprights.map(upright =>\n Object.assign({}, upright, { items: [] })\n );\n\n newUprights.forEach(upright => {\n upright.items = gAllBracketItems\n .filter(\n item =>\n geometry.collides(item, upright, geometry.uniformPadding(1)) &&\n properBracketPos(item, upright)\n )\n .filter(coll => coll.itemid !== upright.itemid)\n .map(item => {\n const bracket =\n products.getFittingBracket(item, undefined, item.filter?.color) ||\n products.getFittingBracket(item, undefined, upright.filter?.color) ||\n products.getFittingBracket(item);\n if (bracket) {\n const fitted = fitBracket(upright, item, bracket);\n // linnmon's brackets are below it, so might not fit even though we collide\n return fitted\n ? {\n ...bracket,\n x: (upright.width - bracket.width) / 2,\n y: fitted.y,\n z: fitted.z,\n }\n : null;\n }\n return null;\n })\n .filter(Boolean);\n });\n // ignore uprights where the recalculated brackets were already present\n const updatedUprights = newUprights;\n\n // remove any recreated brackets\n updatedUprights.forEach(upright => {\n const oldUpright = initialUprights.find(\n iupright => iupright.itemid === upright.itemid\n );\n upright.items = upright.items.map(\n bracket =>\n oldUpright.items.find(\n item =>\n item.y === bracket.y && item.filter?.color === bracket.filter?.color\n ) || bracket\n );\n let uprightItemsWithoutDuplicates = [];\n upright.items.forEach(bracket => {\n const alreadyAddedBracketAtSamePosition =\n uprightItemsWithoutDuplicates.find(\n alreadyAddedBracket => alreadyAddedBracket.y === bracket.y\n );\n if (alreadyAddedBracketAtSamePosition) {\n if (\n alreadyAddedBracketAtSamePosition.filter?.color ===\n bracket.filter?.color ||\n alreadyAddedBracketAtSamePosition.filter?.color ===\n upright.filter?.color\n )\n return;\n else {\n uprightItemsWithoutDuplicates = uprightItemsWithoutDuplicates.filter(\n alreadyAddedBracket =>\n alreadyAddedBracket !== alreadyAddedBracketAtSamePosition\n );\n }\n }\n uprightItemsWithoutDuplicates.push(bracket);\n });\n upright.items = uprightItemsWithoutDuplicates;\n });\n\n return updatedUprights;\n}\n\nexport function causedBracketChanges(tac) {\n if (!tac) {\n throw new Error('causedBracketChanges requires all params');\n }\n\n const out = {\n added: [],\n removed: [],\n };\n\n const rebracketedUprights = addAllBrackets(tac);\n\n out.added = bracketsAdded(tac, rebracketedUprights);\n out.removed = bracketsRemoved(tac, rebracketedUprights);\n return out;\n}\n\n/*\n these extra functions are just to avoid\n rewriting all the tacs I had in my test spec, since previously\n I asked about what would happen, rather than the current case,\n where I ask what did happen.\n\n*/\n\n// adds an item to tac before asking about brackets\nfunction add(tac, item, parent) {\n const updTac = addItem(tac, item, parent);\n const rebracketedUprights = addAllBrackets(updTac);\n return {\n add: bracketsAdded(updTac, rebracketedUprights),\n remove: [],\n };\n}\n\n// removes an item from tac before asking about brackets\nfunction remove(tac, item, parent) {\n const updTac = removeItem(tac, item, parent);\n const rebracketedUprights = addAllBrackets(updTac);\n return {\n add: [],\n remove: bracketsRemoved(updTac, rebracketedUprights),\n };\n}\n\nexport const test = {\n addAllBrackets,\n add,\n remove,\n};\n","import _ from 'lodash';\n\nimport productService, {\n getFittingBracket,\n} from '../../../../services/products';\nimport geometry from '../../../../scene/util/geometry';\nimport { config as asConfig } from '../../../../scene/boaxel/AdjustableConfig';\nimport tacHelpers from '../../tacHelpers';\nimport constants from '../../../../settings/constants';\nimport getMountingRails from './mountingRail';\nimport { causedBracketChanges, fitBracket } from './brackets';\nimport { flatten, unique } from '../../../../util/array';\nimport { ITEMS } from '../../../../constants';\n\nconst sectionWidthsCache = [];\n\nfunction getIntactUprights(old, curr, enforceColor = false) {\n return old.uprights.filter(oldUpright =>\n curr.uprights.find(\n upright =>\n upright.itemid === oldUpright.itemid &&\n (!enforceColor || upright.filter?.color === oldUpright.filter?.color)\n )\n );\n}\n\nfunction hasAllUprightsIntact(old, curr, enforceColor = false) {\n const intactUprights = getIntactUprights(old, curr, enforceColor);\n return (\n intactUprights.length === old.uprights.length &&\n intactUprights.length === curr.uprights.length\n );\n}\n\nfunction hasIntactUprightsOnBothSides(old, curr) {\n return (\n getIntactUprights(old, curr).reduce((acc, curr) => {\n if (!acc.length || !acc.find(prev => prev.x === curr.x)) {\n acc.push(curr);\n }\n return acc;\n }, []).length >= 2\n );\n}\n\nfunction sameX(a, b) {\n return a.x === b.x;\n}\n\nfunction sameY(a, b) {\n return a.y === b.y;\n}\n\nfunction sameWidth(a, b) {\n return a.width === b.width;\n}\n\nfunction sameHeight(a, b) {\n return a.height === b.height;\n}\n\nfunction totallyIntact(old, curr) {\n return (\n (!old.uprights || hasAllUprightsIntact(old, curr, true)) &&\n sameX(old, curr) &&\n sameY(old, curr) &&\n sameWidth(old, curr) &&\n sameHeight(old, curr)\n );\n}\n\nfunction sameRightmostPos(a, b) {\n return a.x + a.width === b.x + b.width;\n}\n\nfunction hasMovedOneGridStepInX(a, b) {\n const gridStep = constants.DYNAMIC_GRID[constants.DRAG_MODE.FLOAT].x.step;\n return [gridStep, -gridStep].some(\n stepDistance =>\n a.x - b.x === stepDistance &&\n a.x + a.width - stepDistance === b.x + b.width\n );\n}\n\nfunction hasMovedAdjustableRangeInX(old, curr) {\n const maxMove = asConfig.section.maxWidth - asConfig.section.minWidth;\n const move = Math.abs(old.x - curr.x);\n return move <= maxMove;\n}\n\nfunction hasMovedAdjustableToFixedRangeInX(old, curr, prio) {\n if (!prio) {\n return false;\n }\n const move = Math.abs(old.x - curr.x);\n return (\n [600, 800].some(width => width === prio.width + move) ||\n prio.width - move === asConfig.section.minWidth\n );\n}\n\nfunction wasConsequentlyMoved(old, curr, prio) {\n return (\n hasMovedOneGridStepInX(old, curr) ||\n hasMovedAdjustableRangeInX(old, curr) ||\n hasMovedAdjustableToFixedRangeInX(old, curr, prio)\n );\n}\n\nfunction wasResized(old, curr) {\n if (!old.logic.extendable) {\n return false;\n }\n if (\n (old.y === curr.y && old.x > curr.x && old.width < curr.width) ||\n (old.x < curr.x && old.width > curr.width)\n ) {\n return true;\n }\n}\n\nfunction partiallyIntact(old, curr, prio) {\n return (\n (!old.uprights || hasIntactUprightsOnBothSides(old, curr)) &&\n ((old.height > 0 && geometry.collides(old, curr)) || sameY(old, curr)) &&\n (sameX(old, curr) ||\n sameRightmostPos(old, curr) ||\n wasResized(old, curr) ||\n wasConsequentlyMoved(old, curr, prio))\n );\n}\n\nfunction findOldSections(sectionsInTac, curr, prio) {\n if (sectionsInTac.find(old => totallyIntact(old, curr))) {\n return [];\n }\n return sectionsInTac.filter(old => partiallyIntact(old, curr, prio));\n}\n\nfunction findOldSectionToUpdate(sectionsInTac, curr, prio) {\n if (\n prio &&\n geometry.collides(prio, curr) &&\n hasIntactUprightsOnBothSides(prio, curr)\n ) {\n //If our current section shares at least two uprights with our prio, we want to update that section.\n return prio;\n }\n const oldSections = findOldSections(sectionsInTac, curr, prio);\n // if were only setting height, then look at same place\n const unmovedSection = oldSections.find(\n cand => cand.x === curr.x && cand.y === curr.y && cand.width === curr.width\n );\n if (unmovedSection) {\n return unmovedSection;\n }\n const oldSection =\n oldSections.length && geometry.closestCollidingRect(curr, oldSections, 1);\n return oldSection;\n}\n\nfunction belongsToSection(gItem, newSection, allSections, tac) {\n const padding = geometry.uniformPadding(1);\n\n const gBrackets = newSection.uprights\n .filter(unique)\n .map(up => tacHelpers.getItem(tac, up.itemid))\n .map(upright => {\n const bracket = fitBracket(upright, gItem, getFittingBracket(gItem));\n if (!bracket) {\n return null;\n }\n const gBracket = {\n ...bracket,\n x: bracket.x + upright.x,\n y: bracket.y + upright.y,\n z: bracket.z + upright.z,\n };\n return gBracket;\n })\n .filter(Boolean);\n\n if (gBrackets.length !== 2) {\n // all items need two brackets\n return false;\n }\n\n const gBracket = geometry.closest(\n gBrackets.filter(gBracket => {\n return geometry.collides(gItem, gBracket, padding);\n }),\n gItem\n );\n\n if (newSection.y + newSection.height < gBracket?.y + gBracket?.height) {\n // the closest fitting bracket would be in an invalid place\n return false;\n }\n\n const collider = {\n ...gItem,\n y: gBracket?.y || gItem.y,\n height: 1,\n };\n\n const candidates = allSections.filter(\n section =>\n newSection.depth === section.depth && geometry.collides(section, gItem)\n );\n\n if (candidates.length < 2) {\n return geometry.collides(newSection, collider);\n }\n\n if (geometry.closestCollidingRect(collider, candidates) === newSection) {\n // this is where it belongs\n return true;\n }\n\n // consider items that are half a snab distance narrower than a section to be part of it\n const maxDiff = constants.DYNAMIC_GRID[constants.DRAG_MODE.FLOAT].x.step / 2;\n return (\n gItem.width + maxDiff > newSection.width &&\n geometry.contains(newSection, collider, padding)\n );\n}\n\nfunction fitsInSectionY(gItem, newSection) {\n return newSection.uprights.every(upright =>\n upright.items.every(bracket => bracket.y >= 0)\n );\n}\n\nfunction fitsInSectionX(gItem, newSection) {\n return sameWidth(gItem, newSection);\n}\n\nfunction getInvalidItems(newSection, allSections, allChildren, tac) {\n const invalidItems = [];\n for (let i = 0; i < allChildren.length; i++) {\n const child = allChildren[i];\n const gItem = tacHelpers.getGlobalCoords(child, tac);\n const belongs = belongsToSection(gItem, newSection, allSections, tac);\n const fits =\n fitsInSectionX(gItem, newSection) && fitsInSectionY(gItem, newSection);\n\n if (!belongs || !fits) {\n invalidItems.push({\n ...child,\n gy: gItem.y,\n gx: gItem.x,\n keepDuringDrag: true,\n });\n }\n }\n\n return invalidItems;\n}\n\nfunction removeDuplicateItems(targetArray, others) {\n const washed = [];\n [...targetArray].forEach(section => {\n section.items = section.items.filter(\n item =>\n !item.keepDuringDrag ||\n !others.some(\n section =>\n section.items.length &&\n section.items.find(\n other => other.itemid === item.itemid && !other.keepDuringDrag\n )\n )\n );\n washed.push(section);\n });\n\n return washed;\n}\nfunction validDistance(distance) {\n const availableWidths = sectionWidthsCache.length\n ? sectionWidthsCache\n : productService.getSections().map(section => section.width);\n\n if (!sectionWidthsCache.length) {\n sectionWidthsCache.push(...availableWidths);\n }\n\n if (availableWidths.some(width => width === distance)) {\n return true;\n }\n if (\n availableWidths.find(width => width === asConfig.section.minWidth) &&\n distance >= asConfig.section.minWidth &&\n distance <= asConfig.section.maxWidth\n ) {\n return true;\n }\n return false;\n}\n\nfunction getAllValidUprightConnections(uprights) {\n const connections = [];\n // Get all connections of uprights placed at a valid distance from each other\n for (let i = 0; i < uprights.length; i++) {\n for (let j = i; j < uprights.length; j++) {\n const distance = Math.abs(uprights[i].x - uprights[j].x);\n if (validDistance(distance)) {\n const connectingRect = geometry.connectingRect(\n uprights[i],\n uprights[j]\n );\n\n if (connectingRect.height > 0 && connectingRect.width > 0) {\n connectingRect.uprights = [uprights[i], uprights[j]];\n connections.push(connectingRect);\n }\n }\n }\n }\n\n return connections;\n}\n\nfunction mergeSections(unmerged) {\n const sectionRects = [...unmerged];\n\n // Merge same-width sections with no vertical gap in-between\n let i = sectionRects.length;\n while (i--) {\n let j = i;\n while (j--) {\n const potentialMerge =\n i !== j && sectionRects[i].width === sectionRects[j].width;\n if (potentialMerge) {\n const intersection = geometry.connectingRect(\n sectionRects[i],\n sectionRects[j]\n );\n const mergeable =\n intersection.height === 0 &&\n [sectionRects[i].width, sectionRects[j].width].every(\n width => width === intersection.width\n );\n const merged =\n mergeable &&\n geometry.mergeSiblings(sectionRects[i], sectionRects[j], true);\n if (merged) {\n merged.uprights = [\n ...sectionRects[i].uprights,\n ...sectionRects[j].uprights,\n ];\n sectionRects.splice(i, 1);\n sectionRects.splice(j, 1, merged);\n i--;\n }\n }\n }\n }\n\n return sectionRects;\n}\n\nfunction getMainCollisions(section, collisions) {\n return collisions.reduce((acc, curr) => {\n if (acc.contained) {\n return acc;\n }\n const collisionRect = geometry.connectingRect(curr, section);\n const contained =\n sameHeight(curr, collisionRect) &&\n geometry.contains(section, collisionRect);\n\n if (contained) {\n return { ...acc, contained: collisionRect };\n } else if (\n collisionRect.y === section.y &&\n (!acc.bottom || collisionRect.height > acc.bottom.height)\n ) {\n return {\n ...acc,\n bottom: collisionRect,\n };\n } else if (\n collisionRect.y + collisionRect.height === section.y + section.height &&\n (!acc.top || collisionRect.height > acc.top.height)\n ) {\n return {\n ...acc,\n top: collisionRect,\n };\n }\n\n return acc;\n }, {});\n}\n\nfunction splitSection(section, rects, index, collisionRect) {\n const sections = [...rects];\n const split = [{ ...section }, { ...section }];\n\n split[0].height = collisionRect.y - split[0].y;\n split[1].y = collisionRect.y + collisionRect.height;\n split[1].height -= split[0].height + collisionRect.height;\n\n sections.splice(index, 1);\n sections.push(split[0], split[1]);\n return sections;\n}\n\nfunction splitSections(rects, uprightsInTac) {\n let i = rects.length;\n while (i--) {\n const section = rects[i];\n if (section.height <= 0) {\n continue;\n }\n const otherUprights = uprightsInTac.filter(\n upright =>\n !section.uprights\n .map(upright => upright.itemid)\n .some(itemid => itemid === upright.itemid)\n );\n\n // 800 section is 820\n if (section.width > 900) {\n // large sections are allowed to overlap, both smaller and each other\n continue;\n }\n // If this section collides with other upright inside it, we need to adjust the section accordingly\n const collisions = geometry.getCollidingRects(section, otherUprights);\n if (!collisions.length) {\n continue;\n }\n\n const mainCollisions = getMainCollisions(section, collisions);\n\n if (mainCollisions.contained) {\n const collisionRect = mainCollisions.contained;\n rects = splitSection(section, rects, i, collisionRect);\n i = rects.length;\n } else {\n if (mainCollisions.top) {\n section.height -= mainCollisions.top.height;\n }\n\n if (mainCollisions.bottom) {\n section.y += mainCollisions.bottom.height;\n section.height -= mainCollisions.bottom.height;\n }\n }\n }\n\n return rects.map(section => {\n return {\n ...section,\n uprights: section.uprights\n .map(upright =>\n tacHelpers.getItem({ items: uprightsInTac }, upright.itemid)\n )\n .filter(upright =>\n geometry.collides(section, upright, { front: 10, back: 10 })\n ),\n };\n });\n}\nfunction getSlotSources(item, tac) {\n return item.uprights\n ? item.uprights\n .map(upright => tacHelpers.getItem(tac, upright.itemid))\n .filter(upright => upright.x < item.x)\n : [];\n}\n\nfunction getSectionProducts(rects) {\n const uprightDepth = 20;\n\n return rects.map(sectionRect => {\n const { x, y, width, height, uprights } = sectionRect;\n const fullSection = {\n ...productService.getFit(\n { filter: { type: ITEMS.SECTION } },\n {\n width: width - productService.getPostWidth(),\n }\n ),\n x: x + productService.getPostWidth() / 2,\n y,\n z: uprightDepth,\n height,\n uprights,\n items: [],\n };\n\n return fullSection;\n });\n}\n\nfunction getUntouchedSections(sectionsInTac, sections) {\n return (\n sectionsInTac.filter(oldSection =>\n sections.find(newSection => totallyIntact(oldSection, newSection))\n ) || []\n );\n}\n\nfunction getNewSections(sectionsInTac, sections, prio) {\n return (\n sections.filter(\n newSection =>\n !sectionsInTac.find(\n oldSection =>\n totallyIntact(oldSection, newSection) ||\n partiallyIntact(oldSection, newSection, prio)\n )\n ) || []\n );\n}\n\nfunction getUpdatedSections(\n sectionsInTac,\n sections,\n priorityItem,\n tac,\n isPersistent\n) {\n return (\n sections\n .filter(newSection =>\n findOldSectionToUpdate(sectionsInTac, newSection, priorityItem)\n )\n .map(newSection => {\n const oldSection = findOldSectionToUpdate(\n sectionsInTac,\n newSection,\n priorityItem\n );\n const slotSources = getSlotSources(newSection, tac);\n\n // update items in sections\n const oldSections = findOldSections(\n sectionsInTac,\n newSection,\n priorityItem\n ).sort((a, b) => a.y - b.y);\n\n const allChildren = flatten(oldSections.map(section => section.items));\n\n const keepAll =\n priorityItem &&\n wasConsequentlyMoved(oldSection, newSection, priorityItem);\n const myChildren = keepAll\n ? allChildren\n : allChildren\n .map(item => {\n const gItem = tacHelpers.getGlobalCoords(item, tac);\n const belongs = belongsToSection(\n gItem,\n newSection,\n sections,\n tac\n );\n const fits = fitsInSectionY(gItem, newSection);\n\n return (\n belongs && fits && { ...item, y: gItem.y - newSection.y }\n );\n })\n .filter(Boolean);\n\n newSection.items =\n tacHelpers.getNewChildren(\n { items: myChildren },\n newSection,\n {\n type: ITEMS.SECTION,\n switchingProp: 'width',\n width: newSection.width,\n },\n { slotSources }\n ) || [];\n\n if (!isPersistent) {\n // get articles from oldSection that are now not fitting in new section\n const invalidItems = getInvalidItems(\n newSection,\n sections,\n allChildren.filter(\n child =>\n !newSection.items.some(item => item.itemid === child.itemid)\n ),\n tac\n );\n\n // add them to new section because they are going to be dimmed\n invalidItems.forEach(item => {\n const updatedItem = {\n ...item,\n y: item.gy - newSection.y,\n x: item.gx - newSection.x,\n };\n newSection.items.push(updatedItem);\n });\n }\n\n return {\n ...oldSection,\n ...newSection,\n };\n }) || []\n );\n}\n\nfunction getDuplicateSections(updated) {\n let duplicates = [];\n if (updated.length > 1) {\n const uniques = _.uniqBy(updated, 'itemid');\n if (uniques.length !== updated.length) {\n duplicates = updated.filter(\n item => !uniques.find(unique => unique === item)\n );\n }\n }\n return duplicates;\n}\n\nfunction getRemovedSections(sectionsInTac, untouched, added, updated) {\n return (\n sectionsInTac\n .filter(\n oldSection =>\n ![...untouched, ...added, ...updated].some(\n section => section.itemid === oldSection.itemid\n )\n )\n .map(section => {\n return {\n ...section,\n items: section.items.map(item => ({ ...item, keepDuringDrag: true })),\n keepDuringDrag: true,\n };\n }) || []\n );\n}\n\nfunction addSectionDiff(tac, options, diff) {\n const { priorityItem, isPersistent } = options;\n const sectionsInTac = tacHelpers.getSections(tac);\n const uprightsInTac = tac.items.filter(\n item =>\n productService.isType(item, ITEMS.UPRIGHT) &&\n tacHelpers.isWithinWall(item, tac)\n );\n let sectionRects = getAllValidUprightConnections(uprightsInTac);\n\n //Sort sections to prepare them for merge\n sectionRects.sort((a, b) => a.x - b.x || a.width - b.width || a.y - b.y);\n\n sectionRects = mergeSections(sectionRects);\n sectionRects = splitSections(sectionRects, uprightsInTac);\n\n //Remove any invalid sections still left\n sectionRects = sectionRects.filter(rect => rect.height > 0);\n\n const sections = getSectionProducts(sectionRects);\n\n const untouched = getUntouchedSections(sectionsInTac, sections);\n const added = getNewSections(sectionsInTac, sections, priorityItem);\n\n let updated = getUpdatedSections(\n sectionsInTac,\n sections,\n priorityItem,\n tac,\n isPersistent\n );\n\n const keepers = [...untouched, ...added, ...updated];\n updated = removeDuplicateItems(updated, keepers);\n\n const duplicates = getDuplicateSections(updated);\n\n if (duplicates.length) {\n updated = updated.filter(\n item => !duplicates.find(duplicate => duplicate === item)\n );\n added.push(\n ...duplicates.map(duplicate => {\n return { ...duplicate, itemid: null };\n })\n );\n }\n\n let removed = getRemovedSections(sectionsInTac, untouched, added, updated);\n removed = removeDuplicateItems(removed, keepers);\n\n diff.added.push(...added);\n diff.updated.push(...updated);\n diff.removed.push(...removed);\n diff.untouched.push(...untouched);\n\n return diff;\n}\n\nfunction addMountingRailDiff(tac, diff) {\n //Remove all mounting rails, since these will be re-added\n const removed = [\n ...tac.items.filter(item => productService.isType(item, 'mounting-rail')),\n ];\n\n const added = [];\n if (\n productService.areMountingRailsValid() &&\n tac.wall &&\n !tac.settings.disableMountingRails\n ) {\n added.push(...getMountingRails(tac));\n }\n\n diff.added.push(...added);\n diff.removed.push(...removed);\n\n return diff;\n}\n\nfunction addBracketDiff(tac, diff) {\n const bracketDiff = causedBracketChanges(tac);\n\n bracketDiff.added.forEach(upright => {\n const brackets = upright.items\n .filter(item => productService.isType(item, 'bracket'))\n .map(bracket => ({ ...bracket, parentRef: upright.itemid }));\n diff.added.push(...brackets);\n });\n\n bracketDiff.removed.forEach(upright => {\n const brackets = upright.items\n .filter(item => productService.isType(item, 'bracket'))\n .map(bracket => ({ ...bracket, parentRef: upright.itemid }));\n diff.removed.push(...brackets);\n });\n\n return diff;\n}\n\nfunction getDependencyDiff(tac, options = {}) {\n let diff = { added: [], updated: [], removed: [], untouched: [] };\n\n if (\n options.triggerItem &&\n (productService.isInsert(options.triggerItem) ||\n productService.isType(options.triggerItem, ITEMS.CLOTHES_RAIL))\n ) {\n if (!options.keepBrackets) {\n diff = addBracketDiff(tac, diff);\n }\n } else {\n diff = addSectionDiff(tac, options, diff);\n const validSections = [...diff.untouched, ...diff.added, ...diff.updated];\n\n const modifiedTac = {\n ...tac,\n items: [...tac.items.filter(item => !productService.isSection(item))],\n };\n\n modifiedTac.items.push(...validSections);\n\n diff = addMountingRailDiff(modifiedTac, diff);\n\n if (!options.keepBrackets) {\n diff = addBracketDiff(modifiedTac, diff);\n }\n }\n\n return diff;\n}\n\nfunction getDependentItems(item, tac) {\n if (productService.isType(item, ITEMS.UPRIGHT)) {\n return tac.items.filter(item =>\n productService.isType(item, ['mounting-rail', ITEMS.SECTION])\n );\n }\n return [];\n}\n\nexport default { getDependencyDiff, getDependentItems, getSlotSources };\n\nexport { getDependencyDiff, getDependentItems, getSlotSources };\n","import geometry from '../../../../scene/util/geometry';\nimport { unique } from '../../../../util/array';\nimport { isType } from '../../../../services/products';\nimport tacHelpers from '../../tacHelpers';\nimport { mergePolygons } from '../../../../util/mergePolygons';\nimport { ITEMS } from '../../../../constants';\n\nfunction digSuperSection(tac, member, skiplist) {\n // pad items just a little so they overlap a bit with stuff they touch\n const padding = {\n right: 4,\n left: 4,\n front: 10,\n back: 10,\n };\n\n const superSection = [member];\n\n const candidates = tac.items.filter(\n item =>\n item.itemid !== member.itemid && skiplist.indexOf(item.itemid) === -1\n );\n\n const collisions = candidates.filter(candidate =>\n geometry.collides(member, candidate, padding)\n );\n let skip = skiplist\n .concat(collisions.map(item => item.itemid))\n .concat(superSection.map(item => item.itemid))\n .filter(unique);\n\n const rest = [];\n collisions.forEach(collision => {\n const dug = digSuperSection(tac, collision, skip);\n Array.prototype.push.apply(rest, dug.items);\n skip = skip.concat(dug.skip).filter(unique);\n });\n\n return {\n skip,\n items: Array.prototype.concat.apply(superSection, rest).filter(unique),\n };\n}\n\nfunction findSuperSection(tac, member, skiplist) {\n return digSuperSection(tac, member, skiplist).items;\n}\n\nfunction isSuperSectionHandle(item) {\n return isType(item, ITEMS.SECTION);\n}\n\nfunction getSuperSectionSpace(tac, superSection) {\n const wallPoly = {\n regions: [tac.wall.points.map(coords => [coords.x, coords.y])],\n inverted: true,\n };\n\n const ssItemIds = tacHelpers\n .getAllItems(superSection)\n .map(item => item.itemid);\n const rest = tacHelpers.filterTac(tac, ssItemIds);\n if (!rest.items.length) {\n //supersection is alone on scene\n return wallPoly;\n }\n\n const rects = tacHelpers.getRects(rest, null, tac, false);\n const withHeight = rects.map(rect => ({\n ...rect,\n height: rect.height || tacHelpers.getItem(tac, rect.itemid).height,\n }));\n const itemsPoly = geometry.rects2poly(withHeight);\n\n return mergePolygons(itemsPoly, wallPoly);\n}\n\nexport default {\n findSuperSection,\n isSuperSectionHandle,\n getSuperSectionSpace,\n};\n","import productService from '../../../../services/products';\nimport geometry from '../../../../scene/util/geometry';\nimport tacHelpers from '../../tacHelpers';\nimport constants from '../../../../settings/constants';\nimport { config as asConfig } from '../../../../scene/boaxel/AdjustableConfig';\n\nimport {\n getDependencyDiff,\n getDependentItems,\n getSlotSources,\n} from './dependentItems';\nimport { causedBracketChanges } from './brackets';\nimport superSection from './supersection';\nimport { getCuttableMountingRailData } from './mountingRail';\nimport { unique } from '../../../../util/array';\nimport { ITEMS } from '../../../../constants';\n\nconst BRACKET_HEIGHT = 80;\n\nfunction hasBlockingTable(slot, sections, tac) {\n return sections.some(section => {\n if (\n tacHelpers.hasTable(section) &&\n geometry.collides(section, slot, { bottom: 1000 })\n ) {\n const table = section.items.find(item =>\n productService.isType(item, ITEMS.TABLE)\n );\n const globalTable = tacHelpers.getGlobalCoords(table, tac);\n return globalTable.y > slot.y;\n }\n return false;\n });\n}\n\nfunction hasBlockingInsert(slot, sections, tac) {\n return sections.some(section => {\n if (\n !productService.fitsTable(section) &&\n geometry.collides(section, slot)\n ) {\n return section.items.some(item => {\n const gItem = tacHelpers.getGlobalCoords(item, tac);\n return gItem.y < slot.y;\n });\n }\n return false;\n });\n}\n\nfunction validTableHeight(slot) {\n return slot.y >= asConfig.legs.minHeight && slot.y <= asConfig.legs.maxHeight;\n}\n\nfunction slotFitsSection(slot, section) {\n if (section.width === slot.width) {\n return true;\n }\n if (section.logic.extendable) {\n return (\n section.width >= asConfig.section.minWidth &&\n section.width <= asConfig.section.maxWidth &&\n slot.width >= asConfig.section.minWidth &&\n slot.width <= asConfig.section.maxWidth\n );\n }\n return false;\n}\n\nfunction filterSlots(slots, tac) {\n const sections = tacHelpers.getSections(tac);\n slots = slots\n .filter(slot => {\n if (productService.isInsert(slot)) {\n return sections.some(section => {\n // if the slot fits inside a section, it's ok\n // padding since sections are between uprights, and stuff that goes in sections extend a bit over them\n // and remove top and bottom since we don't want to allow stuff positioned above right rail\n const padding = geometry.uniformPadding(\n productService.getPostWidth()\n );\n padding.top = 0;\n padding.bottom = 0;\n return (\n geometry.contains(\n section,\n {\n ...slot,\n height: productService.isTable(slot)\n ? slot.height + BRACKET_HEIGHT\n : BRACKET_HEIGHT, // to not filter out items that stretches outside\n y: slot.y + slot.height - BRACKET_HEIGHT,\n x: slot.x + productService.getPostWidth(),\n },\n padding\n ) && slotFitsSection(slot, section)\n );\n });\n }\n return true;\n // since lagkapten sections are at the same spot as other sections,\n // multiple sections share the same upright, thus yielding duplicate slots.\n })\n .filter(unique);\n\n slots\n .filter(slot => slot.logic.extendable)\n .forEach(slot => {\n const parentid = slot.parent.itemid;\n const section = sections\n .filter(\n section =>\n productService.isExtendable(section) &&\n geometry.contains(section, slot)\n )\n .find(\n section =>\n section.uprights.find(upright => upright.itemid === parentid) ||\n section.items.find(item => item.itemid === parentid)\n );\n\n slot.width = section ? section.width : slot.parent.width;\n });\n\n slots = slots.filter(slot => {\n if (productService.isType(slot, ITEMS.TABLE)) {\n return validTableHeight(slot) && !hasBlockingInsert(slot, sections, tac);\n }\n return !hasBlockingTable(slot, sections, tac);\n });\n\n return slots;\n}\n\nfunction getProppingItemsToAdapt(tac, movingItem, previous, original) {\n return tacHelpers.getClothesRails(tac).filter(cr => {\n if (!movingItem) {\n return true;\n }\n const globalCr = tacHelpers.getGlobalCoords(cr, tac);\n const padding = {\n top: constants.ROOM_MAX.height,\n bottom: constants.ROOM_MAX.height,\n front: constants.ROOM_DEPTH,\n back: constants.ROOM_DEPTH,\n };\n return [movingItem, previous, original].some(\n item => item && geometry.collides(globalCr, item, padding)\n );\n });\n}\n\nfunction filterVariants(tac, variants) {\n const sections = tacHelpers\n .getAllItems(tac.items)\n .filter(productService.isSection);\n return variants.filter(variant => {\n if (\n productService.isShelf(variant) &&\n productService.isExtendable(variant)\n ) {\n return sections.some(productService.isExtendable);\n }\n return true;\n });\n}\n\n/**\n * Filters out those of an item's variants for which there is no section of\n * suitable width in the TAC. This is only a prescreening and it does not\n * consider other aspects such as whether the sections are full.\n * (If the variants are not inserts, they will not be filtered out regardless\n * of their width.)\n * @param {Array} variants The variants to be filtered.\n * @param {object} tac The TAC.\n * @returns {Array} The remaining variants that were not filtered out.\n */\nfunction preScreenSlots(variants, tac) {\n const availableSectionWidths = tacHelpers\n .getSections(tac)\n .map(section => section.width)\n .filter(unique);\n\n return variants.filter(\n variant =>\n !productService.isInsert(variant) ||\n availableSectionWidths.find(\n width => Math.abs(variant.width - width) <= 100\n )\n );\n}\n\nconst api = {\n filterSlots,\n filterVariants,\n getDependencyDiff,\n getDependentItems,\n getSlotSources,\n causedBracketChanges,\n getProppingItemsToAdapt,\n superSection,\n preScreenSlots,\n getCuttableMountingRailData,\n};\n\nObject.assign(api, superSection);\n\nexport default api;\n","const baseConfig = { maxWidth: 900, minWidth: 570, mountOffset: 0 };\n\nexport default function (item) {\n switch (item.filter.color) {\n case 'white':\n return {\n ...baseConfig,\n innerId: 'clothes_rail_88_36_3_white_in',\n outerId: 'clothes_rail_88_36_3_white_out',\n fullId: '30460934',\n };\n case 'dark_grey':\n return {\n ...baseConfig,\n innerId: 'clothes_rail_88_36_3_dark_grey_in',\n outerId: 'clothes_rail_88_36_3_dark_grey_out',\n fullId: '10460949',\n };\n default:\n break;\n }\n}\n","export default {\n maxDepth: 1040,\n minDepth: 300,\n};\n","import { applicationSettings } from '../../settings/application';\nimport { config as JonaxelCrConfig } from '../jonaxel/ClothesRailConfig';\nimport getAurdalClothesRailConfig from '../aurdal/getClothesRailConfig';\nimport IvarTableConfig from '../ivar/TableConfig';\nimport { ITEMS } from '../../constants';\n\n/**\n * Gets a config object for specific items (e.g. extendables)\n *\n * @param {Object} item The item in need of a config\n * @returns {Object} The found config for the item, or undefined if no config\n */\nexport default function getItemConfig(item) {\n switch (item.filter.type) {\n case ITEMS.CLOTHES_RAIL: {\n switch (applicationSettings.applicationName) {\n case 'JONAXEL':\n return JonaxelCrConfig;\n case 'AURDAL':\n return getAurdalClothesRailConfig(item);\n default:\n break;\n }\n break;\n }\n case ITEMS.TABLE: {\n switch (applicationSettings.applicationName) {\n case 'IVAR':\n return IvarTableConfig;\n default:\n break;\n }\n break;\n }\n default:\n break;\n }\n}\n","import _ from 'lodash';\n\nimport { getMountingRailChains } from '../common';\nimport geometry from '../../../../scene/util/geometry';\nimport productService, { getProduct } from '../../../../services/products';\nimport constants from '../../../../settings/constants';\nimport { flatten } from '../../../../util/array';\nimport { floor } from '../../../../util/round';\nimport tacHelpers from '../../tacHelpers';\nimport { ITEMS } from '../../../../constants';\n\nconst SHORT_RAIL_WIDTH = 650;\nconst LONG_RAIL_WIDTH = 1250;\nconst WIDTH_DIFF_ON_MERGE = 2 * SHORT_RAIL_WIDTH - LONG_RAIL_WIDTH; //50\nconst SECTION_WIDTH = 610;\nconst MOUNTING_RAIL_Y_POS = 1975;\n\n/* assorted helper functions for getAdjustedMountingRails2 */\n\nfunction getColorCount(sections) {\n return sections.reduce(\n (acc, curr) => {\n curr.items\n .filter(item =>\n productService.isType(item, ['end-shelf', 'side-panel'])\n )\n .forEach(item => {\n switch (item.filter.color) {\n case 'white':\n acc.white++;\n break;\n case 'dark_grey':\n acc.darkGrey++;\n break;\n default:\n break;\n }\n });\n return acc;\n },\n { white: 0, darkGrey: 0 }\n );\n}\n\nfunction expandChain(chain) {\n const startWidth = geometry.surround(chain).width;\n let moveTo = Math.max(chain[0].x, 0);\n const newChain = chain.map(link => {\n const movedLink = {\n ...link,\n x: moveTo,\n };\n moveTo = movedLink.x + movedLink.width;\n return movedLink;\n });\n\n return {\n startWidth,\n chain: newChain,\n };\n}\nfunction moveChain(chain, move) {\n return chain.map(link => ({\n ...link,\n x: link.x + move,\n }));\n}\n\nfunction colorLink(link, color) {\n const swappables = productService.getSwappables(link, {\n color: color,\n width: link.width,\n });\n\n return {\n ...link,\n ...swappables?.[0],\n };\n}\n\nfunction colorChain(chain, sections) {\n const collider = {\n x: chain[0].x,\n y: chain[0].y,\n height: 1,\n width: geometry.surround(chain).width,\n };\n\n const touchedSections = sections.filter(section =>\n geometry.collides(section, collider, { front: 100 })\n );\n\n const colorCount = getColorCount(touchedSections);\n const color = colorCount.darkGrey > colorCount.white ? 'dark_grey' : 'white';\n\n return chain.map(link => colorLink(link, color));\n}\n\nfunction centerChain(expand, sections) {\n const collider = {\n x: expand.chain[0].x,\n y: expand.chain[0].y,\n height: 1,\n width: expand.startWidth,\n };\n\n const touchedSections = sections.filter(section =>\n geometry.collides(section, collider, { front: 100 })\n );\n\n const expandedWidth = geometry.surround(expand.chain).width;\n const touchedWidth = geometry.surround(touchedSections).width;\n const sideExtension = floor((expandedWidth - touchedWidth) / 2, 1);\n\n return moveChain(expand.chain, -sideExtension);\n}\n\nfunction dodgeWall(chain, wallWidth) {\n if (chain[0].x < 0) {\n return moveChain(chain, -chain[0].x);\n }\n const [rightMostLink] = chain.slice(-1);\n const rightMostPos = rightMostLink.x + rightMostLink.width;\n\n if (rightMostPos > wallWidth) {\n return moveChain(chain, wallWidth - rightMostPos);\n }\n return chain;\n}\n\nfunction getChainSections(chain, sections) {\n const collider = {\n x: chain[0].x,\n y: chain[0].y,\n height: 1,\n width: geometry.surround(chain).width,\n };\n\n return sections.filter(section =>\n geometry.collides(section, collider, { front: 100 })\n );\n}\n\nfunction overlapOK(overlap, chain, cand, remaining, sections) {\n if (overlap.width >= WIDTH_DIFF_ON_MERGE && cand === remaining[0]) {\n return true;\n }\n\n const touchedSections = getChainSections(cand, sections);\n const sectionsMaxX = tacHelpers.getLimits({ items: touchedSections }).max.x;\n const currentMaxX = tacHelpers.getLimits({ items: chain }).max.x;\n const inBetweeners = remaining.filter(\n other => other[0].x > chain[0].x && other[0].x < sectionsMaxX\n );\n\n const inBetweenWidth = tacHelpers.getCondensedTacWidth({\n items: flatten(inBetweeners),\n });\n\n return currentMaxX + inBetweenWidth - WIDTH_DIFF_ON_MERGE >= sectionsMaxX;\n}\n\nfunction getMergeBuddy(chain, remaining, overlap, sections) {\n if (chain[chain.length - 1].width === SHORT_RAIL_WIDTH) {\n return remaining.find(\n cand =>\n chain !== cand &&\n cand.find(link => link.width === SHORT_RAIL_WIDTH) &&\n overlapOK(overlap, chain, cand, remaining, sections)\n );\n }\n}\n\nfunction mergeBetweenChains(chain, mergeBuddy) {\n const link = chain.pop();\n const sharedRail = getProduct(\n constants.MOUNTING_RAILS[_.camelCase(link.filter.color)][LONG_RAIL_WIDTH]\n );\n chain.push({\n ...sharedRail,\n x: link.x,\n y: MOUNTING_RAIL_Y_POS,\n z: 0,\n });\n\n const linkBuddyIdx = mergeBuddy.findIndex(\n link => link.width === SHORT_RAIL_WIDTH\n );\n mergeBuddy.splice(linkBuddyIdx, 1);\n}\n\nfunction mergeWithinChain(chain) {\n const totalWidth = chain.reduce((acc, curr) => acc + curr.width, 0);\n const widthNeeded = chain[chain.length - 1].x + SECTION_WIDTH - chain[0].x;\n let excess = totalWidth - widthNeeded;\n return chain\n .map((link, index, links) => {\n if (excess < WIDTH_DIFF_ON_MERGE || link.width > SHORT_RAIL_WIDTH) {\n return link;\n }\n const mergeBuddyIdx = links.find(\n (other, otherIdx) =>\n other.width === SHORT_RAIL_WIDTH && otherIdx > index\n );\n\n if (!mergeBuddyIdx) {\n return link;\n }\n\n const sharedRail = getProduct(\n constants.MOUNTING_RAILS[_.camelCase(link.filter.color)][\n LONG_RAIL_WIDTH\n ]\n );\n links.splice(mergeBuddyIdx, 1);\n excess -= WIDTH_DIFF_ON_MERGE;\n\n return {\n ...sharedRail,\n x: link.x,\n y: MOUNTING_RAIL_Y_POS,\n z: 0,\n };\n })\n .filter(Boolean);\n}\n\nfunction sortChains(chains) {\n // sort all links in the chains\n chains.forEach(chain => chain.sort((a, b) => a.x - b.x));\n // ..and the chains themselves\n chains.sort((c1, c2) => c1[0].x - c2[0].x);\n}\n\nfunction adjustOverlaps(overlapping, wallWidth, sections) {\n /*\n the strategy is:\n - adjust all overlaps to the right,\n - merge all 650s to 1250s if possible\n - if it doesn't fit, do the same thing to the left\n - give up\n */\n let remainingChains = overlapping.slice();\n if (remainingChains.length < 2) {\n return remainingChains;\n }\n\n const leftAdjusted = [];\n\n leftAdjusted.push(remainingChains[0]);\n let chain = remainingChains.shift();\n\n while (remainingChains.length) {\n if (!chain.length) {\n chain = remainingChains.shift();\n }\n let left = geometry.surround(chain);\n const next = remainingChains[0];\n let right = geometry.surround(next);\n let overlap = geometry.intersectingRect(left, right);\n if (overlap.width > 0) {\n const mergeBuddy = getMergeBuddy(\n chain,\n remainingChains,\n overlap,\n sections\n );\n if (mergeBuddy) {\n mergeBetweenChains(chain, mergeBuddy);\n\n if (!next.length) {\n // the next chain only had a single member, that has now been merged to the current chain\n remainingChains.shift();\n } else {\n // re-calculate overlap post-merge\n left = geometry.surround(chain);\n right = geometry.surround(next);\n overlap = geometry.intersectingRect(left, right);\n }\n }\n if (next.length) {\n remainingChains[0] = moveChain(remainingChains[0], overlap.width);\n }\n }\n if (next.length) {\n leftAdjusted.push(remainingChains[0]);\n chain = remainingChains.shift();\n left = geometry.surround(chain);\n }\n remainingChains = remainingChains.filter(chain => chain.length);\n }\n\n remainingChains = leftAdjusted.slice();\n let rightAdjusted = [];\n\n chain = remainingChains[remainingChains.length - 1];\n let right = geometry.surround(chain);\n right.x = wallWidth;\n right.width = 10000;\n\n while (remainingChains.length) {\n const next = remainingChains[remainingChains.length - 1];\n let left = geometry.surround(next);\n let overlap = geometry.intersectingRect(left, right);\n if (overlap.width > 0) {\n const mergeBuddy = getMergeBuddy(next, rightAdjusted, overlap, sections);\n if (mergeBuddy) {\n const buddyIndex = rightAdjusted.indexOf(mergeBuddy);\n mergeBetweenChains(next, mergeBuddy);\n rightAdjusted = rightAdjusted.map((chain, index) =>\n index <= buddyIndex ? moveChain(chain, SHORT_RAIL_WIDTH) : chain\n );\n chain = rightAdjusted[0];\n if (chain.length) {\n left = geometry.surround(next);\n right = geometry.surround(chain);\n overlap = geometry.intersectingRect(left, right);\n }\n }\n if (chain.length) {\n remainingChains[remainingChains.length - 1] = moveChain(\n remainingChains[remainingChains.length - 1],\n -overlap.width\n );\n }\n }\n\n rightAdjusted.unshift(remainingChains[remainingChains.length - 1]);\n chain = remainingChains.pop();\n right = geometry.surround(chain);\n rightAdjusted = rightAdjusted.filter(chain => chain.length);\n }\n\n const finalOverlap = rightAdjusted[0][0].x;\n if (finalOverlap < 0) {\n const move = Math.abs(finalOverlap);\n return rightAdjusted.map(chain => moveChain(chain, move));\n }\n\n return rightAdjusted;\n}\n\n/*\n when we enter here, all the mountingrails are positioned\n on top of their respective sections.\n our objective is:\n - pull them apart so they dont overlap\n - center the chain relative its sections and paint it correctly\n - move it away from any walls it overlaps\n - move it away from other overlapping chains\n */\n\nfunction getAdjustedMountingRails(mountingRails, sections, wallWidth) {\n if (!mountingRails.length) {\n return [];\n }\n\n // consider rails right next to each other part of the same chain\n const padding = {\n right: 1,\n left: 1,\n };\n\n const chains = getMountingRailChains(mountingRails, padding);\n sortChains(chains);\n\n // merge double shorter rails to longer version where applicable\n const merged = chains.map(mergeWithinChain);\n\n // expand the chains to their full width\n const expanded = merged.map(expandChain);\n\n // center them over 'their' sections\n const centeredChains = expanded.map(exp => centerChain(exp, sections));\n\n // dodge walls\n const fittedChains = centeredChains.map(chain => dodgeWall(chain, wallWidth));\n\n if (fittedChains.length === 1) {\n // a single chain does not overlap, only coloring left\n const colored = fittedChains.map(chain => colorChain(chain, sections));\n\n return colored;\n }\n\n // fix eventual overlaps, if possible\n const adjusted = adjustOverlaps(fittedChains, wallWidth, sections);\n\n // links might now have merged between chains, so we need to re-calculate them\n const newChains = getMountingRailChains(flatten(adjusted), padding);\n sortChains(newChains);\n\n // finally, let's color them according to which sections they touch\n const colored = newChains.map(chain => colorChain(chain, sections));\n\n return colored;\n}\n\nexport const __test__ = {\n getAdjustedMountingRails,\n adjustOverlaps,\n};\n\nfunction createMountingRail(sections, index, isShared) {\n const MOUNTING_RAIL_Y_POS = 1975;\n // Use white rail for now, we will decide on color in a later step\n const mountingRailProduct =\n getProduct(constants.MOUNTING_RAILS.white[isShared ? '1250' : '650']) ||\n // If white is unavailable on market, use the grey one\n getProduct(constants.MOUNTING_RAILS.darkGrey[isShared ? '1250' : '650']);\n\n return {\n ...mountingRailProduct,\n x: sections[index].x,\n y: MOUNTING_RAIL_Y_POS,\n z: 0,\n };\n}\n\nfunction useSharedRail(sections, index) {\n return (\n index + 1 < sections.length &&\n sections[index + 1].x + sections[index + 1].width - sections[index].x <\n LONG_RAIL_WIDTH\n );\n}\n\nexport default function getMountingRails(tac) {\n const sections = tac.items\n .filter(item => productService.isType(item, ITEMS.SECTION))\n .sort((a, b) => a.x - b.x);\n\n const mountingRails = [];\n\n for (let i = 0; i < sections.length; i++) {\n if (useSharedRail(sections, i)) {\n mountingRails.push(createMountingRail(sections, i, true));\n i++; // skip next section that is now covered by this rail\n } else {\n mountingRails.push(createMountingRail(sections, i, false));\n }\n }\n\n const adjustedMountingRails = getAdjustedMountingRails(\n mountingRails.slice(),\n sections,\n geometry.surround(tac.wall.points).width\n );\n\n return flatten(adjustedMountingRails);\n}\n","import productService from '../../../../services/products';\nimport geometry from '../../../../scene/util/geometry';\nimport getMountingRails from './mountingRail';\nimport range from './';\nimport getDefaultPac from '../../../../util/aactools/getDefaultPAC';\nimport tacHelpers from '../../tacHelpers';\nimport { replace } from '../../replace';\nimport { ITEMS } from '../../../../constants';\n\nfunction addSectionDiff(tac, options, diff) {\n if (options.triggerItem) {\n /*\n \"Updated\" is a bit misleading here since no update is actually made here.\n The actual update will instead happen during the connectParts phase of updateItem\n */\n\n const updatedSections = [\n ...tac.items.filter(\n item =>\n item.itemid !== options.triggerItem &&\n range.isIncompleteSection(item) &&\n geometry.collides(item, options.triggerItem)\n ),\n ];\n diff.updated.push(...updatedSections);\n\n return diff;\n } else {\n const updatedSections = [];\n const rightSidewallUpdated = diff.updated.find(\n other => other.x > 0 && productService.isType(other, 'sidewall')\n );\n\n if (rightSidewallUpdated) {\n const allSections = tacHelpers.getSections(tac).sort((a, b) => b.x - a.x);\n const rightmostSection = allSections[0];\n\n if (rightmostSection) {\n const superSection = tacHelpers.findSuperSection(tac, rightmostSection);\n const sections = superSection\n .filter(member => productService.isSection(member))\n .sort((a, b) => a.x - b.x);\n\n const rightmostFullSize = tacHelpers.getFullSize(rightmostSection);\n\n if (\n rightmostFullSize.x + rightmostFullSize.width >\n rightSidewallUpdated.x ||\n tacHelpers.isClothesRailConnected(tac, rightSidewallUpdated)\n ) {\n const oldSidewall = tac.items.find(\n item => item.itemid === rightSidewallUpdated.itemid\n );\n const limits = tacHelpers.getLimits(tac);\n const freeSpaceBefore = oldSidewall.x - limits.max.x;\n const diff = rightSidewallUpdated.x - oldSidewall.x + freeSpaceBefore;\n\n // move all sections in supersection\n updatedSections.push(\n ...sections.map(section => ({\n ...section,\n x: section.x + diff,\n localOptions: {\n keepParts: true,\n },\n }))\n );\n }\n }\n }\n diff.updated.push(...updatedSections);\n\n return diff;\n }\n}\n\nfunction addSuspensionRailDiff(tac, diff, options) {\n //Remove all mounting rails, since these will be re-added\n const removed = [\n ...tac.items.filter(item => productService.isType(item, 'mounting-rail')),\n ];\n diff.removed.push(...removed);\n\n if (!options?.isPersistent) {\n return diff;\n }\n\n const modifiedTac = { ...tac };\n diff.updated.forEach(item => {\n replace(modifiedTac.items, item);\n });\n\n const added = getMountingRails(modifiedTac);\n diff.added.push(...added);\n\n return diff;\n}\n\nfunction addSidewallDiff(tac, diff, options) {\n const sidewalls = tac.items.filter(item =>\n productService.isType(item, 'sidewall')\n );\n\n const wallSize = geometry.surround(\n tac?.wall?.points || getDefaultPac().wall.points\n );\n const leftSideWall = productService.getProduct('sidewall_left');\n const rightSidewall = productService.getProduct('sidewall_right');\n\n if (sidewalls.length === 2) {\n // both sidewalls present, just update the right one if needed\n if (!options.wallUpdate) {\n return diff;\n }\n\n const rightSidewall = sidewalls.find(sidewall => sidewall.x > 0);\n if (rightSidewall.x !== wallSize.width) {\n diff.updated.push({ ...rightSidewall, x: wallSize.width });\n }\n return diff;\n } else if (sidewalls.length === 1 && sidewalls[0].x === 0) {\n // this is an old tac with only the left sidewall, add the right\n diff.added.push({ ...rightSidewall, x: wallSize.width, y: 0, z: 20 });\n return diff;\n }\n\n // no sidewalls, add both\n\n const added = [\n { ...leftSideWall, x: 0, y: 0, z: 20 },\n { ...rightSidewall, x: wallSize.width, y: 0, z: 20 },\n ];\n\n diff.added.push(...added);\n\n return diff;\n}\n\nfunction getDependencyDiff(tac, options = {}) {\n let diff = { added: [], updated: [], removed: [], untouched: [] };\n\n diff = addSidewallDiff(tac, diff, options);\n if (\n (options.triggerItem && !productService.isInsert(options.triggerItem)) ||\n options.wallUpdate\n ) {\n diff = addSectionDiff(tac, options, diff);\n diff = addSuspensionRailDiff(tac, diff, options);\n }\n\n return diff;\n}\n\nfunction getDependentItems(item, tac) {\n if (productService.isType(item, ITEMS.SECTION)) {\n return tac.items.filter(item =>\n productService.isType(item, 'mounting-rail')\n );\n }\n return [];\n}\n\nexport default { getDependencyDiff, getDependentItems };\n\nexport { getDependencyDiff, getDependentItems };\n","import productService from '../../../../services/products';\nimport tacHelpers from '../../tacHelpers';\nimport getItemConfig from '../../../../scene/util/getItemConfig';\nimport { getDependencyDiff, getDependentItems } from './dependentItems';\nimport geometry from '../../../../scene/util/geometry';\nimport constants from '../../../../settings/constants';\nimport { unique } from '../../../../util/array';\nimport { ITEMS } from '../../../../constants';\n\n/**\n * Function called automatically to filter the slots available for sections in aurdal\n *\n * @param {Array} slots list of slots available to drop per section\n * @return {Boolean} Weither if thats a droppable or not it will return true or false\n */\nfunction filterSlots(slots) {\n const NUMBER_OF_SLOTS = 24;\n const UNAVAILABLE_SLOTS = 2;\n const NUMBER_OF_FREE_SLOTS = (NUMBER_OF_SLOTS - UNAVAILABLE_SLOTS) / 2;\n\n return slots.filter(slot => {\n if (productService.isType(slot, ITEMS.DRAWER)) {\n return +slot.y < +slot.height * NUMBER_OF_FREE_SLOTS;\n }\n\n return true;\n });\n}\n\nfunction getAllowedOverlap(tac, item, relative) {\n const sectionRelation =\n productService.isType(item, ITEMS.SECTION) &&\n productService.isType(relative, ITEMS.SECTION);\n\n if (sectionRelation) {\n const tacItem = tacHelpers.getItem(tac, item.itemid);\n const sidePanelWidth = 20;\n\n if (\n tacItem &&\n tacHelpers.hasExtClothesRail(tacItem) &&\n tacHelpers.isClothesRailConnected({ items: [tacItem] }, relative)\n ) {\n // the dragged item has an ext cr connected to the current relative\n return {\n x: 0,\n y: 0,\n z: 0,\n width: -sidePanelWidth,\n height: 0,\n depth: 0,\n };\n } else if (\n tacItem &&\n tacHelpers.hasExtClothesRail(relative) &&\n tacHelpers.isClothesRailConnected({ items: [relative] }, tacItem)\n ) {\n // the current relative has an ext cr connected to the dragged item\n return { x: sidePanelWidth, y: 0, z: 0, width: 0, height: 0, depth: 0 };\n }\n\n // two sections with no interfering clothes rails\n return {\n x: sidePanelWidth,\n y: 0,\n z: 0,\n width: -sidePanelWidth * 2,\n height: 0,\n depth: 0,\n };\n }\n\n return { x: 0, y: 0, z: 0, width: 0, height: 0, depth: 0 };\n}\n\nfunction getNeighbouringSections(tac, section, direction) {\n return tacHelpers\n .getSections(tac)\n .filter(\n cand =>\n cand.itemid !== section.itemid &&\n geometry.collides(cand, section) &&\n (!direction ||\n (direction === 'right' && cand.x > section.x) ||\n (direction === 'left' && cand.x < section.x))\n );\n}\n\nfunction getSlotSources(item, tac) {\n //Not implemented for aurdal\n return [];\n}\n\nfunction getSnappingPosition(item, rects) {\n if (!productService.isSection(item)) {\n return;\n }\n\n const postWidth = productService.getPostWidth();\n const sectionRects = rects.filter(productService.isSection);\n const closeRects = geometry.getCollidingRects(item, sectionRects, {\n right: constants.SECTION_SNAPPING_DISTANCE + postWidth - 1,\n left: constants.SECTION_SNAPPING_DISTANCE + postWidth - 1,\n });\n\n const partner = geometry.closestCollidingRect(item, closeRects);\n\n if (!partner) {\n return;\n }\n\n if (partner.x < item.x) {\n return { x: partner.x + partner.width };\n } else if (partner.x > item.x) {\n return { x: partner.x - item.width };\n }\n}\n\nfunction getPartnerConfig(partner) {\n return getItemConfig(partner);\n}\n\nfunction isIncompleteSection(item) {\n return (\n productService.isSection(item) &&\n item.items.filter(item => productService.isType(item, 'side-panel'))\n .length < 2\n );\n}\n\nfunction isMultiColouredSection(section, tac) {\n if (!isIncompleteSection(section)) {\n return false;\n }\n\n const sidePanels = section.items.filter(item =>\n productService.isType(item, 'side-panel')\n );\n\n let neighboursOfInterest;\n if (!sidePanels.length) {\n neighboursOfInterest = getNeighbouringSections(tac, section);\n } else if (!sidePanels.find(side => side.x === 0)) {\n neighboursOfInterest = getNeighbouringSections(tac, section, 'left');\n } else {\n neighboursOfInterest = getNeighbouringSections(tac, section, 'right');\n }\n\n return neighboursOfInterest.some(\n neighbour => neighbour.filter.color !== section.filter.color\n );\n}\n\nfunction partnerSlots(slots, otherSlots, tac) {\n const config = getItemConfig(slots[0]);\n // add complement slot for other half of extendable clothes rod\n const insideSlots = slots.filter(slot => slot.id === config.innerId);\n if (insideSlots.length) {\n const outsideSlots = otherSlots;\n insideSlots.forEach(left => {\n const fits = outsideSlots.filter(\n right =>\n right.x > left.x &&\n right.y === left.y &&\n right.x + right.width <= left.x + config.maxWidth\n );\n if (fits.length === 1) {\n left.partnerSlot = fits[0];\n }\n if (fits.length > 1) {\n throw new Error('found more than one fit for outside clothes rail');\n }\n });\n }\n return slots.filter(slot => slot.id !== config.innerId || slot.partnerSlot);\n}\n\nfunction moveWithCr(item) {\n return productService.isSection(item) || tacHelpers.hasExtClothesRail(item);\n}\n\nfunction findSuperSection(tac, member, skiplist) {\n // pad items just a little so they overlap a bit with stuff they touch\n const padding = {\n right: 1,\n left: 1,\n };\n\n const superSection = [member];\n\n const candidates = tac.items.filter(\n item =>\n item.itemid !== member.itemid && skiplist.indexOf(item.itemid) === -1\n );\n\n const fullSizeMember = tacHelpers.getFullSize(member);\n\n if (productService.isType(member, [ITEMS.SECTION, 'mounting-rail'])) {\n // set padding to 0 so we only collide with merged sections, if no cr\n padding.left = tacHelpers.isClothesRailConnected(tac, member) ? 1 : 0;\n padding.right = tacHelpers.hasExtClothesRail(member) ? 1 : 0;\n }\n\n const collisions = candidates.filter(candidate =>\n geometry.collides(\n fullSizeMember,\n tacHelpers.getFullSize(candidate),\n padding\n )\n );\n const skip = skiplist\n .concat(collisions.map(item => item.itemid))\n .filter(unique);\n\n const rest = collisions.map(collision =>\n findSuperSection(tac, collision, skip)\n );\n\n return Array.prototype.concat.apply(superSection, rest).filter(unique);\n}\n\nfunction getWallResizingLimits(tac) {\n const limits = tacHelpers.getLimits(tac);\n\n const allSections = tacHelpers.getSections(tac).sort((a, b) => b.x - a.x);\n const rightmostSection = allSections[0];\n\n if (!rightmostSection) {\n //no sections in tac\n return limits;\n }\n\n const superSection = findSuperSection(tac, rightmostSection, []);\n\n if (\n superSection.filter(\n member =>\n productService.isType(member, 'sidewall') &&\n (tacHelpers.hasExtClothesRail(member) ||\n tacHelpers.isClothesRailConnected(tac, member))\n ).length > 1\n ) {\n limits.blockedX = true;\n }\n\n const outsidersLimits = tacHelpers.getLimits({\n items: tac.items.filter(\n item => !superSection.some(member => member.itemid === item.itemid)\n ),\n });\n\n const condensed = tacHelpers.getCondensedTacWidth({ items: superSection });\n if (outsidersLimits.max.x > 0) {\n limits.max.x = outsidersLimits.max.x + condensed;\n } else {\n limits.min.x = 0;\n limits.max.x = condensed;\n }\n return limits;\n}\n\nconst api = {\n filterSlots,\n findSuperSection,\n getAllowedOverlap,\n getDependencyDiff,\n getDependentItems,\n getNeighbouringSections,\n getPartnerConfig,\n getSlotSources,\n getSnappingPosition,\n isIncompleteSection,\n isMultiColouredSection,\n getWallResizingLimits,\n moveWithCr,\n partnerSlots,\n};\n\nexport default api;\n","import range from './';\nimport tacHelpers from '../../tacHelpers';\nimport productService from '../../../../services/products';\nimport geometry from '../../../../scene/util/geometry';\nimport idGenerator from '../../../../util/aactools/idGenerator';\nimport { ITEMS } from '../../../../constants';\n\nconst CROSS_BRACE_ID = '87749600';\n\nfunction shouldSkipCrossBrace(section, index, movedWasEven) {\n return movedWasEven ? index % 2 === 0 : index % 2 !== 0;\n}\n\nfunction getSectionsPreparedForUpdate(superSection, movedItem = null) {\n /*\n Sort touching sections first by x,\n to decide which should have the cross braces.\n Then sort them again by height,\n to allow higher sections priority for their side panels.\n */\n let movedWasEven;\n const sectionsSorted = superSection\n .filter(product => productService.isSection(product))\n .sort((a, b) => a.x - b.x)\n .map((section, index) => {\n if (movedItem?.itemid === section.itemid) {\n movedWasEven = index % 2 === 0;\n return null;\n }\n\n return {\n ...section,\n localOptions: {\n partsToSkip: shouldSkipCrossBrace(section, index, movedWasEven)\n ? [\n Object.values(section.parts).find(part =>\n part.includes(CROSS_BRACE_ID)\n ),\n ]\n : [],\n },\n };\n })\n .filter(Boolean)\n .sort((a, b) => b.height - a.height || a.x - b.x);\n\n return sectionsSorted.length ? sectionsSorted : [];\n}\n\nfunction replaceSectionsTransformedByMerge(superSection) {\n return superSection\n .filter(product => productService.isSection(product))\n .sort((a, b) => a.x - b.x)\n .map((section, index, allSections) => {\n if (\n allSections[index - 1]?.height > section.height &&\n allSections[index + 1]?.height > section.height\n ) {\n // This section has higher sections on both sides,\n // thereby changing its height\n const newHeight = [allSections[index - 1], allSections[index + 1]].sort(\n (a, b) => a.height - b.height\n )[0].height;\n\n // New height is equal to the lowest of the enclosing neighbors\n const replacementProduct = productService.getFit(section, {\n height: newHeight,\n });\n\n const newSection = tacHelpers.getSwitchableItem(\n section,\n replacementProduct\n );\n\n return newSection;\n }\n return section;\n });\n}\n\n/**\n * Splits an array of sections into two arrays of sections based on whether they\n * are located to the left or to the right of a specific section (dividingSection)\n * and returns an array of the resulting left/right arrays.\n * dividingSection may, but does not have to, be present in the input array. Even if\n * present, it will not be included neither in the left nor right arrays, since its x\n * value is neither less than nor greater than its own x value.\n * @param {Array} superSection The input array of sections.\n * @param {object} dividingSection The dividing section to compare to.\n * @returns {Array} The array containing the resulting left and right arrays.\n */\nfunction splitSuperSectionIntoLeftAndRightParts(superSection, dividingSection) {\n const superSectionParts = [];\n\n const left = superSection.filter(section => section.x < dividingSection.x);\n const right = superSection.filter(section => section.x > dividingSection.x);\n superSectionParts.push(left, right);\n\n return superSectionParts;\n}\n\n/**\n * Gets the caused updates and removals in the sections due to a specific move\n * @param {*} tac\n * @param {*} triggerItem The moved item causing the changes\n * @param {*} isPersistent False if the user is still dragging\n * @returns {Object}\n */\nconst getSectionChanges = (tac, triggerItem, isPersistent) => {\n let sectionsToUpdate = [];\n\n const tacItem = tacHelpers.getItem(tac, triggerItem.itemid);\n let currentSuperSection = tacItem && range.findSuperSection(tac, tacItem, []);\n\n if (currentSuperSection?.length > 1) {\n if (isPersistent) {\n currentSuperSection =\n replaceSectionsTransformedByMerge(currentSuperSection);\n }\n sectionsToUpdate.push(...getSectionsPreparedForUpdate(currentSuperSection));\n }\n\n const oldSuperSection = range\n .findSuperSection(tac, triggerItem, [])\n .filter(member => {\n return (\n !currentSuperSection ||\n !currentSuperSection.find(\n candidate => candidate.itemid === member.itemid\n )\n );\n });\n\n const superSectionParts = oldSuperSection\n ? splitSuperSectionIntoLeftAndRightParts(oldSuperSection, triggerItem)\n : [];\n superSectionParts.forEach(superSectionPart => {\n if (superSectionPart.length) {\n sectionsToUpdate.push(\n ...getSectionsPreparedForUpdate(superSectionPart, triggerItem)\n );\n }\n });\n\n if (!tacItem) {\n sectionsToUpdate = sectionsToUpdate.filter(\n section => section.itemid !== triggerItem.itemid\n );\n }\n\n return {\n updated: sectionsToUpdate,\n removed: sectionsToUpdate.flatMap(section => {\n // Remove all side panels and cross braces in the sections staged for update,\n // so we can re-assemble them according to the new setup\n return section.items.filter(item =>\n productService.isType(item, ['side-panel', 'cross-brace'])\n );\n }),\n };\n};\n\n/**\n * Checks whether a door is in front of a shelf\n * @param {*} door\n * @param {*} shelf\n * @returns {Boolean}\n */\nconst isDoorInFrontOfShelf = (door, shelf) => {\n return geometry.collides(door, shelf, {\n front: 100,\n back: 100,\n });\n};\n\n/**\n * Re-evaluates all doors in the tac and adds references on their shelves\n * @param {*} tac\n * @returns {Object}\n */\nconst getAllDoorSectionsWithFreshReferencesOnShelves = tac => {\n const sectionsWithDoors = tac.items.filter(section =>\n section.items.find(item => productService.isType(item, ITEMS.DOOR))\n );\n\n return {\n updated: sectionsWithDoors.map(section => {\n const doors = section.items.filter(item =>\n productService.isType(item, ITEMS.DOOR)\n );\n\n return {\n ...section,\n items: section.items.map(item => {\n const collidingDoor =\n productService.isType(item, 'shelf') &&\n doors.find(door => isDoorInFrontOfShelf(door, item));\n\n return {\n ...item,\n ...(collidingDoor ? { belongsTo: collidingDoor.itemid } : {}),\n };\n }),\n localOptions: {\n keepParts: true,\n },\n };\n }),\n };\n};\n\n/**\n * Get the slots for shelves behind a specific door\n * @param {*} door\n * @param {*} section\n * @returns {Array}\n */\nconst getBehindDoorSlots = (door, section) => {\n const fakedTac = { items: [{ ...section }] };\n return tacHelpers\n .getSlots(fakedTac, productService.getFittingShelf(section))\n .filter(slot => isDoorInFrontOfShelf(door, slot.local));\n};\n\n/**\n * Gets the section where the door as moved, with added shelves behind the door\n * @param {*} door\n * @param {*} section\n * @param {*} isPersistent\n * @returns {Object}\n */\nconst getNewSectionWithDoorShelves = (door, section, isPersistent) => {\n const availableSlots = getBehindDoorSlots(door, section);\n\n if (availableSlots.length < 13) {\n throw new Error('Not possible to place shelves behind door');\n }\n\n const newShelves = [0, 6, 12].map(index => availableSlots[index].local);\n\n return {\n ...section,\n items: [\n ...section.items.filter(item => item.belongsTo !== door.itemid),\n ...newShelves.map(shelf => ({\n ...shelf,\n itemid: isPersistent ? idGenerator.id() : idGenerator.fakeId(),\n belongsTo: door.itemid,\n })),\n ],\n localOptions: {\n keepParts: true,\n },\n };\n};\n\n/**\n * Gets the old section where the door was placed previously,\n * with the shelves that used to be behind the door removed\n * @param {*} door\n * @param {*} section\n * @returns {Object}\n */\nconst getOldSectionWithoutDoorShelves = (door, section) => {\n return {\n ...section,\n items: section.items.filter(item => item.belongsTo !== door.itemid),\n localOptions: {\n keepParts: true,\n },\n };\n};\n\n/**\n * Gets the caused updates due to a specific door moving in the scene\n * @param {*} tac\n * @param {*} triggerItem\n * @param {*} isPersistent\n * @returns {Object}\n */\nconst getSpecificDoorChanges = (tac, triggerItem, isPersistent) => {\n const door = tacHelpers.getItem(tac, triggerItem.itemid) || triggerItem;\n const oldParent = tac.items.find(section =>\n section.items.find(item => item.belongsTo === door.itemid)\n );\n const newParent = tacHelpers.getParent(tac, door);\n const hasChangedParent = oldParent && oldParent.itemid !== newParent?.itemid;\n\n return {\n updated: [\n ...(hasChangedParent\n ? [getOldSectionWithoutDoorShelves(door, oldParent)]\n : []),\n ...(newParent\n ? [getNewSectionWithDoorShelves(door, newParent, isPersistent)]\n : []),\n ],\n };\n};\n\n/**\n * Gets all the dependency changes caused by a change in the TAC\n * @param {*} tac\n * @param {*} options\n * @returns {Object}\n */\nfunction getDependencyDiff(tac, options = {}) {\n const { isPersistent, triggerItem } = options;\n\n const diff = { added: [], updated: [], removed: [], untouched: [] };\n\n if (triggerItem) {\n if (productService.isType(triggerItem, ITEMS.DOOR)) {\n return {\n ...diff,\n ...getSpecificDoorChanges(tac, triggerItem, isPersistent),\n };\n }\n if (productService.isSection(triggerItem)) {\n return { ...diff, ...getSectionChanges(tac, triggerItem, isPersistent) };\n }\n } else if (isPersistent) {\n return { ...diff, ...getAllDoorSectionsWithFreshReferencesOnShelves(tac) };\n }\n}\n\nexport default { getDependencyDiff };\n\nexport { getDependencyDiff };\n","import geometry from '../../../../scene/util/geometry';\nimport {\n isType,\n isSection,\n getFittingShelf,\n} from '../../../../services/products';\nimport { getBoundedItem } from '../../../../services/products/models';\nimport constants from '../../../../settings/constants';\nimport { unique } from '../../../../util/array';\nimport tacHelpers from '../../tacHelpers';\nimport { getDependencyDiff } from './dependentItems';\nimport { ITEMS } from '../../../../constants';\nimport productService from '../../../../services/products';\n\nconst { SHELF_DRAWER, BOX, DOOR, SHELF, DRAWER, CABINET, CHEST, SECTION } =\n ITEMS;\n\n/**\n * A function which will be called when dropping an ivar component in the planner\n *\n * @param {Array} slots An array of sots describing the holes available to hang the component on it\n * @returns {Boolean}\n */\nfunction filterSlots(slots, tac) {\n const SHELF_DRAWER_MAX_Y = 1400;\n const DRAWER_MAX_Y_PERCENTAGE_OF_SECTION_HEIGHT = 0.51;\n const HIGH_CABINET_HEIGHT = 1600;\n const SHORT_SECTION_HEIGHT = 1240;\n const MESH_CABINETS_HEIGHT = 830;\n const SLOT_Y_LIMIT = 121;\n const SLOT_Y_LIMIT_MESH_CABINET_SHORT_SECTION = 377;\n return slots.filter(slot => {\n const collisionItem = getBoundedItem(slot, 'collision');\n const extendsAbove =\n collisionItem.y + collisionItem.height >\n slot.parent.y + slot.parent.height;\n if (\n isType(slot, CABINET) &&\n slot.height === MESH_CABINETS_HEIGHT &&\n slot.parent.height === SHORT_SECTION_HEIGHT\n ) {\n return slot.y <= SLOT_Y_LIMIT_MESH_CABINET_SHORT_SECTION;\n }\n if (\n isType(slot, CHEST) ||\n (isType(slot, CABINET) && slot.height === HIGH_CABINET_HEIGHT)\n ) {\n return slot.y <= SLOT_Y_LIMIT && !extendsAbove;\n }\n if (isType(slot, SHELF_DRAWER)) {\n const grandParent = tacHelpers.getParent(tac, slot.parent);\n return (\n slot.y + slot.height <= SHELF_DRAWER_MAX_Y &&\n (!isSection(grandParent) ||\n slot.parent.y + constants.DISTANCE_BETWEEN_ATTACHMENTS <\n grandParent.y + grandParent.height)\n );\n }\n if (isType(slot, DRAWER)) {\n return (\n slot.parent.height * DRAWER_MAX_Y_PERCENTAGE_OF_SECTION_HEIGHT > slot.y\n );\n }\n if (isType(slot, SHELF) && slot.items?.length) {\n return (\n slot.width >=\n slot.items.reduce((totalWidth, item) => totalWidth + item.width, 0) &&\n slot.y <= SHELF_DRAWER_MAX_Y &&\n slot.y + constants.DISTANCE_BETWEEN_ATTACHMENTS <\n slot.parent.y + slot.parent.height\n );\n }\n if (isType(slot, DOOR)) {\n return (\n !!getFittingShelf(slot.parent) &&\n slot.y + slot.height <= slot.parent.y + slot.parent.height\n );\n }\n\n return !extendsAbove;\n });\n}\n\nfunction findSuperSection(tac, member, skiplist) {\n if (!tacHelpers.isWithinWall(member, tac)) {\n return [member];\n }\n\n const superSection = [member];\n\n const candidates = tac.items.filter(\n item =>\n item.itemid !== member.itemid &&\n skiplist.indexOf(item.itemid) === -1 &&\n tacHelpers.isWithinWall(item, tac)\n );\n\n const fullSizeMember = tacHelpers.getFullSize(member);\n\n const collisions = candidates.filter(candidate =>\n geometry.collides(fullSizeMember, tacHelpers.getFullSize(candidate))\n );\n const skip = skiplist\n .concat(collisions.map(item => item.itemid))\n .filter(unique);\n\n const rest = collisions.map(collision =>\n findSuperSection(tac, collision, skip)\n );\n\n return Array.prototype.concat.apply(superSection, rest).filter(unique);\n}\n\n/**\n * Checks whether a section has an obstructing item placed\n * above the space in-between its posts\n *\n * @param {Object} section The section to check if blocked\n * @param {Object} overlapper The other section asking to overlap\n * @param {Array} nonSections All top-level items in the TAC that are not sections\n * @param {Object} tac The entire TAC\n * @returns {Boolean} If there is a blocking item or not\n */\nfunction isBlockedAboveCenter(section, overlapper, nonSections, tac) {\n const postWidth = productService.getPostWidth();\n const spaceBetweenAfterMerge = {\n ...section,\n x: section.x + postWidth,\n z: 0,\n width: section.width - postWidth * 2,\n height: Math.max(overlapper.height, section.height),\n };\n\n if (\n nonSections.some(item =>\n geometry.contains(spaceBetweenAfterMerge, { ...item, height: 1 })\n )\n ) {\n // Something is placed inside the area above the relative\n const superSection = findSuperSection(tac, section, [overlapper.itemid]);\n if (\n superSection.find(\n item =>\n isSection(item) &&\n item.height > section.height &&\n (item.x < section.x || item.x > section.x)\n )\n ) {\n // Our relative has a higher neighbour on at least one side,\n // meaning that the area above would become a section area post-merge\n return true;\n }\n }\n}\n\n/**\n * Checks the conditions for potential merge on respective side of the relative\n *\n * @param {Object} item The section asking for what overlap is allowed\n * @param {Object} relative The other section we might want to overlap\n * @param {Object} tac The entire TAC\n * @returns {Object} An object with boolean properties \"left\" and \"right\" stating whether overlap is allowed\n */\nfunction getBlockedDirections(item, relative, tac) {\n const postWidth = productService.getPostWidth();\n const blockedDirections = {\n left: false,\n right: false,\n };\n\n const nonSections = tac.items.filter(item => !isSection(item));\n\n if (isBlockedAboveCenter(relative, item, nonSections, tac)) {\n blockedDirections.left = true;\n blockedDirections.right = true;\n }\n\n // Either side could also be blocked if there is something placed above the actual post\n if (!blockedDirections.left) {\n const leftPostAfterMerge = {\n ...relative,\n width: postWidth,\n height: Math.max(item.height, relative.height),\n };\n\n const itemAfterMergeLeft = {\n ...item,\n x: leftPostAfterMerge.x - item.width,\n y: leftPostAfterMerge.y,\n z: leftPostAfterMerge.z,\n };\n\n blockedDirections.left =\n geometry.getCollidingRects(leftPostAfterMerge, nonSections).length ||\n isBlockedAboveCenter(itemAfterMergeLeft, relative, nonSections, tac);\n }\n\n if (!blockedDirections.right) {\n const rightPostAfterMerge = {\n ...relative,\n x: relative.x + relative.width - postWidth,\n width: postWidth,\n height: Math.max(item.height, relative.height),\n };\n\n const itemAfterMergeRight = {\n ...item,\n x: rightPostAfterMerge.x,\n y: rightPostAfterMerge.y,\n z: rightPostAfterMerge.z,\n };\n\n blockedDirections.right =\n geometry.getCollidingRects(rightPostAfterMerge, nonSections).length ||\n isBlockedAboveCenter(itemAfterMergeRight, relative, nonSections, tac);\n }\n\n return blockedDirections;\n}\n\n/**\n * Get the allowed overlap for an item to on another item (the relative)\n *\n * @param {Object} tac The entire TAC\n * @param {Object} item The item asking for what overlap is allowed\n * @param {Object} relative The other item we might want to overlap\n * @returns {Object} An object with the dimensions of the allowed overlap\n */\nfunction getAllowedOverlap(tac, item, relative) {\n let x = 0;\n let width = 0;\n\n const postWidth = productService.getPostWidth();\n const sameDepthSectionRelation =\n item.depth === relative.depth && [item, relative].every(isSection);\n\n if (sameDepthSectionRelation) {\n const blockedDirections = getBlockedDirections(item, relative, tac);\n\n x = !blockedDirections.left ? postWidth : 0;\n width =\n -postWidth *\n (blockedDirections.left && blockedDirections.right\n ? 0\n : !blockedDirections.left && !blockedDirections.right\n ? 2\n : 1);\n }\n\n return { x, y: 0, z: 0, width, height: 0, depth: 0 };\n}\n\n/**\n * Get the \"snapped\" position of a dragged item, if close enough to a suitable partner\n *\n * @param {Object} item The item asking to snap\n * @param {Array} rects All other rects on the scene\n * @returns {Object | undefined} An object with new coordinates for the snapped position,\n * or undefined if no suitable partner found\n */\nfunction getSnappingPosition(item, rects) {\n if (!isSection(item)) {\n return;\n }\n\n const postWidth = productService.getPostWidth();\n const sectionRects = rects.filter(isSection);\n const closeRects = geometry.getCollidingRects(item, sectionRects, {\n right: constants.SECTION_SNAPPING_DISTANCE + postWidth - 1,\n left: constants.SECTION_SNAPPING_DISTANCE + postWidth - 1,\n });\n\n const partner = geometry.closestCollidingRect(item, closeRects);\n\n if (!partner?.overlap || partner.depth !== item.depth) {\n return;\n }\n\n const { overlap } = partner;\n\n if (partner.x < item.x && overlap.width + overlap.x < 0) {\n return { x: partner.x + partner.width };\n } else if (partner.x > item.x && overlap.x > 0) {\n return { x: partner.x - item.width };\n }\n}\n\n/**\n * Checks whether an incoming item is allowed to ignore the spacing bounds of\n * an already present item.\n *\n * @param {Object} candidate The item asking to ignore its spacing bounds\n * @param {Object} relative The other item in the relation\n * @param {Object} [tac] The entire TAC\n * @returns {Boolean} True/false depending on if ignoring is allowed,\n */\nfunction shouldIgnoreSpacingBounds(candidate, relative = null, tac = null) {\n if (\n [candidate, relative].every(\n item => item && (item.belongsTo || isType(item, DOOR))\n ) ||\n [candidate, relative].every(item => item && isType(item, CABINET))\n ) {\n // Never enforce spacing bounds between doors or cabinets\n return true;\n }\n\n const relativeIsStorageBox = relative && isType(relative, BOX);\n\n const candidateHasDrawers =\n isType(candidate, SHELF) &&\n candidate.items?.length &&\n candidate.items.some(item => isType(item, SHELF_DRAWER));\n\n if (relativeIsStorageBox) {\n // A storage box never cares about spacing bounds,\n // unless it sits below a shelf with drawer\n return !candidateHasDrawers;\n }\n\n const someoneIsStandAloneCabinet =\n [candidate, relative]\n .filter(Boolean)\n .some(item => isType(item, CABINET) && item.z < 0) ||\n (tac && isType(tacHelpers.getTopAncestor(tac, candidate), CABINET));\n\n if (someoneIsStandAloneCabinet) {\n // Cabinet spacing bounds only apply when placed inside a section and towards\n // other stuff in that same section\n return true;\n }\n\n if (relative) {\n const relativeIsShelfDrawer = isType(relative, SHELF_DRAWER);\n const relativeHasShelfDrawer =\n isType(relative, SHELF) &&\n relative.items?.length &&\n relative.items.some(item => isType(item, SHELF_DRAWER));\n\n const isShelfDrawerAboveSiblingRelation =\n (relativeIsShelfDrawer || relativeHasShelfDrawer) &&\n relative.y > candidate.y + candidate.height;\n\n if (isShelfDrawerAboveSiblingRelation) {\n // Spacing bounds doesn't apply to shelf drawers above\n return true;\n }\n\n const isShelfOrDoorAboveDrawerRelation =\n isType(candidate, [SHELF, DOOR]) &&\n !candidate.items?.length &&\n isType(relative, DRAWER) &&\n candidate.y > relative.y + relative.height;\n\n if (isShelfOrDoorAboveDrawerRelation) {\n // Shelf spacing bounds doesn't apply to drawers below it\n return true;\n }\n }\n\n return false;\n}\n\nfunction shouldIgnoreCollision(item, parent, relative) {\n return (\n isType(parent, SECTION) &&\n isType(relative, SECTION) &&\n geometry.collides(parent, relative)\n );\n}\n\n/**\n * Gets all the other items in the TAC that are dependent of a specific item\n * @param {*} item\n * @param {*} tac\n * @returns {Array}\n */\nconst getDependentItems = (item, tac) => {\n if (isType(item, DOOR)) {\n const allItems = tacHelpers.getAllItems(tac.items);\n return allItems.filter(\n ({ belongsTo }) => belongsTo && belongsTo === item.itemid\n );\n }\n return [];\n};\n\nexport default {\n filterSlots,\n findSuperSection,\n getAllowedOverlap,\n getDependencyDiff,\n getSnappingPosition,\n shouldIgnoreCollision,\n shouldIgnoreSpacingBounds,\n getDependentItems,\n};\n","import range from './';\nimport tacHelpers from '../../tacHelpers';\nimport productService from '../../../../services/products';\nimport { ITEMS } from '../../../../constants';\n\nfunction getSectionsPreparedForUpdate(superSection, movedItem = null) {\n const sectionsSorted = superSection\n .filter(product => productService.isSection(product))\n .sort((a, b) => b.x - a.x)\n .filter(Boolean);\n\n return sectionsSorted.length ? sectionsSorted : [];\n}\n\nfunction replaceSectionsTransformedByMerge(superSection) {\n return superSection\n .filter(product => productService.isSection(product))\n .sort((a, b) => a.x - b.x)\n .map((section, index, allSections) => {\n if (\n allSections[index - 1]?.height > section.height &&\n allSections[index + 1]?.height > section.height\n ) {\n // This section has higher sections on both sides,\n // thereby changing its height\n const newHeight = [allSections[index - 1], allSections[index + 1]].sort(\n (a, b) => a.height - b.height\n )[0].height;\n\n // New height is equal to the lowest of the enclosing neighbors\n const replacementProduct = productService.getFit(section, {\n height: newHeight,\n });\n\n const newSection = tacHelpers.getSwitchableItem(\n section,\n replacementProduct\n );\n\n return newSection;\n }\n return section;\n });\n}\n\n/**\n * Splits an array of sections into two arrays of sections based on whether they\n * are located to the left or to the right of a specific section (dividingSection)\n * and returns an array of the resulting left/right arrays.\n * dividingSection may, but does not have to, be present in the input array. Even if\n * present, it will not be included neither in the left nor right arrays, since its x\n * value is neither less than nor greater than its own x value.\n * @param {Array} superSection The input array of sections.\n * @param {object} dividingSection The dividing section to compare to.\n * @returns {Array} The array containing the resulting left and right arrays.\n */\nfunction splitSuperSectionIntoLeftAndRightParts(superSection, dividingSection) {\n const superSectionParts = [];\n\n const left = superSection.filter(section => section.x < dividingSection.x);\n const right = superSection.filter(section => section.x > dividingSection.x);\n superSectionParts.push(left, right);\n\n return superSectionParts;\n}\n\n/**\n * Gets the caused updates and removals in the sections due to a specific move\n * @param {*} tac\n * @param {*} triggerItem The moved item causing the changes\n * @param {*} isPersistent False if the user is still dragging\n * @returns {Object}\n */\nconst getSectionChanges = (tac, triggerItem, isPersistent) => {\n let sectionsToUpdate = [];\n\n const tacItem = tacHelpers.getItem(tac, triggerItem.itemid);\n let currentSuperSection = tacItem && range.findSuperSection(tac, tacItem, []);\n\n if (currentSuperSection?.length > 1) {\n if (isPersistent) {\n currentSuperSection =\n replaceSectionsTransformedByMerge(currentSuperSection);\n }\n sectionsToUpdate.push(...getSectionsPreparedForUpdate(currentSuperSection));\n }\n\n const oldSuperSection = range\n .findSuperSection(tac, triggerItem, [])\n .filter(member => {\n return (\n !currentSuperSection ||\n !currentSuperSection.find(\n candidate => candidate.itemid === member.itemid\n )\n );\n });\n\n const superSectionParts = oldSuperSection\n ? splitSuperSectionIntoLeftAndRightParts(oldSuperSection, triggerItem)\n : [];\n superSectionParts.forEach(superSectionPart => {\n if (superSectionPart.length) {\n sectionsToUpdate.push(\n ...getSectionsPreparedForUpdate(superSectionPart, triggerItem)\n );\n }\n });\n\n if (!tacItem) {\n sectionsToUpdate = sectionsToUpdate.filter(\n section => section.itemid !== triggerItem.itemid\n );\n }\n\n return {\n updated: sectionsToUpdate,\n removed: sectionsToUpdate.flatMap(section => {\n // Remove all side panels, posts and cross braces in the sections staged for update,\n // so we can re-assemble them according to the new setup\n return section.items.filter(item =>\n productService.isType(item, ['side-panel', 'post', 'cross-brace'])\n );\n }),\n };\n};\n\n/**\n * Gets all the dependency changes caused by a change in the TAC\n * @param {*} tac\n * @param {*} options\n * @returns {Object}\n */\n\nfunction getDependencyDiff(tac, options = {}) {\n const { isPersistent, triggerItem, fromTacRemoveItem } = options;\n\n let diff = { added: [], updated: [], removed: [], untouched: [] };\n\n if (triggerItem && productService.isSection(triggerItem)) {\n diff = { ...diff, ...getSectionChanges(tac, triggerItem, isPersistent) };\n } else if (\n triggerItem &&\n productService.isType(triggerItem, ITEMS.SHELF_DRAWER)\n ) {\n const allItems = tac.items.reduce(\n (acc, curr) => [...acc, ...curr.items],\n []\n );\n const isShelfDrawer = item =>\n productService.isType(item, ITEMS.SHELF_DRAWER);\n if (!fromTacRemoveItem) {\n const shelfDrawersToUpdate = allItems.filter(isShelfDrawer).map(item => {\n return {\n ...item,\n parentRef: tacHelpers.getParent(tac, item).itemid,\n };\n });\n diff = {\n ...diff,\n updated: shelfDrawersToUpdate,\n };\n } else {\n const isShelf = item => productService.isType(item, ITEMS.SHELF);\n const isShelfDrawerWithoutShelf = shelfDrawer =>\n !shelfDrawer.items.find(item => isShelf(item));\n const shelfDrawersWithoutShelves = allItems\n .filter(isShelfDrawer)\n .filter(isShelfDrawerWithoutShelf)\n .map(item => {\n return {\n ...item,\n parentRef: tacHelpers.getParent(tac, item).itemid,\n };\n });\n diff = {\n ...diff,\n updated: [...shelfDrawersWithoutShelves],\n };\n }\n }\n return diff;\n}\n\nexport default { getDependencyDiff };\n\nexport { getDependencyDiff };\n","import geometry from '../../../../scene/util/geometry';\nimport productService, {\n isType,\n isSection,\n} from '../../../../services/products';\nimport constants from '../../../../settings/constants';\nimport { unique } from '../../../../util/array';\nimport tacHelpers from '../../tacHelpers';\nimport { getDependencyDiff } from './dependentItems';\nimport { ITEMS } from '../../../../constants';\nimport store from '../../..';\nimport _ from 'lodash';\n\nconst { SECTION_SIDE_UNITS, SECTION_POSTS, SHOE_SHELF } = ITEMS;\n\nconst INSERT_MIN_DISTANCE_FROM_TOP = 70;\n\n/**\n * Removes slots that should be closed in the extendable part of posts sections.\n * @param {Array} slots An array of slots describing the holes available to hang the component on it\n * @returns\n */\nfunction filterSlotsExtendablePart(slots) {\n return slots.filter(slot => {\n if (!isType(slot.parent, ITEMS.SECTION_POSTS) || isType(slot, ITEMS.POST))\n return true;\n\n const sectionHeightCurrent = slot.parent.height;\n const sectionHeightUnextended = slot.parent.filter.height;\n const itemsInExtendablePart = slot.parent.items.filter(\n item => item.y > sectionHeightUnextended\n );\n const otherItemsInExtendablePart = itemsInExtendablePart.filter(\n item => item.itemid !== slot.itemid\n );\n\n return otherItemsInExtendablePart.length\n ? slot.y + slot.height <=\n sectionHeightUnextended - INSERT_MIN_DISTANCE_FROM_TOP\n : slot.y + slot.height <=\n sectionHeightCurrent - INSERT_MIN_DISTANCE_FROM_TOP;\n });\n}\n\n/**\n * A function which will be called when dropping an ELVARLI component in the planner\n *\n * @param {Array} slots An array of slots describing the holes available to hang the component on it\n * @returns {Boolean}\n */\nfunction filterSlots(slots, tac) {\n const elvarliVariant = store.getState().rangeData.elvarliVariant;\n const slotsRemaining =\n elvarliVariant === ITEMS.SECTION_POSTS\n ? filterSlotsExtendablePart(slots)\n : slots;\n\n return slotsRemaining.filter(slot => {\n if (isType(slot, ITEMS.SHELF_DRAWER)) {\n const SHELF_DRAWER_MAX_Y = 1389;\n return slot.y + slot.height <= SHELF_DRAWER_MAX_Y;\n } else if (isType(slot, SHOE_SHELF)) {\n const SHOE_SHELF_MAX_Y = 2160;\n return slot.y + slot.height <= SHOE_SHELF_MAX_Y;\n } else if (\n isType(slot, ITEMS.SHELF_CLOTHES_RAIL) &&\n isType(slot.parent, ITEMS.SECTION_SIDE_UNITS)\n ) {\n const SHELF_CLOTHES_RAIL_MIN_Y = 900;\n return slot.y + slot.height >= SHELF_CLOTHES_RAIL_MIN_Y;\n }\n return slot => slot.y + slot.height <= slot.parent.height + slot.parent.y;\n });\n}\n\nfunction filterVariants(tac, variants) {\n const usableVariants = variants.filter(\n variant => !productService.hasMissingBrackets(variant)\n );\n\n return usableVariants;\n}\n\nfunction findSuperSection(tac, member, skiplist) {\n if (!tacHelpers.isWithinWall(member, tac)) {\n return [member];\n }\n\n const superSection = [member];\n\n const candidates = tac.items.filter(\n item =>\n item.itemid !== member.itemid &&\n skiplist.indexOf(item.itemid) === -1 &&\n tacHelpers.isWithinWall(item, tac)\n );\n\n const fullSizeMember = tacHelpers.getFullSize(member);\n\n const collisions = candidates.filter(candidate =>\n geometry.collides(fullSizeMember, tacHelpers.getFullSize(candidate))\n );\n const skip = skiplist\n .concat(collisions.map(item => item.itemid))\n .filter(unique);\n\n const rest = collisions.map(collision =>\n findSuperSection(tac, collision, skip)\n );\n\n return Array.prototype.concat.apply(superSection, rest).filter(unique);\n}\n\nfunction getWallResizingLimits(tac) {\n const tacAdjusted = _.cloneDeep(tac);\n const sectionsPosts =\n tacAdjusted.items?.filter(item => isType(item, ITEMS.SECTION_POSTS)) || [];\n sectionsPosts.forEach(sectionPosts => {\n const inserts =\n sectionPosts.items?.filter(item => !isType(item, ITEMS.POST)) || [];\n const highestPositionReachedByInsert = inserts.reduce((highest, insert) => {\n return Math.max(highest, insert.y + insert.height);\n }, 0);\n sectionPosts.height = Math.max(\n highestPositionReachedByInsert + INSERT_MIN_DISTANCE_FROM_TOP,\n constants.WALL.height.min\n );\n });\n const limits = tacHelpers.getLimits(tacAdjusted);\n return limits;\n}\n\n/**\n * Checks whether a section has an obstructing item placed\n * above the space in-between its posts\n *\n * @param {Object} section The section to check if blocked\n * @param {Object} overlapper The other section asking to overlap\n * @param {Array} nonSections All top-level items in the TAC that are not sections\n * @param {Object} tac The entire TAC\n * @returns {Boolean} If there is a blocking item or not\n */\nfunction isBlockedAboveCenter(section, overlapper, nonSections, tac) {\n const postWidth = productService.getPostWidth();\n const spaceBetweenAfterMerge = {\n ...section,\n x: section.x + postWidth,\n z: 0,\n width: section.width - postWidth * 2,\n height: Math.max(overlapper.height, section.height),\n };\n\n if (\n nonSections.some(item =>\n geometry.contains(spaceBetweenAfterMerge, { ...item, height: 1 })\n )\n ) {\n // Something is placed inside the area above the relative\n const superSection = findSuperSection(tac, section, [overlapper.itemid]);\n if (\n superSection.find(\n item =>\n isSection(item) &&\n item.height > section.height &&\n (item.x < section.x || item.x > section.x)\n )\n ) {\n // Our relative has a higher neighbour on at least one side,\n // meaning that the area above would become a section area post-merge\n return true;\n }\n }\n}\n\n/**\n * Checks the conditions for potential merge on respective side of the relative\n *\n * @param {Object} item The section asking for what overlap is allowed\n * @param {Object} relative The other section we might want to overlap\n * @param {Object} tac The entire TAC\n * @returns {Object} An object with boolean properties \"left\" and \"right\" stating whether overlap is allowed\n */\nfunction getBlockedDirections(item, relative, tac) {\n const postWidth = productService.getPostWidth();\n const blockedDirections = {\n left: false,\n right: false,\n };\n\n const nonSections = tac.items.filter(item => !isSection(item));\n\n if (isBlockedAboveCenter(relative, item, nonSections, tac)) {\n blockedDirections.left = true;\n blockedDirections.right = true;\n }\n\n // Either side could also be blocked if there is something placed above the actual post\n if (!blockedDirections.left) {\n const leftPostAfterMerge = {\n ...relative,\n width: postWidth,\n height: Math.max(item.height, relative.height),\n };\n\n const itemAfterMergeLeft = {\n ...item,\n x: leftPostAfterMerge.x - item.width,\n y: leftPostAfterMerge.y,\n z: leftPostAfterMerge.z,\n };\n\n blockedDirections.left =\n geometry.getCollidingRects(leftPostAfterMerge, nonSections).length ||\n isBlockedAboveCenter(itemAfterMergeLeft, relative, nonSections, tac);\n }\n\n if (!blockedDirections.right) {\n const rightPostAfterMerge = {\n ...relative,\n x: relative.x + relative.width - postWidth,\n width: postWidth,\n height: Math.max(item.height, relative.height),\n };\n\n const itemAfterMergeRight = {\n ...item,\n x: rightPostAfterMerge.x,\n y: rightPostAfterMerge.y,\n z: rightPostAfterMerge.z,\n };\n\n blockedDirections.right =\n geometry.getCollidingRects(rightPostAfterMerge, nonSections).length ||\n isBlockedAboveCenter(itemAfterMergeRight, relative, nonSections, tac);\n }\n\n return blockedDirections;\n}\n\n/**\n * Get the allowed overlap for an item to on another item (the relative)\n *\n * @param {Object} tac The entire TAC\n * @param {Object} item The item asking for what overlap is allowed\n * @param {Object} relative The other item we might want to overlap\n * @returns {Object} An object with the dimensions of the allowed overlap\n */\nfunction getAllowedOverlap(tac, item, relative) {\n let x = 0;\n let width = 0;\n const postWidth = productService.getPostWidth();\n\n const sameDepthSectionRelation =\n item.depth === relative.depth && [item, relative].every(isSection);\n\n if (sameDepthSectionRelation) {\n const blockedDirections = getBlockedDirections(item, relative, tac);\n\n x = !blockedDirections.left ? postWidth : 0;\n width =\n -postWidth *\n (blockedDirections.left && blockedDirections.right\n ? 0\n : !blockedDirections.left && !blockedDirections.right\n ? 2\n : 1);\n }\n\n return { x, y: 0, z: 0, width, height: 0, depth: 0 };\n}\n\n/**\n * Get the \"snapped\" position of a dragged item, if close enough to a suitable partner\n *\n * @param {Object} item The item asking to snap\n * @param {Array} rects All other rects on the scene\n * @returns {Object | undefined} An object with new coordinates for the snapped position,\n * or undefined if no suitable partner found\n */\nfunction getSnappingPosition(item, rects) {\n if (!isSection(item)) {\n return;\n }\n\n const postWidth = productService.getPostWidth();\n const sectionRects = rects.filter(isSection);\n const closeRects = geometry.getCollidingRects(item, sectionRects, {\n right: constants.SECTION_SNAPPING_DISTANCE + postWidth - 1,\n left: constants.SECTION_SNAPPING_DISTANCE + postWidth - 1,\n });\n\n const partner = geometry.closestCollidingRect(item, closeRects);\n\n if (!partner?.overlap || partner.depth !== item.depth) {\n return;\n }\n\n const { overlap } = partner;\n\n if (partner.x < item.x && overlap.width + overlap.x < 0) {\n return { x: partner.x + partner.width };\n } else if (partner.x > item.x && overlap.x > 0) {\n return { x: partner.x - item.width };\n }\n}\n\n/**\n * Checks whether an incoming item is allowed to ignore the spacing bounds of\n * an already present item.\n *\n * @param {Object} candidate The item asking to ignore its spacing bounds\n * @param {Object} relative The other item in the relation\n * @param {Object} [tac] The entire TAC\n * @returns {Boolean} True/false depending on if ignoring is allowed,\n */\nfunction shouldIgnoreSpacingBounds(candidate, relative = null, tac = null) {\n return false;\n}\n\nfunction bothAreCrossBraces(item, candidate) {\n return (\n isType(item, ITEMS.CROSS_BRACE) && isType(candidate, ITEMS.CROSS_BRACE)\n );\n}\n\nfunction itemIsPostBase(item) {\n return item.modelid.includes('post') && item.modelid.includes('_base');\n}\n\nfunction itemIsPostTop(item) {\n return item.modelid.includes('post') && item.modelid.includes('_top');\n}\n\nfunction itemIsDrawer(item) {\n return isType(item, ITEMS.DRAWER);\n}\n\nfunction itemIsShelf(item) {\n return isType(item, ITEMS.SHELF);\n}\n\nconst oneOfIsShelf = (item, candidate) => {\n return isType(item, ITEMS.SHELF) || isType(candidate, ITEMS.SHELF);\n};\n\nfunction itemIsShelfDrawer(item) {\n return isType(item, ITEMS.SHELF_DRAWER);\n}\n\nconst shelfDrawerShouldIgnoreShelfDrawer = (drawer, drawerSibling, parent) => {\n if (drawer.y === drawerSibling.y) {\n return false;\n }\n return true;\n};\n\nconst oneOfIsSideUnit = (item, candidate) => {\n return (\n isType(item, ITEMS.SECTION_SIDE_UNITS) ||\n isType(candidate, ITEMS.SECTION_SIDE_UNITS)\n );\n};\n\nconst oneOfIsPosts = (item, candidate) => {\n return (\n isType(item, ITEMS.SECTION_POSTS) || isType(candidate, ITEMS.SECTION_POSTS)\n );\n};\n\nconst oneItemIsBracket = (item, candidate) => {\n return isType(item, ITEMS.BRACKET) || isType(candidate, ITEMS.BRACKET);\n};\n\nfunction shouldIgnoreCollision(item, parent, candidate) {\n if (bothAreCrossBraces(item, candidate)) return true;\n if (\n (itemIsPostBase(item) && itemIsPostTop(candidate)) ||\n (itemIsPostBase(candidate) && itemIsPostTop(item))\n )\n return true;\n\n if (itemIsDrawer(candidate) && itemIsShelf(item) && itemIsShelfDrawer(parent))\n return true;\n if (itemIsDrawer(item) && itemIsShelf(candidate) && itemIsShelfDrawer(parent))\n return true;\n\n if (oneOfIsSideUnit(item, candidate) && oneOfIsShelf(item, candidate))\n return true;\n\n if (oneOfIsPosts(item, candidate) && oneOfIsShelf(item, candidate))\n return true;\n\n if (oneItemIsBracket(item, candidate)) return true;\n if (\n [item, candidate].every(item =>\n productService.isType(item, ITEMS.SHELF_DRAWER)\n )\n ) {\n return shelfDrawerShouldIgnoreShelfDrawer(item, candidate, parent);\n }\n\n return (\n isType(parent, [SECTION_SIDE_UNITS, SECTION_POSTS]) &&\n isType(candidate, [SECTION_SIDE_UNITS, SECTION_POSTS]) &&\n geometry.collides(parent, candidate)\n );\n}\n\n/**\n * Gets all the other items in the TAC that are dependent of a specific item\n * @param {*} item\n * @param {*} tac\n * @returns {Array}\n */\nconst getDependentItems = (item, tac) => {\n return [];\n};\n\nexport default {\n filterSlots,\n filterVariants,\n findSuperSection,\n getAllowedOverlap,\n getDependencyDiff,\n getSnappingPosition,\n shouldIgnoreCollision,\n shouldIgnoreSpacingBounds,\n getDependentItems,\n getWallResizingLimits,\n};\n","import { applicationSettings } from '../../../settings/application';\nimport Bror from './bror';\nimport Jonaxel from './jonaxel';\nimport Boaxel from './boaxel';\nimport Aurdal from './aurdal';\nimport Ivar from './ivar';\nimport Elvarli from './elvarli';\n\nfunction getRange(name) {\n switch (name) {\n case 'BROR':\n return Bror;\n case 'JONAXEL':\n return Jonaxel;\n case 'BOAXEL':\n return Boaxel;\n case 'AURDAL':\n return Aurdal;\n case 'IVAR':\n return Ivar;\n case 'ELVARLI':\n return Elvarli;\n default:\n console.error('Missing range-specific implementation ');\n return {};\n }\n}\n\nfunction partnerSlots(slots, otherSlots, tac) {\n return slots;\n}\n\nfunction causedBracketChanges(tac, item, parent) {\n return {\n add: [],\n remove: [],\n };\n}\n\nfunction getProppingItemsToAdapt(tac, movingItem, previous, original) {\n return [];\n}\n\nfunction findSuperSection(tac, member, skiplist) {\n return [];\n}\n\nfunction isSuperSectionHandle(item) {\n return false;\n}\n\nfunction getSuperSectionSpace(tac, supersection) {\n return [];\n}\n\nfunction getDependencyDiff(tac, options) {\n return null;\n}\n\nfunction getDependentItems(item, tac) {\n return [];\n}\n\nfunction filterSlots(slots) {\n return slots;\n}\n\nfunction filterVariants(tac, variants) {\n return variants;\n}\n\nfunction getAllowedOverlap(tac, item, relative) {\n return null;\n}\n\nfunction moveWithCr(item) {\n return false;\n}\n\nfunction isIncompleteSection(item) {\n return false;\n}\n\nfunction isMultiColouredSection(item, tac) {\n return false;\n}\n\nfunction getNeighbouringSections(section, tac, direction) {\n return [];\n}\n\nfunction preScreenSlots(variants, tac) {\n return variants;\n}\n\nfunction getCuttableMountingRailData(tac) {\n return [];\n}\n\nconst api = {\n partnerSlots,\n causedBracketChanges,\n getProppingItemsToAdapt,\n findSuperSection,\n isSuperSectionHandle,\n getSuperSectionSpace,\n getDependencyDiff,\n getDependentItems,\n filterSlots,\n filterVariants,\n getAllowedOverlap,\n moveWithCr,\n isIncompleteSection,\n isMultiColouredSection,\n getNeighbouringSections,\n preScreenSlots,\n getCuttableMountingRailData,\n};\n\nconst rangeApi = getRange(applicationSettings.applicationName);\nObject.assign(api, rangeApi);\n\nexport const range = api;\n","/* eslint-disable @typescript-eslint/no-use-before-define */\nimport _ from 'lodash';\n\nimport constants from '../../settings/constants';\nimport geometry from '../../scene/util/geometry';\nimport productService from '../../services/products';\nimport { floor } from '../../util/round';\nimport { filter } from '../../util/propFilter';\nimport {\n getOpenMatchingConnections,\n fitToPoint,\n getBounds,\n expandBounds,\n isWallMounted,\n isFloorStanding,\n getMatchingConnections,\n connectionPointOpen,\n topPropBounds,\n} from '../../services/products/models';\nimport { flatten, unique } from '../../util/array';\nimport idGenerator from '../../util/aactools/idGenerator';\nimport { range } from './range';\nimport { config as asConfig } from '../../scene/boaxel/AdjustableConfig';\nimport convert from '../../util/aactools/convert';\nimport { isFixedRoom } from '../../util/room';\nimport getItemConfig from '../../scene/util/getItemConfig';\nimport { ServiceSettings } from '@ikea-aoa/ikea-shared-services';\nimport { ITEMS } from '../../constants';\nimport { getArticleContent } from '../../services/products/articles';\n\nconst cache = {\n state: {\n tac: null,\n tacItems: null,\n getAllItems: null,\n },\n temp: {\n tacItems: null,\n getAllItems: null,\n },\n};\n\nfunction clearCache(tac) {\n Object.keys(cache.state).forEach(key => {\n cache.state[key] = null;\n });\n cache.state.tac = tac;\n cache.state.tacItems = getItems(tac);\n}\n\nfunction getItem(tac, itemid) {\n return getAllItems(getItems(tac)).find(item => item.itemid === itemid);\n}\n\nfunction getItems(tac) {\n return tac.items;\n}\n\nexport function getItemPrice(item) {\n const articleContent = getArticleContent(item.id);\n return articleContent.priceInformation.salesPrice[0].priceInclTax;\n}\n\nexport function getAllItems(items = [], omit) {\n if (cache.state.tacItems === items) {\n // this is the correct tac to cache\n if (cache.state.getAllItems) {\n return removeTree(cache.state.getAllItems, omit);\n }\n // this was first call using this tac\n cache.state.getAllItems = digAllItems(items);\n return removeTree(cache.state.getAllItems, omit);\n }\n // this is not the state tac. cache it somewhere else in case we're doing getOpenSlots\n if (cache.temp.tacItems !== items) {\n cache.temp.getAllItems = digAllItems(items);\n }\n return removeTree(cache.temp.getAllItems, omit);\n}\n\nfunction removeTree(items, remove = {}) {\n const root = items.find(item => item.itemid === remove.itemid) || {\n items: [],\n };\n const removeIDs = digAllItems(root.items || [])\n .map(item => item.itemid)\n .concat(root.itemid)\n .filter(Boolean);\n\n return items.filter(item => removeIDs.indexOf(item.itemid) === -1);\n}\n\nexport function digAllItems(items) {\n return items\n .map(item => {\n return digAllItems(item.items || []).concat(item);\n })\n .reduce((acc, item) => acc.concat(item), []);\n}\n\nfunction getItemIds(parent) {\n return parent.items.reduce((ids, item) => {\n ids.push(item.itemid);\n\n if (item.items) {\n return ids.concat(getItemIds(item));\n } else {\n return ids;\n }\n }, []);\n}\n\n/**\n * Recursively checks whether an item has any real articles\n * @param {Object} item\n * @returns {Boolean}\n */\nconst hasRealArticle = item => {\n return (\n productService.isRealArticleProduct(item) ||\n item.items?.some(hasRealArticle)\n );\n};\n\n/**\n * Recursively maps an item to only the properties wanted when comparing for changes\n * @param {Object} item\n * @param {Array} props the props to include in the return object\n * @param {Boolean|undefined} realArticles Whether to include only items with real articles\n * @returns {Object}\n */\nconst getComparableItem = (item, props, realArticles) => {\n if (realArticles && !hasRealArticle(item)) {\n return;\n }\n\n return props.reduce((acc, prop) => {\n return {\n ...acc,\n [prop]:\n prop === 'items'\n ? _.sortBy(\n item.items\n ?.map(item => getComparableItem(item, props))\n .filter(Boolean) || [],\n props\n )\n : item[prop],\n };\n }, {});\n};\n/**\n * Checks whether to version of tac items are identical in all important aspects\n *\n * @param {Array} currentItems\n * @param {Array} previousItems\n * @param {Object} [options] Specific options for the comparison\n * @returns {Boolean}\n */\nconst itemsUnchanged = (currentItems, previousItems, options = {}) => {\n const { props: specificProps, realArticles } = options;\n const props = specificProps || [\n 'id',\n 'x',\n 'y',\n 'z',\n 'width',\n 'height',\n 'depth',\n 'items',\n ];\n\n const currentForCompare = currentItems\n .map(item => getComparableItem(item, props, realArticles))\n .filter(Boolean);\n const previousForCompare = previousItems\n .map(item => getComparableItem(item, props, realArticles))\n .filter(Boolean);\n\n return _.isEqual(\n _.sortBy(currentForCompare, props),\n _.sortBy(previousForCompare, props)\n );\n};\n\nfunction isUnchanged(curr, prev) {\n if (curr === prev) {\n return true;\n }\n\n if (curr.items === prev.items && curr.wall === prev.wall) {\n return true;\n }\n\n return (\n _.isEqual(curr.wall, prev.wall) &&\n itemsUnchanged(curr.items, prev.items) &&\n _.isEqualWith(curr.settings, prev.settings, (value1, value2, key) => {\n return key === 'filter' ? true : undefined;\n })\n );\n}\n\nfunction getSections(tac, ...filters) {\n let sections = tac.items.filter(productService.isSection);\n\n if (filters) {\n sections = productService.filter(sections, ...filters);\n }\n\n return sections;\n}\n\nfunction getClothesRails(tac, ...filters) {\n let clothesRails = getAllItems(tac.items).filter(item =>\n productService.isType(item, ITEMS.CLOTHES_RAIL)\n );\n\n if (filters) {\n clothesRails = productService.filter(clothesRails, ...filters);\n }\n\n return clothesRails.map(cr => {\n const out = Object.assign({}, cr);\n out.parentItemId = getParent(tac, cr).itemid;\n return out;\n });\n}\n\nfunction getTopAncestor(tac, item) {\n let ancestor;\n let parent = item; // start value assuming item itself is stand alone\n while (parent && parent !== tac) {\n ancestor = parent;\n parent = getParent(tac, parent);\n }\n return ancestor;\n}\n\nfunction getPotentialBlockers(tac, item) {\n const allRelativesWithGlobalPos = getRawRects(tac, item);\n\n return allRelativesWithGlobalPos\n .filter(\n relative =>\n relative.logic &&\n relative.logic.extendable &&\n relative.connectsTo &&\n geometry.eclipsed(item, relative) &&\n getTopAncestor(tac, relative.connectsTo).itemid !==\n getTopAncestor(tac, item).itemid\n )\n .map(potentialBlocker => {\n return {\n ...potentialBlocker,\n topAncestor: getTopAncestor(tac, potentialBlocker),\n };\n });\n}\n\nfunction getParent(tac, item) {\n const allItems = getAllItems(getItems(tac));\n const parent = allItems.find(\n candidate =>\n candidate.items &&\n candidate.items.find(child => child.itemid === item.itemid)\n );\n\n if (parent) {\n return parent;\n }\n if (allItems.find(candidate => candidate.itemid === item.itemid)) {\n return tac;\n }\n}\n\nfunction getGlobalCoords(item, tac) {\n if (!item || item === tac) {\n return { x: 0, y: 0, z: 0 };\n }\n const parent = getParent(tac, item);\n const parentCoords = getGlobalCoords(parent, tac);\n\n return {\n ...item,\n x: item.x + parentCoords.x,\n y: item.y + parentCoords.y,\n z: item.z + parentCoords.z,\n };\n}\n\nfunction getArea(item, parent, connection, tac) {\n const placed = fitToPoint(item, connection, null, true);\n\n //only offset coords for child items\n let parentOffset = tac.items.includes(parent) ? { x: 0, y: 0, z: 0 } : parent;\n parentOffset = getGlobalCoords(parent, tac);\n\n return {\n x: placed.x + parentOffset.x,\n y: placed.y + parentOffset.y,\n z: placed.z + parentOffset.z,\n depth: placed.depth,\n width: placed.width,\n local: placed,\n };\n}\n\nfunction filterOutParts(parentItem) {\n return parentItem.items && parentItem.items.length && parentItem.parts\n ? parentItem.items.filter(\n childItem =>\n !Object.values(parentItem.parts).some(part => part === childItem.id)\n )\n : parentItem.items;\n}\n\nfunction getRoomSlots(space, rects, fitItem, slotItem, tac) {\n const fullFit = getFullSize(fitItem);\n const fullSlot = getFullSize(slotItem);\n\n return geometry\n .sliceRectangle(space, rects)\n .map(rect => ({ ...rect, z: 0, depth: constants.ROOM_DEPTH }))\n .filter(slot => {\n if (geometry.fits(slot, fullFit)) {\n const allowedOverlap = range.getAllowedOverlap(tac, slotItem, slotItem);\n if (allowedOverlap) {\n // if the slot is wider than our item,\n // but not wide enough to fit an entire overlap,\n // we can't fit\n return (\n slot.width === fullFit.width ||\n slot.width >= fullFit.width + allowedOverlap.x ||\n geometry.getCollidingRects(slot, rects, {\n left: allowedOverlap.x,\n right: allowedOverlap.x,\n }).length < 2\n );\n }\n return true;\n }\n //fullsize didnt fit, but if naked does, maybe we can squeeze it in\n if (geometry.fits(slot, fullSlot)) {\n // first find what prevented fullSize fit\n const collidingParts = rects.filter(rect =>\n geometry.collides(rect, fullFit)\n );\n // use individual parts instead of the fullSize\n const itemParts = getRects({ items: [slotItem] });\n // and see if the parts can coexist\n const collides = collidingParts.some(coll =>\n itemParts.some(rect => geometry.collides(rect, coll))\n );\n return !collides;\n }\n return false;\n });\n}\n\nfunction getVariants(currentItem, options = {}) {\n const swappableFilters = {\n color: currentItem.filter.color,\n variant: currentItem.filter.variant,\n };\n if (options.extendabilityLocked) {\n swappableFilters.extendable = currentItem.logic.extendable;\n }\n // to fit an item that comes in many sizes, eg a shelf, we find all similar items\n return productService\n .getSwappables(currentItem, swappableFilters)\n .map(variant => ({\n ...variant,\n itemid: currentItem.itemid, // and treat them as the same item\n items: filterOutParts(currentItem), // keep children, except those listed as parts on the item\n }));\n}\n\nfunction getSlot(outer, inner, tac, currentItem) {\n // since variants may be of different size\n // we need to figure out the size of the slot it goes into\n const box = getArea(outer.variant, outer, inner, tac);\n return {\n // make sure that it's the same object as in the tac,\n // since filtering in Section.drawDropAreas depends on this\n parent: {\n ...getItem(tac, outer.itemid),\n },\n ...currentItem,\n ...outer.variant,\n x: box.x,\n y: box.y,\n z: box.z,\n local: box.local,\n depth: box.depth,\n width: box.width,\n };\n}\n\nfunction hasOpenSlot(tac, currentItem, rebasedItems) {\n const variantOptions = {\n extendabilityLocked: currentItem.logic?.extendabilityLockedOnDrag,\n };\n\n const variants = range.filterVariants(\n tac,\n range.preScreenSlots(getVariants(currentItem, variantOptions), tac)\n );\n\n return variants.some(variant => {\n const connectionGroups = getMatchingConnections(variant, tac);\n\n let partnerSlots;\n\n if (productService.isMultiParentProduct(variant)) {\n partnerSlots = getOpenPartnerSlots(tac, variant, {\n noVariants: true,\n });\n }\n return connectionGroups.some(parent => {\n return parent.connections.some(point => {\n if (!connectionPointOpen(point, variant, parent, rebasedItems, tac)) {\n return false;\n }\n\n let slots = [getSlot(parent, point, tac, currentItem)];\n\n if (partnerSlots) {\n slots = range.partnerSlots(slots, partnerSlots, tac);\n }\n return range.filterSlots(slots, tac).length;\n });\n });\n });\n}\n\nfunction getOpenSlots(tac, currentItem, options) {\n return getSlots(tac, currentItem, options, getOpenMatchingConnections);\n}\n\nfunction getSlots(\n tac,\n currentItem,\n options = {},\n connectionFinder = getMatchingConnections\n) {\n const { noVariants, extendabilityLocked, proposedParent } = options;\n const allVariants = noVariants\n ? [currentItem]\n : getVariants(currentItem, { extendabilityLocked: extendabilityLocked });\n const variants = range.filterVariants(tac, allVariants);\n\n // now get connections for any size of currentItem\n const allItems = getAllItems(tac.items);\n\n // omit any clothes rails that connect to the item being fitted so it ca be put back\n const attachedExtendables = allItems.filter(\n item =>\n item.logic &&\n item.logic.extendable &&\n item.connectsTo &&\n item.connectsTo.itemid === currentItem.itemid\n );\n\n const dependentItems = range.getDependentItems(currentItem, tac);\n\n const outsiders = allItems.filter(item => {\n if (!proposedParent) {\n return !isWithinWall(item, tac);\n }\n return (\n proposedParent.itemid !== item.itemid &&\n !isAncestor(tac, proposedParent, item) &&\n !isWithinWall(item, tac)\n );\n });\n\n const ignored = [\n ...attachedExtendables,\n ...outsiders,\n ...dependentItems,\n currentItem,\n ].map(item => item.itemid);\n\n const examinedTac = filterTac(tac, ignored);\n\n const connections = flatten(\n variants.map(variant =>\n connectionFinder(variant, examinedTac, proposedParent)\n )\n ).map(outer => {\n return outer.connections.map(inner =>\n getSlot(outer, inner, tac, currentItem)\n );\n });\n\n let allSlots = flatten(connections).filter(unique);\n if (\n !options.isPartnerCall &&\n variants.some(variant => productService.isMultiParentProduct(variant))\n ) {\n const partnerSlots = getOpenPartnerSlots(tac, currentItem, options);\n allSlots = range.partnerSlots(allSlots, partnerSlots, tac);\n }\n\n return range.filterSlots(allSlots, tac);\n}\n\nfunction getMeasureBounds(item, options) {\n const bounds = {\n x: item.x || 0,\n y: item.y || 0,\n z: item.z || 0,\n height: item.height || 0,\n width: item.width || 0,\n depth: item.depth || 0,\n };\n\n if (productService.isType(item, ITEMS.SHOE_SHELF)) {\n const proppingOffset = topPropBounds(item);\n bounds.y += proppingOffset.height;\n }\n\n if (\n options &&\n options.realArticles &&\n (!item.id || !productService.isRealArticleProduct(item.id))\n ) {\n bounds.height = 0;\n bounds.width = 0;\n bounds.depth = 0;\n }\n return bounds;\n}\n\nfunction getLimits(item, room, options) {\n if (!item.items || item.items.length === 0) {\n const bounds = getMeasureBounds(item, options);\n return {\n min: {\n x: bounds.x,\n y: room ? 0 : bounds.y,\n z: bounds.z,\n },\n max: {\n x: bounds.x + bounds.width,\n y: bounds.y + bounds.height,\n z: bounds.z + bounds.depth,\n },\n };\n }\n\n const out = item.items.reduce(\n (limits, child) => {\n if (Array.isArray(child.items) && child.items.length) {\n const childLimits = getLimits(child, room, options);\n const x = Math.min(child.x, child.x + childLimits.min.x);\n const y = Math.min(child.y, child.y + childLimits.min.y);\n const z = Math.min(child.z, child.z + childLimits.min.z);\n if (\n options?.realArticles &&\n !productService.isRealArticleProduct(child.id)\n ) {\n child = { ...child, height: 0, width: 0, depth: 0 };\n }\n const width =\n Math.max(child.x + childLimits.max.x, child.x + child.width) - x;\n const height =\n Math.max(child.y + childLimits.max.y, child.y + child.height) - y;\n const depth =\n Math.max(child.z + childLimits.max.z, child.z + child.depth) - z;\n\n child = {\n id: child.id,\n x,\n y,\n z,\n width,\n height,\n depth,\n condensed: true,\n };\n }\n\n if (!productService.isRealArticleProduct(child) && !child.condensed) {\n return limits;\n }\n const bounds = getMeasureBounds(child);\n\n limits.min.x = Math.min(limits.min.x, bounds.x);\n limits.min.y = Math.min(limits.min.y, bounds.y);\n limits.min.z = Math.min(limits.min.z, bounds.z);\n limits.max.x = Math.max(limits.max.x, bounds.x + bounds.width);\n limits.max.y = Math.max(limits.max.y, bounds.y + bounds.height);\n limits.max.z = Math.max(limits.max.z, bounds.z + bounds.depth);\n\n return limits;\n },\n {\n min: {\n x: Infinity,\n y: room ? 0 : Infinity,\n z: 0,\n },\n max: {\n x: -Infinity,\n y: -Infinity,\n z: 0,\n },\n }\n );\n return out;\n}\n\nfunction getSize(tac, room, options) {\n const limits = getLimits(tac, room, options);\n\n return {\n width: limits.max.x - limits.min.x,\n height: limits.max.y - limits.min.y,\n depth: limits.max.z - limits.min.z,\n };\n}\n\nfunction getSpace(tac, wallResizerActive) {\n if (isFixedRoom()) {\n return wallResizerActive ? getMaxSpace() : getCurrentSpace(tac);\n }\n return getInitialSpace(tac);\n}\n\nfunction getInitialSpace(tac) {\n const limits = getLimits(tac);\n const width = limits.max.x - limits.min.x;\n const diff = constants.ROOM_MAX.width - width;\n\n return {\n width: width + 2 * diff,\n height: constants.ROOM_MAX.height,\n depth: constants.ROOM_DEPTH,\n x: limits.min.x - diff,\n y: 0,\n z: 0,\n };\n}\n\nfunction getCurrentSpace(tac) {\n if (tac && tac.wall) {\n const { width, height } = geometry.surround(tac.wall.points);\n\n return {\n x: 0,\n y: 0,\n z: 0,\n width,\n height,\n depth: constants.ROOM_DEPTH,\n };\n } else {\n const defaultWallPoints = [...constants.WALL.points];\n const xValues = defaultWallPoints.map(point => point.x);\n const yValues = defaultWallPoints.map(point => point.y);\n\n return {\n x: Math.min(...xValues),\n y: Math.min(...yValues),\n z: 0,\n width: Math.max(...xValues),\n height: Math.max(...yValues),\n depth: constants.ROOM_DEPTH,\n };\n }\n}\n\nfunction getMaxSpace() {\n return {\n x: 0,\n y: 0,\n z: 0,\n width: constants.WALL.width.max,\n height: constants.WALL.height.max,\n depth: constants.ROOM_DEPTH,\n };\n}\n\nfunction getSpaceForItem(space, item) {\n if (isWallMounted(item) && !isFloorStanding(item)) {\n // Items that only fit on the wall can't use the skirt area\n return {\n ...space,\n y: space.y + constants.SKIRT_HEIGHT,\n height: space.height - constants.SKIRT_HEIGHT,\n };\n }\n return space;\n}\n\n/**\n * Function to get room size with result of width/height\n *\n * @param {Object} tac an object of the tac should be passed to this function\n * @param {Number} ratio a number representing the ratio\n * @param {Object} minRoom a config with minimum width and height to calculate dimensions\n * @param {Object} options an object with options to be passed to the function\n * @returns {Object} an object with width and height dimensions.\n */\nfunction getRoom(tac, ratio, minRoom, options = {}) {\n if (minRoom?.width == null || minRoom?.height == null) {\n //NOTE: This missing config attribute can cause the page to be broken so we double check on it here\n throw new Error(\n 'Missing Configuration: minRoom is not defined in the constants config'\n );\n }\n\n function getWidth(height) {\n return floor(\n ratio * (height + bottomDepth) - leftDepth,\n constants.GRID.x.step\n );\n }\n\n function getHeight(width) {\n return floor(\n (width + leftDepth) / ratio - bottomDepth,\n constants.GRID.y.step\n );\n }\n\n const size = tac\n ? getSize(tac, true, options)\n : { width: 0, height: 0, depth: 0 };\n const leftDepth =\n Math.cos(constants.OBLIQUE_ANGLE) * constants.ROOM_DEPTH * 0.29;\n const bottomDepth =\n Math.sin(constants.OBLIQUE_ANGLE) * constants.ROOM_DEPTH * 0.29;\n\n const minimumSpaceRatio =\n (minRoom.width + leftDepth) / (minRoom.height + bottomDepth);\n\n let height;\n let width;\n\n if (size.width < minRoom.width && size.height < minRoom.height) {\n if (ratio > minimumSpaceRatio) {\n height = minRoom.height;\n width = getWidth(height);\n } else {\n width = minRoom.width;\n height = getHeight(width);\n }\n } else if (size.width < minRoom.width) {\n height = size.height;\n width = getWidth(height);\n\n if (width < minRoom.width) {\n width = minRoom.width;\n height = getHeight(width);\n }\n } else if (size.height < minRoom.height) {\n width = size.width;\n height = getHeight(width);\n\n if (height < minRoom.height) {\n height = minRoom.height;\n width = getWidth(height);\n }\n } else {\n height = size.height;\n width = getWidth(height);\n\n if (width < size.width) {\n width = size.width;\n height = getHeight(width);\n }\n }\n\n return {\n height,\n width,\n };\n}\n\nfunction withAllowedOverlaps(item, tac, fitItem) {\n if (!fitItem) {\n return item;\n }\n\n const allowedOverlap = range.getAllowedOverlap(tac, fitItem, item);\n\n if (!allowedOverlap) {\n return item;\n }\n\n const out = Object.assign({}, item);\n\n out.x += allowedOverlap.x;\n out.y += allowedOverlap.y;\n out.z += allowedOverlap.z;\n out.width += allowedOverlap.width;\n out.height += allowedOverlap.height;\n out.depth += allowedOverlap.depth;\n out.overlap = allowedOverlap;\n\n return out;\n}\n\nfunction withSpaceBounds(item, tac, fitItem) {\n let bounds = getBounds(item, 'space');\n if (fitItem && tac && item.logic && item.logic.extendable) {\n let shouldExpand;\n if (!fitItem.itemid) {\n shouldExpand = true;\n } else {\n const topAncestor = getTopAncestor(tac, item);\n const connectsToTopAncestor =\n topAncestor &&\n item.connectsTo &&\n getTopAncestor(tac, getItem(tac, item.connectsTo.itemid));\n shouldExpand =\n topAncestor &&\n topAncestor.itemid !== fitItem.itemid &&\n connectsToTopAncestor &&\n connectsToTopAncestor.itemid !== fitItem.itemid;\n }\n\n if (shouldExpand) {\n bounds = expandBounds(item, bounds);\n }\n }\n const out = Object.assign({}, item);\n out.x = item.x + bounds.pos.x;\n out.y = item.y + bounds.pos.y;\n out.z = item.z + bounds.pos.z;\n\n out.height = bounds.size.height;\n out.width = bounds.size.width;\n out.depth = bounds.size.depth;\n\n return out;\n}\n\nfunction getItemRects(item, omit, includeAll, parent = { x: 0, y: 0 }) {\n if (omit && item.itemid === omit.itemid) {\n return [];\n }\n const globalMe = Object.assign({}, item);\n globalMe.x = item.x + parent.x;\n globalMe.y = item.y + parent.y;\n\n const localMe = Object.assign({}, item);\n localMe.x = 0;\n localMe.y = 0;\n\n const kids = Array.isArray(item.items)\n ? flatten(item.items.map(item => getItemRects(item, omit, includeAll)))\n : [];\n return kids\n .filter(kid => includeAll || geometry.extendsOutside(kid, localMe))\n .map(localKid => {\n return {\n ...localKid,\n x: localKid.x + globalMe.x,\n y: localKid.y + globalMe.y,\n };\n })\n .concat(globalMe);\n}\n\nfunction getRawRects(tac, omit, includeAll) {\n return flatten(tac.items.map(item => getItemRects(item, omit, includeAll)));\n}\n\nfunction getRects(tac, omit, fullTac, includeAll = false) {\n return getRawRects(tac, omit, includeAll)\n .map(rect =>\n shouldIgnoreSpacingBounds(rect, omit, tac)\n ? rect\n : withSpaceBounds(rect, fullTac, omit)\n )\n .map(rect => withAllowedOverlaps(rect, fullTac, omit));\n}\n\nfunction hasPegboard(tac, filters = {}) {\n const allItems = getAllItems(tac.items);\n const someItems = filter(allItems, filters);\n return someItems.some(item => {\n return productService.isPegboard(item.id);\n });\n}\n\nfunction hasExtClothesRail(item) {\n const allItems = getAllItems(item.items || []);\n return allItems.some(\n item =>\n productService.isType(item, ITEMS.CLOTHES_RAIL) &&\n productService.isExtendable(item)\n );\n}\n\nfunction hasTable(item) {\n const allItems = getAllItems(item.items || []);\n return allItems.some(item => productService.isType(item, ITEMS.TABLE));\n}\n\nfunction hasBracket(item) {\n const allItems = getAllItems(item.items || []);\n return allItems.some(item => productService.isType(item, ITEMS.BRACKET));\n}\n\nfunction isClothesRailConnected(tac, item) {\n const topAncestor = getTopAncestor(tac, item);\n const itemAndChildren = getAllItems([topAncestor]);\n const allOtherItems = getAllItems(tac.items, topAncestor);\n\n return allOtherItems.some(\n item =>\n item.connectsTo &&\n itemAndChildren.some(child => child.itemid === item.connectsTo.itemid)\n );\n}\n\nfunction getConnectedClothesRails(tac, item) {\n const topAncestor = getTopAncestor(tac, item);\n const itemAndChildren = getAllItems([topAncestor]);\n const allOtherItems = getAllItems(tac.items, topAncestor);\n\n return (\n allOtherItems.filter(\n item =>\n item.connectsTo &&\n itemAndChildren.some(child => child.itemid === item.connectsTo.itemid)\n ) || []\n );\n}\n\nfunction hasSection(tac) {\n return tac.items.some(productService.isSection);\n}\n\nfunction modifySlot(slot, currentItem, rects, room, origo) {\n if (currentItem.items) {\n const newSlot = { ...slot };\n\n rects = rects.concat(\n {\n x: origo.x - 100,\n y: 0,\n height: room.height,\n width: 100,\n },\n {\n x: origo.x + room.width,\n y: 0,\n height: room.height,\n width: 100,\n }\n );\n\n const currentRects = getRects(currentItem);\n\n if (\n currentRects.every(item => {\n if (item.x < 0) {\n item = {\n ...item,\n x: newSlot.x + item.x,\n y: newSlot.y + item.y,\n };\n const colliding = geometry.getCollidingRects(item, rects);\n\n const offset = colliding.reduce(\n (offset, collider) =>\n Math.max(collider.x + collider.width - item.x, offset),\n 0\n );\n\n if (newSlot.width - offset < currentItem.width) {\n return false;\n }\n\n newSlot.x += offset;\n newSlot.width -= offset;\n } else if (\n item.x >= currentItem.width ||\n item.x + item.width > currentItem.width\n ) {\n item = {\n ...item,\n x: newSlot.x + (newSlot.width - currentItem.width) + item.x,\n y: newSlot.y + item.y,\n };\n\n const colliding = geometry.getCollidingRects(item, rects);\n\n const offset = colliding.reduce(\n (offset, collider) =>\n Math.max(item.x + item.width - collider.x, offset),\n 0\n );\n\n if (newSlot.width - offset < currentItem.width) {\n return false;\n }\n\n newSlot.width -= offset;\n }\n return true;\n })\n ) {\n return newSlot;\n } else {\n return null;\n }\n }\n\n return slot;\n}\n\n/**\n * Iterates over a changed items children, replacing them with versions that match their new parent.\n *\n * @param {Object} from The original parent\n * @param {Object} to The new version of the parent\n * @param {Object} filter Settings for how and what should be changed\n * @param {Object} filter Which filter properties the change should be based\n * @param {Object} [options] Additional options sometimes needed to conduct the change\n * @returns {Array} The replaced versions of the children\n */\nfunction getNewChildren(from, to, filter, options = {}) {\n if (!Array.isArray(from.items)) {\n return [];\n }\n\n let parent;\n\n return from.items\n .map(item => {\n if (options.targetType && options.targetType !== item.filter.type) {\n return item;\n } else if (productService.isMultiParentProduct(item)) {\n // we can't find slots for this item this way, so keep it\n return item;\n }\n\n parent =\n parent ||\n Object.assign({}, to, {\n // BOAXEL default section height is 1mm\n height: to.height > 1 ? to.height : from.height,\n items: [...(to.items || [])],\n itemid: to.itemid || idGenerator.fakeId(),\n });\n\n const slotSources = options.slotSources || [];\n let slots = getOpenSlots({ items: [parent, ...slotSources] }, item);\n\n const grandParent = options.newParent;\n if (!slots.length && grandParent) {\n // No slots found, but we might find some if we mock the tac change one level higher\n const fakeGrandParent = Object.assign({}, grandParent);\n fakeGrandParent.items = [...grandParent.items, parent];\n const fakeTac = { items: [fakeGrandParent] };\n slots = getOpenSlots(fakeTac, item);\n }\n\n if (!slots.length) {\n // still no matches. remove.\n return null;\n }\n\n const localSlots = slots.map(slot => ({\n ...slot.local,\n width: slot.width,\n parent: slot.parent,\n }));\n\n const parentSlots = localSlots.filter(\n slot => slot.parent.itemid === parent.itemid\n );\n\n const collidingSlots = geometry.getCollidingRects(item, parentSlots);\n\n let slot =\n collidingSlots.length || parentSlots.length\n ? geometry.closestCollidingRect(\n item,\n collidingSlots.length ? collidingSlots : parentSlots\n )\n : localSlots[0];\n\n const targetSlot = {\n width: slot.width,\n depth: slot.depth,\n logic: parent.logic,\n };\n\n if (!targetSlot[filter.switchingProp]) {\n /*\n If we are switching on \"variant\", this property is never relevant on children of a different type than its parent,\n e.g when switching to a shelving unit of variant \"narrow\", we don't want to use this variant for legs/pads.\n Other variantTypes might be relevant for children however,\n e.g. changing \"depth\" on a section should be used for it's children as well.\n */\n targetSlot[filter.switchingProp] =\n filter.switchingProp !== 'variant' || filter.type === item.filter.type\n ? filter[filter.switchingProp]\n : null;\n }\n\n const refitted = productService.getFit(item, targetSlot);\n if (refitted && productService.isType(refitted, 'leg')) {\n //Legs are a special case that needs a new slot calculation after the re-fit\n const slots = getOpenSlots({ items: [parent] }, refitted);\n const localSlots = slots.map(slot => ({\n ...slot.local,\n width: slot.width,\n parent: slot.parent,\n }));\n const collidingSlots = geometry.getCollidingRects(item, localSlots);\n slot = geometry.closestCollidingRect(item, collidingSlots);\n }\n\n // If we're using slotSources we cannot trust the slot positioning\n // since it will be relative to another item\n const isParentSlot = slot.parent?.itemid === parent.itemid;\n const pos = isParentSlot && {\n x: slot.x,\n y: slot.y,\n z: slot.z,\n };\n\n const newItem = {\n ...item,\n ...refitted,\n ...pos,\n itemid: item.itemid,\n };\n\n // if a frame with baskets is being replaced, the frame isn't refitted,\n // so replace with itself\n newItem.items = getNewChildren(\n item,\n ServiceSettings?.applicationName?.toLowerCase() === 'boaxel'\n ? { ...newItem, x: 0, y: 0 }\n : newItem,\n filter,\n options\n );\n\n if (item.logic && item.logic.extendable) {\n const testItem = { ...newItem };\n testItem.width = parent.width;\n if (!geometry.extendsOutside(testItem, parent, 'x')) {\n newItem.width = parent.width;\n }\n }\n\n parent.items.push(newItem);\n return newItem;\n })\n .filter(Boolean);\n}\n\nfunction getSwitchableItem(from, to, options = {}) {\n const replacementItem = {\n ...productService.getProduct(to.id),\n x: from.x,\n y: from.y,\n z: from.z,\n itemid: from.itemid,\n propping: from.propping,\n uprights: from.uprights,\n };\n\n if (productService.isExtendable(to)) {\n replacementItem.width = to.width || from.width;\n }\n\n // switchingProp can be e.g. depth, variant or color\n const filter = {\n type: to.type,\n switchingProp: options.switchingProp,\n [options.switchingProp]: to.value,\n };\n\n const newItem = {\n ...replacementItem,\n items: getNewChildren(from, replacementItem, filter, options),\n };\n\n return newItem;\n}\n\nfunction diffFromTac(tac, moveItem) {\n const original = getItem(tac, moveItem.itemid);\n const gOriginal = getGlobalCoords(original, tac);\n return {\n x: moveItem.x - gOriginal.x,\n y: moveItem.y - gOriginal.y,\n z: moveItem.z - gOriginal.z,\n };\n}\n\n/* return a tac with all items specified removed */\nfunction filterTac(tac, itemids = []) {\n const rest = Object.assign({}, tac);\n rest.items = (tac.items || [])\n .filter(item => !itemids.includes(item.itemid))\n .map(item => filterTac(item, itemids));\n return rest;\n}\n\nfunction isAncestor(tac, ancestor, descendant) {\n if (ancestor === descendant) {\n console.warn('checking if descendant is ancestor of itself!');\n return false;\n }\n if (ancestor && ancestor.itemid && descendant && descendant.itemid) {\n // ancestor may be stripped of some children ie the extendable clothes rail\n // so need to look up the real object\n ancestor = getItem(tac, ancestor.itemid);\n\n if (!ancestor) {\n console.warn('looking for a missing ancestor!');\n return false;\n }\n return getAllItems(ancestor.items || [])\n .map(item => item.itemid)\n .includes(descendant.itemid);\n }\n return false;\n}\n\nfunction isWithinWall(item, tac = null, isGlobal = false) {\n const gItem = tac && !isGlobal ? getGlobalCoords(item, tac) : item;\n\n if (gItem.y < 0) {\n return false;\n }\n\n if (!isFixedRoom() || !tac?.wall) {\n return true;\n }\n\n const wall = {\n ...geometry.surround(tac.wall.points),\n depth: constants.ROOM_DEPTH,\n };\n\n return geometry.contains(wall, gItem);\n}\n\nfunction getWallLimits(tac) {\n if (!tac.wall) {\n return;\n }\n\n return {\n max: {\n x: Math.max(...tac.wall.points.map(point => point.x)),\n y: Math.max(...tac.wall.points.map(point => point.y)),\n },\n min: {\n x: Math.min(...tac.wall.points.map(point => point.x)),\n y: Math.min(...tac.wall.points.map(point => point.y)),\n },\n };\n}\n\nfunction getVerticalNeighbours(section, superSection) {\n const sections = superSection.filter(\n item => item.itemid !== section.itemid && productService.isSection(item)\n );\n\n return sections.filter(cand => {\n return geometry.collides(\n { ...section, y: 0, height: constants.ROOM_MAX.height },\n cand\n );\n });\n}\n\nfunction extendableLimits(tac, item) {\n if (!productService.isAdjustable(item)) {\n return null;\n }\n if (productService.isType(item, ITEMS.CLOTHES_RAIL)) {\n return crLimits(tac, item);\n }\n if (productService.isType(item, ITEMS.SECTION)) {\n // if there are other sections that could interfere, disallow\n\n const supersection = findSuperSection(tac, item);\n const blockers = getVerticalNeighbours(item, supersection);\n\n if (blockers.length) {\n /*\n Vertical neighbours (inside same supersection) are considered blockers,\n since it can be very hard to pre-validate width change.\n\n but, we ignore lagkapten sections that are empty\n */\n\n const relevant = blockers.filter(\n blocker => blocker.filter.switchable || blocker.items?.length\n );\n\n if (relevant.length) {\n return null;\n }\n }\n\n const xMargin = getXmargin(tac);\n\n return {\n min: asConfig.section.minWidth,\n max: Math.min(asConfig.section.maxWidth, item.width + xMargin),\n };\n }\n}\nfunction getCondensedWidth(items) {\n return items.reduce(\n (result, item) => {\n if (item.x + item.width > result.reachedX) {\n // item can overlap backwards so need to adjust\n const backwardOverlap =\n item.x < result.reachedX ? result.reachedX - item.x : 0;\n result.width += item.width - backwardOverlap;\n result.reachedX = item.x + item.width;\n }\n return result;\n },\n { width: 0, reachedX: Math.min(...items.map(item => item.x)) }\n ).width;\n}\n\nfunction getCondensedTacWidth(tac) {\n const fullItems = tac.items.map(getFullSize).sort((a, b) => a.x - b.x);\n return getCondensedWidth(fullItems);\n}\n\nfunction getXmargin(tac) {\n const xAdjustedItems = convert.resetX([...tac.items]);\n const fullItems = xAdjustedItems.map(getFullSize).sort((a, b) => a.x - b.x);\n const condensedTacWidth = getCondensedWidth(fullItems);\n const roomWidth =\n tac.wall && tac.wall.points\n ? geometry.surround(tac.wall.points).width\n : constants.ROOM_MAX.width;\n return roomWidth - condensedTacWidth;\n}\n\nfunction crLimits(tac, cr) {\n const topAncestor = getTopAncestor(tac, cr);\n const chain = getClothesRailChain(tac, topAncestor, cr);\n if (\n [chain.leftEnd, chain.rightEnd].every(end =>\n productService.isType(end, 'sidewall')\n )\n ) {\n return {\n max: cr.width,\n min: cr.width,\n };\n }\n const crConfig = getItemConfig(cr);\n const mountOffset = crConfig.mountOffset;\n const limits = {\n max: crConfig.maxWidth,\n min: crConfig.minWidth,\n };\n\n const xMargin = getXmargin(tac);\n\n limits.max = Math.min(limits.max, cr.width + xMargin);\n\n //FIXME: Remove this?\n // if there's a wider clothes rail above us, this limits expansion\n const rects = getRawRects(tac);\n const crRect = rects.find(rect => rect.itemid === cr.itemid);\n if (!crRect) return;\n const otherRects = rects\n .filter(rect => rect !== crRect)\n .filter(\n rect =>\n !(\n productService.isMultiParentProduct(rect) &&\n rect.width === crRect.width\n )\n );\n\n // expand cr to reach floor and ceiling\n crRect.height = constants.ROOM_MAX.height;\n crRect.y = 0;\n // and tighten it a bit\n crRect.x += mountOffset + 1;\n crRect.width -= mountOffset * 2 + 2;\n\n const collisions = geometry.getCollidingRects(crRect, otherRects);\n\n if (collisions.length) {\n // if we collide with another clothes rail that's another width,\n // we don't allow resize.\n if (collisions.some(productService.isMultiParentProduct)) {\n return {\n min: cr.width,\n max: cr.width,\n };\n }\n const parents = collisions.filter(\n item => getTopAncestor(tac, item) === item\n );\n\n const fullParents = parents.map(getFullSize);\n const condensedParentsWidth = getCondensedWidth(fullParents);\n limits.min = Math.max(limits.min, condensedParentsWidth + 2 * mountOffset);\n }\n\n return limits;\n}\n\nfunction getFullSize(item, parent = null) {\n const out = {\n x: item.x,\n y: item.y,\n height: item.height,\n width:\n //The ext CR is allowed to deduct the part of itself that will be extending into another frame\n productService.isMultiParentProduct(item)\n ? item.width - (getItemConfig(item)?.mountOffset || 0)\n : item.width,\n logic: item.logic,\n filter: item.filter,\n valid: item.valid,\n };\n if (parent && item.x + item.width > parent.width) {\n out.verticalExtensionPoint = parent.y + item.y - item.height;\n }\n if (!item.items || !item.items.length) {\n return out;\n }\n\n const children = item.items.map(child => getFullSize(child, item));\n const merged = geometry.mergeKids(item, children);\n merged.logic = item.logic;\n merged.filter = item.filter;\n merged.valid = item.valid;\n merged.items = item.items;\n\n if (children.some(child => child.verticalExtensionPoint)) {\n merged.verticalExtensionPoint = children\n .filter(child => child.verticalExtensionPoint)\n .reduce(function (prev, current) {\n return prev.verticalExtensionPoint < current.verticalExtensionPoint\n ? prev\n : current;\n }).verticalExtensionPoint;\n }\n return merged;\n}\n\nfunction getClothesRailChain(tac, item, cr) {\n const config = getItemConfig(cr);\n function findLeftmostItem(item, otherItems, tac) {\n function findLeft(item, otherItems, tac) {\n const fullSizeItem = getFullSize(item);\n const leftTopAncestorsWithCrs = otherItems.filter(\n left =>\n left.x < fullSizeItem.x &&\n (range.moveWithCr(left) || isClothesRailConnected(tac, left))\n );\n\n const collidingItems = [];\n leftTopAncestorsWithCrs.forEach(item => {\n const fullSize = getFullSize(item);\n\n if (hasExtClothesRail(item)) {\n fullSize.width += config.mountOffset;\n }\n if (\n geometry.getCollidingRects(\n fullSizeItem,\n [fullSize],\n hasExtClothesRail(item) && { left: 1 }\n ).length\n ) {\n collidingItems.push(item);\n }\n });\n\n if (collidingItems.length === 0) {\n return [];\n }\n\n const closestItem = geometry.closest(collidingItems, 'left');\n const remaining = leftTopAncestorsWithCrs.filter(\n i => i.itemid !== closestItem.itemid\n );\n return [closestItem].concat(findLeft(closestItem, remaining, tac));\n }\n\n const leftItems = findLeft(item, otherItems, tac);\n\n return (\n leftItems.length &&\n leftItems.reduce(function (prev, current) {\n return prev.x < current.x ? prev : current;\n })\n );\n }\n\n function findRightmostItem(item, otherItems, tac) {\n function findRight(item, otherItems, tac) {\n const fullSizeItem = getFullSize(item);\n const rightTopAncestorsWithCrs = otherItems.filter(\n right =>\n right.x > fullSizeItem.x &&\n (range.moveWithCr(right) || isClothesRailConnected(tac, right))\n );\n\n fullSizeItem.width += config.mountOffset;\n const collidingItems = geometry.getCollidingRects(\n fullSizeItem,\n rightTopAncestorsWithCrs,\n hasExtClothesRail(item) && { right: 1 }\n );\n\n if (collidingItems.length === 0) {\n return [];\n }\n const closestItem = geometry.closest(collidingItems, 'right');\n const remaining = rightTopAncestorsWithCrs.filter(\n i => i.itemid !== closestItem.itemid\n );\n\n const nextItem = range.moveWithCr(closestItem) ? closestItem : item;\n\n return [closestItem].concat(findRight(nextItem, remaining, tac));\n }\n\n const rightItems = findRight(item, otherItems, tac);\n\n return (\n rightItems.length &&\n rightItems.reduce(function (prev, current) {\n return prev.x > current.x ? prev : current;\n })\n );\n }\n\n const parent = getParent(tac, item);\n\n const otherItems = parent.items\n ? parent.items.filter(i => i.itemid !== item.itemid)\n : [];\n\n const chain = {\n leftEnd: findLeftmostItem(item, otherItems, tac) || item,\n rightEnd: findRightmostItem(item, otherItems, tac),\n };\n\n return chain;\n}\n\nfunction withoutPartner(tac, partner) {\n function without(items, remove) {\n if (items.find(item => item.itemid === remove.itemid)) {\n return items.filter(item => item.itemid !== remove.itemid);\n }\n return items.map(item => ({\n ...item,\n items: without(item.items || [], remove),\n }));\n }\n\n const out = {\n ...tac,\n items: without(tac.items, partner),\n };\n return out;\n}\n\nfunction getOpenPartnerSlots(tac, partner, options) {\n return getPartnerSlots(tac, partner, options, getOpenSlots);\n}\n\nfunction getPartnerSlots(tac, partner, options = {}, slotFinder = getSlots) {\n const config = range.getPartnerConfig(partner);\n const outsideProduct = productService.getProduct(config.outerId);\n const partnerlessTac = withoutPartner(tac, partner);\n options.isPartnerCall = true;\n\n return slotFinder(partnerlessTac, outsideProduct, options);\n}\n\nfunction findSuperSection(tac, member, skiplist = []) {\n return range.findSuperSection(tac, member, skiplist);\n}\n\nfunction isSuperSectionHandle(item) {\n return range.isSuperSectionHandle(item);\n}\n\nfunction getSuperSectionSpace(tac, supersection) {\n return range.getSuperSectionSpace(tac, supersection);\n}\n\nfunction getProppingItemsToAdapt(tac, movingItem, previous, original) {\n return range.getProppingItemsToAdapt(tac, movingItem, previous);\n}\n\n/**\n * Gets the snapping position (x-position) to the closest colliding item.\n * @param {Object} item\n * @param {Array} rects\n * @returns The snapping x-position to the closest colliding item.\n */\nfunction getSnappingPosition(item, rects) {\n return range.getSnappingPosition?.(item, rects);\n}\n\n/**\n * Gets the y-coordinate that the item should snap to if the item is placed on the skirt.\n * @param {Object} item\n * @returns {Object} An object with the y-coordinate that the item should snap to in relation to the skirt.\n */\nfunction getSkirtSnappingPosition(item) {\n return {\n y: this.getSkirtSnappingYCoordinate(item),\n };\n}\n\n/**\n * If the y-coordinate of the item is less than half of the skirt height it will snap to the floor, if its more than half\n * of the skirt height it will snap to the top of the skirt.\n * @param {Object} item\n * @returns {number} The y-coordinate where the item shold snap to.\n */\nfunction getSkirtSnappingYCoordinate(item) {\n const halfOfSkirtHeight = constants.SKIRT_HEIGHT / 2;\n return item.y >= halfOfSkirtHeight ? constants.SKIRT_HEIGHT : 0;\n}\n\nfunction getWallResizingLimits(tac) {\n return range.getWallResizingLimits?.(tac) || getLimits(tac);\n}\n\nfunction shouldIgnoreCollision(item, parent, candidate) {\n if (range.shouldIgnoreCollision?.(item, parent, candidate)) {\n return true;\n }\n\n return (\n (item.logic?.noCollision || candidate.logic?.noCollision) &&\n item.filter.type !== candidate.filter.type\n );\n}\n\nfunction isPartOf(item, parent) {\n return (\n parent.parts && Object.values(parent.parts).some(part => part === item.id)\n );\n}\n\n/**\n * Checks whether an incoming item is allowed to ignore the spacing bounds of\n * an already present item.\n *\n * @param {Object} candidate The item asking to ignore its spacing bounds\n * @param {Object} relative The other item in the relation\n * @param {Object} [tac] The entire TAC\n * @returns {Boolean | undefined} True/false depending on if ignoring is allowed,\n * or undefined if no range specific implementation exists\n */\nfunction shouldIgnoreSpacingBounds(candidate, relative, tac) {\n return range.shouldIgnoreSpacingBounds?.(candidate, relative, tac);\n}\n\nfunction isItem(item) {\n return !!item.itemid;\n}\n\n/**\n * Recursive function for updating all items with certain ids\n * @param items {array}\n * @param ids {array}\n * @param updateFunc {function} Function for doing the updating. Need to return an object\n * @param args {any} Any other args that might be needed for the function doing the updating\n * @returns {*}\n */\nconst addToAllWithId = (items, ids, updateFunc, ...args) =>\n items.map(item => {\n const obj = {\n ...item,\n ...(item.items && {\n items: addToAllWithId(item.items, ids, updateFunc, ...args),\n }),\n };\n\n return ids.includes(item.id)\n ? { ...obj, ...updateFunc(item, ...args) }\n : obj;\n });\n\nexport default {\n clearCache,\n extendableLimits,\n diffFromTac,\n filterTac,\n getItem,\n getItemIds,\n getItems,\n getAllItems,\n getLimits,\n getTopAncestor,\n getPotentialBlockers,\n getParent,\n getGlobalCoords,\n getItemRects,\n getRawRects,\n getRects,\n getRoom,\n getRoomSlots,\n getOpenSlots,\n getSlots,\n getClothesRails,\n getSections,\n getSize,\n getSpace,\n getInitialSpace,\n getCurrentSpace,\n getMaxSpace,\n getSpaceForItem,\n hasOpenSlot,\n hasSection,\n hasExtClothesRail,\n hasTable,\n hasBracket,\n isClothesRailConnected,\n isAncestor,\n isWithinWall,\n getWallLimits,\n findSuperSection,\n isSuperSectionHandle,\n getSuperSectionSpace,\n getVerticalNeighbours,\n modifySlot,\n hasPegboard,\n getConnectedClothesRails,\n getSwitchableItem,\n getNewChildren,\n getFullSize,\n getClothesRailChain,\n getProppingItemsToAdapt,\n getXmargin,\n getSnappingPosition,\n getSkirtSnappingPosition,\n getSkirtSnappingYCoordinate,\n withAllowedOverlaps,\n isUnchanged,\n getCondensedTacWidth,\n getWallResizingLimits,\n shouldIgnoreCollision,\n isPartOf,\n shouldIgnoreSpacingBounds,\n isItem,\n addToAllWithId,\n itemsUnchanged,\n hasRealArticle,\n range,\n __test__: {\n getCondensedWidth,\n },\n};\n","import { flatten } from '../../util/array';\nimport geometry from '../../scene/util/geometry';\nimport tacHelpers from '../../state/tac/tacHelpers';\nimport productService from './';\nimport { ITEMS } from '../../constants';\n\nlet products;\nlet productMap;\nlet modelMap;\n\nconst connectionCache = {\n tac: null,\n item: null,\n};\n\nexport function init(files) {\n products = files.find(file => file.hasOwnProperty('products')).products;\n productMap = products.reduce((map, prod) => {\n map[prod.id] = prod;\n return map;\n }, {});\n\n modelMap = files.find(file => file.hasOwnProperty('models')).models;\n}\n\nfunction getModel(item) {\n const product = productMap[item.id];\n return modelMap[product.modelid];\n}\n\nexport function getBounds(model, type) {\n if (!model.bounds) {\n // this is not a model\n model = getModel(model);\n }\n // we want the largest bound so we sort them by volume\n const boundsBySize = Object.values(model.bounds)\n .filter(bounds => bounds.type === type)\n .sort(\n (a, b) =>\n a.size.height * a.size.width * a.size.depth -\n b.size.height * b.size.width * b.size.depth\n );\n if (boundsBySize.length) {\n return boundsBySize.pop();\n }\n\n // if there are no space bounds, try collision bounds,\n // since default bounds might be 'size'\n if (type === 'space') {\n return getBounds(model, 'collision');\n }\n\n // there were no bounds of specified type\n return model.bounds.default;\n}\n\nfunction moveOrigo(connection, model) {\n const bounds = model.bounds.default.size;\n const out = {\n x: connection.x + bounds.width / 2,\n y: connection.y + bounds.height / 2,\n z: connection.z + bounds.depth / 2,\n rx: connection.rx,\n ry: connection.ry,\n rz: connection.rz,\n name: connection.name,\n };\n\n return out;\n}\n\nexport function getProppingBounds(item) {\n const model = getModel(item);\n return Object.values(model.bounds).filter(\n bounds => bounds.type === 'propping'\n );\n}\n\nexport function getConnections(item) {\n const model = getModel(item);\n return Object.entries(model.connections).reduce((out, [key, val]) => {\n out[key] = moveOrigo(val, model);\n out[key][key] = key;\n return out;\n }, {});\n}\n\nfunction getOffsets(item, connectionName) {\n const connections = getConnections(item);\n const localConn =\n connectionName && connections[connectionName]\n ? [connections[connectionName]]\n : Object.values(connections);\n if (localConn.length === 1) {\n return localConn[0];\n }\n return { x: 0, y: 0, z: 0 };\n}\n\nfunction getOffset(item, connectionName, prop) {\n const offsets = getOffsets(item, connectionName);\n return -offsets[prop] || 0;\n}\n\nexport function getYoffset(item, connectionName) {\n return getOffset(item, connectionName, 'y');\n}\n\nfunction matchingConnections(model, parent) {\n const parentModel = getModel(parent);\n\n const modelConnections = Object.keys(model.connections);\n const parentsRelations = flatten(Object.values(parentModel.relations));\n return parentsRelations\n .filter(function (relation) {\n return modelConnections.includes(relation.child);\n })\n .map(function (relation) {\n const conn = parentModel.connections[relation.parent];\n conn.rx = relation.pos.rx;\n conn.ry = relation.pos.ry;\n conn.rz = relation.pos.rz;\n conn.name = relation.child;\n return conn;\n });\n}\n\nexport function isWallMounted(model) {\n if (!model.bounds) {\n // this is not a model\n model = getModel(model);\n }\n return !!model.logic.wall;\n}\n\nexport function isFloorStanding(model) {\n if (!model.bounds) {\n // this is not a model\n model = getModel(model);\n }\n return !!model.logic.floor;\n}\n\nexport function getMatchingConnections(item, tac) {\n if (!item || !tac) {\n throw new Error('getConnections: bad input');\n }\n\n const product = productMap[item.id];\n if (!product) {\n throw new Error('getConnections: bad product');\n }\n\n const model = modelMap[product.modelid];\n if (!model) {\n throw new Error('getConnections: bad model');\n }\n if (item === connectionCache.item && tac === connectionCache.tac) {\n return connectionCache.connections;\n }\n\n // now that we're sure that the item has non-generic connections,\n // map all the connections to matching parents\n // ignoring the groups\n const out = tacHelpers\n .getAllItems(tac.items, item)\n .map(parent => {\n const out = Object.assign({}, parent);\n const parentModel = getModel(parent);\n out.connections = matchingConnections(model, parent).map(connection =>\n moveOrigo(connection, parentModel)\n );\n return out;\n })\n .filter(item => item.connections.length);\n if (isWallMounted(model)) {\n out.push({\n wall: true,\n connections: [],\n });\n }\n\n if (isFloorStanding(model)) {\n out.push({\n floor: true,\n connections: [],\n });\n }\n\n const withVariant = out.map(group => ({\n ...group,\n variant: item,\n }));\n\n connectionCache.tac = tac;\n connectionCache.item = item;\n connectionCache.connections = withVariant;\n\n return withVariant;\n}\n\nfunction getOnlyMatchingConnectionPoint(child, parent) {\n const connParents = getMatchingConnections(child, { items: [parent] });\n return (\n connParents?.[0]?.connections?.length === 1 && connParents[0].connections[0]\n );\n}\n\nexport function fitToPoint(item, point, boundsType, skipChildren = false) {\n const bounds = getBounds(getModel(item), boundsType);\n\n let size;\n let pos;\n\n if (!skipChildren && item.items && item.items.length) {\n const parent = Object.assign({}, bounds.pos, bounds.size);\n const merged = geometry.mergeKids(\n parent,\n item.items.map(child => {\n const fitted = productService.getFit(child, item) || child;\n\n // TODO: How de we handle childs with more than one connection point here?\n const childPoint = getOnlyMatchingConnectionPoint(fitted, item);\n\n if (childPoint) {\n return fitToPoint(fitted, childPoint, boundsType);\n }\n\n return { x: 0, y: 0, z: 0, width: 0, height: 0, depth: 0 };\n })\n );\n pos = { x: merged.x, y: merged.y, z: merged.z };\n size = { width: merged.width, height: merged.height, depth: merged.depth };\n } else {\n pos = bounds.pos;\n size = bounds.size;\n }\n\n const out = Object.assign({}, item, size, point);\n if (point.ry) {\n // assume == PI\n out.x -= size.width;\n out.x -= pos.x;\n } else {\n out.x += pos.x;\n }\n out.y += pos.y;\n out.z += pos.z;\n\n const offsets = getOffsets(item, point.name);\n out.x -= offsets.x;\n out.y -= offsets.y;\n out.z -= offsets.z;\n\n return out;\n}\n\nexport function expandBounds(item, bounds) {\n const collisionBounds = item.id\n ? getBounds(getModel(item), 'collision')\n : { pos: item };\n function expand(axis, prop) {\n const replace = item[prop];\n if (Number.isFinite(replace)) {\n const offset =\n axis && bounds.pos\n ? bounds.pos[axis] || 0 - collisionBounds.pos[axis] || 0\n : 0;\n return Math.max(replace, bounds.size[prop]) - 2 * offset;\n }\n return bounds.size[prop];\n }\n return {\n ...bounds,\n size: [\n ['x', 'width'],\n [false, 'height'],\n [false, 'depth'],\n ].reduce(\n (out, [axis, prop]) => {\n out[prop] = expand(axis, prop);\n return out;\n },\n { ...bounds.size }\n ),\n };\n}\n\nexport function getBoundedItem(item, boundsType) {\n let bounds = getBounds(getModel(item), boundsType);\n\n if (productService.isExtendable(item)) {\n bounds = expandBounds(item, bounds);\n }\n\n return Object.assign({}, item, bounds.size, {\n boundsType: bounds.type,\n x: item.x + bounds.pos.x,\n y: item.y + bounds.pos.y,\n z: item.z + bounds.pos.z,\n });\n}\n\n/**\n * Checks whether two item intersect each other based on their model bounds\n *\n * @param {Object} present The already present item (in the TAC)\n * @param {Object} placed The \"incoming\" item\n * @param {Object} placedFitted Collision/space versions of placed, fitted to a point\n * @returns {Boolean}\n */\nfunction intersects(present, placed, placedFitted) {\n if (placed.itemid === present.itemid) {\n return false;\n }\n\n const presentCollisionItem = getBoundedItem(present, 'collision');\n const placedCollisionItem = placedFitted.fittedCollisionItem;\n\n // easy case, use collision bounds for both\n if (geometry.collides(presentCollisionItem, placedCollisionItem)) {\n return true;\n }\n\n const ignorePlacedSpace = tacHelpers.shouldIgnoreSpacingBounds(\n placedCollisionItem,\n present\n );\n const ignorePresentSpace = tacHelpers.shouldIgnoreSpacingBounds(\n present,\n placedCollisionItem\n );\n\n if (ignorePlacedSpace && ignorePresentSpace) {\n return false;\n }\n\n const presentSpaceItem = getBoundedItem(present, 'space');\n const placedSpaceItem = placedFitted.fittedSpaceItem;\n\n if (ignorePlacedSpace) {\n return geometry.collides(placedCollisionItem, presentSpaceItem);\n } else if (ignorePresentSpace) {\n return geometry.collides(placedSpaceItem, presentCollisionItem);\n } else {\n return (\n geometry.collides(placedCollisionItem, presentSpaceItem) ||\n geometry.collides(placedSpaceItem, presentCollisionItem)\n );\n }\n}\n\nfunction connectionPointBlocked(point, item, parent, fittedItems, items = []) {\n const collisionItems = items.filter(\n otherItem =>\n !tacHelpers.shouldIgnoreCollision(\n fittedItems.fittedCollisionItem,\n parent,\n otherItem\n )\n );\n return collisionItems.some(present => intersects(present, item, fittedItems));\n}\n\nexport function rebasedItems(items, parent = { x: 0, y: 0, z: 0 }, omit = {}) {\n return items\n .filter(item => item.itemid !== omit.itemid)\n .reduce((out, item) => {\n const rebasedItem = {\n ...item,\n x: item.x + parent.x,\n y: item.y + parent.y,\n z: item.z + parent.z,\n };\n out.push(rebasedItem);\n if (item.items) {\n out.push(...rebasedItems(item.items, rebasedItem, omit));\n }\n return out;\n }, []);\n}\n\nfunction hitsLimits(point, item, parent, tac, fittedItems) {\n // Legs are allowed to collide with floor\n if (productService.isType(item, 'leg') || tacHelpers.isPartOf(item, parent)) {\n return false;\n }\n\n const floor = {\n x: -80000,\n y: -1,\n z: -1000,\n height: 1,\n width: 160000,\n depth: 2000,\n };\n const items = [\n fittedItems.fittedSpaceItem,\n fittedItems.fittedCollisionItem,\n ].map(item => ({\n ...item,\n x: parent.x + item.x,\n y: parent.y + item.y,\n z: parent.z + item.z,\n }));\n\n const ceiling = { ...floor, y: 160000 };\n if (tac.wall && tac.wall.points) {\n const room = geometry.surround(tac.wall.points);\n ceiling.y = room.height - room.y;\n }\n\n return items.some(item => {\n return (\n !productService.shouldIgnoreFloorAndCeilingCollision(item) &&\n (geometry.collides(item, floor) || geometry.collides(item, ceiling))\n );\n });\n}\n\nexport function connectionPointOpen(point, item, parent, allItems, tac) {\n const fittedSpaceItem = fitToPoint(item, point, 'space');\n const fittedCollisionItem = fitToPoint(item, point, 'collision');\n const fittedItems = {\n fittedSpaceItem: { ...fittedSpaceItem, boundsType: 'space' },\n fittedCollisionItem: { ...fittedCollisionItem, boundsType: 'collision' },\n };\n const rebasedParent = allItems.find(item => item.itemid === parent.itemid);\n const zeroParent = Object.assign({}, parent, { x: 0, y: 0, z: 0 });\n\n const blocksInside = () =>\n connectionPointBlocked(point, item, parent, fittedItems, parent.items);\n\n const blocksOutside = () =>\n connectionPointBlocked(\n point,\n item,\n zeroParent,\n fittedItems,\n allItems\n .filter(\n item =>\n item.itemid !== parent.itemid &&\n !geometry.contains(item, rebasedParent)\n )\n // since the item we're fitting has coords relative to the containing section,\n // stuff outside needs rebasing to the same,\n // hence the subtraction of section's coords.\n .map(item =>\n Object.assign({}, item, {\n x: item.x - rebasedParent.x,\n y: item.y - rebasedParent.y,\n z: item.z - rebasedParent.z,\n })\n )\n ) || hitsLimits(point, item, rebasedParent, tac, fittedItems);\n\n if (\n productService.isType(item, [\n ITEMS.CLOTHES_RAIL,\n 'side-panel',\n 'post',\n 'cross-brace',\n ])\n ) {\n /*\n Clothes rails cannot be mounted on both sides of a connection point,\n side panels cannot collide with other side panels\n */\n return !blocksInside() && !blocksOutside();\n }\n\n if (geometry.contains(zeroParent, fittedSpaceItem)) {\n return !blocksInside();\n }\n\n if (!geometry.collides(zeroParent, fittedSpaceItem)) {\n return !blocksOutside();\n }\n\n return !blocksInside() && !blocksOutside();\n}\n\nexport function getOpenMatchingConnections(item, tac, proposedParent = null) {\n const connectionGroups = proposedParent\n ? getMatchingConnections(item, { ...tac, items: [proposedParent] })\n : getMatchingConnections(item, tac);\n const allItems = rebasedItems(tac.items, undefined, item);\n connectionGroups.forEach(parent => {\n parent.connections = parent.connections.filter(point =>\n connectionPointOpen(point, item, parent, allItems, tac)\n );\n });\n return connectionGroups;\n}\n\nexport function isStandAlone(item) {\n const model = getModel(item);\n return isFloorStanding(model) || isWallMounted(model);\n}\n\nexport function topPropBounds(item) {\n const nothing = {\n height: 0,\n width: 0,\n depth: 0,\n };\n if (item.propping) {\n const stripped = getBounds(item, 'size');\n const bounds = getProppingBounds(item);\n const used = bounds.find(bounds => 1 * bounds.id === item.propping);\n if (!used) {\n return nothing;\n }\n const diff = {\n height:\n used.pos.y + used.size.height - stripped.size.height - stripped.pos.y,\n width:\n used.pos.x + used.size.width - stripped.size.width - stripped.pos.y,\n depth:\n used.pos.z + used.size.depth - stripped.size.depth - stripped.pos.z,\n };\n if (diff.height > 0) {\n return diff;\n }\n }\n\n return nothing;\n}\n","import productsServiceCommon from '../';\nimport constants from '../../../settings/constants';\nimport { articleNo } from '../../../util/articleNo';\nimport { isStandAlone } from '../models';\nimport { FILTERS, ITEMS, MISSING_PRODUCT_CATEGORIES } from '../../../constants';\nimport { colorFilterObject, subFilterObject } from '../itemsFilters';\n\nconst articleImages = [];\nconst sectionImages = [];\n\n/**\n * See {@link getFilters} for docs on filters structure.\n * @type {[{filter: (function(*=): function({product: *}): boolean), condition: (function(): boolean), Component: () => JSX.Element}]}\n */\nconst getFilters = () => [\n colorFilterObject(),\n subFilterObject({ appliesTo: [FILTERS.SECTIONS] }),\n];\n\nfunction makeSectionImages() {\n const imagelist = productsServiceCommon\n .getAll(true)\n .filter(productsServiceCommon.isSection)\n .map(function (section) {\n const [width, _, height] = section.modelid.match(/\\d+/g); // eslint-disable-line no-unused-vars\n const { color } = section.filter;\n\n const sceneFileName = `BROR_post_${height}_${width}_${color}.png`;\n return {\n name: section.id,\n url: `${constants.IMAGE_ROOT}${sceneFileName}`,\n thumbnailUrl: `${constants.IMAGE_ROOT}thumbnails/${sceneFileName}`,\n thumbnailUrlPortrait: `${constants.IMAGE_ROOT}thumbnails/portrait/${sceneFileName}`,\n thumbnailUrlRTL: `${constants.IMAGE_ROOT}thumbnails/rtl/${sceneFileName}`,\n };\n });\n\n sectionImages.push(...imagelist);\n}\n\nfunction isInsert(id) {\n const found = productsServiceCommon.getProduct(id, true);\n return found && !isStandAlone(found);\n}\n\nfunction makeArticleImages() {\n const imagelist = productsServiceCommon\n .getAll()\n .filter(item => item.modelid)\n .filter(\n item =>\n !productsServiceCommon.isSection(item) &&\n !productsServiceCommon.isType(item, 'shelves')\n )\n .map(product => {\n const { color } = product.filter;\n let articleId = product.id;\n\n if (!articleNo(articleId)) {\n const includedArticles = productsServiceCommon.getIncludedArticles(\n product.id\n );\n\n while (includedArticles.length && !articleNo(articleId)) {\n articleId = includedArticles.shift();\n }\n }\n\n const sceneFileName = `${articleNo(articleId)}_${\n product.modelid\n }_${color}_0001.png`;\n const menuFileName = `${articleNo(articleId)}_${\n product.modelid\n }_${color}_t_0001.png`;\n\n const hasPortraitSpecificThumb = productsServiceCommon.isType(product, [\n ITEMS.CABINET,\n ITEMS.PEGBOARD,\n ITEMS.TROLLEY,\n ITEMS.WORKBENCH,\n ]);\n return {\n name: product.id,\n url: `${constants.IMAGE_ROOT}${sceneFileName}`,\n thumbnailUrl: `${constants.IMAGE_ROOT}thumbnails/${menuFileName}`,\n thumbnailUrlPortrait: hasPortraitSpecificThumb\n ? `${constants.IMAGE_ROOT}thumbnails/portrait/${menuFileName}`\n : null,\n thumbnailUrlRTL: `${constants.IMAGE_ROOT}thumbnails/rtl/${menuFileName}`,\n };\n });\n\n articleImages.push(...imagelist);\n\n makeSectionImages();\n}\n\nfunction getArticleImage(id) {\n return articleImages.concat(sectionImages).find(image => image.name === id);\n}\n\nfunction getDragMode(product) {\n return isInsert(product.id)\n ? constants.DRAG_MODE.INSERT\n : constants.DRAG_MODE.FLOAT;\n}\n\nfunction getFit(item, slot) {\n if (\n productsServiceCommon.isType(item, [\n ITEMS.SHELF,\n ITEMS.CABINET,\n ITEMS.DRAWER,\n ])\n ) {\n const color = slot.color || item.filter.color;\n\n let out = productsServiceCommon.getFilteredItems(\n _item => productsServiceCommon.isType(_item, [item.filter.type]),\n {\n width: slot.width,\n depth: slot.depth,\n color: color,\n }\n )[0];\n\n if (!out && productsServiceCommon.isCabinet(item) && color === 'wood') {\n out = productsServiceCommon.getFilteredItems(\n _item => productsServiceCommon.isType(_item, [item.filter.type]),\n {\n width: slot.width,\n depth: slot.depth,\n color: 'black',\n }\n )[0];\n }\n\n return Object.assign({}, out);\n } else if (productsServiceCommon.isAddOnShelf(item)) {\n const out = productsServiceCommon.getProduct(\n slot.depth <= 400 ? '00382792' : '20402001'\n );\n return Object.assign({}, out);\n }\n\n return null;\n}\n\nfunction isModule(id) {\n const found = productsServiceCommon.getProduct(id, true);\n return found && isStandAlone(found) && found.filter.type !== ITEMS.POST;\n}\n\nfunction isSection(id) {\n const found = productsServiceCommon.getProduct(id, true);\n return found && found.filter.type === ITEMS.POST;\n}\n\nfunction isMultiParentProduct(item) {\n // Not implemented for Bror\n return false;\n}\n\n/**\n * Gets the items relation to its parent section in terms of offsets.\n * This is calculated as (section.width - item.width) / 2 for the x axis.\n * @param {object} item\n * @returns {object} Dimensions of the offset for the 3 axis.\n */\nfunction getSectionOffset(item) {\n if (productsServiceCommon.isType(item, [ITEMS.SHELF, ITEMS.DIVIDER])) {\n return { x: 5, y: 0, z: 5 };\n }\n if (productsServiceCommon.isType(item, ITEMS.CABINET)) {\n return { x: 4, y: 0, z: 0 };\n }\n if (productsServiceCommon.isType(item, ITEMS.DRAWER)) {\n return { x: 4, y: 0, z: -4 };\n }\n return { x: 0, y: 0, z: 0 };\n}\n\nfunction getSectionInserts(...filters) {\n return productsServiceCommon.getFilteredItems(\n item => productsServiceCommon.isType(item, [ITEMS.SHELF]),\n ...filters\n );\n}\n\nfunction shouldHaveColorSelector(item) {\n return productsServiceCommon.isType(item, [\n ITEMS.SHELF,\n ITEMS.PEGBOARD,\n ITEMS.POST,\n ITEMS.CABINET,\n ITEMS.TROLLEY,\n ITEMS.WORKBENCH,\n ]);\n}\n\n/**\n * Returns boolean whether or not to show info icon for items\n *\n * @param id\n * @returns {boolean}\n */\nconst showInfoIcon = id =>\n !productsServiceCommon.isType(id, constants.HIDE_INFO_ICON_FOR_TYPE);\n\nfunction getMissingMandatoryProductCategories() {\n const sections = productsServiceCommon.getFilteredItems(item =>\n isSection(item)\n );\n return sections.length === 0 ? [MISSING_PRODUCT_CATEGORIES.SECTION] : [];\n}\n\nexport default {\n articleImages,\n getArticleImage,\n getDragMode,\n getFit,\n isModule,\n isInsert,\n isSection,\n isMultiParentProduct,\n getSectionOffset,\n getSectionInserts,\n makeArticleImages,\n shouldHaveColorSelector,\n showInfoIcon,\n getMissingMandatoryProductCategories,\n getFilters,\n};\n","import productsServiceCommon from '../';\nimport constants from '../../../settings/constants';\nimport { articleNo } from '../../../util/articleNo';\nimport { getProppingBounds } from '../models';\nimport { ITEMS } from '../../../constants';\n\nconst articleImages = [];\n\n/**\n * See {@link getFilters} for docs on filters structure.\n * @type {[{filter: (function(*=): function({product: *}): boolean), condition: (function(): boolean), Component: () => JSX.Element}]}\n */\nconst getFilters = () => [];\n\nfunction makeArticleImages() {\n const imageList = productsServiceCommon\n .getAll()\n .filter(item => item.modelid)\n .map(product => {\n const { color } = product.filter;\n let articleId = product.id;\n\n if (!articleNo(articleId)) {\n const includedArticles = productsServiceCommon.getIncludedArticles(\n product.id\n );\n\n while (includedArticles.length && !articleNo(articleId)) {\n articleId = includedArticles.shift();\n }\n }\n\n const sceneFileName = `${product.modelid}_${color}_0001.png`;\n const menuFileName = `${product.modelid}_${color}_t_0001.png`;\n\n const hasPortraitSpecificImage = productsServiceCommon.isType(product, [\n ITEMS.FRAME,\n ITEMS.SHELVING_UNIT,\n ]);\n\n return {\n name: product.id,\n color: color,\n type: product.filter.type,\n modelid: product.modelid,\n proppingBounds: getProppingBounds(product),\n url: `${constants.IMAGE_ROOT}${sceneFileName}`,\n thumbnailUrl: `${constants.IMAGE_ROOT}thumbnails/${menuFileName}`,\n thumbnailUrlPortrait: hasPortraitSpecificImage\n ? `${constants.IMAGE_ROOT}thumbnails/portrait/${menuFileName}`\n : null,\n thumbnailUrlRTL: `${constants.IMAGE_ROOT}thumbnails/rtl/${menuFileName}`,\n };\n });\n\n imageList.forEach(image => {\n if (image.proppingBounds && image.proppingBounds.length) {\n for (let i = 1; i <= image.proppingBounds.length; i++) {\n const proppingImage = {\n name: `${image.name}_propping_${i}`,\n url: `${constants.IMAGE_ROOT}${image.modelid}_propping_${i}_${image.color}_0001.png`,\n };\n imageList.push(proppingImage);\n }\n }\n });\n\n articleImages.push(...imageList);\n}\n\nfunction getArticleImage(id) {\n return articleImages.find(image => image.name === id);\n}\n\nfunction getDragMode(product) {\n return productsServiceCommon.isType(product, ITEMS.TOP_SHELF) ||\n productsServiceCommon.isType(product, ITEMS.BASKET)\n ? constants.DRAG_MODE.INSERT\n : constants.DRAG_MODE.FLOAT;\n}\n\nfunction isBasket(item) {\n return productsServiceCommon.isType(item, ITEMS.BASKET);\n}\n\nfunction isTopShelf(item) {\n return productsServiceCommon.isType(item, ITEMS.TOP_SHELF);\n}\n\nfunction isClothesRail(item) {\n return productsServiceCommon.isType(item, ITEMS.CLOTHES_RAIL);\n}\nfunction isCover(item) {\n return productsServiceCommon.isType(item, 'cover');\n}\n\nfunction isStackable(item) {\n return [ITEMS.SHELVING_UNIT, ITEMS.FRAME].indexOf(item.filter.type) !== -1;\n}\n\nfunction getFit(item, slot) {\n // TODO: Make this more generic.\n let fits;\n if (isBasket(item)) {\n fits = productsServiceCommon.getFilteredItems(isBasket, {\n width: slot.width,\n depth: slot.depth,\n variant: slot.variant || item.filter.variant,\n });\n } else if (isTopShelf(item)) {\n fits = productsServiceCommon.getFilteredItems(isTopShelf, {\n width: slot.width,\n depth: slot.depth,\n color: slot.color || item.filter.color,\n });\n } else if (productsServiceCommon.isShelvingUnit(item)) {\n fits = productsServiceCommon.getFilteredItems(\n productsServiceCommon.isShelvingUnit,\n {\n width: slot.width,\n depth: slot.depth,\n color: slot.color || item.filter.color,\n }\n );\n } else if (productsServiceCommon.isLeg(item)) {\n fits = productsServiceCommon.getFilteredItems(productsServiceCommon.isLeg, {\n width: slot.width,\n depth: slot.depth,\n variant: slot.variant || item.filter.variant,\n });\n } else if (isClothesRail(item)) {\n if (slot.partnerSlot) {\n const extRail = productsServiceCommon.getProduct(\n 'clothes_rail_50_51_4_white_in'\n );\n const extWidth = slot.partnerSlot.x + slot.partnerSlot.width - slot.x;\n return { ...extRail, width: extWidth, slot: slot };\n }\n return {\n ...productsServiceCommon.getProduct('clothes_rail_50_51_4_white'),\n };\n } else if (isCover(item)) {\n fits = productsServiceCommon.getFilteredItems(isCover, {\n width: slot.width,\n depth: slot.depth,\n height: slot.height || item.height,\n variant: slot.variant || item.filter.variant,\n });\n } else if (productsServiceCommon.isFrame(item)) {\n fits = productsServiceCommon.getFilteredItems(\n productsServiceCommon.isFrame,\n {\n width: slot.width,\n depth: slot.depth,\n height: slot.height || item.height,\n color: slot.color || item.filter.color,\n }\n );\n }\n\n if (fits && fits.length) {\n return Object.assign({}, fits[0]);\n }\n return null;\n}\n\nfunction isModule(id) {\n // Not implemented for Jonaxel\n return false;\n}\n\nfunction isInsert(id) {\n // Not implemented for Jonaxel\n return false;\n}\n\nfunction isSection(id) {\n // Not implemented for Jonaxel\n return false;\n}\n\nfunction isMultiParentProduct(item) {\n return (\n productsServiceCommon.isType(item, ITEMS.CLOTHES_RAIL) &&\n productsServiceCommon.isExtendable(item)\n );\n}\n\nfunction shouldHaveColorSelector(item) {\n return productsServiceCommon.isType(item, ITEMS.FRAME);\n}\n\nexport default {\n articleImages,\n getArticleImage,\n getDragMode,\n getFit,\n isAdjustable: isMultiParentProduct,\n isModule,\n isInsert,\n isSection,\n isStackable,\n isMultiParentProduct,\n makeArticleImages,\n shouldHaveColorSelector,\n getFilters,\n};\n","import productsServiceCommon from '../';\nimport constants from '../../../settings/constants';\nimport { articleNo } from '../../../util/articleNo';\nimport { getProppingBounds, isStandAlone } from '../models';\nimport { config as asConfig } from '../../../scene/boaxel/AdjustableConfig';\nimport { generateProppingImageResources } from '../index';\nimport { FILTERS, ITEMS, MISSING_PRODUCT_CATEGORIES } from '../../../constants';\nimport { colorFilterObject, subFilterObject } from '../itemsFilters';\n\nconst BRACKET_IDS = {\n white: '60448733',\n anthracite: '10575587',\n};\n\nconst articleImages = [];\nconst fitCache = {};\n\n/**\n * See {@link getFilters} for docs on filters structure.\n * @type {[{filter: (function(*=): function({product: *}): boolean), condition: (function(): boolean), Component: () => JSX.Element}]}\n */\nconst getFilters = () => [\n colorFilterObject(),\n subFilterObject({ appliesTo: [FILTERS.UPRIGHT] }),\n];\n\nfunction isImageDummy(item) {\n return item.type === 'dummy';\n}\n\nfunction makeArticleImages() {\n const imageList = productsServiceCommon\n .getAll()\n .filter(\n item =>\n item.modelid &&\n (productsServiceCommon.isRealArticleProduct(item) || isImageDummy(item))\n )\n .map(product => {\n const { color } = product.filter;\n let articleId = product.id;\n\n if (!articleNo(articleId)) {\n const includedArticles = productsServiceCommon.getIncludedArticles(\n product.id\n );\n\n while (includedArticles.length && !articleNo(articleId)) {\n articleId = includedArticles.shift();\n }\n }\n\n const sceneFileName = `${product.modelid}_${color}_0001.png`;\n const menuFileName = `${product.modelid}_${color}_t_0001.png`;\n const hasPortraitSpecificThumb =\n productsServiceCommon.isType(product, ITEMS.UPRIGHT) ||\n productsServiceCommon.isExtendable(product);\n\n const hasMobileLandscapeSpecificThumb = productsServiceCommon.isType(\n product,\n ITEMS.UPRIGHT\n );\n\n const hasMobilePortraitSpecificThumb = productsServiceCommon.isType(\n product,\n [\n ITEMS.CLOTHES_RAIL,\n ITEMS.DRYING_RACK,\n ITEMS.TROUSER_HANGER,\n ITEMS.SHOE_SHELF,\n ]\n );\n\n return {\n name: product.id,\n color: color,\n type: product.filter.type,\n modelid: product.modelid,\n proppingBounds: getProppingBounds(product),\n url: `${constants.IMAGE_ROOT}${sceneFileName}`,\n thumbnailUrl: `${constants.IMAGE_ROOT}thumbnails/${menuFileName}`,\n thumbnailUrlPortrait: hasPortraitSpecificThumb\n ? `${constants.IMAGE_ROOT}thumbnails/portrait/${menuFileName}`\n : null,\n thumbnailUrlMobilePortrait: hasMobilePortraitSpecificThumb\n ? `${constants.IMAGE_ROOT}thumbnails/mobile-portrait/${menuFileName}`\n : null,\n thumbnailUrlMobileLandscape: hasMobileLandscapeSpecificThumb\n ? `${constants.IMAGE_ROOT}thumbnails/mobile-landscape/${menuFileName}`\n : null,\n };\n });\n\n articleImages.push(...generateProppingImageResources(imageList));\n}\n\nfunction getArticleImage(id) {\n return articleImages.find(image => image.name === id);\n}\n\nfunction getDragMode(product) {\n //Not yet implemented for Boaxel\n return constants.DRAG_MODE.FLOAT;\n}\n\nfunction isStackable(item) {\n //Not yet implemented for Boaxel\n return false;\n}\n\nfunction getWidth(item, slot) {\n const range = {\n max: asConfig.section.maxWidth,\n min: asConfig.section.minWidth,\n };\n if (\n (slot.logic && slot.logic.extendable) ||\n (item.logic && item.logic.extendable)\n ) {\n if (slot.width <= range.max && slot.width >= range.min) {\n return range;\n }\n }\n return slot.width;\n}\n\nfunction isClothesRail(id) {\n return productsServiceCommon.isType(id, ITEMS.CLOTHES_RAIL);\n}\n\nfunction isSection(id) {\n const found = productsServiceCommon.getProduct(id, true);\n return found && found.filter.type === ITEMS.SECTION;\n}\n\nfunction getFit(item, slot) {\n let fits;\n switch (item.filter.type) {\n case ITEMS.TABLE:\n fits = productsServiceCommon.getFilteredItems(\n productsServiceCommon.isTable,\n {\n width: getWidth(item, slot),\n color: slot.color || item.filter.color,\n }\n );\n break;\n case ITEMS.SHELF:\n fits = productsServiceCommon.getFilteredItems(\n productsServiceCommon.isShelf,\n {\n width: getWidth(item, slot),\n color: slot.color || item.filter.color,\n }\n );\n break;\n case ITEMS.METAL_SHELF:\n fits = productsServiceCommon.getFilteredItems(\n item => productsServiceCommon.isType(item, ITEMS.METAL_SHELF),\n {\n width: getWidth(item, slot),\n color: slot.color || item.filter.color,\n }\n );\n break;\n case ITEMS.WIRE_SHELF:\n fits = productsServiceCommon.getFilteredItems(\n item => productsServiceCommon.isType(item, ITEMS.WIRE_SHELF),\n {\n width: getWidth(item, slot),\n color: slot.color || item.filter.color,\n }\n );\n break;\n case ITEMS.SHOE_SHELF:\n fits = productsServiceCommon.getFilteredItems(\n item => productsServiceCommon.isType(item, ITEMS.SHOE_SHELF),\n {\n width: getWidth(item, slot),\n color: slot.color || item.filter.color,\n }\n );\n break;\n case ITEMS.BASKET:\n fits = productsServiceCommon.getFilteredItems(\n item => productsServiceCommon.isType(item, ITEMS.BASKET),\n {\n width: slot.width,\n color: slot.color || item.filter.color,\n }\n );\n break;\n case ITEMS.DRYING_RACK:\n fits = productsServiceCommon.getFilteredItems(\n item => productsServiceCommon.isType(item, ITEMS.DRYING_RACK),\n {\n width: slot.width,\n color: slot.color || item.filter.color,\n }\n );\n break;\n case ITEMS.SECTION:\n fits = productsServiceCommon.getFilteredItems(isSection, {\n width: slot.width,\n });\n if (\n !fits.length &&\n slot.width <= asConfig.section.maxWidth &&\n slot.width >= asConfig.section.minWidth\n ) {\n fits = productsServiceCommon.getFilteredItems(isSection, {\n width: asConfig.section.minWidth,\n });\n if (fits[0]) {\n fits[0] = { ...fits[0], width: slot.width };\n }\n }\n break;\n case ITEMS.CLOTHES_RAIL:\n if (fitCache[item.id]) {\n const oldFit = fitCache[item.id];\n if (\n oldFit.width === slot.width &&\n oldFit.filter?.color === slot.filter?.color\n ) {\n return item;\n }\n }\n fits = productsServiceCommon.getFilteredItems(isClothesRail, {\n width: getWidth(item, slot),\n color: slot.color || item.filter.color,\n });\n break;\n default:\n fits = productsServiceCommon.getFilteredItems(\n other =>\n productsServiceCommon.isType(\n other,\n slot.filter ? slot.filter.type : item.filter.type\n ),\n {\n width: slot.width,\n }\n );\n break;\n }\n\n if (fits && fits.length) {\n const fit = Object.assign({}, fits[0]);\n if (getWidth(slot, item) !== slot.width) {\n fit.width = slot.width;\n } else {\n // this was a static item, cache it\n fitCache[fit.id] = slot;\n }\n return fit;\n }\n return null;\n}\n\nfunction isModule(id) {\n //Not yet implemented for Boaxel\n return false;\n}\n\nfunction isInsert(id) {\n return !productsServiceCommon.isType(id, [\n ITEMS.SECTION,\n ITEMS.UPRIGHT,\n ITEMS.BRACKET,\n ITEMS.CLOTHES_RAIL,\n ITEMS.MOUNTING_RAIL,\n ]);\n}\n\nfunction getFittingBracket(item, side, color) {\n if (isStandAlone(item)) {\n return null;\n }\n\n if (\n productsServiceCommon.isType(item, [\n ITEMS.SECTION,\n ITEMS.MOUNTING_RAIL,\n ITEMS.CLOTHES_RAIL,\n ITEMS.BRACKET,\n ])\n ) {\n return null;\n }\n\n if (color && !BRACKET_IDS[color]) {\n return null;\n }\n\n const bracketIdsToConsider = color\n ? [BRACKET_IDS[color]]\n : Object.values(BRACKET_IDS);\n const bracketProduct = bracketIdsToConsider\n .map(bracketId => productsServiceCommon.getProduct(bracketId))\n .find(product => product !== undefined);\n\n return bracketProduct;\n}\n\nfunction isDependentItem(item) {\n return productsServiceCommon.isType(item, [\n ITEMS.SECTION,\n ITEMS.BRACKET,\n ITEMS.MOUNTING_RAIL,\n ]);\n}\n\nfunction isAdjustable(item) {\n return isSection(item) && item.logic && item.logic.extendable;\n}\n\nfunction shouldHaveColorSelector(item) {\n return productsServiceCommon.isType(item, [\n ITEMS.UPRIGHT,\n ITEMS.SHELF,\n ITEMS.METAL_SHELF,\n ITEMS.WIRE_SHELF,\n ITEMS.SHOE_SHELF,\n ITEMS.BASKET,\n ITEMS.DRYING_RACK,\n ITEMS.CLOTHES_RAIL,\n ITEMS.TABLE,\n ]);\n}\n\nfunction shouldHaveHeadlineForSelectors(item) {\n return productsServiceCommon.isType(item, [\n ITEMS.SHELF,\n ITEMS.METAL_SHELF,\n ITEMS.WIRE_SHELF,\n ]);\n}\n\nfunction fitsTable(section) {\n return productsServiceCommon\n .getFilteredItems(item => productsServiceCommon.isType(item, ITEMS.TABLE))\n .some(table => table.width === section.width);\n}\n\nfunction getSectionInserts(...filters) {\n return productsServiceCommon.getFilteredItems(\n item =>\n productsServiceCommon.isType(item, [\n ITEMS.SHELF,\n ITEMS.METAL_SHELF,\n ITEMS.WIRE_SHELF,\n ITEMS.TABLE,\n ]),\n ...filters\n );\n}\n\nfunction canHaveOverlappingDropAreas(item) {\n return productsServiceCommon.isTable(item);\n}\n\nfunction areMountingRailsOfSpecificColorValid(color) {\n const mountingRails = productsServiceCommon.getFilteredItems(item =>\n productsServiceCommon.isType(item, ITEMS.MOUNTING_RAIL)\n );\n\n const shortRail = mountingRails.find(\n rail => rail.id === constants.MOUNTING_RAILS[color][600]\n );\n const longRail = mountingRails.find(\n rail => rail.id === constants.MOUNTING_RAILS[color][800]\n );\n\n const valid = !!(shortRail?.valid && longRail?.valid);\n return valid;\n}\n\nfunction getValidMountingRailColors() {\n const candidateColors = Object.keys(constants.MOUNTING_RAILS);\n const colorsWithValidMountingRails = candidateColors.filter(candidateColor =>\n areMountingRailsOfSpecificColorValid(candidateColor)\n );\n\n return colorsWithValidMountingRails;\n}\n\nfunction areMountingRailsValid() {\n return !!getValidMountingRailColors().length;\n}\n\nfunction getMissingMandatoryProductCategories() {\n const uprights = productsServiceCommon.getFilteredItems(item =>\n productsServiceCommon.isType(item, ITEMS.UPRIGHT)\n );\n const brackets = productsServiceCommon.getFilteredItems(item =>\n productsServiceCommon.isType(item, ITEMS.BRACKET)\n );\n\n const missingProductCategories = [];\n if (uprights.length === 0)\n missingProductCategories.push(MISSING_PRODUCT_CATEGORIES.UPRIGHT);\n if (brackets.length === 0)\n missingProductCategories.push(MISSING_PRODUCT_CATEGORIES.BRACKET);\n\n return missingProductCategories;\n}\n\nexport default {\n articleImages,\n canHaveOverlappingDropAreas,\n getArticleImage,\n getFittingBracket,\n getDragMode,\n getFit,\n isAdjustable,\n isModule,\n isInsert,\n isSection,\n isStackable,\n isDependentItem,\n makeArticleImages,\n shouldHaveColorSelector,\n shouldHaveHeadlineForSelectors,\n fitsTable,\n getSectionInserts,\n areMountingRailsOfSpecificColorValid,\n getValidMountingRailColors,\n areMountingRailsValid,\n getMissingMandatoryProductCategories,\n getFilters,\n};\n","import productsServiceCommon, {\n isType,\n isExtendable,\n generateProppingImageResources,\n} from '../';\nimport constants from '../../../settings/constants';\nimport { getProppingBounds } from '../models';\nimport { FILTERS, ITEMS, MISSING_PRODUCT_CATEGORIES } from '../../../constants';\nimport { colorFilterObject } from '../itemsFilters';\n\nconst MOUNTING_RAIL_WIDTH_SHORT = 650;\nconst MOUNTING_RAIL_WIDTH_LONG = 1250;\n\nconst articleImages = [];\n\n/**\n * See {@link getFilters} for docs on filters structure.\n * @type {[{filter: (function(*=): function({product: *}): boolean), condition: (function(): boolean), Component: () => JSX.Element}]}\n */\nconst getFilters = () => [colorFilterObject({ appliesTo: [FILTERS.PARTS] })];\n\nfunction makeArticleImages() {\n const imageList = productsServiceCommon\n .getAll()\n .filter(\n item =>\n item.modelid && !productsServiceCommon.isType(item, ITEMS.SIDEWALL)\n )\n .map(product => {\n const { color } = product.filter;\n\n const proppingBounds = getProppingBounds(product);\n const proppingName = proppingBounds.length > 0 ? 'propping_1_' : '';\n\n const sceneFileName = `${product.modelid}_${color}_0001.png`;\n const menuFileName = `${product.modelid}_${proppingName}${color}_t_0001.png`;\n\n const hasPortraitSpecificThumb = productsServiceCommon.isType(\n product,\n ITEMS.SECTION\n );\n\n return {\n name: product.id,\n color: color,\n type: product.filter.type,\n modelid: product.modelid,\n proppingBounds,\n url: `${constants.IMAGE_ROOT}${sceneFileName}`,\n thumbnailUrl: `${constants.IMAGE_ROOT}thumbnails/${menuFileName}`,\n thumbnailUrlPortrait: hasPortraitSpecificThumb\n ? `${constants.IMAGE_ROOT}thumbnails/portrait/${menuFileName}`\n : null,\n };\n });\n\n articleImages.push(...generateProppingImageResources(imageList));\n}\n\nfunction getDragMode(product) {\n return constants.DRAG_MODE.FLOAT;\n}\n\nfunction getArticleImage(id) {\n return articleImages.find(image => image.name === id);\n}\n\nfunction isMultiParentProduct(item) {\n return (\n productsServiceCommon.isType(item, ITEMS.CLOTHES_RAIL) &&\n productsServiceCommon.isExtendable(item)\n );\n}\n\nfunction isClothesRail(item) {\n return productsServiceCommon.isType(item, ITEMS.CLOTHES_RAIL);\n}\n\nfunction isDependentItem(item) {\n return (\n productsServiceCommon.isType(item, ITEMS.MOUNTING_RAIL) ||\n productsServiceCommon.isType(item, ITEMS.SIDEWALL)\n );\n}\n\nfunction isSection(item) {\n return productsServiceCommon.isType(item, ITEMS.SECTION);\n}\n\nfunction isInsert(item) {\n return !isSection(item) && !isDependentItem(item);\n}\n\nfunction getFit(item, slot) {\n let fits;\n\n switch (item.filter.type) {\n case ITEMS.CLOTHES_RAIL:\n const crs = productsServiceCommon.getFilteredItems(isClothesRail, {\n color: slot.color || item.filter.color,\n });\n\n if (slot.partnerSlot) {\n const extRails = crs.filter(product => isExtendable(product));\n const extWidth = slot.partnerSlot.x + slot.partnerSlot.width - slot.x;\n fits = extRails.map(rail => ({ ...rail, width: extWidth }));\n } else {\n fits = crs;\n }\n break;\n\n case ITEMS.SHELF:\n case ITEMS.BASKET:\n case ITEMS.DRAWER:\n case ITEMS.SHOE_SHELF:\n fits = productsServiceCommon.getFilteredItems(\n product => isType(product, item.filter.type),\n {\n color: slot.color || item.filter.color,\n }\n );\n break;\n default:\n return item;\n }\n\n if (fits?.length) {\n return Object.assign({}, fits[0]);\n }\n}\n\nfunction shouldHaveColorSelector(item) {\n return productsServiceCommon.isType(item, [\n ITEMS.BASKET,\n ITEMS.SHELF,\n ITEMS.SHOE_SHELF,\n ITEMS.CLOTHES_RAIL,\n ITEMS.DRAWER,\n ITEMS.SECTION,\n ]);\n}\n\nfunction shouldHaveChildColorSelector(item) {\n return (\n isSection(item) &&\n item.items.some(\n child => shouldHaveColorSelector(child) && !isMultiParentProduct(child)\n )\n );\n}\n\nfunction getInitialZPos(item) {\n if (isSection(item)) {\n return 20;\n }\n return 0;\n}\n\nfunction isMountingRail(item) {\n return !!productsServiceCommon.isType(item, ITEMS.MOUNTING_RAIL);\n}\n\nfunction isShortMountingRail(item) {\n return isMountingRail(item) && item.width === MOUNTING_RAIL_WIDTH_SHORT;\n}\n\nfunction isLongMountingRail(item) {\n return isMountingRail(item) && item.width === MOUNTING_RAIL_WIDTH_LONG;\n}\n\nfunction isSidePanel(item) {\n return !!productsServiceCommon.isType(item, ITEMS.SIDE_PANEL);\n}\n\nfunction isEndShelf(item) {\n return !!productsServiceCommon.isType(item, ITEMS.END_SHELF);\n}\n\nfunction getMandatoryProductAvailabilityPerColor(colors) {\n const productAvailabilityPerColor = {};\n colors.forEach(color => {\n const filteredProductOfThisColor = productsServiceCommon.getFilteredItems(\n Boolean,\n { color }\n );\n productAvailabilityPerColor[color] = {\n shortMountingRailAvailable: !!filteredProductOfThisColor.find(prod =>\n isShortMountingRail(prod)\n ),\n longMountingRailAvailable: !!filteredProductOfThisColor.find(prod =>\n isLongMountingRail(prod)\n ),\n sidePanelAvailable: !!filteredProductOfThisColor.find(prod =>\n isSidePanel(prod)\n ),\n endShelfAvailable: !!filteredProductOfThisColor.find(prod =>\n isEndShelf(prod)\n ),\n };\n });\n\n return productAvailabilityPerColor;\n}\n\nfunction hasAllMandatoryProductsOfColor(color) {\n const availability = getMandatoryProductAvailabilityPerColor([color]);\n\n return (\n availability[color].shortMountingRailAvailable &&\n availability[color].longMountingRailAvailable &&\n availability[color].sidePanelAvailable &&\n availability[color].endShelfAvailable\n );\n}\n\nfunction getMissingMandatoryProductCategories() {\n const sections = productsServiceCommon.getFilteredItems(item =>\n isSection(item)\n );\n\n const colors = ['white', 'dark_grey'];\n const availabilityPerColor = getMandatoryProductAvailabilityPerColor(colors);\n\n const sectionAvailable = sections.length > 0;\n const shortMountingRailAvailable = colors.some(\n color => availabilityPerColor[color].shortMountingRailAvailable\n );\n const longMountingRailAvailable = colors.some(\n color => availabilityPerColor[color].longMountingRailAvailable\n );\n const sidePanelAvailable = colors.some(\n color => availabilityPerColor[color].sidePanelAvailable\n );\n const endShelfAvailable = colors.some(\n color => availabilityPerColor[color].endShelfAvailable\n );\n const matchingColorsAvailable = colors.some(color =>\n hasAllMandatoryProductsOfColor(color)\n );\n\n const missingProductCategories = [];\n if (!sectionAvailable)\n missingProductCategories.push(MISSING_PRODUCT_CATEGORIES.SECTION);\n if (!shortMountingRailAvailable)\n missingProductCategories.push(\n MISSING_PRODUCT_CATEGORIES.MOUNTING_RAIL_SHORT\n );\n if (!longMountingRailAvailable)\n missingProductCategories.push(\n MISSING_PRODUCT_CATEGORIES.MOUNTING_RAIL_LONG\n );\n if (!sidePanelAvailable)\n missingProductCategories.push(MISSING_PRODUCT_CATEGORIES.SIDE_PANEL);\n if (!endShelfAvailable)\n missingProductCategories.push(MISSING_PRODUCT_CATEGORIES.END_SHELF);\n if (\n shortMountingRailAvailable &&\n longMountingRailAvailable &&\n sidePanelAvailable &&\n endShelfAvailable &&\n !matchingColorsAvailable\n )\n missingProductCategories.push(\n MISSING_PRODUCT_CATEGORIES.MOUNTING_RAILS_SIDE_PANEL_END_SHELF_COLOR_MISMATCH\n );\n\n return missingProductCategories;\n}\n\nfunction isAdjustable(item) {\n return productsServiceCommon.isExtendable(item);\n}\n\nexport default {\n articleImages,\n getArticleImage,\n getDragMode,\n getFit,\n getInitialZPos,\n getMissingMandatoryProductCategories,\n hasAllMandatoryProductsOfColor,\n isAdjustable,\n isDependentItem,\n isMultiParentProduct,\n isInsert,\n isSection,\n isClothesRail,\n makeArticleImages,\n shouldHaveColorSelector,\n shouldHaveChildColorSelector,\n getFilters,\n};\n","import productsServiceCommon, {\n generateProppingImageResources,\n isType,\n} from '../';\nimport constants from '../../../settings/constants';\nimport { getProppingBounds } from '../models';\nimport { FILTERS, ITEMS, MISSING_PRODUCT_CATEGORIES } from '../../../constants';\nimport { translate } from '../../L10n';\nimport { subFilterObject } from '../itemsFilters';\nimport { t } from '../../../translations';\n\nconst articleImages = [];\n\n/**\n * See {@link getFilters} for docs on filters structure.\n * @type {[{filter: (function(*=): function({product: *}): boolean), condition: (function(): boolean), Component: () => JSX.Element}]}\n */\nconst getFilters = () => [subFilterObject({ appliesTo: [FILTERS.SECTIONS] })];\n\nfunction getArticleImage(id) {\n return articleImages.find(image => image.name === id);\n}\n\nfunction getDragMode(product) {\n return constants.DRAG_MODE.FLOAT;\n}\n\nfunction getFit(item, slot) {\n let fits;\n\n switch (item.filter.type) {\n case ITEMS.SECTION:\n fits = productsServiceCommon.getFilteredItems(\n product => productsServiceCommon.isType(product, item.filter.type),\n {\n width: item.width,\n depth: item.depth,\n height: slot.height,\n }\n );\n break;\n case ITEMS.SHELF:\n case ITEMS.FELT_SHELF:\n case ITEMS.BOTTLE_RACK:\n case ITEMS.CABINET:\n case ITEMS.DRAWER:\n case ITEMS.TABLE:\n fits = productsServiceCommon.getFilteredItems(\n product => productsServiceCommon.isType(product, item.filter.type),\n {\n width: slot.width,\n depth: slot.depth,\n height: slot.height,\n color: slot.color || item.filter.color,\n }\n );\n break;\n default:\n return item;\n }\n\n if (fits?.length) {\n return Object.assign({}, fits[0]);\n }\n}\n\nconst getColorSortValue = shelf => {\n if (!isType(shelf, 'shelf') || !shelf) return 0;\n const sortValue = {\n pine: 1,\n white: 2,\n };\n const shelfColor = shelf.filter.color;\n\n const sortValueKey = Object.keys(sortValue).find(key =>\n shelfColor.includes(key)\n );\n return sortValue[sortValueKey] || 0;\n};\n\n/**\n * Always sort in the following order if it exists on the market:\n * 1) Metal white\n * 2) Pine\n * @param {*} shelves\n */\nconst sortByColor = (shelf1, shelf2) => {\n return getColorSortValue(shelf1) - getColorSortValue(shelf2);\n};\n\n/**\n * Gets a matching shelf for a specific section\n * @param {*} section\n * @returns {Object | undefined}\n */\nconst getFittingShelf = section => {\n const WIDTH_DIFF = 60;\n\n const match = productsServiceCommon\n .getShelves({\n width: section.width - WIDTH_DIFF,\n depth: section.depth,\n })\n ?.sort(sortByColor)[0];\n\n return match ? { ...match } : null;\n};\n\nfunction makeArticleImages() {\n const imageList = productsServiceCommon.getAll().map(product => {\n const { color } = product.filter;\n const proppingBounds = getProppingBounds(product);\n\n const sceneFileName = `${product.modelid}_${color}_0001.png`;\n const menuFileName = `${product.modelid}_${color}_t_0001.png`;\n return {\n name: product.id,\n color: color,\n type: product.filter.type,\n modelid: product.modelid,\n proppingBounds,\n url: !productsServiceCommon.isSection(product)\n ? `${constants.IMAGE_ROOT}${sceneFileName}`\n : `${constants.IMAGE_ROOT}thumbnails/${menuFileName}`,\n thumbnailUrl: `${constants.IMAGE_ROOT}thumbnails/${menuFileName}`,\n thumbnailUrlRTL: `${constants.IMAGE_ROOT}thumbnails/rtl/${menuFileName}`,\n thumbnailUrlPortrait: `${constants.IMAGE_ROOT}thumbnails/portrait/${menuFileName}`,\n };\n });\n\n articleImages.push(...generateProppingImageResources(imageList));\n}\n\n/**\n * Translates key if id matches specific value\n * @param product {Object}\n * @returns {String}\n */\nfunction getCustomArticleTypeText(product) {\n if (productsServiceCommon.isType(product.id, ITEMS.DOOR)) {\n return translate(t.MENU_PRODUCT_DOORS);\n }\n}\n\nfunction isInsert(item) {\n return productsServiceCommon.isType(item, [\n ITEMS.SHELF,\n ITEMS.FELT_SHELF,\n ITEMS.BOTTLE_RACK,\n ITEMS.CABINET,\n ITEMS.CHEST,\n ITEMS.TABLE,\n ITEMS.BOX,\n ITEMS.DOOR,\n ITEMS.DRAWER,\n ]);\n}\n\nfunction isSection(item) {\n return productsServiceCommon.isType(item, ITEMS.SECTION);\n}\n\nfunction getInitialZPos(item) {\n if (isSection(item)) {\n return 5;\n }\n return 0;\n}\n\nfunction shouldHaveColorSelector(item) {\n return productsServiceCommon.isType(item, [\n ITEMS.CABINET,\n ITEMS.SHELF_DRAWER,\n ]);\n}\n\nfunction shouldIgnoreFloorAndCeilingCollision(item) {\n return item.boundsType === 'space' && isInsert(item) && !item.items?.length;\n}\n\nfunction shouldShowDoorHint(item) {\n return productsServiceCommon.isType(item, ITEMS.DOOR);\n}\n\nfunction getMissingMandatoryProductCategories() {\n const sections = productsServiceCommon.getFilteredItems(item =>\n isSection(item)\n );\n return sections.length === 0 ? [MISSING_PRODUCT_CATEGORIES.SECTION] : [];\n}\n\nfunction showInfoIcon() {\n return true;\n}\n\nexport default {\n articleImages,\n getArticleImage,\n getCustomArticleTypeText,\n getDragMode,\n getFit,\n getFittingShelf,\n getInitialZPos,\n isInsert,\n isSection,\n showInfoIcon,\n makeArticleImages,\n shouldHaveColorSelector,\n shouldIgnoreFloorAndCeilingCollision,\n shouldShowDoorHint,\n getMissingMandatoryProductCategories,\n getFilters,\n};\n","import products from './productHandler';\nimport { filter } from '../../util/propFilter';\nimport { applicationSettings } from '../../settings/application';\nimport rangeSettings from '../../settings/constants';\nimport store from '../../state';\nimport Bror from './bror';\nimport Jonaxel from './jonaxel';\nimport Boaxel from './boaxel';\nimport Aurdal from './aurdal';\nimport Ivar from './ivar';\nimport Elvarli from './elvarli';\nimport { ITEMS } from '../../constants';\nimport constants from '../../settings/constants';\nimport { selectCurrentFilterName } from '../../state/productMenu/productMenuSelectors';\nimport { selectTac } from '../../state/tac/tacSelectors';\n\nlet rangeApi;\n\nfunction getRangeApi(appName) {\n switch (appName) {\n case 'BROR':\n return Bror;\n case 'JONAXEL':\n return Jonaxel;\n case 'BOAXEL':\n return Boaxel;\n case 'AURDAL':\n return Aurdal;\n case 'IVAR':\n return Ivar;\n case 'ELVARLI':\n return Elvarli;\n default:\n throw new Error(\n 'Missing implementation of range specific product service'\n );\n }\n}\n\nfunction init() {\n rangeApi = getRangeApi(applicationSettings.applicationName);\n}\n\nfunction makeArticleImages() {\n rangeApi.makeArticleImages();\n}\n\nfunction isStackable(item) {\n return rangeApi.isStackable && rangeApi.isStackable(item);\n}\n\nfunction getArticleImages() {\n return rangeApi.articleImages;\n}\n\nfunction getArticleImage(id) {\n return rangeApi.getArticleImage(id);\n}\nfunction getCustomProductTypeText(product) {\n return rangeApi.getCustomArticleTypeText?.(product);\n}\n\nfunction isOnlyAvailableInMultipack(productId) {\n const multipackProductId = constants.BULK_ARTICLES?.[productId]?.id;\n if (!multipackProductId) return false;\n\n const isAvailableAndValid = id =>\n products.some(product => product.id === id && product.valid);\n const productIsValid = isAvailableAndValid(productId);\n const multipackProductIsValid = isAvailableAndValid(multipackProductId);\n\n const isMultipackOnly = multipackProductIsValid && !productIsValid;\n return isMultipackOnly;\n}\n\n/**\n * Gets the known products.\n * Note that invalid products are normally not returned, unless they are invalid\n * products for which there is a corresponding valid multipack product. In that case,\n * they are returned even if includeAllInvalid is false. When includeAllInvalid is true,\n * all known products are returned regardless of whether they are valid or invalid.\n * @param includeAllInvalid Whether to also return invalid products\n * even when they are not \"multipack only\".\n * @returns {TacItem[]} The products.\n */\nfunction getAll(includeAllInvalid = false) {\n if (includeAllInvalid) {\n return products;\n }\n return products.filter(\n product => product.valid || isOnlyAvailableInMultipack(product.id)\n );\n}\n\n/**\n * Gets the product with the specified id.\n * Note that invalid products are normally not included in the search, unless they are\n * invalid products for which there is a corresponding valid multipack product. In that\n * case, they are included even if includeAllInvalid is false. When includeAllInvalid is\n * true, all invalid articles are included in the search.\n * @param id The id of the product to get.\n * @param includeAllInvalid Whether to also search for invalid products\n * even when they are not \"multipack only\".\n * @returns {TacItem | undefined} The product, if found. Otherwise undefined.\n */\nfunction getProduct(id, includeAllInvalid = false) {\n if (id.filter && (id.valid || isOnlyAvailableInMultipack(id.id))) {\n // This is already a \"complete\" and valid product obj, no need to do extra lookup.\n return id;\n }\n if (id.id) {\n id = id.id;\n }\n return getAll(includeAllInvalid).find(product => product.id === id);\n}\n\nfunction getMultipackProductOf(productId) {\n const multipackProductId = constants.BULK_ARTICLES?.[productId]?.id;\n if (!multipackProductId) return null;\n\n return getProduct(multipackProductId);\n}\n\nfunction getFilteredItems(selector, ...filters) {\n let items = getAll().filter(selector);\n\n if (filters) {\n items = filter(items, ...filters);\n }\n\n return items;\n}\n\nfunction getIncludedArticles(productId, list = []) {\n const product = getProduct(productId, true);\n\n if (!product) {\n return;\n }\n\n if (product.iows) {\n for (let i = 0; i < product.iows.length; i++) {\n const itemno = product.iows[i].itemno;\n list.push(itemno);\n }\n }\n\n if (product.parts) {\n Object.values(product.parts).forEach(part => {\n getIncludedArticles(part, list);\n });\n }\n return list;\n}\n\nfunction getSectionInserts(...filters) {\n return rangeApi.getSectionInserts\n ? rangeApi.getSectionInserts(filters)\n : null;\n}\n\nfunction isAddOnShelf(id) {\n const found = getProduct(id, true);\n return found && found.filter.type === ITEMS.ADD_ON;\n}\n\nfunction isCabinet(id) {\n const found = getProduct(id, true);\n return found && found.filter.type === ITEMS.CABINET;\n}\n\nfunction isPegboard(id) {\n const found = getProduct(id, true);\n return found && found.filter.type === ITEMS.PEGBOARD;\n}\n\nfunction isSection(id) {\n return rangeApi.isSection(id);\n}\n\nfunction getSections(...filters) {\n return getFilteredItems(isSection, ...filters);\n}\n\nfunction isShelf(id) {\n const found = getProduct(id, true);\n return found && found.filter.type === ITEMS.SHELF;\n}\n\nfunction isDrawer(id) {\n const found = getProduct(id, true);\n return found && found.filter.type === ITEMS.DRAWER;\n}\n\nfunction getShelves(...filters) {\n return getFilteredItems(isShelf, ...filters);\n}\n\nfunction getDrawers(...filters) {\n return getFilteredItems(isDrawer, ...filters);\n}\n\nfunction isTable(id) {\n const found = getProduct(id, true);\n return found && found.filter.type === ITEMS.TABLE;\n}\n\nfunction isTrolley(id) {\n const found = getProduct(id, true);\n return found && found.filter.type === ITEMS.TROLLEY;\n}\n\nfunction isWorkbench(id) {\n const found = getProduct(id, true);\n return found && found.filter.type === ITEMS.WORKBENCH;\n}\n\nfunction isShelvingUnit(id) {\n const found = getProduct(id, true);\n return found && found.filter.type === ITEMS.SHELVING_UNIT;\n}\n\nfunction isFrame(id) {\n const found = getProduct(id, true);\n return found && found.filter.type === ITEMS.FRAME;\n}\n\nfunction isLeg(id) {\n const found = getProduct(id, true);\n return found && found.filter.type === 'leg';\n}\n\nfunction isUpright(id) {\n const found = getProduct(id, true);\n return found && found.filter.type === ITEMS.UPRIGHT;\n}\n\nfunction isExtendable(id) {\n const found = getProduct(id, true);\n return found && found.logic && found.logic.extendable;\n}\n\nfunction isType(id, typeOrTypes) {\n const types = [].concat(typeOrTypes);\n const found = getProduct(id, true);\n return found && types.some(type => type === found.filter.type);\n}\n\nfunction isModule(id) {\n return rangeApi.isModule(id);\n}\n\nfunction isInsert(id) {\n return rangeApi.isInsert(id);\n}\n\nfunction getFit(item, slot) {\n return rangeApi.getFit(item, slot);\n}\n\nfunction getDragMode(product) {\n return rangeApi.getDragMode(product);\n}\n\nfunction getSwappablesOfType(typeOrTypes, ...filters) {\n function isOfCorrectType(candidate) {\n return isType(candidate, typeOrTypes);\n }\n return getFilteredItems(isOfCorrectType, ...filters);\n}\n\nfunction getSwappables(item, ...filters) {\n return getSwappablesOfType(item.filter.type, ...filters);\n}\n\nfunction isMultiParentProduct(item) {\n return rangeApi.isMultiParentProduct\n ? rangeApi.isMultiParentProduct(item)\n : false;\n}\n\nfunction isRealArticleProduct(id) {\n const found = getProduct(id, true);\n return found && found.iows && found.iows.length;\n}\n\nconst showInfoIcon = id =>\n rangeApi.showInfoIcon ? rangeApi.showInfoIcon(id) : isRealArticleProduct(id);\n\nfunction getFittingBracket(product, side, color) {\n return rangeApi.getFittingBracket\n ? rangeApi.getFittingBracket(product, side, color)\n : null;\n}\n\nfunction areMountingRailsOfSpecificColorValid(color) {\n return rangeApi.areMountingRailsOfSpecificColorValid\n ? rangeApi.areMountingRailsOfSpecificColorValid(color)\n : false;\n}\n\nfunction getValidMountingRailColors() {\n return rangeApi.getValidMountingRailColors\n ? rangeApi.getValidMountingRailColors()\n : null;\n}\n\nfunction areMountingRailsValid() {\n return rangeApi.areMountingRailsValid\n ? rangeApi.areMountingRailsValid()\n : false;\n}\n\nfunction isDependentItem(item) {\n return rangeApi.isDependentItem ? rangeApi.isDependentItem(item) : false;\n}\n\nfunction isAdjustable(item) {\n return rangeApi.isAdjustable ? rangeApi.isAdjustable(item) : false;\n}\n\nfunction getSectionOffset(item) {\n return rangeApi.getSectionOffset\n ? rangeApi.getSectionOffset(item)\n : { x: 0, y: 0, z: 0 };\n}\n\nfunction shouldHaveColorSelector(item) {\n return rangeApi.shouldHaveColorSelector?.(item);\n}\n\nfunction shouldHaveChildColorSelector(item) {\n return rangeApi.shouldHaveChildColorSelector\n ? rangeApi.shouldHaveChildColorSelector(item)\n : isSection(item);\n}\n\nfunction shouldHaveHeadlineForSelectors(item) {\n return rangeApi.shouldHaveHeadlineForSelectors?.(item) || false;\n}\n\nfunction shouldShowDoorHint(item) {\n return rangeApi.shouldShowDoorHint?.(item) || false;\n}\n\nfunction getInitialZPos(item) {\n return rangeApi.getInitialZPos?.(item) || 0;\n}\n\nfunction hasAllMandatoryProductsOfColor(color) {\n return rangeApi.hasAllMandatoryProductsOfColor\n ? rangeApi.hasAllMandatoryProductsOfColor(color)\n : true;\n}\n\nfunction getMissingMandatoryProductCategories(filter) {\n return rangeApi.getMissingMandatoryProductCategories\n ? rangeApi.getMissingMandatoryProductCategories(filter)\n : [];\n}\n\nfunction fitsTable(section) {\n return rangeApi.fitsTable?.(section) || false;\n}\n\nfunction shouldIgnoreFloorAndCeilingCollision(item) {\n return rangeApi?.shouldIgnoreFloorAndCeilingCollision?.(item) || false;\n}\n\nfunction canHaveOverlappingDropAreas(item) {\n return rangeApi?.canHaveOverlappingDropAreas?.(item);\n}\n\nconst generateProppingImageResources = imageList =>\n imageList.reduce((acc, image) => {\n if (!image?.proppingBounds.length) return acc;\n\n const proppingImageResources = image.proppingBounds.map((img, i) => ({\n name: `${image.name}_propping_${i + 1}`,\n url: `${rangeSettings.IMAGE_ROOT}${image.modelid}_propping_${i + 1}_${\n image.color\n }_0001.png`,\n }));\n\n return [...acc, ...proppingImageResources];\n }, imageList);\n\nconst deriveMeasurements = products => {\n const { depths, widths, heights } = products.reduce(\n ({ depths, widths, heights }, { depth, width, height }) => ({\n depths: depth ? depths.add(depth) : depths,\n widths: width ? widths.add(width) : widths,\n heights: height ? heights.add(height) : heights,\n }),\n { depths: new Set(), widths: new Set(), heights: new Set() }\n );\n\n return { depth: [...depths], width: [...widths], height: [...heights] };\n};\n\nconst getPostWidth = () =>\n rangeApi?.getPostWidth ? rangeApi?.getPostWidth() : constants.POST_WIDTH;\n\n/**\n * Gets a matching shelf for a specific section\n * @param {*} section\n * @returns {Object | undefined}\n */\nconst getFittingShelf = section => {\n return rangeApi?.getFittingShelf?.(section);\n};\n\n/**\n * The general thought with the items filter system is that each item-filter-object\n * has 4 keys: 'Component', 'condition', 'filter' and 'appliesTo'. Each is explained below.\n *\n * The 'Component' is simply a react component used to handle nad update the filter.\n *\n * 'condition' decides whether or not said filter should have its component\n * rendered as well as whether or not to run the actual filtering functionality.\n * In case of returning false, the filter will not be applied on the productMenuItems,\n * and the filter-component will not be rendered.\n *\n * 'filter' is a curry function which takes the redux-state as the first argument\n * where it can then derive and prep any needed redux data for the coming\n * filter function. The second part is the actual filter, where it takes an item\n * and must return a boolean, which will determine whether or not said item will be\n * part of productMenu.\n *\n * 'appliesTo' is a way to only apply said filter-function to a specific swiper filter.\n * It is an array in which you put the names of the filters to which the filter-function\n * is to be applied. In case 'appliesTo' is left empty, the filter-function should apply\n * to all filters.\n *\n * This way we can reuse filter functionality and combine different filter functionality\n * with different filter components. If a specific combination is only supposed to be\n * rendered for a certain filter group, such as 'Sections' or 'Inserts', the 'condition'\n * part can be used to single out when the filter is to be applied.\n *\n * @returns {*|*[]}\n */\nconst getFilters = () => {\n const filters = rangeApi.getFilters() ? rangeApi.getFilters() : [];\n\n return filters.filter(({ condition, appliesTo }) => {\n const currentFilter = selectCurrentFilterName(store.getState());\n\n return (\n (!appliesTo.length || appliesTo.includes(currentFilter)) && condition()\n );\n });\n};\n\nconst isWallDependentSection = section => {\n return section.items?.some(item => item.logic.ceiling);\n};\n\nconst getWallHeightDependentSections = allItems => {\n return allItems?.filter(item => isWallDependentSection(item));\n};\n\nconst getWallHeightDependentParts = sections => {\n return sections.reduce((acc, curr) => {\n return [\n ...acc,\n ...curr.items.reduce((acc, curr) => {\n return curr.logic.ceiling ? [...acc, curr] : acc;\n }, []),\n ];\n }, []);\n};\n\nexport const getWallHeightDependentItems = () => {\n const tac = selectTac(store.getState());\n const allItems = tac.items;\n return {\n sections: getWallHeightDependentSections(allItems),\n parts: getWallHeightDependentParts(\n getWallHeightDependentSections(allItems)\n ),\n };\n};\n\nconst hasMissingBrackets = product => {\n return rangeApi?.hasMissingBrackets\n ? rangeApi.hasMissingBrackets(product)\n : false;\n};\n\nconst getColorsOfProducts = products =>\n products.reduce(\n (colorsThisFar, product) =>\n colorsThisFar.includes(product.filter.color)\n ? colorsThisFar\n : [...colorsThisFar, product.filter.color],\n []\n );\n\nexport {\n filter,\n getAll,\n getArticleImage,\n getArticleImages,\n getCustomProductTypeText,\n getFittingBracket,\n getProduct,\n getSections,\n getShelves,\n getDrawers,\n hasAllMandatoryProductsOfColor,\n init,\n isAddOnShelf,\n isAdjustable,\n isCabinet,\n isModule,\n isInsert,\n isPegboard,\n isSection,\n isShelf,\n isDrawer,\n isTable,\n isTrolley,\n isWorkbench,\n isShelvingUnit,\n isFrame,\n isLeg,\n isUpright,\n isExtendable,\n isType,\n isStackable,\n products,\n getFilteredItems,\n getFit,\n getDragMode,\n getIncludedArticles,\n getSwappablesOfType,\n getSwappables,\n isMultiParentProduct,\n isRealArticleProduct,\n areMountingRailsOfSpecificColorValid,\n getValidMountingRailColors,\n areMountingRailsValid,\n isDependentItem,\n getSectionOffset,\n shouldHaveColorSelector,\n shouldHaveChildColorSelector,\n shouldHaveHeadlineForSelectors,\n shouldShowDoorHint,\n getInitialZPos,\n getMissingMandatoryProductCategories,\n fitsTable,\n shouldIgnoreFloorAndCeilingCollision,\n canHaveOverlappingDropAreas,\n generateProppingImageResources,\n deriveMeasurements,\n getFittingShelf,\n getFilters,\n getPostWidth,\n hasMissingBrackets,\n isOnlyAvailableInMultipack,\n getMultipackProductOf,\n getColorsOfProducts,\n};\n\nexport default {\n filter,\n getAll,\n getArticleImage,\n getArticleImages,\n getCustomProductTypeText,\n getFittingBracket,\n getProduct,\n getSections,\n getShelves,\n hasAllMandatoryProductsOfColor,\n init,\n isAddOnShelf,\n isAdjustable,\n isCabinet,\n isModule,\n isInsert,\n isPegboard,\n isSection,\n isShelf,\n isDrawer,\n isTable,\n isTrolley,\n isWorkbench,\n isShelvingUnit,\n isFrame,\n isUpright,\n isExtendable,\n isType,\n isLeg,\n isStackable,\n products,\n getFilteredItems,\n getFit,\n getDragMode,\n getIncludedArticles,\n getSectionInserts,\n getSwappablesOfType,\n getSwappables,\n isMultiParentProduct,\n isRealArticleProduct,\n areMountingRailsOfSpecificColorValid,\n getValidMountingRailColors,\n areMountingRailsValid,\n isDependentItem,\n getSectionOffset,\n shouldHaveColorSelector,\n shouldHaveChildColorSelector,\n shouldHaveHeadlineForSelectors,\n getInitialZPos,\n getMissingMandatoryProductCategories,\n fitsTable,\n shouldIgnoreFloorAndCeilingCollision,\n makeArticleImages,\n canHaveOverlappingDropAreas,\n getFittingShelf,\n showInfoIcon,\n getFilters,\n getPostWidth,\n hasMissingBrackets,\n isOnlyAvailableInMultipack,\n getMultipackProductOf,\n getColorsOfProducts,\n};\n","import products from './products';\nimport tacHelper from '../state/tac/tacHelpers';\nimport convert from '../util/aactools/convert';\n\nexport default function isValid(pac) {\n const canValidate = pac && pac.model && pac.model.items;\n if (!canValidate) {\n return false;\n }\n const tac = convert.PACtoTAC(pac);\n if (!tac) {\n return false;\n }\n\n const items = [...tacHelper.getAllItems(tac.items)];\n return items.every(item => !!products.getProduct(item.id));\n}\n","import React from 'react';\nimport {\n DesignInteractionEnum,\n DesignSourceEnum,\n} from '@insights/insights-data-provider';\nimport { OpenDesignCodeStateEnum } from '@inter-ikea-kompis/component-open-design-code';\nimport fixOldVpc from '../services/FixVPC';\nimport pacValid from '../services/PacValidator';\nimport localStatisticsReporter from '../services/statistics/insights/custom/local/localStatisticsReporter';\nimport mandatoryStatisticsReporter from '../services/statistics/insights/mandatory/mandatoryStatisticsReporter';\nimport { getVpcService } from '../services/ServiceHandler';\n\nconst useVpc = ({\n successCallback = ({ pac }) => {},\n failCallback = () => {},\n}) => {\n const [loadRes, setLoadRes] = React.useState(null);\n const [loadingVpcState, setLoadingVpcState] = React.useState(\n OpenDesignCodeStateEnum.default\n );\n\n const resolveFunctions = {\n [OpenDesignCodeStateEnum.default]: successCallback,\n [OpenDesignCodeStateEnum.failed]: failCallback,\n };\n\n React.useEffect(() => {\n loadRes && resolveFunctions[loadingVpcState](loadRes);\n }, [loadingVpcState, loadRes]);\n\n /**\n * Handles successfull loading of VPC\n *\n * @param vpc\n * @param pac\n */\n const handleSuccess = (vpc, pac) => {\n setLoadingVpcState(OpenDesignCodeStateEnum.default);\n setLoadRes({ vpc, pac });\n mandatoryStatisticsReporter.reportInitialPlanningSession(\n vpc.configurationId,\n DesignSourceEnum.enterCode\n );\n mandatoryStatisticsReporter.reportDesignInteractionEvent(\n vpc.configurationId,\n DesignInteractionEnum.getConfiguration\n );\n localStatisticsReporter.reportStartViewVpcEnterCodeSuccess(\n vpc.configurationId\n );\n };\n\n /**\n * Handles any failure to load VPC\n *\n * @param err\n */\n const handleFailure = err => {\n setLoadingVpcState(OpenDesignCodeStateEnum.failed);\n setLoadRes(err);\n };\n\n /**\n * Check pac validity\n *\n * @param pac\n * @param vpcConfig\n * @returns {*}\n */\n const handlePacValidity = (pac, vpcConfig) =>\n pacValid(pac)\n ? handleSuccess(vpcConfig, pac)\n : handleFailure('Pac invalid');\n\n /**\n * Handle VPC response\n *\n * @param vpcConfig\n */\n const handleResponse = vpcConfig =>\n handlePacValidity(fixOldVpc(vpcConfig.configuration), vpcConfig);\n\n /**\n * Initiates fetching of VPC\n *\n * @param vpc\n * @returns {Promise}\n */\n const fetchVpc = async vpc => {\n setLoadRes(null);\n setLoadingVpcState(OpenDesignCodeStateEnum.loading);\n\n return await getVpcService()\n .getConfiguration(vpc)\n .then(handleResponse)\n .catch(handleFailure);\n };\n\n /**\n * Reset hook and it's data\n */\n const resetHook = () => {\n setLoadRes(null);\n setLoadingVpcState(OpenDesignCodeStateEnum.default);\n };\n\n return {\n vpcLoadingState: loadingVpcState,\n fetchVpc,\n resetHook,\n };\n};\n\nexport default useVpc;\n","export const getCoatHangerIconSvg = (\n width = 24,\n height = 18\n) => `\n\n\n`;\n","export const getCoverIconSvg = (\n width = 24,\n height = 18\n) => `\n\n\n`;\n","export const getDoorsIconSvg = (\n width = 24,\n height = 18\n) => `\n\n\n\n\n`;\n","export const getFramesIconSvg = (\n width = 24,\n height = 18\n) => `\n\n\n`;\n","export const getHandPointIconSvg = (\n width = 24,\n height = 24\n) => `\n\n`;\n","export const getInteriorsIconSvg = (\n width = 24,\n height = 18\n) => `\n\n\n\n\n`;\n","export const getSectionsIconSvg = (\n width = 24,\n height = 18\n) => `\n\n\n`;\n","export const getShelvingUnitsIconSvg = (\n width = 24,\n height = 18\n) => `\n\n\n`;\n","export const getTablesIconSvg = (\n width = 24,\n height = 18\n) => `\n\n\n`;\n","export const getTrashcanIconSvg = (\n width = 24,\n height = 18\n) => `\n\n\n\n\n`;\n","export const getUprightsIconSvg = (\n width = 24,\n height = 18\n) => `\n\n\n`;\n","export const getWallMeasurementIconSvg = (\n width = 24,\n height = 18\n) => `\n\n\n`;\n","export const getWheelsIconSvg = (\n width = 24,\n height = 18\n) => `\n\n\n\n`;\n","import { getCoatHangerIconSvg } from './CoatHangerIcon';\nimport { getCoverIconSvg } from './CoverIcon';\nimport { getDoorsIconSvg } from './DoorsIcon';\nimport { getFramesIconSvg } from './FramesIcon';\nimport { getHandPointIconSvg } from './HandPointIcon';\nimport { getInteriorsIconSvg } from './InteriorsIcon';\nimport { getSectionsIconSvg } from './SectionsIcon';\nimport { getShelvingUnitsIconSvg } from './ShelvingUnitsIcon';\nimport { getTablesIconSvg } from './TablesIcon';\nimport { getTrashcanIconSvg } from './TrashcanIcon';\nimport { getUprightsIconSvg } from './UprightsIcon';\nimport { getWallMeasurementIconSvg } from './WallMeasurementIcon';\nimport { getWheelsIconSvg } from './WheelsIcon';\n\nexport default function getCustomIcon(iconName: string) {\n let customIconSvg: string | undefined = undefined;\n\n switch (iconName) {\n case 'CoatHangerIcon':\n customIconSvg = getCoatHangerIconSvg();\n break;\n case 'CoverIcon':\n customIconSvg = getCoverIconSvg();\n break;\n case 'DoorsIcon':\n customIconSvg = getDoorsIconSvg();\n break;\n case 'FramesIcon':\n customIconSvg = getFramesIconSvg();\n break;\n case 'HandPointIcon':\n customIconSvg = getHandPointIconSvg();\n break;\n case 'InteriorsIcon':\n customIconSvg = getInteriorsIconSvg();\n break;\n case 'SectionsIcon':\n customIconSvg = getSectionsIconSvg();\n break;\n case 'ShelvingUnitsIcon':\n customIconSvg = getShelvingUnitsIconSvg();\n break;\n case 'TablesIcon':\n customIconSvg = getTablesIconSvg();\n break;\n case 'TrashcanIcon':\n customIconSvg = getTrashcanIconSvg();\n break;\n case 'UprightsIcon':\n customIconSvg = getUprightsIconSvg();\n break;\n case 'WallMeasurementIcon':\n customIconSvg = getWallMeasurementIconSvg();\n break;\n case 'WheelsIcon':\n customIconSvg = getWheelsIconSvg();\n break;\n default:\n break;\n }\n return customIconSvg;\n}\n","import getCustomIcon from '../../../icons/custom/getCustomIcon';\nimport * as kompisIcons from '@inter-ikea-kompis/icons';\nimport { ITheme } from '@inter-ikea-kompis/types';\nimport { ButtonTypeEnum } from '@inter-ikea-kompis/component-button';\nimport { IconButtonSizeEnum } from '@inter-ikea-kompis/component-icon-button';\nimport { SkapaTheme } from '@inter-ikea-kompis/themes';\n\nexport interface IconProps {\n iconName: string;\n type?: ButtonTypeEnum;\n size?: IconButtonSizeEnum;\n onClick?: (customEvent: CustomEvent) => any;\n theme?: ITheme;\n className?: any;\n label?: string;\n ariaLabel?: string;\n invertedColors?: boolean;\n flipIconRtl?: boolean;\n}\n\n/**\n * Get kompis icon\n * @param iconName\n * @param theme\n * @returns {string|*}\n */\nexport const getKompisIconData = (iconName: string, theme?: ITheme) => {\n const errorString = `The icon with name ${iconName} is not found amongst our custom icons or kompis icons.`;\n const svgAsString = getCustomIcon(iconName)\n ? getCustomIcon(iconName)\n : // @ts-ignore\n kompisIcons[iconName];\n\n if (!svgAsString) throw new Error(errorString);\n\n return {\n icon: svgAsString,\n theme: SkapaTheme,\n };\n};\n","import React from 'react';\nimport { KompisIconButton } from '@inter-ikea-kompis/react-components';\nimport { getKompisIconData, IconProps } from './utils/kompis/getKompisIconData';\n\nexport const IconButton: React.FunctionComponent = ({\n iconName,\n ...props\n}) => ;\n","import { State } from '../StateTypes';\n\n/**\n * Select vpc\n *\n * @param vpc\n */\nexport const selectVpcSlice = ({ vpc }: State) => vpc;\n\n/**\n * Select vpc save state\n * @param state\n */\nexport const selectSaveVpcState = (state: State) =>\n selectVpcSlice(state).save.state;\n\n/**\n * Select saved vpc code\n * @param state\n */\nexport const selectSavedVpcCode = (state: State) =>\n selectVpcSlice(state).save?.code;\n\n/**\n * Select last saved vpc config\n * @param state\n */\nexport const selectDirtyConfigurationState = (state: State) =>\n selectVpcSlice(state).save.dirtyConfiguration;\n\n/**\n * Select save VPC progress status\n * @param state\n */\nexport const selectSaveVpcProgressStatus = (state: State) =>\n selectVpcSlice(state).save.progressStatus;\n","import { ButtonTypeEnum } from '@inter-ikea-kompis/component-button';\nimport {\n KompisButton,\n KompisTooltip,\n} from '@inter-ikea-kompis/react-components';\nimport { IconButton } from './IconButton';\nimport { SkapaTheme } from '@inter-ikea-kompis/themes/lib';\nimport React from 'react';\nimport { translate } from '../services/L10n';\nimport { useDispatch, useSelector } from 'react-redux';\nimport {\n actionOpenSeriesGallery,\n openIpexGalleryWarning,\n} from '../state/dialog/dialogActions';\n// @ts-ignore\nimport classNames from 'classnames';\nimport styles from './TopBar/TopBar.module.less';\nimport { thunkResetApp } from '../state/init/initThunks';\nimport { selectIsRtl } from '../state/dexfSettings/dexfSettingsSelectors';\nimport { IconButtonSizeEnum } from '@inter-ikea-kompis/component-icon-button';\nimport { selectDirtyConfigurationState } from '../state/vpc/vpcSelectors';\nimport { useKioskIntegration } from '../hooks/useKioskIntegration';\nimport { t } from '../translations';\n\nexport interface IProps {\n useArrowButton: boolean;\n}\n\nconst OpenSeriesGalleryButton: React.FC = ({ useArrowButton }) => {\n const dirtyConfiguration = useSelector(selectDirtyConfigurationState);\n const dispatch = useDispatch();\n const openSeriesGallery = (data: any) =>\n dispatch(actionOpenSeriesGallery(data));\n const isRtl = useSelector(selectIsRtl);\n const { integration, isUpptackaOrIpexGallery } = useKioskIntegration();\n\n const ipexGalleryShouldBeUsed = () => isUpptackaOrIpexGallery;\n\n const handleClick = ({ detail: { event } }: CustomEvent) => {\n if (!ipexGalleryShouldBeUsed()) {\n dispatch(\n openSeriesGallery({\n sprGalleryOptions: { closable: true },\n })\n );\n } else if (dirtyConfiguration) {\n dispatch(openIpexGalleryWarning(event));\n } else {\n integration?.exitClicked();\n }\n\n dispatch(thunkResetApp());\n };\n\n const renderArrowButton = () => (\n \n \n \n );\n\n const renderSeriesGalleryButton = () => (\n \n );\n\n return useArrowButton ? renderArrowButton() : renderSeriesGalleryButton();\n};\n\nexport default OpenSeriesGalleryButton;\n","import { useKioskIntegration } from '../../hooks/useKioskIntegration';\nimport { UrlUtility } from '../../util/aactools/urlUtility';\nimport platform from '../../util/platform';\n\nconst urlUtility = new UrlUtility();\n\nconst getApplications = () => {\n const appInfo = urlUtility.getAppInfo().seriesGallery?.applications;\n return appInfo;\n};\n\nconst severalApplicationsExist = () => {\n return getApplications().length > 1;\n};\n\nconst shouldShowOpenSeriesGalleryButton = () => {\n const { isUpptackaOrIpexGallery } = useKioskIntegration();\n return (\n (isUpptackaOrIpexGallery || severalApplicationsExist()) && platform.isKiosk\n );\n};\n\nexport default shouldShowOpenSeriesGalleryButton;\n","/**\n * Gets a new url string where the old locale is replaced with the new locale query parameter.\n * @param fromLocale\n * @param toLocale\n * @param url\n * @returns {string} A new url where the old locale is replaced with the new locale query parameter\n */\nfunction getUrlWithNewLocaleDevelopment(\n fromLocale: string,\n toLocale: string,\n url: string\n): string {\n fromLocale = dashToUnderscore(fromLocale);\n const fromLocaleUpperOrLowercase = new RegExp(fromLocale, 'gi');\n toLocale = dashToUnderscore(toLocale).toLocaleLowerCase();\n return url.replace(fromLocaleUpperOrLowercase, toLocale);\n}\n\n/**\n * Checks to see if the url contains localhost and assumes it is in development\n * environment if it does.\n * @param url\n * @returns A boolean value indicating if we are in development mode\n */\nfunction inLocalhostMode(url: string): boolean {\n return url.includes('localhost') || url.includes('127.0.0.1');\n}\n\n/**\n * Changes the url from the old locale to the new locale for the production environment.\n * @param locale\n * @param url\n * @returns {string} A url that contains the new locale\n */\nfunction getUrlWithNewLocaleProduction(locale: string, url: string): string {\n locale = dashToUnderscore(locale);\n const appUrl = new URL(url);\n appUrl.searchParams.set(\n 'locale',\n dashToUnderscore(locale).toLocaleLowerCase()\n );\n const comma = ',';\n const commaASCIIRegExp = /%2C/g;\n return appUrl.href.replace(commaASCIIRegExp, comma);\n}\n\n/**\n * Handles updating the window with the new url/locale.\n * @param newLocale\n */\nfunction updateLocale(newLocale: string): void {\n window.location.href = newLocale;\n if (!inLocalhostMode(newLocale)) return;\n window.location.reload();\n}\n/**\n * Gets a new url based on the toLocale string and then replaces the current url\n * with the new url/locale and then reloads the window.\n * @param fromLocale\n * @param toLocale\n * @param url\n */\nexport function changeLocale(\n url: string,\n fromLocale: string,\n toLocale: string\n) {\n const newLocale = inLocalhostMode(url)\n ? getUrlWithNewLocaleDevelopment(fromLocale, toLocale, url)\n : getUrlWithNewLocaleProduction(toLocale, url);\n updateLocale(newLocale);\n}\n/**\n * Replaces the dash with an underscore in a locale string.\n * @param locale\n * @returns {string} A locale string in lowercase form where the dash is replaced with and underscore.\n */\nfunction dashToUnderscore(locale: string): string {\n return locale.replace('-', '_');\n}\n","import { ConfirmationSummaryShareDesignStateEnum } from '@inter-ikea-kompis/component-configuration-summary';\n\n/* TODO: Turn this one (including VpcSaveProgressStatusEnumType below) into a\n real enum once our TypeScript version supports it. */\nexport const VpcSaveProgressStatusEnum = {\n default: 'default',\n saving: 'saving',\n success: 'success',\n failure: 'failure',\n};\n\nexport type VpcSaveProgressStatusEnumType = string;\n\nexport type Vpc = {\n load: {\n code: string;\n state: boolean;\n };\n save: {\n code: string;\n state: ConfirmationSummaryShareDesignStateEnum;\n dirtyConfiguration: boolean;\n progressStatus: VpcSaveProgressStatusEnumType;\n };\n vpcErrorList: [];\n};\n\nexport type VpcStoredAction = string;\n\nexport interface VpcLoadedAction {\n currentView: string;\n code: string;\n}\n","import { State } from '../StateTypes';\nimport {\n selectSaveVpcProgressStatus,\n selectSaveVpcState,\n} from '../vpc/vpcSelectors';\nimport { VpcSaveProgressStatusEnum } from '../vpc/vpcTypes';\nimport { ConfirmationSummaryShareDesignStateEnum } from '@inter-ikea-kompis/component-configuration-summary';\nimport { AddToBagStateEnum } from '@inter-ikea-kompis/component-add-to-bag';\n\nexport const selectSummary = (state: State) => state.summary;\n\nexport const selectLocale = (state: State) => selectSummary(state).locale;\n\nexport const selectStoreId = (state: State) => selectSummary(state).storeId;\n\nexport const selectShoppingItems = (state: State) =>\n selectSummary(state).shoppingItems;\n\nexport const selectStoreAvailabilities = (state: State) =>\n selectSummary(state).storeAvailabilities;\n\nexport const selectDesignLink = (state: State) =>\n selectSummary(state).designLink;\n\nexport const selectConfirmationCardType = (state: State) =>\n selectSummary(state).confirmationCardType;\n\nexport const selectSceneImage = (state: State) =>\n selectSummary(state).sceneImage;\n\nexport const selectModalState = (state: State) =>\n selectSummary(state).modal.showModal;\n\nexport const selectAddToCartState = (state: State) =>\n selectSummary(state).states.addToCart;\n\nexport const selectModalType = (state: State) =>\n selectSummary(state).modal.modalType;\n\nexport const selectAddUnavailableProductsToList = (state: State) =>\n selectSummary(state).addUnavailableProductsToList;\n\nexport const selectFailedShoppingItems = (state: State) =>\n selectSummary(state).failedShoppingItems;\n\nexport const selectAddToListState = (state: State) =>\n selectSummary(state).states.addToList;\n\nexport const selectCurrentConfiguration = (state: State) =>\n selectSummary(state).currentConfiguration;\n\nexport const selectShareDesignCard = (state: State) =>\n selectSummary(state).shareDesignCard;\n\nexport const selectHomeDelivery = (state: State) =>\n selectSummary(state).homeDelivery;\n\nexport const selectFinancingOption = (state: State) =>\n selectSummary(state).financingOptions;\n\n/**\n * Selector returning true if the design link is either ready, or,\n * alternatively, its generation has failed (which can happen if\n * VPC save fails). Returns false if its generation has not yet\n * started or is still in progress.\n * @param state\n * @returns {boolean} The current status of the design link.\n */\nexport const selectIsDesignLinkEitherReadyOrFailed = (\n state: State\n): boolean => {\n const vpcSuccess: boolean =\n selectSaveVpcProgressStatus(state) === VpcSaveProgressStatusEnum.success;\n const vpcFailure: boolean =\n selectSaveVpcProgressStatus(state) === VpcSaveProgressStatusEnum.failure;\n const designLinkPresent: boolean = !!selectDesignLink(state);\n\n return vpcFailure || (vpcSuccess && designLinkPresent);\n};\n\n/**\n * Checks whether any \"user action\" is in progress, i.e., whether either \"Add to cart\",\n * \"Add to list\" or \"Save VPC\" is in loading state.\n * @param state\n * @returns {boolean} Whether there is any \"user action\" in progress, as defined above.\n */\nexport const selectIsAnyUserActionInProgress = (state: State): boolean => {\n const saveVpcState: ConfirmationSummaryShareDesignStateEnum =\n selectSaveVpcState(state);\n const addToCartState: AddToBagStateEnum = selectAddToCartState(state);\n const addToListState: AddToBagStateEnum = selectAddToListState(state);\n\n const saveVpcLoading: boolean =\n saveVpcState === ConfirmationSummaryShareDesignStateEnum.loading;\n const addToCartLoading: boolean =\n addToCartState === AddToBagStateEnum.loading;\n const addToListLoading: boolean =\n addToListState === AddToBagStateEnum.loading;\n\n return saveVpcLoading || addToCartLoading || addToListLoading;\n};\n","import React from 'react';\nimport { useSelector } from 'react-redux';\n\nimport { KompisLanguageSelector } from '@inter-ikea-kompis/react-components';\nimport { changeLocale } from '../../util/locale';\n\nimport constants from '../../settings/constants';\nimport storage from '../../services/history/storage';\nimport { SkapaTheme } from '@inter-ikea-kompis/themes';\nimport { applicationSettings } from '../../settings/application';\nimport classNames from 'classnames';\nimport styles from './TopBar.module.less';\nimport { selectKompisTranslations } from '../../state/translations/translationsSelectors';\nimport { selectDexfSettings } from '../../state/dexfSettings/dexfSettingsSelectors';\nimport { selectCurrentView } from '../../state/navigation/navigationSelectors';\nimport { selectLocale } from '../../state/summary/summarySelectors';\nimport localStatisticsReporter from '../../services/statistics/insights/custom/local/localStatisticsReporter';\nimport { selectScreensaverIsActive } from '../../state/screensaver/screensaverSelectors';\nimport { useKioskIntegration } from '../../hooks/useKioskIntegration';\n\nconst LanguageSelector: React.FC = () => {\n const currentView = useSelector(selectCurrentView);\n const kompisTranslations = useSelector(selectKompisTranslations);\n const dexfSettings = useSelector(selectDexfSettings);\n const screenSaverActive = useSelector(selectScreensaverIsActive);\n const locale = useSelector(selectLocale);\n\n const { integration, isGallery } = useKioskIntegration();\n\n const [visibleModal, setVisibleModal] = React.useState();\n\n React.useEffect(() => {\n storage.session.removeItem(\n constants.SESSION_STORAGE.LANGUAGE_PICKER_ACTIVE_VIEW_KEY\n );\n }, []);\n\n React.useEffect(() => {\n onModalClose();\n }, [screenSaverActive]);\n\n const onModalOpen = ({ detail: { visibleModal } }: any) =>\n setVisibleModal(visibleModal);\n\n const onModalClose = () => setVisibleModal(undefined);\n\n const onChange = ({ detail: { locale } }: any) => {\n storage.session.setItem(\n constants.SESSION_STORAGE.LANGUAGE_PICKER_ACTIVE_VIEW_KEY,\n currentView\n );\n const fromLocale = applicationSettings.locale.slice();\n\n isGallery && integration?.languageChange(locale);\n\n changeLocale(window.location.href, fromLocale, locale);\n localStatisticsReporter.reportLanguageSelectorUserChoice(\n fromLocale,\n locale,\n currentView\n );\n };\n\n return (\n \n );\n};\n\nexport default LanguageSelector;\n","import React from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport {\n KompisImage,\n KompisProductCard,\n KompisButton,\n KompisPopover,\n KompisPopoverPosition,\n KompisPopoverPadding,\n KompisIconButton,\n KompisOpenDesignCode,\n KompisText,\n} from '@inter-ikea-kompis/react-components';\nimport { SkapaTheme } from '@inter-ikea-kompis/themes';\nimport { ThemeFontStyleTypeEnum } from '@inter-ikea-kompis/enums';\nimport styles from './StartView.module.less';\nimport {\n ProductCardLayoutEnum,\n ProductCardSelectBehaviourEnum,\n ProductCardMediaEnum,\n} from '@inter-ikea-kompis/component-product-card';\nimport { selectMostRecentHistory } from '../../state/navigation/navigationSelectors';\nimport masterConstants from '../../settings/masterConstants';\nimport { nextView } from '../../state/navigation';\nimport useVpc from '../../hooks/useVpc';\nimport { selectSprsToDisplay } from '../../state/sprs/sprsSelectors';\nimport { thunkLoadSpr, thunkResetScene } from '../../state/tac/tacThunks';\nimport constants from '../../settings/constants';\nimport { translate } from '../../services/L10n';\nimport platform from '../../util/platform';\nimport {\n ButtonTypeEnum,\n ButtonSizeEnum,\n} from '@inter-ikea-kompis/component-button';\nimport { IconButtonSizeEnum } from '@inter-ikea-kompis/component-icon-button';\nimport PopoverDirectionEnum from '@inter-ikea-kompis/component-popover/lib/enums/PopoverDirectionEnum';\nimport deviceTypes from '../../util/deviceTypes';\nimport { selectDeviceType } from '../../state/userAgent/userAgentSelectors';\nimport classNames from 'classnames';\nimport { ArrowRightIcon, ArrowLeftIcon } from '@inter-ikea-kompis/icons';\nimport { selectKompisTranslations } from '../../state/translations/translationsSelectors';\nimport {\n selectDexfSettings,\n selectUseMetric,\n selectIsRtl,\n} from '../../state/dexfSettings/dexfSettingsSelectors';\nimport { selectScreensaverIsActive } from '../../state/screensaver/screensaverSelectors';\nimport {\n DesignInteractionEnum,\n DesignSourceEnum,\n} from '@insights/insights-data-provider';\nimport { selectTacIsEmpty } from '../../state/tac/tacReducer/tacSelectors';\nimport OpenSeriesGalleryButton from '../../components/OpenSeriesGalleryButton';\nimport shouldShowOpenSeriesGalleryButton from '../../components/utils/seriesGallery';\nimport mandatoryStatisticsReporter from '../../services/statistics/insights/mandatory/mandatoryStatisticsReporter';\nimport localStatisticsReporter from '../../services/statistics/insights/custom/local/localStatisticsReporter';\nimport { applicationSettings } from '../../settings/application';\nimport LanguageSelector from '../../components/TopBar/LanguageSelector';\nimport { ProductDescriptionProductMeasureEnum } from '@inter-ikea-kompis/utilities/lib';\nimport { SprObject } from '../../state/sprs/sprsTypes';\nimport { useKioskIntegration } from '../../hooks/useKioskIntegration';\nimport { t } from '../../translations';\n\nconst StartView = () => {\n const sprs = useSelector(selectSprsToDisplay);\n const kompisTranslations = useSelector(selectKompisTranslations);\n const dexfSettings = useSelector(selectDexfSettings);\n const shouldUseMetric = useSelector(selectUseMetric);\n const isRtl = useSelector(selectIsRtl);\n const mostRecentHistory = useSelector(selectMostRecentHistory);\n const deviceType = useSelector(selectDeviceType);\n const screensaverActive = useSelector(selectScreensaverIsActive);\n const tacIsEmpty = useSelector(selectTacIsEmpty);\n\n const { isUpptacka } = useKioskIntegration();\n\n const dispatch = useDispatch();\n const resetScene = () => dispatch(thunkResetScene());\n const loadSpr = (spr: object) => dispatch(thunkLoadSpr(spr));\n const goToScene = () => dispatch(nextView());\n\n const [showVpcInput, setShowVpcInput] = React.useState(false);\n const mobilePortraitDeviceType = `${deviceTypes.MOBILE}_portrait`;\n const APPLICATION_NAME = applicationSettings.applicationName;\n\n /**\n * Handles response where pac was valid\n *\n * @param vpcConfig\n * @param pac { vpc, pac }\n */\n const handleValidPac = ({ pac }: any) => {\n loadSpr(pac);\n };\n\n const { vpcLoadingState, fetchVpc, resetHook } = useVpc({\n successCallback: handleValidPac,\n });\n\n /**\n * Resets start view\n *\n * @private\n */\n const _resetHook = () => {\n resetHook();\n setShowVpcInput(false);\n };\n\n React.useEffect(() => {\n screensaverActive && _resetHook();\n }, [screensaverActive]);\n\n const sprsPerRowBasedOnDevice = {\n [deviceTypes.MOBILE]: 1,\n [mobilePortraitDeviceType]: 2,\n [deviceTypes.TABLET]: 3,\n [deviceTypes.DESKTOP]: 4,\n };\n\n /**\n * Generate an aria label for an SPR product.\n *\n * @param {*} product The SPR product for which to generate an aria label.\n * @returns {string} The generated aria label.\n * @private\n */\n const _getProductCardAriaLabel = (product: SprObject) => {\n if (!product?.kompisSPR?.content) return '';\n\n const { typeName, validDesignText, measureReference } =\n product.kompisSPR.content;\n const measureReferenceString = shouldUseMetric\n ? measureReference?.textMetric\n : measureReference?.textImperial;\n\n const textElements = [];\n typeName && textElements.push(typeName);\n validDesignText && textElements.push(validDesignText);\n measureReferenceString && textElements.push(measureReferenceString);\n\n const ariaLabel = textElements.join(', ');\n\n return ariaLabel;\n };\n\n /**\n * Handles display setting for VPC input on user input\n *\n * @private\n */\n const _setShowVpcInput = () => setShowVpcInput(!showVpcInput);\n\n /**\n * Fetches and opens VPC\n *\n * @returns {Promise}\n */\n const openDesign = (event: CustomEvent) => {\n const { vpcCode } = event.detail;\n fetchVpc(vpcCode);\n };\n\n /**\n * Resets and routes to scene\n */\n const startClean = () => {\n mandatoryStatisticsReporter.reportInitialPlanningSession(\n '',\n DesignSourceEnum.startFromScratch\n );\n resetScene();\n };\n\n /**\n * Returns true for all but the last row of products\n *\n * @param index\n * @returns {boolean}\n */\n const shouldHaveBorder = (index: number) =>\n sprs.length + 1 - index >\n (sprs.length + 1) % sprsPerRowBasedOnDevice[deviceType];\n\n /**\n * Returns correct card wrapper classes\n *\n * @param index\n * @returns {string}\n */\n const getCardWrapperClasses = (index: number) =>\n classNames(styles.kompisProductCardWrapper, {\n [styles.kompisProductCardWrapper__noBorder]: !shouldHaveBorder(index),\n });\n\n /**\n * On user click\n *\n * @param spr\n * @returns {function(): void}\n * @private\n */\n const _onClick = (spr: { vpcCode: string; id: string }) => () => {\n loadSpr(spr);\n\n mandatoryStatisticsReporter.reportDesignInteractionEvent(\n spr.vpcCode,\n DesignInteractionEnum.getConfiguration\n );\n mandatoryStatisticsReporter.reportInitialPlanningSession(\n spr.vpcCode,\n DesignSourceEnum.gallery\n );\n localStatisticsReporter.reportStartViewSprClick(spr.id);\n };\n\n /**\n * Render VPC input\n *\n * @returns {JSX.Element}\n */\n const renderVpcInput = () => (\n \n \n \n
\n \n \n \n
\n \n
\n );\n\n /**\n * Renders a wrapper around a card\n *\n * @param child\n * @param index\n * @param key\n * @returns {JSX.Element}\n */\n const renderCardWrapper = (\n child: {} | null | undefined,\n index = 0,\n key = ''\n ): JSX.Element => (\n
\n {child}\n
\n );\n\n /**\n * Render product card\n *\n * @param product\n * @param index\n * @returns {JSX.Element}\n */\n const renderProductCard = (product: SprObject, index: number): JSX.Element =>\n renderCardWrapper(\n ,\n index + 1,\n product.kompisSPR.itemId\n );\n\n /**\n * Returns SPR button icon based on isRtl\n *\n * @returns {*}\n */\n const getSprButtonIcon = () => (isRtl ? ArrowLeftIcon : ArrowRightIcon);\n\n /**\n * Render go to SPR button\n *\n * @returns {boolean|JSX.Element}\n */\n const renderSprButton = () =>\n mostRecentHistory === masterConstants.VIEW_NAMES.SCENE &&\n !tacIsEmpty && (\n \n );\n\n /**\n * Render OpenSeriesGallery button\n *\n * @returns {JSX.Element}\n */\n const renderOpenSeriesGalleryButton = () => {\n return ;\n };\n\n /**\n * Render start from beginning\n *\n * @returns {JSX.Element}\n */\n const renderStartFromBeginning = () =>\n renderCardWrapper(\n \n \n \n \n );\n\n /**\n * Create product card. Index is incremented by one here, due to the \"renderStartFromBeginning\" which is\n * no included in the SPR's array, yet has to be accounted for when calculating which items should have a bottom-border.\n *\n * @returns {*}\n */\n const createProductCards = () => sprs.map(renderProductCard);\n\n /**\n * Render header\n *\n * @param title\n * @returns {JSX.Element}\n */\n const renderHeader = (title: {} | null | undefined) =>\n platform.isKiosk ? (\n

{title}

\n ) : (\n \n {title}\n \n );\n\n const renderGallery = () => (\n
\n
\n
\n {shouldShowOpenSeriesGalleryButton() &&\n renderOpenSeriesGalleryButton()}\n {platform.isKiosk && (\n

\n {APPLICATION_NAME}\n

\n )}\n {renderHeader(translate(t.SPR_GALLERY_HEADLINE))}\n
\n\n
\n
\n {platform.isKiosk && !isUpptacka && }\n
\n
\n {renderVpcInput()}\n {renderSprButton()}\n
\n
\n
\n
\n {renderStartFromBeginning()}\n {createProductCards()}\n
\n
\n );\n\n return renderGallery();\n};\n\nexport default StartView;\n","import React from 'react';\nimport { getKompisIconData, IconProps } from './utils/kompis/getKompisIconData';\nimport { KompisIconPill } from '@inter-ikea-kompis/react-components';\nimport IconPillSizeOptionsEnum from '@inter-ikea-kompis/component-icon-pill/lib/enums/IconPillSizeOptionsEnum';\n\ninterface IconPillProps extends IconProps {\n iconPillSize: IconPillSizeOptionsEnum;\n onSelect: () => any;\n selected: boolean;\n disabled: boolean;\n}\n\nexport const IconPill: React.FunctionComponent = ({\n iconName,\n ...props\n}) => ;\n","import React from 'react';\nimport styles from '../ActionButtons.module.less';\nimport { IconPillSizeOptionsEnum } from '@inter-ikea-kompis/component-icon-pill';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { translate } from '../../../services/L10n';\nimport { TOP } from '../../Popup/alignments';\nimport Tooltip from '../../Popup/Tooltip';\nimport platform from '../../../util/platform';\nimport { IconPill } from '../../IconPill';\nimport {\n selectIsMobile,\n selectIsMobilePortrait,\n} from '../../../state/userAgent/userAgentSelectors';\nimport {\n selectIsMeasurementsActive,\n selectIsWallResizerActive,\n} from '../../../state/scene/sceneSelectors';\nimport { thunkToggleMeasurements } from '../../../state/scene/sceneThunks';\nimport { actionSetWallResizerInactive } from '../../../state/scene';\nimport { t } from '../../../translations';\n\nconst Measurements: React.FC = () => {\n const dispatch = useDispatch();\n const toggleMeasurements = () => dispatch(thunkToggleMeasurements());\n\n const measurementsActive = useSelector(selectIsMeasurementsActive);\n const wallResizerActive = useSelector(selectIsWallResizerActive);\n const isMobile = useSelector(selectIsMobile);\n const isMobilePortrait = useSelector(selectIsMobilePortrait);\n\n /**\n * On click\n */\n const onClick = (): void => {\n if (!(wallResizerActive && !isMobilePortrait)) {\n toggleMeasurements();\n }\n wallResizerActive &&\n dispatch(actionSetWallResizerInactive({ nonInteraction: true }));\n };\n\n /**\n * Get correct translation based on if measurements is active\n */\n const getMeasurementsActiveTranslation = (): string =>\n measurementsActive\n ? translate(t.BUTTON_HIDE_MEASURES)\n : translate(t.BUTTON_SHOW_MEASURES);\n\n /**\n * Get icon pill size based on whether platform is kiosk or not\n */\n const getIconPillSize = (): IconPillSizeOptionsEnum =>\n platform.isKiosk\n ? IconPillSizeOptionsEnum.medium\n : IconPillSizeOptionsEnum.small;\n\n /**\n * Returns boolean whether icon pill should be disabled\n */\n const disableIconPill = (): boolean =>\n !wallResizerActive || isMobilePortrait\n ? !!(wallResizerActive && !isMobilePortrait)\n : true;\n\n return !wallResizerActive || isMobile ? (\n
\n \n \n \n
\n ) : null;\n};\n\nexport default Measurements;\n","import React from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport styles from './UndoRedo.module.less';\nimport { TOP } from '../Popup/alignments';\nimport Tooltip from '../Popup/Tooltip';\nimport { translate } from '../../services/L10n';\nimport classNames from 'classnames';\nimport platform from '../../util/platform';\nimport { IconButton } from '../IconButton';\nimport { ButtonTypeEnum } from '@inter-ikea-kompis/component-button';\nimport { IconButtonSizeEnum } from '@inter-ikea-kompis/component-icon-button';\nimport { selectUserAgent } from '../../state/userAgent/userAgentSelectors';\nimport { thunkRedo, thunkUndo } from '../../state/tac/tacThunks';\nimport { selectIsWallResizerActive } from '../../state/scene/sceneSelectors.ts';\nimport {\n selectHasRedoHistory,\n selectHasUndoHistory,\n} from '../../state/tac/tacSelectors.ts';\nimport { selectWriteDirection } from '../../state/dexfSettings/dexfSettingsSelectors';\nimport { t } from '../../translations';\n\nconst UndoRedo = () => {\n const canUndo = useSelector(selectHasUndoHistory);\n const canRedo = useSelector(selectHasRedoHistory);\n const userAgent = useSelector(selectUserAgent);\n const wallResizerActive = useSelector(selectIsWallResizerActive);\n const writeDirection = useSelector(selectWriteDirection);\n\n const dispatch = useDispatch();\n const onUndo = () => dispatch(thunkUndo());\n const onRedo = () => dispatch(thunkRedo());\n\n const isKiosk = platform.isKiosk;\n const iconSize = isKiosk\n ? IconButtonSizeEnum.medium\n : IconButtonSizeEnum.small;\n\n return !wallResizerActive || userAgent.isMobile ? (\n
\n \n \n \n\n \n \n \n
\n ) : null;\n};\n\nexport default UndoRedo;\n","import { useDispatch, useSelector } from 'react-redux';\nimport React, { FunctionComponent } from 'react';\n\nimport { DIMENSIONS } from '../../../constants';\nimport { selectUseMetric } from '../../../state/dexfSettings/dexfSettingsSelectors';\nimport { translate } from '../../../services/L10n';\nimport geometry from '../../../scene/util/geometry';\nimport swiperService from '../../../services/swiper';\n\nimport styles from '../ActionButtons.module.less';\nimport { selectPresentTac } from '../../../state/tac/tacReducer/tacSelectors';\nimport { selectIsMobile } from '../../../state/userAgent/userAgentSelectors';\nimport {\n selectIsMeasurementsActive,\n selectIsWallResizerActive,\n} from '../../../state/scene/sceneSelectors';\nimport {\n actionHideMeasurements,\n actionSetWallResizerInactive,\n} from '../../../state/scene';\nimport { t } from '../../../translations';\n\nconst WallMeasurementsLegend: FunctionComponent = () => {\n const tac = useSelector(selectPresentTac);\n const useMetricMeasures = useSelector(selectUseMetric);\n const wallResizerActive = useSelector(selectIsWallResizerActive);\n const measurementsActive = useSelector(selectIsMeasurementsActive);\n const isMobile = useSelector(selectIsMobile);\n\n const dispatch = useDispatch();\n const setWallResizerInactive = () =>\n dispatch(actionSetWallResizerInactive({ nonInteraction: true }));\n const hideMeasurements = () => dispatch(actionHideMeasurements({}));\n\n const wallSize = geometry.surround(tac.wall.points);\n const { width, height } = DIMENSIONS;\n\n /**\n * Renders wallmeasurements\n * @param width\n * @param height\n * @returns {JSX.Element}\n */\n const wallSizeEl = [width, height].map(dimension => (\n \n {`${translate(t[`LABEL_${dimension.toUpperCase()}`])}\n ${swiperService.getWallMeasurementText(\n // @ts-ignore LIMITATION OF ANCIENT, PRE-HISTORIC TS-VERSION NOT SUPPORTING ENUMS\n wallSize[dimension],\n useMetricMeasures\n )}`}\n \n ));\n\n /**\n * Handles click on wallmeasurements\n */\n const onClick = () => {\n wallResizerActive && setWallResizerInactive();\n measurementsActive && hideMeasurements();\n };\n\n return (\n
\n {isMobile && (\n
\n {translate(t.TOOLTIP_WALL_MEASUREMENTS)}\n
\n )}\n {wallSizeEl}\n
\n );\n};\n\nexport default WallMeasurementsLegend;\n","import React from 'react';\nimport { IconPill } from '../../IconPill';\nimport { IconPillSizeOptionsEnum } from '@inter-ikea-kompis/component-icon-pill';\nimport Tooltip from '../../Popup/Tooltip';\nimport { translate } from '../../../services/L10n';\nimport { TOP } from '../../Popup/alignments';\nimport { useDispatch, useSelector } from 'react-redux';\nimport platform from '../../../util/platform';\nimport styles from '../ActionButtons.module.less';\nimport WallMeasurementsLegend from './WallMeasurementsLegend';\nimport Transition from '../../Transition';\nimport Portal from '../../utils/Portal';\nimport { selectIsMobile } from '../../../state/userAgent/userAgentSelectors';\nimport { selectIsWallResizerActive } from '../../../state/scene/sceneSelectors';\nimport { thunkToggleWallResizer } from '../../../state/scene/sceneThunks';\nimport { t } from '../../../translations';\n\nconst WallResizerIcon = () => {\n const readyToPortal = React.useRef(false);\n\n const isMobile = useSelector(selectIsMobile);\n const wallResizerActive = useSelector(selectIsWallResizerActive);\n\n const dispatch = useDispatch();\n const toggleWallResizer = () => dispatch(thunkToggleWallResizer());\n\n React.useEffect(() => {\n readyToPortal.current = true;\n }, []);\n\n const renderLegend = () => (\n \n \n \n );\n\n const renderPortal = () =>\n readyToPortal.current && (\n {renderLegend()}\n );\n\n const renderLegendOrPortal = () =>\n isMobile ? renderPortal() : renderLegend();\n\n return (\n
\n \n \n \n {renderLegendOrPortal()}\n
\n );\n};\n\nexport default WallResizerIcon;\n","import { CustomAction } from '../../generalTypes';\nimport {\n DEQUEUE_SHEET,\n ENQUEUE_SHEET,\n DEQUEUE_ALL_SHEETS,\n} from '../actionConstants';\nimport { SheetObject } from './sheetTypes';\n\nexport const actionEnqueueSheet = (\n sheet: SheetObject\n): CustomAction<{ sheet: SheetObject }> => ({\n type: ENQUEUE_SHEET,\n payload: {\n sheet,\n },\n});\nexport const actionDequeueSheet = () => ({\n type: DEQUEUE_SHEET,\n});\n\nexport const actionDequeueAllSheets = () => ({ type: DEQUEUE_ALL_SHEETS });\n","import React from 'react';\nimport { connect } from 'react-redux';\nimport PropTypes from 'prop-types';\nimport classNames from 'classnames';\n\nimport { KompisProductList } from '@inter-ikea-kompis/react-components';\nimport { SkapaTheme } from '@inter-ikea-kompis/themes';\n\nimport { applicationSettings } from '../../settings/application';\nimport {\n getShoppingItemsFromTAC,\n getShoppingProductsFromTAC,\n} from '../../util/aactools/kompisConvert';\nimport platform from '../../util/platform';\nimport { getCheckoutAvailabilityService } from '../../services/ServiceHandler';\n\nimport styles from './ProductList.module.less';\nimport { selectKompisTranslations } from '../../state/translations/translationsSelectors';\nimport { selectDexfSettings } from '../../state/dexfSettings/dexfSettingsSelectors';\nimport { selectTac } from '../../state/tac/tacSelectors.ts';\n\nconst ProductList = ({ tac, kompisTranslations, dexfSettings }) => {\n const { storeId } = applicationSettings;\n\n const shoppingProducts = tac ? getShoppingProductsFromTAC(tac) : [];\n const isKiosk = platform.isKiosk;\n\n const [storeAvailabilities, setStoreAvailabilities] = React.useState(null);\n const [visibleEnergyLabelCard, setVisibleEnergyLabelCard] =\n React.useState(null);\n const [visibleModal, setVisibleModal] = React.useState(null);\n\n React.useEffect(() => {\n async function getStoreAvailabilities() {\n const productItems = getShoppingItemsFromTAC(tac, true);\n const storeAvailabilities = await (\n await getCheckoutAvailabilityService()\n ).getStoreAvailabilities(storeId, productItems);\n setStoreAvailabilities(storeAvailabilities);\n }\n if (storeId) {\n getStoreAvailabilities();\n }\n }, []);\n\n const _setVisibleEnergyLabelCard = event =>\n event?.detail?.visibleEnergyLabelCard\n ? setVisibleEnergyLabelCard(event.detail.visibleEnergyLabelCard)\n : setVisibleEnergyLabelCard(null);\n\n const _setVisibleModal = event =>\n event?.detail?.visibleModal\n ? setVisibleModal(event.detail.visibleModal)\n : setVisibleModal(null);\n\n return (\n
\n \n \n
\n \n );\n};\n\nProductList.propTypes = {\n tac: PropTypes.object,\n kompisTranslations: PropTypes.object.isRequired,\n dexfSettings: PropTypes.object.isRequired,\n};\n\nexport default connect(state => ({\n kompisTranslations: selectKompisTranslations(state),\n dexfSettings: selectDexfSettings(state),\n tac: selectTac(state),\n}))(ProductList);\n","import React, { useEffect, useState } from 'react';\nimport { SkapaTheme } from '@inter-ikea-kompis/themes';\nimport {\n KompisSheet,\n KompisSheetHeader,\n KompisSheetBody,\n} from '@inter-ikea-kompis/react-components';\nimport { useDispatch } from 'react-redux';\nimport ProductList from '../ProductList/ProductList';\nimport { translate } from '../../services/L10n';\nimport { actionDequeueAllSheets } from '../../state/sheets/sheetActions';\nimport { t } from '../../translations';\n\nexport const WHATS_INCLUDED = 'WHATS_INCLUDED';\n\nexport const WhatsIncludedSheet: React.FC = () => {\n const padding = { padding: '8px 32px 32px 32px' };\n const dispatch = useDispatch();\n const closeAllSheets = () => dispatch(actionDequeueAllSheets());\n\n const [sheetVisible, setSheetVisible] = useState(true);\n\n useEffect(() => {\n return () => setSheetVisible(false);\n }, []);\n\n return (\n \n \n \n
\n \n
\n
\n \n );\n};\n","import { State } from '../StateTypes';\nimport { SheetObject, SheetQueue } from './sheetTypes';\n\nexport const selectSheetQueue = (state: State): SheetQueue =>\n state.sheets.queue;\n\nexport const selectLatestSheetInQueue = (\n state: State\n): SheetObject | undefined =>\n selectSheetQueue(state)[selectLatestSheetIndex(state)];\n\nexport const selectLatestSheetIndex = (state: State): number => {\n const queueLength = selectSheetQueue(state).length;\n return queueLength > 0 ? queueLength - 1 : queueLength;\n};\n","export function getPlannerVersion(): string {\n const versionMatch = document.location.href.match(/\\/(\\d+\\.\\d+\\.\\d+\\.\\d+)\\//);\n return versionMatch ? versionMatch[1] : '0.1';\n}\n","import { getArticleContent } from '../../services/products/articles';\nimport { applicationSettings } from '../../settings/application';\nimport geometry from '../../scene/util/geometry';\nimport tacHelpers from '../../state/tac/tacHelpers';\nimport constants from '../../settings/constants';\nimport { isRealArticleProduct, isType } from '../../services/products';\nimport { TacItem } from '../../generalTypes';\nimport { TacModel } from '../../state/tac/tacTypes';\nimport { getPlannerVersion } from '../plannerVersion';\n\ninterface ICFVector3 {\n x: number;\n y: number;\n z: number;\n}\n\ninterface ICFArticle {\n id: number;\n parent_ids: number[];\n child_ids: number[];\n product_id: string;\n name: string;\n category: string;\n colors: string[];\n attributes: Record;\n dimensions: ICFVector3;\n transform: {\n position: ICFVector3;\n rotation: ICFVector3;\n scale: ICFVector3;\n };\n}\n\ninterface ICFArticleDimensionsAndTransform {\n dimensions: ICFVector3;\n transform: {\n position: ICFVector3;\n rotation: ICFVector3;\n scale: ICFVector3;\n };\n}\n\ninterface ICFModel {\n icf_version: string;\n drawing_id?: string;\n vpc_code?: string;\n application_id: string;\n application_version: string | undefined;\n creation_datetime: string;\n articles: ICFArticle[];\n metadata?: Record;\n}\n\nfunction getFormattedDateTime(): string {\n // '%Y-%m-%dT%H:%M:%SZ', '2021-01-01T01:01:01Z'\n return new Date().toISOString();\n}\n\nfunction getArticleId(internalId: string): string {\n const article = getArticleContent(internalId);\n return article\n ? `${article.itemType}-${article.itemNoGlobal || article.itemNoLocal}`\n : '';\n}\n\nfunction getColorHexCode(item: TacItem): string {\n return constants.COLOR_CODES[item.filter.color];\n}\n\nfunction isValidICFItem(item: TacItem): boolean {\n return isRealArticleProduct(item);\n}\n\nfunction getICFArticleDimensionsAndTransform(\n item: TacItem,\n parent: TacItem | null,\n tac: TacModel\n): ICFArticleDimensionsAndTransform {\n const { depth, height, width } = item;\n\n //Not loving this but can't come up with anything better atm.\n const isSidePanel = isType(item, 'side-panel');\n const actualDepth = isSidePanel && parent ? parent.depth : depth;\n\n const dimensions = {\n x: width,\n y: height,\n z: actualDepth,\n };\n\n const globalCoords = tacHelpers.getGlobalCoords(\n { ...item, depth: actualDepth },\n tac\n );\n const centeredCoords = geometry.getCenter(globalCoords);\n const { x, y, z } = centeredCoords;\n\n return {\n dimensions,\n transform: {\n position: {\n x,\n y,\n z,\n },\n rotation: {\n x: 0,\n y: 0,\n z: 0,\n },\n scale: { x: 1, y: 1, z: 1 },\n },\n };\n}\n\nfunction getICFArticlesFromTacItem(\n item: TacItem,\n parent: TacItem | null,\n tac: TacModel\n): ICFArticle[] {\n const { filter, itemid, items: children, name, iows, sprno } = item;\n\n const articleIds = [\n ...(sprno ? [sprno] : iows.map(iowsItem => iowsItem.itemno)),\n ];\n\n const parentIsValid = parent && isValidICFItem(parent);\n\n // @ts-ignore\n return articleIds.map(id => ({\n id: itemid,\n parent_ids: [...(parent && parentIsValid ? [parent.itemid] : [])],\n child_ids: [\n ...(children\n ? children.filter(isValidICFItem).map((child: TacItem) => child.itemid)\n : []),\n ],\n product_id: getArticleId(id),\n name,\n category: filter.type,\n colors: [getColorHexCode(item)],\n attributes: {},\n ...getICFArticleDimensionsAndTransform(item, parent, tac),\n }));\n}\n\nfunction getICFArticlesFromTacItemHierarchy(\n item: TacItem,\n parent: TacItem | null,\n tac: TacModel\n): ICFArticle[] {\n const { items: children } = item;\n\n const itemIsValid = isValidICFItem(item);\n\n return [\n ...(itemIsValid ? getICFArticlesFromTacItem(item, parent, tac) : []),\n ...(children\n ? children.flatMap((child: TacItem) =>\n getICFArticlesFromTacItemHierarchy(child, item, tac)\n )\n : []),\n ];\n}\n\nfunction getICFArticlesFromTac(tac: TacModel): ICFArticle[] {\n return tac.items.flatMap((item: TacItem) =>\n getICFArticlesFromTacItemHierarchy(item, null, tac)\n );\n}\n\nexport function getICF(tac: TacModel): ICFModel {\n return {\n icf_version: '1.0',\n application_id: applicationSettings.applicationName.toLowerCase(),\n application_version: getPlannerVersion(),\n creation_datetime: getFormattedDateTime(),\n articles: getICFArticlesFromTac(tac),\n };\n}\n","/**\n * Function for creating a delay\n * @param waitTime\n */\nconst wait = async (waitTime: number) => {\n return new Promise(resolve => {\n setTimeout(async () => {\n resolve();\n }, waitTime);\n });\n};\n\nexport default wait;\n","import { STORES_SET } from '../actionConstants';\n\nexport const actionSetStores = stores => ({\n type: STORES_SET,\n payload: { stores },\n});\n","import { getStoreService } from '../../services/ServiceHandler';\nimport { actionSetStores } from './storesActions';\n\nexport const thunkFetchStores = () => async dispatch => {\n try {\n const stores = await getStoreService().getStores();\n dispatch(actionSetStores(stores));\n } catch (error) {\n console.warn('StoreService failed to fetch stores');\n console.error(error);\n }\n};\n","import constants from '../../settings/constants';\nimport { applicationSettings } from '../../settings/application';\nimport {\n actionSetAddToCartState,\n actionSetAddToListState,\n actionSetAddUnavailableProductsToList,\n actionSetConfirmationCardType,\n actionSetShareDesignCardState,\n actionSetHideModal,\n actionSetModalType,\n actionSetShoppingItems,\n actionSetShowModal,\n actionSetStoreAvailabilities,\n actionSetStoreId,\n actionSetDesignLink,\n actionSetFailedShoppingItems,\n actionSetSendByEmailState,\n actionSetHomeDeliveryState,\n actionSetHomeDeliveryZipCode,\n actionSetHomeDeliveryZipAvailabilities,\n actionSetFinancingOptions,\n} from './summaryActions';\nimport { actionSetOverlayState } from '../popups/popupsActions';\nimport {\n selectHomeDelivery,\n selectShoppingItems,\n selectStoreId,\n} from './summarySelectors';\nimport { selectTac } from '../tac/tacSelectors';\nimport { selectDexfSettings } from '../dexfSettings/dexfSettingsSelectors';\nimport { NotificationLinkTypeEnum } from '@inter-ikea-kompis/enums';\nimport { SendByEmailStateEnum } from '@inter-ikea-kompis/component-send-by-email';\nimport { getShoppingItemsFromTAC } from '../../util/aactools/kompisConvert';\nimport {\n SummaryPageModalEnum,\n SummaryPageConfirmationCardTypeEnum,\n} from '@inter-ikea-kompis/component-summary-page';\nimport ZipInCardStateEnum from '@inter-ikea-kompis/component-zip-in/lib/enums/ZipInCardStateEnum.js';\nimport { AddToBagStateEnum } from '@inter-ikea-kompis/component-add-to-bag';\nimport wait from '../../util/wait';\nimport {\n thunkResetSavedVpc,\n thunkSaveVpc,\n thunkRetrySaveVpcIfNeeded,\n} from '../vpc/vpcThunks';\nimport { thunkFetchStores } from '../stores/storeThunk';\nimport {\n selectDirtyConfigurationState,\n selectSavedVpcCode,\n} from '../vpc/vpcSelectors';\nimport { SummaryModal, SummaryCards } from './summaryTypes';\nimport { Thunk } from '../../generalTypes';\nimport {\n ICart,\n IFailedShoppingItem,\n IShoppingItem,\n IShoppingProduct,\n IZipAvailability,\n} from '@inter-ikea-kompis/types';\nimport localStatisticsReporter from '../../services/statistics/insights/custom/local/localStatisticsReporter';\nimport { renderToast } from '../../services/toastMaster';\nimport {\n getNotificationService,\n getZipValidationService,\n getShoppingCartService,\n getShoppingListService,\n getCheckoutAvailabilityService,\n getFinancingService,\n} from '../../services/ServiceHandler';\n\nconst {\n INITIAL_VIEW,\n VIEW_NAMES: { SUMMARY },\n} = constants;\n\nexport const confirmationModal: SummaryModal = {\n itemId: null,\n modal: SummaryPageModalEnum.confirmationCard,\n};\n\nexport const shareDesignModal: SummaryModal = {\n itemId: null,\n modal: SummaryPageModalEnum.shareDesignCard,\n};\n\nexport const buyInStore: SummaryModal = {\n itemId: null,\n modal: SummaryPageModalEnum.buyInStore,\n};\n\nexport const homeDeliveryModal: SummaryModal = {\n itemId: null,\n modal: SummaryPageModalEnum.zipIn,\n};\n\nexport const unavailableProductModal: SummaryModal = {\n itemId: null,\n modal: SummaryPageModalEnum.unavailableProductCard,\n};\n\ninterface IAddToCartError {\n failedShoppingItems?: IFailedShoppingItem[];\n}\n\n/**\n * Loads necessary data on for summary page\n * @param sceneImage\n */\nexport const thunkInitSummary: Thunk = () => async (dispatch, getState) => {\n dispatch(thunkFetchStores());\n dispatch(thunkGetShoppingItems());\n\n const state = getState();\n const dirtyConfiguration = selectDirtyConfigurationState(state);\n if (dirtyConfiguration) {\n dispatch(thunkResetSavedVpcAndDesignLink());\n dispatch(thunkSaveVpcAndGenerateDesignLink());\n }\n\n // If storeId sent through URL, set availability\n if (applicationSettings.storeId) {\n dispatch(thunkSetStoreAvailabilities());\n }\n};\n\n/**\n * Reset saved VPC and design link.\n */\nexport const thunkResetSavedVpcAndDesignLink: Thunk =\n () => (dispatch, getState) => {\n dispatch(thunkResetSavedVpc());\n dispatch(actionSetDesignLink(''));\n };\n\n/**\n * First save VPC, and after that generate design link.\n */\nexport const thunkSaveVpcAndGenerateDesignLink: Thunk =\n () => async (dispatch, getState) => {\n await dispatch(thunkSaveVpc());\n await dispatch(thunkGenerateDesignLink());\n };\n\nexport const thunkSetSummaryInstanceState: Thunk<\n [SummaryModal, SummaryCards]\n> = (modal, cards) => dispatch => {\n cards.confirmationCard &&\n dispatch(actionSetConfirmationCardType(cards.confirmationCard));\n cards.shareDesignCard &&\n dispatch(actionSetShareDesignCardState(cards.shareDesignCard));\n dispatch(actionSetModalType(modal));\n dispatch(actionSetShowModal());\n};\n\n/**\n * Get shopping items\n */\nexport const thunkGetShoppingItems: Thunk = () => (dispatch, getState) => {\n const state = getState();\n const tac = selectTac(state);\n\n if (!tac) return;\n\n dispatch(actionSetShoppingItems(getShoppingItemsFromTAC(tac, true)));\n};\n\n/**\n * Set store availabilities\n */\nexport const thunkSetStoreAvailabilities: Thunk =\n () => async (dispatch, getState) => {\n const state = getState();\n const storeId = selectStoreId(state);\n const tac = selectTac(state);\n const shoppingItems = selectShoppingItems(state);\n\n if (!tac || !storeId) return dispatch(actionSetStoreAvailabilities([]));\n\n try {\n const specificStoreAvailabilities =\n await getCheckoutAvailabilityService().getStoreAvailabilities(\n storeId,\n shoppingItems\n );\n\n dispatch(actionSetStoreAvailabilities(specificStoreAvailabilities));\n } catch (error) {\n console.warn(\n 'CheckoutAvailabilityService failed to fetch availabilities'\n );\n console.error(error);\n }\n };\n\n/**\n *\n * @param email\n * @param vpcCode\n * @returns\n */\nexport const thunkOnSendByEmailSend: Thunk<[string, string]> =\n (email, vpcCode) => (dispatch, getState) => {\n dispatch(actionSetSendByEmailState(SendByEmailStateEnum.loading));\n\n try {\n getNotificationService()\n .sendEmail({\n vpcCode,\n email,\n linkType: NotificationLinkTypeEnum.pickinglist,\n })\n .then(() =>\n dispatch(actionSetSendByEmailState(SendByEmailStateEnum.success))\n )\n .catch(() =>\n dispatch(actionSetSendByEmailState(SendByEmailStateEnum.failed))\n );\n } catch (error) {\n console.error(error);\n dispatch(actionSetSendByEmailState(SendByEmailStateEnum.failed));\n }\n };\n\n/**\n * Handle home delivery\n */\nexport const thunkHandleHomeDelivery =\n (zipCode?: any) => async (dispatch: any, getState: any) => {\n const homeDeliveryInputValue = zipCode\n ? zipCode\n : selectHomeDelivery(getState()).zipCode;\n const shoppingItems = selectShoppingItems(getState());\n dispatch(actionSetHomeDeliveryState(ZipInCardStateEnum.loading));\n\n // Using the Zip Validation Service to first check if the given zip code is valid or\n // not, before retrieving information about it with the Checkout Availability Service\n const validateZipCode = async () => {\n try {\n if (homeDeliveryInputValue) {\n const validate = await getZipValidationService().getZipValidation(\n homeDeliveryInputValue\n );\n return validate;\n }\n } catch (error) {\n dispatch(actionSetHomeDeliveryState(ZipInCardStateEnum.noZipInfo));\n console.error(error);\n return false;\n }\n };\n\n const zipValidation = await validateZipCode();\n if (!zipValidation) return;\n\n let zipAvailabilities: IZipAvailability[] = [];\n if (zipValidation.valid && zipValidation.content) {\n try {\n zipAvailabilities =\n await getCheckoutAvailabilityService().getZipAvailabilities(\n // Send in the formatted zip code\n zipValidation.content.formatted,\n shoppingItems\n );\n } catch (error) {\n console.error(error);\n }\n\n if (!zipAvailabilities || zipAvailabilities.length === 0) {\n dispatch(actionSetHomeDeliveryState(ZipInCardStateEnum.noZipInfo));\n dispatch(actionSetHomeDeliveryZipCode(homeDeliveryInputValue));\n } else {\n dispatch(actionSetHomeDeliveryState(ZipInCardStateEnum.submitted));\n dispatch(actionSetHomeDeliveryZipCode(homeDeliveryInputValue));\n dispatch(actionSetHomeDeliveryZipAvailabilities(zipAvailabilities));\n dispatch(actionSetOverlayState(false));\n dispatch(actionSetHideModal());\n }\n } else {\n dispatch(actionSetHomeDeliveryState(ZipInCardStateEnum.invalidInput));\n }\n };\n\n/**\n * Handle home delivery\n */\nexport const thunkHandleFinancingOptions =\n (shoppingProducts: IShoppingProduct[]) =>\n async (dispatch: any, getState: any) => {\n getFinancingService()\n .getFinancingOption(shoppingProducts)\n .then((financingOption: any) => {\n dispatch(actionSetFinancingOptions(financingOption));\n })\n .catch((error: Error) => {\n // Hide the financial services component in case of error.\n dispatch(actionSetFinancingOptions(undefined));\n console.log(error);\n });\n };\n\n/**\n * Set selected store\n * @param storeId\n */\nexport const thunkSetSelectedStore: Thunk<[string]> =\n storeId => async dispatch => {\n dispatch(actionSetStoreId(storeId));\n dispatch(thunkSetStoreAvailabilities());\n };\n\nexport const thunkGenerateDesignLink =\n () => async (dispatch: any, getState: any) => {\n const vpc = selectSavedVpcCode(getState());\n const splitLocale = applicationSettings.locale.split('-');\n const localeLanguageCode = splitLocale[0];\n const countryCode = splitLocale[1];\n const locale = `?locale=${localeLanguageCode}_${countryCode}`;\n\n const storeId = selectStoreId(getState());\n const storeIdQueryParam = storeId ? `&storeId=${storeId}` : '';\n const applicationName = applicationSettings.applicationName.toLowerCase();\n const initialView = `&${INITIAL_VIEW}=${SUMMARY}`;\n const hostPath = window.location.host + window.location.pathname;\n const trimmedPath = hostPath.replace('index.html', '');\n let url = '';\n\n if (vpc) {\n if (\n //dev environment\n trimmedPath.includes('localhost') ||\n trimmedPath.includes('azurewebsites.')\n ) {\n url = `${trimmedPath}m2/#/${applicationName}/#${vpc}${locale}${storeIdQueryParam}${initialView}`;\n } else {\n try {\n //CTE & Prod\n const dexfSettings = selectDexfSettings(getState());\n url = dexfSettings.vpc.pickinglist.replace(\n /{CODE}/,\n `${vpc}${storeIdQueryParam}${initialView}`\n );\n } catch (error) {\n // Handle error.\n console.log(error);\n }\n }\n }\n\n dispatch(actionSetDesignLink(url));\n };\n\nexport const thunkOnAddUnavailableToCartAndList: Thunk<\n [\n {\n shoppingListItems: IShoppingItem[];\n shoppingCartItems: IShoppingItem[];\n }\n ]\n> =\n ({ shoppingListItems, shoppingCartItems }) =>\n async (dispatch, getState) => {\n const WAITING_TIME_BETWEEN_LIST_AND_CART = 1000;\n\n const thereAreCartItems = !!shoppingCartItems.length;\n const thereAreListItems = !!shoppingListItems.length;\n\n dispatch(actionSetAddToCartState(AddToBagStateEnum.loading));\n\n await dispatch(thunkRetrySaveVpcIfNeeded());\n\n if (thereAreListItems) {\n dispatch(thunkOnAddToList(shoppingListItems, true));\n dispatch(actionSetFailedShoppingItems(shoppingListItems));\n }\n\n if (thereAreListItems && thereAreCartItems) {\n await wait(WAITING_TIME_BETWEEN_LIST_AND_CART);\n dispatch(actionSetHideModal());\n }\n\n if (thereAreCartItems)\n dispatch(\n thunkOnAddToCart({ shoppingCartItems: shoppingCartItems }, true)\n );\n else dispatch(actionSetAddToCartState(AddToBagStateEnum.default));\n };\n\nexport const thunkOnAddToList: Thunk<[boolean | IShoppingItem[], boolean]> =\n (items = false, fromAddUnavailable) =>\n async (dispatch, getState) => {\n const { addToListSuccess } = SummaryPageConfirmationCardTypeEnum;\n\n if (!fromAddUnavailable) {\n dispatch(actionSetAddToListState(AddToBagStateEnum.loading));\n await dispatch(thunkRetrySaveVpcIfNeeded());\n }\n\n const shoppingItems = items ? items : selectShoppingItems(getState());\n\n /**\n * Set add to list state failed\n */\n const setAddToListStateFailed = () => {\n !fromAddUnavailable &&\n dispatch(actionSetAddToListState(AddToBagStateEnum.default));\n renderToast('list', false, false);\n };\n\n /**\n * Set add to list state success\n */\n const setAddToListStateSuccess = async () => {\n dispatch(actionSetOverlayState(true));\n if (!fromAddUnavailable) {\n dispatch(actionSetAddToListState(AddToBagStateEnum.confirmation));\n dispatch(\n thunkSetSummaryInstanceState(confirmationModal, {\n confirmationCard: addToListSuccess,\n })\n );\n }\n };\n\n /**\n * Attempt to contact shopping list service\n */\n const tryShoppingListService = async () => {\n try {\n const vpcCode = selectSavedVpcCode(getState());\n return await getShoppingListService().addToList({\n items: shoppingItems,\n designCode: !fromAddUnavailable ? vpcCode : undefined,\n });\n } catch (error) {\n setAddToListStateFailed();\n return { errorList: [error] };\n }\n };\n\n /**\n * Handle shopping list errors\n */\n const handleShoppingListErrors = () => {\n setAddToListStateFailed();\n localStatisticsReporter.reportAddToListError();\n };\n\n !fromAddUnavailable && actionSetAddToListState(AddToBagStateEnum.loading);\n fromAddUnavailable && dispatch(actionSetAddUnavailableProductsToList(true));\n // onAddUnavailableItems gives us a different variable so we need to check both.\n const { errorList } = await tryShoppingListService();\n\n errorList.length ? handleShoppingListErrors() : setAddToListStateSuccess();\n };\n\n/**\n * On add to cart\n *\n * @param items\n */\nexport const thunkOnAddToCart: Thunk<\n [\n {\n shoppingItems?: IShoppingItem[];\n shoppingCartItems: IShoppingItem[];\n completeConfiguration?: boolean;\n },\n boolean\n ]\n> =\n (\n { shoppingItems, shoppingCartItems, completeConfiguration },\n fromAddUnavailable\n ) =>\n async (dispatch, getState) => {\n const shoppingCartProducts = shoppingItems || shoppingCartItems;\n const { addToCartSuccess } = SummaryPageConfirmationCardTypeEnum;\n\n /**\n * On add to cart error\n */\n const thunkOnAddToCartError: Thunk = () => dispatch => {\n dispatch(actionSetAddToCartState(AddToBagStateEnum.default));\n localStatisticsReporter.reportAddToBagError(shoppingCartProducts);\n };\n\n /**\n * Check if errors exist\n * @param errorList\n */\n const hasErrors = ({ errorList }: ICart) => {\n if (errorList.length) {\n dispatch(thunkOnAddToCartError());\n setAddToCartStateFailed();\n\n return true;\n }\n };\n\n /**\n * Set add to cart state successful\n */\n const setAddToCartStateSuccessful = () => {\n dispatch(actionSetOverlayState(true));\n dispatch(actionSetAddToCartState(AddToBagStateEnum.confirmation));\n dispatch(\n thunkSetSummaryInstanceState(confirmationModal, {\n confirmationCard: addToCartSuccess,\n })\n );\n };\n\n /**\n * Set add to cart state failed\n */\n const setAddToCartStateFailed = () => {\n dispatch(actionSetAddToCartState(AddToBagStateEnum.default));\n renderToast('cart', false, false);\n };\n\n /**\n * Attempt to add to cart\n */\n const tryShoppingCartService = async () => {\n try {\n const vpcCode = selectSavedVpcCode(getState());\n const cart = await getShoppingCartService().addToCart({\n items: shoppingCartProducts,\n designCode: vpcCode,\n addDesignCodeAsGroup: !!completeConfiguration && !!vpcCode,\n });\n\n if (!hasErrors(cart)) setAddToCartStateSuccessful();\n } catch (error) {\n const failedShoppingItems =\n (error as IAddToCartError).failedShoppingItems || [];\n if (failedShoppingItems.length > 0) {\n // Handling of unavailable products in NIF markets.\n dispatch(actionSetFailedShoppingItems(failedShoppingItems));\n dispatch(thunkSetSummaryInstanceState(unavailableProductModal, {}));\n dispatch(actionSetAddToCartState(AddToBagStateEnum.default));\n } else {\n dispatch(thunkOnAddToCartError());\n setAddToCartStateFailed();\n }\n }\n };\n\n if (!fromAddUnavailable) {\n dispatch(actionSetAddToCartState(AddToBagStateEnum.loading));\n await dispatch(thunkRetrySaveVpcIfNeeded());\n }\n\n await tryShoppingCartService();\n };\n\n/**\n *\n */\nexport const thunkOnKioskMainCTA =\n () => async (dispatch: any, getState: any) => {\n const { saveVpcFail: saveVpcFailCard } =\n SummaryPageConfirmationCardTypeEnum;\n\n const onFailedVpcCode = () => {\n dispatch(actionSetOverlayState(true));\n dispatch(\n thunkSetSummaryInstanceState(confirmationModal, {\n confirmationCard: saveVpcFailCard,\n })\n );\n };\n\n const onSavedVpcCode = () => {\n dispatch(\n thunkSetSummaryInstanceState(buyInStore, {\n buyInStore: buyInStore.modal,\n })\n );\n dispatch(actionSetOverlayState(true));\n };\n\n if (selectSavedVpcCode(getState())) {\n onSavedVpcCode();\n } else {\n onFailedVpcCode();\n }\n };\n","import { selectTac } from '../tac/tacSelectors';\nimport {\n selectCurrentConfiguration,\n selectSceneImage,\n selectShoppingItems,\n} from '../summary/summarySelectors';\nimport {\n selectSaveVpcProgressStatus,\n selectSavedVpcCode,\n} from './vpcSelectors';\nimport { SummaryPageConfirmationCardTypeEnum } from '@inter-ikea-kompis/component-summary-page';\nimport { ConfirmationSummaryShareDesignStateEnum } from '@inter-ikea-kompis/component-configuration-summary';\nimport { DesignInteractionEnum } from '@insights/insights-data-provider';\nimport { ShareDesignCardStateEnum } from '@inter-ikea-kompis/component-share-design-card';\nimport { actionSetCurrentConfiguration } from '../summary/summaryActions';\nimport { getICF } from '../../util/aactools/ICF';\nimport convert from '../../util/aactools/convert';\nimport {\n actionSetDirtyConfiguration,\n actionSetSavedVpcCode,\n actionSetSaveVpcState,\n actionSetVpcSaveProgressStatus,\n} from './vpcActions';\nimport { actionSetOverlayState } from '../popups/popupsActions';\nimport { VpcSaveProgressStatusEnum } from './vpcTypes';\nimport { getVpcService } from '../../services/ServiceHandler';\nimport {\n confirmationModal,\n shareDesignModal,\n thunkSetSummaryInstanceState,\n thunkResetSavedVpcAndDesignLink,\n thunkSaveVpcAndGenerateDesignLink,\n} from '../summary/summaryThunks';\nimport mandatoryStatisticsReporter from '../../services/statistics/insights/mandatory/mandatoryStatisticsReporter';\n\n/**\n * Reset saved VPC\n */\nexport const thunkResetSavedVpc = () => (dispatch: any, getState: any) => {\n dispatch(actionSetVpcSaveProgressStatus(VpcSaveProgressStatusEnum.default));\n dispatch(actionSetSavedVpcCode(''));\n dispatch(actionSetDirtyConfiguration(true));\n};\n\n/**\n * Save VPC\n */\nexport const thunkSaveVpc = () => async (dispatch: any, getState: any) => {\n const state = getState();\n const shoppingItems = selectShoppingItems(state);\n const tac = selectTac(getState());\n\n dispatch(actionSetVpcSaveProgressStatus(VpcSaveProgressStatusEnum.saving));\n dispatch(actionSetCurrentConfiguration(convert.TACtoPAC(tac)));\n const vpcConfiguration =\n tac && (await dispatch(thunkSaveVpcConfig(shoppingItems)));\n dispatch(actionSetSavedVpcCode(vpcConfiguration?.configurationId || ''));\n\n if (vpcConfiguration) {\n dispatch(actionSetCurrentConfiguration(vpcConfiguration));\n dispatch(actionSetDirtyConfiguration(false));\n dispatch(actionSetVpcSaveProgressStatus(VpcSaveProgressStatusEnum.success));\n\n mandatoryStatisticsReporter.reportDesignInteractionEvent(\n vpcConfiguration.configurationId,\n DesignInteractionEnum.storeConfiguration\n );\n } else {\n dispatch(actionSetVpcSaveProgressStatus(VpcSaveProgressStatusEnum.failure));\n }\n\n return vpcConfiguration;\n};\n\n/**\n * Save vpc config\n * @param shoppingItems\n */\nconst thunkSaveVpcConfig =\n (shoppingItems: any) => async (dispatch: any, getState: any) => {\n const state = getState();\n const tac = selectTac(state);\n const currentConfiguration = selectCurrentConfiguration(state);\n\n if (!tac)\n throw new Error('No tac existed while trying to save Vpc config.');\n\n const icf: any = getICF(tac);\n let configuration;\n try {\n configuration = await getVpcService().saveConfiguration(\n currentConfiguration,\n shoppingItems,\n selectSceneImage(state),\n icf\n );\n } catch (e) {\n configuration = null;\n }\n\n return configuration;\n };\n\n/* If VPC has not yet been successfully saved, make a new\n attempt to save VPC + generate design link. Returns an\n object containing the new VPC saved progress status and\n the new saved VPC code if a retry was needed, otherwise\n just returns an object containing the old values. */\nexport const thunkRetrySaveVpcIfNeeded =\n () => async (dispatch: any, getState: any) => {\n const readCurrentState = () => {\n const state = getState();\n const currentState = {\n saveVpcProgressStatus: selectSaveVpcProgressStatus(state),\n savedVpcCode: selectSavedVpcCode(state),\n };\n\n return currentState;\n };\n\n const currentState = readCurrentState();\n\n if (\n currentState.saveVpcProgressStatus !== VpcSaveProgressStatusEnum.success\n ) {\n dispatch(thunkResetSavedVpcAndDesignLink());\n await dispatch(thunkSaveVpcAndGenerateDesignLink());\n\n const stateAfterRetry = readCurrentState();\n currentState.saveVpcProgressStatus =\n stateAfterRetry.saveVpcProgressStatus;\n currentState.savedVpcCode = stateAfterRetry.savedVpcCode;\n }\n\n return currentState;\n };\n\n/**\n * On user interaction to share design.\n */\nexport const thunkOnShareDesign =\n () => async (dispatch: any, getState: any) => {\n const { saveVpcFail: saveVpcFailCard } =\n SummaryPageConfirmationCardTypeEnum;\n const { default: defaultShareDesignCard } = ShareDesignCardStateEnum;\n\n /**\n * On failed share design\n */\n const onShareDesignFail = () => {\n dispatch(actionSetOverlayState(true));\n dispatch(\n thunkSetSummaryInstanceState(confirmationModal, {\n confirmationCard: saveVpcFailCard,\n })\n );\n };\n\n /**\n * On successful share design\n */\n const onShareDesignSuccess = () => {\n dispatch(\n thunkSetSummaryInstanceState(shareDesignModal, {\n shareDesignCard: defaultShareDesignCard,\n })\n );\n dispatch(actionSetOverlayState(true));\n };\n\n dispatch(\n actionSetSaveVpcState(ConfirmationSummaryShareDesignStateEnum.loading)\n );\n\n // Retry VPC save if needed. If not needed, we'll get the previous values.\n const { saveVpcProgressStatus } = await dispatch(\n thunkRetrySaveVpcIfNeeded()\n );\n\n dispatch(\n actionSetSaveVpcState(ConfirmationSummaryShareDesignStateEnum.default)\n );\n\n switch (saveVpcProgressStatus) {\n case VpcSaveProgressStatusEnum.success:\n onShareDesignSuccess();\n break;\n case VpcSaveProgressStatusEnum.failure:\n onShareDesignFail();\n break;\n default:\n // This should never happen, but treat it as a failure should it somehow happen.\n // Add a statistics event, so that it's reported to us?\n onShareDesignFail();\n }\n };\n","import { useDispatch, useSelector } from 'react-redux';\nimport React, { FunctionComponent } from 'react';\nimport { getShoppingProductsFromTAC } from '../../util/aactools/kompisConvert';\nimport { isFixedRoom } from '../../util/room';\nimport { TOP, RIGHT, LEFT } from '../Popup/alignments';\nimport Measurements from '../ActionButtons/Measurements';\nimport { KompisMiniConfigurationSummary } from '@inter-ikea-kompis/react-components';\nimport { SkapaTheme } from '@inter-ikea-kompis/themes';\nimport { translate } from '../../services/L10n';\nimport IntroPopup from '../Popup/IntroPopup';\nimport styles from './Footer.module.less';\nimport platform from '../../util/platform';\nimport toastMaster from '../../services/toastMaster';\nimport UndoRedo from '../UndoRedo/UndoRedo';\nimport WallResizerIcon from '../ActionButtons/WallResizerIcon';\nimport { selectTacIsEmpty } from '../../state/tac/tacSelectors';\nimport { selectIsWallResizerActive } from '../../state/scene/sceneSelectors';\nimport { selectIsRtl } from '../../state/dexfSettings/dexfSettingsSelectors';\nimport { selectKompisTranslations } from '../../state/translations/translationsSelectors';\nimport { selectDexfSettings } from '../../state/dexfSettings/dexfSettingsSelectors';\nimport { selectHasHadUserInteraction } from '../../state/popups/popupsSelectors';\nimport { actionSetWallResizerInactive } from '../../state/scene';\nimport {\n selectIsMobile,\n selectIsMobilePortrait,\n} from '../../state/userAgent/userAgentSelectors';\nimport { selectPresentTac } from '../../state/tac/tacReducer/tacSelectors';\nimport { nextView } from '../../state/navigation';\nimport { actionEnqueueSheet } from '../../state/sheets/sheetActions';\nimport { WHATS_INCLUDED } from '../Sheets/WhatsIncludedSheet';\nimport { selectSheetQueue } from '../../state/sheets/sheetSelectors';\nimport { thunkHandleErrors } from '../../state/tac/tacThunks';\nimport { selectCurrentView } from '../../state/navigation/navigationSelectors';\nimport { selectSavedVpcCode } from '../../state/vpc/vpcSelectors';\nimport { useKioskIntegration } from '../../hooks/useKioskIntegration';\nimport { thunkSaveVpc } from '../../state/vpc/vpcThunks';\nimport { thunkGetShoppingItems } from '../../state/summary/summaryThunks';\nimport { t } from '../../translations';\n\nexport interface Props {\n onNextViewClick: Function;\n}\n\nconst Footer: FunctionComponent = ({ onNextViewClick }) => {\n const tac = useSelector(selectPresentTac);\n const wallResizerActive = useSelector(selectIsWallResizerActive);\n const hasHadUserInteraction = useSelector(selectHasHadUserInteraction);\n const kompisTranslations = useSelector(selectKompisTranslations);\n const dexfSettings = useSelector(selectDexfSettings);\n const disableNextButton = useSelector(selectTacIsEmpty);\n const isMobile = useSelector(selectIsMobile);\n const isMobilePortrait = useSelector(selectIsMobilePortrait);\n const isRtl = useSelector(selectIsRtl);\n const sheetQueue = useSelector(selectSheetQueue);\n const currentView = useSelector(selectCurrentView);\n const designCode = useSelector(selectSavedVpcCode);\n\n const dispatch = useDispatch();\n\n const { integration, isUpptacka } = useKioskIntegration();\n\n const [triggerElement, setTriggerElement] =\n React.useState(null);\n const [nextViewHintVisible, setNextViewHintVisible] = React.useState(false);\n const [hasShownNextViewHint, setHasShownNextViewHint] = React.useState(false);\n const [ecoFeeSheetVisible, setEcoFeeSheetVisible] =\n React.useState(undefined);\n\n const setWallResizerInactive = () =>\n dispatch(actionSetWallResizerInactive({ nonInteraction: true }));\n const openWhatsIncluded = (event: {\n detail: { triggerElement: React.SetStateAction };\n }) => {\n dispatch(actionEnqueueSheet({ sheetType: WHATS_INCLUDED }));\n setTriggerElement(event?.detail.triggerElement);\n };\n const renderNextView = () => dispatch(nextView());\n\n const shoppingProducts = tac ? getShoppingProductsFromTAC(tac) : [];\n\n /**\n * Handles intro popup closing\n */\n const onPopupClose = () => {\n setHasShownNextViewHint(true);\n setNextViewHintVisible(false);\n };\n\n /**\n * Handles default next click\n */\n const handleDefaultNextClick = () => {\n renderNextView();\n toastMaster.dismissCurrentToast();\n };\n\n /**\n * Handles upptäcka summary click\n */\n const handleUpptackaSummaryClick = () => {\n dispatch(thunkGetShoppingItems());\n dispatch(thunkSaveVpc());\n };\n\n /**\n * Handles click on next button\n */\n const onNext = async () => {\n const errorsArePresent = !!tac?.errors?.length;\n\n if (errorsArePresent) dispatch(thunkHandleErrors());\n else {\n onNextViewClick();\n\n isUpptacka && currentView === 'SCENE'\n ? handleUpptackaSummaryClick()\n : handleDefaultNextClick();\n }\n };\n\n React.useEffect(() => {\n if (designCode && isUpptacka) {\n integration?.summaryClicked(designCode);\n }\n }, [designCode]);\n\n React.useEffect(() => {\n if (\n !isKiosk &&\n !hasShownNextViewHint &&\n hasHadUserInteraction &&\n tac.items?.length > 0 &&\n tac.errors?.length === 0\n ) {\n setTimeout(() => {\n setHasShownNextViewHint(true);\n setNextViewHintVisible(true);\n }, 5000);\n }\n });\n\n React.useEffect(() => {\n triggerElement?.focus();\n }, [sheetQueue]);\n\n /**\n * Handles click on footer\n */\n const onClick = () => {\n wallResizerActive && setWallResizerInactive();\n };\n\n /**\n * On modal open\n * @param visibleModal\n */\n const onModalOpen = ({ detail: { visibleModal } }: any) => {\n setEcoFeeSheetVisible(visibleModal);\n };\n\n /**\n * On modal close\n * @param visibleModal\n */\n const onModalClose = ({ detail: { visibleModal } }: any) => {\n setEcoFeeSheetVisible(null);\n };\n const isKiosk = platform.isKiosk;\n\n return (\n
\n
\n {!isMobilePortrait && (\n
\n {isFixedRoom() && }\n \n
\n )}\n {!isMobile && }\n
\n\n
\n \n \n \n
\n
\n );\n};\n\nexport default Footer;\n","import { Products } from './productsTypes';\nimport { Action } from '../../generalTypes';\n\nexport const SET_PRODUCTS: string = 'SET_PRODUCTS';\n\nexport const actionSetProducts = (products: Products): Action => ({\n type: SET_PRODUCTS,\n payload: products,\n});\n","import { getProduct } from './index';\nimport {\n getProductMeasurementSettings,\n getSmallestAndLargest,\n} from '../../state/products/productsHelpers.ts';\nimport { fractionLigatures } from '../../util/measures';\nimport { DIMENSIONS, MS, TS } from '../../constants';\nimport { translate } from '../L10n';\nimport { getMeasurementBySoM } from './productHandler';\nimport { selectUseMetric } from '../../state/dexfSettings/dexfSettingsSelectors';\nimport store from '../../state';\nimport { t } from '../../translations';\n\nexport const productMeasurementFormatter = (\n id,\n sectionsSizes,\n measurements,\n data,\n useMetric\n) => {\n const product = getProduct(id);\n\n if (!product) return '';\n\n const { unit, measurementPrefix, format, measure } =\n getProductMeasurementSettings(product.filter.type);\n\n /**\n * Checks if the given item fits in all it's parents variants\n * @param dimension {string}\n * @returns {boolean}\n */\n const fitsInAll = dimension =>\n sectionsSizes[dimension].reduce(\n (acc, curr) => (measurements[dimension].includes(curr) ? acc + 1 : acc),\n 0\n ) === sectionsSizes[dimension].length;\n\n /**\n * Returns a string with min and max values\n *\n * @param min {number}\n * @param max {number}\n * @returns {string}\n */\n const formatWithSeveralValues = ({ min, max }) =>\n `${getMeasurementBySoM(useMetric, min, unit)} - ${getMeasurementBySoM(\n useMetric,\n max,\n unit\n )}`;\n\n /**\n * Formats measurements according to market\n * @param dimension {string}\n * @returns {string}\n */\n const formatMeasurement = dimension => {\n if (!data[dimension].length) return '';\n\n return data[dimension].length > 1\n ? formatWithSeveralValues(getSmallestAndLargest(data[dimension]))\n : getMeasurementBySoM(useMetric, data[dimension][0], unit);\n };\n\n /**\n * Returns measurement prefix in form of a capitalized letter for given dimension\n * according to settings\n *\n * @param dimension\n * @returns {string|string}\n */\n const displayMeasurementPrefix = dimension =>\n measurementPrefix\n ? `${translate(t[`SHORT_LABEL_${dimension.toUpperCase()}`])} `\n : '';\n\n /**\n * Get measurements string\n * @param dimension {string}\n * @returns {string}\n */\n const getMeasurement = dimension =>\n `${displayMeasurementPrefix(dimension)}${formatMeasurement(dimension)}`;\n\n /**\n * Get start position of next dimension\n * @param contains {array}\n * @param dimension {string}\n * @param index {number}\n * @returns {number}\n */\n const getNextDimensionStartPosition = (contains, index, dimension) =>\n format.indexOf(contains[index + 1].dimension) -\n (format.indexOf(dimension) + dimension.length);\n\n /**\n * Derives the separation part between two dimensions of the string\n *\n * @param index {number}\n * @param contains {array}\n * @param dimension {string}\n * @returns {string}\n */\n const formatSeparator = (index, contains, dimension) =>\n format.substr(\n format.indexOf(dimension) + dimension.length,\n index === contains.length - 1\n ? format.length\n : getNextDimensionStartPosition(contains, index, dimension)\n );\n\n /**\n * Sorts array, small to large\n *\n * @param a {object}\n * @param b {object}\n * @returns {number}\n */\n const sortSmallToLarge = (a, b) => a.startsAt - b.startsAt;\n\n /**\n * Add separator key to object\n *\n * @param obj {object}\n * @param index {number}\n * @param arr {array}\n * @returns {{separator: *}}\n */\n const addSeparatorKey = (obj, index, arr) => ({\n ...obj,\n separator: formatSeparator(index, arr, obj.dimension),\n });\n\n /**\n * Extracts dimension start and finish index from string\n *\n * @param acc {object}\n * @param dimension {string}\n * @param index {number}\n * @returns {*[]}\n */\n const getDimensionKeywordPositions = (acc, dimension, index) => [\n ...acc,\n {\n dimension,\n startsAt: format.indexOf(dimension),\n endsAt: dimension.length,\n },\n ];\n\n /**\n *\n * Figures out whether a dimension should be removed from final measurement string\n *\n * @param dimension {string}\n * @returns {*|boolean}\n */\n const removeUnwantedDimensions = ({ dimension }) =>\n format.includes(dimension) && (measure ? !fitsInAll(dimension) : true);\n\n /**\n * If given separator is the last in the array, remove any trailing comma,\n * else return string\n *\n * @param array\n * @param index\n * @param separator\n * @returns {*}\n */\n const shouldRemoveComma = (array, index, separator) =>\n array.length === index + 1 ? separator.replace(',', '') : separator;\n\n /**\n * Compiles the final string\n *\n * @param acc\n * @param dimension\n * @param separator\n * @param rest\n * @param index {number}\n * @param array {array}\n * @returns {string}\n */\n const buildMeasurementString = (\n acc,\n { dimension, separator, ...rest },\n index,\n array\n ) =>\n `${acc}${getMeasurement(dimension)}${shouldRemoveComma(\n array,\n index,\n separator\n )}`;\n\n const useMetricMeasures = selectUseMetric(store.getState());\n\n return Object.values(DIMENSIONS)\n .reduce(getDimensionKeywordPositions, [])\n .sort(sortSmallToLarge)\n .map(addSeparatorKey)\n .filter(removeUnwantedDimensions)\n .reduce(buildMeasurementString, '')\n .replaceAll(\n MS,\n useMetricMeasures\n ? translate(t.MEASURE_VALUE_CM)\n : translate(t.MEASURE_VALUE_IN)\n )\n .replaceAll(TS, translate(t[TS]))\n .replace(/\\d+\\/\\d+/g, fraction => fractionLigatures[fraction]);\n};\n","import { DIMENSIONS, UNIT_CONVERSIONS, UNITS } from '../../constants';\nimport constants from '../../settings/constants';\nimport {\n getProductMeasurementRemapKeys,\n getAllDimensionsKeys,\n getGlobalType,\n} from '../../state/products/productsHelpers.ts';\nimport { findArticle } from './articles';\nimport productService from '.';\n\nexport const gatherArticleMeasurements = productIds => {\n const gatherFromId = productService.isOnlyAvailableInMultipack(productIds[0])\n ? constants.BULK_ARTICLES[productIds[0]].id\n : productIds[0];\n const article = findArticle(gatherFromId);\n const numbersRegex = /\\d+/g;\n const lettersRegex = /[a-zA-Z]+/g;\n\n /**\n * Derives unit from string. If no unit is found, defaults to CM\n *\n * @param string\n * @returns {*|string}\n */\n const deriveUnit = string => {\n const unit = string.match(lettersRegex);\n return unit ? unit[0] : UNITS.cm;\n };\n\n /**\n * Regexes out numbers and letters from measurements string\n *\n * @param string {string}\n * @returns {number}\n */\n const deriveMeasurementsFromKompisData = string =>\n parseInt(string.match(numbersRegex)[0], 10) *\n UNIT_CONVERSIONS[deriveUnit(string)];\n\n /**\n * Returns an object with articles dimensions as key\n * @param art\n * @returns {T | *}\n */\n const getDimensionsAsObject = art => {\n return art.measure.reduce((acc, { typeCode, ...rest }) => {\n const globalType = getGlobalType(typeCode);\n return {\n ...acc,\n ...(globalType && {\n [globalType.toLowerCase()]: rest,\n }),\n };\n }, {});\n };\n\n /**\n * Returns an object with articles measurements\n * @param art\n * @returns {{}|boolean}\n */\n const deriveArticleMeasurements = art => {\n if (!art?.hasOwnProperty('measure')) return false;\n\n const dimensionsAsObject = getDimensionsAsObject(art);\n\n return [...getAllDimensionsKeys()].reduce((acc, dimension) => {\n if (!dimensionsAsObject.hasOwnProperty(dimension)) return acc;\n\n const value = deriveMeasurementsFromKompisData(\n dimensionsAsObject[dimension].textMetric\n );\n\n return value ? { ...acc, [dimension]: value } : acc;\n }, {});\n };\n\n /**\n * Returns all products as an object with productID as key\n *\n * @returns {object}\n */\n const getArticlesDimensionsByIds = () =>\n productIds.reduce((acc, id) => {\n const data = deriveArticleMeasurements(findArticle(id));\n return data ? [...acc, data] : acc;\n }, []);\n\n /**\n * Remodels the data in the case where IKEA's data's keys don't match out keys.\n * Eventual mapping is found in the settings-file for said range\n *\n * @param articles\n * @returns {*}\n */\n const handleDataRemodel = articles => {\n const data = getProductMeasurementRemapKeys(productIds);\n\n return data || !articles\n ? articles.map(article =>\n Object.keys(data).reduce(\n (acc, dimension) => ({\n ...acc,\n [data[dimension]]: article[dimension],\n }),\n {}\n )\n )\n : articles;\n };\n\n /**\n * returns keys for deriving min and max values for dimension from data\n *\n * @param dimension {string}\n * @returns {string[]}\n */\n const getMinMaxKeys = dimension => [`Min. ${dimension}`, `Max. ${dimension}`];\n\n /**\n * Find specific measurement from article data\n *\n * @param measurements {array}\n * @param measurement {string}\n * @returns {string}\n */\n const findMeasurements = (measurements, measurement) => {\n return measurements.find(\n ({ typeCode }) => getGlobalType(typeCode) === measurement\n )?.textMetric;\n };\n\n /**\n * Finds min-max values for measurement\n *\n * @param measurements {array}\n * @returns {function(*, *=): *|*[]}\n */\n const deriveMinMaxMeasurement = measurements => (acc, measurement) => {\n const foundMeasurements = findMeasurements(measurements, measurement);\n\n return !foundMeasurements\n ? acc\n : [...acc, deriveMeasurementsFromKompisData(foundMeasurements)];\n };\n\n /**\n * Gathers min-max values for dimension on an article\n *\n * @returns {*}\n */\n const getMinMaxValues = () =>\n !article.measure\n ? {}\n : Object.keys(DIMENSIONS).reduce((acc, dimension) => {\n const keys = getMinMaxKeys(dimension);\n const foundMinMaxMeasurements = keys.reduce(\n deriveMinMaxMeasurement(article.measure),\n []\n );\n\n return foundMinMaxMeasurements.length\n ? { ...acc, [dimension]: foundMinMaxMeasurements }\n : acc;\n }, {});\n\n const minMaxValues = getMinMaxValues();\n\n return {\n article,\n minMaxValues,\n articleArray: handleDataRemodel(getArticlesDimensionsByIds()),\n };\n};\n","import { selectUseMetric } from '../../state/dexfSettings/dexfSettingsSelectors';\nimport productService, { deriveMeasurements, getSections } from './index';\nimport { getMatchingConnections } from './models';\nimport { productMeasurementFormatter } from './productMeasurementsFormatter';\nimport {\n getProductsAsObject,\n getSmallestAndLargest,\n} from '../../state/products/productsHelpers.ts';\nimport { DIMENSIONS } from '../../constants';\nimport { fractionLigatures } from '../../util/measures';\nimport { gatherArticleMeasurements } from './articleMeasurementsGatherer';\n\nexport const gatherProductMeasurement = (productIds, state) => {\n const allProducts = getProductsAsObject();\n const product = productIds[0];\n const productVariants = productIds\n .map(productId => allProducts[productId])\n .filter(Boolean);\n const useMetric = selectUseMetric(state);\n const measurementsVariants = deriveMeasurements(productVariants);\n const { articleArray, article, minMaxValues } =\n gatherArticleMeasurements(productIds);\n const getMeasurementType = useMetric ? 'textMetric' : 'textImperial';\n const isExtendable = productService.isExtendable(product);\n const ikeaMeasurements = {\n ...deriveMeasurements(articleArray),\n ...minMaxValues,\n };\n\n /**\n * If data from IKEA exists for a given dimension, this will be favored.\n * Else we use our own data\n *\n * @param dimension {string}\n * @returns {*}\n */\n const doMerge = dimension => {\n return ikeaMeasurements?.[dimension].length\n ? ikeaMeasurements[dimension]\n : measurementsVariants[dimension];\n };\n\n /**\n * Returns true if current product is a section and current dimension is width\n *\n * @param dimension\n * @returns {*|boolean}\n */\n const isTypeSectionAndDimensionWidth = dimension =>\n productService.isSection(product) && dimension === DIMENSIONS.width;\n\n /**\n * In case the current product is a section, it should be handled a differently\n *\n * @param dimension {string}\n * @returns {*}\n */\n const getCorrectMeasurements = dimension =>\n isTypeSectionAndDimensionWidth(dimension)\n ? measurementsVariants[DIMENSIONS.width]\n : doMerge(dimension);\n\n /**\n * Merges the two sets of data\n *\n * @returns {{}}\n */\n const mergeMeasurements = () =>\n Object.keys(DIMENSIONS).reduce(\n (acc, dimension) => ({\n ...acc,\n [dimension]: getCorrectMeasurements(dimension),\n }),\n {}\n );\n\n /**\n * Generates an array of dummy-tacs with each version of a section in it\n *\n * @returns {array}\n */\n const generateTacs = () =>\n productService.getSections().map(section => ({\n items: [\n {\n x: 0,\n y: 0,\n z: productService.getInitialZPos(section.id),\n ...section,\n },\n ],\n }));\n\n /**\n * Returns an array with which products fits in which sections\n */\n const calculateMatchingConnections = () =>\n productVariants\n .map(product =>\n generateTacs()\n .map(tac => getMatchingConnections(product, tac))\n .flat()\n )\n .flat();\n\n /**\n * Returns an object with info regarding which sections a product fits in\n */\n const getProductFit = () =>\n calculateMatchingConnections().filter(({ height }) => {\n const { max } = getSmallestAndLargest(measurementsVariants.height);\n return height >= max;\n });\n\n /**\n * Returns a boolean to tell if settings should be overridden\n *\n * @returns {boolean}\n */\n const forceDisplay = () => isExtendable;\n\n /**\n * Returns measurements string, either as is if the\n * product is extendable\n * or builds one\n *\n * @param fitsInProducts {array}\n * @param data\n * @returns {*}\n */\n const getMeasurementsString = (fitsInProducts, data) => {\n return forceDisplay()\n ? article.measureReference[getMeasurementType].replace(\n /\\d+\\/\\d+/g,\n fraction => fractionLigatures[fraction]\n )\n : productMeasurementFormatter(\n product,\n deriveMeasurements(getSections()),\n deriveMeasurements(fitsInProducts),\n data,\n useMetric\n );\n };\n\n /**\n * Creates an object containing a product variants ID's, it's different dimension variants\n * which sections it will fit withing as well as which dimensions it will fit within\n *\n * @param fitsInProducts {array}\n * @returns {{fittingMeasurements: {depth: *[], width: *[], height: *[]}, measurementsVariants: *, products: *, fitsInProducts: *}}\n */\n const generateProductsMeasurementObject = fitsInProducts => ({\n ikeaMeasurements,\n measurementsVariants,\n fitsInProducts,\n products: productIds,\n fittingMeasurements: deriveMeasurements(fitsInProducts),\n forceDisplay: forceDisplay(),\n measurementString: getMeasurementsString(\n fitsInProducts,\n mergeMeasurements()\n ),\n });\n\n /**\n * Gets products measurements, and if measure is true, test products fit in all possible sections\n *\n * @param measure {boolean}\n * @param ids {array}\n * @returns {{fittingMeasurements: {depth: *[], width: *[], height: *[]}, measurementsVariants: *, products: *, fitsInProducts: *}}\n */\n return generateProductsMeasurementObject(getProductFit());\n};\n","import { getProductsAsObject, productSettingsExists } from './productsHelpers';\nimport { gatherProductMeasurement } from '../../services/products/productMeasurementsGatherer';\nimport { GetState, Itemids, ThunkDispatch } from '../../generalTypes';\n\nexport const formatProductsMeasurements =\n (products: Itemids) => (dispatch: ThunkDispatch, getState: GetState) => {\n const prods = getProductsAsObject();\n\n return products.reduce((acc: any, curr: Itemids | any) => {\n if (!curr.length) return acc;\n\n const product = prods[curr[0]];\n if (!productSettingsExists(product)) return acc;\n\n return [...acc, gatherProductMeasurement(curr, getState())];\n }, []);\n };\n","import { SET_RAW_DATA } from '../actionConstants';\nimport { ProductMenuItem } from '../productMenu/productMenuTypes';\n\n/**\n * Set raw products state slice\n * @param rawProducts\n */\nexport const actionSetRawProducts = (rawProducts: ProductMenuItem[]) => ({\n type: SET_RAW_DATA,\n payload: {\n key: 'products',\n data: rawProducts,\n },\n});\n","import { applicationSettings } from '../../settings/application';\nimport { setFilters } from '../productMenu/productMenuActions.ts';\nimport swiperService from '../../services/swiper';\nimport { actionSetProducts } from '../products/productsActions.ts';\nimport { formatProductsMeasurements } from '../products/productsThunks.ts';\nimport productService from '../../services/products/index';\nimport { actionSetRawProducts } from '../rawData/rawDataActions';\nimport { actionSetSwiperSubFilter } from '../productMenu/productMenuActions';\nimport { selectDefaultSubFilterIndex } from '../productMenu/productMenuSelectors';\nimport { getAllPartsAsIdsFromItems } from '../products/productsHelpers';\n\nconst fileUrl = `./configs/${applicationSettings.applicationName.toLowerCase()}`;\n\nfunction request(url, fallback = {}) {\n return fetch(url, { credentials: 'same-origin' })\n .then(res => res.json())\n .catch(err => {\n console.error(err);\n return fallback;\n });\n}\n\nexport default function loadProductMenu() {\n const extractFilePart = (files, part) => files.find(item => part in item);\n\n /**\n * Generate products object,\n * @param ids\n * @param product\n * @returns {*&{relations}}\n */\n const generateProductObject = (ids, product) => ({\n ...product,\n relations: ids.filter(id => product.id !== id),\n });\n\n /**\n * Add relations array to products object\n * @param ids\n */\n const createProductsArray = ids =>\n ids.reduce((acc, itemId) => {\n const product = productService.getProduct(itemId);\n return product ? [...acc, generateProductObject(ids, product)] : acc;\n }, []);\n\n /**\n * Get items by id\n * @param itemsIds\n */\n const getItemsByIds = itemsIds =>\n itemsIds.reduce((acc, curr) => [...acc, ...createProductsArray(curr)], []);\n\n /**\n * Convert items array to object with product ID as key\n * @returns {function(*, *): *}\n */\n const convertToIdObject = (acc, curr) => ({\n ...acc,\n [curr.id]: curr,\n });\n\n /**\n * Validates a filter by making sure it has at least on valid product\n * @param products\n */\n const validateFilter = ({ products }) =>\n products.flat().filter(product => productService.getProduct(product)?.valid)\n .length;\n\n /**\n * Removes invalid products\n * @param products\n * @returns {*}\n */\n const removeInvalidProductIds = products =>\n products.reduce((validProductsIds, productIds) => {\n const validProducts = productIds.filter(\n id => !!productService.getProduct(id)\n );\n\n return validProducts.length\n ? [...validProductsIds, validProducts]\n : validProductsIds;\n }, []);\n\n const subFilterHasItems = (filter, rawItems) => filterOption => {\n const subFilterKey = filter.subFilter.key;\n\n const items = filter.products.reduce((acc, [id]) => {\n if (!id) return acc;\n\n const product = rawItems[id];\n if (id && productService.getProduct(id)) return [...acc, product];\n\n return acc;\n }, []);\n\n return !!items.filter(product => {\n return product[subFilterKey] === filterOption.value;\n }).length;\n };\n\n /**\n * Adds a property (hasItems) to subfilter options to indicate\n * whether they have products or are empty.\n * @param {*} filter\n * @param {*} items\n * @returns {*}\n */\n const flagSubfilterKeys = (filter, items) => {\n if (!filter.hasOwnProperty('subFilter')) return filter;\n\n const subFilterOptionHasItems = subFilterHasItems(filter, items);\n\n return {\n ...filter.subFilter,\n options: filter.subFilter.options.map(option => ({\n ...option,\n hasItems: subFilterOptionHasItems(option),\n })),\n };\n };\n\n /**\n * Function for manipulating individual filters. eg. remove/flag unwanted or disabled products\n * @param filter\n * @param items\n * @returns {*&{products}}\n */\n const cureFilter = (filter, items) => {\n return {\n ...filter,\n products: removeInvalidProductIds(filter.products),\n subFilter: flagSubfilterKeys(filter, items),\n };\n };\n\n /**\n * Returns all valid filters\n * @param files\n */\n const getValidFilters = files =>\n extractFilePart(files, 'filters').filters.filter(validateFilter);\n\n /**\n * Returns all items as object\n */\n const getItemsObject = itemsIds =>\n getItemsByIds(itemsIds).reduce(convertToIdObject, {});\n\n /**\n * Returns all item id's in a flat array\n * @param filters\n * @returns {*}\n */\n const getItemsIds = filters =>\n filters.reduce((acc, { products }) => [...acc, products], []).flat();\n\n /**\n * Convert filters to object with name as key\n * @param items\n * @returns {function(*, *=): *}\n */\n const convertFiltersToObject = items => (acc, curr) => ({\n ...acc,\n [curr.name]: cureFilter(curr, items),\n });\n\n /**\n * Sets thumbnails\n * @param files\n */\n const setThumbnails = files => {\n const thumbnails = extractFilePart(files, 'thumbnails').thumbnails;\n swiperService.init(thumbnails);\n };\n\n return (dispatch, getState) => {\n return new Promise((resolve, reject) => {\n const files = [\n `${fileUrl}/productMenu.json`,\n `${fileUrl}/thumbnails.json`,\n ];\n\n Promise.all(files.map(request)).then(files => {\n setThumbnails(files);\n\n const filters = getValidFilters(files);\n const itemsIds = getItemsIds(filters);\n const items = getItemsObject(itemsIds);\n const partIds = getAllPartsAsIdsFromItems(items);\n const parts = getItemsObject(partIds);\n\n const curedFilters = filters.reduce(convertFiltersToObject(items), {});\n const products = dispatch(formatProductsMeasurements(itemsIds));\n\n dispatch(actionSetProducts(products));\n dispatch(actionSetRawProducts({ ...items, ...parts }));\n dispatch(setFilters(curedFilters, { nonInteraction: true }));\n dispatch(\n actionSetSwiperSubFilter(selectDefaultSubFilterIndex(getState()))\n );\n resolve();\n });\n });\n };\n}\n","import classNames from 'classnames';\nimport { useDispatch, useSelector } from 'react-redux';\nimport {\n ButtonTypeEnum,\n ButtonAlignEnum,\n} from '@inter-ikea-kompis/component-button';\nimport Views from '../../settings/views/views';\nimport React from 'react';\nimport styles from './TopBar.module.less';\nimport platform from '../../util/platform';\nimport { IconButton } from '../IconButton';\nimport LanguageSelector from './LanguageSelector';\nimport UndoRedo from '../UndoRedo/UndoRedo';\nimport { KompisTooltip } from '@inter-ikea-kompis/react-components';\nimport { translate } from '../../services/L10n';\nimport { t } from '../../translations';\nimport { SkapaTheme } from '@inter-ikea-kompis/themes/lib';\nimport {\n actionSetWallResizerInactive,\n actionSetProppingVisibility,\n} from '../../state/scene';\nimport {\n selectIsMobile,\n selectIsMobilePortrait,\n} from '../../state/userAgent/userAgentSelectors';\nimport { selectIsRtl } from '../../state/dexfSettings/dexfSettingsSelectors';\nimport { selectIsWallResizerActive } from '../../state/scene/sceneSelectors';\nimport { selectCurrentView } from '../../state/navigation/navigationSelectors';\nimport { previousView } from '../../state/navigation';\nimport { thunkSetView } from '../../state/navigation/navigationThunks';\nimport views from '../../settings/views/views';\nimport { IconButtonSizeEnum } from '@inter-ikea-kompis/component-icon-button';\nimport loadProductMenu from '../../state/init/loadProductMenu';\nimport { useKioskIntegration } from '../../hooks/useKioskIntegration';\n\nexport interface Props {\n className?: any;\n}\n\nexport const TopBar: React.FunctionComponent = ({ className }) => {\n const dispatch = useDispatch();\n\n const setWallResizerInactive = () =>\n dispatch(actionSetWallResizerInactive({ nonInteraction: true }));\n const _setProppingVisibility = () =>\n dispatch(actionSetProppingVisibility(true));\n const _previousView = () => dispatch(previousView());\n const goToStartView = () => dispatch(thunkSetView(views.START.name));\n const reloadProductMenu = () => dispatch(loadProductMenu());\n\n const isMobile = useSelector(selectIsMobile);\n const wallResizerActive = useSelector(selectIsWallResizerActive);\n const isMobilePortrait = useSelector(selectIsMobilePortrait);\n const isRtl = useSelector(selectIsRtl);\n const currentView = useSelector(selectCurrentView);\n const { integration, isUpptacka } = useKioskIntegration();\n\n const isKiosk = platform.isKiosk;\n\n const handleBackButtonClick = () => {\n integration?.backClicked();\n _previousView();\n _setProppingVisibility();\n };\n\n const startOver = () => {\n integration?.backClicked();\n goToStartView();\n reloadProductMenu();\n };\n\n const renderRestartButton = () => (\n \n \n \n );\n\n const onClick = () => {\n !isMobilePortrait && wallResizerActive && setWallResizerInactive();\n };\n\n const renderSceneViewTopBar = () => (\n
\n {renderRestartButton()}\n {isMobile &&
}\n {isMobile && }\n {isKiosk && !isUpptacka && }\n
\n );\n\n if (currentView === Views.SCENE.name) return renderSceneViewTopBar();\n\n return (\n
\n \n \n \n {isKiosk && }\n
\n );\n};\n\nexport default TopBar;\n","import React from 'react';\nimport { KompisIcon } from '@inter-ikea-kompis/react-components';\nimport { getKompisIconData, IconProps } from './utils/kompis/getKompisIconData';\n\nexport const Icon: React.FunctionComponent = ({\n iconName,\n ...props\n}) => ;\n","import React from 'react';\nimport classNames from 'classnames';\nimport { useDispatch, useSelector } from 'react-redux';\nimport styles from './ProductMenuFilter.module.less';\nimport { LEFT, BOTTOM } from '../Popup/alignments';\nimport { translate } from '../../services/L10n';\nimport Tooltip from '../Popup/Tooltip';\nimport { Icon } from '../Icon';\nimport {\n selectIsWallResizerActive,\n selectIsMeasurementsActive,\n} from '../../state/scene/sceneSelectors';\nimport { selectIsSelectedFilter } from '../../state/productMenu/productMenuSelectors';\nimport {\n selectIsMobilePortrait,\n selectIsTabletPortrait,\n} from '../../state/userAgent/userAgentSelectors';\nimport { thunkSetFilter } from '../../state/productMenu/productMenuThunks';\nimport { actionSetFilterIntroPopupBeVisible } from '../../state/popups/popupsActions';\nimport { Filter } from '../../state/productMenu/productMenuTypes';\nimport { actionSetWallResizerInactive } from '../../state/scene';\nimport { t } from '../../translations';\n\nexport interface Props {\n filter: Filter;\n}\n\nconst ProductMenuFilter: React.FunctionComponent = ({ filter }) => {\n const wallResizerActive = useSelector(selectIsWallResizerActive);\n const measurementsActive = useSelector(selectIsMeasurementsActive);\n const isMobilePortrait = useSelector(selectIsMobilePortrait);\n const isTabletPortrait = useSelector(selectIsTabletPortrait);\n const isSelected = useSelector(selectIsSelectedFilter(filter));\n\n const dispatch = useDispatch();\n\n const setFilter = (filter: string) => dispatch(thunkSetFilter(filter));\n const setHasShownFilterIntroPopup = () =>\n dispatch(actionSetFilterIntroPopupBeVisible(false));\n\n /**\n * Close wallresizer or measurements if active\n */\n const closeActionButtons = () => {\n wallResizerActive &&\n dispatch(actionSetWallResizerInactive({ nonInteraction: true }));\n };\n\n /**\n * Set filter\n * @param filter\n * @returns {function(): *}\n * @private\n */\n const _setFilter =\n ({ name }: Filter) =>\n () => {\n setFilter(name);\n setHasShownFilterIntroPopup();\n closeActionButtons();\n };\n\n /**\n * Checks if filter is selected one\n * @returns {boolean}\n * @private\n */\n const _isSelected = () =>\n isSelected &&\n (!isMobilePortrait || (!measurementsActive && !wallResizerActive));\n\n /**\n * Return item classes\n * @returns {string}\n */\n const getItemClassNames = () =>\n classNames(styles.item, {\n [styles.selected]: _isSelected(),\n });\n\n return (\n \n \n \n );\n};\n\nexport default ProductMenuFilter;\n","import React from 'react';\nimport classNames from 'classnames';\nimport PropTypes from 'prop-types';\nimport { Icon } from '../Icon';\nimport styles from './ProductSwiperIndicator.module.less';\n\nconst ProductSwiperIndicator = props => {\n const { indicator } = props;\n const offset = {};\n offset[styles.offset] = indicator.offset;\n return (\n
\n
\n {}\n
\n
\n );\n};\n\nProductSwiperIndicator.propTypes = {\n indicator: PropTypes.object.isRequired,\n};\n\nexport default ProductSwiperIndicator;\n","import React from 'react';\nimport PropTypes from 'prop-types';\nimport classNames from 'classnames';\nimport { connect } from 'react-redux';\nimport { contain } from 'intrinsic-scale';\nimport { unique } from '../../util/array';\nimport articles from '../../services/products/articles';\nimport products from '../../services/products';\nimport productService from '../../services/products';\nimport swiperService from '../../services/swiper';\nimport emitter from '../../emitter';\nimport constants from '../../settings/constants';\nimport { DROP_ITEM, GRAB_ITEM } from '../../settings/events';\nimport * as supportedEvents from '../../util/supportedEvents';\nimport * as alignments from '../Popup/alignments';\nimport Popup from '../Popup';\nimport popupStyles from '../Popup/Popup.module.less';\nimport BrorTransition from '../Transition/Transition';\nimport styles from './ProductSwiperItem.module.less';\nimport ProductSwiperIndicator from './ProductSwiperIndicator';\nimport platform from '../../util/platform';\nimport {\n selectCurrentColorFilter,\n selectCurrentFilterName,\n selectMeasurementString,\n selectProductMenuFilter,\n selectProductMenuLayout,\n selectShouldDisplayMeasurements,\n} from '../../state/productMenu/productMenuSelectors.ts';\nimport { translate } from '../../services/L10n';\nimport { withLigatures } from '../../util/measures';\nimport {\n selectIsRtl,\n selectUseMetric,\n} from '../../state/dexfSettings/dexfSettingsSelectors';\nimport { Icon } from '../Icon';\nimport localStatisticsReporter from '../../services/statistics/insights/custom/local/localStatisticsReporter';\nimport { FILTERS } from '../../constants';\nimport { applicationSettings } from '../../settings/application';\nimport { t } from '../../translations';\n\nconst actionThreshold = 10;\nlet clicked = false;\n\nclass ProductSwiperItem extends React.Component {\n static propTypes = {\n forwardEvent: PropTypes.func.isRequired,\n product: PropTypes.object.isRequired,\n mode: PropTypes.oneOf(Object.values(constants.DRAG_MODE)).isRequired,\n isVertical: PropTypes.bool.isRequired,\n filter: PropTypes.object.isRequired,\n userAgent: PropTypes.object.isRequired,\n disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.string])\n .isRequired,\n currentColorFilter: PropTypes.object,\n renderMultiline: PropTypes.bool.isRequired,\n displayMeasurement: PropTypes.bool.isRequired,\n swiperLayout: PropTypes.string,\n measurementString: PropTypes.string,\n isRtl: PropTypes.bool.isRequired,\n useMetricMeasures: PropTypes.bool.isRequired,\n };\n\n root = React.createRef();\n imageDiv = React.createRef();\n preventSwipeTriggerClick = false;\n\n action = {\n startPos: {},\n mode: false,\n };\n\n state = {\n errorVisible: false,\n };\n\n constructor(props) {\n super(props);\n\n const { product, filter, currentColorFilter } = props;\n\n this.onPointerDown = this.onPointerDown.bind(this);\n this.onPointerMove = this.onPointerMove.bind(this);\n this.onClick = this.onClick.bind(this);\n\n this.shouldDisplayProductType =\n swiperService.shouldDisplayProductType(product);\n\n const idOfArticleToGet = productService.isOnlyAvailableInMultipack(\n product.id\n )\n ? constants.BULK_ARTICLES[product.id].id\n : product.id;\n this.article = articles.getArticleContent(idOfArticleToGet);\n\n const includedArticles = products.getIncludedArticles(product.id);\n while (\n includedArticles.length &&\n (!this.article ||\n (!this.article.typeName && !this.article.measureReference))\n ) {\n this.article = articles.getArticleContent(includedArticles.shift());\n }\n this.typeText = swiperService.getTypeText(this.article, product);\n\n this.imageObj = productService.getArticleImage(product.id);\n this.shouldDisplayColor = swiperService.displayColor(\n product,\n filter,\n currentColorFilter\n );\n\n this.layouts = {\n pairLayout: () => [this.renderPairTitle(), this.renderImage()],\n default: () => [this.renderImage(), this.renderDetails()],\n rowLayout: () => [this.renderImage(), this.renderDetails()],\n combinedLayout: () => [this.renderImage(), this.renderCombinedLayout()],\n centeredLayout: () => [this.renderImage(), this.renderCenteredText()],\n };\n\n this.indicator =\n filter.indicators &&\n filter.indicators.find(indicator => indicator.id === product.id);\n }\n\n componentDidMount() {\n // a native event is used because calling preventDefault to prevent scroll in iOS\n // does not work when using react events (even with calling e.nativeEvent.preventDefault())\n if (supportedEvents.TOUCH_SUPPORT) {\n this.root.current.addEventListener('touchmove', this.onPointerMove);\n }\n\n emitter.on(DROP_ITEM, this.resetAction);\n }\n\n componentDidUpdate() {\n if (this.state.errorVisible && !this.props.disabled) {\n this.setState({\n errorVisible: false,\n });\n }\n }\n\n resetAction = () => {\n this.preventSwipeTriggerClick = setTimeout(() => {\n this.action = {\n startPos: {},\n mode: false,\n };\n }, 10);\n };\n\n componentWillUnmount() {\n this.root.current.removeEventListener('touchmove', this.onPointerMove);\n if (this.preventSwipeTriggerClick) {\n clearTimeout(this.preventSwipeTriggerClick);\n }\n\n emitter.off(DROP_ITEM, this.resetAction);\n }\n\n onPointerDown(e) {\n this.action.startPos = {\n x: e.clientX || e.touches[0].clientX,\n y: e.clientY || e.touches[0].clientY,\n };\n }\n\n onPointerMove(e) {\n const isVertical = this.props.isVertical;\n if (this.action.mode || this.state.errorVisible) {\n if (this.action.mode === 'dragging' && e.type === 'touchmove') {\n this.props.forwardEvent(e);\n }\n\n return;\n }\n\n const pos = {\n x: e.clientX || e.touches[0].clientX,\n y: e.clientY || e.touches[0].clientY,\n };\n\n const dx = Math.abs(pos.x - this.action.startPos.x);\n const dy = Math.abs(pos.y - this.action.startPos.y);\n // change how much we care about moving in the swipe direction,\n const angle = 130; // degrees\n const swipeWeight = Math.atan((Math.PI / 180) * ((180 - angle) / 2));\n\n if (\n (isVertical && dx > actionThreshold && dx > dy * swipeWeight) ||\n (!isVertical && dy > actionThreshold && dy > dx * swipeWeight)\n ) {\n if (this.props.disabled) {\n localStatisticsReporter.reportProductMenuSwiperInteraction(\n { status: 'disabled', item: this.props.product.id },\n 'drag'\n );\n\n return this.setState({\n errorVisible: true,\n });\n }\n\n this.action.mode = 'dragging';\n const img = e.currentTarget.querySelector('img');\n const trimmedThumb = swiperService.getThumbnailData(\n this.getImageUrl(this.imageObj)\n );\n\n // Since we are using object-fit: contain we need to calculate how large the actual image is\n const containedSize = contain(\n this.imageDiv.current.clientWidth,\n this.imageDiv.current.clientHeight,\n img.naturalWidth,\n img.naturalHeight\n );\n\n const imgScale = containedSize.width\n ? containedSize.width / img.naturalWidth\n : 1;\n\n const domSize = {\n height: imgScale * trimmedThumb.height,\n width: imgScale * trimmedThumb.width,\n };\n\n emitter.emit(\n GRAB_ITEM,\n this.props.product,\n pos,\n this.props.mode,\n e.type,\n domSize\n );\n } else if (\n (isVertical && dy > actionThreshold && dy > dx) ||\n (!isVertical && dx > actionThreshold && dx > dy)\n ) {\n this.action.mode = 'swiping';\n localStatisticsReporter.reportProductMenuSwiperInteraction({}, 'swipe');\n }\n }\n\n onClick() {\n if (clicked || this.action.mode === 'swiping') {\n return;\n }\n localStatisticsReporter.reportProductMenuSwiperInteraction(\n { item: this.props.product.id },\n 'click'\n );\n\n clicked = this.props.product.id;\n this.forceUpdate();\n this.clickHelpActive = setTimeout(() => {\n clicked = false;\n this.forceUpdate();\n }, 600);\n }\n\n getDimensionGroup(dimension, article, product) {\n const headerLabelKey = `SHORT_LABEL_${dimension.toUpperCase()}`;\n return (\n \n \n {translate(t[headerLabelKey])}\n {' '}\n {this.getCombinedMeasurements(dimension, article, product)}\n \n );\n }\n\n onPopupClose = () => {\n this.setState({\n errorVisible: false,\n });\n };\n\n getImageUrl = image => {\n const { isVertical, isRtl, userAgent } = this.props;\n const { isMobile } = userAgent;\n\n const fallbackUrl = image.thumbnailUrl || image.url;\n\n if (!isVertical) {\n if (isMobile) {\n return (\n image.thumbnailUrlMobilePortrait ||\n image.thumbnailUrlPortrait ||\n fallbackUrl\n );\n }\n return image.thumbnailUrlPortrait || fallbackUrl;\n } else if (isRtl) {\n if (isMobile) {\n return (\n image.thumbnailUrlMobileLandscape ||\n image.thumbnailUrlRTL ||\n fallbackUrl\n );\n }\n return image.thumbnailUrlRTL || fallbackUrl;\n } else if (isMobile && isVertical) {\n return image.thumbnailUrlMobileLandscape || fallbackUrl;\n }\n\n return fallbackUrl;\n };\n\n capitalize = (str, locale) => {\n const languageCode = locale.substring(0, 2);\n const capsLetters = languageCode === 'nl' && /^ij/i.test(str) ? 2 : 1;\n\n let capitalizedStr;\n try {\n capitalizedStr =\n str.substring(0, capsLetters).toLocaleUpperCase(locale) +\n str.substring(capsLetters);\n } catch (e) {\n capitalizedStr = str.substring(0, 1).toUpperCase() + str.substring(1);\n }\n\n return capitalizedStr;\n };\n\n getImageStyle = () => {\n const { filter, userAgent } = this.props;\n\n if (filter.swiperLayout.verticalImgWidths?.[userAgent.deviceType]) {\n return {\n flex: `0 0 ${\n filter.swiperLayout.verticalImgWidths[\n platform.isKiosk ? 'kiosk' : userAgent.deviceType\n ]\n }px`,\n };\n }\n };\n\n /**\n * Return classes for images\n * @returns {*}\n */\n getImageClassNames = () => {\n if (\n this.props.swiperLayout === 'centeredLayout' &&\n this.props.isSectionFilter\n )\n return styles.img_centered_layout;\n };\n\n /**\n * Render image\n *\n * @returns {*|JSX.Element}\n */\n renderImage = () =>\n this.imageObj && (\n \n \n {this.indicator && (\n \n )}\n
\n );\n\n /**\n * Returns formatted article text\n *\n * @returns {string|*}\n */\n getArticleText = () => {\n const productTypeText =\n productService.getCustomProductTypeText(this.props.product) ||\n this.typeText;\n return this.shouldDisplayColor\n ? `${productTypeText}, ${this.article.validDesignText}`\n : productTypeText;\n };\n\n /**\n * Renders details\n *\n * @returns {JSX.Element|boolean}\n */\n renderDetails = () =>\n (this.shouldDisplayProductType || this.props.displayMeasurement) &&\n this.renderDefaultLayout();\n\n /**\n * Renders a combined layout.\n * @example Tables with legs need to display different measurements and the product name.\n * @returns {JSX.Element}\n */\n\n renderCombinedLayout = () => (\n
\n
\n {this.article.name}{' '}\n {this.capitalize(\n this.article.validDesignText,\n applicationSettings.locale\n )}\n
\n\n
\n {this.getDimensionGroup('width', this.article, this.props.product)}{' '}\n {this.getDimensionGroup('depth', this.article, this.props.product)}{' '}\n {this.getDimensionGroup('height', this.article, this.props.product)}{' '}\n
\n
\n );\n\n getCombinedMeasurements = (dimension, article, product) => {\n const { useMetricMeasures } = this.props;\n const { products } = this.props.filter;\n\n const linkedProducts = products\n .find(products => products.includes(product.id))\n .map(linkedProductId => productService.getProduct(linkedProductId))\n .filter(Boolean);\n\n const dimensionTexts = linkedProducts\n .map(linkedProduct =>\n withLigatures(\n swiperService.getTableDisplayText(\n article,\n linkedProduct,\n useMetricMeasures,\n dimension\n )\n )\n )\n .filter(unique);\n const dimensionElements = dimensionTexts.map((text, index) => (\n {index === 0 ? text : ', ' + text}\n ));\n\n return {dimensionElements};\n };\n\n /**\n * Returns a string containing measurements string according to settings\n *\n * @returns {string|string}\n */\n getMeasurementString = () =>\n this.props.displayMeasurement ? this.props.measurementString : null;\n\n /**\n * Render product measurements\n *\n * @param renderMultiline {boolean}\n * @returns {*|JSX.Element}\n */\n renderProductMeasurements = renderMultiline =>\n renderMultiline ? (\n
\n {this.getMeasurementString()}\n
\n ) : (\n this.getMeasurementString()\n );\n\n renderCenteredText = () =>\n (this.shouldDisplayProductType || this.props.displayMeasurement) &&\n this.renderDefaultLayout(true);\n\n /**\n * Get default layout classes\n * @param centered\n * @returns {*[]}\n */\n getDefaultLayoutClasses = centered => [\n styles.text,\n centered && styles.text_centered_layout,\n ];\n\n /**\n * Render default layout\n *\n * @returns {JSX.Element}\n */\n renderDefaultLayout = (centered = false) => (\n \n {!this.shouldDisplayProductType &&\n (this.shouldDisplayColor || this.props.renderMultiline) && (\n
\n {this.shouldDisplayColor\n ? this.capitalize(\n this.article.validDesignText,\n applicationSettings.locale\n )\n : ''}\n
\n )}\n\n {this.shouldDisplayProductType && (\n
\n {this.getArticleText()}\n
\n )}\n\n {this.renderProductMeasurements(this.props.renderMultiline)}\n \n );\n\n /**\n * Render paired layout\n *\n * @returns {JSX.Element}\n */\n renderPairTitle = () => (\n
\n {this.getMeasurementString()}\n
\n );\n\n /**\n * Selects and returns correct layout format\n *\n * @returns {*}\n */\n renderLayout = () => this.layouts[this.props.swiperLayout]();\n\n /**\n * Return Item classes\n * @returns {(*|false)[]}\n */\n getItemClasses = () => {\n const {\n disabled,\n product: { id },\n filter: { itemClass },\n swiperLayout,\n } = this.props;\n return [\n styles.item,\n disabled && styles.disabled,\n clicked === id && styles.clicked,\n styles[itemClass],\n styles[swiperLayout],\n swiperLayout === 'centeredLayout' && styles.item_centered_layout,\n ];\n };\n\n render() {\n const { isVertical, disabled } = this.props;\n\n const disabledMessage = (' ' + disabled).slice(1); // Lose the reference to the string IKEABROR-342\n\n return (\n \n {this.renderLayout()}\n {disabled && (\n \n \n \n
{disabledMessage}
\n \n \n )}\n \n );\n }\n}\n\nexport default connect((state, { product }) => ({\n currentColorFilter: selectCurrentColorFilter(state),\n measurementString: selectMeasurementString(state, product.id),\n displayMeasurement: selectShouldDisplayMeasurements(state, product),\n filter: selectProductMenuFilter(state),\n swiperLayout: selectProductMenuLayout(state),\n isSectionFilter: selectCurrentFilterName(state) === FILTERS.SECTIONS,\n isRtl: selectIsRtl(state),\n useMetricMeasures: selectUseMetric(state),\n}))(ProductSwiperItem);\n","import styles from './ProductSwiper.module.less';\n\nlet imagesReadyTimer;\nlet rewriteGrids;\n\nfunction clearTimers() {\n if (imagesReadyTimer) {\n clearTimeout(imagesReadyTimer);\n }\n if (rewriteGrids) {\n clearInterval(rewriteGrids);\n }\n}\n\nfunction onInit() {\n this.isEnd = true;\n}\n\nexport function recalcGrids() {\n const measureProperty =\n this.originalParams.direction === 'vertical'\n ? 'clientHeight'\n : 'clientWidth';\n\n clearTimers();\n imagesReadyTimer = setTimeout(() => {\n const slideWindowHeight = this.el[measureProperty];\n const slides = Array.from(\n this.el.getElementsByClassName(styles.swiperSlide)\n );\n\n const slidesSizesGrid = slides.map(slide => slide[measureProperty]);\n let maxTranslate = -slideWindowHeight;\n let pos = slidesSizesGrid.length;\n while (pos--) {\n maxTranslate += slidesSizesGrid[pos];\n }\n\n let slidesGrid = slidesSizesGrid.reduce(\n function (grid, slide) {\n grid.push((grid.length && grid[grid.length - 1] + slide) || 0);\n return grid;\n },\n [0]\n );\n // first make it snap slides to the top\n let snapGrid = slidesGrid.slice(0);\n /* remove any points that are\n - less than 0\n - would make the swiper translate more than necessary\n - duplicates\n */\n snapGrid = snapGrid.filter(function (pos, index, grid) {\n return pos >= 0 && pos <= maxTranslate && grid.indexOf(pos) === index;\n });\n\n snapGrid.sort(function (a, b) {\n return a - b;\n });\n\n if (snapGrid.length === 0) {\n // no snap points means that we don't have a full page of slides,\n // so give it somewhere to snap so it doesn't scroll all over the place\n snapGrid.push(0);\n slidesGrid = [slidesGrid[0]];\n } else if (snapGrid.length === 1) {\n // if only snapping to front, and maxtranslate doesnt completely hide\n // item 0, we need to add this to allow moving to the end\n snapGrid.push(maxTranslate);\n } else if (snapGrid.length > 1) {\n snapGrid.pop();\n snapGrid.push(maxTranslate);\n }\n\n const updateGridValues = () => {\n this.snapGrid = snapGrid;\n this.slidesGrid = slidesGrid;\n this.slidesSizesGrid = slidesSizesGrid;\n this.isEnd = snapGrid.length === 1;\n this.navigation.update();\n };\n\n updateGridValues();\n // reset to top position to dodge arrow buttons acting weird post flip\n this.setTranslate(0);\n\n let rewrites = 0;\n rewriteGrids = setInterval(() => {\n rewrites++;\n updateGridValues();\n if (rewrites > 30) {\n clearInterval(rewriteGrids);\n }\n }, 50);\n }, 50);\n}\n\nconst settings = isVertical => ({\n direction: isVertical ? 'vertical' : 'horizontal',\n on: {\n beforeDestroy: clearTimers,\n imagesReady: recalcGrids,\n resize: recalcGrids,\n init: onInit,\n },\n initialSlide: 0,\n loop: false,\n mousewheel: {\n mousewheelReleaseOnEdges: true,\n sensitivity: 3,\n },\n keyboard: {\n enabled: false,\n },\n spaceBetween: 0,\n slideToClickedSlide: false,\n simulateTouch: true,\n paginationClickable: true,\n freeMode: true,\n freeModeMinimumVelocity: 0.1,\n freeModeMomentumBounce: false,\n freeModeMomentumRatio: 0.35,\n freeModeSticky: true,\n grabCursor: false,\n roundLengths: true,\n\n preventClicks: false,\n preventClicksPropagation: false,\n centeredSlides: false,\n slidesOffsetAfter: 0,\n\n slideClass: styles.swiperSlide,\n wrapperClass: styles.slidesWrapper,\n navigation: {\n nextEl: '.' + styles.swiperButtonNext,\n prevEl: '.' + styles.swiperButtonPrev,\n disabledClass: styles.disabled,\n },\n});\n\nexport default settings;\n","import React, { useState } from 'react';\nimport PropTypes from 'prop-types';\nimport { connect, useDispatch } from 'react-redux';\nimport Switch from '@ingka/switch';\n\nimport {\n KompisSheet,\n KompisSheetHeader,\n KompisSheetBody,\n KompisSheetBodyPadding,\n} from '@inter-ikea-kompis/react-components';\nimport {\n KompisHyperlink,\n KompisText,\n} from '@inter-ikea-kompis/react-components';\nimport {\n HyperlinkColorEnum,\n HyperlinkTargetEnum,\n} from '@inter-ikea-kompis/component-hyperlink';\nimport { SheetSizeEnum } from '@inter-ikea-kompis/component-sheet';\n\nimport { SkapaTheme } from '@inter-ikea-kompis/themes';\nimport { ProductUtility } from '@inter-ikea-kompis/utilities';\n\nimport styles from './MountingRailSwitch.module.less';\nimport { IconButton } from '../IconButton';\nimport { ButtonTypeEnum } from '@inter-ikea-kompis/component-button';\nimport { IconButtonSizeEnum } from '@inter-ikea-kompis/component-icon-button';\n\nimport Portal from '../utils/Portal';\nimport productService from '../../services/products';\nimport articles from '../../services/products/articles';\nimport { translate } from '../../services/L10n';\nimport constants from '../../settings/constants';\nimport platform from '../../util/platform';\nimport { mountingRailSheetOpened } from '../../state/popups/popupsActions';\nimport { selectDexfSettings } from '../../state/dexfSettings/dexfSettingsSelectors';\nimport { selectHasShownMountingRailSheet } from '../../state/popups/popupsSelectors';\nimport { actionSetDirtyConfiguration } from '../../state/vpc/vpcActions';\nimport localStatisticsReporter from '../../services/statistics/insights/custom/local/localStatisticsReporter';\nimport { t } from '../../translations';\n\nconst MountingRailSwitch = ({\n onChange,\n checked,\n isMobile,\n isLandscape,\n hasShownSheet,\n setHasShownSheet,\n dexfSettings,\n}) => {\n if (!productService.areMountingRailsValid()) {\n return null;\n }\n\n const getAllMountingRailIds = () => {\n const colors = Object.keys(constants.MOUNTING_RAILS);\n const allMountingRailIds = colors.reduce(\n (mountingRailIds, color) => [\n ...mountingRailIds,\n ...Object.values(constants.MOUNTING_RAILS[color]),\n ],\n []\n );\n return allMountingRailIds;\n };\n\n const createPipLink = (mountingRailIds, dexfSettings) => {\n for (const id of mountingRailIds) {\n const article = articles.getArticle(id);\n\n const pipUrl =\n article &&\n ProductUtility.getProductInformationPageLink(article, dexfSettings);\n if (pipUrl) {\n return pipUrl;\n }\n }\n };\n\n const [showSheet, setShowSheet] = useState(false);\n const allMountingRailIds = getAllMountingRailIds();\n const pipLink = createPipLink(allMountingRailIds, dexfSettings);\n const dispatch = useDispatch();\n\n const handleOnChange = toggleSheet => {\n if (toggleSheet) {\n onChange(checked);\n dispatch(actionSetDirtyConfiguration(true));\n }\n if (!hasShownSheet || !toggleSheet) {\n setShowSheet(true);\n setHasShownSheet(true);\n }\n };\n\n const handleHelpIconClick = () => {\n localStatisticsReporter.reportProductMenuMountingRailInfoClicked();\n handleOnChange();\n };\n\n const renderHelpIcon = (\n \n );\n\n return (\n \n
\n
\n \n {!isMobile && renderHelpIcon}\n
\n
\n handleOnChange(true)}\n />\n {isMobile && renderHelpIcon}\n
\n
\n \n {\n setShowSheet(false);\n }}\n visible={showSheet}\n theme={SkapaTheme}\n sheetSize={SheetSizeEnum.small}\n >\n {\n setShowSheet(false);\n }}\n />\n \n \n
\n

\n {translate(t.MOUNTING_RAIL)}\n

\n

{constants.APPLICATION_NAME}

\n

\n {translate(\n t.MOUNTING_RAIL_HELPS_YOU_HANG_STORAGE_RAILS_EVENLY\n )}\n

\n

\n {translate(t.MOUNTING_RAIL_COMES_IN_THREE_DIFFERENT_LENGTHS)}{' '}\n {translate(t.MOUNTING_RAIL_CUT)}\n

\n {!platform.isKiosk && (\n \n \n \n )}\n
\n
\n \n
\n
\n
\n \n
\n
\n );\n};\n\nMountingRailSwitch.propTypes = {\n onChange: PropTypes.func.isRequired,\n checked: PropTypes.bool,\n isMobile: PropTypes.bool.isRequired,\n isLandscape: PropTypes.bool.isRequired,\n hasShownSheet: PropTypes.bool.isRequired,\n setHasShownSheet: PropTypes.func.isRequired,\n};\n\nexport default connect(\n state => ({\n hasShownSheet: selectHasShownMountingRailSheet(state),\n dexfSettings: selectDexfSettings(state),\n }),\n dispatch => ({\n setHasShownSheet: () => dispatch(mountingRailSheetOpened()),\n })\n)(MountingRailSwitch);\n","import React, { useState } from 'react';\nimport classNames from 'classnames';\n// @ts-ignore // Can't find no working type for external lib\nimport Swiper from 'swiper';\nimport { SkapaTheme } from '@inter-ikea-kompis/themes';\nimport { useDispatch, useSelector } from 'react-redux';\nimport ProductSwiperItem from './ProductSwiperItem';\nimport { translate } from '../../services/L10n';\nimport constants from '../../settings/constants';\nimport { DROP_ITEM, GRAB_ITEM } from '../../settings/events';\nimport styles from './ProductSwiper.module.less';\nimport { Icon } from '../Icon';\nimport swiperSettings from './SwiperSettings';\nimport { preventDefault } from '../../util/events';\nimport { TOUCH_SUPPORT } from '../../util/supportedEvents';\nimport emitter from '../../emitter';\nimport {\n POINTER_LEAVE,\n POINTER_UP,\n POINTER_DOWN,\n} from '../../util/supportedEvents';\nimport localStatisticsActions from '../../services/statistics/insights/custom/local/localStatisticsActions';\nimport swiperService from '../../services/swiper';\nimport productService from '../../services/products';\nimport MountingRailSwitch from './MountingRailSwitch';\nimport { getLocalEvent } from '../../services/statistics/insights/custom/local/localStatisticsEvents';\nimport {\n selectHasScenePreloadFinished,\n selectIsWallResizerActive,\n} from '../../state/scene/sceneSelectors';\nimport {\n selectCurrentColorFilter,\n selectProductMenuFilter,\n selectFilteredSwiperItems,\n selectCurrentSubFilterValue,\n} from '../../state/productMenu/productMenuSelectors';\nimport { selectDisableMountingRails } from '../../state/tac/tacSelectors';\nimport {\n selectIsLandscape,\n selectIsMobile,\n selectIsPortrait,\n selectIsTablet,\n selectIsWidescreen,\n selectUserAgent,\n} from '../../state/userAgent/userAgentSelectors';\nimport { ProductMenuItem } from '../../state/productMenu/productMenuTypes';\nimport { thunkSetUsingMountingRail } from '../../state/tac/tacThunks';\nimport { FilterObject } from '../../services/products/productsServiceTypes';\nimport {\n KompisIconPill,\n KompisText,\n KompisPopover,\n KompisPopoverPosition,\n KompisPopoverPadding,\n} from '@inter-ikea-kompis/react-components';\nimport {\n PopoverAlignmentEnum,\n PopoverDirectionEnum,\n} from '@inter-ikea-kompis/component-popover';\nimport { InformationIcon } from '@inter-ikea-kompis/icons';\nimport { IconPillSizeOptionsEnum } from '@inter-ikea-kompis/component-icon-pill';\nimport { ThemeFontStyleTypeEnum } from '@inter-ikea-kompis/enums';\nimport { t } from '../../translations';\n\nconst ProductSwiper = ({ forwardEvent }: any) => {\n const wallResizerActive = useSelector(selectIsWallResizerActive);\n const disableMountingRails = useSelector(selectDisableMountingRails);\n const currentFilter = useSelector(selectProductMenuFilter);\n const isPortrait = useSelector(selectIsPortrait);\n const currentColorFilter = useSelector(selectCurrentColorFilter);\n const userAgent = useSelector(selectUserAgent);\n const isMobile = useSelector(selectIsMobile);\n const isTablet = useSelector(selectIsTablet);\n const isLandscape = useSelector(selectIsLandscape);\n const scenePreloadDone = useSelector(selectHasScenePreloadFinished);\n const isWideScreen = useSelector(selectIsWidescreen);\n const filterTextKey = useSelector(swiperService.getSwiperFilterText);\n const items = useSelector(selectFilteredSwiperItems).map(([item]) => item);\n const currentSubFilter = useSelector(selectCurrentSubFilterValue);\n\n const [infoButtonSelected, setInfoButtonSelected] = useState(false);\n\n const onSelect = () => setInfoButtonSelected(!infoButtonSelected);\n\n const onClickAnywhere = (e: any) =>\n e.target.parentNode?.id !== 'info-pill-wrapper' &&\n setInfoButtonSelected(false);\n\n const dispatch = useDispatch();\n const setUseMountingRails = (disableMountingRails: boolean) =>\n dispatch(thunkSetUsingMountingRail(disableMountingRails));\n\n const rootRef = React.useRef(null);\n const swiperRef = React.useRef(null);\n const swiper = React.useRef(null);\n const unsubscribe = React.useRef<(() => any)[]>();\n\n const { labelTranslationKey, name: filterName } = currentFilter;\n\n /**\n * Get dimension values\n * @param items\n * @param dimensionKey\n */\n const getDimensionValues = (items: ProductMenuItem[], dimensionKey: any) =>\n items\n // @ts-ignore LIMITATION OF ANCIENT, PRE-HISTORIC TS-VERSION NOT SUPPORTING ENUMS\n .map((item: ProductMenuItem) => item.product[dimensionKey])\n .filter((item, pos, arr) => arr.indexOf(item) === pos)\n .sort();\n\n /**\n * Is valid product\n * @param item\n */\n const isValidProduct = ({ product: { id } }: ProductMenuItem) =>\n id !== 'missing';\n\n /**\n * Create item pairs\n * @param items\n * @param itemPairs\n */\n const createItemPairs = (\n items: ProductMenuItem[],\n itemPairs: ProductMenuItem[][]\n ): ProductMenuItem[][] => {\n if (items.length < 2)\n return items.length ? [...itemPairs, items] : itemPairs;\n\n const [item1, item2, ...rest] = items;\n return createItemPairs(rest, [...itemPairs, [item1, item2]]);\n };\n\n /**\n * Get paired product slides\n * @param allItems\n */\n const getPairedProductSlides = (allItems: ProductMenuItem[]) => {\n if (!currentFilter.pairingSettings) return;\n\n const pairKey = currentFilter.pairingSettings.key;\n const validItems = allItems.filter(isValidProduct);\n const pairingDimensionValues = getDimensionValues(allItems, pairKey);\n\n if (pairingDimensionValues.length !== 2) {\n return validItems.map(item => makeSlide([item]));\n } else {\n if (isWideScreen) {\n // @ts-ignore LIMITATION OF ANCIENT, PRE-HISTORIC TS-VERSION NOT SUPPORTING ENUMS\n return createItemPairs(validItems, []).map(items => makeSlide(items));\n } else {\n return pairingDimensionValues.map(value =>\n allItems\n // @ts-ignore LIMITATION OF ANCIENT, PRE-HISTORIC TS-VERSION NOT SUPPORTING ENUMS\n .filter(({ product }) => product[pairKey] === value)\n .map(product => makeSlide([product]))\n );\n }\n }\n };\n\n /**\n * Should render multiline\n * @param allItems\n */\n const shouldRenderMultiline = (allItems: ProductMenuItem[]) =>\n !isVertical() &&\n allItems.some(\n item =>\n // @ts-ignore we need to find a different way of handling these range specific returns instead of\n // errors, since it really upsets poor TS\n (!swiperService.shouldDisplayProductType(item.product) &&\n swiperService.displayColor(\n item.product,\n currentFilter,\n currentColorFilter\n )) ||\n swiperService.shouldDisplayProductType(item.product)\n );\n\n /**\n * Get product slides\n */\n const getProductSlides = () => {\n if (!scenePreloadDone || !items?.length) return null;\n\n const renderMultiline = shouldRenderMultiline(items);\n\n if (currentFilter.pairingSettings) return getPairedProductSlides(items);\n\n return items.map((items: any) => makeSlide([items], renderMultiline));\n };\n\n /**\n * Disable swiping\n */\n const _disableSwiping = () => (swiper.current.allowTouchMove = false);\n\n /**\n * Enable swiping\n */\n const _enableSwiping = () => (swiper.current.allowTouchMove = true);\n\n /**\n * Prevent default event\n * @param event\n */\n const _preventDefaultEvent = (event: any) =>\n rootRef.current &&\n rootRef.current.addEventListener(event, preventDefault, { passive: false });\n\n /**\n * Is vertical\n */\n const isVertical = () => (!isMobile && !isTablet) || !isPortrait;\n\n React.useEffect(() => {\n if (TOUCH_SUPPORT) {\n _preventDefaultEvent('touchmove');\n }\n\n _preventDefaultEvent('wheel');\n _preventDefaultEvent('scroll');\n\n addSwiper();\n\n emitter.on(GRAB_ITEM, _disableSwiping);\n emitter.on(DROP_ITEM, _enableSwiping);\n\n // NIF markets can't handle window.parent. Catch possible error.\n try {\n if (window.parent !== window) {\n unsubscribe.current = [handleMouseOutsideIframe()];\n }\n } catch (error) {}\n\n return () => {\n unsubscribe.current &&\n unsubscribe.current.forEach((fn: () => any) => fn());\n\n emitter.off(GRAB_ITEM, _disableSwiping);\n emitter.off(DROP_ITEM, _enableSwiping);\n\n killSwiper();\n };\n }, []);\n\n React.useEffect(() => {\n addSwiper();\n }, [currentFilter, currentSubFilter]);\n\n React.useEffect(() => {\n window.addEventListener(POINTER_DOWN, onClickAnywhere);\n return () => window.removeEventListener(POINTER_DOWN, onClickAnywhere);\n }, []);\n\n /**\n * Handle mouse outside iframe event\n */\n const handleMouseOutsideIframe = () => {\n // NIF markets can't handle window.parent. Catch possible error.\n try {\n window.parent.addEventListener(POINTER_UP, stopSwiping);\n\n return () => window.parent.removeEventListener(POINTER_UP, stopSwiping);\n } catch (e) {\n document.addEventListener(POINTER_LEAVE, stopSwiping);\n\n return () => document.removeEventListener(POINTER_LEAVE, stopSwiping);\n }\n };\n\n /**\n * Stop swiping\n */\n const stopSwiping = () =>\n swiper.current && swiper.current.$el.trigger(POINTER_UP);\n\n /**\n * Kill swiper\n */\n const killSwiper = () => swiper.current && swiper.current.destroy();\n\n /**\n * Add swiper\n */\n const addSwiper = () => {\n const wasMouseEntered = swiper.current?.mouseEntered;\n killSwiper();\n swiper.current = new Swiper(\n `.${styles.swiperContainer}`,\n swiperSettings((!isMobile && !isTablet) || !isPortrait)\n );\n swiper.current.mouseEntered = wasMouseEntered;\n };\n\n /**\n * Report swiper analytics\n */\n const reportSwiperAnalytics = () =>\n getLocalEvent(\n localStatisticsActions.PRODUCT_MENU_SWIPER_INTERACTION\n ).reportEvent({ interaction: 'arrow' });\n\n /**\n * Render product swiper item\n * @param items\n * @param renderMultiline\n */\n const renderProductSwiperItems = (\n items: ProductMenuItem[],\n renderMultiline: boolean\n ) => {\n // @ts-ignore LIMITATION OF ANCIENT, PRE-HISTORIC TS-VERSION NOT SUPPORTING ENUMS\n const mode = constants.DRAG_MODE[currentFilter.dragMode];\n\n return items.map(item => (\n \n ));\n };\n\n /**\n * Renders filters\n */\n const renderFilters = () => {\n const filters = productService\n .getFilters()\n .map(({ Component }: FilterObject, index: number) => (\n \n ));\n\n return filters.length ? (\n
\n {filters}\n
\n ) : null;\n };\n\n /**\n * Get header\n */\n const getHeader = () =>\n !isPortrait && (\n \n \n {translate(t[labelTranslationKey])}\n \n \n );\n\n /**\n * Make slide\n */\n const makeSlide = (items: ProductMenuItem[], renderMultiline = false) => (\n \n {renderProductSwiperItems(items, renderMultiline)}\n \n );\n\n /**\n * Render mounting rail switch\n */\n const renderMountingRailSwitch = () =>\n currentFilter.settings?.mountingRail && (\n \n );\n\n /**\n * Get settings slide\n */\n const getSettingsSlide = () =>\n currentFilter.settings && (\n \n {renderMountingRailSwitch()}\n \n );\n\n const renderConfigurators = () => {\n const configs = swiperService\n .getSwiperConfigSectionItems()\n .reduce(\n // @ts-ignore\n (acc, { condition, appliesToFilter, Component }, index) => {\n return condition() && appliesToFilter === filterName\n ? [...acc, ]\n : acc;\n },\n []\n );\n\n return configs ?
{configs}
: null;\n };\n\n /**\n * Render filter text component for smaller screens\n */\n const renderMobileFilterTextComponent = () => (\n
\n \n
\n \n
\n \n \n \n {translate(t[filterTextKey!])}\n \n \n \n
\n
\n );\n\n /**\n * Render filter text component for larger screens\n */\n const renderFilterTextComponent = () => (\n
\n {translate(t[filterTextKey!])}\n
\n );\n\n /**\n * Render text for filter\n */\n const renderSwiperFilterText = () => {\n if (!filterTextKey) return null;\n\n return isMobile || isTablet\n ? renderMobileFilterTextComponent()\n : renderFilterTextComponent();\n };\n\n return (\n \n \n \n
\n \n
\n \n
\n \n {getHeader()}\n {renderSwiperFilterText()}\n {renderConfigurators()}\n {renderFilters()}\n {getProductSlides()}\n {getSettingsSlide()}\n
\n \n\n \n
\n \n
\n \n \n
\n );\n};\n\nexport default ProductSwiper;\n","import React, { FunctionComponent } from 'react';\nimport IntroPopup from '../Popup/IntroPopup';\nimport { LEFT, TOP } from '../Popup/alignments';\nimport { translate } from '../../services/L10n';\nimport { t } from '../../translations';\nimport styles from './ProductMenu.module.less';\nimport ProductMenuFilter from './ProductMenuFilter';\nimport ProductSwiper from './ProductSwiper';\nimport { useDispatch, useSelector } from 'react-redux';\nimport {\n selectFiltersAsArray,\n selectProductMenuFilter,\n} from '../../state/productMenu/productMenuSelectors';\nimport { thunkSetFilter } from '../../state/productMenu/productMenuThunks';\nimport {\n selectIsMobileOrTablet,\n selectIsMobilePortrait,\n selectIsTabletPortrait,\n} from '../../state/userAgent/userAgentSelectors';\nimport classNames from 'classnames';\nimport { isFixedRoom } from '../../util/room';\nimport WallResizerIcon from '../ActionButtons/WallResizerIcon/WallResizerIcon';\nimport Measurements from '../ActionButtons/Measurements/Measurements';\nimport {\n selectFilterIntroPopupVisible,\n selectIntroPopupsVisible,\n} from '../../state/popups/popupsSelectors';\n\nexport interface Props {\n forwardEvent: any;\n}\n\nconst ProductMenu: FunctionComponent = ({ forwardEvent }) => {\n const filters = useSelector(selectFiltersAsArray);\n const currentFilter = useSelector(selectProductMenuFilter);\n const introPopupsVisible = useSelector(selectIntroPopupsVisible);\n const displayIntro = !useSelector(selectIsMobileOrTablet);\n const isMobilePortrait = useSelector(selectIsMobilePortrait);\n const isTabletPortrait = useSelector(selectIsTabletPortrait);\n const filterIntroPopupVisible = useSelector(selectFilterIntroPopupVisible);\n\n const dispatch = useDispatch();\n\n const setFilter = (filter: string) => dispatch(thunkSetFilter(filter));\n\n React.useEffect(() => {\n setFilter(currentFilter.name);\n }, []);\n\n /**\n * Render header label\n * @returns {JSX.Element}\n */\n const renderHeaderLabel = () =>\n isTabletPortrait && (\n
\n {translate(t[currentFilter.labelTranslationKey])}\n
\n );\n\n /**\n * Render action buttons\n * @returns {JSX.Element}\n */\n const renderActionButtons = () =>\n isMobilePortrait && (\n
\n {isFixedRoom() && }\n \n
\n );\n\n /**\n * Render filters\n * @returns {unknown[]}\n */\n const renderFilters = () =>\n filters.map(filter => (\n \n ));\n\n /**\n * Render product Menu filters\n * @returns {JSX.Element}\n */\n const renderProductMenuFilters = () => (\n
\n \n
\n {renderFilters()}\n {renderHeaderLabel()}\n
\n \n {renderActionButtons()}\n
\n );\n\n return (\n \n
\n {renderProductMenuFilters()}\n
\n \n
\n
\n \n );\n};\n\nexport default ProductMenu;\n","import _ from 'lodash';\nimport productService from '../../../../services/products';\n\nfunction getMoveDirection(item, tac) {\n //Not implemented\n return 'center';\n}\n\nfunction moveSuperSection(section, superSection, diff, direction) {\n //Not implemented\n}\n\nfunction getChangeDimension(item) {\n if (productService.isSection(item)) {\n return 'depth';\n }\n}\n\nfunction getSelectedChild(item, optionItems, switchingProp) {\n const isCabinetWood = item.items.some(\n childItem => childItem.filter.color === 'wood'\n );\n\n const [[dominantSwitchingPropValue]] = Object.entries(\n item.items\n .filter(childItem => !productService.isAddOnShelf(childItem))\n .reduce((switchingPropValues, childItem) => {\n if (isCabinetWood && productService.isCabinet(childItem)) {\n switchingPropValues.wood = switchingPropValues.wood\n ? switchingPropValues.wood + 1\n : 1;\n } else if (switchingPropValues[childItem.filter[switchingProp]]) {\n switchingPropValues[childItem.filter[switchingProp]]++;\n } else {\n switchingPropValues[childItem.filter[switchingProp]] = 1;\n }\n return switchingPropValues;\n }, {})\n ).sort((a, b) => _.clamp(b[1] - a[1], -1, 1));\n\n const hasDivergentItems = !item.items.every(childItem => {\n if (childItem.filter[switchingProp] === dominantSwitchingPropValue) {\n return true;\n }\n const canSwap =\n productService.getSwappables(childItem, {\n depth: childItem.filter.depth,\n width: childItem.filter.width,\n [switchingProp]: dominantSwitchingPropValue,\n }).length > 0;\n\n return !canSwap;\n });\n\n return {\n hasDivergentItems,\n filter: { [switchingProp]: dominantSwitchingPropValue },\n };\n}\n\nexport default {\n getMoveDirection,\n moveSuperSection,\n getChangeDimension,\n getSelectedChild,\n};\n","function getMoveDirection(item, tac) {\n //Not implemented\n return 'center';\n}\n\nfunction moveSuperSection(section, superSection, diff, direction) {\n //Not implemented\n}\n\nfunction getChangeDimension(item) {\n //Not implemented\n}\n\nexport default {\n getMoveDirection,\n moveSuperSection,\n getChangeDimension,\n};\n","import tacHelpers from '../../../../state/tac/tacHelpers';\nimport productService from '../../../../services/products';\nimport geometry from '../../../../scene/util/geometry';\nimport { isFixedRoom } from '../../../../util/room';\nimport { ITEMS } from '../../../../constants';\n\nfunction shouldMoveRight(upright, section) {\n return upright.x > section.x;\n}\n\nfunction shouldMoveLeft(upright, section) {\n return upright.x < section.x;\n}\n\nfunction getMoveDirection(from, to, dimension, tac) {\n if (\n (from.id === to.id && !from.logic.extendable) ||\n !productService.isSection(from)\n ) {\n return;\n }\n\n const diff = to.value - from.width;\n const superSection = tacHelpers.findSuperSection(tac, from);\n const blockers = tacHelpers.getVerticalNeighbours(from, superSection);\n if (blockers.length) {\n /*\n Vertical neighbours (inside same supersection) are considered blockers,\n since it can be very hard to pre-validate width change.\n\n but, we ignore lagkapten sections that are empty\n */\n\n const relevant = blockers.filter(\n blocker => blocker.filter.switchable || blocker.items?.length\n );\n\n if (relevant.length) {\n return;\n }\n }\n\n const wallMaxX = Math.max(...tac.wall.points.map(point => point.x));\n\n if (!isFixedRoom()) {\n const space = tacHelpers.getInitialSpace(tac);\n const itemSpace = tacHelpers.getSpaceForItem(space, from);\n if (wallMaxX + diff > itemSpace.width) {\n return;\n }\n }\n\n const uprights = superSection.filter(item =>\n productService.isType(item, ITEMS.UPRIGHT)\n );\n\n function getUprightChains(uprights) {\n uprights.sort((a, b) => {\n return a.y - b.y;\n });\n\n return uprights.reduce((chains, upright) => {\n const chainIndex = chains.findIndex(\n chain => chain.x === upright.x && chain.y + chain.height === upright.y\n );\n const chain = chains[chainIndex];\n\n if (chain) {\n chain.height = chain.height + upright.height;\n chains[chainIndex] = chain;\n } else {\n chains.push(upright);\n }\n\n return chains;\n }, []);\n }\n\n const outsiders = tacHelpers\n .filterTac(\n tac,\n superSection.map(item => item.itemid)\n )\n .items.filter(item => productService.isType(item, ITEMS.UPRIGHT));\n const outsiderChains = getUprightChains(outsiders);\n\n const movers = uprights\n .filter(upright => shouldMoveRight(upright, from))\n .map(mover => {\n return { ...mover, x: mover.x + diff };\n });\n\n const blockedRight =\n outsiderChains.some(uprightChain =>\n movers.some(\n mover =>\n /**\n * If collision or jumping past other upright and\n * is encapsulated y-wise by the other upright\n * the selected section will be destroyed, so don't allow\n */\n geometry.collides(uprightChain, mover) ||\n (mover.y >= uprightChain.y &&\n mover.height + mover.y <= uprightChain.height + uprightChain.y &&\n mover.x > uprightChain.x &&\n mover.x - diff < uprightChain.x)\n )\n ) || movers.some(mover => mover.x + mover.width > wallMaxX);\n if (!blockedRight) {\n return 'right';\n } else {\n const movers = uprights\n .filter(upright => shouldMoveLeft(upright, from))\n .map(mover => {\n return { ...mover, x: mover.x - diff };\n });\n\n const blockedLeft =\n outsiderChains.some(uprightChain =>\n movers.some(\n mover =>\n geometry.collides(uprightChain, mover) ||\n (mover.y >= uprightChain.y &&\n mover.height + mover.y <= uprightChain.height + uprightChain.y &&\n mover.x < uprightChain.x &&\n mover.x + diff > uprightChain.x)\n )\n ) || movers.some(mover => mover.x < 0);\n if (!blockedLeft) {\n return 'left';\n }\n }\n}\n\nfunction moveSuperSection(section, superSection, to) {\n const itemsToUpdate = [];\n const uprights = superSection.filter(item =>\n productService.isType(item, ITEMS.UPRIGHT)\n );\n const diff = to.value - section.width;\n\n if (to.moveDirection === 'right') {\n itemsToUpdate.push(\n ...uprights\n .filter(upright => shouldMoveRight(upright, section))\n .map(item => {\n return { ...item, x: item.x + diff };\n })\n .sort((a, b) => (diff > 0 ? b.x - a.x : a.x - b.x))\n );\n } else if (to.moveDirection === 'left') {\n itemsToUpdate.push(\n ...uprights\n .filter(upright => shouldMoveLeft(upright, section))\n .map(item => {\n return { ...item, x: item.x - diff };\n })\n .sort((a, b) => (diff > 0 ? b.x - a.x : a.x - b.x))\n );\n }\n return itemsToUpdate;\n}\n\nfunction getChangeDimension(item) {\n if (productService.isSection(item)) {\n return 'width';\n }\n}\n\nexport default {\n getMoveDirection,\n moveSuperSection,\n getChangeDimension,\n};\n","import productService from '../../../../services/products';\n\nfunction getChangeDimension(item) {\n if (productService.isSection(item)) {\n return 'width';\n }\n}\nexport default { getChangeDimension };\n","import productService from '../../../../services/products';\n\nfunction getChangeDimension(item) {\n if (productService.isSection(item)) {\n return 'width';\n }\n}\n\nexport default { getChangeDimension };\n","import productService from '../../../../services/products';\n\nfunction getChangeDimension(item) {\n if (productService.isSection(item)) {\n return 'width';\n }\n}\n\nexport default { getChangeDimension };\n","import productService from '../../../services/products/';\nimport { applicationSettings } from '../../../settings/application';\nimport Bror from './bror';\nimport Jonaxel from './jonaxel';\nimport Boaxel from './boaxel';\nimport Aurdal from './aurdal';\nimport Ivar from './ivar';\nimport Elvarli from './elvarli';\n\nfunction getRange(name) {\n switch (name) {\n case 'BROR':\n return Bror;\n case 'JONAXEL':\n return Jonaxel;\n case 'BOAXEL':\n return Boaxel;\n case 'AURDAL':\n return Aurdal;\n case 'IVAR':\n return Ivar;\n case 'ELVARLI':\n return Elvarli;\n default:\n console.error('Missing range-specific implementation ');\n return {};\n }\n}\n\nfunction getSelectedChild(item, optionItems, switchingProp) {\n const swappableChildren = item.items.filter(\n childItem =>\n !productService.isMultiParentProduct(childItem) &&\n optionItems.some(optionItem =>\n productService.isType(childItem, optionItem.filter.type)\n )\n );\n\n const hasDivergentItems = !swappableChildren.every(\n childItem =>\n childItem.filter[switchingProp] ===\n swappableChildren[0].filter[switchingProp]\n );\n\n return {\n hasDivergentItems,\n filter: {\n [switchingProp]: swappableChildren[0].filter[switchingProp],\n },\n };\n}\n\nconst rangeApi = getRange(applicationSettings.applicationName);\n\nexport const range = {\n getSelectedChild,\n ...rangeApi,\n};\n","import React from 'react';\nimport PropTypes from 'prop-types';\nimport { connect as reduxConnect } from 'react-redux';\nimport emitter from '../../emitter';\nimport tacHelpers from '../../state/tac/tacHelpers';\nimport productService from '../../services/products';\nimport { CONF_MENU_CHANGE } from '../../settings/events';\nimport { range } from './range';\nimport { selectUseMetric } from '../../state/dexfSettings/dexfSettingsSelectors';\nimport { ITEMS } from '../../constants';\nimport { selectTac } from '../../state/tac/tacSelectors';\nimport {\n thunkAddItem,\n thunkRemoveItem,\n thunkUpdateItem,\n thunkUpdateMultiple,\n} from '../../state/tac/tacThunks';\n\nexport default class AbstractPicker extends React.Component {\n static propTypes = {\n dispatch: PropTypes.func.isRequired,\n tac: PropTypes.object.isRequired,\n };\n\n switch({ from, to, switchingProp, moveOthers = false, targetType }) {\n const { tac } = this.props;\n const superSection = tacHelpers.findSuperSection(tac, from);\n if (to.moveDirection && superSection && superSection.length > 1) {\n const itemsToUpdate = range.moveSuperSection(from, superSection, to);\n if (itemsToUpdate.length) {\n this.props.dispatch(\n thunkUpdateMultiple(itemsToUpdate, tac, {\n origin: 'dialog',\n optionType: to.type,\n optionValue: to.value,\n moveOthers: false,\n priorityItem: from,\n hasDependencies: true,\n })\n );\n }\n emitter.emit(CONF_MENU_CHANGE);\n } else {\n const parent = tacHelpers.getParent(tac, from);\n const slotSources =\n tacHelpers.range.getSlotSources?.({ ...from, id: to.id }, tac) || [];\n const newItem = tacHelpers.getSwitchableItem(from, to, {\n switchingProp,\n slotSources,\n targetType,\n });\n if (\n targetType === ITEMS.SECTION &&\n tacHelpers.range.isIncompleteSection(newItem)\n ) {\n const neighbours = tacHelpers.range.getNeighbouringSections(\n tac,\n newItem\n );\n\n this.props.dispatch(\n thunkUpdateMultiple([newItem, ...neighbours], tac, {\n origin: 'dialog',\n optionType: to.type,\n optionValue: to.value,\n moveOthers,\n forcePartsOnFirstItem: true,\n triggerItem: newItem,\n })\n );\n } else {\n this.props.dispatch(\n thunkUpdateItem(newItem, parent, {\n origin: 'dialog',\n optionType: to.type,\n optionValue: to.value,\n moveOthers,\n })\n );\n }\n emitter.emit(CONF_MENU_CHANGE);\n }\n }\n\n add(parent, slot, childId) {\n const product = productService.getProduct(childId);\n const offset = tacHelpers.getGlobalCoords(slot.parent, this.props.tac) || {\n x: 0,\n y: 0,\n z: 0,\n };\n\n const newItem = {\n ...product,\n x: slot.x - offset.x,\n y: slot.y - offset.y,\n z: slot.z - offset.z,\n };\n\n this.props.dispatch(\n thunkAddItem(newItem, parent, {\n origin: 'dialog',\n optionType: newItem.type,\n })\n );\n emitter.emit(CONF_MENU_CHANGE);\n }\n\n removeChildren(parent, type) {\n parent.items &&\n parent.items\n .filter(child => child.type === type)\n .forEach(item => {\n this.props.dispatch(\n thunkRemoveItem(item, undefined, {\n origin: 'dialog',\n optionType: type,\n })\n );\n emitter.emit(CONF_MENU_CHANGE);\n });\n }\n\n isSelected(option, selected, filterProp) {\n if (option.id === selected.id && option.modelid === selected.modelid) {\n return true;\n }\n if (\n !selected.hasDivergentItems &&\n option.value === selected.filter[filterProp]\n ) {\n return true;\n }\n\n return false;\n }\n}\n\nexport const connect = reduxConnect(state => ({\n tac: selectTac(state),\n useMetricMeasures: selectUseMetric(state),\n}));\n","import React from 'react';\nimport styles from './ConfColorPicker.module.less';\nimport AbstractPicker, { connect } from '../AbstractPicker';\nimport { translate } from '../../../services/L10n';\nimport { t } from '../../../translations';\nimport CircleOption from '../../CircleOption';\n\nconst sortOrder = {\n insert: [\n 'black_mesh',\n 'mesh_white',\n 'metal_white',\n 'metal_anthracite',\n 'wire_white',\n 'wire_anthracite',\n 'black',\n 'grey_green',\n 'wood',\n 'white',\n 'anthracite',\n 'grey',\n 'oak',\n 'dark_grey',\n 'bamboo',\n ],\n section: ['wood', 'black', 'grey_green', 'white', 'dark_grey'],\n item: [\n 'black_mesh',\n 'mesh_white',\n 'black',\n 'grey_green',\n 'wood',\n 'white',\n 'anthracite',\n 'dark_grey',\n 'light_blue',\n 'white_stained_oak_effect',\n 'black_brown',\n 'bamboo',\n ],\n};\n\nclass ConfColorPicker extends AbstractPicker {\n state = {\n options: [],\n };\n\n tooltips = {\n white: translate(t.TOOLTIP_WHITE),\n red: translate(t.TOOLTIP_RED),\n wood: translate(t.TOOLTIP_WOOD),\n metal_white: `${translate(t.TOOLTIP_METAL)}/${translate(t.TOOLTIP_WHITE)}`,\n metal_anthracite: `${translate(t.TOOLTIP_METAL)}/${translate(\n t.TOOLTIP_ANTHRACITE\n )}`,\n black: translate(t.TOOLTIP_BLACK),\n wire_anthracite: `${translate(t.TOOLTIP_WIRE_SHELF)}/${translate(\n t.TOOLTIP_ANTHRACITE\n )}`,\n grey_green: translate(t.TOOLTIP_GREY_GREEN),\n wire_white: `${translate(t.TOOLTIP_WIRE_SHELF)}/${translate(\n t.TOOLTIP_WHITE\n )}`,\n oak: translate(t.TOOLTIP_OAK_PATTERNED),\n white_stained_oak_effect: translate(t.TOOLTIP_WHITE_OAK),\n light_blue: translate(t.TOOLTIP_LIGHT_BLUE),\n grey: translate(t.TOOLTIP_GREY),\n anthracite: translate(t.TOOLTIP_ANTHRACITE),\n dark_grey: translate(t.TOOLTIP_DARK_GREY),\n black_brown: translate(t.TOOLTIP_BLACKBROWN),\n mesh_grey: translate(t.TOOLTIP_GREY_MESH),\n mesh_white: translate(t.TOOLTIP_MESH),\n mesh_green: translate(t.TOOLTIP_GREEN_MESH),\n grey_green_mesh: translate(t.TOOLTIP_GREY_GREEN_MESH),\n black_mesh: translate(t.TOOLTIP_BLACK_MESH),\n pine: translate(t.TOOLTIP_PINE),\n bamboo: translate(t.TOOLTIP_BAMBOO),\n };\n\n itemsAsOptions() {\n const colorOptions = new Set();\n return this.props.items\n .map(item => ({\n value: item.filter.color,\n id: item.id,\n type: item.filter.type,\n }))\n .reduce((options, currentOption) => {\n if (!colorOptions.has(currentOption.value)) {\n colorOptions.add(currentOption.value);\n options.push(currentOption);\n }\n return options;\n }, [])\n .sort((a, b) => {\n if (sortOrder[this.props.sortOrder]) {\n return (\n sortOrder[this.props.sortOrder].indexOf(a.value) -\n sortOrder[this.props.sortOrder].indexOf(b.value)\n );\n }\n return 1;\n });\n }\n\n render() {\n const options = this.itemsAsOptions();\n const selected = this.props.selected;\n return (\n
\n {this.props.showHeadline && (\n
{translate(t.COLOUR)}:
\n )}\n
\n
\n {options.map((option, index) => (\n \n this.switch({\n from: this.props.item,\n to: option,\n switchingProp: 'color',\n targetType: this.props.targetType,\n })\n }\n tooltipContent={this.tooltips?.[option.value]}\n dataTestId={`color-picker-${index}`}\n />\n ))}\n
\n
\n
\n );\n }\n}\n\nexport default connect(ConfColorPicker);\n","import { config as asConfig } from '../boaxel/AdjustableConfig';\nimport { metricToImperial } from '../../util/measures';\nimport { selectUseMetric } from '../../state/dexfSettings/dexfSettingsSelectors';\nimport store from '../../state';\n\nexport function getWidthText(itemContext) {\n function localUnit(sectionWidth) {\n const useMetricMeasures = selectUseMetric(store.getState());\n return useMetricMeasures\n ? Math.ceil(sectionWidth / 10)\n : metricToImperial(sectionWidth).onlyInches;\n }\n\n if (itemContext.item.width > asConfig.section.maxWidth) {\n return localUnit(itemContext.item.width);\n }\n const min = localUnit(asConfig.section.minWidth);\n const max = localUnit(asConfig.section.maxWidth);\n return `${min} - ${max}`;\n}\n","import React from 'react';\nimport classNames from 'classnames';\n\nimport styles from './ConfSizePicker.module.less';\nimport AbstractPicker, { connect } from '../AbstractPicker';\nimport { translate } from '../../../services/L10n';\nimport { t } from '../../../translations';\nimport { metricToImperial } from '../../../util/measures';\nimport { range } from '../range';\nimport { getWidthText } from '../../../scene/util/dimensionDisplay';\n\nclass ConfSizePicker extends AbstractPicker {\n state = {\n options: [],\n };\n\n getLabelText(dimensionValue) {\n const { dimension, useMetricMeasures } = this.props;\n\n let dimensionLabel;\n let displayValue = useMetricMeasures\n ? dimensionValue / 10\n : metricToImperial(dimensionValue).onlyInches;\n switch (dimension) {\n case 'depth':\n dimensionLabel = translate(t.SHORT_LABEL_DEPTH);\n break;\n case 'width':\n dimensionLabel = translate(t.SHORT_LABEL_WIDTH);\n displayValue = getWidthText({\n ...this.props,\n item: { width: dimensionValue },\n });\n break;\n default:\n break;\n }\n\n return `${dimensionLabel} ${displayValue} ${\n this.props.useMetricMeasures\n ? translate(t.MEASURE_VALUE_CM)\n : translate(t.MEASURE_VALUE_IN)\n }`;\n }\n\n itemsAsOptions() {\n const { dimension } = this.props;\n\n return this.props.items.map(item => ({\n display: item[dimension],\n value: item.filter[dimension],\n id: item.id,\n type: item.filter.type,\n }));\n }\n\n render() {\n const { selected, dimension, wrapped } = this.props;\n const options = this.itemsAsOptions().sort((a, b) =>\n a.value < b.value ? 1 : a.value > b.value ? -1 : 0\n );\n options.forEach(option => {\n option.moveDirection = range.getMoveDirection(\n selected,\n option,\n dimension,\n this.props.tac\n );\n });\n\n return (\n
\n {options.map((option, index) => {\n const disabled =\n this.props.disabled ||\n !(option.id === selected.id || option.moveDirection);\n\n return (\n
\n \n \n this.switch({\n from: selected,\n to: option,\n switchingProp: dimension,\n })\n }\n />\n {this.getLabelText(option.display)}\n \n
\n );\n })}\n
\n );\n }\n}\n\nexport default connect(ConfSizePicker);\n","import React from 'react';\nimport classNames from 'classnames';\n\nimport styles from './ConfVariantPicker.module.less';\nimport AbstractPicker, { connect } from '../AbstractPicker';\nimport { translate } from '../../../services/L10n';\nimport CircleOption from '../../CircleOption';\nimport { Icon } from '../../Icon';\nimport Tooltip from '../../Popup/Tooltip';\nimport { TOP } from '../../Popup/alignments';\nimport { t } from '../../../translations';\n\nclass ConfVariantPicker extends AbstractPicker {\n state = {\n options: [],\n };\n\n icons = {\n wire: {\n tooltip: translate(t.TOOLTIP_TYPE_WIRE),\n type: 'select',\n sort: 1,\n },\n metal: {\n tooltip: translate(t.TOOLTIP_METAL),\n type: 'select',\n sort: 2,\n },\n particleboard: {\n tooltip: translate(t.TOOLTIP_TYPE_PARTICLEBOARD),\n type: 'select',\n sort: 3,\n },\n mesh: {\n tooltip: translate(t.TOOLTIP_TYPE_MESH),\n type: 'select',\n sort: 2,\n },\n mesh_anthracite: {\n tooltip: translate(t.TOOLTIP_MESH_ANTHRACITE),\n type: 'select',\n sort: 2,\n },\n textile: {\n tooltip: translate(t.TOOLTIP_TEXTILE),\n type: 'select',\n sort: 3,\n },\n pads: {\n iconName: 'WheelsIcon',\n tooltip: translate(t.TOOLTIP_REMOVE_CASTORS),\n type: 'select',\n highlighted: true,\n sort: 1,\n },\n castors: {\n iconName: 'WheelsIcon',\n tooltip: translate(t.TOOLTIP_ADD_CASTORS),\n type: 'select',\n sort: 2,\n },\n empty: {\n iconName: 'DoorsIcon',\n tooltip: translate(t.TOOLTIP_REMOVE_CABINET),\n type: 'toggle',\n highlighted: true,\n },\n doors: {\n iconName: 'DoorsIcon',\n tooltip: translate(t.TOOLTIP_ADD_CABINET),\n type: 'toggle',\n },\n short: {\n iconName: 'RotateLeftIcon',\n tooltip: translate(t.TOOLTIP_ROTATE),\n type: 'toggle',\n },\n narrow: {\n iconName: 'RotateLeftIcon',\n tooltip: translate(t.TOOLTIP_ROTATE),\n type: 'toggle',\n },\n };\n\n itemsAsOptions(items, includeColor) {\n return items\n .map(item => ({\n value: item.filter.variant,\n color: includeColor ? item.filter.color : null,\n id: item.id,\n modelid: item.modelid,\n type: item.filter.type,\n }))\n .sort((a, b) =>\n this.icons[a.value].sort > this.icons[b.value].sort ? 1 : -1\n );\n }\n\n render() {\n const { selected, disabled, moveOthers, useColorSpecificIcon } = this.props;\n const options = this.itemsAsOptions(this.props.items, useColorSpecificIcon);\n return (\n
\n {this.props.showHeadline && (\n
\n {translate(t.TYPE)}:\n
\n )}\n 1 ? styles.radioButton : styles.toggle\n )}\n >\n {options.map((option, index) => {\n const highlighted =\n (options.length > 1 &&\n this.isSelected(option, selected, 'variant')) ||\n this.icons[option.value].highlighted;\n\n if (this.icons[option.value].iconName) {\n return (\n \n this.switch({\n from: selected,\n to: option,\n switchingProp: 'variant',\n moveOthers,\n })\n }\n >\n \n \n \n
\n );\n }\n\n return (\n \n this.switch({\n from: selected,\n to: option,\n switchingProp: 'variant',\n moveOthers,\n })\n }\n tooltipContent={this.icons[option.value].tooltip}\n dataTestId={`variant-picker-${index}`}\n />\n );\n })}\n \n \n );\n }\n}\n\nexport { ConfVariantPicker };\nexport default connect(ConfVariantPicker);\n","import { useRef, useEffect, useState } from 'react';\n\nconst useHover = (initialValue = false) => {\n const [value, setValue] = useState(initialValue);\n const ref = useRef(null);\n\n const handleMouseOver = () => setValue(true);\n const handleMouseOut = () => setValue(false);\n\n useEffect(() => {\n const node = ref.current;\n if (node) {\n node.addEventListener('mouseenter', handleMouseOver);\n node.addEventListener('mouseleave', handleMouseOut);\n return () => {\n node.removeEventListener('mouseenter', handleMouseOver);\n node.removeEventListener('mouseleave', handleMouseOut);\n };\n }\n }, [ref.current]);\n return [ref, value];\n};\n\nexport default useHover;\n","import React, { useState } from 'react';\nimport { connect } from 'react-redux';\nimport PropTypes from 'prop-types';\nimport bowser from 'bowser';\nimport { translate } from '../../services/L10n';\nimport { t } from '../../translations';\nimport _ from 'lodash';\nimport { selectUseStaticToolTip } from '../../state/userAgent/userAgentSelectors';\nimport useHover from '../../hooks/useHover';\nimport {\n selectUseMetric,\n selectWriteDirection,\n} from '../../state/dexfSettings/dexfSettingsSelectors';\nimport styles from './Slider.module.less';\nimport { getMeasurementBySoM } from '../../services/products/productHandler';\n\nconst Slider = ({\n direction,\n useStaticToolTip,\n onRelease,\n disabled,\n useMetricMeasures,\n minVal,\n maxVal,\n step,\n initialValue,\n}) => {\n const [value, setValue] = useState(initialValue);\n const [input, showTooltip] = useHover(useStaticToolTip);\n const unit = useMetricMeasures\n ? translate(t.MEASURE_VALUE_CM)\n : translate(t.MEASURE_VALUE_IN);\n const valueMax = maxVal - (maxVal % step);\n const valueMin = minVal + (step - (minVal % step));\n const getDisplayValues = {\n min: getMeasurementBySoM(useMetricMeasures, minVal),\n max: getMeasurementBySoM(useMetricMeasures, maxVal),\n current: `${getMeasurementBySoM(useMetricMeasures, value)} ${unit}`,\n };\n\n const _onRelease = () => onRelease(value);\n\n const inputRange = _.range(valueMin, valueMax, step);\n inputRange.push(valueMax);\n\n if (maxVal !== inputRange[inputRange.length - 1]) {\n inputRange.push(maxVal);\n }\n if (minVal !== inputRange[0]) {\n inputRange.unshift(minVal);\n }\n\n /**\n * Handles value change\n * @param value\n */\n const onChange = ({ target: { value } }) => setValue(inputRange[value]);\n\n const sliderMax = inputRange.length - 1;\n\n let inputValue;\n if (!inputRange.includes(value)) {\n let closestValue, minDelta;\n inputRange.forEach(rangeValue => {\n const delta = Math.abs(value - rangeValue);\n if (!minDelta || delta < minDelta) {\n minDelta = delta;\n closestValue = rangeValue;\n }\n });\n\n inputValue = inputRange.indexOf(closestValue);\n } else {\n inputValue = inputRange.indexOf(value);\n }\n\n const getSliderStyle = (sliderMax, inputValue) => {\n if (bowser.msie || bowser.msedge) return {};\n if (disabled) {\n return {\n backgroundColor: '#ccc',\n height: '2px',\n };\n }\n const val = inputValue / sliderMax;\n\n return {\n backgroundColor: '#ccc',\n backgroundImage: `linear-gradient(180deg, white 0 33%, transparent 33% 66%, white 66% 100%),\n linear-gradient(to ${direction}, #407ab1 0 ${val * 100}%, #ccc ${\n val * 100\n }% 100%)`,\n height: '6px',\n };\n };\n\n const getTooltipPosition = (sliderMax, inputValue) => {\n const tooltipPos = (inputValue / sliderMax) * 100;\n\n if (direction === 'right') {\n return {\n right: `${tooltipPos}%`,\n };\n } else {\n return {\n left: `${tooltipPos}%`,\n };\n }\n };\n\n /**\n * Renders tooltip element\n *\n * @returns {JSX.Element}\n */\n const renderTooltip = () =>\n showTooltip && (\n
\n \n {getDisplayValues.current}\n
\n \n
\n );\n\n /**\n * Renders slider element\n *\n * @returns {JSX.Element}\n */\n const renderSlider = () => (\n
\n \n
\n );\n\n return (\n
\n {!disabled && {getDisplayValues.min}}\n
\n {renderTooltip()}\n {renderSlider()}\n
\n {!disabled && {getDisplayValues.max}}\n
\n );\n};\n\nSlider.propTypes = {\n disabled: PropTypes.bool.isRequired,\n minVal: PropTypes.number.isRequired,\n maxVal: PropTypes.number.isRequired,\n initialValue: PropTypes.number.isRequired,\n step: PropTypes.number,\n onRelease: PropTypes.func.isRequired,\n useStaticToolTip: PropTypes.bool.isRequired,\n useMetricMeasures: PropTypes.bool.isRequired,\n direction: PropTypes.string.isRequired,\n};\n\nexport default connect(state => ({\n useStaticToolTip: selectUseStaticToolTip(state),\n useMetricMeasures: selectUseMetric(state),\n direction: selectWriteDirection(state),\n}))(Slider);\n","import React from 'react';\nimport PropTypes from 'prop-types';\nimport AbstractPicker, { connect } from '../AbstractPicker';\nimport Slider from '../../Slider/Slider';\nimport Tooltip from '../../Popup/Tooltip';\nimport { TOP } from '../../Popup/alignments';\nimport { replace } from '../../../state/tac/replace';\nimport emitter from '../../../emitter';\nimport { CONF_MENU_CHANGE } from '../../../settings/events';\nimport tacHelpers from '../../../state/tac/tacHelpers';\nimport productService from '../../../services/products';\nimport { ceil } from '../../../util/round';\nimport styles from './ConfExtendableSlider.module.less';\nimport { range } from '../range';\nimport { ITEMS } from '../../../constants';\nimport {\n thunkUpdateItem,\n thunkUpdateMultiple,\n} from '../../../state/tac/tacThunks';\nimport localStatisticsReporter from '../../../services/statistics/insights/custom/local/localStatisticsReporter';\n\nclass ConfExtendableSlider extends AbstractPicker {\n static propTypes = {\n limits: PropTypes.object.isRequired,\n item: PropTypes.object.isRequired,\n dispatch: PropTypes.func.isRequired,\n topAncestor: PropTypes.object.isRequired,\n tac: PropTypes.object.isRequired,\n mountOffset: PropTypes.number.isRequired,\n useMetricMeasures: PropTypes.bool.isRequired,\n };\n\n reportChange(width, itemId) {\n localStatisticsReporter.reportConfMenuSliderChange(width, itemId);\n }\n\n onNegativeCrDiff = (diff, chain, newWidth, sidewallConnections = {}) => {\n const { topAncestor, dispatch, tac } = this.props;\n\n const extCrs = tacHelpers\n .getClothesRails({ items: [topAncestor] })\n .filter(item => productService.isExtendable(item));\n\n const moveLeft = Math.ceil(diff / 2);\n const moveRight = Math.floor(diff / 2);\n\n const itemsToUpdate = [];\n const newLeftEnd = Object.assign(\n {},\n chain.leftEnd,\n sidewallConnections.left\n ? {}\n : {\n x: chain.leftEnd.x - (sidewallConnections.right ? diff : moveLeft),\n }\n );\n\n if (newLeftEnd.itemid === topAncestor.itemid) {\n extCrs.forEach(cr => {\n const newCrItem = Object.assign({}, cr, { width: newWidth });\n replace(newLeftEnd.items, newCrItem);\n });\n } else {\n const newTopAncestor = Object.assign({}, topAncestor);\n\n extCrs.forEach(cr => {\n const newCrItem = Object.assign({}, cr, { width: newWidth });\n replace(newTopAncestor.items, newCrItem);\n });\n\n itemsToUpdate.push(newTopAncestor);\n }\n\n const newRightEnd = Object.assign(\n {},\n chain.rightEnd,\n sidewallConnections.right\n ? {}\n : {\n x: chain.rightEnd.x + (sidewallConnections.left ? diff : moveRight),\n }\n );\n\n itemsToUpdate.push(newLeftEnd, newRightEnd);\n\n dispatch(\n thunkUpdateMultiple(itemsToUpdate, tac, {\n origin: 'dialog',\n optionValue: newWidth,\n moveOthers: true,\n triggerItem: newLeftEnd,\n })\n );\n };\n\n onPositiveCrDiff = (diff, chain, newWidth, sidewallConnections = {}) => {\n const { item, topAncestor, dispatch, tac } = this.props;\n\n const newCrItem = Object.assign({}, item, { width: newWidth });\n\n const moveLeft = Math.floor(diff / 2);\n const moveRight = Math.ceil(diff / 2);\n\n let newTopAncestor;\n\n if (!sidewallConnections.left) {\n const space = tacHelpers.getSpace(tac);\n\n const rightOverflow =\n chain.rightEnd.x + chain.rightEnd.width + moveRight - space.width;\n\n const leftOverflow = chain.leftEnd.x - moveLeft - space.x;\n\n newTopAncestor = Object.assign({}, topAncestor, {\n x: Math.max(\n topAncestor.x -\n moveLeft -\n Math.max(rightOverflow, 0) -\n Math.min(leftOverflow, 0),\n space.x\n ),\n });\n } else {\n newTopAncestor = Object.assign({}, topAncestor);\n }\n\n replace(newTopAncestor.items, newCrItem);\n\n dispatch(\n thunkUpdateItem(newTopAncestor, tac, {\n origin: 'dialog',\n optionValue: newWidth,\n moveOthers: true,\n triggerItem: newTopAncestor,\n })\n );\n };\n\n onRelease = value => {\n const { item, topAncestor, tac, limits, mountOffset } = this.props;\n\n // Diff needs to be even mm\n let newWidth = parseInt(value);\n\n // Slider doesn't know that item can actually be a bit wider\n newWidth += mountOffset * 2;\n\n if (newWidth < limits.min) {\n newWidth = limits.min;\n } else if (newWidth > limits.max) {\n newWidth = limits.max;\n }\n\n const diff = newWidth - item.width;\n\n if (diff !== 0) {\n if (productService.isType(item, ITEMS.CLOTHES_RAIL)) {\n const chain = tacHelpers.getClothesRailChain(tac, topAncestor, item);\n\n const sidewallConnections = {\n left: productService.isType(chain.leftEnd, 'sidewall'),\n right: productService.isType(chain.rightEnd, 'sidewall'),\n };\n if (diff > 0) {\n this.onPositiveCrDiff(diff, chain, newWidth, sidewallConnections);\n } else {\n this.onNegativeCrDiff(diff, chain, newWidth, sidewallConnections);\n }\n\n this.reportChange(newWidth, item.id);\n emitter.emit(CONF_MENU_CHANGE);\n } else if (productService.isType(item, ITEMS.SECTION)) {\n const updItem = { ...item, width: newWidth, value: newWidth };\n updItem.moveDirection = range.getMoveDirection(\n item,\n updItem,\n 'width',\n tac\n );\n this.reportChange(newWidth, item.id);\n this.switch({ from: item, to: updItem, switchingProp: 'width' });\n }\n }\n };\n\n render() {\n const { item, limits, useMetricMeasures, mountOffset } = this.props;\n const itemWidth = item.width - mountOffset * 2;\n\n const displayedWidth = useMetricMeasures\n ? ceil(itemWidth, 10)\n : ceil(itemWidth, 1);\n\n return (\n \n
\n = limits.displayedMax}\n minVal={limits.displayedMin}\n maxVal={limits.displayedMax}\n initialValue={displayedWidth}\n step={useMetricMeasures ? 10 : 25.4 / 2}\n onRelease={this.onRelease}\n />\n
\n
\n );\n }\n}\n\nexport default connect(ConfExtendableSlider);\n","import { ConfVariantPicker } from './ConfVariantPicker';\nimport { connect as abstractPickerConnect } from './AbstractPicker';\n\nclass ConfVariantToggler extends ConfVariantPicker {\n itemsAsOptions(items) {\n return [\n super.itemsAsOptions(items).find(item => {\n if (!this.isSelected(item, this.props.selected, 'variant')) {\n return true;\n } else if (\n this.props.selected.id !== 'dont select' &&\n item.id !== 'dont select' &&\n item.id !== this.props.selected.id\n ) {\n return true;\n }\n return false;\n }),\n ];\n }\n}\n\nexport default abstractPickerConnect(ConfVariantToggler);\n","import React from 'react';\n\nimport styles from './AddonPicker.module.less';\nimport AbstractPicker, { connect } from '../AbstractPicker';\nimport { translate } from '../../../services/L10n';\nimport { Icon } from '../../Icon';\nimport Tooltip from '../../Popup/Tooltip';\nimport { TOP } from '../../Popup/alignments';\nimport classnames from 'classnames';\nimport { t } from '../../../translations';\n\nclass AddonPicker extends AbstractPicker {\n constructor(props) {\n super(props);\n\n this.state = { enabled: !!props.selected };\n this.onClick = this.onClick.bind(this);\n this.addableItem = props.items && props.items[0];\n }\n\n icons = {\n cover: {\n iconName: 'CoverIcon',\n tooltip: {\n enabled: translate(t.TOOLTIP_REMOVE_COVER),\n disabled: translate(t.TOOLTIP_ADD_COVER),\n },\n },\n };\n\n onClick() {\n const { parent, slot, addonType } = this.props;\n\n !this.state.enabled\n ? this.add(parent, slot, this.addableItem)\n : this.removeChildren(parent, addonType);\n this.setState({ enabled: !this.state.enabled });\n }\n\n render() {\n const { addonType } = this.props;\n return (\n \n
\n \n
\n \n );\n }\n}\n\nexport default connect(AddonPicker);\n","import React from 'react';\n\nimport ConfColorPicker from './ConfColorPicker';\nimport ConfSizePicker from './ConfSizePicker';\nimport ConfVariantPicker from './ConfVariantPicker';\nimport ConfExtendableSlider from './ConfExtendableSlider';\nimport ConfVariantToggler from './ConfVariantToggler';\nimport AddonPicker from './AddonPicker';\n\nimport productService from '../../services/products';\nimport tacHelpers from '../../state/tac/tacHelpers';\nimport { unique } from '../../util/array';\nimport updateItem from '../../state/tac/tacReducer/updateItem';\nimport constants from '../../settings/constants';\nimport { round } from '../../util/round';\nimport { range } from './range';\nimport { isSection } from '../../services/products';\nimport { translate } from '../../services/L10n';\nimport styles from './ConfMenu.module.less';\nimport _ from 'lodash';\nimport { getUserAgent, isMobile } from '../../util/userAgent';\nimport getItemConfig from '../../scene/util/getItemConfig';\nimport { isFloorStanding, isWallMounted } from '../../services/products/models';\nimport { ITEMS } from '../../constants';\nimport { t } from '../../translations';\n\nconst selections = {\n section: {\n key: ITEMS.SECTION,\n header: 'POPUP_MENU_HEADER_SECTION',\n order: 1,\n },\n frame: { key: ITEMS.FRAME, header: 'POPUP_MENU_HEADER_FRAME', order: 2 },\n basket: { key: ITEMS.BASKET, header: 'POPUP_MENU_HEADER_BASKETS', order: 3 },\n insert: { key: 'insert', header: 'POPUP_MENU_HEADER_INSERTS', order: 4 },\n};\n\nfunction rotationAllowed(currentItem, rotatedItem, tac) {\n const newItem = tacHelpers.getSwitchableItem(\n currentItem,\n {\n ...rotatedItem,\n value: rotatedItem.filter.variant,\n type: rotatedItem.filter.type,\n },\n {\n switchingProp: 'variant',\n }\n );\n\n const parent = tacHelpers.getParent(tac, currentItem);\n const rotationIsValid = updateItem(tac, newItem, parent, {\n moveOthers: true,\n });\n return rotationIsValid;\n}\n\nfunction getChildOptions(item) {\n const optionItems = item.items\n .filter(\n item =>\n productService.shouldHaveColorSelector(item) &&\n !productService.isMultiParentProduct(item)\n )\n .map(childItem =>\n productService.getSwappables(childItem, {\n depth: childItem.filter.depth,\n width: childItem.filter.width,\n })\n )\n .filter(childSwappables => childSwappables && childSwappables.length > 1)\n .flat()\n .map(swappable => ({ ...swappable, id: item.id }));\n\n return { optionItems };\n}\n\nfunction multiSelector(currentItem, tac, type, filterProp) {\n let optionItems = [];\n let defaultProduct;\n\n const usedProductsOfType = currentItem.items\n ? tacHelpers.getAllItems(currentItem.items).filter(item => {\n return productService.isType(item, type);\n })\n : [];\n\n const usedPeers = usedProductsOfType\n .map(product => product.filter[filterProp])\n .filter(unique);\n\n // if there are no items of {type}, then there's nothing to change\n // leave optionItems empty\n if (usedProductsOfType.length > 0) {\n const optionToChangeIsValid = usedProductsOfType.every(product => {\n /*\n Castors is a swappable for pads, but it won't fit on all of the same parent (Lex shelving units).\n Therefore we need to validate that all swappables actually have a valid slot on the parent,\n given that the parent were to be cleared of all children with the same type.\n */\n const currentItemAndDescendants = tacHelpers.getAllItems([currentItem]);\n const section = currentItemAndDescendants.find(isSection);\n if (section) {\n Array.prototype.push.apply(\n currentItemAndDescendants,\n tacHelpers.range.getSlotSources(section, tac)\n );\n }\n\n const itemsOfType = currentItemAndDescendants.filter(item =>\n productService.isType(item, type)\n );\n const filteredTac = tacHelpers.filterTac(\n { items: [currentItem] },\n itemsOfType.map(item => item.itemid)\n );\n\n const swappables = productService.getSwappables(product, {\n depth: product.filter.depth,\n width: product.filter.width,\n });\n if (!productService.isType(product, 'leg') && swappables.length > 1) {\n return true;\n }\n\n return swappables.every(swappable => {\n return tacHelpers.getOpenSlots(filteredTac, swappable, {\n noVariants: true,\n }).length;\n });\n });\n if (optionToChangeIsValid) {\n defaultProduct = usedProductsOfType[0];\n optionItems = productService\n .getSwappables(defaultProduct, {\n depth: defaultProduct.filter.depth,\n width: defaultProduct.filter.width,\n })\n .map(defaultProduct => defaultProduct.filter)\n .map(peer => ({\n ...currentItem,\n ...{\n filter: {\n [filterProp]: peer[filterProp],\n type: peer.type,\n },\n },\n }));\n }\n }\n\n if (optionItems.length > 1) {\n const selected = defaultProduct\n ? {\n ...currentItem,\n id: 'dont select',\n hasDivergentItems: usedPeers.length > 1,\n filter: defaultProduct.filter,\n }\n : currentItem;\n\n return { selected, optionItems };\n }\n}\n\nfunction colorSelector(item, tac) {\n const optionItems = [];\n let items = [];\n\n const defaultFilters = {\n width: item.filter.width,\n depth: item.filter.depth,\n ...(tacHelpers.getTopAncestor(tac, item).itemid === item.itemid && {\n ...(isWallMounted(item) && item.y > 0 && { wall: true }),\n ...(isFloorStanding(item) && item.y === 0 && { floor: true }),\n }),\n };\n\n if (productService.shouldHaveColorSelector(item)) {\n if (productService.isFrame(item)) {\n items = productService.getSwappables(item, {\n ...defaultFilters,\n height: item.filter.height,\n variant: item.filter.variant,\n });\n\n const swappablesChildren = item.items\n .filter(\n childItem =>\n productService.isType(childItem, [\n ITEMS.SHELVING_UNIT,\n ITEMS.FRAME,\n ]) && item.id !== childItem.id\n )\n .flatMap(childItem =>\n productService.getSwappables(childItem, {\n ...defaultFilters,\n height: childItem.filter.height,\n variant: childItem.filter.variant,\n })\n );\n\n const canSwapAllChildren =\n swappablesChildren.length === 0 ||\n items.every(swappableItem =>\n swappablesChildren.find(\n child => child.filter.color === swappableItem.filter.color\n )\n );\n\n if (items.length > 1 && !canSwapAllChildren) {\n items = [];\n } else {\n item.hasDivergentItems = item.items\n .filter(childItem => productService.isFrame(childItem))\n .some(childItem => childItem.filter.color !== item.filter.color);\n }\n } else if (productService.isSection(item)) {\n items = productService.getSwappables(item, {\n ...defaultFilters,\n height: item.filter.height,\n });\n item.hasDivergentItems = tacHelpers.range.isMultiColouredSection(\n item,\n tac\n );\n } else if (productService.isUpright(item)) {\n items = productService.getSwappables(item, {\n ...defaultFilters,\n height: item.filter.height,\n });\n } else if (productService.isTrolley(item)) {\n items = productService.getSwappables(item, {\n ...defaultFilters,\n variant: item.filter.variant,\n });\n } else {\n items = productService.getSwappables(item, {\n ...defaultFilters,\n });\n }\n if (items.length > 1) {\n optionItems.push({\n selection: (() => {\n if (productService.isFrame(item)) {\n return selections.frame.key;\n } else if (productService.isSection(item)) {\n return selections.section.key;\n }\n\n return selections.insert.key;\n })(),\n component: (\n {\n if (productService.isSection(item)) {\n return ITEMS.SECTION;\n } else if (\n productService.isType([\n ITEMS.SHELF,\n ITEMS.METAL_SHELF,\n ITEMS.WIRE_SHELF,\n ])\n ) {\n return 'insert';\n }\n return ITEMS.ITEM;\n })()}\n item={item}\n selected={item}\n items={items}\n targetType={item.filter.type}\n />\n ),\n });\n }\n }\n\n if (productService.shouldHaveChildColorSelector(item)) {\n const selector = getChildOptions(item);\n\n if (selector?.optionItems.length) {\n optionItems.push({\n selection: selections.insert.key,\n component: (\n \n ),\n });\n }\n }\n\n return optionItems;\n}\n\nfunction extendableSlider(item, tac) {\n if (!productService.isExtendable(item)) {\n return;\n }\n\n const limits = tacHelpers.extendableLimits(tac, item);\n\n if (!limits) {\n return;\n }\n\n const config = getItemConfig(item);\n const mountOffset = config?.mountOffset || 0;\n\n //Round to cm and remove possible mounting width\n limits.displayedMin = round(limits.min - mountOffset * 2, 10);\n limits.displayedMax = round(limits.max - mountOffset * 2, 10);\n\n const topAncestor = tacHelpers.getTopAncestor(tac, item);\n\n return (\n limits && {\n selection: productService.isSection(item)\n ? selections.section.key\n : selections.insert.key,\n component: (\n \n ),\n }\n );\n}\n\nfunction sizeSelector(section) {\n if (!productService.isSection(section)) {\n return;\n }\n\n const dimension = range.getChangeDimension(section);\n\n const otherDimensions = ['width', 'height', 'depth'].filter(\n dim => dim !== dimension\n );\n\n const filter = { color: section.filter.color, switchable: true };\n\n otherDimensions.forEach(dim => (filter[dim] = section.filter[dim]));\n\n const sections = productService.getSwappables(section, filter);\n\n let validSections = sections;\n if (section.items && section.items.length !== 0) {\n validSections = sections.filter(function (cand) {\n if (cand.filter[dimension] === section.filter[dimension]) {\n // this is the original section so it obviously works\n return true;\n }\n if (cand.logic.extendable) {\n // we allow switching to extendable even if stuff wont fit\n return true;\n }\n\n const dimensionAxisMap = {\n width: 'x',\n height: 'y',\n depth: 'z',\n };\n // detect articles that can't be replaced\n return section.items.every(function (part) {\n const offset = productService.getSectionOffset(part);\n const partFilter = {\n [dimension]:\n cand.filter[dimension] - offset[dimensionAxisMap[dimension]] * 2,\n color: part.filter.color,\n };\n otherDimensions.forEach(dim => (partFilter[dim] = part.filter[dim]));\n\n return productService.getSwappables(part, partFilter).length > 0;\n });\n });\n }\n\n if (sections.length > 1) {\n return {\n selection: selections.section.key,\n component: (\n \n ),\n };\n }\n}\n\nfunction variantSelector(item, tac) {\n const variantSelectors = [];\n\n if (productService.isTrolley(item)) {\n const trolleys = productService.getSwappables(item, {\n color: item.filter.color,\n });\n\n if (trolleys.length > 1) {\n return {\n selection: selections.insert.key,\n component: (\n \n ),\n };\n }\n } else if (\n productService.isType(item, ITEMS.BASKET) &&\n !productService.shouldHaveColorSelector(item)\n ) {\n const baskets = productService.getSwappables(item, {\n depth: item.filter.depth,\n width: item.filter.width,\n });\n\n if (baskets.length > 1) {\n variantSelectors.push({\n selection: selections.basket.key,\n component: (\n \n ),\n });\n }\n } else if (\n productService.isType(item, [\n ITEMS.SHELF,\n ITEMS.METAL_SHELF,\n ITEMS.WIRE_SHELF,\n ])\n ) {\n const shelves = productService.getSwappablesOfType(\n [ITEMS.SHELF, ITEMS.METAL_SHELF, ITEMS.WIRE_SHELF],\n {\n depth: item.filter.depth,\n width: item.filter.width,\n color: item.filter.color,\n }\n );\n\n if (shelves.length > 1) {\n variantSelectors.push({\n selection: selections.insert.key,\n component: (\n \n ),\n });\n }\n } else if (productService.isType(item, [ITEMS.FRAME, ITEMS.SHELVING_UNIT])) {\n const legSelector = multiSelector(item, tac, 'leg', 'variant');\n const basketSelector = multiSelector(item, tac, ITEMS.BASKET, 'variant');\n variantSelectors.push(\n legSelector && {\n selection: selections.frame.key,\n component: (\n \n ),\n },\n basketSelector && {\n selection: selections.basket.key,\n component: (\n \n ),\n }\n );\n }\n if (productService.isType(item, ITEMS.SHELVING_UNIT)) {\n if (item.y > constants.PAD_HEIGHT) {\n // item is not on floor\n return variantSelectors;\n }\n if (\n item.items &&\n item.items.some(\n child =>\n !productService.isType(child, [\n ITEMS.SHELVING_UNIT,\n ITEMS.TOP_SHELF,\n 'leg',\n ])\n )\n ) {\n /*\n item has something other than other shelving unit/top shelf/legs as a child,\n which means we can't rotate it\n */\n return variantSelectors;\n }\n const shelvingUnits = productService.getSwappables(item, {\n height: item.filter.height,\n variant: item.filter.variant,\n });\n\n if (shelvingUnits.length > 1) {\n const otherShelvingUnit = shelvingUnits.find(\n other => other.id !== item.id\n );\n\n const disabled = !rotationAllowed(item, otherShelvingUnit, tac);\n\n variantSelectors.push({\n selection: selections.frame.key,\n component: (\n \n ),\n });\n }\n }\n return variantSelectors.length > 0 ? variantSelectors : undefined;\n}\n\n// Note: Not in any way related to BROR addon shelves\nfunction addonSelector(item, tac) {\n if (productService.isType(item, ITEMS.FRAME)) {\n const covers = productService.getFilteredItems(\n item => {\n return productService.isType(item, 'cover');\n },\n {\n width: item.width,\n depth: item.depth,\n height: item.height,\n }\n );\n\n if (covers.length === 1) {\n const addableCover = covers[0];\n const slots = covers.length && tacHelpers.getOpenSlots(tac, addableCover);\n const currentItemSlot =\n slots && slots.find(slot => slot.parent.itemid === item.itemid);\n\n const currentCover =\n item.items &&\n item.items.find(item => covers.some(cover => cover.id === item.id));\n\n if (currentCover || currentItemSlot) {\n return {\n selection: selections.frame.key,\n component: (\n \n ),\n };\n }\n }\n }\n}\n\nfunction addDivider(selectionOptions) {\n const userAgent = getUserAgent();\n if (\n userAgent.orientation === 'portrait' &&\n userAgent.deviceType === 'mobile'\n ) {\n return selectionOptions.reduce((options, current, index) => {\n if (index > 0 && index < selectionOptions.length) {\n options.push(
);\n }\n options.push(current);\n return options;\n }, []);\n }\n\n return selectionOptions;\n}\n\nfunction generateSelectionOptions(selectionOptions) {\n const addon = selectionOptions.find(option => option.key === 'addon');\n const legVariant = selectionOptions.find(\n option => option.key === 'legVariant'\n );\n\n if (addon && legVariant) {\n selectionOptions.unshift(\n \n {[legVariant, addon]}\n
\n );\n return addDivider(\n selectionOptions.filter(\n option => option.key !== 'addon' && option.key !== 'legVariant'\n )\n );\n }\n\n return addDivider(selectionOptions);\n}\n\nfunction shouldSplitSelections(options) {\n let conflictingSelections = 0;\n const conflictingSelectionKeys = ['color', 'basketVariant'];\n\n return options.some(option => {\n conflictingSelections += conflictingSelectionKeys.includes(\n option.component.key\n )\n ? 1\n : 0;\n return conflictingSelections > 1;\n });\n}\n\nfunction getOptionsSelections(options) {\n return Object.entries(\n options.reduce((optionSelections, option) => {\n if (optionSelections[option.selection]) {\n optionSelections[option.selection].push(option.component);\n } else {\n optionSelections[option.selection] = [option.component];\n }\n return optionSelections;\n }, {})\n ).map(optionSelection => (\n
\n {selections[optionSelection[0]] && (\n \n {translate(t[selections[optionSelection[0]].header])}\n \n )}\n
\n {generateSelectionOptions(optionSelection[1])}\n
\n
\n ));\n}\n\nfunction wrapSelections(options) {\n const userAgent = getUserAgent();\n const wrappingOptionKeys = ['slider', 'width', 'color'];\n const sizeSelector = options.find(option => option.component.key === 'width');\n const slider = options.find(option => option.component.key === 'slider');\n const colorSelector = options.find(\n option => option.component.key === 'color'\n );\n\n if (\n (sizeSelector || colorSelector) &&\n slider &&\n isMobile() &&\n userAgent.orientation === 'portrait'\n ) {\n const wrappedComponents = [];\n const separateComponents = [];\n options.forEach(option => {\n if (wrappingOptionKeys.includes(option.component.key)) {\n wrappedComponents.push(\n React.cloneElement(option.component, {\n wrapped: true,\n })\n );\n } else {\n separateComponents.push(option.component);\n }\n });\n\n wrappedComponents.sort((a, b) => {\n return (\n wrappingOptionKeys.indexOf(a.key) - wrappingOptionKeys.indexOf(b.key)\n );\n });\n\n return [\n
\n {wrappedComponents}\n
,\n separateComponents,\n ];\n }\n\n return null;\n}\n\nexport function generateOptions(item, tac) {\n const options = [\n variantSelector(item, tac),\n addonSelector(item, tac),\n ...colorSelector(item, tac),\n sizeSelector(item),\n extendableSlider(item, tac),\n ]\n .flat()\n .filter(Boolean)\n .sort((a, b) =>\n _.clamp(\n selections[a.selection].order - selections[b.selection].order,\n -1,\n 1\n )\n );\n\n if (shouldSplitSelections(options)) {\n return getOptionsSelections(options);\n }\n\n return wrapSelections(options) || options.map(option => option.component);\n}\n","import tacHelpers from '../../state/tac/tacHelpers';\nimport { generateOptions } from './generateOptions';\nimport productService from '../../services/products';\n\nexport function findItem(tac, container) {\n const itemid = container.item.itemid;\n\n return tacHelpers\n .getAllItems(tacHelpers.getItems(tac))\n .find(item => item.itemid === itemid);\n}\n\nexport function getMenuItems(tac, container) {\n const item = findItem(tac, container);\n\n const options = generateOptions(item, tac);\n const showInfoIcon = productService.showInfoIcon(item);\n const showTrashCan =\n productService.isRealArticleProduct(item) ||\n item.items?.length ||\n (item.parts && Object.values(item.parts)?.length);\n\n return {\n options,\n showInfoIcon,\n showTrashCan,\n showConfMenu: showInfoIcon || showTrashCan || options.length,\n };\n}\n","import articles from '../../services/products/articles';\nimport productService from '../../services/products';\nimport platform from '../../util/platform';\nimport { SkapaTheme } from '@inter-ikea-kompis/themes/lib';\nimport {\n KompisProductCard,\n KompisSheet,\n KompisSheetBody,\n KompisSheetBodyPadding,\n KompisSheetHeader,\n} from '@inter-ikea-kompis/react-components';\nimport { SheetSizeEnum } from '@inter-ikea-kompis/component-sheet';\nimport classNames from 'classnames';\nimport styles from '../ConfMenu/ConfMenu.module.less';\nimport ProductCardLayoutEnum from '@inter-ikea-kompis/component-product-card/lib/enums/ProductCardLayoutEnum';\nimport { ProductCardMediaEnum } from '@inter-ikea-kompis/component-product-card';\nimport { ProductDescriptionProductMeasureEnum } from '@inter-ikea-kompis/utilities/lib';\nimport React, { useEffect } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { articleNo } from '../../util/articleNo';\nimport {\n selectIsLandscape,\n selectIsMobile,\n} from '../../state/userAgent/userAgentSelectors';\nimport { selectKompisTranslations } from '../../state/translations/translationsSelectors';\nimport { selectDexfSettings } from '../../state/dexfSettings/dexfSettingsSelectors';\nimport { TacItem } from '../../generalTypes';\nimport {\n actionDequeueAllSheets,\n actionDequeueSheet,\n} from '../../state/sheets/sheetActions';\n\nexport interface Props {\n item: TacItem;\n hasBackButton?: boolean;\n}\n\nexport const ADDITIONAL_INFO = 'ADDITIONAL_INFO';\n\nexport const AdditionalInfoSheet: React.FunctionComponent = ({\n item,\n hasBackButton,\n}) => {\n const dispatch = useDispatch();\n const isMobile = useSelector(selectIsMobile);\n const isLandscape = useSelector(selectIsLandscape);\n const kompisTranslations = useSelector(selectKompisTranslations);\n const dexfSettings = useSelector(selectDexfSettings);\n\n const [selectedIndex, setSelectedIndex] = React.useState(0);\n const [visibleModal, setVisibleModal] = React.useState(undefined);\n\n const [sheetVisible, setSheetVisible] = React.useState(true);\n\n useEffect(() => {\n return () => setSheetVisible(false);\n }, []);\n\n const _createAdditionalInfoObject = () => {\n if (!item) {\n return null;\n }\n\n const itemToDisplay = productService.isOnlyAvailableInMultipack(item.id)\n ? productService.getMultipackProductOf(item.id)\n : item;\n\n if (!itemToDisplay) {\n return null;\n }\n\n const { itemno: id } = articleNo(itemToDisplay.id)\n ? { itemno: articleNo(itemToDisplay.id) }\n : itemToDisplay.iows[0];\n\n return {\n id: id.replace(/^S/i, ''),\n type: /^S/.test(id) ? 'SPR' : 'ART',\n };\n };\n\n const aiItem = _createAdditionalInfoObject();\n const kItem = articles.getArticle(aiItem?.id);\n\n const onBodyClick = (event: Event) => event.stopPropagation();\n\n const onCarouselChange = ({\n detail: { carouselSelectedIndex },\n }: CustomEvent) => setSelectedIndex(carouselSelectedIndex);\n\n const onModalOpen = ({ detail: { visibleModal } }: CustomEvent) =>\n setVisibleModal(visibleModal);\n\n const onModalClose = (event: Event) => {\n event.stopPropagation();\n event.preventDefault();\n setVisibleModal(null);\n };\n\n const closeAllSheets = () => dispatch(actionDequeueAllSheets());\n const closeSheet = () => dispatch(actionDequeueSheet());\n\n return (\n \n \n \n \n \n \n \n \n );\n};\n","import { SheetObject } from './sheetTypes';\n\nimport { selectSheetQueue } from './sheetSelectors';\nimport { GetState } from '../../generalTypes';\nimport { actionEnqueueSheet } from './sheetActions';\n\nexport const thunkEnqueueSheet =\n (sheet: SheetObject) => (dispatch: any, getState: GetState) => {\n const queueLength = selectSheetQueue(getState()).length;\n const hasBackButton = queueLength >= 1;\n const modifiedSheet = { ...sheet, hasBackButton };\n dispatch(actionEnqueueSheet(modifiedSheet));\n };\n","import { TacItem } from '../../generalTypes';\nimport React, { useEffect, useState } from 'react';\nimport {\n getAllPartsThatAreAlsoAttachedAsProducts,\n getCommunicatedItemImageUrl,\n getCommunicatedItemName,\n} from '../../state/products/productsHelpers';\nimport { SkapaTheme } from '@inter-ikea-kompis/themes';\nimport {\n KompisActionList,\n KompisActionListItem,\n KompisSheet,\n KompisSheetBody,\n KompisSheetBodyPadding,\n KompisSheetHeader,\n} from '@inter-ikea-kompis/react-components';\n\nimport { ActionListControlEnum } from '@inter-ikea-kompis/component-action-list';\n\nimport {\n selectIsLandscape,\n selectIsMobile,\n} from '../../state/userAgent/userAgentSelectors';\nimport { useSelector, useDispatch } from 'react-redux';\nimport styles from '../ConfMenu/ConfMenu.module.less';\nimport classNames from 'classnames';\nimport { SheetSizeEnum } from '@inter-ikea-kompis/component-sheet';\nimport { ADDITIONAL_INFO } from './AdditionalInfoSheet';\nimport { actionDequeueAllSheets } from '../../state/sheets/sheetActions';\nimport { thunkEnqueueSheet } from '../../state/sheets/sheetThunks';\nimport { translate } from '../../services/L10n';\nimport { t } from '../../translations';\n\nexport interface Props {\n item: TacItem;\n}\n\nexport const COMBINED_PRODUCT_INFO = 'COMBINED_PRODUCT_INFO';\n\nexport const CombinedProductsSheet: React.FunctionComponent = ({\n item,\n}) => {\n const dispatch = useDispatch();\n const isMobile = useSelector(selectIsMobile);\n const isLandscape = useSelector(selectIsLandscape);\n const attachedParts = getAllPartsThatAreAlsoAttachedAsProducts(item);\n const partsAsSheetsSelectedIndexMap = new Map();\n\n const [sheetVisible, setSheetVisible] = useState(true);\n\n const isRealProduct = (item: TacItem) => item.iows.length;\n\n useEffect(() => {\n attachedParts.forEach((part, index) => {\n if (isRealProduct(part))\n partsAsSheetsSelectedIndexMap.set(index, {\n sheetType: ADDITIONAL_INFO,\n item: part,\n });\n });\n\n return () => setSheetVisible(false);\n }, []);\n\n const onBodyClick = (event: Event) => event.stopPropagation();\n\n const itemSelected = ({ detail: { selectedIndex } }: any) => {\n const sheet = partsAsSheetsSelectedIndexMap.get(selectedIndex);\n dispatch(thunkEnqueueSheet(sheet));\n };\n\n const renderActionListItem = (item: TacItem, index: number) => {\n const communicatedImageUrl = getCommunicatedItemImageUrl(item);\n const communicatedItemName = getCommunicatedItemName(item);\n return (\n \n );\n };\n\n const closeAllSheets = () => dispatch(actionDequeueAllSheets());\n\n const headingStyle = {\n fontSize: '24px',\n marginBottom: '4px',\n };\n\n const descriptionStyle = {\n fontSize: '14px',\n marginBottom: '16px',\n };\n\n return (\n \n \n \n \n

{`${translate(t.PRODUCT_INFO_SHEET)}`}

\n

{`${translate(\n t.PRODUCT_INFO_SHEET_DESCRIPTION\n )}`}

\n \n {attachedParts.map((part, index) =>\n isRealProduct(part) ? renderActionListItem(part, index) : null\n )}\n \n
\n \n \n );\n};\n","import React from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport classNames from 'classnames';\nimport { CONF_MENU_CHANGE } from '../../settings/events';\nimport { findItem, getMenuItems } from './getMenuItems';\nimport { Icon } from '../Icon';\nimport { TOP } from '../Popup/alignments';\nimport { translate } from '../../services/L10n';\nimport emitter from '../../emitter';\nimport productService from '../../services/products';\nimport StopPropagation from '../utils/StopPropagation';\nimport Tooltip from '../Popup/Tooltip';\nimport styles from './ConfMenu.module.less';\nimport { thunkHideItem, thunkRemoveItem } from '../../state/tac/tacThunks';\nimport { ADDITIONAL_INFO } from '../Sheets/AdditionalInfoSheet';\nimport localStatisticsReporter from '../../services/statistics/insights/custom/local/localStatisticsReporter';\nimport { selectTac } from '../../state/tac/tacSelectors';\nimport { COMBINED_PRODUCT_INFO } from '../Sheets/CombinedProductsSheet';\nimport { isCombinedItem } from '../../state/products/productsHelpers';\nimport { SheetObject } from '../../state/sheets/sheetTypes';\nimport { actionEnqueueSheet } from '../../state/sheets/sheetActions';\nimport { applicationSettings } from '../../settings/application';\nimport { RANGES } from '../../constants';\nimport { t } from '../../translations';\n\ninterface Props {\n container: any;\n onDelete: (item: any) => {};\n}\n\nconst ConfMenu: React.FunctionComponent = ({ container, onDelete }) => {\n const [showAdditionalInformation, setShowAdditionalInformation] =\n React.useState(false);\n\n const dispatch = useDispatch();\n\n const tac = useSelector(selectTac);\n const item = findItem(tac, container);\n\n const hideItem = (item: any) =>\n // @ts-ignore\n dispatch(thunkHideItem(item, undefined, { origin: 'dialog' }));\n const removeItem = (item: any) =>\n // @ts-ignore\n dispatch(thunkRemoveItem(item, undefined, { origin: 'dialog' }));\n\n React.useEffect(() => {\n emitter.emit(CONF_MENU_CHANGE);\n }, [item]);\n\n if (!item) return null;\n\n const menuItems = getMenuItems(tac, container);\n const isSection = productService.isSection(item);\n\n /**\n * Handles on delete click\n */\n const onDeleteClick = () => {\n hideItem(item);\n setTimeout(() => {\n removeItem(item);\n }, 200);\n\n onDelete(item);\n };\n\n /**\n * Handles info click\n */\n const onInfoClick = () => {\n _showInfo();\n localStatisticsReporter.reportConfMenuInfoButtonClicked(container.item.id);\n };\n\n /**\n * Hide additional information\n */\n const _hideAdditionalInformation = () => setShowAdditionalInformation(false);\n\n const _showCombinedProductInfo = () => {\n const sheetObject: SheetObject = {\n sheetType: COMBINED_PRODUCT_INFO,\n item,\n onOverlayClick: _hideAdditionalInformation,\n displaySheet: showAdditionalInformation,\n onCloseButtonClick: _hideAdditionalInformation,\n };\n dispatch(actionEnqueueSheet(sheetObject));\n };\n\n /**\n * Render additional information\n */\n const _showAdditionalInfo = () => {\n const sheet: SheetObject = {\n sheetType: ADDITIONAL_INFO,\n item,\n };\n dispatch(actionEnqueueSheet(sheet));\n };\n\n const isIvar = () => applicationSettings.applicationName === RANGES.IVAR;\n\n const _showInfo = () => {\n isIvar() && isCombinedItem(item)\n ? _showCombinedProductInfo()\n : _showAdditionalInfo();\n };\n\n /**\n * Render info icon\n */\n const renderInfoIcon = () => (\n
\n \n \n \n
\n \n
\n );\n\n /**\n * Render trashcan\n */\n const renderTrashCan = () => (\n
\n \n \n \n
\n \n \n );\n\n return item ? (\n <>\n \n {!!menuItems.showInfoIcon && renderInfoIcon()}\n {menuItems.options}\n {menuItems.showTrashCan && renderTrashCan()}\n \n \n ) : null;\n};\n\nexport default ConfMenu;\n","import React from 'react';\nimport { findDOMNode } from 'react-dom';\nimport classNames from 'classnames';\nimport PropTypes from 'prop-types';\n\nimport { IFRAME_RESIZED, CONF_MENU_CHANGE } from '../../settings/events';\nimport emitter from '../../emitter';\nimport TransitionTarget from '../Transition/TransitionTarget';\nimport Portal from '../utils/Portal';\nimport Outline from '../Outline';\nimport styles from './Modal.module.less';\nimport popupStyles from '../Popup/Popup.module.less';\nimport Popup from '../Popup';\n\nexport default class Modal extends TransitionTarget {\n static propTypes = {\n calculateTargetRect: PropTypes.func,\n children: PropTypes.node,\n className: PropTypes.string,\n container: PropTypes.string,\n onClose: PropTypes.func,\n outlineTarget: PropTypes.bool,\n target: PropTypes.any,\n targetRect: PropTypes.object,\n };\n\n classNames = styles;\n\n state = {};\n\n componentDidMount() {\n if (this.props.outlineTarget) {\n this._setTargetRect();\n\n emitter.on(IFRAME_RESIZED, this._setTargetRect);\n emitter.on(CONF_MENU_CHANGE, this._setTargetRect);\n }\n }\n\n componentWillUnmount() {\n emitter.off(IFRAME_RESIZED, this._setTargetRect);\n emitter.off(CONF_MENU_CHANGE, this._setTargetRect);\n }\n\n onModalClose = () => {\n this.props.onClose();\n };\n\n _setTargetRect = () => {\n const { calculateTargetRect, targetRect, target } = this.props;\n\n let rect;\n\n if (targetRect) {\n rect = targetRect;\n } else if (calculateTargetRect) {\n rect = calculateTargetRect();\n } else if (target) {\n const DOMNode = target instanceof Element ? target : findDOMNode(target);\n\n rect = DOMNode.getBoundingClientRect();\n } else {\n throw Error(\n 'You need to pass target, targetRect or calculateTargetRect to Popup'\n );\n }\n\n this.setState({\n targetRect: rect,\n });\n };\n\n render() {\n const { className, container, onClose, transitionState, outlineTarget } =\n this.props;\n const { targetRect } = this.state;\n const target = document.querySelector('#canvas-container');\n return (\n \n \n \n \n\n \n {this.props.children}\n \n\n {targetRect && outlineTarget && (\n \n )}\n \n );\n }\n}\n","import React from 'react';\nimport PropTypes from 'prop-types';\nimport { connect } from 'react-redux';\n\nimport emitter from '../emitter';\nimport {\n DISMISS_POPUP,\n SHOW_CONF,\n PICKUP_ITEM,\n SHOW_POPUP_ERROR,\n SHOW_POPUP_INTRO,\n} from '../settings/events';\nimport { TransitionGroup } from 'react-transition-group';\nimport BrorTransition from './Transition';\nimport platform from '../util/platform';\n\nimport ConfMenu from './ConfMenu';\nimport Modal from './Modal';\nimport Popup from './Popup';\nimport { Icon } from './Icon';\n\nimport { BOTTOM, TOP, LEFT } from './Popup/alignments';\nimport popupStyles from './Popup/Popup.module.less';\nimport {\n actionConfOpened,\n thunkConfClosed,\n actionItemPickedUp,\n actionClearPreviousConf,\n} from '../state/popups';\nimport { translate } from '../services/L10n';\nimport productService from '../services/products';\nimport { getMenuItems } from './ConfMenu/getMenuItems';\nimport { selectTac } from '../state/tac/tacSelectors.ts';\nimport { selectIsWallResizerActive } from '../state/scene/sceneSelectors.ts';\nimport { selectUserAgent } from '../state/userAgent/userAgentSelectors';\nimport {\n selectConfDialog,\n selectHasShownExtendableConf,\n selectHasShownPegboardHint,\n selectHasShownCuttableMountingRailHint,\n selectIntroPopupsVisible,\n selectPegboardHint,\n selectCuttableMountingRailHint,\n selectCuttableMountingRailHintAlignment,\n selectShowDoorsPopupHint,\n selectShowExtendableConf,\n} from '../state/popups/popupsSelectors';\nimport localStatisticsReporter from '../services/statistics/insights/custom/local/localStatisticsReporter';\nimport { t } from '../translations';\n\nconst types = {\n CONF: 'CONF',\n ERROR: 'ERROR',\n INTRO: 'INTRO',\n INFO: 'INFO',\n};\n\nclass ScenePopupManager extends React.Component {\n static propTypes = {\n introPopupsVisible: PropTypes.bool,\n pegboardPopupHintTarget: PropTypes.object,\n cuttableMountingRailPopupHintTarget: PropTypes.object,\n cuttableMountingRailPopupHintAlignment: PropTypes.string,\n doorsPopupHintTarget: PropTypes.object,\n confDialog: PropTypes.object,\n userAgent: PropTypes.object,\n confOpened: PropTypes.func.isRequired,\n confClosed: PropTypes.func.isRequired,\n clearPreviousConf: PropTypes.func.isRequired,\n itemPickedUp: PropTypes.func.isRequired,\n getItemSceneBounds: PropTypes.func,\n getSprite: PropTypes.func,\n tac: PropTypes.object,\n wallResizerActive: PropTypes.bool,\n };\n\n static getDerivedStateFromProps(props, state) {\n if (!props.introPopupsVisible) {\n return {\n ...state,\n popups: state.popups.filter(popup => popup.type !== types.INTRO),\n };\n }\n return state;\n }\n\n state = {\n popups: [],\n };\n\n constructor(...args) {\n super(...args);\n\n // This component should never be unmounted.\n emitter.on(SHOW_CONF, this.onShowConf);\n emitter.on(PICKUP_ITEM, this.onItemPickup);\n\n emitter.on(SHOW_POPUP_ERROR, this.onShowError);\n emitter.on(SHOW_POPUP_INTRO, this.onShowIntro);\n emitter.on(DISMISS_POPUP, this.onPopupClose);\n }\n\n shouldComponentUpdate(nextProps, nextState) {\n return (\n nextProps.userAgent !== this.props.userAgent ||\n nextProps.introPopupsVisible !== this.props.introPopupsVisible ||\n nextProps.pegboardPopupHintTarget !==\n this.props.pegboardPopupHintTarget ||\n nextProps.cuttableMountingRailPopupHintTarget !==\n this.props.cuttableMountingRailPopupHintTarget ||\n nextProps.cuttableMountingRailPopupHintAlignment !==\n this.props.cuttableMountingRailPopupHintAlignment ||\n nextProps.doorsPopupHintTarget !== this.props.doorsPopupHintTarget ||\n nextProps.extendableConfTarget !== this.props.extendableConfTarget ||\n nextProps.extendableConfShown !== this.props.extendableConfShown ||\n nextProps.getItemSceneBounds !== this.props.getItemSceneBounds ||\n nextProps.getSprite !== this.props.getSprite ||\n nextState.popups !== this.state.popups\n );\n }\n\n /**\n * Calls the individual methods for handling item intro popups and extendable\n * item intro popup.\n * @param {Object} prevProps The previous props from before the component update,\n * or null if there are no previous props.\n */\n handleItemIntros(prevProps) {\n this.handleItemIntro(prevProps, {\n popupHintTargetPropName: 'pegboardPopupHintTarget',\n translationKey: 'POPUP_PEGBOARD_HINT',\n alignment: LEFT,\n });\n this.handleItemIntro(prevProps, {\n popupHintTargetPropName: 'cuttableMountingRailPopupHintTarget',\n translationKey: 'MOUNTING_RAIL_CUT',\n alignment: this.props.cuttableMountingRailPopupHintAlignment,\n });\n this.handleItemIntro(prevProps, {\n popupHintTargetPropName: 'doorsPopupHintTarget',\n translationKey: 'POPUP_INFORMATION_DOORS',\n alignment: TOP,\n });\n this.handleExtendableItemIntro(prevProps);\n }\n\n /**\n * Handles the introduction popup for a type of item, as configured by\n * the settings parameter.\n * @param {Object} prevProps The previous props from before the component update,\n * or null if there are no previous props.\n * @param {Object} settings Settings for the popup.\n */\n handleItemIntro(prevProps, settings) {\n const popupHintTarget = this.props[settings.popupHintTargetPropName];\n const prevPopupHintTarget = prevProps\n ? prevProps[settings.popupHintTargetPropName]\n : null;\n const popupHintTargetHasChanged =\n popupHintTarget?.itemid !== prevPopupHintTarget?.itemid;\n const confMenuHasBeenOpened =\n !prevProps?.confDialog?.open && this.props.confDialog?.open;\n\n // Add new hint popup\n if (popupHintTarget && popupHintTargetHasChanged) {\n this.onShowInfo(\n {\n item: {\n ...popupHintTarget,\n },\n },\n translate(t[settings.translationKey]),\n {\n dismissible: false,\n alignment: settings.alignment,\n }\n );\n }\n\n // Delete old hint popup\n if (\n prevPopupHintTarget &&\n (popupHintTargetHasChanged || confMenuHasBeenOpened)\n ) {\n const removeKey = `${types.INFO}-${prevPopupHintTarget.itemid}`;\n this.setState({\n popups: this.state.popups.filter(popup => popup.key !== removeKey),\n });\n }\n }\n\n handleExtendableItemIntro(prevProps) {\n const { extendableConfTarget } = this.props;\n const extendableConfShown = prevProps\n ? prevProps.extendableConfShown\n : false;\n if (extendableConfTarget && !extendableConfShown) {\n setTimeout(() => {\n const sprite = this.props.getSprite(extendableConfTarget);\n if (!sprite) {\n console.log('sprite missing');\n }\n this.onShowConf(sprite);\n });\n }\n }\n\n componentDidMount() {\n this.handleItemIntros(null);\n }\n\n componentDidUpdate(prevProps) {\n const { wallResizerActive } = this.props;\n const { isMobile, isPortrait } = this.props.userAgent;\n\n if (\n (isMobile && isPortrait) !==\n (prevProps.userAgent.isMobile && prevProps.userAgent.isPortrait)\n ) {\n this.closeConf();\n }\n\n if (wallResizerActive && !prevProps.wallResizerActive) {\n this.closeErrors();\n }\n\n this.handleItemIntros(prevProps);\n }\n\n componentWillUnmount() {\n emitter.off(SHOW_CONF, this.onShowConf);\n emitter.off(SHOW_POPUP_ERROR, this.onShowError);\n emitter.off(SHOW_POPUP_INTRO, this.onShowIntro);\n emitter.off(DISMISS_POPUP, this.onPopupClose);\n emitter.off(PICKUP_ITEM, this.onItemPickup);\n }\n\n getChild(items, childItemid) {\n return items.filter(item => item.itemid === childItemid);\n }\n\n onShowConf = sprite => {\n const { isMobile, isPortrait } = this.props.userAgent;\n const { confDialog } = this.props;\n let selectSprite = sprite;\n\n if (typeof this.previousConfTimeout !== 'undefined') {\n clearTimeout(this.previousConfTimeout);\n }\n\n const Component = isMobile && isPortrait ? Modal : Popup;\n if (confDialog) {\n if (sprite?.item?.itemid === confDialog.closedId) {\n if (\n sprite.parentItem.itemid &&\n !productService.isMultiParentProduct(sprite.item) &&\n !productService.isTable(sprite.item)\n ) {\n const parentSprite = sprite.parent.parent;\n const parentChild = this.getChild(\n parentSprite.item.items,\n sprite.item.itemid\n );\n if (parentChild.length > 0) {\n selectSprite = parentSprite;\n }\n } else if (sprite.childrenContainer) {\n const children = sprite.childrenContainer.children;\n const matchingChild = this.getChild(children, confDialog.closedId);\n if (matchingChild.length !== 0) {\n selectSprite = matchingChild;\n }\n }\n }\n }\n\n const key = `${types.CONF}-${selectSprite.item.itemid}`;\n\n const menuItems = getMenuItems(this.props.tac, selectSprite);\n if (!menuItems.showConfMenu) {\n return;\n }\n this.openPopup({\n sprite: selectSprite,\n Component,\n content: (\n this.onPopupClose(key)}\n />\n ),\n type: types.CONF,\n props: {\n container: '#mobileMenu',\n autoWidth: true,\n outlineTarget: true,\n alignment: isMobile ? BOTTOM : TOP,\n spacing: platform.isKiosk ? 15 : 10,\n },\n key,\n });\n\n localStatisticsReporter.reportTacArticleClicked(sprite.item.id);\n this.props.confClosed();\n this.props.confOpened();\n };\n\n onItemPickup = sprite => {\n const item = sprite.item;\n this.props.itemPickedUp(item);\n };\n\n onShowError = (sprite, message, props) => {\n const content = (\n \n \n {translate(t[message])}\n \n );\n this.openPopup({ sprite, content, type: types.ERROR });\n };\n\n onShowIntro = (sprite, content) => {\n if (this.props.introPopupsVisible) {\n this.openPopup({\n sprite,\n content,\n type: types.INTRO,\n props: { wiggle: true, dismissible: false },\n });\n }\n };\n\n onShowInfo = (sprite, content, props) => {\n const key = types.INFO;\n this.openPopup({ sprite, content, type: types.INFO, props, key });\n };\n\n calculateTargetRect(sprite) {\n if (sprite._destroyed) {\n return;\n }\n\n // TODO: This should be handled differently -- extremely ugly\n const canvas = document.getElementsByTagName('canvas')[0];\n\n if (!canvas) {\n throw new Error('No canvas element found');\n }\n\n const canvasPos = canvas.getBoundingClientRect();\n\n const itemSceneBounds = this.props.getItemSceneBounds(sprite.item);\n\n if (!itemSceneBounds) {\n // Item is not on scene.\n return;\n }\n\n const { height, width, x, y } = itemSceneBounds;\n const offsetX = x + canvasPos.left;\n const offsetY = y + canvasPos.top;\n\n return {\n width,\n height,\n top: offsetY,\n left: offsetX,\n right: offsetX + width,\n bottom: offsetY + height,\n x: offsetX,\n y: offsetY,\n };\n }\n\n closeConf() {\n this.setState({\n popups: this.state.popups.filter(popup => popup.type !== types.CONF),\n });\n }\n\n closeErrors() {\n this.setState({\n popups: this.state.popups.filter(popup => popup.type !== types.ERROR),\n });\n }\n\n openPopup({ sprite, Component = Popup, content, type, props = {} }) {\n setTimeout(() => {\n props.alignment = props.alignment || TOP;\n\n const key = `${type}-${sprite.item.itemid}`;\n\n if (this.state.popups.some(popup => popup.key === key)) {\n return;\n }\n\n this.setState({\n popups: this.state.popups.concat({\n Component,\n type,\n key,\n content,\n sprite,\n item: sprite.item,\n props: {\n ...props,\n calculateTargetRect: this.calculateTargetRect.bind(this, sprite),\n },\n }),\n });\n }, 0);\n }\n\n onPopupClose = key => {\n const popups = this.state.popups;\n const index = popups.findIndex(popup => popup.key === key);\n\n // TODO Place conf dialog in redux state instead\n const popup = popups[index];\n if (popup && popup.type === types.CONF) {\n emitter.emit(DISMISS_POPUP);\n this.props.confClosed(popup.item.itemid);\n this.previousConfTimeout = setTimeout(() => {\n this.props.clearPreviousConf();\n }, 300);\n }\n\n if (index >= 0) {\n this.setState({\n popups: [...popups.slice(0, index), ...popups.slice(index + 1)],\n });\n }\n };\n\n render() {\n const { popups } = this.state;\n if (!this.props.getItemSceneBounds) {\n return null;\n }\n\n return (\n \n \n {popups.map(popup => (\n \n this.onPopupClose(popup.key)}\n {...popup.props}\n >\n {popup.content}\n \n \n ))}\n \n \n );\n }\n}\n\nexport default connect(\n state => ({\n doorsPopupHintTarget: selectShowDoorsPopupHint(state),\n introPopupsVisible: selectIntroPopupsVisible(state),\n pegboardPopupHintTarget: selectPegboardHint(state),\n pegboardPopupShown: selectHasShownPegboardHint(state),\n cuttableMountingRailPopupHintTarget: selectCuttableMountingRailHint(state),\n cuttableMountingRailPopupHintAlignment:\n selectCuttableMountingRailHintAlignment(state),\n cuttableMountingRailPopupShown:\n selectHasShownCuttableMountingRailHint(state),\n extendableConfTarget: selectShowExtendableConf(state),\n extendableConfShown: selectHasShownExtendableConf(state),\n confDialog: selectConfDialog(state),\n userAgent: selectUserAgent(state),\n tac: selectTac(state),\n wallResizerActive: selectIsWallResizerActive(state),\n }),\n dispatch => ({\n confOpened: () => dispatch(actionConfOpened()),\n confClosed: closedId => dispatch(thunkConfClosed(closedId)),\n clearPreviousConf: () => dispatch(actionClearPreviousConf()),\n itemPickedUp: item => dispatch(actionItemPickedUp(item)),\n })\n)(ScenePopupManager);\n","import constants from '../../settings/constants';\nimport { applicationSettings } from '../../settings/application';\nimport productsService from '../../services/products';\nimport { ITEMS } from '../../constants';\n\ninterface padding {\n top: number;\n bottom: number;\n left: number;\n right: number;\n front?: number;\n back?: number;\n}\n\nconst defaultPadding: padding = {\n top: 100,\n bottom: 100,\n left: 100,\n right: 100,\n};\n\nfunction getMixedModePadding(item: any): padding {\n if (productsService.isShelvingUnit(item)) {\n return {\n ...defaultPadding,\n top: -200,\n };\n } else if (productsService.isType(item, ITEMS.FRAME)) {\n return {\n ...defaultPadding,\n top: -200,\n left: -(item.width / 2),\n right: -(item.width / 2),\n };\n } else if (productsService.isType(item, [ITEMS.CABINET, 'box'])) {\n return {\n ...defaultPadding,\n top: 0,\n bottom: 0,\n left: -(item.width / 3),\n right: -(item.width / 3),\n };\n }\n\n return { ...defaultPadding };\n}\n\nfunction getInsertModePadding(item: any): padding {\n if (\n productsService.isType([ITEMS.SHELF, ITEMS.METAL_SHELF, ITEMS.WIRE_SHELF])\n ) {\n switch (applicationSettings.applicationName) {\n case 'BOAXEL': {\n return {\n ...defaultPadding,\n top: 50,\n bottom: 50,\n };\n }\n case 'IVAR': {\n return {\n ...defaultPadding,\n top: 30,\n bottom: 30,\n };\n }\n default:\n break;\n }\n } else if (\n productsService.isType(item, ITEMS.SHOE_SHELF) &&\n applicationSettings.applicationName === 'BOAXEL'\n ) {\n return {\n ...defaultPadding,\n bottom: 300,\n front: constants.ROOM_DEPTH,\n back: constants.ROOM_DEPTH,\n };\n }\n return {\n ...defaultPadding,\n front: constants.ROOM_DEPTH,\n back: constants.ROOM_DEPTH,\n };\n}\n\nexport function snapPadding(mode: number, item: any): padding {\n if (item) {\n if (mode === constants.DRAG_MODE.MIXED) {\n return getMixedModePadding(item);\n }\n if (mode === constants.DRAG_MODE.INSERT) {\n return getInsertModePadding(item);\n }\n }\n\n return { ...defaultPadding };\n}\n","export default function offset(el) {\n const result = {\n top: 0,\n left: 0,\n };\n\n let ref = el;\n\n // in IE offsetParent is null when position: fixed\n while (ref && ref !== document.body) {\n result.top += ref.offsetTop;\n result.left += ref.offsetLeft;\n ref = ref.offsetParent;\n }\n\n return result;\n}\n","import * as PIXI from 'pixi.js-legacy';\n\nfunction getVertices(vertices, face = 'front') {\n switch (face) {\n case 'front':\n return [vertices[0], vertices[1], vertices[2], vertices[3]];\n case 'back':\n return [vertices[5], vertices[6], vertices[7], vertices[4]];\n case 'top':\n return [vertices[3], vertices[2], vertices[6], vertices[7]];\n case 'bottom':\n return [vertices[0], vertices[1], vertices[5], vertices[4]];\n case 'left':\n return [vertices[0], vertices[4], vertices[7], vertices[3]];\n case 'right':\n return [vertices[1], vertices[2], vertices[6], vertices[5]];\n default:\n throw new Error('Unknown face');\n }\n}\n\nexport default {\n outline: {\n polygon(vertices, color = 0x33dd33, width = 2) {\n const graphic = new PIXI.Graphics();\n\n graphic.lineStyle(width, color, 1);\n\n graphic.moveTo(...vertices[0]);\n\n for (let i = 1; i < vertices.length; i++) {\n graphic.lineTo(...vertices[i]);\n }\n\n graphic.lineTo(...vertices[0]);\n\n return graphic;\n },\n\n face(vertices, face = 'front', color, width) {\n return this.polygon(getVertices(vertices, face), color, width);\n },\n },\n\n fill: {\n polygon(vertices, color = 0x33dd33) {\n const graphic = new PIXI.Graphics();\n\n graphic.beginFill(color);\n graphic.drawPolygon([].concat(...vertices));\n graphic.endFill();\n\n return graphic;\n },\n\n face(vertices, face = 'front', color) {\n return this.polygon(getVertices(vertices, face), color);\n },\n },\n};\n","import { vec3, mat4 } from 'gl-matrix';\n\nimport constants from '../../settings/constants';\n\nfunction calculateVertices(item) {\n return [\n // 0: front bottom left\n [0, 0, item.depth],\n // 1: front bottom right\n [item.width, 0, item.depth],\n // 2: front top right\n [item.width, item.height, item.depth],\n // 3: front top left\n [0, item.height, item.depth],\n // 4: rear bottom left\n [0, 0, 0],\n // 5: rear bottom right\n [item.width, 0, 0],\n // 6: rear top right\n [item.width, item.height, 0],\n // 7: rear top left\n [0, item.height, 0],\n ];\n}\n\nfunction calculateViewProjection() {\n const cameraPosition = [0, 0, 1];\n const cameraLookAt = [0, 0, 0];\n const cameraUp = [0, 1, 0];\n\n const view = mat4.lookAt(\n mat4.create(),\n cameraPosition,\n cameraLookAt,\n cameraUp\n );\n\n const projection = mat4.ortho(mat4.create(), -1, 1, -1, 1, 0, 1000);\n\n // append oblique transformation\n const oblique = mat4.create();\n oblique[8] = Math.cos(constants.OBLIQUE_ANGLE) * -0.29;\n oblique[9] = Math.sin(constants.OBLIQUE_ANGLE) * -0.29;\n mat4.multiply(projection, projection, oblique);\n\n // combine view and projection\n const viewProjection = mat4.multiply(mat4.create(), projection, view);\n\n return viewProjection;\n}\n\nconst viewProjection = calculateViewProjection();\n\nfunction isBehind(a, b) {\n if (a.x >= b.x + b.width || (a.y >= b.y + b.height && b.x < a.x + a.width)) {\n return false;\n }\n\n return true;\n}\n\nfunction point(point) {\n return vec3.transformMat4(vec3.create(), point, viewProjection);\n}\n\nfunction points(points) {\n if (\n typeof points.width === 'number' &&\n typeof points.height === 'number' &&\n typeof points.depth === 'number'\n ) {\n points = calculateVertices(points);\n } else if (!Array.isArray(points)) {\n throw new Error('Only an item or array of points can be projected');\n }\n\n return points.map(point);\n}\n\nfunction face(item) {\n const vertices = calculateVertices(item).slice(0, 4);\n\n const projectedVertices = vertices.map(point);\n\n return {\n width: projectedVertices[1][0] - projectedVertices[0][0],\n height: projectedVertices[2][1] - projectedVertices[0][1],\n };\n}\n\nfunction outer(item) {\n const vertices = calculateVertices(item);\n\n const projectedVertices = vertices.map(vertex => {\n const coords = vec3.create();\n\n vec3.transformMat4(coords, vertex, viewProjection);\n\n return coords;\n });\n\n return {\n width: projectedVertices[5][0] - projectedVertices[0][0],\n height: projectedVertices[6][1] - projectedVertices[0][1],\n };\n}\n\nexport { calculateVertices, face, outer, point, points };\n\nexport default {\n calculateVertices,\n face,\n isBehind,\n outer,\n point,\n points,\n};\n","import * as PIXI from 'pixi.js-legacy';\n\nimport draw from './util/draw';\nimport project from './util/project';\n\nimport { vec3 } from 'gl-matrix';\nimport sorter from '../state/tac/tacReducer/sorter';\nimport constants from '../settings/constants';\nimport geometry from './util/geometry';\n\nexport default class Base extends PIXI.Container {\n isPureContainer(container) {\n return (\n !(container instanceof PIXI.Sprite) && container instanceof PIXI.Container\n );\n }\n\n calculateProjection() {\n const { item, ratio } = this;\n\n const outerSize = project.outer(item);\n const faceSize = project.face(item);\n const vertices = project.calculateVertices(item);\n\n let projectedVertices = vertices.map(project.point);\n\n const backBottomLeft = projectedVertices[4];\n\n projectedVertices = projectedVertices.map(vertex =>\n vec3.sub(vec3.create(), vertex, backBottomLeft)\n );\n\n const frontBottom = projectedVertices[0];\n\n const scaledVertices = projectedVertices.map(vertex => {\n return [\n (vertex[0] - frontBottom[0]) * ratio,\n (outerSize.height - (vertex[1] - frontBottom[1])) * ratio,\n ];\n });\n\n this.data = {\n vertices,\n projectedVertices,\n scaledVertices,\n outerSize,\n faceSize,\n };\n }\n\n drawBox() {\n this.eraseBox();\n\n const { scaledVertices } = this.data;\n\n this.box = new PIXI.Container();\n\n this.box.addChild(draw.fill.face(scaledVertices, 'top', 0x66ff33));\n this.box.addChild(draw.fill.face(scaledVertices, 'front', 0x44dd11));\n this.box.addChild(draw.fill.face(scaledVertices, 'right', 0x22bb00));\n\n this.addChild(this.box);\n }\n\n eraseBox() {\n if (this.box) {\n this.box.destroy();\n\n this.box = null;\n }\n }\n\n drawMesh(drawHidden) {\n this.eraseMesh();\n\n const { scaledVertices } = this.data;\n\n this.mesh = new PIXI.Container();\n\n if (drawHidden) {\n this.mesh.addChild(draw.outline.face(scaledVertices, 'back'));\n this.mesh.addChild(draw.outline.face(scaledVertices, 'bottom'));\n this.mesh.addChild(draw.outline.face(scaledVertices, 'left'));\n }\n\n this.mesh.addChild(draw.outline.face(scaledVertices, 'front'));\n this.mesh.addChild(draw.outline.face(scaledVertices, 'top'));\n this.mesh.addChild(draw.outline.face(scaledVertices, 'right'));\n\n this.addChild(this.mesh);\n }\n\n eraseMesh() {\n if (this.mesh) {\n this.mesh.destroy();\n\n this.mesh = null;\n }\n }\n\n drawOutline(color) {\n this.eraseOutline();\n\n const { scaledVertices } = this.data;\n\n const vertices = [\n scaledVertices[0],\n scaledVertices[1],\n scaledVertices[5],\n scaledVertices[6],\n scaledVertices[7],\n scaledVertices[3],\n ];\n\n this.outline = draw.outline.polygon(vertices, color);\n\n this.addChild(this.outline);\n }\n\n eraseOutline() {\n if (this.outline) {\n this.outline.destroy();\n\n this.outline = null;\n }\n }\n\n sortLast(sprite, container) {\n if (container.children[container.children.length - 1] !== sprite) {\n const index = container.children.indexOf(sprite);\n container.children.splice(index, 1);\n container.children.push(sprite);\n }\n }\n\n draggingSpriteIsInTrashcan() {\n return !!(\n this.app?.dragging?.sprite &&\n this.app.isInTrashcan(this.app.dragging.lastDragEventPos)\n );\n }\n\n isInContainer(sprite, container) {\n return container.children.indexOf(sprite) > -1;\n }\n\n sortChildren() {\n const container = this.childrenContainer || this;\n\n const sprites = container.children;\n\n const itemToSpriteMap = sprites.reduce((map, sprite) => {\n map[sprite.item.itemid] = sprite;\n return map;\n }, {});\n const items = sprites.map(sprite => sprite.item);\n container.children = sorter\n .inDrawOrder(items)\n .map(item => itemToSpriteMap[item.itemid]);\n\n if (this.draggingSpriteIsInTrashcan()) {\n const { sprite } = this.app.dragging;\n this.isInContainer(sprite, container) && this.sortLast(sprite, container);\n }\n }\n\n sortChildrenDragging(draggingSprite, draggingItem, draggingMode) {\n const container = this.childrenContainer || this;\n const hasRoomSlots = this.app.dragging?.slots?.filter(\n slot => !slot.parent\n ).length;\n\n if (\n draggingMode === constants.DRAG_MODE.INSERT ||\n (draggingMode === constants.DRAG_MODE.MIXED && !hasRoomSlots) ||\n this.draggingSpriteIsInTrashcan()\n ) {\n this.sortLast(draggingSprite, container);\n return;\n }\n\n const collisions = container.children.filter(\n sibling =>\n sibling !== draggingSprite &&\n geometry.collides(draggingSprite.getBounds(), sibling.getBounds())\n );\n const currentIndex = container.children.indexOf(draggingSprite);\n const result = collisions.reduce(\n (result, collision) => {\n const isBehind = project.isBehind(draggingItem, collision.item);\n const index = container.children.indexOf(collision);\n\n if (isBehind && index < result.below) {\n result.below = index;\n } else if (!isBehind && index > result.above) {\n result.above = index;\n }\n\n return result;\n },\n { above: -Infinity, below: Infinity }\n );\n\n if (result.below <= result.above) {\n // bad result, calling regular sort\n return this.sortChildren();\n }\n\n if (currentIndex > result.above && currentIndex < result.below) {\n // is already fine do nothing\n return;\n }\n\n if (currentIndex <= result.above) {\n // place it above\n container.children.splice(currentIndex, 1);\n container.children.splice(result.above, 0, draggingSprite);\n return;\n }\n\n if (currentIndex >= result.below) {\n // place it below\n container.children.splice(currentIndex, 1);\n container.children.splice(result.below, 0, draggingSprite);\n return;\n }\n\n return;\n }\n}\n","import * as PIXI from 'pixi.js-legacy';\nimport Base from './Base';\nimport constants from '../settings/constants';\nimport { ITEMS } from '../constants';\nimport productService from '../services/products';\n\nexport default class DropArea extends Base {\n constructor(options) {\n super(options);\n this.ratio = options.ratio;\n this.slot = options.slot;\n this.shouldRenderFullSize = options.fullSize;\n this.draw(options.width, options.height);\n }\n\n clampedHeight(height) {\n const max =\n Math.min(\n constants.DROP_AREA_MAX_HEIGHT,\n constants.DISTANCE_BETWEEN_ATTACHMENTS *\n (1 - constants.DROP_AREA_SEPARATION)\n ) * this.ratio;\n const min = constants.DROP_AREA_MIN_HEIGHT * this.ratio;\n return Math.max(min, Math.min(max, height));\n }\n /**\n * Gets the the information about the dimensions of the drop area to be drawn.\n * Also includes an attribute that helps with adjusting the width of the drop area in relation to\n * how much the drop area overlaps with the posts. In the case for tables it also considers the possible\n * overlap between drop areas that are next to each other horizontally.\n * @param {Object} slot\n * @param {number} wantedHeight\n * @returns {Object} An object that contains information about the dimensions of the drop area to be drawn as well as how much to adjust the width.\n */\n getSize(slot, wantedHeight) {\n const postWidth = productService.getPostWidth();\n\n if (this.shouldRenderFullSize) {\n return {\n x: 0,\n y: 0,\n widthToSubtract: 0,\n height: wantedHeight,\n };\n }\n switch (slot.filter.type) {\n case 'cover':\n return {\n x: Math.floor(postWidth * this.ratio),\n y: (this.slot.height - this.slot.parent.height) * this.ratio,\n widthToSubtract: Math.floor(2 * postWidth * this.ratio),\n height: this.slot.parent.height * this.ratio,\n };\n default:\n return {\n x: Math.floor(postWidth * this.ratio),\n y: 0,\n widthToSubtract: !slot.horizontalOverlap\n ? Math.floor(2 * postWidth * this.ratio)\n : slot.horizontalOverlap * this.ratio,\n height: this.clampedHeight(wantedHeight),\n };\n }\n }\n\n draw(width, wantedHeight) {\n const size = this.getSize(this.slot, wantedHeight);\n\n const height = size.height;\n\n width -= size.widthToSubtract;\n const back = new PIXI.Graphics()\n .beginFill(0xfafafa)\n .drawRect(0, 0, width, height)\n .endFill();\n\n back.alpha = 0.5;\n back.x = size.x;\n back.y = size.y;\n\n this.addChild(back);\n\n const arrowMaxHeight = this.shouldRenderFullSize ? Infinity : 13;\n // UX design says arraw needs 1px margin\n const arrowHeight = Math.min(height, arrowMaxHeight);\n const arrowWidth = arrowHeight / 2;\n const arrowY = (height - arrowHeight) / 2;\n\n const buffer = 1;\n\n const leftArrow = new PIXI.Graphics()\n .beginFill(0x407ab1)\n .drawPolygon(\n 0,\n 0,\n 0,\n arrowHeight,\n buffer,\n arrowHeight,\n buffer + arrowWidth,\n arrowHeight / 2,\n buffer,\n 0\n )\n .endFill();\n\n leftArrow.x = back.x;\n leftArrow.y = arrowY;\n\n this.addChild(leftArrow);\n\n if (this.hasRightArrow(this.slot)) {\n const rightArrow = leftArrow.clone();\n\n rightArrow.pivot = new PIXI.Point(rightArrow.width, rightArrow.height);\n rightArrow.rotation = Math.PI;\n rightArrow.x = back.x + width - rightArrow.width;\n rightArrow.y = arrowY;\n\n this.addChild(rightArrow);\n }\n }\n\n hasRightArrow(slot) {\n return slot.filter.type !== ITEMS.TABLE;\n }\n}\n","function pickPropping() {\n //BROR articles does not have propping\n return;\n}\nexport default { pickPropping };\n","import * as PIXI from 'pixi.js-legacy';\n\nimport { SHOW_POPUP_INTRO, SHOW_POPUP_ERROR } from '../settings/events';\nimport { translate } from '../services/L10n';\nimport { t } from '../translations';\nimport emitter from '../emitter';\nimport tacHelpers from '../state/tac/tacHelpers';\nimport { floor } from '../util/round';\nimport constants from '../settings/constants';\nimport productService from '../services/products';\n\nimport Base from './Base';\nimport getComponent from './util/getComponent';\nimport { isFixedRoom } from '../util/room';\nimport idGenerator from '../util/aactools/idGenerator';\nimport geometry from './util/geometry';\nimport localStatisticsReporter from '../services/statistics/insights/custom/local/localStatisticsReporter';\nimport { MeansOfNotification } from '../state/tac/tacReducer/validate';\n\nexport default class ItemContainer extends Base {\n constructor(args) {\n super();\n\n this.space = args.space;\n this.onDragStart = args.onDragStart;\n this.app = args.app;\n\n if (isFixedRoom()) {\n this.origo = { x: 0, y: 0 };\n }\n this.alphaFilter = new PIXI.filters.AlphaFilter(0.5);\n this.alphaFilter.resolution = 3;\n }\n\n update({ room, tac, options, ratio, wallResizerActive, updateOrigo = true }) {\n this.filters = wallResizerActive ? [this.alphaFilter] : [];\n\n const limits = tacHelpers.getLimits(tac, room);\n const width = limits.max.x - limits.min.x;\n this.room = room;\n this.ratio = ratio;\n this.options = options;\n this.tac = tac;\n\n if (updateOrigo) {\n if (!isFixedRoom()) {\n if (tac.centerConfiguration || !this.origo) {\n this.origo = {\n x: floor(\n limits.min.x - (room.width - width) / 2,\n constants.GRID.x.step\n ),\n y: 0,\n };\n } else {\n if (limits.min.x < this.origo.x) {\n this.origo = {\n x: limits.min.x,\n y: 0,\n };\n } else if (limits.max.x > this.origo.x + room.width) {\n this.origo = {\n x: limits.max.x - room.width,\n y: 0,\n };\n }\n }\n } else {\n this.origo = { x: 0, y: 0 };\n }\n }\n\n this.eraseDropAreas();\n\n this.drawChildren(wallResizerActive);\n\n if (this.options.errorVisible) {\n this.tac.errors.forEach((error, index, array) => {\n if (!constants.SHOW_MULTIPLE_ERRORS && index > 0) {\n return;\n }\n if (\n array.some(error => error.options?.showInstantly) &&\n !error.options?.showInstantly\n ) {\n return;\n }\n\n const { itemid, translationKey, statisticsLabel } = error;\n const message = translate(translationKey);\n\n const sprite = this.getSprite(itemid);\n\n if (sprite) {\n localStatisticsReporter.reportValidationError(\n statisticsLabel,\n MeansOfNotification.Popup\n );\n\n emitter.emit(SHOW_POPUP_ERROR, sprite, message);\n }\n });\n }\n }\n\n drawChildren(wallResizerActive) {\n const oldSprites = this.sprites;\n\n this.sprites = new Map();\n\n const introPopupIndex = this.tac.items.findIndex(\n item => item.width && item.height\n );\n\n this.tac.items.forEach((item, index) => {\n let sprite = oldSprites && oldSprites.get(item.itemid);\n\n if (sprite) {\n if (sprite.eraseDropAreas) {\n sprite.eraseDropAreas();\n }\n\n sprite.update({\n origo: this.origo,\n parentItem: this.room,\n item,\n ratio: this.ratio,\n options: this.options,\n wallResizerActive,\n });\n\n oldSprites.delete(item.itemid);\n\n this.sprites.set(item.itemid, sprite);\n } else {\n sprite = this.addItem(item);\n\n if (\n this.app.dragging &&\n !idGenerator.hasRealId(item) &&\n productService.isType(item, this.app.dragging.item?.filter.type)\n ) {\n //this is a temporary item that should have the same scale as the one currently being dragged\n const draggingScale = this.app.dragging.sprite?.scale;\n\n if (draggingScale) {\n sprite.position.x += sprite.width / 2;\n sprite.position.y += sprite.height / 2;\n sprite.pivot.set(sprite.width / 2, sprite.height / 2);\n sprite.scale.set(draggingScale.x, draggingScale.y);\n }\n }\n }\n\n if (this.options.introPopupsVisible && index === introPopupIndex) {\n setTimeout(() => {\n emitter.emit(\n SHOW_POPUP_INTRO,\n sprite,\n translate(t.BALLOON_HINT_STEP_THREE_WALL_INTRO)\n );\n }, 100);\n }\n });\n\n if (oldSprites) {\n // remove any remaining old sprites (their items have been removed from the TAC)\n oldSprites.forEach(sprite => sprite.destroy());\n }\n\n this.sortChildren();\n }\n\n addItem(item) {\n const { origo, room, options, ratio, onDragStart, app } = this;\n const args = {\n origo,\n parentItem: room,\n item,\n onDragStart,\n ratio,\n options,\n app,\n };\n\n const sprite = getComponent(args);\n\n super.addChild(sprite);\n\n this.sprites.set(item.itemid, sprite);\n\n return sprite;\n }\n\n addChild(sprite, sort = true) {\n super.addChild(sprite);\n\n if (sort) {\n this.sortChildren();\n }\n }\n /**\n * Draws the drop areas for each slot belonging to each parent where each parent is a section.\n * @param {Array} slots\n */\n drawDropAreas(slots) {\n const parents = Object.values(\n slots\n .map(slot => slot.parent)\n .reduce((out, parent) => {\n out[parent.itemid] = parent;\n return out;\n }, {})\n );\n\n if (this.canHaveOverlappingDropAreas(slots)) {\n // Currently only applies to boaxel range and the LAGKAPTEN tables.\n slots = this.removeSlotsUnderneathWiderSlots(slots);\n slots = this.adjustDropAreasForTables(parents, slots);\n this.drawDropAreasDefault(parents, slots);\n } else {\n this.drawDropAreasDefault(parents, slots);\n }\n }\n\n /**\n * Checks if the first slot belongs to an item of type table, then assumes the rest of the slots also belong to the same item.\n * Currently this function will only return true for the boaxel range tables.\n * @param {Array} slots\n * @returns {boolean} If the slots can have overlapping drop areas, which means they are of type table (LAGKAPTEN) and belong to the boaxel range.\n */\n canHaveOverlappingDropAreas(slots) {\n return productService.canHaveOverlappingDropAreas(slots[0]);\n }\n\n /**\n * Draws the drop areas for all slots, for each parent.\n * @param {Array} parents\n * @param {Array} slots\n */\n drawDropAreasDefault(parents, slots) {\n parents.forEach(parent => {\n const sprite = this.getSprite(parent.itemid);\n const slotsBelongingToParent = this.getSlotsBelongingToParent(\n parent,\n slots\n );\n sprite.drawDropAreas(slotsBelongingToParent);\n });\n }\n\n /**\n * Adjusts the drop areas by adding an attribute of horizontalOverlap\n * which determines how much the width of the drop-area should be adjusted.\n * @param {Array} parents\n * @param {Array} slots\n * @returns {Array} All slots, some with a horizontalOverlap attribute added.\n */\n adjustDropAreasForTables(parents, slots) {\n parents.sort((a, b) => a.x - b.x); // Sort the sections from the leftmost-placement to the rightmost-placement in the scene.\n const adjustedSlots = this.addHorizontalOverlapToRelevantSlots(\n parents,\n slots\n );\n return adjustedSlots;\n }\n\n /**\n * Recursively compares the currentParent with the nextParents\n * to determine if there is a horizontal overlap between any of their slots and then adds that horizontalOverlap as an attribute.\n * @param {Array} param0 A destructured array with the current parent being used as reference and the next parents it should be compared to.\n * @param {Array} slots The slots that will have a horizontalOverlap attributed added.\n * @returns {Array} The second parameter, slots, is mutated and then returned.\n */\n addHorizontalOverlapToRelevantSlots([currentParent, ...nextParents], slots) {\n if (!nextParents.length) return slots;\n const currentParentSlots = this.getSlotsBelongingToParent(\n currentParent,\n slots\n );\n nextParents.forEach(nextParent => {\n const nextParentSlots = this.getSlotsBelongingToParent(nextParent, slots);\n currentParentSlots.forEach(currentParentSlot => {\n nextParentSlots.forEach(nextParentSlot =>\n this.addHorizontalOverlapIfItExists(currentParentSlot, nextParentSlot)\n );\n });\n });\n return this.addHorizontalOverlapToRelevantSlots(nextParents, slots);\n }\n /**\n * Compares the currentParentSlot with the nextParentSlot\n * to see if the nextParentSlot overlaps horizontally with the currentParentSlot.\n * If there is an overlap the currentParentSlot receives a horizontalOverlap attribute (representing how much the nextParentSlot overlaps with the currentParentSlot)\n * The attribute is then used in the DropArea class to adjust the width of the relevant slots drop-area.\n * @param {Object} currentParentSlot\n * @param {Object} nextParentSlot\n */\n addHorizontalOverlapIfItExists(currentParentSlot, nextParentSlot) {\n if (\n this.isOverlappingPartiallyHorizontally(currentParentSlot, nextParentSlot)\n ) {\n const horizontalOverlap = this.getHorizontalOverlap(\n currentParentSlot,\n nextParentSlot\n );\n if (!currentParentSlot.horizontalOverlap)\n currentParentSlot.horizontalOverlap = horizontalOverlap;\n else if (horizontalOverlap > currentParentSlot.horizontalOverlap)\n currentParentSlot.horizontalOverlap = horizontalOverlap;\n }\n }\n /**\n * If two slots have the same x and y-coordinates it will remove the slot with the smaller width.\n * @example Assume the drop-areas (s1 and s2) are on-top of eachother.\n * |s1---------s1|\n * |s2----s2| This slot is considered to be underneath s1 and will be removed.\n * @param {Array} slots\n * @returns {Array} All slots without the slots that are underneath other slots.\n */\n removeSlotsUnderneathWiderSlots(slots) {\n const slotsUnderneath = this.getSlotsUnderneathWiderSlots([], slots);\n return slots.filter(slot => !slotsUnderneath.includes(slot));\n }\n\n /**\n * Gets the slots that are considered to be underneath other slots.\n * @param {Array} slotsUnderneath The slots that have the same x and y-coordinates as well as are overlapped entirely by another drop-area.\n * @param {Array} param1 This array is used and sent in as a paremeter recursively to compare the slot at the first index with the rest of the slots.\n * @returns {Array} The slots that are considered to be underneath other slots.\n */\n getSlotsUnderneathWiderSlots(slotsUnderneath, [currentItem, ...restArr]) {\n if (!restArr.length) return slotsUnderneath;\n\n const filteredSlotsUnderneath = restArr.filter(item => {\n return (\n this.slotsHaveSameCoordinates(item, currentItem) &&\n this.getSlotWithShortestWidth(item, currentItem)\n );\n });\n return filteredSlotsUnderneath.length\n ? this.getSlotsUnderneathWiderSlots(\n [...slotsUnderneath, ...filteredSlotsUnderneath],\n restArr\n )\n : this.getSlotsUnderneathWiderSlots(slotsUnderneath, restArr);\n }\n\n /**\n * Gets the slot with the shortest width of the two that are compared.\n * @param {Object} slot1\n * @param {Object} slot2\n * @returns {Object} The slot with the shortest width.\n */\n getSlotWithShortestWidth(slot1, slot2) {\n return slot1.width < slot2.width ? slot1 : slot2;\n }\n\n /**\n * Checks if the slots have the same x, y and z-coordinates.\n * @param {Object} slot1\n * @param {Object} slot2\n * @returns {boolean} If the slots have the same x, y and z-coordinates.\n */\n slotsHaveSameCoordinates(slot1, slot2) {\n return slot1.x === slot2.x && slot1.y === slot2.y && slot1.z === slot2.z;\n }\n\n /**\n * Gets all of the slots belonging to a specific parent/item.\n * @param {Object} parent\n * @param {Array} slots\n * @returns {Array} The slots that belong to the parent.\n */\n getSlotsBelongingToParent(parent, slots) {\n return slots.filter(slot => {\n return slot.parent.itemid === parent.itemid;\n });\n }\n\n /**\n * Checks to see if two slots partially overlap horizontally.\n * @example |s1-------|s2--s1|----s2| Slot1 partially overlaps with Slot2.\n * @example |s1s2-----s2|--s1|\n * @param {Object} slot1\n * @param {Object} slot2\n * @returns {boolean} If the two slots partially overlap.\n */\n isOverlappingPartiallyHorizontally(slot1, slot2) {\n return geometry.collides(slot1, slot2) && slot1.y === slot2.y;\n }\n\n /**\n * Gets the horizontal overlap between two slots by checking the width of the intersecting rectangle.\n * @param {Object} slot1\n * @param {Object} slot2\n * @returns {number} The width of the overlap in mm.\n */\n getHorizontalOverlap(slot1, slot2) {\n return geometry.intersectingRect(slot1, slot2).width;\n }\n\n eraseDropAreas() {\n this.children.forEach(sprite => {\n if (sprite.item && sprite.eraseDropAreas) {\n sprite.eraseDropAreas();\n }\n });\n }\n\n /**\n *\n * @param {*} item\n * @returns {Object | undefined} Sprite bounds, or undefined if not on scene\n */\n getItemBounds(item) {\n const sprite = this.getSprite(item.itemid);\n\n if (!sprite) {\n return;\n }\n\n sprite.toggleMultiParentChildren && sprite.toggleMultiParentChildren();\n const bounds = sprite.getBounds();\n sprite.toggleMultiParentChildren && sprite.toggleMultiParentChildren();\n return bounds;\n }\n\n getSprite(itemid) {\n if (itemid?.itemid) {\n // this was an item, not an id\n itemid = itemid.itemid;\n }\n return this.getFromMap(this.sprites, itemid);\n }\n\n getFromMap(sprites, itemid) {\n if (!sprites || !sprites.size) {\n return;\n }\n if (sprites.has(itemid)) {\n return sprites.get(itemid);\n }\n for (const item of sprites.entries()) {\n if (Array.isArray(item) && item[1]) {\n const found = this.getFromMap(item[1].sprites, itemid);\n if (found) {\n return found;\n }\n }\n }\n return;\n }\n}\n","import { getProppingBounds } from '../products/models';\nimport tacHelpers from '../../state/tac/tacHelpers';\nimport productService from '../products';\nimport geometry from '../../scene/util/geometry';\nimport ItemContainer from '../../scene/ItemContainer';\nimport { ITEMS } from '../../constants';\n\nexport function calcAvailableSpaceInParent(clothesRailPos, closestItemPos) {\n return closestItemPos ? clothesRailPos - closestItemPos : clothesRailPos;\n}\n\nexport function calcAvailableSpaceInScene(measuringAreaHeight, collidingRects) {\n if (collidingRects.length === 0) {\n // Floor is nearest colliding object\n return measuringAreaHeight;\n } else {\n const availableSpace = collidingRects.map(\n item => measuringAreaHeight - (item.y + item.height)\n );\n return Math.min(...availableSpace) || 0;\n }\n}\n\nfunction createMeasuringArea(sizingItem, globalCoords, parentItem) {\n // To find out if there are items under the clothes-rail we need\n // to create a rect that we use with getCollidingRects()\n const measuringArea = Object.assign({}, sizingItem, globalCoords);\n // The rect needs to stretch from the floor up to the clothes-rails y position\n measuringArea.height = measuringArea.y;\n measuringArea.y = 0;\n // We need to move the rects x position so that it won't\n // collide with the frames to the left\n measuringArea.x += parentItem.width - sizingItem.x + 1;\n // And then we need to make the width smaller so\n // that it won't collide with the frames on the right\n measuringArea.width -= (parentItem.width - sizingItem.x) * 2 + 1;\n return measuringArea;\n}\n\nexport function getAvailableProppingBounds(\n proppingBounds,\n availableSpace,\n parentHeight\n) {\n return proppingBounds\n .sort((a, b) => b.size.height - a.size.height)\n .filter(bound => {\n if (availableSpace) {\n if (bound.size.height < availableSpace) {\n return true;\n }\n } else {\n if (bound.size.height < parentHeight) {\n return true;\n }\n }\n return false;\n });\n}\n\nfunction getClosestItemBelow(items, omit = {}) {\n return items\n .filter(\n item =>\n (!item.itemid || item.itemid !== omit.itemid) &&\n item.y < omit.y &&\n !productService.isType(item, [ITEMS.FRAME, ITEMS.SHELVING_UNIT]) &&\n item.y > 0\n )\n .filter(item => {\n if (productService.isExtendable(omit)) {\n return productService.isExtendable(item);\n } else {\n return !productService.isExtendable(item);\n }\n })\n .sort((a, b) => b.y - a.y)\n .map(item => item.y + item.height)[0];\n}\n\n/**\n * Finds the largest (by height) propping that fits in the available space.\n * If there are several proppings of the highest fitting height, one of\n * those is chosen randomly. If none fits, null is returned.\n * @param {*} proppingBounds\n * @param {*} availableSpace\n * @param {*} padding\n * @returns The largest fitting propping, or null if none found.\n */\nexport function getLargestPropping(proppingBounds, availableSpace, padding) {\n const proppingsByHeight = {};\n proppingBounds.forEach(bound => {\n const height = bound.size.height;\n if (!proppingsByHeight[height]) proppingsByHeight[height] = [];\n proppingsByHeight[height].push(bound);\n });\n\n const availableHeightsDescending = Object.keys(proppingsByHeight).sort(\n (a, b) => b - a\n );\n\n let highestFittingHeight = null;\n for (let i = 0; i < availableHeightsDescending.length; i++) {\n if (availableHeightsDescending[i] < availableSpace - padding) {\n highestFittingHeight = availableHeightsDescending[i];\n break;\n }\n }\n\n if (!highestFittingHeight) return null;\n\n const proppingsOfHighestFittingHeight =\n proppingsByHeight[highestFittingHeight];\n const randomProppingOfHighestFittingHeight =\n proppingsOfHighestFittingHeight[\n Math.floor(Math.random() * proppingsOfHighestFittingHeight.length)\n ];\n\n return randomProppingOfHighestFittingHeight;\n}\n\nexport function getRandomPropping(item) {\n //FIXME: This currently returns a number, while rest of pickPropping returns obj.\n const proppingBounds = getProppingBounds(item);\n return Math.floor(Math.random() * proppingBounds.length + 1);\n}\n\nexport function replaceMovingItem(rects, movingItem) {\n /*\n If we are here due to another item moving in the scene,\n use that items moving rect instead of tac version.\n */\n const index = rects.findIndex(item => item.itemid === movingItem.itemid);\n if (index > -1) {\n rects[index] = movingItem;\n } else {\n rects.push(movingItem);\n }\n}\n\nexport function pickMultiParentCrPropping(args) {\n const {\n tac,\n item,\n proppingItem,\n parentItem,\n parentContainer,\n grandParentContainer,\n movingItem,\n } = args;\n\n const proppingBounds = getProppingBounds(proppingItem || item);\n if (movingItem) {\n // Re-use existing propping if possible to speed things up\n const topAncestor = tacHelpers.getTopAncestor(tac, item);\n const connectsToItem =\n item.connectsTo && tacHelpers.getItem(tac, item.connectsTo.itemid);\n const connectsToTopAncestor =\n connectsToItem && tacHelpers.getTopAncestor(tac, connectsToItem);\n const movingItemTopAncestor =\n movingItem && tacHelpers.getTopAncestor(tac, movingItem);\n if (\n item.propping &&\n ((movingItemTopAncestor &&\n movingItemTopAncestor.itemid === topAncestor.itemid) ||\n (connectsToTopAncestor &&\n connectsToTopAncestor.itemid === movingItemTopAncestor.itemid))\n ) {\n return proppingBounds.find(bound => bound.id === item.propping);\n }\n }\n\n let largestFittingPropping;\n\n if (\n !parentItem ||\n !parentItem.itemid ||\n parentContainer instanceof ItemContainer\n ) {\n // Parent is Scene, return hanger bounds.\n return proppingBounds[0];\n } else if (parentItem.itemid && !productService.isExtendable(item)) {\n // We are snapped inside a frame/section\n const clothesRailPos = item.y;\n const closestItemPos = getClosestItemBelow(\n parentContainer\n ? parentContainer.children.map(child => child.item)\n : parentItem.items,\n item\n );\n const availableSpace = calcAvailableSpaceInParent(\n clothesRailPos,\n closestItemPos\n );\n const availableProppingBounds = getAvailableProppingBounds(\n proppingBounds,\n availableSpace,\n parentItem.height\n );\n\n largestFittingPropping = availableProppingBounds[0];\n } else if (parentItem.itemid && productService.isExtendable(item)) {\n // We are snapped on the outside of a frame/section\n let availableSpace;\n let closestItemPos;\n let clothesRailPos = item.y;\n\n if (parentContainer) {\n // First check if we have any items below us in this container\n closestItemPos = getClosestItemBelow(\n parentContainer.children.map(child => child.item),\n item\n );\n\n if (!closestItemPos && grandParentContainer) {\n /*\n If not, check if we have a grandParent and if so\n whether we have any items below us in its corresponding container\n */\n closestItemPos = getClosestItemBelow(\n grandParentContainer.children.map(child => child.item),\n { ...item, y: parentItem.y + item.y }\n );\n\n if (closestItemPos) {\n // We found something, adjust item values to be relative the grandParent instead\n clothesRailPos += parentItem.y;\n }\n }\n }\n\n if (closestItemPos) {\n availableSpace = calcAvailableSpaceInParent(\n clothesRailPos,\n closestItemPos\n );\n } else {\n // Still no collisions found, let's try once more against all other items in scene\n const tacParentItem = tacHelpers.getItem(tac, parentItem.itemid);\n const parentGlobalCoords = tacHelpers.getGlobalCoords(tacParentItem, tac);\n const itemGlobalCoords = {\n x: parentGlobalCoords.x + item.x,\n y: parentGlobalCoords.y + item.y,\n z: parentGlobalCoords.z + item.z,\n };\n\n const rects = tacHelpers.getRects(tac, item, tac);\n\n if (movingItem) {\n const movingFullSize = {\n ...movingItem,\n ...tacHelpers.getFullSize(movingItem),\n };\n\n replaceMovingItem(rects, movingFullSize);\n }\n\n const measuringArea = createMeasuringArea(\n item,\n itemGlobalCoords,\n parentItem\n );\n\n const collidingRects = geometry.getCollidingRects(measuringArea, rects);\n\n availableSpace = calcAvailableSpaceInScene(\n measuringArea.height,\n collidingRects\n );\n }\n\n largestFittingPropping = getLargestPropping(\n proppingBounds,\n availableSpace,\n 20\n );\n }\n\n // Fallback to hangers\n return largestFittingPropping || proppingBounds[0];\n}\n","import { getRandomPropping, pickMultiParentCrPropping } from '../common';\nimport productService from '../../products';\nimport { ITEMS } from '../../../constants';\n\nfunction pickPropping(args) {\n const { isDynamic, proppingItem, item } = args;\n if (!isDynamic) {\n return getRandomPropping(proppingItem || item);\n } else if (productService.isType(item, ITEMS.CLOTHES_RAIL)) {\n return pickMultiParentCrPropping(args);\n }\n}\n\nexport default { pickPropping };\n","import { getProppingBounds } from '../../products/models';\nimport tacHelpers from '../../../state/tac/tacHelpers';\nimport productService from '../../products';\nimport {\n calcAvailableSpaceInParent,\n calcAvailableSpaceInScene,\n getLargestPropping,\n getRandomPropping,\n replaceMovingItem,\n} from '../common';\nimport geometry from '../../../scene/util/geometry';\nimport { ITEMS } from '../../../constants';\n\nfunction getClosestItemBelow(items, omit = {}) {\n return items\n .filter(\n item =>\n (!item.itemid || item.itemid !== omit.itemid) &&\n item.y < omit.y &&\n item.y > 0\n )\n .sort((a, b) => b.y - a.y)\n .map(item => item.y + item.height)[0];\n}\n\nfunction pickPropping({\n isDynamic,\n tac,\n item,\n parentItem,\n section,\n movingItem,\n movingSprite,\n}) {\n if (!isDynamic) {\n return getRandomPropping(item);\n }\n\n const proppingBounds = getProppingBounds(item);\n\n if (!parentItem || !parentItem.itemid) {\n // Parent is Scene, return default bounds.\n return proppingBounds[0];\n }\n\n let space;\n let closest;\n let myPos = item.y;\n\n if (section) {\n closest = getClosestItemBelow(\n section.childrenContainer.children.map(child => child.item),\n { ...item, y: parentItem.y + item.y }\n );\n\n if (closest) {\n // We found something, adjust item values to be relative to the section instead\n myPos += parentItem.y;\n }\n }\n\n function createMeasuringArea(sizingItem, globalCoords, parentItem) {\n const postWidth = productService.getPostWidth();\n\n // To find out if there are items under the clothes-rail we need\n // to create a rect that we use with getCollidingRects()\n const measuringArea = Object.assign({}, sizingItem, globalCoords);\n\n // The rect needs to stretch from the floor up to the clothes-rails y position\n measuringArea.height = measuringArea.y;\n measuringArea.y = 0;\n\n // It should stretch all the way back to wall to not block items placed behind\n const oldZ = measuringArea.z;\n measuringArea.z = 0;\n measuringArea.depth += oldZ;\n\n // Make it half a post width narrower on each side since we don't care about,\n // collisions with our own posts\n measuringArea.x += postWidth / 2;\n measuringArea.width -= postWidth;\n\n return measuringArea;\n }\n\n if (closest) {\n space = calcAvailableSpaceInParent(myPos, closest);\n } else {\n // No collisions found inside section, let's try once more against all other items in scene\n const tacParentItem = tacHelpers.getItem(tac, parentItem.itemid);\n const globalParent = tacHelpers.getGlobalCoords(tacParentItem, tac);\n const globalItem = {\n x: globalParent.x + item.x,\n y: globalParent.y + item.y,\n z: globalParent.z + item.z,\n };\n\n const rects = tacHelpers\n .getRects(tac, item, tac, true)\n // We don't care about collisions with sections or brackets\n .filter(rect => !productService.isType(rect, [ITEMS.SECTION, 'bracket']));\n\n if (movingItem) {\n const movingFullSize = {\n ...movingItem,\n ...tacHelpers.getFullSize(movingItem),\n };\n\n const globalMoving = {\n ...movingFullSize,\n };\n\n if (movingSprite?.parentItem?.itemid) {\n const parentOfMoving = tacHelpers.getGlobalCoords(\n movingSprite.parentItem,\n tac\n );\n\n globalMoving.x += parentOfMoving.x;\n globalMoving.y += parentOfMoving.y;\n globalMoving.z += parentOfMoving.z;\n }\n\n const newRect = tacHelpers.getRects({ items: [globalMoving] })[0];\n\n replaceMovingItem(rects, newRect);\n }\n\n const measuringArea = createMeasuringArea(item, globalItem, parentItem);\n\n const collidingRects = geometry.getCollidingRects(measuringArea, rects);\n\n space = calcAvailableSpaceInScene(measuringArea.height, collidingRects);\n }\n\n const largestFittingPropping = getLargestPropping(proppingBounds, space, 50);\n\n // Fallback to default bounds\n return largestFittingPropping || proppingBounds[0];\n}\n\nexport default { pickPropping };\n","import { getRandomPropping, pickMultiParentCrPropping } from '../common';\nimport productService from '../../products';\nimport { ITEMS } from '../../../constants';\n\nfunction pickPropping(args) {\n const { isDynamic, proppingItem, item } = args;\n if (!isDynamic) {\n return getRandomPropping(proppingItem || item);\n } else if (productService.isType(item, ITEMS.CLOTHES_RAIL)) {\n return pickMultiParentCrPropping(args);\n }\n}\n\nexport default { pickPropping };\n","import { getRandomPropping } from '../common';\n\nfunction pickPropping({ item }) {\n return getRandomPropping(item);\n}\n\nexport default { pickPropping };\n","import { getProppingBounds } from '../../products/models';\nimport tacHelpers from '../../../state/tac/tacHelpers';\nimport productService from '../../products';\nimport {\n calcAvailableSpaceInParent,\n calcAvailableSpaceInScene,\n getLargestPropping,\n getRandomPropping,\n} from '../common';\nimport { ITEMS } from '../../../constants';\n\nfunction getClosestItemBelow(items, omit = {}, excludedTypes = []) {\n const closestItemBelow = items\n .filter(\n item =>\n (!item.itemid || item.itemid !== omit.itemid) &&\n item.y < omit.y &&\n item.y > 0 &&\n !excludedTypes.includes(item.filter.type)\n )\n .sort((a, b) => b.y - a.y)[0];\n\n return closestItemBelow ? closestItemBelow.y + closestItemBelow.height : null;\n}\n\nfunction pickPropping({ isDynamic, tac, item, parentItem, section }) {\n if (!isDynamic) {\n return getRandomPropping(item);\n }\n\n const proppingBounds = getProppingBounds(item);\n\n if (!parentItem || !parentItem.itemid) {\n // Parent is Scene, return default bounds.\n return proppingBounds[0].id;\n }\n\n let space;\n let closest;\n let myPos = item.y;\n\n if (section) {\n const excludedTypes = [ITEMS.POST, ITEMS.SIDE_PANEL, ITEMS.CROSS_BRACE];\n closest = getClosestItemBelow(\n section.childrenContainer.children.map(child => child.item),\n { ...item, y: parentItem.y + item.y },\n excludedTypes\n );\n\n if (closest) {\n // We found something, adjust item values to be relative to the section instead\n myPos += parentItem.y;\n }\n }\n\n function createMeasuringArea(sizingItem, globalCoords) {\n const postWidth = productService.getPostWidth();\n\n // To find out if there are other items under the item, we need\n // to create a rect that we use with getCollidingRects()\n const measuringArea = Object.assign({}, sizingItem, globalCoords);\n\n // The rect needs to stretch from the floor up to the clothes-rails y position\n measuringArea.height = measuringArea.y;\n measuringArea.y = 0;\n\n // It should stretch all the way back to wall to not block items placed behind\n const oldZ = measuringArea.z;\n measuringArea.z = 0;\n measuringArea.depth += oldZ;\n\n // Make it half a post width narrower on each side since we don't care about,\n // collisions with our own posts\n measuringArea.x += postWidth / 2;\n measuringArea.width -= postWidth;\n\n return measuringArea;\n }\n\n if (closest) {\n space = calcAvailableSpaceInParent(myPos, closest);\n } else {\n const tacParentItem = tacHelpers.getItem(tac, parentItem.itemid);\n const globalParent = tacHelpers.getGlobalCoords(tacParentItem, tac);\n const globalItem = {\n x: globalParent.x + item.x,\n y: globalParent.y + item.y,\n z: globalParent.z + item.z,\n };\n\n const measuringArea = createMeasuringArea(item, globalItem, parentItem);\n\n space = calcAvailableSpaceInScene(measuringArea.height, []);\n }\n\n const largestFittingPropping = getLargestPropping(proppingBounds, space, 50);\n\n // Fallback to default bounds\n return largestFittingPropping?.id || proppingBounds[0].id;\n}\n\nexport default { pickPropping };\n","import { applicationSettings } from '../../settings/application';\nimport { getProppingBounds } from '../products/models';\nimport Bror from './bror';\nimport Jonaxel from './jonaxel';\nimport Boaxel from './boaxel';\nimport Aurdal from './aurdal';\nimport Ivar from './ivar';\nimport Elvarli from './elvarli';\nimport { getAvailableProppingBounds } from './common';\n\nfunction getRangeApi() {\n switch (applicationSettings.applicationName) {\n case 'BROR':\n return Bror;\n case 'JONAXEL':\n return Jonaxel;\n case 'BOAXEL':\n return Boaxel;\n case 'AURDAL':\n return Aurdal;\n case 'IVAR':\n return Ivar;\n case 'ELVARLI':\n return Elvarli;\n default:\n console.error(\n 'Missing range-specific implementation of propping service'\n );\n return {};\n }\n}\n\nconst rangeApi = getRangeApi();\n\nfunction smallestPropping(proppingItem) {\n const proppingBounds = getProppingBounds(proppingItem);\n const allBounds = getAvailableProppingBounds(\n proppingBounds,\n Infinity,\n Infinity\n );\n return allBounds.pop();\n}\n\nfunction pickPropping(args) {\n throw new Error(\n 'NotImplementedError',\n 'Range-specific implementation needed'\n );\n}\n\nconst api = {\n smallestPropping,\n pickPropping,\n};\n\nObject.assign(api, rangeApi);\n\nexport default api;\n","import project from './project';\nimport { vec3 } from 'gl-matrix';\n\nexport default function getDepthOffset(item) {\n const gap = {\n width: 0,\n height: 0,\n depth: item.z,\n };\n\n const gapVertices = project.calculateVertices(gap);\n let gapProjectedVertices = gapVertices.map(project.point);\n const backBottomLeft = gapProjectedVertices[4];\n gapProjectedVertices = gapProjectedVertices.map(vertex =>\n vec3.sub(vec3.create(), vertex, backBottomLeft)\n );\n\n const x = gapProjectedVertices[4][0] - gapProjectedVertices[0][0];\n const y = gapProjectedVertices[4][1] - gapProjectedVertices[0][1];\n\n return { x, y };\n}\n","import * as PIXI from 'pixi.js-legacy';\nimport _ from 'lodash';\nimport Base from './Base';\nimport InnerDropArea from './InnerDropArea';\nimport constants from '../settings/constants';\nimport { SHOW_CONF, PICKUP_ITEM, SCENE_REDRAWN } from '../settings/events';\nimport productService from '../services/products';\nimport proppingService from '../services/propping';\nimport {\n POINTER_DOWN,\n POINTER_MOVE,\n POINTER_UP,\n POINTER_UP_OUTSIDE,\n} from '../util/supportedEvents';\nimport emitter from '../emitter';\nimport geometry from './util/geometry';\nimport getComponent from './util/getComponent';\nimport getDepthOffset from './util/getDepthOffset';\nimport tacHelpers from '../state/tac/tacHelpers';\nimport { getProppingBounds, topPropBounds } from '../services/products/models';\nimport project from './util/project';\nimport { mergePolygons } from '../util/mergePolygons';\nimport { isStandAlone } from '../services/products/models';\nimport { ITEMS } from '../constants';\n\nconst DRAGGING_THRESHOLD = 2;\n\nconst LEFT = 'left';\nconst RIGHT = 'right';\n\nexport default class Item extends Base {\n constructor(args) {\n super(args);\n\n this.app = args.app;\n this.origo = args.origo || { x: 0, y: 0 };\n this.onDragStart = args.onDragStart;\n this.pickUp = this.pickUp.bind(this);\n this.options = args.options;\n this.measurementsActive = args.options.measurementsActive;\n\n if (this.onDragStart) {\n this.interactive = true;\n this.cursor = 'pointer';\n }\n\n this.onHitAreaRequired = this.onHitAreaRequired.bind(this);\n this.db_onHitAreaRequired = _.debounce(this.onHitAreaRequired, 100, {\n leading: false,\n trailing: true,\n });\n\n emitter.on(SCENE_REDRAWN, this.db_onHitAreaRequired);\n\n this.on(POINTER_DOWN, this.onPointerDown);\n this.update(args);\n }\n\n destroy(args) {\n emitter.off(SCENE_REDRAWN, this.db_onHitAreaRequired);\n super.destroy(args);\n }\n\n onHitAreaRequired() {\n this.setHitArea();\n }\n\n //\n // Pointer events\n\n onPointerUp = (event, abort = false) => {\n if (!this._pointerPos || this.disableInteraction) {\n return;\n }\n\n this._pointerPos = null;\n\n this.off(POINTER_MOVE, this.onPointerMove);\n this.off(POINTER_UP, this.onPointerUp);\n this.off(POINTER_UP_OUTSIDE, this.onPointerUp);\n\n if (!abort && event.type !== POINTER_UP_OUTSIDE && !this._isDragging) {\n emitter.emit(SHOW_CONF, this);\n }\n };\n\n onPointerDown = event => {\n event.stopPropagation();\n\n if (this.wallResizerActive || this.disableInteraction) {\n return;\n }\n\n this.on(POINTER_UP, this.onPointerUp);\n this.on(POINTER_MOVE, this.onPointerMove);\n\n this._isDragging = false;\n\n if (\n productService.isSection(this.parentItem) &&\n this.parent &&\n this.parent.parent\n ) {\n this.parent.parent.on(POINTER_UP_OUTSIDE, this.onPointerUp);\n }\n\n this._pointerPos = {\n x: event.data.global.x,\n y: event.data.global.y,\n };\n };\n\n onPointerMove = event => {\n if (!event.data.buttons) {\n // moving without buttons means that user started\n // dragging too close to the border to pick it up,\n // and is now returning from a perceived failed drag\n this.onPointerUp(event, true);\n }\n\n if (!this._pointerPos || this._isDragging || this.disableInteraction) {\n return;\n }\n\n const currentX = event.data.global.x;\n const currentY = event.data.global.y;\n\n this._isDragging =\n Math.abs(currentX - this._pointerPos.x) > DRAGGING_THRESHOLD ||\n Math.abs(currentY - this._pointerPos.y) > DRAGGING_THRESHOLD;\n\n if (this._isDragging) {\n event.data.global = this._pointerPos;\n this.pickUp(event);\n }\n };\n\n pickUp(e) {\n let mode;\n let position;\n\n if (productService.isInsert(this.item) && !isStandAlone(this.item)) {\n this.dummy = true;\n mode = constants.DRAG_MODE.INSERT;\n position = this.toGlobal(new PIXI.Point(0, 0));\n } else if (this.parent && this.parent.parent && this.parent.parent.item) {\n // TODO fix above condition\n // this is a child item\n this.dummy = true;\n mode = constants.DRAG_MODE.MIXED;\n position = this.toGlobal(new PIXI.Point(0, 0));\n } else {\n mode = constants.DRAG_MODE.FLOAT;\n position = { x: this.x, y: this.y };\n }\n\n const offset = {\n x: e.data.global.x - position.x,\n y: e.data.global.y - position.y,\n };\n\n this.onDragStart({\n data: this.data,\n item: this.item,\n mode: mode,\n mouse: Object.assign({}, e.data.global),\n offset: offset,\n sprite: this,\n });\n\n emitter.emit(PICKUP_ITEM, this);\n }\n\n getAllChildren() {\n return this.childrenContainer.children;\n }\n\n getAllContainers() {\n return this.children.filter(child => this.isPureContainer(child));\n }\n\n getSelfHitArea() {\n const { scaledVertices } = this.data;\n let vertices;\n\n if (\n !geometry.extendsOutside(this.item, this.parentItem) &&\n this.parentItem.itemid\n ) {\n const padding = 4;\n vertices = [\n [scaledVertices[0][0] + padding, scaledVertices[0][1]],\n [scaledVertices[3][0] + padding, scaledVertices[3][1]],\n scaledVertices[7],\n [scaledVertices[2][0] - padding, scaledVertices[6][1]],\n [scaledVertices[2][0] - padding, scaledVertices[2][1]],\n [scaledVertices[1][0] - padding, scaledVertices[1][1]],\n ];\n } else {\n vertices = [\n scaledVertices[0],\n scaledVertices[1],\n scaledVertices[5],\n scaledVertices[6],\n scaledVertices[7],\n scaledVertices[3],\n ];\n }\n\n return {\n regions: [vertices],\n };\n }\n\n getHitArea() {\n let polygon = this.getSelfHitArea();\n this.getAllChildren()\n .filter(child => !this.excludeChildFromHitArea?.(child))\n .forEach(child => {\n const childRegions = child.getHitArea().polygon.regions;\n const regions = childRegions.map(region =>\n region.map(vertex => [\n vertex[0] + child.x + child.parent.x,\n vertex[1] + child.y + child.parent.y,\n ])\n );\n polygon = mergePolygons(polygon, {\n regions: regions,\n });\n });\n\n if (polygon.regions.length > 1) {\n const filler = this.getFiller(polygon.regions);\n polygon = mergePolygons(polygon, {\n regions: [filler],\n });\n }\n\n return {\n polygon,\n };\n }\n\n setHitArea() {\n const polygon = this.getHitArea().polygon;\n this.hitArea = new PIXI.Polygon([].concat(...polygon.regions?.[0]));\n }\n\n getResourceName(item) {\n const proppingBounds = getProppingBounds(item);\n if (this.shouldShowPropping(proppingBounds)) {\n if (!item.propping) {\n item.propping = proppingService.pickPropping({\n isDynamic: false,\n tac: this.app.tac,\n item,\n });\n }\n return `${item.id}_propping_${item.propping}`;\n } else {\n return item.id;\n }\n }\n\n shouldShowPropping(proppingBounds) {\n if (!this.options.showPropping || this.measurementsActive) {\n return false;\n }\n return proppingBounds.length > 0;\n }\n\n draw() {\n const { item, ratio } = this;\n const { outerSize } = this.data;\n this.sprite = this.sprite || new PIXI.Sprite();\n\n this.bracketContainers = this.bracketContainers || {\n left: new PIXI.Container(),\n right: new PIXI.Container(),\n };\n this.brackets = this.brackets || {};\n this.visible = !item.hidden;\n\n if (item.keepDuringDrag) {\n this.alpha = 0.5;\n } else {\n this.alpha = 1;\n }\n\n let resourceName;\n\n if (productService.isAddOnShelf(item)) {\n resourceName = `${item.id}:${\n item.x != null && item.x < 0 ? LEFT : RIGHT\n }`;\n } else {\n resourceName = this.getResourceName(item);\n }\n const resource = PIXI.Loader.shared.resources[resourceName];\n this.sprite.texture = resource.texture;\n\n this.sprite.width = outerSize.width * ratio;\n this.sprite.height = outerSize.height * ratio;\n if (item.propping) {\n const proppedBounds = getProppingBounds(item);\n if (this.shouldShowPropping(proppedBounds)) {\n const proppedSize = project.outer(\n proppedBounds[item.propping - 1].size\n );\n this.sprite.height = proppedSize.height * ratio;\n }\n }\n if (!this.bracketContainers[LEFT].parent) {\n this.addChild(this.bracketContainers[LEFT]);\n }\n this.drawBracket(LEFT);\n\n if (!this.sprite.parent) {\n this.addChild(this.sprite);\n }\n\n const { scaledVertices } = this.data;\n\n this.dropAreas = this.dropAreas || new PIXI.Container();\n this.dropAreas.x = scaledVertices[4][0] - scaledVertices[0][0];\n\n if (!this.dropAreas.parent) {\n this.addChild(this.dropAreas);\n }\n\n this.childrenContainer = this.childrenContainer || new PIXI.Container();\n this.childrenContainer.x = scaledVertices[4][0] - scaledVertices[0][0];\n\n if (!this.childrenContainer.parent) {\n this.addChild(this.childrenContainer);\n }\n this.drawChildren();\n if (!this.bracketContainers[RIGHT].parent) {\n this.addChild(this.bracketContainers[RIGHT]);\n }\n this.drawBracket(RIGHT);\n }\n\n // Override with an implementation as needed in ranges/items with brackets.\n drawBracket(side) {}\n\n _getProppingTop() {\n const proppedBounds = getProppingBounds(this.item);\n let proppingTop = 0;\n if (this.shouldShowPropping(proppedBounds)) {\n const proppingOffset = topPropBounds(this.item);\n proppingTop = proppingOffset.height;\n }\n return proppingTop;\n }\n\n place(x = this.item.x, y = this.item.y) {\n const { origo, data, parentItem, ratio } = this;\n\n if (this.item.z > 0) {\n const depthOffset = getDepthOffset(this.item);\n\n x -= depthOffset.x;\n y -= depthOffset.y;\n }\n const proppedBounds = getProppingBounds(this.item);\n if (this.shouldShowPropping(proppedBounds)) {\n const proppingOffset = topPropBounds(this.item);\n y += proppingOffset.height;\n }\n this.x =\n (x -\n (data.projectedVertices[4][0] - data.projectedVertices[0][0]) -\n origo.x) *\n ratio;\n this.y =\n (parentItem.height - (data.faceSize.height + y) + this.origo.y) * ratio;\n }\n\n update({ origo, ratio, item, parentItem, options }) {\n this.wallResizerActive = options.wallResizerActive;\n\n this.interactive = !options.wallResizerActive;\n\n if (\n (!!this.measurementsActive !== !!options.measurementsActive ||\n !!this.options.showPropping !== !!options.showPropping) &&\n this.bracketContainers\n ) {\n this.bracketContainers[LEFT].removeChildren();\n this.bracketContainers[RIGHT].removeChildren();\n }\n\n this.measurementsActive = options.measurementsActive;\n\n if (options && options !== this.options) {\n this.options = options;\n }\n\n if (ratio && this.ratio !== ratio) {\n this.ratio = ratio;\n if (this.bracketContainers) {\n this.bracketContainers[LEFT].removeChildren();\n this.bracketContainers[RIGHT].removeChildren();\n }\n }\n\n if (origo && origo !== this.origo) {\n this.origo = origo;\n }\n\n if (item && item !== this.item) {\n if (this.bracketContainers && item.width !== this.item.width) {\n this.bracketContainers[RIGHT].removeChildren();\n }\n\n this.item = item;\n }\n\n if (parentItem && parentItem !== this.parentItem) {\n this.parentItem = parentItem;\n }\n\n this.visible = true;\n this.calculateProjection();\n this.draw();\n this.place();\n }\n\n /*\n decide what container a new sprite belongs in\n */\n getContainer(sprite) {\n return this.childrenContainer;\n }\n\n addSprite(sprite, slot, tac) {\n const item = Object.assign({}, sprite.item);\n const parentPos = tacHelpers.getGlobalCoords(this.item, tac);\n\n const refitted = productService.getFit(item, slot);\n // product is null if no refit was done\n\n if (refitted) {\n refitted.x = slot.x - parentPos.x;\n refitted.y = slot.y - parentPos.y;\n refitted.z = slot.z - parentPos.z;\n }\n item.x = slot.x - parentPos.x;\n item.y = slot.y - parentPos.y;\n item.z = slot.z - parentPos.z;\n\n const fullFit = refitted\n ? tacHelpers.getSwitchableItem(item, refitted, {\n newParent: this.item,\n })\n : refitted;\n\n Object.assign(item, refitted, fullFit);\n\n sprite.update({\n item: item,\n parentItem: this.item,\n options: this.options,\n origo: { x: 0, y: 0 },\n });\n\n const container = this.getContainer(sprite);\n container.addChild(sprite);\n this.displayDropAreas(slot);\n\n sprite.once('added', () => {\n if (sprite.parent !== container) {\n this.displayDropAreas();\n }\n });\n\n this.sortChildren();\n\n return sprite;\n }\n\n /**\n * Cleans up an old sprite that no longer has a connection to a child.\n * Default implementation is to just destroy the sprite, but this is\n * overridden with additional logic in some item type classes.\n *\n * @param {Object} sprite\n * @return {undefined}\n */\n handleOldSprite(sprite) {\n sprite.destroy();\n }\n\n drawChildren() {\n const { item } = this;\n\n const oldSprites = this.sprites;\n\n this.sprites = new Map();\n\n if (Array.isArray(item.items)) {\n item.items.forEach(child => {\n /* We don't want brackets drawn here.\n They are drawn in drawBracket instead. */\n if (productService.isType(child, ITEMS.BRACKET)) return;\n\n const sprite = oldSprites && oldSprites.get(child.itemid);\n\n if (sprite && sprite.parent) {\n sprite.update({\n item: child,\n parentItem: item,\n options: this.options,\n ratio: this.ratio,\n });\n\n oldSprites.delete(child.itemid);\n this.sprites.set(child.itemid, sprite);\n } else {\n this.addItem(child);\n }\n });\n\n item.items.length && this.sortChildren();\n }\n if (oldSprites) {\n oldSprites.forEach(sprite => this.handleOldSprite(sprite));\n }\n }\n\n addItem(item) {\n const sprite = getComponent({\n options: this.options,\n item: item,\n parentItem: this.item,\n ratio: this.ratio,\n onDragStart: this.onDragStart,\n app: this.app,\n });\n this.getContainer(sprite).addChild(sprite);\n\n this.sprites.set(item.itemid, sprite);\n\n this.sortChildren();\n\n return sprite;\n }\n\n getDropAreaSprite(localSlot, width) {\n const localItem = {\n ...this.item,\n x: 0,\n y: 0,\n };\n if (geometry.collides(localSlot, localItem)) {\n const sprite = new InnerDropArea({\n width: width,\n height: localSlot.height * this.ratio,\n ratio: this.ratio,\n slot: localSlot,\n });\n\n sprite.slot = localSlot;\n\n sprite.x = 0;\n sprite.y =\n (this.item.height - (localSlot.y + localSlot.height)) * this.ratio;\n\n return sprite;\n }\n }\n\n drawDropAreas(slots) {\n slots = slots.filter(slot => slot.parent.itemid === this.item.itemid);\n\n const width = Math.floor(this.data.faceSize.width * this.ratio) - 2;\n\n slots.forEach(slot => {\n const localSlot = { ...slot, ...slot.local };\n localSlot.width = slot.width; // TODO does this break anything?\n const sprite = this.getDropAreaSprite(localSlot, width, slots);\n sprite && this.dropAreas.addChild(sprite);\n });\n }\n\n eraseDropAreas() {\n this.dropAreas.removeChildren();\n this.childrenContainer.children.forEach(child => child.eraseDropAreas());\n }\n\n displayDropAreas(slot) {\n this.displayDropArea(this.dropAreas, slot);\n }\n\n displayDropArea(dropAreas, slot) {\n const localSlot = slot && { ...slot, ...slot.local };\n dropAreas.children.forEach(dropArea => {\n this.toggleDropAreaVisibility(dropArea, localSlot);\n }, this);\n }\n\n toggleMultiParentChildren() {\n if (this.item && productService.isMultiParentProduct(this.item)) {\n this.visible = !this.visible;\n }\n\n this.getAllContainers().forEach(container => {\n container.children.forEach(grandChild => {\n grandChild.toggleMultiParentChildren &&\n grandChild.toggleMultiParentChildren();\n });\n });\n }\n\n toggleDropAreaVisibility(dropArea, localSlot) {\n dropArea.visible =\n dropArea.slot.y !== localSlot?.y || dropArea.slot.x !== localSlot?.x;\n }\n\n distance(a, b) {\n const x = a[0] - b[0];\n const y = a[1] - b[1];\n return x * x + y * y;\n }\n\n getFiller(regions) {\n const first = regions[0];\n const second = regions[1];\n const distances = [];\n first.forEach(start => {\n second.forEach(stop => {\n const res = {\n start,\n stop,\n distance: this.distance(start, stop),\n };\n\n distances.push(res);\n });\n });\n\n distances.sort((a, b) => a.distance - b.distance);\n\n const left = distances[0];\n const right = distances[1];\n\n const filler = [left.start, right.start, right.stop, left.stop];\n return filler;\n }\n}\n","import * as PIXI from 'pixi.js-legacy';\n\nimport productService from '../../services/products';\nimport tacHelpers from '../../state/tac/tacHelpers';\nimport geometry from '../util/geometry';\nimport project from '../util/project';\n\nconst LEFT = 'left';\nconst RIGHT = 'right';\n\nconst bracketsMixin = superclass =>\n class extends superclass {\n // Override\n drawBracket(side) {\n if (!productService.getFittingBracket(this.item)) {\n // this is not a bracketed item\n return;\n }\n if (this._isDragging) {\n this.bracketContainers[side].alpha = 0;\n return;\n }\n this.bracketContainers[side].alpha = 1;\n\n let bracket = tacHelpers.getItem(\n this.app.tac,\n this.brackets?.[side]?.itemRef\n );\n\n if (this.brackets?.[side]?.parent) {\n if (bracket) {\n return;\n } else {\n this.bracketContainers[side].removeChildren();\n delete this.brackets[side];\n }\n }\n\n const parent = tacHelpers.getParent(this.app.tac, this.item);\n if (!parent) {\n return;\n }\n\n const proppingTop = this._getProppingTop();\n\n const tacItem = tacHelpers.getItem(this.app.tac, this.item.itemid);\n const gItem = tacHelpers.getGlobalCoords(tacItem, this.app.tac);\n\n if (!bracket) {\n const brackets = parent.uprights\n .map(upright => {\n return tacHelpers.getItem(this.app.tac, upright.itemid);\n })\n .filter(upright =>\n side === LEFT ? upright.x <= parent.x : upright.x > parent.x\n )\n .flatMap(upright => upright.items)\n .filter(item => productService.isType(item, 'bracket'));\n\n const tacItem = tacHelpers.getItem(this.app.tac, this.item.itemid);\n const gItem = tacHelpers.getGlobalCoords(tacItem, this.app.tac);\n const padding =\n side === RIGHT ? { left: -50, right: 20 } : { left: 20, right: -50 };\n\n padding.bottom = 1;\n\n // Possible to collide with multiple brackets, then choose the closest one on the Y-axis\n const collidingBrackets = brackets.filter(bracket => {\n const gBracket = tacHelpers.getGlobalCoords(bracket, this.app.tac);\n return geometry.collides(gItem, gBracket, padding);\n });\n\n if (collidingBrackets.length > 1) {\n bracket = collidingBrackets.reduce((prev, curr) => {\n const gBracketPrev = tacHelpers.getGlobalCoords(prev, this.app.tac);\n const gBracketCurr = tacHelpers.getGlobalCoords(curr, this.app.tac);\n return Math.abs(\n gBracketCurr.y + gBracketCurr.height - (gItem.y + gItem.height)\n ) <\n Math.abs(\n gBracketPrev.height + gBracketPrev.y - (gItem.height + gItem.y)\n )\n ? curr\n : prev;\n });\n } else {\n bracket = collidingBrackets[0];\n }\n\n if (!bracket) {\n return;\n }\n }\n\n const gBracket = tacHelpers.getGlobalCoords(bracket, this.app.tac);\n\n const bracketOffset =\n gBracket.y + gBracket.height - (gItem.y + gItem.height);\n\n // we've found the bracket we want\n const resourceName = this.getResourceName(bracket);\n const resource = PIXI.Loader.shared.resources[resourceName];\n this.brackets[side] =\n this.brackets[side] || new PIXI.Sprite(resource.texture);\n const outerSize = project.outer(bracket);\n this.brackets[side].width = outerSize.width * this.ratio;\n this.brackets[side].height = outerSize.height * this.ratio;\n this.brackets[side].itemRef = bracket.itemid;\n if (side === RIGHT) {\n this.brackets[side].x = this.sprite.width - this.brackets[side].width;\n } else if (side === LEFT) {\n const bracketPairBox = {\n ...bracket,\n width: this.item.width,\n };\n const bracketPairBoxSize = project.outer(bracketPairBox);\n\n this.brackets[side].x =\n this.sprite.width - bracketPairBoxSize.width * this.ratio;\n }\n this.brackets[side].y = (proppingTop - bracketOffset) * this.ratio;\n\n if (!this.brackets[side].parent) {\n this.bracketContainers[side].addChild(this.brackets[side]);\n }\n }\n };\n\nexport default bracketsMixin;\n","import Item from '../Item';\nimport bracketsMixin from './bracketsMixin';\n\nexport default class BoaxelItem extends bracketsMixin(Item) {}\n","import * as PIXI from 'pixi.js-legacy';\nimport Base from './Base';\n\nimport constants from '../settings/constants';\n\nexport default class DropArea extends Base {\n constructor(options) {\n super(options);\n\n this.ratio = options.ratio;\n this.slot = options.slot;\n\n this.draw(options.width, options.height);\n }\n\n draw() {\n const { slot, ratio } = this;\n const height = Math.ceil(constants.DROP_AREA_MAX_HEIGHT * ratio);\n\n const back = new PIXI.Graphics()\n .beginFill(0xfafafa)\n .drawRect(0, 0, slot.width * ratio, height)\n .endFill();\n\n back.alpha = 0.5;\n back.x = 1;\n back.y = 0;\n\n this.addChild(back);\n\n const arrowHeight = height;\n const arrowWidth = arrowHeight / 2;\n const arrowY = (height - arrowHeight) / 2;\n\n if (slot.x - slot.parent.x >= slot.parent.width) {\n const rightArrow = new PIXI.Graphics()\n .beginFill(0x407ab1)\n .drawPolygon(0, arrowHeight / 2, arrowWidth, 0, arrowWidth, arrowHeight)\n .endFill();\n\n rightArrow.x = 0;\n rightArrow.y = arrowY;\n\n this.addChild(rightArrow);\n } else {\n const leftArrow = new PIXI.Graphics()\n .beginFill(0x407ab1)\n .drawPolygon(0, 0, 0, arrowHeight, arrowWidth, arrowHeight / 2)\n .endFill();\n\n leftArrow.x = slot.width * ratio - leftArrow.width;\n leftArrow.y = arrowY;\n\n this.addChild(leftArrow);\n }\n }\n}\n","import * as PIXI from 'pixi.js-legacy';\nimport project from '../util/project';\nimport Item from '../Item';\nimport InnerDropArea from '../InnerDropArea';\nimport OuterDropArea from '../OuterDropArea';\nimport geometry from '../util/geometry';\nimport constants from '../../settings/constants';\nimport productsService, { getProduct, isType } from '../../services/products';\nimport { mergePolygons } from '../../util/mergePolygons';\nimport { ITEMS } from '../../constants';\nimport productService from '../../services/products';\n\nexport default class Section extends Item {\n constructor(...args) {\n super(...args);\n\n this.drawChildren();\n }\n\n draw() {\n const { item, ratio } = this;\n const outerSize = project.outer(item);\n const faceSize = project.face(item);\n const postWidth = productService.getPostWidth();\n\n const postVertices = project.calculateVertices({\n height: item.height,\n width: postWidth,\n depth: postWidth,\n });\n\n const frontBottomLeft = project.point(postVertices[0]);\n const backBottomLeft = project.point(postVertices[4]);\n\n const offsetX = (backBottomLeft[0] - frontBottomLeft[0]) * ratio;\n const offsetY = (backBottomLeft[1] - frontBottomLeft[1]) * ratio;\n\n const vertices = project.calculateVertices(item);\n const projectedVertices = vertices.map(project.point);\n const frontBottom = projectedVertices[0];\n\n const scaledVertices = projectedVertices.map(vertex => {\n return [\n (vertex[0] - frontBottom[0]) * ratio,\n (outerSize.height - (vertex[1] - frontBottom[1])) * ratio,\n ];\n });\n\n const itemHeight = this.item.height / 10;\n\n const { color } = this.item.filter;\n\n const textures = {\n brace:\n PIXI.Loader.shared.resources[\n `brace_${this.item.width === 850 ? '84' : '64'}_${color}`\n ].texture,\n bl: PIXI.Loader.shared.resources[`${itemHeight}_back_left_${color}`]\n .texture,\n br: PIXI.Loader.shared.resources[`${itemHeight}_back_right_${color}`]\n .texture,\n bro: PIXI.Loader.shared.resources[\n `${itemHeight}_back_right_over_${color}`\n ].texture,\n fl: PIXI.Loader.shared.resources[`${itemHeight}_front_left_${color}`]\n .texture,\n fr: PIXI.Loader.shared.resources[`${itemHeight}_front_right_${color}`]\n .texture,\n };\n\n this.leftDropAreasContainer =\n this.leftDropAreasContainer || new PIXI.Container();\n this.leftDropAreasContainer.x = 0;\n\n if (!this.leftDropAreasContainer.parent) {\n this.addChild(this.leftDropAreasContainer);\n }\n\n this.brace = this.brace || new PIXI.Sprite();\n this.brace.texture = textures.brace;\n this.brace.width =\n constants.CROSS_BRACE_DIMENSIONS[item.width].width * ratio;\n this.brace.height =\n constants.CROSS_BRACE_DIMENSIONS[item.width].height * ratio;\n this.brace.x = scaledVertices[7][0] + offsetX;\n this.brace.y = (constants.DISTANCE_BETWEEN_ATTACHMENTS + 30) * ratio;\n\n if (!this.brace.parent) {\n this.addChild(this.brace);\n }\n\n this.innerDropAreasContainer =\n this.innerDropAreasContainer || new PIXI.Container();\n this.innerDropAreasContainer.x =\n scaledVertices[4][0] - scaledVertices[0][0];\n\n if (!this.innerDropAreasContainer.parent) {\n this.addChild(this.innerDropAreasContainer);\n }\n\n this.back = this.back || new PIXI.Container();\n this.back.x = scaledVertices[7][0];\n\n if (!this.back.parent) {\n this.addChild(this.back);\n }\n\n this.bl = this.bl || new PIXI.Sprite();\n this.bl.texture = textures.bl;\n this.bl.height = faceSize.height * ratio + offsetY;\n this.bl.width =\n faceSize.height * (textures.bl.width / textures.bl.height) * ratio;\n this.bl.x = -offsetX;\n this.bl.y = 0;\n\n if (!this.bl.parent) {\n this.back.addChild(this.bl);\n }\n\n this.br = this.br || new PIXI.Sprite();\n this.br.texture = textures.br;\n this.br.height = faceSize.height * ratio + offsetY;\n this.br.width = this.bl.height * (textures.br.width / textures.br.height);\n this.br.x = item.width * ratio - this.br.width + 1;\n this.br.y = 0;\n\n if (!this.br.parent) {\n this.back.addChild(this.br);\n }\n\n this.childrenContainer = this.childrenContainer || new PIXI.Container();\n this.childrenContainer.x = scaledVertices[4][0] - scaledVertices[0][0];\n\n if (!this.childrenContainer.parent) {\n this.addChild(this.childrenContainer);\n }\n\n this.front = this.front || new PIXI.Container();\n this.front.y = scaledVertices[3][1];\n\n if (!this.front.parent) {\n this.addChild(this.front);\n }\n\n this.bro = this.bro || new PIXI.Sprite();\n this.bro.height = faceSize.height * ratio;\n this.bro.width =\n this.bro.height * (textures.bro.width / textures.bro.height);\n this.bro.x = item.width * ratio - this.bro.width;\n this.bro.y = 0;\n\n if (!this.bro.parent) {\n this.front.addChild(this.bro);\n }\n\n this.fl = this.fl || new PIXI.Sprite();\n this.fl.texture = textures.fl;\n this.fl.height = faceSize.height * ratio;\n this.fl.width = this.fl.height * (textures.fl.width / textures.fl.height);\n this.fl.x = -1;\n this.fl.y = 0;\n\n if (!this.fl.parent) {\n this.front.addChild(this.fl);\n }\n\n this.fr = this.fr || new PIXI.Sprite();\n this.fr.texture = textures.fr;\n this.fr.height = faceSize.height * ratio;\n this.fr.width = this.fr.height * (textures.fr.width / textures.fr.height);\n this.fr.x = item.width * ratio - this.fr.width + offsetX;\n this.fr.y = 0;\n\n if (!this.fr.parent) {\n this.front.addChild(this.fr);\n }\n\n this.rightDropAreasContainer =\n this.rightDropAreasContainer || new PIXI.Container();\n this.rightDropAreasContainer.x = this.item.width * this.ratio;\n\n if (!this.rightDropAreasContainer.parent) {\n this.addChild(this.rightDropAreasContainer);\n }\n\n this.data = {\n vertices,\n projectedVertices,\n scaledVertices,\n outerSize,\n faceSize,\n };\n\n this.drawChildren();\n }\n\n setHitArea() {\n const { scaledVertices } = this.data;\n\n const vertices = [\n scaledVertices[0],\n scaledVertices[1],\n scaledVertices[5],\n scaledVertices[6],\n scaledVertices[7],\n scaledVertices[3],\n ];\n\n let polygon = {\n regions: [vertices],\n };\n\n this.childrenContainer.children.forEach(child => {\n if (geometry.extendsOutside(child.item, this.item)) {\n const scaledVertices = child.data.scaledVertices;\n\n const vertices = [\n scaledVertices[0],\n scaledVertices[1],\n scaledVertices[5],\n scaledVertices[6],\n scaledVertices[7],\n scaledVertices[3],\n ].map(vertex => [\n vertex[0] + child.x + child.parent.x,\n vertex[1] + child.y + child.parent.y,\n ]);\n\n polygon = mergePolygons(polygon, {\n regions: [vertices],\n });\n }\n });\n\n this.hitArea = new PIXI.Polygon([].concat(...polygon.regions[0]));\n }\n\n sortChildren() {\n // Always draw BROR inserts from lowest to highest.\n this.childrenContainer.children.sort((a, b) => {\n if (\n [a, b].some(sprite => productsService.isType(sprite.item, 'add-on'))\n ) {\n return a.item.x - b.item.x;\n }\n return a.item.y - b.item.y;\n });\n }\n\n addItem(item) {\n const sprite = new Item({\n options: this.options,\n item: item,\n parentItem: this.item,\n ratio: this.ratio,\n onDragStart: this.onDragStart,\n app: this.app,\n });\n\n this.childrenContainer.addChild(sprite);\n\n this.sprites.set(item.itemid, sprite);\n\n this.sortChildren();\n\n return sprite;\n }\n\n addSprite(sprite, slot) {\n const item = Object.assign({}, sprite.item);\n\n const product = productsService.getFit(item, slot);\n\n // only shelves (including add on) will return another object\n Object.assign(item, product);\n\n item.x = slot.x - this.item.x;\n item.y = slot.y - this.item.y;\n item.z = slot.z - this.item.z;\n\n sprite.update({\n item: item,\n parentItem: this.item,\n options: this.options,\n });\n\n this.childrenContainer.addChild(sprite);\n this.displayDropAreas(slot);\n\n sprite.once('added', () => {\n if (sprite.parent !== this.childrenContainer) {\n this.displayDropAreas();\n }\n });\n\n this.sortChildren();\n\n return sprite;\n }\n\n itemBelongingToSlot(slot) {\n return getProduct({ id: slot.id });\n }\n\n drawDropAreas(slots) {\n slots = slots.filter(slot => slot.parent.itemid === this.item.itemid);\n\n const width = Math.floor(this.item.width * this.ratio) + 1;\n\n slots.forEach(slot => {\n const x = slot.x - this.item.x;\n\n if (x >= 0 && x < this.item.width) {\n const sprite = new InnerDropArea({\n width: width,\n height: 100 * this.ratio,\n ratio: this.ratio,\n slot: slot,\n });\n\n sprite.slot = slot;\n\n sprite.x = 0;\n sprite.y =\n (this.item.height -\n (slot.y + 50 + (constants.DROP_AREA_MAX_HEIGHT - 50) / 2)) *\n this.ratio;\n if (isType(this.itemBelongingToSlot(slot), ITEMS.DRAWER)) {\n sprite.y -= (215 / 2) * this.ratio;\n }\n this.innerDropAreasContainer.addChild(sprite);\n } else {\n const sprite = new OuterDropArea({\n width: width,\n height: 100 * this.ratio,\n ratio: this.ratio,\n slot: slot,\n });\n\n sprite.slot = slot;\n sprite.y = (this.item.height - (slot.y + 126)) * this.ratio;\n\n if (x >= this.item.width) {\n this.rightDropAreasContainer.addChild(sprite);\n } else {\n sprite.x = -slot.width * this.ratio - 2;\n\n this.leftDropAreasContainer.addChild(sprite);\n }\n }\n });\n }\n\n eraseDropAreas() {\n this.leftDropAreasContainer.removeChildren();\n this.rightDropAreasContainer.removeChildren();\n this.innerDropAreasContainer.removeChildren();\n }\n\n displayDropAreas(slot) {\n const dropAreas = this.innerDropAreasContainer.children.concat(\n this.leftDropAreasContainer.children,\n this.rightDropAreasContainer.children\n );\n\n const slotPadding =\n slot && productsService.isType(slot, ITEMS.DRAWER)\n ? {\n // Drawers will collide with drop area above but shouldn't hide it\n top: -20,\n }\n : {};\n\n dropAreas.forEach(dropArea => {\n if (\n slot &&\n slot.y <= dropArea.slot.y &&\n geometry.collides(slot, dropArea.slot, slotPadding)\n ) {\n dropArea.visible = false;\n } else {\n dropArea.visible = true;\n }\n });\n }\n}\n","import Item from './Item';\n\nexport default class DynamicProppingAncestor extends Item {\n adaptPropping() {\n const children = this.getAllChildren();\n children.forEach(child => {\n if (child.adaptPropping) {\n child.adaptPropping();\n }\n });\n }\n\n addSprite(sprite, slot, tac) {\n let oldParent;\n if (sprite.parentItem && sprite.parentItem.itemid !== slot.parent.itemid) {\n oldParent = this.app.itemContainer.getSprite(sprite.parentItem.itemid);\n }\n super.addSprite(sprite, slot, tac);\n if (oldParent && oldParent.adaptPropping) {\n oldParent.adaptPropping();\n }\n\n this.adaptPropping();\n }\n}\n","import addItem from '../../state/tac/tacReducer/addItem';\nimport updateDependentItems from '../../state/tac/tacReducer/updateDependentItems';\nimport updateItem from '../../state/tac/tacReducer/updateItem';\nimport Item from '../Item';\nimport geometry from '../util/geometry';\nimport productService from '../../services/products';\nimport { ITEMS } from '../../constants';\n\nexport default class Upright extends Item {\n constructor(args) {\n super(args);\n this.sectionSprites = [];\n }\n\n static hitAreaPadding = 8;\n\n getHitArea() {\n const { scaledVertices } = this.data;\n\n const vertices = [\n [\n scaledVertices[0][0] - Upright.hitAreaPadding,\n scaledVertices[0][1] + Upright.hitAreaPadding,\n ], // front bottom left\n [\n scaledVertices[7][0] - Upright.hitAreaPadding,\n scaledVertices[7][1] - Upright.hitAreaPadding,\n ], // rear top left\n [\n scaledVertices[6][0] + Upright.hitAreaPadding,\n scaledVertices[6][1] - Upright.hitAreaPadding,\n ], // rear top right\n [\n scaledVertices[1][0] + Upright.hitAreaPadding,\n scaledVertices[1][1] + Upright.hitAreaPadding,\n ], // front bottom right\n ];\n\n const polygon = {\n regions: [vertices],\n };\n\n return { polygon };\n }\n\n scanPadding() {\n return {\n top: 0,\n bottom: 0,\n left: 800,\n right: 800,\n front: 0,\n back: 10,\n };\n }\n\n /**\n * Updates the dependents of a specific upright during a dragging event\n * @param {*} newItem\n * @returns {undefined}\n */\n updateDependentItemsDragging(newItem) {\n let updatedTac;\n\n if (newItem.itemid) {\n updatedTac = updateItem(this.app.tac, newItem);\n } else {\n // when dragging from swiper\n updatedTac = addItem(this.app.tac, newItem);\n\n // a new sprite will be created from the tmp item added to the fake tac,\n // so let's detach from our parent to avoid a conflict\n this.app.itemContainer.removeChild(this);\n }\n updatedTac = updateDependentItems(updatedTac, {\n triggerItem: newItem,\n flagTempItems: true,\n });\n\n this.app.itemContainer.update({\n room: this.app.room.model,\n options: this.app.itemContainerOptions,\n ratio: this.app.room.ratio,\n tac: updatedTac,\n updateOrigo: false,\n });\n }\n\n drawDropAreas(slots) {\n this.sectionSprites.forEach(section => section.drawDropAreas(slots));\n }\n\n eraseDropAreas() {\n this.sectionSprites.forEach(section => section.eraseDropAreas());\n }\n\n getChildDisplacementSprite(slot) {\n const sections = this.sectionSprites.map(sprite => sprite.item);\n const closest = geometry.closestCollidingRect(\n slot,\n sections.filter(section => slot.width === section.width)\n );\n\n return this.sectionSprites.find(\n section => section.item.itemid === closest.itemid\n );\n }\n\n getChildDisplacement(slot) {\n return this.getChildDisplacementSprite(slot).item;\n }\n\n addSprite(sprite, slot, tac) {\n this.getChildDisplacementSprite(slot).addSprite(sprite, slot, tac);\n }\n\n addSection(section) {\n this.removeSection(section);\n this.sectionSprites.push(section);\n }\n\n removeSection(section) {\n this.sectionSprites = this.sectionSprites.filter(\n sprite => sprite.item.itemid !== section.item.itemid\n );\n }\n\n cleanOutSectionSprites() {\n const currSecs = this.app.tac.items\n .filter(item => productService.isType(item, ITEMS.SECTION))\n .map(section => section.itemid);\n\n const sectionSpritesAfter = this.sectionSprites.filter(sprite =>\n currSecs.includes(sprite.item.itemid)\n );\n this.sectionSprites = sectionSpritesAfter;\n }\n}\n","import * as PIXI from 'pixi.js-legacy';\nimport DynamicProppingAncestor from '../DynamicProppingAncestor';\nimport { translate } from '../../services/L10n';\nimport { t } from '../../translations';\nimport productsService from '../../services/products';\nimport geometry from '../util/geometry';\nimport Upright from './Upright';\nimport platform from '../../util/platform';\nimport tacHelpers from '../../state/tac/tacHelpers';\nimport { getWidthText } from '../util/dimensionDisplay';\nimport { ITEMS } from '../../constants';\nimport productService from '../../services/products';\nimport {\n selectIsRtl,\n selectUseMetric,\n} from '../../state/dexfSettings/dexfSettingsSelectors';\nimport store from '../../state';\n\nexport default class Section extends DynamicProppingAncestor {\n update(args) {\n super.update(args);\n const section = args.item;\n // pad a bit on the front to reach and collide with the sections\n this.leftUprights = this.app.tac.items.filter(\n item =>\n productsService.isType(item, ITEMS.UPRIGHT) &&\n item.x + productService.getPostWidth() / 2 === section.x &&\n geometry.collides(item, section, { front: 20 })\n );\n\n this.leftUprights.forEach(upright => {\n const sprite = upright && this.app.getSprite(upright.itemid);\n sprite && sprite.cleanOutSectionSprites();\n sprite && sprite.addSection(this);\n });\n }\n\n cleanUpHintObjects() {\n if (this.app.hintContainer && this.app.hintContainer.children.length) {\n this.app.hintContainer &&\n this.app.hintContainer.removeChild(this.textObject, this.background);\n }\n }\n\n destroy(args) {\n this.leftUprights.forEach(upright => {\n const sprite = upright && this.app.getSprite(upright.itemid);\n sprite && sprite.removeSection(this);\n });\n this.cleanUpHintObjects();\n super.destroy(args);\n }\n\n getGlobalPos() {\n const { data, item, origo, ratio, parentItem } = this;\n\n //Stolen from Item.place()\n return {\n x:\n (item.x -\n (data.projectedVertices[4][0] - data.projectedVertices[0][0]) -\n origo.x) *\n ratio,\n y:\n (parentItem.height - (data.faceSize.height + item.y) + origo.y) * ratio,\n };\n }\n\n writeWidth() {\n const style = new PIXI.TextStyle({\n fontSize: platform.isKiosk ? 20 : 12,\n fontFamily: 'NotoIKEALatin, Verdana, sans-serif',\n lineHeight: platform.isKiosk ? 1.2 : 1.33,\n trim: this.isMobile,\n fill: '#222',\n });\n\n const textPadding = 4;\n\n const measure = getWidthText(this);\n const useMetricMeasures = selectUseMetric(store.getState());\n const unitText = useMetricMeasures\n ? translate(t.MEASURE_VALUE_CM)\n : translate(t.MEASURE_VALUE_IN);\n const isRtl = selectIsRtl(store.getState());\n const textContent = isRtl\n ? `${unitText}${String.fromCharCode(parseInt('200E', 16))} ${measure}`\n : `${measure} ${unitText}`;\n if (!this.textObject) {\n this.textObject = new PIXI.Text(textContent, style);\n } else {\n this.textObject.text = textContent;\n }\n\n this.cleanUpHintObjects();\n\n const { faceSize, scaledVertices } = this.data;\n\n const globalPos = this.getGlobalPos();\n this.textObject.x =\n globalPos.x + scaledVertices[4][0] + (faceSize.width / 2) * this.ratio;\n this.textObject.anchor.set(0.5, 0);\n this.textObject.y = globalPos.y + this.sprite.height * 0.29;\n\n this.background = new PIXI.Graphics();\n\n const { textObject } = this;\n\n this.background.lineStyle(0.75, 0x000000);\n this.background.beginFill(0xffffff);\n this.background.drawRoundedRect(\n textObject.x - textPadding - textObject.width / 2,\n textObject.y - textPadding,\n textObject.width + textPadding * 2,\n textObject.height + textPadding * 2,\n 2\n );\n this.background.endFill();\n\n this.app.hintContainer.addChild(this.background, this.textObject);\n }\n\n removeSectionHighlight() {\n this.sprite.alpha = 0;\n this.cleanUpHintObjects();\n }\n\n shouldDisplaySectionHint() {\n const { app, item } = this;\n\n const draggingItem = app.dragging && app.dragging.item;\n const connectedUprights = item.uprights;\n if (item.width > 800) {\n // no hints for LAGKAPTEN sections\n return false;\n }\n\n if (item.isTempItem) {\n return true;\n }\n if (!draggingItem) {\n return false;\n }\n if (\n tacHelpers.isWithinWall(draggingItem, app.tac) &&\n connectedUprights &&\n connectedUprights.some(upright => upright.itemid === draggingItem.itemid)\n ) {\n return true;\n }\n }\n\n draw() {\n const { item, ratio } = this;\n const { faceSize } = this.data;\n const { scaledVertices } = this.data;\n const padding = 2;\n\n this.sprite = this.sprite || new PIXI.Sprite();\n\n this.sprite.visible = !item.hidden;\n this.sprite.width = faceSize.width * ratio - padding * 2;\n this.sprite.height = faceSize.height * ratio;\n this.sprite.x = scaledVertices[4][0] + padding;\n\n if (!this.sprite.parent) {\n this.addChild(this.sprite);\n }\n\n this.dropAreas = this.dropAreas || new PIXI.Container();\n this.dropAreas.x = scaledVertices[4][0] - scaledVertices[0][0];\n\n if (!this.dropAreas.parent) {\n this.addChild(this.dropAreas);\n }\n\n this.childrenContainer = this.childrenContainer || new PIXI.Container();\n this.childrenContainer.x = scaledVertices[4][0] - scaledVertices[0][0];\n\n if (!this.childrenContainer.parent) {\n this.addChild(this.childrenContainer);\n }\n\n if (this.shouldDisplaySectionHint()) {\n this.sprite.texture = PIXI.Texture.WHITE;\n this.sprite.tint = 0x0058a3;\n this.sprite.alpha = 0.2;\n this.writeWidth();\n } else {\n this.removeSectionHighlight();\n }\n\n if (item.keepDuringDrag) {\n this.alpha = 0.5;\n this.removeSectionHighlight();\n } else {\n this.alpha = 1;\n }\n\n this.drawChildren();\n }\n\n drawDropAreas(slots) {\n slots = slots.filter(\n slot =>\n this.leftUprights.find(\n upright => upright.itemid === slot.parent.itemid\n ) && this.item.width === slot.width\n );\n\n const width = this.data.faceSize.width * this.ratio;\n\n slots.forEach(slot => {\n const localSlot = { ...slot, ...slot.local, y: slot.y - this.item.y };\n const sprite = this.getDropAreaSprite(localSlot, width);\n sprite && this.dropAreas.addChild(sprite);\n });\n }\n\n displayDropArea(dropareas, slot) {\n const localSlot = slot && {\n ...slot,\n ...slot.local,\n y: slot.y - this.item.y,\n };\n dropareas.children.forEach(dropArea => {\n this.toggleDropAreaVisibility(dropArea, localSlot);\n });\n }\n // override Item.getSelfHitArea\n getSelfHitArea() {\n const { scaledVertices } = this.data;\n\n let topCrop = 0;\n if (!this.item.filter.switchable) {\n const kids = this.getAllChildren();\n if (!kids.length) {\n return {\n regions: [[]],\n };\n }\n topCrop = kids[0].y;\n }\n\n // calc only using back side, and tighten it a bit\n const vertices = [\n [scaledVertices[4][0] + Upright.hitAreaPadding, scaledVertices[4][1]],\n [scaledVertices[5][0] - Upright.hitAreaPadding, scaledVertices[5][1]],\n [\n scaledVertices[6][0] - Upright.hitAreaPadding,\n scaledVertices[6][1] + topCrop,\n ],\n [\n scaledVertices[7][0] + Upright.hitAreaPadding,\n scaledVertices[7][1] + topCrop,\n ],\n ];\n\n return {\n regions: [vertices],\n };\n }\n}\n","import * as PIXI from 'pixi.js-legacy';\nimport InnerDropArea from '../InnerDropArea';\nimport DynamicProppingAncestor from '../DynamicProppingAncestor';\nimport productService from '../../services/products';\nimport addItem from '../../state/tac/tacReducer/addItem';\nimport updateDependentItems from '../../state/tac/tacReducer/updateDependentItems';\nimport updateItem from '../../state/tac/tacReducer/updateItem';\nimport geometry from '../util/geometry';\nimport tacHelpers from '../../state/tac/tacHelpers';\n\nexport default class Section extends DynamicProppingAncestor {\n update(args) {\n super.update(args);\n this.childrenContainer &&\n this.childrenContainer.children.forEach(child => {\n if (\n child.item &&\n productService.isType(child.item, ['side-panel', 'end-shelf'])\n ) {\n child.interactive = false;\n }\n });\n }\n\n draw() {\n super.draw();\n\n this.sprite.visible = !tacHelpers.isWithinWall(this.item, this.app.tac);\n const { scaledVertices } = this.data;\n\n this.rightDropAreas = this.rightDropAreas || new PIXI.Container();\n this.rightDropAreas.x = scaledVertices[4][0] - scaledVertices[0][0];\n\n if (!this.rightDropAreas.parent) {\n this.addChildAt(\n this.rightDropAreas,\n this.getChildIndex(this.childrenContainer)\n );\n }\n }\n\n drawDropAreas(slots) {\n slots = slots.filter(slot => slot.parent.itemid === this.item.itemid);\n\n const width = Math.floor(this.data.faceSize.width * this.ratio) - 2;\n const localItem = {\n ...this.item,\n x: 0,\n y: 0,\n };\n\n slots.forEach(slot => {\n const localSlot = { ...slot, ...slot.local };\n\n if (\n geometry.extendsOutside(localSlot, localItem) &&\n localSlot.y < localItem.height\n ) {\n const width = localSlot.partnerSlot\n ? localSlot.partnerSlot.x + localSlot.partnerSlot.width - slot.x\n : localSlot.width;\n const sprite = new InnerDropArea({\n width: width * this.ratio,\n height: localSlot.height * this.ratio,\n ratio: this.ratio,\n slot: localSlot,\n });\n\n sprite.slot = localSlot;\n\n // FIXME: This shouldn't really be an InnerDropArea, but since it is\n // we need to adjust for the POST_WIDTH offset that's added to all InnerDropAreas\n sprite.x = (localSlot.x - productService.getPostWidth()) * this.ratio;\n sprite.y =\n (this.item.height - (localSlot.y + localSlot.height)) * this.ratio;\n\n this.rightDropAreas.addChild(sprite);\n } else {\n const sprite = super.getDropAreaSprite(localSlot, width);\n sprite && this.dropAreas.addChild(sprite);\n }\n });\n }\n\n displayDropAreas(slot) {\n this.displayDropArea(this.rightDropAreas, slot);\n this.displayDropArea(this.dropAreas, slot);\n }\n\n eraseDropAreas() {\n this.rightDropAreas.removeChildren();\n super.eraseDropAreas();\n }\n\n /**\n * Updates the dependents of a specific section during a dragging event\n * @param {*} newItem\n * @returns {undefined}\n */\n updateDependentItemsDragging(newItem) {\n let updatedTac;\n\n if (newItem.itemid) {\n const hadPanel = this.app.dragging.originalItem.items.some(item =>\n productService.isType(item, 'side-panel')\n );\n updatedTac = updateItem(this.app.tac, newItem);\n if (hadPanel) {\n updatedTac = updateDependentItems(updatedTac, {\n triggerItem: this.app.dragging.originalItem,\n });\n }\n } else {\n // when dragging from swiper\n updatedTac = addItem(this.app.tac, newItem);\n\n // a new sprite will be created from the tmp item added to the fake tac,\n // so let's detach from our parent to avoid a conflict\n this.app.itemContainer.removeChild(this);\n }\n\n this.app.itemContainer.update({\n room: this.app.room.model,\n options: this.app.itemContainerOptions,\n ratio: this.app.room.ratio,\n tac: updatedTac,\n updateOrigo: false,\n });\n }\n}\n","import DynamicProppingAncestor from '../DynamicProppingAncestor';\nimport productService from '../../services/products';\nimport addItem from '../../state/tac/tacReducer/addItem';\nimport updateDependentItems from '../../state/tac/tacReducer/updateDependentItems';\nimport updateItem from '../../state/tac/tacReducer/updateItem';\nimport tacHelpers from '../../state/tac/tacHelpers';\nimport constants from '../../settings/constants';\nimport idGenerator from '../../util/aactools/idGenerator';\nimport { replace } from '../../state/tac/replace';\nimport * as PIXI from 'pixi.js-legacy';\nimport { ITEMS } from '../../constants';\n\nexport default class Section extends DynamicProppingAncestor {\n /**\n * Sorts the the sections children (drawing order) according to specific rules.\n * Overrides implementation in Item\n * @returns {undefined}\n */\n sortChildren() {\n const postWidth = productService.getPostWidth();\n\n this.childrenContainer.children.forEach(child => {\n if (productService.isType(child.item, 'cross-brace')) {\n child.zIndex = 1;\n } else if (productService.isType(child.item, 'side-panel')) {\n if (child.item.id.includes('_back')) {\n // Back posts first, order of left and right one doesn't matter\n child.zIndex = 2;\n } else if (\n child.item.id.includes('_middle') &&\n child.item.x < postWidth\n ) {\n // Then the left connector\n child.zIndex = 3;\n } else if (\n child.item.id.includes('_front') &&\n child.item.x < postWidth\n ) {\n // Then the left front post\n child.zIndex = 4;\n } else if (\n child.item.id.includes('_middle') &&\n child.item.x > postWidth\n ) {\n // Then we reserve a number of z-indexes for the inserts,\n // before adding the right connector\n child.zIndex = 10000;\n } else if (\n child.item.id.includes('_front') &&\n child.item.x > postWidth\n ) {\n // And finally the right front post\n child.zIndex = 10001;\n }\n } else if (productService.isType(child.item, ITEMS.DOOR)) {\n // Doors should be drawn on top of everything else in the section\n child.zIndex = 10002;\n } else {\n // Inserts need to have z-indexes between 4 and 10000,\n // and sorted internally through their y position\n child.zIndex = 5 + child.item.y;\n }\n });\n this.childrenContainer.sortChildren();\n }\n\n drawChildren() {\n super.drawChildren();\n const postWidth = productService.getPostWidth();\n\n if (!tacHelpers.isWithinWall(this.item, this.app.tac)) {\n // If we are being dragged around outside the scene, display all parts\n this.childrenContainer.children.forEach(child => {\n child.alpha = 1;\n });\n return;\n }\n\n this.childrenContainer.children.forEach(child => {\n child.alpha = 1;\n if (\n productService.isType(child.item, 'side-panel') &&\n child.item.x < postWidth &&\n tacHelpers\n .findSuperSection(this.app.itemContainer.tac, this.item, [])\n .some(item => item.x < this.item.x)\n ) {\n // Hide the left side unit when we have a section to the left of us.\n child.alpha = 0;\n }\n });\n\n if (\n !this.childrenContainer.children.some(\n child =>\n productService.isType(child.item, 'side-panel') &&\n child.item.x > postWidth\n )\n ) {\n // If we are missing the right side unit in the TAC\n // we need to borrow it from the section next to us\n const rightNeighbour = tacHelpers\n .findSuperSection(this.app.itemContainer.tac, this.item, [])\n .find(item => item.x > this.item.x);\n\n if (rightNeighbour) {\n // Get the left side unit items from our neighbour and make it our right side unit\n rightNeighbour.items\n .filter(\n item =>\n productService.isType(item, 'side-panel') && item.x < postWidth\n )\n .forEach(sidePanelItem => {\n this.addItem({\n ...sidePanelItem,\n x: sidePanelItem.x + (this.item.width - postWidth),\n itemid: idGenerator.fakeId(),\n });\n });\n }\n }\n }\n\n update(args) {\n super.update(args);\n this.shadow && this.setShadowDimensions();\n this.childrenContainer &&\n this.childrenContainer.children.forEach(child => {\n if (child.item && tacHelpers.isPartOf(child.item, this.item)) {\n child.interactive = false;\n }\n });\n }\n\n clearCache() {\n this.dependencyUpdatedTac = null;\n }\n\n needsFullDependencyReCalc() {\n /*\n If we haven't calculated dependencies before during the drag event,\n or we have altered the super section (with more members than just me)\n we need a full re-calc of all dependencies\n */\n return (\n !this.dependencyUpdatedTac ||\n (this.mySuperSection !== this.previousSuperSection &&\n [this.mySuperSection, this.previousSuperSection].some(\n superSection => superSection.length > 1\n ))\n );\n }\n\n getDependencyUpdates(newItem, updatedTac, triggerItem) {\n if (this.needsFullDependencyReCalc()) {\n this.dependencyUpdatedTac = updateDependentItems(updatedTac, {\n triggerItem,\n });\n } else {\n // If not we can just return the previously calculated tac,\n // replacing the item currently being dragged with the newest version\n const isFake = !idGenerator.hasRealId(newItem);\n replace(\n this.dependencyUpdatedTac.items,\n updatedTac.items.find(\n item =>\n item.itemid === newItem.itemid ||\n (isFake && !idGenerator.hasRealId(item))\n ),\n { replaceFakes: isFake }\n );\n }\n\n return this.dependencyUpdatedTac;\n }\n\n /**\n * Updates the dependents of a specific section during a dragging event\n * @param {*} newItem\n * @returns {undefined}\n */\n updateDependentItemsDragging(newItem) {\n let updatedTac;\n\n this.previousSuperSection = this.mySuperSection;\n this.mySuperSection = tacHelpers.findSuperSection(this.app.tac, newItem);\n const isAlone = this.mySuperSection.length === 1;\n\n if (newItem.itemid) {\n // moving an item already on the scene\n updatedTac = updateItem(this.app.tac, newItem, this.app.tac, {\n // Optimization: If we now that we are alone in the super section,\n // we can safely force-connect all parts on ourselves\n forceParts: isAlone,\n });\n\n updatedTac = this.getDependencyUpdates(\n newItem,\n updatedTac,\n this.app.dragging.originalItem\n );\n } else {\n // when dragging from swiper\n updatedTac = addItem(this.app.tac, newItem, this.app.tac, {\n // Optimization: If we now that we are alone in the super section,\n // we can safely force-connect all parts on ourselves\n forceParts: isAlone,\n });\n\n updatedTac = this.getDependencyUpdates(newItem, updatedTac, newItem);\n\n // a new sprite will be created from the tmp item added to the fake tac,\n // so let's detach from our parent to avoid a conflict\n this.app.itemContainer.removeChild(this);\n }\n\n this.app.itemContainer.update({\n room: this.app.room.model,\n options: this.app.itemContainerOptions,\n ratio: this.app.room.ratio,\n tac: updatedTac,\n updateOrigo: false,\n });\n }\n\n getDropAreaSprite(localSlot, width, allSlots) {\n if (\n allSlots.some(\n slot => slot.y + constants.DISTANCE_BETWEEN_ATTACHMENTS === localSlot.y\n )\n ) {\n /*\n Special case for the top two slots in the 1785 high IVAR sections:\n to stop these from overlapping, we move the top one a bit further up\n */\n localSlot.height += constants.DISTANCE_BETWEEN_ATTACHMENTS;\n }\n const sprite = super.getDropAreaSprite(localSlot, width);\n\n if (localSlot.height * this.ratio > sprite.height) {\n sprite.y = (this.item.height - localSlot.y) * this.ratio - sprite.height;\n }\n return sprite;\n }\n\n getSectionWidthAndHeight() {\n const { width, height } = this.data.faceSize;\n return [width * this.ratio, height * this.ratio];\n }\n\n getShadowWidthAndHeight(scaledVertices) {\n const [sectionWidth] = this.getSectionWidthAndHeight();\n\n return {\n width: sectionWidth - productService.getPostWidth() * this.ratio * 2,\n height: scaledVertices[3][1] / 2 + scaledVertices[4][0],\n };\n }\n\n setShadowDimensions() {\n const scaledVertices = this.data.scaledVertices;\n const sectionDimensions = this.getSectionWidthAndHeight();\n const { width, height } = this.getShadowWidthAndHeight(scaledVertices);\n\n this.shadow.height = height;\n this.shadow.width = width;\n\n this.shadow.position.set(\n scaledVertices[4][0] + productService.getPostWidth() * this.ratio,\n sectionDimensions[1]\n );\n }\n\n createSectionShadow() {\n const scaledVertices = this.data.scaledVertices;\n const { width, height } = this.getShadowWidthAndHeight(scaledVertices);\n\n this.shadow = new PIXI.Graphics();\n this.shadow.beginFill(0x000000, 0.2);\n this.shadow.drawRect(0, 0, width, height);\n this.setShadowDimensions();\n this.shadow.endFill();\n this.shadow.skew.x = -20;\n\n this.addChild(this.shadow);\n }\n\n isBeingDragged() {\n if (!this.app.dragging) return false;\n\n const matchingIds = this.app.dragging.item.itemid === this.item.itemid;\n return matchingIds;\n }\n\n setShadowVisibility(visible) {\n this.shadow.alpha = visible ? 1 : 0;\n }\n\n draw() {\n !this.shadow && this.createSectionShadow();\n const shadowVisible = !this.isBeingDragged();\n this.setShadowVisibility(shadowVisible);\n super.draw();\n this.sprite.alpha = 0;\n }\n\n excludeChildFromHitArea(child) {\n return child.item && tacHelpers.isPartOf(child.item, this.item);\n }\n\n /**\n * Removes the drop areas. Overrides implementation in Item.\n * @returns {undefined}\n */\n eraseDropAreas() {\n !this.app.dragging && super.eraseDropAreas();\n }\n\n /**\n * Handles an old sprite that no longer is a part of the section.\n * Overrides implementation in Item.\n * @param {*} sprite\n * @returns {undefined}\n */\n handleOldSprite(sprite) {\n sprite !== this.app.dragging?.sprite && sprite.destroy();\n }\n}\n","import DynamicProppingAncestor from '../DynamicProppingAncestor';\nimport productService from '../../services/products';\nimport addItem from '../../state/tac/tacReducer/addItem';\nimport updateDependentItems from '../../state/tac/tacReducer/updateDependentItems';\nimport updateItem from '../../state/tac/tacReducer/updateItem';\nimport tacHelpers from '../../state/tac/tacHelpers';\nimport idGenerator from '../../util/aactools/idGenerator';\nimport { replace } from '../../state/tac/replace';\nimport * as PIXI from 'pixi.js-legacy';\n\nexport default class Section extends DynamicProppingAncestor {\n /**\n * Sorts the the sections children (drawing order) according to specific rules.\n * Overrides implementation in Item\n * @returns {undefined}\n */\n sortChildren() {\n const postWidth = productService.getPostWidth();\n\n this.childrenContainer.children.forEach(child => {\n if (productService.isType(child.item, 'cross-brace')) {\n child.zIndex = 1;\n } else if (productService.isType(child.item, 'side-panel')) {\n if (child.item.id.includes('_back')) {\n // Back posts first, order of left and right one doesn't matterx\n child.zIndex = 2;\n } else if (\n child.item.id.includes('_middle') &&\n child.item.x < postWidth\n ) {\n // Then the left connector\n child.zIndex = 3;\n } else if (\n child.item.id.includes('_middle') &&\n child.item.x > postWidth\n ) {\n // Then we reserve a number of z-indexes for the inserts,\n // before adding the right connector\n child.zIndex = 10001;\n } else if (child.item.id.includes('_front')) {\n // And finally the front posts\n child.zIndex = 10002;\n }\n } else if (productService.isType(child.item, 'post')) {\n if (child.item.id.includes('_top') && child.item.x < postWidth) {\n // Left top post first.\n child.zIndex = 2;\n } else if (\n child.item.id.includes('_base') &&\n child.item.x < postWidth\n ) {\n // Then the left base post.\n child.zIndex = 3;\n } else if (child.item.id.includes('_top') && child.item.x > postWidth) {\n // Then we reserve a number of z-indexes for the inserts,\n // before adding the right top post.\n child.zIndex = 10001;\n } else if (\n child.item.id.includes('_base') &&\n child.item.x > postWidth\n ) {\n // And finally the right base post.\n child.zIndex = 10002;\n }\n } else {\n // Inserts need to have z-indexes between 4 and 10000,\n // and sorted internally through their y position\n child.zIndex = 4 + child.item.y;\n }\n });\n this.childrenContainer.sortChildren();\n }\n\n drawChildren() {\n super.drawChildren();\n\n if (!tacHelpers.isWithinWall(this.item, this.app.tac)) {\n // If we are being dragged around outside the scene, display all parts\n this.childrenContainer.children.forEach(child => {\n child.alpha = 1;\n });\n return;\n }\n }\n\n update(args) {\n super.update(args);\n this.shadow && this.setShadowDimensions();\n this.childrenContainer &&\n this.childrenContainer.children.forEach(child => {\n if (child.item && tacHelpers.isPartOf(child.item, this.item)) {\n child.interactive = false;\n }\n });\n }\n\n clearCache() {\n this.dependencyUpdatedTac = null;\n }\n\n needsFullDependencyReCalc() {\n /*\n If we haven't calculated dependencies before during the drag event,\n or we have altered the super section (with more members than just me)\n we need a full re-calc of all dependencies\n */\n return (\n !this.dependencyUpdatedTac ||\n (this.mySuperSection !== this.previousSuperSection &&\n [this.mySuperSection, this.previousSuperSection].some(\n superSection => superSection.length > 1\n ))\n );\n }\n\n getDependencyUpdates(newItem, updatedTac, triggerItem) {\n if (this.needsFullDependencyReCalc()) {\n this.dependencyUpdatedTac = updateDependentItems(updatedTac, {\n triggerItem,\n });\n } else {\n // If not we can just return the previously calculated tac,\n // replacing the item currently being dragged with the newest version\n const isFake = !idGenerator.hasRealId(newItem);\n replace(\n this.dependencyUpdatedTac.items,\n updatedTac.items.find(\n item =>\n item.itemid === newItem.itemid ||\n (isFake && !idGenerator.hasRealId(item))\n ),\n { replaceFakes: isFake }\n );\n }\n\n return this.dependencyUpdatedTac;\n }\n\n /**\n * Updates the dependents of a specific section during a dragging event\n * @param {*} newItem\n * @returns {undefined}\n */\n updateDependentItemsDragging(newItem) {\n let updatedTac;\n\n this.previousSuperSection = this.mySuperSection;\n this.mySuperSection = tacHelpers.findSuperSection(this.app.tac, newItem);\n const isAlone = this.mySuperSection.length === 1;\n\n if (newItem.itemid) {\n // moving an item already on the scene\n updatedTac = updateItem(this.app.tac, newItem, this.app.tac, {\n // Optimization: If we now that we are alone in the super section,\n // we can safely force-connect all parts on ourselves\n forceParts: isAlone,\n });\n\n updatedTac = this.getDependencyUpdates(\n newItem,\n updatedTac,\n this.app.dragging.originalItem\n );\n } else {\n // when dragging from swiper\n updatedTac = addItem(this.app.tac, newItem, this.app.tac, {\n // Optimization: If we now that we are alone in the super section,\n // we can safely force-connect all parts on ourselves\n forceParts: isAlone,\n });\n\n updatedTac = this.getDependencyUpdates(newItem, updatedTac, newItem);\n\n // a new sprite will be created from the tmp item added to the fake tac,\n // so let's detach from our parent to avoid a conflict\n this.app.itemContainer.removeChild(this);\n }\n\n this.app.itemContainer.update({\n room: this.app.room.model,\n options: this.app.itemContainerOptions,\n ratio: this.app.room.ratio,\n tac: updatedTac,\n updateOrigo: false,\n });\n }\n\n getDropAreaSprite(localSlot, width, allSlots) {\n const sprite = super.getDropAreaSprite(localSlot, width);\n\n if (localSlot.height * this.ratio > sprite.height) {\n sprite.y = (this.item.height - localSlot.y) * this.ratio - sprite.height;\n }\n return sprite;\n }\n\n getSectionWidthAndHeight() {\n const { width, height } = this.data.faceSize;\n return [width * this.ratio, height * this.ratio];\n }\n\n getShadowWidthAndHeight(scaledVertices) {\n const [sectionWidth] = this.getSectionWidthAndHeight();\n\n return {\n width: sectionWidth - productService.getPostWidth() * this.ratio * 2,\n height: scaledVertices[3][1] / 2 + scaledVertices[4][0],\n };\n }\n\n setShadowDimensions() {\n const scaledVertices = this.data.scaledVertices;\n const sectionDimensions = this.getSectionWidthAndHeight();\n const { width, height } = this.getShadowWidthAndHeight(scaledVertices);\n\n this.shadow.height = height;\n this.shadow.width = width;\n\n this.shadow.position.set(\n scaledVertices[4][0] + productService.getPostWidth() * this.ratio,\n sectionDimensions[1]\n );\n }\n\n createSectionShadow() {\n const scaledVertices = this.data.scaledVertices;\n const { width, height } = this.getShadowWidthAndHeight(scaledVertices);\n\n this.shadow = new PIXI.Graphics();\n this.shadow.beginFill(0x000000, 0.2);\n this.shadow.drawRect(0, 0, width, height);\n this.setShadowDimensions();\n this.shadow.endFill();\n this.shadow.skew.x = -20;\n\n this.addChild(this.shadow);\n }\n\n isBeingDragged() {\n if (!this.app.dragging) return false;\n\n const matchingIds = this.app.dragging.item.itemid === this.item.itemid;\n return matchingIds;\n }\n\n setShadowVisibility(visible) {\n this.shadow.alpha = visible ? 1 : 0;\n }\n\n draw() {\n !this.shadow && this.createSectionShadow();\n const shadowVisible = !this.isBeingDragged();\n this.setShadowVisibility(shadowVisible);\n super.draw();\n this.sprite.alpha = 0;\n }\n\n excludeChildFromHitArea(child) {\n return child.item && tacHelpers.isPartOf(child.item, this.item);\n }\n\n /**\n * Removes the drop areas. Overrides implementation in Item.\n * @returns {undefined}\n */\n eraseDropAreas() {\n !this.app.dragging && super.eraseDropAreas();\n }\n\n /**\n * Handles an old sprite that no longer is a part of the section.\n * Overrides implementation in Item.\n * @param {*} sprite\n * @returns {undefined}\n */\n handleOldSprite(sprite) {\n sprite !== this.app.dragging?.sprite && sprite.destroy();\n }\n}\n","import * as PIXI from 'pixi.js';\n\nimport project from './project';\nimport { getProduct } from '../../services/products';\n\nexport default function (\n container,\n size,\n { leftId, rightId, topId, bottomId },\n ratio,\n propping = {}\n) {\n const innerId = leftId || bottomId;\n const outerId = rightId || topId;\n\n const proppingBounds = propping.singleProppingBounds\n ? propping.singleProppingBounds.size\n : null;\n const combined = container || new PIXI.Container();\n const innerProduct = getProduct(innerId);\n const outerProduct = getProduct(outerId);\n\n /*P in sizes indicate that the sizes are the projected sizes thus considering the rotation of the bounds */\n const innerPSize = project.outer(innerProduct);\n const outerPSize = project.outer(proppingBounds || outerProduct);\n\n const textures = {\n inner: PIXI.Loader.shared.resources[innerId].texture,\n outer:\n PIXI.Loader.shared.resources[propping.proppingName || outerId].texture,\n };\n\n const innerSprite = combined.innerSprite || new PIXI.Sprite();\n innerSprite.texture = textures.inner;\n innerSprite.width = innerPSize.width * ratio;\n innerSprite.height = innerPSize.height * ratio;\n combined.innerSprite = innerSprite;\n\n const outerSprite = combined.outerSprite || new PIXI.Sprite();\n outerSprite.texture = textures.outer;\n outerSprite.width = outerPSize.width * ratio;\n outerSprite.height = outerPSize.height * ratio;\n combined.outerSprite = outerSprite;\n\n if (leftId && rightId) {\n //horizontal adj\n outerSprite.x = (size.width - outerPSize.width) * ratio;\n } else if (topId && bottomId) {\n //vertical adj\n innerSprite.x = ((outerPSize.width - innerPSize.width) * ratio) / 2;\n innerSprite.y = (size.height - innerPSize.height) * ratio;\n }\n\n !innerSprite.parent && combined.addChild(innerSprite);\n !outerSprite.parent && combined.addChild(outerSprite);\n\n return combined;\n}\n","import * as PIXI from 'pixi.js';\n\nimport InnerDropArea from './InnerDropArea';\nimport constants from '../settings/constants';\nimport geometry from './util/geometry';\nimport DynamicProppingAncestor from './DynamicProppingAncestor';\nimport { getProppingBounds } from '../services/products/models';\nimport productService from '../services/products';\nimport { config as asConfig } from './boaxel/AdjustableConfig';\nimport adjustableContainer from './util/adjustableContainer';\nimport { ITEMS } from '../constants';\n\nexport default class Shelf extends DynamicProppingAncestor {\n setSpriteVisibilities() {\n if (this.sprite) {\n this.sprite.visible = !this.isExtendable;\n }\n if (this.adjustableContainer) {\n this.adjustableContainer.visible = this.isExtendable;\n }\n }\n\n drawAdjustable() {\n this.adjustableContainer = adjustableContainer(\n this.adjustableContainer,\n this.data.outerSize,\n asConfig.shelf[this.item.id],\n this.ratio\n );\n\n if (!this.adjustableContainer.parent) {\n // Put it between the brackets\n this.addChildAt(\n this.adjustableContainer,\n this.children.indexOf(this.sprite)\n );\n }\n }\n\n draw() {\n const { scaledVertices } = this.data;\n this.bottomContainer = this.bottomContainer || new PIXI.Container();\n this.bottomContainer.x = scaledVertices[4][0] - scaledVertices[0][0];\n\n if (!this.bottomContainer.parent) {\n this.addChild(this.bottomContainer);\n }\n\n super.draw();\n\n if (this.isExtendable) {\n this.drawAdjustable();\n }\n\n this.setSpriteVisibilities();\n }\n\n getContainer(sprite) {\n return this.bottomContainer;\n }\n\n update(args) {\n this.isExtendable = productService.isExtendable(args.item);\n\n super.update(args);\n\n // this extra update should only occur on load,\n // and is needed since we don't have the adjusted width in pac.\n if (this.isExtendable) {\n const item = this.item;\n const parent = this.parentItem;\n const parentLogic = parent.id ? parent.logic : this.item.logic;\n\n if (parent && item.width !== parent.width) {\n if (productService.isExtendable(parentLogic)) {\n const newItem = { ...item, width: parent.width };\n this.app.updateItem(newItem, parent, {\n automatedUpdate: true,\n });\n }\n }\n }\n }\n\n getBounds() {\n const bounds = super.getBounds();\n if (this.item.items && this.item.items.length > 0) {\n const clothesRail = this.item.items.find(item =>\n productService.isType(item, ITEMS.CLOTHES_RAIL)\n );\n if (clothesRail) {\n const proppingItem = productService.isExtendable(clothesRail)\n ? productService.getProduct(\n asConfig.clothesrail[clothesRail.id].rightId\n )\n : clothesRail;\n const paddingBottom = 10;\n const shelfHeight = this.item.height * this.ratio;\n const smallestProppingHeight =\n getProppingBounds(proppingItem)[0].size.height * this.ratio;\n\n bounds.height = shelfHeight + smallestProppingHeight + paddingBottom;\n }\n }\n return bounds;\n }\n\n getSelfHitArea() {\n const { scaledVertices } = this.data;\n // the 4 is a \"magical\" number that created hit areas that touch (or very\n // slightly overlap) when two shelves are placed in adjacent connections\n const padding = (constants.DISTANCE_BETWEEN_ATTACHMENTS / 4) * this.ratio;\n\n const vertices = [\n [scaledVertices[0][0], scaledVertices[0][1] + padding],\n [scaledVertices[3][0], scaledVertices[3][1]],\n [scaledVertices[7][0], scaledVertices[7][1] - padding],\n [scaledVertices[6][0], scaledVertices[6][1] - padding],\n [scaledVertices[5][0], scaledVertices[5][1]],\n [scaledVertices[1][0], scaledVertices[1][1] + padding],\n ];\n\n return {\n regions: [vertices],\n };\n }\n\n getAllChildren() {\n return super.getAllChildren().concat(this.bottomContainer.children);\n }\n\n getDropAreaSprite(localSlot) {\n const localItem = {\n ...this.item,\n x: 0,\n y: 0,\n };\n if (\n geometry.collides(localSlot, localItem, {\n top: 50,\n bottom: 50,\n })\n ) {\n const sprite = new InnerDropArea({\n width: localSlot.width * this.ratio,\n height: localSlot.height * this.ratio,\n ratio: this.ratio,\n slot: localSlot,\n });\n\n sprite.slot = localSlot;\n\n sprite.x = 0;\n sprite.y = (this.item.height - localSlot.y) * this.ratio;\n\n return sprite;\n }\n }\n}\n","import * as PIXI from 'pixi.js-legacy';\nimport Item from '../Item';\n\nexport default class Legs extends Item {\n draw() {\n const { item, ratio } = this;\n const { faceSize, scaledVertices } = this.data;\n\n this.visible = !item.hidden;\n\n const isPads = this.item.modelid.indexOf('pads') > -1;\n const baseResourceName = isPads\n ? this.item.id\n : this.item.modelid.substring(this.item.modelid.indexOf('legs'));\n\n const textures = {\n front:\n PIXI.Loader.shared.resources[\n `${baseResourceName}${isPads ? '' : '_front'}`\n ].texture,\n back: PIXI.Loader.shared.resources[\n `${baseResourceName}${isPads ? '' : '_back'}`\n ].texture,\n };\n\n // Front pads/legs are connected ~2.5mm inwards from frame...\n const frontLegsOffset = 2.5;\n // ...and so are back pads, while back castors are expanding ~2.5mm outwards\n const backLegsOffset = isPads ? 2.5 : -2.5;\n\n // Front legs are slightly shorter than castors/model\n const backTextureRatio = faceSize.height / textures.back.height;\n const frontHeight = isPads\n ? faceSize.height\n : backTextureRatio * textures.front.height;\n\n this.dropAreas = this.dropAreas || new PIXI.Container();\n this.dropAreas.x = 0;\n\n if (!this.dropAreas.parent) {\n this.addChild(this.dropAreas);\n }\n\n this.back = this.back || new PIXI.Sprite();\n this.back.texture = textures.back;\n this.back.height = faceSize.height * ratio;\n this.back.width = (faceSize.width - backLegsOffset * 2) * ratio;\n this.back.x = scaledVertices[4][0] + backLegsOffset * ratio;\n\n // Can't explain this, but it looks a lot better than without it ¯\\_(ツ)_/¯\n this.back.y = isPads ? 5 * ratio : 0;\n\n if (!this.back.parent) {\n this.addChild(this.back);\n }\n\n this.front = this.front || new PIXI.Sprite();\n this.front.texture = textures.front;\n this.front.height = frontHeight * ratio;\n this.front.width = (faceSize.width - frontLegsOffset * 2) * ratio;\n this.front.x = frontLegsOffset * ratio;\n this.front.y =\n scaledVertices[3][1] - (faceSize.height - frontHeight) * ratio;\n\n if (!this.front.parent) {\n this.addChild(this.front);\n }\n\n this.drawChildren();\n }\n\n setHitArea() {\n this.hitArea = new PIXI.Polygon([]);\n }\n}\n","import * as PIXI from 'pixi.js-legacy';\nimport productService from '../../services/products';\nimport DynamicProppingAncestor from '../DynamicProppingAncestor';\nimport InnerDropArea from '../InnerDropArea';\nimport constants from '../../settings/constants';\nimport productsService from '../../services/products';\nimport geometry from '../util/geometry';\nimport { ITEMS } from '../../constants';\n\nexport default class Frame extends DynamicProppingAncestor {\n constructor(args) {\n super(args);\n this.skipSprites = [];\n }\n\n draw() {\n const { item, ratio } = this;\n const { outerSize, scaledVertices } = this.data;\n const postWidth = productService.getPostWidth();\n\n this.visible = !item.hidden;\n\n const textures = {\n front:\n PIXI.Loader.shared.resources[\n `${this.item.modelid.substring(\n this.item.modelid.indexOf(ITEMS.FRAME)\n )}_front_${this.item.filter.color}`\n ].texture,\n back: PIXI.Loader.shared.resources[\n `${this.item.modelid.substring(\n this.item.modelid.indexOf(ITEMS.FRAME)\n )}_back_${this.item.filter.color}`\n ].texture,\n backCover:\n PIXI.Loader.shared.resources[\n `cover${this.item.modelid.substring(\n this.item.modelid.indexOf(ITEMS.FRAME) + ITEMS.FRAME.length\n )}_back`\n ].texture,\n frontCover:\n PIXI.Loader.shared.resources[\n `cover${this.item.modelid.substring(\n this.item.modelid.indexOf(ITEMS.FRAME) + ITEMS.FRAME.length\n )}_front`\n ].texture,\n };\n\n const hasCover =\n (item.items && item.items.find(item => item.filter.type === 'cover')) ||\n (this.childrenContainer &&\n this.childrenContainer.children.some(\n child => child.item.filter.type === 'cover'\n ));\n\n this.legsContainer = this.legsContainer || new PIXI.Container();\n this.legsContainer.x = scaledVertices[4][0] - scaledVertices[0][0];\n\n if (!this.legsContainer.parent) {\n this.addChild(this.legsContainer);\n }\n\n // Since we cannot use the actual values from the cover model.\n const coverPadding = 25;\n\n this.backCover = this.backCover || new PIXI.Sprite(textures.backCover);\n this.backCover.height = (outerSize.height + coverPadding / 2) * ratio;\n this.backCover.width = (outerSize.width + coverPadding / 2) * ratio;\n this.backCover.x = -(coverPadding / 2) * ratio;\n this.backCover.y = -(coverPadding / 2) * ratio;\n this.backCover.visible = hasCover;\n\n if (!this.backCover.parent) {\n this.addChild(this.backCover);\n }\n\n this.back = this.back || new PIXI.Sprite();\n this.back.texture = textures.back;\n this.back.height = outerSize.height * ratio;\n this.back.width = (outerSize.width - postWidth) * ratio;\n\n if (!this.back.parent) {\n this.addChild(this.back);\n }\n\n this.dropAreas = this.dropAreas || new PIXI.Container();\n this.dropAreas.x = scaledVertices[4][0] - scaledVertices[0][0];\n\n if (!this.dropAreas.parent) {\n this.addChild(this.dropAreas);\n }\n\n this.rightDropAreas = this.rightDropAreas || new PIXI.Container();\n this.rightDropAreas.x = scaledVertices[4][0] - scaledVertices[0][0];\n\n if (!this.rightDropAreas.parent) {\n this.addChild(this.rightDropAreas);\n }\n\n this.childrenContainer = this.childrenContainer || new PIXI.Container();\n this.childrenContainer.x = scaledVertices[4][0] - scaledVertices[0][0];\n\n if (!this.childrenContainer.parent) {\n this.addChild(this.childrenContainer);\n }\n\n this.front = this.front || new PIXI.Sprite();\n this.front.texture = textures.front;\n this.front.height = outerSize.height * ratio;\n this.front.width = (outerSize.width - postWidth) * ratio;\n this.front.x = postWidth * ratio;\n\n if (!this.front.parent) {\n this.addChild(this.front);\n }\n\n this.frontCover = this.frontCover || new PIXI.Sprite(textures.frontCover);\n this.frontCover.height = (outerSize.height + coverPadding / 2) * ratio;\n this.frontCover.width = (outerSize.width + coverPadding / 2) * ratio;\n this.frontCover.x = -(coverPadding / 2) * ratio;\n this.frontCover.y = -(coverPadding / 2) * ratio;\n this.frontCover.visible = hasCover;\n\n this.stackContainer = this.stackContainer || new PIXI.Container();\n this.stackContainer.x = scaledVertices[4][0] - scaledVertices[0][0];\n\n if (!this.stackContainer.parent) {\n this.addChild(this.stackContainer);\n }\n\n if (!this.frontCover.parent) {\n this.addChild(this.frontCover);\n }\n\n this.coverDropAreaContainer =\n this.coverDropAreaContainer || new PIXI.Container();\n this.coverDropAreaContainer.x = scaledVertices[4][0] - scaledVertices[0][0];\n\n if (!this.coverDropAreaContainer.parent) {\n this.addChild(this.coverDropAreaContainer);\n }\n\n this.rightContainer = this.rightContainer || new PIXI.Container();\n this.rightContainer.x = scaledVertices[4][0] - scaledVertices[0][0];\n if (!this.rightContainer.parent) {\n this.addChild(this.rightContainer);\n }\n\n this.drawChildren();\n }\n\n /*\n override item's getcontainer to place outside stuff in correct container\n */\n getContainer(sprite) {\n if (geometry.extendsOutside(sprite.item, this.item, 'x')) {\n return this.rightContainer;\n }\n if (productsService.isType(sprite.item, 'leg')) {\n return this.legsContainer;\n }\n if (\n productsService.isType(sprite.item, [\n ITEMS.FRAME,\n ITEMS.SHELVING_UNIT,\n ITEMS.TOP_SHELF,\n ])\n ) {\n return this.stackContainer;\n }\n\n return super.getContainer(sprite);\n }\n\n /*\n override to supply all children for Item.setHitArea\n */\n getAllChildren() {\n return this.childrenContainer.children.concat(\n this.rightContainer.children,\n this.stackContainer.children\n );\n }\n\n drawDropAreas(slots) {\n slots = slots.filter(slot => slot.parent.itemid === this.item.itemid);\n\n if (\n slots.length &&\n !slots.every(slot => slot.filter.type === 'cover') &&\n this.backCover &&\n this.frontCover\n ) {\n this.backCover.alpha = constants.DEFAULT_TRANSPARENCY_ALPHA;\n this.frontCover.alpha = constants.DEFAULT_TRANSPARENCY_ALPHA;\n }\n\n const width = Math.floor(this.data.faceSize.width * this.ratio) - 2;\n const localItem = {\n ...this.item,\n x: 0,\n y: 0,\n };\n\n slots.forEach(slot => {\n const localSlot = { ...slot, ...slot.local };\n\n if (\n geometry.extendsOutside(localSlot, localItem) &&\n localSlot.y < localItem.height\n ) {\n const width = localSlot.partnerSlot\n ? localSlot.partnerSlot.x + localSlot.partnerSlot.width - slot.x\n : localSlot.width;\n const sprite = new InnerDropArea({\n width: width * this.ratio,\n height: localSlot.height * this.ratio,\n ratio: this.ratio,\n slot: localSlot,\n });\n\n sprite.slot = localSlot;\n\n sprite.x = localSlot.x * this.ratio;\n sprite.y =\n (this.item.height - (localSlot.y + localSlot.height)) * this.ratio;\n\n this.rightDropAreas.addChild(sprite);\n } else {\n const sprite = super.getDropAreaSprite(localSlot, width);\n\n if (sprite) {\n if (localSlot.filter.type === 'cover') {\n this.coverDropAreaContainer.addChild(sprite);\n } else {\n this.dropAreas.addChild(sprite);\n }\n }\n }\n });\n }\n\n eraseDropAreas() {\n if (this.backCover && this.frontCover) {\n this.backCover.alpha = 1;\n this.frontCover.alpha = 1;\n }\n this.coverDropAreaContainer.removeChildren();\n this.rightDropAreas.removeChildren();\n this.stackContainer.children.forEach(child => child.eraseDropAreas());\n super.eraseDropAreas();\n }\n\n displayDropAreas(slot) {\n const localSlot = slot && { ...slot, ...slot.local };\n\n this.coverDropAreaContainer.children.forEach(dropArea => {\n super.toggleDropAreaVisibility(dropArea, localSlot);\n });\n this.displayDropArea(this.rightDropAreas, slot);\n this.displayDropArea(this.dropAreas, slot);\n }\n\n addSprite(sprite, slot, tac) {\n let oldParent;\n if (sprite.parentItem && sprite.parentItem.itemid !== slot.parent.itemid) {\n oldParent = this.app.itemContainer.getSprite(sprite.parentItem.itemid);\n }\n super.addSprite(sprite, slot, tac);\n if (sprite.item.filter.type === 'cover') {\n this.draw();\n }\n if (oldParent && oldParent.adaptPropping) {\n oldParent.adaptPropping();\n }\n\n this.adaptPropping();\n }\n\n handleOldSprite(sprite) {\n if (sprite.item?.filter.type === 'cover') {\n if (this.backCover) {\n this.backCover.visible = false;\n }\n if (this.frontCover) {\n this.frontCover.visible = false;\n }\n }\n sprite.destroy();\n }\n}\n","import Item from '../Item';\n\nexport default class TopShelf extends Item {\n getHitArea() {\n const { scaledVertices } = this.data;\n const padding = 10;\n\n const vertices = [\n scaledVertices[0],\n scaledVertices[3],\n scaledVertices[7],\n [scaledVertices[7][0], scaledVertices[7][1] - padding],\n [scaledVertices[6][0], scaledVertices[6][1] - padding],\n scaledVertices[5],\n scaledVertices[1],\n ];\n\n const polygon = {\n regions: [vertices],\n };\n\n return {\n polygon,\n };\n }\n}\n","import * as PIXI from 'pixi.js-legacy';\nimport Item from '../Item';\nimport productsService from '../../services/products';\nimport { ITEMS } from '../../constants';\n\nexport default class ShelvingUnit extends Item {\n draw() {\n const { scaledVertices } = this.data;\n\n this.legsContainer = this.legsContainer || new PIXI.Container();\n this.legsContainer.x = scaledVertices[4][0] - scaledVertices[0][0];\n\n if (!this.legsContainer.parent) {\n this.addChild(this.legsContainer);\n }\n\n this.stackContainer = this.stackContainer || new PIXI.Container();\n this.stackContainer.x = scaledVertices[4][0] - scaledVertices[0][0];\n\n super.draw();\n\n if (!this.stackContainer.parent) {\n this.addChild(this.stackContainer);\n }\n }\n\n eraseDropAreas() {\n this.stackContainer.children.forEach(child => child.eraseDropAreas());\n super.eraseDropAreas();\n }\n\n getAllChildren() {\n return this.childrenContainer.children.concat(this.stackContainer.children);\n }\n\n getContainer(sprite) {\n if (productsService.isType(sprite.item, 'leg')) {\n return this.legsContainer;\n }\n if (\n productsService.isType(sprite.item, [ITEMS.FRAME, ITEMS.SHELVING_UNIT])\n ) {\n return this.stackContainer;\n }\n\n return super.getContainer(sprite);\n }\n}\n","import Item from './Item';\nimport { getProppingBounds } from '../services/products/models';\n\nexport default class ProppingItem extends Item {\n adaptPropping(movingItem, movingSprite) {\n this.draw(movingItem, movingSprite);\n }\n\n getProppingItem() {\n return this.item;\n }\n\n getHitArea() {\n const { scaledVertices } = this.data;\n const { ratio, item } = this;\n const padding = 8;\n\n const proppingItem = this.getProppingItem();\n\n let proppingHeight = 0;\n if (proppingItem) {\n const bounds = getProppingBounds(proppingItem)[0];\n if (bounds) {\n proppingHeight = bounds.size.height - item.height;\n }\n }\n\n const vertices = [\n [scaledVertices[0][0], scaledVertices[0][1] + proppingHeight * ratio],\n scaledVertices[7],\n [scaledVertices[6][0] - padding, scaledVertices[6][1]],\n [\n scaledVertices[1][0] - padding,\n scaledVertices[1][1] + proppingHeight * ratio,\n ],\n ];\n\n const polygon = {\n regions: [vertices],\n };\n\n return {\n polygon,\n };\n }\n}\n","import * as PIXI from 'pixi.js-legacy';\nimport _ from 'lodash';\nimport deepEqual from 'fast-deep-equal';\n\nimport ProppingItem from './ProppingItem';\n\nimport emitter from '../emitter';\n\nimport {\n getMatchingConnections,\n getBounds,\n getProppingBounds,\n} from '../services/products/models';\nimport productService, { getProduct } from '../services/products';\nimport proppingService from '../services/propping';\n\nimport { ITEM_MOVING, SCENE_REDRAWN } from '../settings/events';\n\nimport { replace } from '../state/tac/replace';\nimport tacHelpers from '../state/tac/tacHelpers';\n\nimport idGenerator from '../util/aactools/idGenerator';\nimport adjustableContainer from './util/adjustableContainer';\nimport getItemConfig from './util/getItemConfig';\nimport { flatten } from '../util/array';\nimport geometry from './util/geometry';\nimport project from './util/project';\n\nexport default class MultiParentClothesRail extends ProppingItem {\n constructor(args) {\n super(args);\n this.getAlpha = this.getAlpha.bind(this);\n this.onSceneRedrawn = this.onSceneRedrawn.bind(this);\n this.onItemMove = this.onItemMove.bind(this);\n\n this.db_onSceneRedrawn = _.debounce(this.onSceneRedrawn, 50, {\n leading: true,\n trailing: false,\n });\n emitter.on(ITEM_MOVING, this.onItemMove);\n emitter.on(SCENE_REDRAWN, this.db_onSceneRedrawn);\n }\n\n destroy(args) {\n emitter.off(ITEM_MOVING, this.onItemMove);\n emitter.off(SCENE_REDRAWN, this.db_onSceneRedrawn);\n super.destroy(args);\n }\n\n update(args) {\n this.config = getItemConfig(args.item);\n this.outerPipe = this.outerPipe || getProduct(this.config.outerId);\n this.selfMoving = this.app.dragging?.item?.itemid === args.item?.itemid;\n\n const { localItem, selfMoving } = this;\n\n if (args.item && args.item !== localItem) {\n const updatedItem = Object.assign({}, args.item);\n if (localItem && productService.isExtendable(updatedItem)) {\n if (!selfMoving) {\n updatedItem.width = localItem.width;\n updatedItem.height = localItem.height;\n }\n updatedItem.onlyConnectTo = localItem.onlyConnectTo;\n updatedItem.connectsTo = localItem.connectsTo;\n }\n\n this.localItem = updatedItem;\n }\n\n super.update(args);\n }\n\n setSpriteVisibilities(isExtendable) {\n if (this.sprite) {\n this.sprite.visible = !isExtendable;\n }\n if (this.adjustableContainer) {\n this.adjustableContainer.visible = isExtendable;\n }\n }\n\n drawAdjustable(propping) {\n const { config, ratio, localItem } = this;\n const { shouldShowPropping, proppingName, singleProppingBounds } = propping;\n\n const outerSize = project.outer(localItem);\n\n this.adjustableContainer = adjustableContainer(\n this.adjustableContainer,\n outerSize,\n {\n leftId: config.innerId,\n rightId: config.outerId,\n },\n ratio,\n shouldShowPropping ? { proppingName, singleProppingBounds } : {}\n );\n\n this.adjustableContainer.alpha = this.getAlpha(localItem.width);\n\n if (!this.adjustableContainer.parent) {\n this.addChild(this.adjustableContainer);\n }\n }\n\n drawFixed(propping) {\n const { item, ratio, localItem } = this;\n const { shouldShowPropping, proppingName, singleProppingBounds } = propping;\n\n const outerSize = project.outer(localItem);\n\n const texture = shouldShowPropping\n ? PIXI.Loader.shared.resources[proppingName].texture\n : PIXI.Loader.shared.resources[item.id].texture;\n\n this.sprite = this.sprite || new PIXI.Sprite();\n this.sprite.texture = texture;\n this.sprite.width = outerSize.width * ratio;\n this.sprite.height =\n (shouldShowPropping\n ? singleProppingBounds.size.height\n : outerSize.height) * ratio;\n\n if (!this.sprite.parent) {\n this.addChild(this.sprite);\n }\n }\n\n draw(movingItem) {\n const { item, options } = this;\n this.visible = !item.hidden;\n\n const isExtendable = productService.isExtendable(item);\n\n const { proppingName, singleProppingBounds } = this.getResourceName(\n movingItem,\n isExtendable\n );\n\n if (singleProppingBounds) {\n item.propping = singleProppingBounds.id;\n }\n\n const shouldShowPropping =\n this.shouldShowPropping([singleProppingBounds]) &&\n !options.measurementsActive;\n\n const proppingInfo = {\n shouldShowPropping,\n proppingName,\n singleProppingBounds,\n };\n\n if (isExtendable) {\n this.drawAdjustable(proppingInfo);\n } else {\n this.drawFixed(proppingInfo);\n }\n\n this.setSpriteVisibilities(isExtendable);\n }\n\n getResourceName(movingItem, isExtendable) {\n const { config, item, parentItem, parent, outerPipe } = this;\n const proppingItem = isExtendable ? outerPipe : item;\n\n const topAncestor = tacHelpers.getTopAncestor(this.app.tac, parentItem);\n\n const topAncestorSprite =\n isExtendable &&\n topAncestor &&\n topAncestor.itemid !== parentItem.itemid &&\n this.app.itemContainer &&\n this.app.itemContainer.getSprite(topAncestor.itemid);\n\n const useSmallestPropping = this.localItem.width > config.maxWidth;\n if (this.recalcPropping) {\n this.item.propping = null;\n }\n\n const singleProppingBounds = useSmallestPropping\n ? proppingService.smallestPropping(proppingItem)\n : proppingService.pickPropping({\n isDynamic: true,\n tac: this.app.tac,\n item,\n proppingItem,\n parentItem,\n parentContainer: parent,\n grandParentContainer:\n topAncestorSprite && topAncestorSprite.rightContainer,\n movingItem,\n });\n\n this.recalcPropping = useSmallestPropping;\n\n let proppingName;\n\n if (singleProppingBounds) {\n if (!isExtendable) {\n proppingName = `${item.id}_propping_${singleProppingBounds.id}`;\n } else {\n proppingName = `${config.outerId}_propping_${singleProppingBounds.id}`;\n }\n return {\n proppingName,\n singleProppingBounds,\n };\n }\n }\n\n getBounds() {\n const { item } = this;\n\n const bounds = super.getBounds();\n const proppingItem = productService.isExtendable(item)\n ? this.outerPipe\n : this.item;\n const proppingHeight = getProppingBounds(proppingItem).find(\n bounds => bounds.id === '1'\n ).size.height;\n bounds.height = proppingHeight * this.ratio;\n return bounds;\n }\n\n getHitArea() {\n const { scaledVertices } = this.data;\n const { item, ratio, outerPipe, config } = this;\n const padding = 8;\n\n let proppingHeight;\n if (item) {\n const proppingItem = productService.isExtendable(item) ? outerPipe : item;\n proppingHeight = getProppingBounds(proppingItem)[0].size.height;\n } else {\n proppingHeight = getProppingBounds(getProduct(config.fullId))[0].size\n .height;\n }\n\n const vertices = [\n [scaledVertices[0][0], scaledVertices[0][1] + proppingHeight * ratio],\n [scaledVertices[7][0], scaledVertices[7][1] - padding],\n [scaledVertices[6][0] - padding, scaledVertices[6][1] - padding],\n //[scaledVertices[6][0] - padding, scaledVertices[6][1]],\n [\n scaledVertices[1][0] - padding,\n scaledVertices[1][1] + proppingHeight * ratio,\n ],\n ];\n\n const polygon = {\n regions: [vertices],\n };\n\n return {\n polygon,\n };\n }\n\n getAllChildren() {\n return [];\n }\n\n eraseDropAreas() {}\n\n displayDropArea() {}\n\n getAlpha(width) {\n const { config, selfMoving, localItem, item } = this;\n\n if (selfMoving) {\n return 1; // dragging around, undocked\n }\n if (width < config.minWidth) {\n return 0; // invisible\n }\n if (localItem.partnerBlocked) {\n return 0; // something between here and connectsTo\n }\n if (!localItem.connectsTo && idGenerator.hasRealId(item)) {\n return 0; // invisible and considered not here (temporary parent switch)\n }\n if (width > config.maxWidth) {\n return 0.4; // dimmed\n }\n return 1; // opaque\n }\n\n railCanReach(stateTac, targetSlot, left, moveItem) {\n const { config } = this;\n const tac = _.cloneDeep(stateTac);\n replace(tac.items, moveItem);\n\n const right = targetSlot.parent || targetSlot;\n const tacRight = tacHelpers.getItem(tac, right.itemid);\n const gtacRight = tacHelpers.getGlobalCoords(tacRight, tac);\n const gPos = tacHelpers.getGlobalCoords(this.item, tac);\n\n const bounds = getBounds(this.item, 'space');\n const crRect = {\n x: gPos.x,\n y: gPos.y - bounds.size.height,\n z: gPos.z,\n height: bounds.size.height,\n width: gtacRight.x - gPos.x + config.mountOffset + 1, // a little wider so it collides\n depth: bounds.size.depth,\n };\n if (crRect.width < 0) {\n return false;\n }\n\n const rects = tacHelpers.getRawRects(tac, left);\n const blockers = rects.filter(blocker =>\n geometry.collides(blocker, crRect)\n );\n // get rid of self and parents\n const reached = blockers.filter(\n item => !tacHelpers.isAncestor(tac, item, this.item)\n );\n // make sure we collide with intended frame and any other collisions is in same stack\n const topRight = tacHelpers.getTopAncestor(tac, tacRight);\n if (\n reached.every(\n item =>\n topRight.itemid === item.itemid ||\n tacHelpers.isAncestor(tac, topRight, item)\n )\n ) {\n return true;\n }\n return false;\n }\n\n /*\n returns false if update failed, true if successful, and undefined if not applicable\n */\n onItemMove(tac, moveItem) {\n // start of left stack\n const left = tacHelpers.getTopAncestor(tac, this.item);\n const isExtendable = productService.isExtendable(this.item);\n if (!moveItem) {\n // nothing moved but we need to set ourselves up. imagine parent moving 0 pixels\n moveItem = left;\n }\n // there are many reasons not to care about this:\n\n if (!Number.isFinite(this.item.itemid)) {\n // new items from swiper aren't interesting\n this.selfMoving = true;\n\n // ...but we need to make sure that they are at set to the minimum width,\n // in order to be drawn correctly\n if (isExtendable && this.localItem.width < this.config.minWidth) {\n this.localItem.width = this.config.minWidth;\n this.draw();\n }\n\n return;\n }\n\n if (!Number.isFinite(moveItem.itemid)) {\n // new items from swiper aren't interesting\n return;\n }\n\n if (!isExtendable) {\n // we're mounted inside a frame\n return;\n }\n this.selfMoving = moveItem.itemid === this.item.itemid;\n\n if (\n !tacHelpers.isAncestor(tac, moveItem, this.item) &&\n !tacHelpers.isAncestor(tac, moveItem, this.item.connectsTo) &&\n (!this.item.connectsTo ||\n moveItem.itemid !== this.item.connectsTo.itemid) &&\n !this.selfMoving\n ) {\n // we're attached to something entirely different, or positioned at a lower position in the stack\n return;\n }\n\n let conns;\n let moveRight = 0;\n const move = tacHelpers.diffFromTac(tac, moveItem);\n\n if (this.localItem.onlyConnectTo) {\n // since we decided on a partner, drop the promiscuity and stick together\n const stack = tacHelpers.getTopAncestor(\n tac,\n this.localItem.onlyConnectTo\n );\n // find the tac pos of right stack\n const tacStack = tacHelpers.getItem(tac, stack.itemid);\n if (!tacStack) {\n // stack is missing since it was deleted.\n // 'this' will be destroyed so we dont _need_ cleanup, but drawing it makes it look faster\n this.localItem.connectsTo = null;\n this.draw(moveItem);\n return false;\n }\n conns = getMatchingConnections(this.outerPipe, {\n items: [tacStack],\n });\n\n moveRight = stack.itemid === moveItem.itemid ? move.x : 0;\n } else {\n conns = getMatchingConnections(this.outerPipe, tac);\n }\n\n const tacItem = tacHelpers.getItem(tac, this.item.itemid);\n const startPos = tacHelpers.getGlobalCoords(tacItem, tac);\n const globalPos = {\n x: startPos.x + move.x,\n y: startPos.y + move.y,\n z: startPos.z + move.z,\n };\n const targetY = this.item.height + globalPos.y;\n\n const usefulConnections = flatten(\n flatten(\n conns\n .map(conn => {\n const pos = tacHelpers.getGlobalCoords(conn, tac);\n return Object.assign({}, conn, pos);\n })\n .filter(conn => conn.x + moveRight > globalPos.x)\n ).map(parent =>\n parent.connections.map(connection => {\n const out = Object.assign({}, connection);\n\n out.x = connection.x + parent.x;\n out.y = connection.y + parent.y;\n out.z = connection.z + parent.z;\n out.parent = parent;\n return out;\n })\n )\n );\n\n const targetSlot = usefulConnections\n .filter(conn => conn.y === targetY)\n .sort((a, b) => (a.x > b.x ? 1 : b.x > a.x ? -1 : 0))[0];\n\n if (!targetSlot) {\n this.localItem.connectsTo = null;\n this.localItem.width = this.config.minWidth;\n this.draw(moveItem);\n return false;\n }\n const right = tacHelpers.getTopAncestor(tac, targetSlot.parent);\n\n // if we're on the wrong parent we drop the connection\n if (\n !this.selfMoving &&\n (move.x || move.y || move.z) &&\n tacHelpers.getTopAncestor(tac, moveItem).itemid !== moveItem.itemid\n ) {\n this.localItem.connectsTo = null;\n this.draw(moveItem);\n return false;\n }\n\n this.localItem.partnerBlocked = !this.railCanReach(\n tac,\n targetSlot,\n left,\n moveItem\n );\n\n if (this.localItem.partnerBlocked) {\n this.localItem.connectsTo = null;\n this.draw(moveItem);\n return false;\n }\n\n if (\n targetSlot &&\n this.localItem.onlyConnectTo &&\n targetSlot.parent.itemid !== this.localItem.onlyConnectTo.itemid\n ) {\n this.localItem.connectsTo = null;\n this.draw(moveItem);\n return false;\n }\n\n const preMoveItem = tacHelpers.getItem(tac, moveItem.itemid);\n const gPreMoveItem = tacHelpers.getGlobalCoords(preMoveItem, tac);\n\n let moveDistance = 0;\n if (moveItem.itemid === left.itemid) {\n moveDistance = gPreMoveItem.x - moveItem.x + move.x;\n }\n if (moveItem.itemid === right.itemid) {\n moveDistance = moveItem.x - gPreMoveItem.x + move.x;\n }\n\n const newWidth = targetSlot.x - globalPos.x + moveDistance;\n this.localItem.width = newWidth;\n this.localItem.connectsTo = targetSlot.parent;\n this.localItem.onlyConnectTo = targetSlot.parent;\n this.draw(moveItem);\n return true;\n }\n\n onSceneRedrawn(tac) {\n if (this.removing || !this.item) {\n this.destroy();\n return;\n }\n\n // update our copy\n this.item = tacHelpers.getItem(tac, this.item.itemid);\n\n if (!this.item) {\n this.destroy();\n return;\n }\n\n const { config, item, localItem } = this;\n\n const parent = tacHelpers.getParent(tac, item);\n const isExtendable = productService.isExtendable(this.item);\n\n if (\n isExtendable &&\n (!this.onItemMove(tac) || localItem.width > config.maxWidth)\n ) {\n this.removing = true;\n this.app.removeItem &&\n this.app.removeItem(item, {\n automatedUpdate: true,\n });\n } else if (\n isExtendable &&\n (item.width !== localItem.width ||\n (!item.connectsTo && localItem.connectsTo) ||\n (item.connectsTo &&\n localItem.connectsTo &&\n !deepEqual(item.connectsTo, localItem.connectsTo)))\n ) {\n const newItem = {\n ...item,\n width: localItem.width,\n connectsTo: localItem.connectsTo,\n };\n\n const hasChangedWidth = item.width !== localItem.width;\n\n this.app.updateItem &&\n this.app.updateItem(newItem, parent, {\n automatedUpdate: true,\n extended: hasChangedWidth,\n });\n } else if (!isExtendable && localItem.connectsTo) {\n const newItem = {\n ...this.item,\n connectsTo: null,\n };\n\n this.app.updateItem &&\n this.app.updateItem(newItem, parent, {\n automatedUpdate: true,\n });\n }\n }\n}\n","import MultiParentClothesRail from '../MultiParentClothesRail';\n\nexport default class ClothesRail extends MultiParentClothesRail {\n //Keep for now, JONAXEL and AURDAL implementations will differ in the future\n}\n","import * as PIXI from 'pixi.js';\n\nimport ProppingItem from '../ProppingItem';\nimport tacHelpers from '../../state/tac/tacHelpers';\nimport project from '../util/project';\nimport proppingService from '../../services/propping';\nimport { getProppingBounds } from '../../services/products/models';\nimport constants from '../../settings/constants';\nimport { config as asConfig } from './AdjustableConfig';\nimport productService, { getProduct } from '../../services/products';\nimport adjustableContainer from '../util/adjustableContainer';\nimport { ITEMS } from '../../constants';\n\nexport default class ClothesRail extends ProppingItem {\n update(args) {\n this.isExtendable = productService.isExtendable(args.item);\n super.update(args);\n }\n\n getSectionSprite(parent, section) {\n if (\n !section ||\n !parent ||\n parent.itemid === section.itemid ||\n !this.app.itemContainer\n ) {\n return;\n }\n return (\n this.app.itemContainer.getSprite(section.itemid) ||\n this.app.itemContainer.children.find(\n sprite => sprite.item.itemid === section.itemid\n )\n );\n }\n\n isMobilePortrait() {\n const { isMobile, isPortrait } = this.app.itemContainer.options;\n return isMobile && isPortrait;\n }\n\n getProppingItem() {\n return this.isExtendable\n ? {\n ...this.item,\n ...getProduct(asConfig.clothesrail[this.item.id].rightId),\n }\n : this.item;\n }\n\n getBounds() {\n const bounds = super.getBounds();\n const proppingItem = this.getProppingItem();\n const proppingHeight = getProppingBounds(proppingItem).find(\n bounds => bounds.id === '1'\n ).size.height;\n bounds.height = proppingHeight * this.ratio;\n\n // Compensate for padding, otherwise it will look like the shelf is included in the selection\n bounds.y += this.isMobilePortrait()\n ? constants.OUTLINE_PADDING_MOBILE_PORTRAIT\n : constants.OUTLINE_PADDING;\n\n return bounds;\n }\n\n shouldUseSmallestPropping() {\n const { app, parentItem } = this;\n\n // If we are either coming from the swiper or our parent (shelf) or any upright is being dragged\n return (\n !parentItem ||\n !parentItem.itemid ||\n (app.dragging &&\n (productService.isType(app.dragging.item, ITEMS.UPRIGHT) ||\n app.dragging.item.itemid === parentItem.itemid))\n );\n }\n\n getResourceName(movingItem, movingSprite) {\n const { item, parentItem } = this;\n\n const proppingItem = this.getProppingItem();\n\n const section = tacHelpers.getTopAncestor(this.app.tac, parentItem);\n\n const sectionSprite = this.getSectionSprite(parentItem, section);\n\n const singleProppingBounds = this.shouldUseSmallestPropping()\n ? proppingService.smallestPropping(proppingItem)\n : proppingService.pickPropping({\n isDynamic: true,\n tac: this.app.itemContainer\n ? this.app.itemContainer.tac\n : this.app.tac,\n item: proppingItem,\n parentItem,\n section: sectionSprite,\n movingItem,\n movingSprite,\n });\n\n let proppingName;\n\n if (singleProppingBounds) {\n if (!this.isExtendable) {\n proppingName = `${item.id}_propping_${singleProppingBounds.id}`;\n } else {\n proppingName = `${\n asConfig.clothesrail[this.item.id].rightId\n }_propping_${singleProppingBounds.id}`;\n }\n return {\n proppingName,\n singleProppingBounds,\n };\n }\n return {\n proppingName: `${item.id}_propping_${singleProppingBounds.id}`,\n singleProppingBounds,\n };\n }\n\n setSpriteVisibilities() {\n if (this.sprite) {\n this.sprite.visible = !this.isExtendable;\n }\n if (this.adjustableContainer) {\n this.adjustableContainer.visible = this.isExtendable;\n }\n }\n\n drawFixed(propping) {\n const { item, ratio } = this;\n const { shouldShowPropping, proppingName, singleProppingBounds } = propping;\n const { outerSize } = this.data;\n\n const texture = shouldShowPropping\n ? PIXI.Loader.shared.resources[proppingName].texture\n : PIXI.Loader.shared.resources[item.id].texture;\n\n this.sprite = this.sprite || new PIXI.Sprite();\n this.sprite.texture = texture;\n this.sprite.width = item.width * ratio;\n this.sprite.height = shouldShowPropping\n ? project.outer(singleProppingBounds.size).height * ratio\n : outerSize.height * ratio;\n\n if (!this.sprite.parent) {\n this.addChild(this.sprite);\n }\n }\n\n drawAdjustable(propping) {\n const { shouldShowPropping, proppingName, singleProppingBounds } = propping;\n\n this.adjustableContainer = adjustableContainer(\n this.adjustableContainer,\n this.data.outerSize,\n asConfig.clothesrail[this.item.id],\n this.ratio,\n shouldShowPropping ? { proppingName, singleProppingBounds } : {}\n );\n\n if (!this.adjustableContainer.parent) {\n this.addChild(this.adjustableContainer);\n }\n }\n\n draw(movingItem, movingSprite) {\n const { item } = this;\n\n const { proppingName, singleProppingBounds } = this.getResourceName(\n movingItem,\n movingSprite\n );\n\n item.propping = singleProppingBounds.id;\n const shouldShowPropping =\n this.shouldShowPropping([singleProppingBounds]) &&\n !this.options.measurementsActive;\n\n const proppingInfo = {\n shouldShowPropping,\n proppingName,\n singleProppingBounds,\n };\n\n if (this.isExtendable) {\n this.drawAdjustable(proppingInfo);\n } else {\n this.drawFixed(proppingInfo);\n }\n\n this.setSpriteVisibilities();\n }\n}\n","import MultiParentClothesRail from '../MultiParentClothesRail';\n\nexport default class ClothesRail extends MultiParentClothesRail {\n // Keep for now, we'll probably need it when connecting to wall\n}\n","import * as PIXI from 'pixi.js-legacy';\nimport Item from '../Item';\n\nexport default class Cover extends Item {\n constructor(args) {\n super(args);\n this.parentFrame = null;\n }\n\n draw() {\n const { scaledVertices } = this.data;\n\n this.childrenContainer = this.childrenContainer || new PIXI.Container();\n this.childrenContainer.x = scaledVertices[4][0] - scaledVertices[0][0];\n\n if (!this.childrenContainer.parent) {\n this.addChild(this.childrenContainer);\n }\n\n if (\n !this.parentItem ||\n !this.parentItem.itemid ||\n (this.parentFrame &&\n this.parentItem.itemid !== this.parentFrame.item.itemid)\n ) {\n if (this.parentFrame) {\n this.parentFrame.childrenContainer.removeChild(this);\n this.parentFrame && this.parentFrame.draw();\n }\n super.draw();\n } else {\n this.removeChildren();\n }\n\n this.parentFrame =\n this.parentItem &&\n this.app &&\n this.app.itemContainer &&\n this.app.itemContainer.getSprite(this.parentItem.itemid);\n }\n setHitArea() {\n this.hitArea = new PIXI.Polygon([]);\n }\n drawDropAreas() {}\n eraseDropAreas() {}\n}\n","import Item from '../Item';\nimport emitter from '../../emitter';\nimport { PICKUP_ITEM } from '../../settings/events';\nimport geometry from '../util/geometry';\n\nexport default class MountingRail extends Item {\n constructor(args) {\n super(args);\n\n this.onPickup = this.onPickup.bind(this);\n emitter.on(PICKUP_ITEM, this.onPickup);\n this.disableInteraction = true;\n this.cursor = 'default';\n }\n\n onPickup(item) {\n this.alpha = geometry.collides(this.item, item.item, {\n right: 50,\n left: 50,\n })\n ? 0\n : 1;\n }\n\n destroy(args) {\n emitter.off(PICKUP_ITEM, this.onPickup);\n super.destroy(args);\n }\n}\n","import Item from '../Item';\n\nexport default class MountingRail extends Item {\n constructor(args) {\n super(args);\n this.disableInteraction = true;\n this.cursor = 'default';\n }\n\n draw() {\n super.draw();\n\n this.alpha =\n this.options?.wallResizerActive || this.app?.dragging?.item ? 0 : 1;\n }\n}\n","import ProppingItem from '../ProppingItem';\nimport bracketsMixin from './bracketsMixin';\n\nexport default class BoaxelProppingItem extends bracketsMixin(ProppingItem) {}\n","import Section from './Section';\n\nexport default class AdjustableSection extends Section {}\n","import * as PIXI from 'pixi.js-legacy';\n\nimport InnerDropArea from '../InnerDropArea';\n\nimport DynamicProppingAncestor from '../DynamicProppingAncestor';\nimport geometry from '../util/geometry';\nimport project from '../util/project';\nimport tacHelpers from '../../state/tac/tacHelpers';\nimport constants from '../../settings/constants';\n\nexport default class Sidewall extends DynamicProppingAncestor {\n drawSideWall() {\n this.place();\n const { item } = this;\n const { outerSize, scaledVertices } = this.data;\n\n this.sideWallContainer = this.sideWallContainer || new PIXI.Container();\n\n const currentSpace = tacHelpers.getCurrentSpace(this.app.tac);\n\n const entireWall = project.outer({\n ...item,\n height: currentSpace.height,\n });\n\n this.sideWallSprite =\n this.sideWallSprite || new PIXI.Sprite(PIXI.Texture.WHITE);\n this.sideWallSprite.width = outerSize.width * this.ratio;\n this.sideWallSprite.x = 1; //can't explain, but looks better.\n this.sideWallSprite.y = -this.y;\n this.sideWallSprite.height =\n entireWall.height * this.ratio + scaledVertices[3][1];\n this.sideWallSprite.tint = 0xe0e0e0;\n\n this.skirt = this.skirt || new PIXI.Graphics();\n const skirtHeight = this.app.room.getSkirtHeight();\n const skirtPoints = [\n {\n x: 0,\n y: scaledVertices[0][1],\n },\n { x: 0, y: scaledVertices[0][1] - skirtHeight },\n {\n x: scaledVertices[5][0],\n y: scaledVertices[5][1] - skirtHeight,\n },\n {\n x: scaledVertices[5][0],\n y: scaledVertices[5][1],\n },\n {\n x: 0,\n y: scaledVertices[0][1],\n },\n ].map(point => {\n return new PIXI.Point(point.x, point.y);\n });\n\n this.skirt\n .clear()\n .beginFill(constants.RIGHT_SIDEWALL_SKIRT_COLOR)\n .drawPolygon(skirtPoints)\n .endFill();\n\n this.skirtEdge = this.skirtEdge || new PIXI.Graphics();\n this.skirtEdge\n .clear()\n .lineStyle(\n constants.SKIRT_EDGE_THICKNESS * this.ratio,\n constants.RIGHT_SIDEWALL_SKIRT_EDGE_COLOR\n )\n .moveTo(skirtPoints[1].x, skirtPoints[1].y)\n .lineTo(skirtPoints[2].x, skirtPoints[2].y);\n\n this.floorCrop = this.floorCrop || new PIXI.Graphics();\n\n const maskPoints = [\n {\n x: scaledVertices[5][0] - this.app.room.slopeWidth,\n y: scaledVertices[5][1] + this.app.room.slopeHeight,\n },\n {\n x: this.sideWallSprite.x + this.sideWallSprite.width,\n y: scaledVertices[5][1] + this.app.room.slopeHeight,\n },\n {\n x: this.sideWallSprite.x + this.sideWallSprite.width,\n y: scaledVertices[5][1],\n },\n {\n x: scaledVertices[5][0] - this.app.room.slopeWidth,\n y: scaledVertices[5][1] + this.app.room.slopeHeight,\n },\n ].map(point => {\n return new PIXI.Point(point.x, point.y);\n });\n\n this.floorCrop.clear().beginTextureFill().drawPolygon(maskPoints).endFill();\n\n !this.skirt.parent && this.sideWallContainer.addChild(this.skirt);\n !this.skirtEdge.parent && this.sideWallContainer.addChild(this.skirtEdge);\n !this.sideWallSprite.parent &&\n this.sideWallContainer.addChild(this.sideWallSprite);\n !this.floorCrop.parent && this.sideWallContainer.addChild(this.floorCrop);\n !this.sideWallContainer.parent && this.addChild(this.sideWallContainer);\n }\n\n draw() {\n this.interactive = false;\n\n const { item } = this;\n this.visible = !item.hidden;\n\n const { scaledVertices } = this.data;\n\n this.rightDropAreas = this.rightDropAreas || new PIXI.Container();\n this.rightDropAreas.x = scaledVertices[4][0] - scaledVertices[0][0];\n\n if (!this.rightDropAreas.parent) {\n this.addChild(this.rightDropAreas);\n }\n\n this.childrenContainer = this.childrenContainer || new PIXI.Container();\n this.childrenContainer.x = scaledVertices[4][0] - scaledVertices[0][0];\n\n if (!this.childrenContainer.parent) {\n this.addChild(this.childrenContainer);\n }\n\n if (item.x > 0) {\n // only for right sidewall\n this.drawSideWall();\n\n if (\n !this.options.wallResizerActive &&\n tacHelpers.isClothesRailConnected(this.app.tac, item)\n ) {\n this.sideWallContainer.alpha = 1;\n this.sideWallSprite.alpha = 0.5;\n } else {\n this.sideWallContainer.alpha = 0;\n }\n }\n\n this.drawChildren();\n }\n\n drawDropAreas(slots) {\n slots = slots.filter(slot => slot.parent.itemid === this.item.itemid);\n\n const localItem = {\n ...this.item,\n x: 0,\n y: 0,\n };\n\n slots.forEach(slot => {\n const localSlot = { ...slot, ...slot.local };\n\n if (\n geometry.extendsOutside(localSlot, localItem) &&\n localSlot.y < localItem.height\n ) {\n const width = localSlot.partnerSlot\n ? localSlot.partnerSlot.x + localSlot.partnerSlot.width - slot.x\n : localSlot.width;\n const sprite = new InnerDropArea({\n width: width * this.ratio,\n height: localSlot.height * this.ratio,\n ratio: this.ratio,\n slot: localSlot,\n });\n\n sprite.slot = localSlot;\n\n sprite.x = localSlot.x * this.ratio;\n sprite.y =\n (this.item.height - (localSlot.y + localSlot.height)) * this.ratio;\n\n this.rightDropAreas.addChild(sprite);\n }\n });\n }\n\n displayDropAreas(slot) {\n this.displayDropArea(this.rightDropAreas, slot);\n }\n\n eraseDropAreas() {\n this.rightDropAreas.removeChildren();\n }\n}\n","import Shelf from '../Shelf';\nimport bracketsMixin from './bracketsMixin';\n\nexport default class BoaxelShelf extends bracketsMixin(Shelf) {}\n","import * as PIXI from 'pixi.js-legacy';\n\nimport bracketsMixin from './bracketsMixin';\nimport Item from '../Item';\nimport constants from '../../settings/constants';\nimport getDepthOffset from '../util/getDepthOffset';\nimport { config as asConfig } from './AdjustableConfig';\nimport productService from '../../services/products';\nimport project from '../util/project';\nimport tacHelpers from '../../state/tac/tacHelpers';\nimport geometry from '../util/geometry';\nimport adjustableContainer from '../util/adjustableContainer';\n\nexport default class Table extends bracketsMixin(Item) {\n update(args) {\n this.localLegItem = this.localLegItem || {\n ...productService.getProduct(asConfig.legs.topId),\n };\n\n super.update(args);\n\n if (this.parentItem?.itemid) {\n const { item, parentItem } = this;\n const rects = tacHelpers.getRawRects(this.app.tac);\n const gItem = {\n ...item,\n x: item.x + parentItem.x,\n y: item.y + parentItem.y,\n z: item.z + parentItem.z,\n };\n\n const collidingRects = geometry.getCollidingRects(gItem, rects);\n const collidingTableSections = collidingRects.filter(\n rect =>\n productService.isSection(rect) &&\n rect.itemid !== parentItem.itemid &&\n productService.fitsTable(rect)\n );\n\n this.forceClosedDropAreas = this.forceClosedDropAreas || [];\n this.forceClosedDropAreas.forEach(dropArea => {\n // only force-show dropAreas that are not on our current parent\n if (dropArea.parent?.parent?.item?.itemid !== parentItem.itemid) {\n dropArea.visible = true;\n }\n });\n this.forceClosedDropAreas = [];\n\n const gSpriteBounds = {\n ...this.getGlobalPosition(),\n height: this.data.faceSize.height * this.ratio,\n width: this.sprite.width,\n };\n\n collidingTableSections.forEach(section => {\n const sprite = this.app.getSprite(section.itemid);\n\n if (sprite) {\n sprite.dropAreas.children\n .filter(dropArea => {\n return geometry.collides(\n {\n ...dropArea.getGlobalPosition(),\n height: dropArea.height,\n width: dropArea.width,\n },\n gSpriteBounds\n );\n })\n .forEach(dropArea => {\n dropArea.visible = false;\n this.forceClosedDropAreas.push(dropArea);\n });\n }\n });\n }\n }\n\n getSelfHitArea() {\n const { scaledVertices } = this.data;\n const topPadding = constants.DISTANCE_BETWEEN_ATTACHMENTS * this.ratio;\n const bottomPadding = topPadding / 2;\n const vertices = [\n [scaledVertices[0][0], scaledVertices[0][1] + bottomPadding],\n [scaledVertices[3][0], scaledVertices[3][1]],\n [scaledVertices[7][0], scaledVertices[7][1] - topPadding],\n [scaledVertices[6][0], scaledVertices[6][1] - topPadding],\n [scaledVertices[5][0], scaledVertices[5][1]],\n [scaledVertices[1][0], scaledVertices[1][1] + bottomPadding],\n ];\n\n return {\n regions: [vertices],\n };\n }\n\n draw() {\n const { scaledVertices } = this.data;\n\n const depthOffset = getDepthOffset({ z: this.localLegItem.depth });\n this.localLegItem.height = Math.min(\n Math.max(this.item?.y + this.parentItem?.y, asConfig.legs.minHeight),\n asConfig.legs.maxHeight\n );\n\n this.legsContainer = this.legsContainer || new PIXI.Container();\n this.legsContainer.x = scaledVertices[0][0] + depthOffset.x * this.ratio;\n this.legsContainer.y = scaledVertices[0][1] - depthOffset.y * this.ratio;\n\n if (!this.legsContainer.parent) {\n this.addChild(this.legsContainer);\n }\n\n const legOuterSize = project.outer(this.localLegItem);\n\n this.leftLegContainer = adjustableContainer(\n this.leftLegContainer,\n legOuterSize,\n asConfig.legs,\n this.ratio\n );\n\n if (!this.leftLegContainer.parent) {\n this.legsContainer.addChild(this.leftLegContainer);\n }\n\n this.rightLegContainer = adjustableContainer(\n this.rightLegContainer,\n legOuterSize,\n asConfig.legs,\n this.ratio\n );\n\n this.rightLegContainer.x =\n scaledVertices[1][0] -\n this.rightLegContainer.width -\n depthOffset.x * this.ratio;\n\n if (!this.rightLegContainer.parent) {\n this.legsContainer.addChild(this.rightLegContainer);\n }\n\n super.draw();\n }\n}\n","import constants from '../../settings/constants';\nimport InnerDropArea from '../InnerDropArea';\nimport Shelf from '../Shelf';\nimport geometry from '../util/geometry';\n\nexport default class IvarShelf extends Shelf {\n draw() {\n super.draw();\n this.interactive = !this.item.belongsTo;\n }\n\n drawDropAreas(slots) {\n const { scaledVertices } = this.data;\n\n this.dropAreas.x = 0;\n this.dropAreas.y = scaledVertices[0][1];\n\n super.drawDropAreas(slots);\n }\n\n getDropAreaSprite(localSlot) {\n const localItem = {\n ...this.item,\n x: 0,\n y: 0,\n };\n if (\n geometry.collides(localSlot, localItem, {\n top: 1,\n })\n ) {\n const sprite = new InnerDropArea({\n width: localSlot.width * this.ratio,\n height: localSlot.height * this.ratio,\n ratio: this.ratio,\n slot: localSlot,\n fullSize: true,\n });\n\n sprite.slot = localSlot;\n sprite.x = localSlot.x * this.ratio;\n\n return sprite;\n }\n }\n\n getSelfHitArea() {\n const { scaledVertices } = this.data;\n\n const padding = constants.DISTANCE_BETWEEN_ATTACHMENTS * this.ratio;\n\n const vertices = [\n [scaledVertices[0][0], scaledVertices[0][1] + padding],\n [scaledVertices[3][0], scaledVertices[3][1]],\n [scaledVertices[7][0], scaledVertices[7][1] - padding],\n [scaledVertices[6][0], scaledVertices[6][1] - padding],\n [scaledVertices[5][0], scaledVertices[5][1]],\n [scaledVertices[1][0], scaledVertices[1][1] + padding],\n ];\n\n return {\n regions: [vertices],\n };\n }\n\n sortChildren() {\n this.bottomContainer.children.sort((a, b) => a.item.x - b.item.x);\n }\n}\n","import Item from '../Item';\nimport constants from '../../settings/constants';\n\nexport default class Drawer extends Item {\n getSelfHitArea() {\n const { scaledVertices } = this.data;\n\n // The top padding is negative, we are decreasing the hit area\n const topPadding = -constants.DISTANCE_BETWEEN_ATTACHMENTS * this.ratio;\n\n const vertices = [\n [scaledVertices[0][0], scaledVertices[0][1]],\n [scaledVertices[3][0], scaledVertices[3][1]],\n [scaledVertices[7][0], scaledVertices[7][1] - topPadding],\n [scaledVertices[6][0], scaledVertices[6][1] - topPadding],\n [scaledVertices[5][0], scaledVertices[5][1]],\n [scaledVertices[1][0], scaledVertices[1][1]],\n ];\n\n return {\n regions: [vertices],\n };\n }\n}\n","import _ from 'lodash';\n\nimport Item from '../Item';\nimport updateItem from '../../state/tac/tacReducer/updateItem';\nimport updateDependentItems from '../../state/tac/tacReducer/updateDependentItems';\nimport addItem from '../../state/tac/tacReducer/addItem';\nimport removeItem from '../../state/tac/tacReducer/removeItem';\n\nexport default class Doors extends Item {\n /**\n * Gets a new version of the TAC as it looks after the doors have been changed\n * @param {*} door\n * @param {*} parentItem\n * @returns {Object}\n */\n getPostDoorUpdateTac = (door, parentItem) => {\n if (!parentItem.itemid) {\n // Existing, un-docked\n return { ...removeItem(this.app.itemContainer.tac, door) };\n } else if (!door.itemid) {\n // New, docked\n return { ...addItem(this.app.itemContainer.tac, door, parentItem) };\n } else {\n // Existing, docked\n return { ...updateItem(this.app.itemContainer.tac, door, parentItem) };\n }\n };\n\n /**\n * Gets a new version of the TAC as it looks after the (simulated) update\n * @param {*} door\n * @param {*} parentItem\n * @returns {Object | undefined}\n */\n\n getSimulatedUpdateTac = (door, parentItem) => {\n const tacWithDoorsUpdated = this.getPostDoorUpdateTac(door, parentItem);\n\n if (tacWithDoorsUpdated && !_.isEmpty(tacWithDoorsUpdated)) {\n const tacWithDoorDependentsUpdated = {\n ...updateDependentItems(tacWithDoorsUpdated, { triggerItem: door }),\n };\n\n if (\n tacWithDoorDependentsUpdated &&\n !_.isEmpty(tacWithDoorDependentsUpdated)\n ) {\n return tacWithDoorDependentsUpdated;\n }\n }\n };\n\n /**\n * Simulates the update that would happen if a set of doors were to be dropped\n * at a given position during a dragging event\n * @param {*} newItem The doors with their new position\n * @param {*} parentItem The new parent at this position\n * @returns {undefined}\n */\n updateDependentItemsDragging = (newItem, parentItem) => {\n if (!newItem.itemid && !parentItem.itemid) {\n // New, un-docked\n return;\n }\n\n const updatedTac = this.getSimulatedUpdateTac(newItem, parentItem);\n\n if (updatedTac) {\n this.app.itemContainer.update({\n room: this.app.room.model,\n options: this.app.itemContainerOptions,\n ratio: this.app.room.ratio,\n tac: updatedTac,\n updateOrigo: false,\n });\n }\n };\n}\n","import * as PIXI from 'pixi.js-legacy';\nimport project from '../util/project';\nimport productService from '../../services/products';\nimport tacHelpers from '../../state/tac/tacHelpers';\nimport { ITEMS } from '../../constants';\n\nconst LEFT = 'left';\nconst RIGHT = 'right';\n\nconst bracketsMixin = superclass =>\n class extends superclass {\n // Override\n drawBracket(side) {\n if (!productService.getFittingBracket(this.item, side)) {\n // This is not a bracketed item.\n return;\n }\n\n const parent = tacHelpers.getParent(this.app.tac, this.item);\n const ancestor = tacHelpers.getTopAncestor(this.app.tac, this.item);\n\n if (!parent || !productService.isType(ancestor, ITEMS.SECTION_POSTS)) {\n return;\n }\n\n if (this._isDragging) {\n this.bracketContainers[side].alpha = 0;\n return;\n }\n this.bracketContainers[side].alpha = 1;\n\n if (this.brackets[side] && this.brackets[side].parent) {\n return;\n }\n\n let positionIdentifier;\n if (side === LEFT) {\n positionIdentifier = '_L';\n } else if (side === RIGHT) {\n positionIdentifier = '_R';\n } else {\n // We don't seem to have a valid direction.\n return;\n }\n\n const proppingTop = this._getProppingTop();\n\n const tacItem = tacHelpers.getItem(this.app.tac, this.item.itemid);\n const gItem = tacHelpers.getGlobalCoords(tacItem, this.app.tac);\n const bracket = tacItem.items.find(item =>\n item.modelid.includes(positionIdentifier)\n );\n if (!bracket) return;\n const gBracket = tacHelpers.getGlobalCoords(bracket, this.app.tac);\n const bracketOffset =\n gBracket.y + gBracket.height - (gItem.y + gItem.height);\n\n const resourceName = bracket.id;\n const resource = PIXI.Loader.shared.resources[resourceName];\n this.brackets[side] =\n this.brackets[side] || new PIXI.Sprite(resource.texture);\n const outerSize = project.outer(bracket);\n this.brackets[side].width = outerSize.width * this.ratio;\n this.brackets[side].height = outerSize.height * this.ratio;\n this.brackets[side].itemRef = bracket.itemid;\n if (side === RIGHT) {\n this.brackets[side].x = this.sprite.width - this.brackets[side].width;\n } else if (side === LEFT) {\n const bracketPairBox = {\n ...bracket,\n width: this.item.width,\n };\n const bracketPairBoxSize = project.outer(bracketPairBox);\n\n this.brackets[side].x =\n this.sprite.width - bracketPairBoxSize.width * this.ratio;\n }\n this.brackets[side].y = (proppingTop - bracketOffset) * this.ratio;\n\n if (!this.brackets[side].parent) {\n this.bracketContainers[side].addChild(this.brackets[side]);\n }\n }\n };\n\nexport default bracketsMixin;\n","import bracketsMixin from './bracketsMixin';\nimport Shelf from '../Shelf';\n\nexport default class ElvarliShelf extends bracketsMixin(Shelf) {}\n","import bracketsMixin from './bracketsMixin';\nimport Item from '../Item';\n\nexport default class ElvarliDrawer extends bracketsMixin(Item) {}\n","import tacHelpers from '../../state/tac/tacHelpers';\nimport productService from '../../services/products';\n\nimport Item from '../Item';\nimport { ITEMS } from '../../constants';\nexport default class ElvarliShelfDrawer extends Item {\n draw() {\n super.draw();\n const parentIsSection = () => {\n const parent = tacHelpers.getParent(this.app.tac, this.item);\n if (!parent || !parent.itemid) return;\n return (\n parent.filter.type === ITEMS.SECTION_POSTS ||\n parent.filter.type === ITEMS.SECTION_SIDE_UNITS\n );\n };\n if (parentIsSection()) {\n this.sprite.visible = false;\n }\n }\n\n update(args) {\n super.update(args);\n this.childrenContainer &&\n this.childrenContainer.children.forEach(child => {\n if (child.item && tacHelpers.isPartOf(child.item, this.item)) {\n child.interactive = false;\n }\n });\n }\n sortChildren() {\n this.childrenContainer.children.forEach(child => {\n if (productService.isType(child.item, 'drawer')) {\n child.zIndex = 1;\n } else if (productService.isType(child.item, 'shelf')) {\n child.zIndex = 2;\n }\n });\n this.childrenContainer.sortChildren();\n }\n\n removeBracketsWhileDragging() {\n this.childrenContainer &&\n this.childrenContainer.children.forEach(child => {\n if (child.item && tacHelpers.isPartOf(child.item, this.item)) {\n child.brackets &&\n Object.values(child.brackets).forEach(\n bracket => (bracket.visible = false)\n );\n }\n });\n }\n\n updateDependentItemsDragging(newItem, parent) {\n if (!parent.itemid && !newItem.itemid) return;\n this.removeBracketsWhileDragging();\n }\n}\n","import ProppingItem from '../ProppingItem';\nimport proppingService from '../../services/propping';\nimport { getProppingBounds } from '../../services/products/models';\nimport tacHelpers from '../../state/tac/tacHelpers';\n\nexport default class ElvarliProppingItem extends ProppingItem {\n getSectionSprite(section) {\n return (\n this.app.itemContainer.getSprite(section.itemid) ||\n this.app.itemContainer.children.find(\n sprite => sprite.item.itemid === section.itemid\n )\n );\n }\n\n getResourceName() {\n const { item, parentItem } = this;\n\n const proppingBounds = getProppingBounds(item);\n if (!this.shouldShowPropping(proppingBounds)) return item.id;\n\n const proppingItem = this.item;\n const section = tacHelpers.getTopAncestor(this.app.tac, parentItem);\n\n const sectionSprite = this.getSectionSprite(section);\n\n const pickedPropping = proppingService.pickPropping({\n isDynamic: true,\n tac: this.app.itemContainer ? this.app.itemContainer.tac : this.app.tac,\n item: proppingItem,\n parentItem,\n section: sectionSprite,\n });\n\n const pickedProppingBound = proppingBounds[pickedPropping - 1];\n const oldProppingBound = item.propping\n ? proppingBounds[item.propping - 1]\n : null;\n const pickedProppingHasOldHeight =\n pickedProppingBound.size.height === oldProppingBound?.size.height;\n\n /* If the newly picked propping has the same height as before, we want to\n keep the old one, so that the propping won't cycle through randomly\n chosen variants of the same size when moving things on the scene. */\n if (!pickedProppingHasOldHeight) item.propping = pickedPropping;\n\n const resourceName = `${item.id}_propping_${item.propping}`;\n\n return resourceName;\n }\n}\n","import bracketsMixin from './bracketsMixin';\nimport ProppingItem from './ElvarliProppingItem';\n\nexport default class ElvarliShelfClothesRail extends bracketsMixin(\n ProppingItem\n) {}\n","import ElvarliProppingItem from './ElvarliProppingItem';\n\nexport default class ElvarliClothesRail extends ElvarliProppingItem {}\n","import Item from '../Item';\nimport BoaxelItem from '../boaxel/BoaxelItem';\nimport BrorSection from '../bror/Section';\nimport BoaxelSection from '../boaxel/Section';\nimport AurdalSection from '../aurdal/Section';\nimport IvarSection from '../ivar/Section';\nimport ElvarliSection from '../elvarli/Section';\nimport Shelf from '../Shelf';\nimport Legs from '../jonaxel/Legs';\nimport Frame from '../jonaxel/Frame';\nimport TopShelf from '../jonaxel/TopShelf';\nimport ShelvingUnit from '../jonaxel/ShelvingUnit';\nimport JonaxelClothesRail from '../jonaxel/ClothesRail';\nimport BoaxelClothesRail from '../boaxel/ClothesRail';\nimport AurdalClothesRail from '../aurdal/ClothesRail';\nimport Cover from '../jonaxel/Cover';\nimport Upright from '../boaxel/Upright';\nimport BoaxelMountingRail from '../boaxel/MountingRail';\nimport AurdalMountingRail from '../aurdal/MountingRail';\nimport { applicationSettings } from '../../settings/application';\nimport ProppingItem from '../ProppingItem';\nimport BoaxelProppingItem from '../boaxel/BoaxelProppingItem';\nimport AdjustableSection from '../boaxel/AdjustableSection';\nimport Sidewall from '../aurdal/Sidewall';\nimport BoaxelShelf from '../boaxel/BoaxelShelf';\nimport BoaxelTable from '../boaxel/Table';\nimport IvarShelf from '../ivar/IvarShelf';\nimport IvarDrawer from '../ivar/Drawer';\nimport IvarDoors from '../ivar/Doors';\nimport ElvarliShelf from '../elvarli/ElvarliShelf';\nimport ElvarliDrawer from '../elvarli/ElvarliDrawer';\nimport ElvarliShelfDrawer from '../elvarli/ElvarliShelfDrawer';\nimport ElvarliShelfClothesRail from '../elvarli/ElvarliShelfClothesRail';\nimport ElvarliClothesRail from '../elvarli/ElvarliClothesRail';\nimport { ITEMS } from '../../constants';\n\n/**\n * Gets a new instance of the correct PIXI class for an item\n * @param {*} args\n * @returns {Object}\n */\nexport default function getComponent(args) {\n switch (args.item.type) {\n case ITEMS.DOOR:\n switch (applicationSettings.applicationName) {\n case 'IVAR':\n return new IvarDoors(args);\n default:\n return new Item(args);\n }\n case ITEMS.SECTION:\n switch (applicationSettings.applicationName) {\n case 'BROR':\n return new BrorSection(args);\n case 'BOAXEL':\n if (args.item.width >= 600) {\n return new BoaxelSection(args);\n }\n return new AdjustableSection(args);\n case 'AURDAL':\n return new AurdalSection(args);\n case 'IVAR':\n return new IvarSection(args);\n case 'ELVARLI':\n return new ElvarliSection(args);\n default:\n return new Item(args);\n }\n case ITEMS.SHELF:\n case ITEMS.METAL_SHELF:\n case ITEMS.WIRE_SHELF:\n case ITEMS.FELT_SHELF:\n switch (applicationSettings.applicationName) {\n case 'BOAXEL':\n return new BoaxelShelf(args);\n case 'IVAR':\n return new IvarShelf(args);\n case 'ELVARLI':\n return new ElvarliShelf(args);\n default:\n return new Shelf(args);\n }\n case ITEMS.SHELF_DRAWER:\n switch (applicationSettings.applicationName) {\n case 'IVAR':\n return new IvarDrawer(args);\n case 'ELVARLI':\n return new ElvarliShelfDrawer(args);\n default:\n return new Item(args);\n }\n case ITEMS.FRAME:\n return new Frame(args);\n case ITEMS.SHELVING_UNIT:\n return new ShelvingUnit(args);\n case 'leg':\n return new Legs(args);\n case 'cover':\n return new Cover(args);\n case ITEMS.TOP_SHELF:\n return new TopShelf(args);\n case ITEMS.CLOTHES_RAIL:\n switch (applicationSettings.applicationName) {\n case 'JONAXEL':\n return new JonaxelClothesRail(args);\n case 'BOAXEL':\n return new BoaxelClothesRail(args);\n case 'AURDAL':\n return new AurdalClothesRail(args);\n case 'ELVARLI':\n return new ElvarliClothesRail(args);\n default:\n return new Item(args);\n }\n case ITEMS.SHELF_CLOTHES_RAIL:\n switch (applicationSettings.applicationName) {\n case 'ELVARLI':\n return new ElvarliShelfClothesRail(args);\n default:\n return new Item(args);\n }\n case ITEMS.UPRIGHT:\n return new Upright(args);\n case ITEMS.DRYING_RACK:\n case ITEMS.TROUSER_HANGER:\n case ITEMS.SHOE_SHELF:\n case ITEMS.BOTTLE_RACK:\n switch (applicationSettings.applicationName) {\n case 'BOAXEL':\n return new BoaxelProppingItem(args);\n default:\n return new ProppingItem(args);\n }\n case ITEMS.DRAWER: {\n switch (applicationSettings.applicationName) {\n case 'ELVARLI':\n return new ElvarliDrawer(args);\n default:\n return new Item(args);\n }\n }\n case 'mounting-rail':\n switch (applicationSettings.applicationName) {\n case 'BOAXEL':\n return new BoaxelMountingRail(args);\n case 'AURDAL': {\n return new AurdalMountingRail(args);\n }\n default:\n return new Item(args);\n }\n case 'sidewall':\n return new Sidewall(args);\n case ITEMS.TABLE:\n switch (applicationSettings.applicationName) {\n case 'BOAXEL':\n return new BoaxelTable(args);\n default:\n return new Item(args);\n }\n default:\n switch (applicationSettings.applicationName) {\n case 'BOAXEL':\n return new BoaxelItem(args);\n default:\n return new Item(args);\n }\n }\n}\n","/*\nThis is just a rectangle that can be drawn for debug purposes.\nIt sets up a flat object for Base.drawOutline to work with\n*/\nimport Base from './Base';\n\nexport default class Rect extends Base {\n constructor(args) {\n super(args);\n this.item = { ...args.item, z: 0, depth: 1 };\n this.ratio = args.ratio;\n this.x = this.item.x;\n this.y = this.item.y;\n\n this.calculateProjection();\n }\n}\n","import * as PIXI from 'pixi.js-legacy';\n\nimport Base from './Base';\nimport constants from '../settings/constants';\nimport {\n selectSceneMargins,\n selectSceneRect,\n} from '../state/scene/sceneSelectors';\nimport store from '../state';\nimport { selectUserAgent } from '../state/userAgent/userAgentSelectors';\n\nexport default class Room extends Base {\n update() {\n const sceneRect = selectSceneRect(store.getState());\n const margins = selectSceneMargins(store.getState());\n const userAgent = selectUserAgent(store.getState());\n if (sceneRect) {\n this.x = sceneRect.x;\n this.y = sceneRect.y;\n this.targetWidth = sceneRect.width;\n this.targetHeight = sceneRect.height;\n }\n\n this.leftDepth =\n Math.cos(constants.OBLIQUE_ANGLE) * constants.ROOM_DEPTH * 0.29;\n this.bottomDepth =\n Math.sin(constants.OBLIQUE_ANGLE) * constants.ROOM_DEPTH * 0.29;\n\n this.margins = margins;\n this.isMobile = userAgent && userAgent.isMobile;\n this.isPortrait = userAgent && userAgent.isPortrait;\n }\n\n draw() {\n this.drawWall();\n this.drawSkirt();\n this.drawFloor();\n }\n\n drawSkirt() {\n const width = this.getSkirtWidth();\n\n this.skirt = this.skirt || new PIXI.Graphics();\n\n const { skirt } = this;\n\n skirt.clear();\n skirt.beginFill(constants.SKIRT_COLOR);\n skirt.drawRect(0, 0, width, this.getSkirtHeight());\n skirt.endFill();\n skirt.lineStyle(\n constants.SKIRT_EDGE_THICKNESS * this.ratio,\n constants.SKIRT_EDGE_COLOR\n );\n skirt.moveTo(0, 0);\n skirt.lineTo(width, 0);\n skirt.y = this.getSkirtY();\n skirt.x = this.getSkirtX();\n\n !skirt.parent && this.addChild(skirt);\n }\n\n getSkirtWidth() {\n return this.currentWall.width;\n }\n\n getSkirtHeight() {\n return constants.SKIRT_HEIGHT * this.ratio;\n }\n\n getSkirtY() {\n return (\n this.targetHeight - this.floorHeight - constants.SKIRT_HEIGHT * this.ratio\n );\n }\n\n getSkirtX() {\n return 0;\n }\n\n getDefaultWallPoints() {\n return [\n { x: 0, y: 0 },\n { x: this.targetWidth, y: 0 },\n { x: this.targetWidth, y: this.targetHeight },\n { x: 0, y: this.targetHeight },\n ];\n }\n\n getMaxWallPoints() {\n return [\n { x: 0, y: 0 },\n { x: constants.WALL.width.max * this.ratio, y: 0 },\n {\n x: constants.WALL.width.max * this.ratio,\n y: constants.WALL.height.max * this.ratio,\n },\n { x: 0, y: constants.WALL.height.max * this.ratio },\n ];\n }\n\n createGradientCanvas({ angle, lightColor, darkColor }) {\n const width = this.targetWidth;\n const height = this.targetHeight;\n\n const canvas = document.createElement('canvas');\n canvas.width = width;\n canvas.height = height;\n\n const ctx = canvas.getContext('2d');\n\n const move = (Math.tan((angle / 180) * Math.PI) * height) / 2;\n\n const grd = ctx.createLinearGradient(\n width / 2 - move,\n 0,\n width / 2 + move,\n height\n );\n grd.addColorStop(0, PIXI.utils.hex2string(lightColor));\n grd.addColorStop(1, PIXI.utils.hex2string(darkColor));\n\n ctx.fillStyle = grd;\n ctx.fillRect(0, 0, width, height);\n\n return canvas;\n }\n}\n","import * as PIXI from 'pixi.js-legacy';\n\nimport Room from './Room';\nimport tacHelpers from '../state/tac/tacHelpers';\nimport geometry from './util/geometry';\nimport {\n selectMinRoom,\n selectSceneMargins,\n} from '../state/scene/sceneSelectors';\nimport store from '../state';\nimport { selectTac } from '../state/tac/tacSelectors';\nimport constants from '../settings/constants';\n\nexport default class DynamicRoom extends Room {\n update(args) {\n super.update(args);\n const minRoom = selectMinRoom(store.getState());\n const margins = selectSceneMargins(store.getState());\n const tac = selectTac(store.getState());\n\n // Margins cannot be bigger than the scene div\n margins.right = margins.right < this.targetWidth ? margins.right : 0;\n margins.top = margins.top < this.targetHeight ? margins.top : 0;\n\n const elementRatio =\n (this.targetWidth - margins.right) / (this.targetHeight - margins.top);\n this.model = tacHelpers.getRoom(tac, elementRatio, minRoom);\n\n this.ratio =\n (this.targetWidth - margins.right) / (this.model.width + this.leftDepth);\n\n this.floorHeight = this.bottomDepth * this.ratio;\n\n this.currentWall = this.getWallPxObject();\n\n this.draw({});\n }\n\n getSpace(tac) {\n return tacHelpers.getInitialSpace(tac);\n }\n\n /**\n * @returns {object[]} [{x, y}] in px\n */\n getWallPoints() {\n return this.getDefaultWallPoints();\n }\n\n /**\n * @param {number} wallHeight wall height in px\n * @param {number} wallWidth wall width in px\n */\n getWallPosition({ wallHeight, wallWidth }) {\n return {\n y: 0,\n x: 0,\n };\n }\n\n getWallPxObject() {\n const points = this.getWallPoints();\n const { width, height } = geometry.surround(points);\n const position = this.getWallPosition({\n wallWidth: width,\n wallHeight: height,\n });\n\n return { points, width, height, position };\n }\n\n fillWall(ctx, points) {\n ctx.fillRect(0, 0, this.targetWidth, this.targetHeight);\n }\n\n drawWall() {\n this.gradientBaseTexture =\n this.gradientBaseTexture ||\n new PIXI.BaseTexture(\n this.createGradientCanvas({\n angle: constants.WALL_GRADIENT_ANGLE,\n lightColor: constants.GRADIENT_LIGHT_COLOR,\n darkColor: constants.GRADIENT_DARK_COLOR,\n })\n );\n\n this.wallTexture =\n this.wallTexture || new PIXI.Texture(this.gradientBaseTexture);\n this.wallSprite = this.wallSprite || new PIXI.Sprite(this.wallTexture);\n this.wallMask = this.wallMask || new PIXI.Graphics();\n this.wall = this.wall || new PIXI.Container();\n\n const { currentWall, gradientBaseTexture, wall, wallSprite } = this;\n\n gradientBaseTexture.setSize(currentWall.width, currentWall.height);\n wallSprite.position.set(currentWall.position.x, currentWall.position.y);\n\n wall.x = currentWall.position.x;\n wall.y = currentWall.position.y;\n\n !wall.parent && this.addChild(wall);\n !wallSprite.parent && this.addChild(wallSprite);\n }\n\n drawFloor(bottomPadding = 0) {\n const width = this.currentWall.width;\n const texture = PIXI.Loader.shared.resources.floor.texture;\n this.floor = this.floor || new PIXI.Sprite(texture);\n\n const { floor } = this;\n\n floor.width = width;\n floor.height = this.floorHeight;\n floor.x = (this.targetWidth - this.currentWall.width) / 2;\n floor.y = this.targetHeight - floor.height - bottomPadding;\n\n if (!floor.parent) {\n this.addChild(floor);\n }\n }\n}\n","import _ from 'lodash';\n\nimport store from '../../state';\nimport { setDraggingWallResizer } from '../../state/scene/sceneActions.ts';\nimport constants from '../../settings/constants';\nimport { ceil } from '../../util/round';\nimport { actionSetDirtyConfiguration } from '../../state/vpc/vpcActions';\nimport { thunkSetWall } from '../../state/tac/tacThunks';\n\nexport default class DraggableWallResizer {\n static draggingWallResizer = false;\n\n constructor({\n moveableAxes,\n ratio,\n canvasContainer,\n canvasHeight,\n prevWallSize,\n itemContainerLimits,\n wallResizerOffset,\n }) {\n this.itemContainerLimits = itemContainerLimits;\n this.prevWallSize = prevWallSize;\n this.canvasHeight = canvasHeight;\n this.canvasContainer = canvasContainer;\n this.moveableAxes = moveableAxes;\n this.wallResizerOffset = wallResizerOffset;\n this.ratio = ratio;\n this.dragging = false;\n this.draggingPosition = null;\n this.prevEmittedWallSize = null;\n\n this.onPointerDown = this.onPointerDown.bind(this);\n this.onPointerUp = this.onPointerUp.bind(this);\n this.onPointerMove = this.onPointerMove.bind(this);\n }\n\n update({ prevWallSize }) {\n this.prevWallSize = prevWallSize;\n if (DraggableWallResizer.draggingWallResizer && !this.dragging) {\n this.getDisplayObject().cursor = null;\n } else if (this.dragging) {\n this.getDisplayObject().cursor = 'grabbing';\n } else {\n this.getDisplayObject().cursor = this.getHoverCursor();\n }\n }\n\n onPointerDown(e) {\n this.pointerOffset = {\n x: e.data.global.x - this.getPosition().x,\n y: e.data.global.y - this.getPosition().y,\n };\n DraggableWallResizer.draggingWallResizer = true;\n store.dispatch(\n setDraggingWallResizer(DraggableWallResizer.draggingWallResizer)\n );\n this.draggingPosition = e.data.global;\n this.dragging = true;\n document.body.style.cursor = 'grabbing';\n this.getDisplayObject().cursor = 'grabbing';\n }\n\n onPointerUp() {\n if (this.dragging) {\n DraggableWallResizer.draggingWallResizer = false;\n store.dispatch(\n setDraggingWallResizer(DraggableWallResizer.draggingWallResizer)\n );\n this.draggingPosition.x -= this.pointerOffset.x;\n this.draggingPosition.y -= this.pointerOffset.y;\n store.dispatch(\n thunkSetWall(this._getWallMeasurement(this.draggingPosition), {\n origin: 'drag',\n isPersistent: true,\n })\n );\n this.dragging = false;\n store.dispatch(actionSetDirtyConfiguration(true));\n }\n document.body.style.cursor = '';\n this.getDisplayObject().cursor = this.getHoverCursor();\n }\n\n onPointerMove(e) {\n if (e.data.originalEvent && e.data.originalEvent.cancelable) {\n e.data.originalEvent.preventDefault();\n }\n\n if (this.dragging) {\n this.draggingPosition = e.data.global;\n\n this.draggingPosition.x -= this.pointerOffset.x;\n this.draggingPosition.y -= this.pointerOffset.y;\n\n const wallSize = this._getWallMeasurement(this.draggingPosition);\n\n if (\n !this.prevEmittedWallSize ||\n Math.abs(wallSize.width - this.prevEmittedWallSize.width) >= 10 ||\n Math.abs(wallSize.height - this.prevEmittedWallSize.height) >= 10\n ) {\n store.dispatch(\n thunkSetWall(this._getWallMeasurement(this.draggingPosition), {\n shouldReport: false,\n })\n );\n this.prevEmittedWallSize = wallSize;\n }\n }\n }\n\n /**\n * @param {number} position.x Drag dot x coordinate in px.\n * @param {number} position.y Drag dot y coordinate in px.\n *\n * @returns {object} {width, height} in mm.\n */\n _getWallMeasurement(position) {\n const width = ceil(\n this.moveableAxes.x\n ? parseInt((position.x - this.wallResizerOffset.x) / this.ratio)\n : this.prevWallSize.width / this.ratio,\n 10\n );\n\n const height = ceil(\n this.moveableAxes.y\n ? parseInt(\n (this.canvasHeight -\n this.wallResizerOffset.y -\n (position.y - this.canvasContainer.top)) /\n this.ratio\n )\n : this.prevWallSize.height / this.ratio,\n 10\n );\n\n return {\n width: _.clamp(\n width,\n Math.max(\n constants.WALL.width.min,\n this.itemContainerLimits ? this.itemContainerLimits.max.x : 0\n ),\n this.itemContainerLimits?.blockedX\n ? this.itemContainerLimits.max.x\n : constants.WALL.width.max\n ),\n height: _.clamp(\n height,\n Math.max(\n constants.WALL.height.min,\n this.itemContainerLimits ? this.itemContainerLimits.max.y : 0\n ),\n constants.WALL.height.max\n ),\n };\n }\n}\n","import * as PIXI from 'pixi.js';\nimport DraggableWallResizer from './DraggableWallResizer';\n\nconst DIAMETER = 16;\nconst RADIUS = DIAMETER / 2;\nconst HIT_AREA_RADIUS = RADIUS * 4;\n\nexport default class DragDot extends DraggableWallResizer {\n constructor({\n x,\n y,\n moveableAxes,\n ratio,\n canvasContainer,\n canvasHeight,\n prevWallSize,\n itemContainerLimits,\n wallResizerOffset,\n }) {\n super({\n moveableAxes,\n ratio,\n canvasContainer,\n canvasHeight,\n prevWallSize,\n itemContainerLimits,\n wallResizerOffset,\n });\n\n this.dragging = false;\n this.draggingPosition = null;\n this.prevEmittedWallSize = null;\n this.pointerOffset = { x: 0, y: 0 };\n\n const resource = PIXI.Loader.shared.resources['drag_dot'];\n this.texture = resource.texture;\n\n this.sprite && this.sprite.destroy();\n this.container && this.container.destroy();\n\n this.sprite = new PIXI.Sprite(this.texture);\n this.sprite.x = 0;\n this.sprite.y = 0;\n this.sprite.width = DIAMETER;\n this.sprite.height = DIAMETER;\n this.sprite.anchor.set(0.5, 0.5);\n\n this.container = new PIXI.Container();\n this.container.cursor = this.getHoverCursor();\n this.container.interactive = true;\n this.container.x = x;\n this.container.y = y;\n\n this.container.hitArea = new PIXI.Circle(0, 0, HIT_AREA_RADIUS);\n\n this.container.on('pointerup', this.onPointerUp);\n this.container.on('pointerupoutside', this.onPointerUp);\n this.container.on('pointermove', this.onPointerMove);\n this.container.on('pointerdown', this.onPointerDown);\n\n this.container.addChild(this.sprite);\n }\n\n update({ x, y, prevWallSize }) {\n super.update({ prevWallSize });\n this.container.x = x;\n this.container.y = y;\n }\n\n getHoverCursor() {\n return 'grab';\n }\n\n getPosition() {\n return { x: this.container.x, y: this.container.y };\n }\n\n getDisplayObject() {\n return this.container;\n }\n\n destroy() {\n this.container.off('pointermove');\n this.container.off('pointerdown');\n this.container.off('pointerup');\n this.container.off('pointerupoutside');\n\n this.sprite && this.sprite.destroy();\n this.container && this.container.destroy();\n }\n}\n","import DraggableWallResizer from './DraggableWallResizer';\nimport * as PIXI from 'pixi.js';\nimport { isMobile } from '../../util/userAgent';\n\nconst WIDTH = 2;\nconst COLOR = 0x0058a3;\nconst HIT_AREA_SCALE = 25;\n\nexport default class DragLine extends DraggableWallResizer {\n constructor(args) {\n super(args);\n\n this.dragLine = null;\n this.points = args.points;\n\n this.dragLine = new PIXI.Graphics();\n this.dragLine.cursor = this.getHoverCursor();\n this.dragLine.lineStyle(WIDTH, COLOR);\n this.x = args.points[0].x;\n this.y = args.points[0].y;\n this.dragLine.moveTo(args.points[0].x, args.points[0].y);\n this.dragLine.lineTo(args.points[1].x, args.points[1].y);\n\n this.dragLine.hitArea = this.getHitArea();\n\n this.dragLine.on('pointerup', this.onPointerUp);\n this.dragLine.on('pointerupoutside', this.onPointerUp);\n this.dragLine.on('pointermove', this.onPointerMove);\n this.dragLine.on('pointerdown', this.onPointerDown);\n\n this.dragLine.interactive =\n !isMobile() && (this.moveableAxes.x || this.moveableAxes.y);\n }\n\n update({ points, prevWallSize }) {\n super.update({ prevWallSize });\n this.dragLine.clear();\n\n this.dragLine.lineStyle(WIDTH, COLOR);\n this.x = points[0].x;\n this.y = points[0].y;\n this.dragLine.moveTo(points[0].x, points[0].y);\n this.dragLine.lineTo(points[1].x, points[1].y);\n\n this.dragLine.hitArea = this.getHitArea();\n }\n\n getHoverCursor() {\n return this.moveableAxes.y ? 'ns-resize' : 'ew-resize';\n }\n\n getHitArea() {\n const hitArea = this.dragLine.getBounds();\n if (this.moveableAxes.x) {\n hitArea.width *= HIT_AREA_SCALE;\n hitArea.x -= hitArea.width / 2;\n } else if (this.moveableAxes.y) {\n hitArea.height *= HIT_AREA_SCALE;\n hitArea.y -= hitArea.height / 2;\n }\n return hitArea;\n }\n\n getPosition() {\n return { x: this.x, y: this.y };\n }\n\n getDisplayObject() {\n return this.dragLine;\n }\n\n destroy() {\n this.dragLine.off('pointermove');\n this.dragLine.off('pointerdown');\n this.dragLine.off('pointerup');\n this.dragLine.off('pointerupoutside');\n\n this.dragLine && this.dragLine.destroy();\n }\n}\n","import * as PIXI from 'pixi.js-legacy';\nimport Room from './Room';\nimport constants from '../settings/constants';\nimport geometry from './util/geometry';\nimport emitter from '../emitter';\nimport { WALL_RESIZED } from '../settings/events';\nimport tacHelpers from '../state/tac/tacHelpers';\nimport DragDot from './boaxel/DragDot';\nimport DragLine from './boaxel/DragLine';\nimport store from '../state';\nimport { round } from '../util/round';\nimport { actionSetWallResizerInactive } from '../state/scene';\nimport { selectUserAgent } from '../state/userAgent/userAgentSelectors';\nimport { selectIsWallResizerActive } from '../state/scene/sceneSelectors';\nimport { selectTac } from '../state/tac/tacSelectors';\n\nexport default class FixedRoom extends Room {\n constructor(view, renderScene, stage) {\n super();\n\n this.stage = stage;\n this.renderScene = renderScene;\n\n this.dragDots = null;\n this.dragLines = null;\n\n this.view = view;\n }\n\n update(args) {\n super.update(args);\n const tac = selectTac(store.getState());\n const userAgent = selectUserAgent(store.getState());\n this.wallResizerActive = selectIsWallResizerActive(store.getState());\n this.model = this.getSpace(tac);\n\n this.ratio = this.getRatio(userAgent);\n\n this.floorHeight = this.slopeHeight = this.bottomDepth * this.ratio;\n this.slopeWidth = this.leftDepth * this.ratio;\n\n const wallPoints = tac?.wall?.points || constants.WALL.points;\n\n this.currentWall = this.getWallPxObject(wallPoints);\n this.maxWall = this.getWallPxObject();\n\n const itemContainerLimits = tac && tacHelpers.getWallResizingLimits(tac);\n\n this.draw({\n wallPoints,\n isMobile: this.isMobile,\n itemContainerLimits,\n });\n }\n\n getWallPxObject(pointsInMm) {\n const points = pointsInMm\n ? this.getWallPoints({ wallPoints: pointsInMm })\n : this.getMaxWallPoints();\n const { width, height } = geometry.surround(points);\n const position = this.getWallPosition({\n wallWidth: width,\n wallHeight: height,\n });\n\n return { points, width, height, position };\n }\n\n getSpace(tac) {\n return this.wallResizerActive\n ? tacHelpers.getMaxSpace()\n : tacHelpers.getCurrentSpace(tac);\n }\n\n getRatio(userAgent) {\n const { margins, wallResizerActive } = this;\n\n const horizontalRatio =\n this.targetWidth /\n (margins.right +\n (wallResizerActive\n ? constants.WALL.width.max + this.leftDepth\n : this.model.width + this.leftDepth));\n\n const verticalRatio =\n this.targetHeight /\n (this.margins.top +\n (wallResizerActive\n ? constants.WALL.height.max + this.bottomDepth\n : this.model.height + this.bottomDepth));\n\n return Math.min(horizontalRatio, verticalRatio);\n }\n\n getSkirtWidth() {\n return this.currentWall.width;\n }\n\n getSkirtY() {\n return super.getSkirtY();\n }\n\n getDefaultWallPoints() {\n if (constants.WALL) {\n return constants.WALL.points.map(point => ({\n x: point.x * this.ratio,\n y: point.y * this.ratio,\n }));\n }\n return super.getDefaultWallPoints();\n }\n\n /**\n * @param {object} args.wallPoints [{x, y}] in mm\n * @returns {object[]} [{x, y}] in px\n */\n getWallPoints({ wallPoints }) {\n if (this.wallResizerActive) {\n return this.getMaxWallPoints();\n } else if (wallPoints) {\n return wallPoints.map(point => ({\n x: (point.x > 0 ? point.x + this.margins.right : point.x) * this.ratio,\n y: (point.y > 0 ? point.y + this.margins.top : point.y) * this.ratio,\n }));\n } else {\n return this.getDefaultWallPoints();\n }\n }\n\n /**\n * @param {number} wallHeight wall height in px\n * @param {number} wallWidth wall width in px\n */\n getWallPosition({ wallHeight, wallWidth }) {\n return {\n y: round(this.targetHeight - wallHeight - this.floorHeight, 1),\n x: round(\n (this.targetWidth - wallWidth + this.leftDepth * this.ratio) / 2,\n 1\n ),\n };\n }\n\n getWallResizerPosition(points, canvasContainerClientRect) {\n const { height } = geometry.surround(points);\n\n return {\n y:\n this.targetHeight -\n this.floorHeight -\n height +\n canvasContainerClientRect.top,\n x: this.maxWall.position.x + canvasContainerClientRect.left,\n };\n }\n\n draw(args) {\n super.draw(args);\n\n if (this.wallResizerActive) {\n this.drawSideWall();\n this.drawWallResizer(args.wallPoints, args.itemContainerLimits);\n } else {\n this.drawSideWall();\n this.dragDots && this.dragDots.forEach(dot => dot.destroy());\n this.dragDots = null;\n this.dragLines && this.dragLines.forEach(line => line.destroy());\n this.dragLines = null;\n this.backgroundSprite && this.backgroundSprite.destroy();\n this.backgroundSprite = null;\n }\n }\n\n getSkirtX() {\n return this.currentWall.position.x;\n }\n\n drawFloor() {\n const texture = PIXI.Loader.shared.resources.floor.texture;\n this.floor = this.floor || new PIXI.Sprite(texture);\n this.floorMask = this.floorMask || new PIXI.Graphics();\n\n const width = this.currentWall.width + this.slopeWidth;\n\n this.floor.width = width;\n this.floor.height = this.floorHeight;\n this.floor.x = this.currentWall.position.x - this.slopeWidth;\n this.floor.y = this.targetHeight - this.floor.height;\n this.floor.visible = true;\n\n this.floorMask.clear();\n this.floorMask.beginFill();\n const maskPoints = [\n new PIXI.Point(this.floor.x, this.floor.y + this.floorHeight),\n new PIXI.Point(this.floor.x + this.slopeWidth, this.floor.y),\n new PIXI.Point(this.floor.x + this.floor.width, this.floor.y),\n new PIXI.Point(\n this.floor.x + this.floor.width,\n this.floor.y + this.floorHeight\n ),\n new PIXI.Point(this.floor.x, this.floor.y + this.floorHeight),\n ];\n this.floorMask.drawPolygon(maskPoints);\n this.floorMask.endFill();\n\n this.addChild(this.floorMask);\n this.floor.mask = this.floorMask;\n\n this.addChild(this.floor);\n }\n\n drawSideWall() {\n // like in the flat wall we add lighting with a gradient, but change the angle\n this.sideWallBaseTexture =\n this.sideWallBaseTexture ||\n new PIXI.BaseTexture(\n this.createGradientCanvas({\n angle: constants.SIDEWALL_GRADIENT_ANGLE,\n lightColor: constants.SIDEWALL_GRADIENT_LIGHT_COLOR,\n darkColor: constants.SIDEWALL_GRADIENT_DARK_COLOR,\n })\n );\n\n this.sideWallTexture =\n this.sideWallTexture || new PIXI.Texture(this.sideWallBaseTexture);\n this.sideWallSprite =\n this.sideWallSprite || new PIXI.Sprite(this.sideWallTexture);\n this.sideWallMask = this.sideWallMask || new PIXI.Graphics();\n this.sideWallSkirt = this.sideWallSkirt || new PIXI.Graphics();\n this.sideWallSkirtEdge = this.sideWallSkirtEdge || new PIXI.Graphics();\n this.sideWallContainer = this.sideWallContainer || new PIXI.Container();\n\n const {\n currentWall,\n maxWall,\n sideWallContainer,\n sideWallBaseTexture,\n sideWallMask,\n sideWallSkirt,\n sideWallSkirtEdge,\n sideWallSprite,\n slopeHeight,\n slopeWidth,\n } = this;\n\n sideWallContainer.visible = true;\n\n sideWallBaseTexture.setSize(slopeWidth, maxWall.height + slopeHeight);\n\n sideWallSprite.position.set(\n currentWall.position.x - slopeWidth,\n maxWall.position.y\n );\n\n const maskPoints = [\n {\n x: 0,\n y: 0,\n },\n {\n x: slopeWidth,\n y: 0,\n },\n {\n x: slopeWidth,\n y: currentWall.height,\n },\n {\n x: 0,\n y: currentWall.height + slopeHeight,\n },\n {\n x: 0,\n y: 0,\n },\n ].map(point => {\n return new PIXI.Point(\n point.x + currentWall.position.x - slopeWidth,\n Math.max(point.y + currentWall.position.y, 0)\n );\n });\n\n sideWallMask.clear().beginFill().drawPolygon(maskPoints).endFill();\n sideWallSprite.mask = sideWallMask;\n\n // the skirting\n const skirtHeight = this.getSkirtHeight();\n const skirtPoints = [\n { x: 0, y: currentWall.height + slopeHeight - skirtHeight },\n { x: 0, y: currentWall.height + slopeHeight },\n { x: slopeWidth, y: currentWall.height },\n { x: slopeWidth, y: currentWall.height - skirtHeight },\n { x: 0, y: currentWall.height + slopeHeight - skirtHeight },\n ].map(point => {\n return new PIXI.Point(\n point.x + currentWall.position.x - slopeWidth,\n point.y + currentWall.position.y\n );\n });\n\n sideWallSkirt\n .clear()\n .beginFill(constants.SIDEWALL_SKIRT_COLOR)\n .drawPolygon(skirtPoints)\n .endFill();\n\n sideWallSkirtEdge\n .clear()\n .lineStyle(\n constants.SKIRT_EDGE_THICKNESS * this.ratio,\n constants.SIDEWALL_SKIRT_EDGE_COLOR\n )\n .moveTo(skirtPoints[1].x, skirtPoints[1].y - skirtHeight)\n .lineTo(skirtPoints[2].x, skirtPoints[2].y - skirtHeight);\n\n if (!sideWallContainer.parent) {\n sideWallContainer.addChild(\n sideWallMask,\n sideWallSprite,\n sideWallSkirt,\n sideWallSkirtEdge\n );\n this.addChild(sideWallContainer);\n }\n }\n\n drawWall() {\n this.gradientBaseTexture =\n this.gradientBaseTexture ||\n new PIXI.BaseTexture(\n this.createGradientCanvas({\n angle: constants.WALL_GRADIENT_ANGLE,\n lightColor: constants.GRADIENT_LIGHT_COLOR,\n darkColor: constants.GRADIENT_DARK_COLOR,\n })\n );\n\n this.wallTexture =\n this.wallTexture || new PIXI.Texture(this.gradientBaseTexture);\n this.wallSprite = this.wallSprite || new PIXI.Sprite(this.wallTexture);\n this.wallMask = this.wallMask || new PIXI.Graphics();\n this.wall = this.wall || new PIXI.Container();\n\n const {\n currentWall,\n floorHeight,\n gradientBaseTexture,\n maxWall,\n targetHeight,\n targetWidth,\n wall,\n wallMask,\n wallSprite,\n } = this;\n\n gradientBaseTexture.setSize(maxWall.width, maxWall.height);\n wallSprite.position.set(maxWall.position.x, maxWall.position.y);\n\n wallMask\n .clear()\n .beginFill()\n .drawRect(\n Math.max(currentWall.position.x, 0),\n Math.max(currentWall.position.y, 0),\n Math.min(currentWall.width, targetWidth - currentWall.position.x),\n Math.min(\n currentWall.height,\n this.wallResizerActive ? targetHeight : targetHeight - floorHeight\n )\n )\n .endFill();\n\n wallSprite.mask = wallMask;\n\n wall.x = currentWall.position.x;\n wall.y = currentWall.position.y;\n\n !wall.parent && this.addChild(wall);\n !wallMask.parent && this.addChild(wallMask);\n !wallSprite.parent && this.addChild(wallSprite);\n }\n\n getWallResizerPoints(wallPoints) {\n if (wallPoints) {\n return wallPoints.map(point => ({\n x: point.x * this.ratio,\n y: point.y * this.ratio,\n }));\n }\n\n return this.getDefaultWallPoints();\n }\n\n drawBackground(wallResizerWorldPoints) {\n // Find if an backgroundClickArea already exists\n // so we dont create multiple backgroundClickAreas\n // (draw() runs on every drag)\n const backgroundClickArea = this.stage.children.find(\n child => child.type && child.type === 'backgroundClickArea'\n );\n\n if (this.isMobile && this.isPortrait) {\n if (this.backgroundSprite) {\n this.backgroundSprite.pointerdown = null;\n }\n return;\n }\n\n if (!backgroundClickArea) {\n // Create the background area that will detect the click\n const canvas = this.view.getBoundingClientRect();\n const background = new PIXI.Graphics();\n background.drawRect(0, 0, canvas.width, canvas.height);\n const backgroundTexture = background.generateCanvasTexture();\n backgroundTexture.interactive = true;\n // For a PIXI element to be able to be interactive it can't be a Graphics\n this.backgroundSprite = new PIXI.Sprite(backgroundTexture);\n this.backgroundSprite.interactive = true;\n this.backgroundSprite.type = 'backgroundClickArea';\n\n this.stage.addChild(this.backgroundSprite);\n }\n\n this.backgroundSprite.pointerdown = e => {\n const polygon = [\n ...wallResizerWorldPoints.map(point => [point.x, point.y]),\n ];\n const point = [e.data.global.x, e.data.global.y];\n\n if (!geometry.isInsidePolygon(polygon, point)) {\n store.dispatch(actionSetWallResizerInactive({ nonInteraction: true }));\n this.backgroundSprite && this.backgroundSprite.destroy();\n this.backgroundSprite = null;\n }\n };\n }\n\n drawWallResizer(wallPoints, itemContainerLimits) {\n const points = this.getWallResizerPoints(wallPoints);\n\n const canvasContainerClientRect =\n this.view.parentElement.getBoundingClientRect();\n\n const wallResizerPosition = this.getWallResizerPosition(\n points,\n canvasContainerClientRect\n );\n\n const wallResizerWorldPoints = points.map(point => ({\n x: point.x + wallResizerPosition.x,\n y: point.y + wallResizerPosition.y,\n }));\n\n this.drawBackground(wallResizerWorldPoints);\n\n this.drawDragLines(points, itemContainerLimits, canvasContainerClientRect);\n\n if (this.isMobile) {\n this.dragDots && this.dragDots.forEach(dot => dot.destroy());\n this.dragDots = null;\n } else {\n this.drawDragDots(\n points,\n itemContainerLimits,\n canvasContainerClientRect,\n wallResizerPosition\n );\n }\n this.renderScene();\n\n emitter.emit(WALL_RESIZED, { points: wallResizerWorldPoints });\n }\n\n drawDragLines(points, itemContainerLimits, canvasContainerClientRect) {\n const position = this.getWallResizerPosition(\n points,\n canvasContainerClientRect\n );\n const dragLinePointsList = this.getDragLinePoints(points);\n if (!this.dragLines) {\n this.dragLines = [];\n\n for (let i = 0; i < dragLinePointsList.length; i++) {\n const dragLine = new DragLine({\n itemContainerLimits,\n prevWallSize: geometry.surround(points),\n canvasHeight: this.targetHeight,\n canvasContainer: canvasContainerClientRect,\n ratio: this.ratio,\n moveableAxes: this.getMoveableLineAxes(dragLinePointsList, i),\n points: [\n {\n x: dragLinePointsList[i][0].x + position.x,\n y: dragLinePointsList[i][0].y + position.y,\n },\n {\n x: dragLinePointsList[i][1].x + position.x,\n y: dragLinePointsList[i][1].y + position.y,\n },\n ],\n wallResizerOffset: {\n x: position.x,\n y: this.floorHeight,\n },\n });\n\n this.dragLines.push(dragLine);\n this.stage.addChild(dragLine.getDisplayObject());\n }\n } else {\n this.dragLines.forEach((line, i) => {\n line.update({\n points: [\n {\n x: dragLinePointsList[i][0].x + position.x,\n y: dragLinePointsList[i][0].y + position.y,\n },\n {\n x: dragLinePointsList[i][1].x + position.x,\n y: dragLinePointsList[i][1].y + position.y,\n },\n ],\n prevWallSize: geometry.surround(points),\n });\n });\n }\n }\n\n getDragLinePoints(points) {\n return points.map((point, i) => [point, points[(i + 1) % points.length]]);\n }\n\n drawDragDots(\n points,\n itemContainerLimits,\n canvasContainerClientRect,\n wallResizerPosition\n ) {\n let nonDraggablePointIndex = 0;\n for (let i = 1; i < points.length; i++) {\n if (\n points[i].y >= points[nonDraggablePointIndex].y &&\n points[i].x <= points[nonDraggablePointIndex].x\n ) {\n nonDraggablePointIndex = i;\n }\n }\n\n const dragPoints = points.slice();\n dragPoints.splice(nonDraggablePointIndex, 1);\n\n if (!this.dragDots) {\n this.dragDots = [];\n for (let i = 0; i < dragPoints.length; i++) {\n const dragDot = new DragDot({\n x: dragPoints[i].x + wallResizerPosition.x,\n y: dragPoints[i].y + wallResizerPosition.y,\n ratio: this.ratio,\n canvasHeight: this.targetHeight,\n canvasContainer: canvasContainerClientRect,\n moveableAxes: this.getMoveableDotAxes(dragPoints, i),\n prevWallSize: geometry.surround(points),\n itemContainerLimits,\n wallResizerOffset: {\n x: wallResizerPosition.x,\n y: this.floorHeight,\n },\n });\n this.dragDots.push(dragDot);\n\n this.stage.addChild(dragDot.getDisplayObject());\n }\n } else {\n this.dragDots.forEach((dot, i) => {\n dot.update({\n x: dragPoints[i].x + wallResizerPosition.x,\n y: dragPoints[i].y + wallResizerPosition.y,\n prevWallSize: geometry.surround(points),\n });\n });\n }\n }\n\n getMoveableLineAxes(dragLinePointsList, index) {\n if (\n dragLinePointsList.every(\n points =>\n dragLinePointsList[index][0].x >= points[0].x &&\n dragLinePointsList[index][1].x >= points[1].x\n )\n ) {\n return {\n x: true,\n y: false,\n };\n } else if (\n dragLinePointsList.every(\n points =>\n dragLinePointsList[index][0].y <= points[0].y &&\n dragLinePointsList[index][1].y <= points[1].y\n )\n ) {\n return {\n x: false,\n y: true,\n };\n }\n return {\n x: false,\n y: false,\n };\n }\n\n getMoveableDotAxes(points, index) {\n if (points.every(point => points[index].x <= point.x)) {\n return { x: false, y: true };\n } else if (\n points.every(point => points[index].x >= point.x) &&\n points.every(point => points[index].y <= point.y)\n ) {\n return { x: true, y: true };\n } else if (\n points.every(point => points[index].x >= point.x) &&\n points.every(point => points[index].y >= point.y)\n ) {\n return { x: true, y: false };\n }\n\n throw new Error('Unknown moveable Axes');\n }\n\n fillMobileWall(ctx, wallPointsInPx, position) {\n const { width: wallWidth, height: wallHeight } =\n geometry.surround(wallPointsInPx);\n\n const startingY = position.y < 0 ? -position.y : 0;\n const points = [\n { x: 0, y: startingY },\n { x: wallWidth, y: startingY },\n { x: wallWidth, y: wallHeight },\n { x: 0, y: wallHeight },\n ];\n\n ctx.beginPath();\n for (let i = 0; i < points.length; i++) {\n ctx.lineTo(points[i].x, points[i].y);\n }\n ctx.lineTo(points[0].x, points[0].y);\n ctx.closePath();\n ctx.fill();\n }\n\n fillWall(ctx, points, position) {\n ctx.beginPath();\n for (let i = 0; i < points.length; i++) {\n ctx.lineTo(points[i].x, points[i].y);\n }\n ctx.lineTo(points[0].x, points[0].y);\n ctx.closePath();\n ctx.fill();\n }\n\n getMaxWallPoints() {\n return [\n { x: 0, y: 0 },\n {\n x: (constants.WALL.width.max + this.margins.right) * this.ratio,\n y: 0,\n },\n {\n x: (constants.WALL.width.max + this.margins.right) * this.ratio,\n y: (constants.WALL.height.max + this.margins.top) * this.ratio,\n },\n {\n x: 0,\n y: (constants.WALL.height.max + this.margins.top) * this.ratio,\n },\n ];\n }\n}\n","import * as PIXI from 'pixi.js-legacy';\nimport { metricToImperial } from '../util/measures';\nimport platform from '../util/platform';\nimport constants from '../settings/constants';\nimport { ceil } from '../util/round';\nimport { selectUseMetric } from '../state/dexfSettings/dexfSettingsSelectors';\nimport store from '../state';\n\nexport default class MeasurementLine extends PIXI.Container {\n constructor({\n ratio,\n length,\n isMobile,\n textInside,\n textUnderLine,\n skipFirstSerif,\n isVertical,\n isTable,\n isDepth,\n size,\n }) {\n super();\n\n this.ratio = ratio;\n this.length = length;\n this.isMobile = isMobile;\n this.textInside = textInside;\n this.textUnderLine = textUnderLine;\n this.skipFirstSerif = skipFirstSerif;\n this.isVertical = isVertical;\n this.isTable = isTable;\n this.isDepth = isDepth;\n this.size = size;\n this.useMetricMeasures = selectUseMetric(store.getState());\n\n this.draw();\n }\n\n getSerifPoints(serif, lineLength) {\n /*\n if the length of the line is smaller than text background\n start drawing serif from where text background ends to\n prevent ugly fading issue. Serifs showing behind text background\n */\n\n let minY = lineLength > this.background.width ? -serif / 2 : 0;\n let maxY = serif / 2;\n\n if (this.textUnderLine) {\n minY = -serif / 2;\n maxY = lineLength > this.background.width ? serif / 2 : 0;\n }\n\n if (!!this.isDepth) {\n minY = -serif / 2;\n maxY = serif / 2;\n }\n\n return [\n [\n [0, minY],\n [0, maxY],\n ],\n [\n [lineLength, minY],\n [lineLength, maxY],\n ],\n ];\n }\n\n draw() {\n const length = this.length * this.ratio;\n\n let adjustedLength;\n\n if (!!this.isDepth) {\n adjustedLength = this.useMetricMeasures\n ? ceil(this.size, 10)\n : ceil(this.size, 1);\n } else {\n adjustedLength = this.useMetricMeasures\n ? ceil(this.length, 10)\n : ceil(this.length, 1);\n }\n\n const measure = this.useMetricMeasures\n ? adjustedLength / 10\n : metricToImperial(adjustedLength).onlyInches;\n const textContent = `${measure}`;\n\n const style = new PIXI.TextStyle({\n fontSize: platform.isKiosk ? 20 : 12,\n fontWeight: 900,\n fontFamily: 'NotoIKEALatin, Verdana, sans-serif',\n lineHeight: platform.isKiosk ? 1.2 : 1.33,\n trim: this.isMobile,\n fill: '#ffffff',\n });\n\n if (!this.label) {\n this.label = new PIXI.Text(textContent, style);\n } else {\n this.label.text = textContent;\n }\n this.label.resolution = 3;\n\n const textPaddingHorizontal = 6;\n const textPaddingVertical = platform.isKiosk ? 8 : 4;\n\n this.label.x =\n this.isVertical || this.isTable\n ? (length - this.label.height) / 2\n : (length - this.label.width) / 2;\n\n if (!this.isVertical) {\n if (!this.textUnderLine) {\n this.label.y = this.textInside\n ? textPaddingVertical\n : -textPaddingVertical - this.label.height;\n } else {\n this.label.y = textPaddingVertical;\n }\n } else {\n this.label.y = -textPaddingHorizontal;\n }\n\n this.background = new PIXI.Graphics();\n const thickness = !!this.isVertical ? 0.5 : 1.0;\n this.background.lineStyle(thickness, 0x111111);\n this.background.beginFill(0x111111);\n\n !this.isVertical &&\n !this.isTable &&\n this.background.drawRoundedRect(\n this.label.x - textPaddingHorizontal,\n this.label.y - textPaddingVertical,\n this.label.width + textPaddingHorizontal * 2.0,\n this.label.height + textPaddingVertical * 2.0,\n 2\n );\n\n !!this.isVertical &&\n this.background.drawRoundedRect(\n this.label.x - textPaddingVertical,\n this.label.y - textPaddingHorizontal - this.label.width,\n this.label.height + textPaddingVertical * 2.0,\n this.label.width + textPaddingHorizontal * 2.0,\n 2\n );\n\n const serif = constants.MEASUREMENTS_LINE_SERIF * this.ratio;\n const lineThickness = this.isMobile ? 1 : 2;\n const depthLineThickness = this.isMobile ? 2 : 3;\n const serifPoints = this.getSerifPoints(serif, length);\n if (!this.isDepth) {\n this.line = new PIXI.Graphics()\n .lineStyle(lineThickness, 0x111111, 1)\n .moveTo(0, 0)\n .lineTo(length, 0);\n } else {\n this.line = new PIXI.Graphics()\n .lineStyle(depthLineThickness, 0x111111, 1)\n .moveTo(0, 0)\n .lineTo(length, 0);\n }\n\n if (!this.skipFirstSerif) {\n this.line.moveTo(...serifPoints[0][0]).lineTo(...serifPoints[0][1]);\n }\n this.line.moveTo(...serifPoints[1][0]).lineTo(...serifPoints[1][1]);\n\n if (!!this.isTable) {\n this.background.drawRoundedRect(\n this.label.x - textPaddingVertical,\n this.label.y - textPaddingVertical,\n this.label.height + textPaddingVertical * 2.0,\n this.label.width + textPaddingHorizontal * 2.0,\n 2\n );\n\n this.rotation = Math.PI / 2;\n this.label.pivot = new PIXI.Point(this.label.width, 0);\n this.label.rotation = Math.PI / -2;\n this.background.y = this.background.y + this.background.width;\n this.label.y = this.label.y + this.background.width;\n } else if (!!this.isVertical) {\n this.label.pivot = new PIXI.Point(0, 0);\n this.label.rotation = Math.PI / -2;\n } else if (!!this.isDepth) {\n if (!!this.isMobile) {\n this.background.x = this.background.x + this.label.width;\n this.label.x = this.label.x + this.background.x;\n } else if (platform.isKiosk) {\n this.background.x = this.background.x + this.label.width;\n this.background.y = this.background.y + this.background.height;\n this.label.x = this.label.x + this.background.x;\n this.label.y = this.label.y + this.background.y;\n } else {\n this.background.y = this.background.y + this.background.width;\n this.label.y = this.label.y + this.background.y;\n }\n this.line.angle =\n Math.sin(constants.OBLIQUE_ANGLE) * constants.ROOM_DEPTH * 0.33;\n this.line.skew.x = -5.25;\n }\n\n this.background.endFill();\n\n this.textContainer = new PIXI.Container();\n this.textContainer.addChild(this.background, this.label);\n\n this.addChild(this.textContainer);\n this.addChild(this.line);\n }\n}\n","import bowser from 'bowser';\n\n/*\nAs an approximation we consider devices running\n iOS versions 10 and lower\n Android versions 6 and lower\ntoo old and slow\n*/\nexport default function slowDevice() {\n return (\n (bowser.ios && parseInt(bowser.osversion, 10) < 11) ||\n (bowser.android && parseInt(bowser.osversion, 10) < 7)\n );\n}\n","import * as PIXI from 'pixi.js-legacy';\nimport MeasurementLine from './MeasurementLine';\nimport constants from '../settings/constants';\nimport tacHelpers from '../state/tac/tacHelpers';\nimport slowDevice from './util/slowDevice';\nimport platform from '../util/platform';\nimport getDepthOffset from './util/getDepthOffset';\nimport productService from '../services/products';\nimport { isFloorStanding, isWallMounted } from '../services/products/models';\nimport { ITEMS, RANGES } from '../constants';\nimport { applicationSettings } from '../settings/application';\n\nconst animationEnabled = !slowDevice();\nconst animationSpeed = 0.08;\n\nexport default class Measurements extends PIXI.Container {\n constructor({ app, isMobile, scene }) {\n super();\n this.app = app;\n this.isMobile = isMobile;\n this.scene = scene;\n }\n\n hasMeasurementsCovered(section) {\n const { tac } = this;\n if (productService.isExtendable(section)) {\n // we are already the smallest section, cannot be covered by others\n return false;\n }\n\n const superSection = tacHelpers.findSuperSection(tac, section);\n\n const others = superSection.filter(\n other =>\n productService.isSection(other) && other.itemid !== section.itemid\n );\n\n // first, let's make sure we have aligned sections on both ends\n const leftAligned = others.filter(other => other.x === section.x);\n\n if (!leftAligned.length) {\n return false;\n }\n\n const rightmostPoint = section.x + section.width;\n const rightAligned = others.filter(\n other => other.x + other.width === rightmostPoint\n );\n\n if (!rightAligned.length) {\n return false;\n }\n\n // we are aligned both left and right, now let's see if we are completely covered\n if (\n leftAligned.some(\n other =>\n other.x + other.width === section.x + section.width &&\n other.y < section.y\n )\n ) {\n // we have another equally sized section below us\n return true;\n }\n\n // finally, let's check that all our other positions in the grid are covered\n const stepInterval =\n constants.DYNAMIC_GRID?.[constants.DRAG_MODE.FLOAT].x.step ||\n constants.GRID?.x.step;\n for (let pos = section.x + stepInterval; pos < rightmostPoint; ) {\n if (\n !others.some(\n other =>\n other.width < section.width &&\n other.x <= pos &&\n other.x + other.width >= pos\n )\n ) {\n // ...they weren't\n return false;\n }\n pos += stepInterval;\n }\n\n // ...they were\n return true;\n }\n\n drawGapLines() {\n const { tac, ratio, room, origo, offset } = this;\n\n this.gapLines = this.gapLines || new PIXI.Container();\n\n if (!this.gapLines.parent) {\n this.addChild(this.gapLines);\n }\n\n this.gapLines &&\n this.gapLines.children.slice().forEach(child => {\n child.destroy();\n });\n\n const floorStanders = tac.items\n .filter(\n item =>\n (isFloorStanding(item) && (!isWallMounted(item) || item.y === 0)) ||\n productService.isType(item, 'sidewall')\n )\n .sort((a, b) => a.x - b.x);\n\n floorStanders.forEach((item, index, array) => {\n if (index === array.length - 1) {\n //no line on last item\n return;\n }\n\n const next = array[index + 1];\n\n const length = next.x - (item.x + item.width);\n\n if (length < 1) {\n // no line if no space between\n return;\n }\n\n if (\n productService.isType(item, 'sidewall') &&\n productService.isType(next, 'sidewall')\n ) {\n // no line between sidewalls in empty room\n return;\n }\n\n const line = new MeasurementLine({\n length,\n ratio,\n isMobile: this.isMobile,\n });\n\n const sprite = this.scene.itemContainer.getSprite(item.itemid);\n const { projectedVertices } = sprite.data;\n\n line.y =\n (room.height - item.y - projectedVertices[0][1]) * ratio + offset;\n line.x =\n (item.x + item.width + projectedVertices[0][0] - origo.x) * ratio - 2;\n\n line.itemRefs = {\n from: item.itemid,\n to: next.itemid,\n };\n\n this.gapLines.addChild(line);\n });\n }\n\n drawSectionLines() {\n const { tac, ratio, room, origo, offset } = this;\n\n const sections = tacHelpers.getSections(tac);\n\n this.sectionLines = this.sectionLines || new PIXI.Container();\n if (!this.sectionLines.parent) {\n this.addChild(this.sectionLines);\n }\n this.sectionLines &&\n this.sectionLines.children.slice().forEach(child => {\n child.destroy();\n });\n\n sections.forEach(section => {\n if (this.hasMeasurementsCovered(section)) {\n return;\n }\n const hasLeftNeighbour = sections.some(\n other => other.y === section.y && other.x + other.width === section.x\n );\n const sectionLine = new MeasurementLine({\n length: section.width,\n ratio,\n isMobile: this.isMobile,\n skipFirstSerif: hasLeftNeighbour,\n });\n\n sectionLine.y = (room.height - section.y) * ratio + offset;\n sectionLine.x = (section.x - origo.x) * ratio - 2; // is it the serifs width the reason we have to offset the line by 2px?\n\n this.sectionLines.addChild(sectionLine);\n });\n }\n\n /**\n * Instantiates, adds and positions the measurement lines for\n * tables in the configuration.\n * Lines whose label would not fully fit within the drawable area\n * are shifted to the right as much as necessary to prevent the\n * label from being cut off.\n */\n drawTableLines() {\n const { tac, ratio, room, origo, offset } = this;\n\n const tables = tacHelpers\n .getAllItems(tac.items)\n .filter(item => productService.isType(item, ITEMS.TABLE))\n .map(table => ({ ...table, ...tacHelpers.getGlobalCoords(table, tac) }));\n\n if (tables.length) {\n this.tableLines = this.tableLines || new PIXI.Container();\n\n if (!this.tableLines.parent) {\n this.addChild(this.tableLines);\n }\n this.tableLines &&\n this.tableLines.children.slice().forEach(child => {\n child.destroy();\n });\n\n tables.forEach(table => {\n const hasLeftNeighbour = tables.some(\n other => other.y === table.y && other.x + other.width === table.x\n );\n if (hasLeftNeighbour) {\n return;\n }\n const tableLine = new MeasurementLine({\n length: table.y + table.height,\n ratio,\n isMobile: this.isMobile,\n textInside: false,\n textUnderLine: false,\n isTable: true,\n });\n\n const sprite = this.scene.itemContainer.getSprite(table.itemid);\n const { projectedVertices } = sprite.data;\n\n tableLine.y =\n (room.height - table.y - table.height - projectedVertices[0][1]) *\n ratio;\n tableLine.x =\n (table.x - origo.x + projectedVertices[0][0]) * ratio - offset;\n\n this.tableLines.addChild(tableLine);\n\n const { x, y } = tableLine.getBounds();\n const tableLineUpperLeftWorld = new PIXI.Point(x, y);\n const tableLineUpperLeftLocal = this.toLocal(tableLineUpperLeftWorld);\n const drawableUpperLeftWorld = new PIXI.Point(0, 0);\n const drawableUpperLeftLocal = this.toLocal(drawableUpperLeftWorld);\n const shift = Math.max(\n 0,\n drawableUpperLeftLocal.x - tableLineUpperLeftLocal.x\n );\n tableLine.x += shift;\n });\n }\n }\n\n drawClothesRailLines() {\n const { tac, ratio, room, origo, offset } = this;\n\n const extendableClothesRails = tacHelpers\n .getClothesRails(tac)\n .filter(productService.isMultiParentProduct);\n\n this.clothesRailLines = this.clothesRailLines || new PIXI.Container();\n if (!this.clothesRailLines.parent) {\n this.addChild(this.clothesRailLines);\n }\n\n this.clothesRailLines &&\n this.clothesRailLines.children.slice().forEach(child => {\n child.destroy();\n });\n\n extendableClothesRails.forEach((item, index) => {\n const currentLine = 'clothesRailLine_' + index;\n this[currentLine] && this[currentLine].destroy();\n\n const topAncestor = tacHelpers.getTopAncestor(tac, item);\n const connectsToTopAncestor = tacHelpers.getTopAncestor(\n tac,\n item.connectsTo\n );\n\n if (\n !connectsToTopAncestor ||\n this.gapLines.children.some(line => {\n return (\n line.itemRefs.from === topAncestor.itemid &&\n line.itemRefs.to === connectsToTopAncestor.itemid\n );\n })\n ) {\n // no line if this gap is already measured on floor\n return;\n }\n\n const length =\n connectsToTopAncestor.x - (topAncestor.x + topAncestor.width);\n\n this[currentLine] = new MeasurementLine({\n length,\n ratio: ratio,\n isMobile: this.isMobile,\n // FIXME: textInside is currently rendered the same way as textUnderLine.\n // Do we still need textInside? Consider changing to textUnderLine and\n // removing the textInside implementation from the MeasurementLine class.\n textInside: true,\n });\n\n const parentCoords = tacHelpers.getGlobalCoords(item, tac);\n const depthOffset = getDepthOffset(item);\n const sprite = this.scene.itemContainer.getSprite(item.itemid);\n\n const { projectedVertices } = sprite.data;\n\n this[currentLine].x =\n (topAncestor.x +\n topAncestor.width -\n depthOffset.x +\n projectedVertices[0][0] -\n origo.x) *\n ratio;\n this[currentLine].y =\n (room.height - parentCoords.y + item.height) * ratio + offset;\n\n this.clothesRailLines.addChild(this[currentLine]);\n });\n }\n\n getVerticalLineX(limits, size) {\n const { origo, ratio, offset, isPortrait } = this;\n const preferred = (limits.min.x + size.width - origo.x) * ratio + offset;\n if (\n isPortrait &&\n this.x + preferred + this.verticalLine.height >\n this.scene.room.targetWidth\n ) {\n return this.scene.room.targetWidth - this.verticalLine.height - this.x;\n }\n\n return preferred;\n }\n\n getDepthLineLength() {\n let depthLineLength = 0;\n this.scene.itemContainer.children.forEach(child => {\n if (\n child.data.outerSize.width - child.data.faceSize.width >\n depthLineLength\n ) {\n if (applicationSettings.applicationName !== RANGES.BOAXEL) {\n depthLineLength =\n child.data.outerSize.width - child.data.faceSize.width;\n } else if (child.childrenContainer.children.length > 0) {\n depthLineLength =\n child.data.outerSize.width - child.data.faceSize.width;\n }\n }\n });\n return depthLineLength;\n }\n\n getDepthText(size) {\n let depthText = 0;\n this.tac.items.forEach(item => {\n if (applicationSettings.applicationName === RANGES.BOAXEL) {\n depthText = size.depth;\n } else if (item.depth > depthText) {\n depthText = item.depth;\n }\n });\n return depthText;\n }\n\n draw() {\n const { tac, ratio, room, origo = { x: 0, y: 0 }, offset } = this;\n\n const limits = tacHelpers.getLimits(tac);\n const size = tacHelpers.getSize(tac);\n const depthLineLength = this.getDepthLineLength();\n\n this.horizontalLine && this.horizontalLine.destroy();\n this.verticalLine && this.verticalLine.destroy();\n this.depthLine && this.depthLine.destroy();\n this.tableLines &&\n this.tableLines.children.length &&\n this.tableLines.children.forEach(item => item.destroy());\n\n this.horizontalLine = new MeasurementLine({\n length: size.width,\n ratio: ratio,\n isMobile: this.isMobile,\n });\n this.horizontalLine.x = (limits.min.x - origo.x) * ratio;\n this.horizontalLine.y = (room.height - limits.max.y) * ratio - offset;\n\n this.addChild(this.horizontalLine);\n\n this.verticalLine = new MeasurementLine({\n length: size.height,\n ratio: ratio,\n isMobile: this.isMobile,\n isVertical: true,\n });\n this.verticalLine.x = this.getVerticalLineX(limits, size);\n this.verticalLine.y = (room.height - limits.max.y) * ratio;\n this.verticalLine.rotation = Math.PI / 2;\n\n this.addChild(this.verticalLine);\n\n this.depthLine = new MeasurementLine({\n length: depthLineLength,\n ratio: ratio,\n isMobile: this.isMobile,\n isDepth: true,\n size: this.getDepthText(size),\n });\n this.depthLine.x = this.getVerticalLineX(limits, size);\n this.depthLine.y = (room.height - limits.min.y) * ratio;\n\n this.addChild(this.depthLine);\n\n constants.DISPLAY_SECTION_MEASUREMENTS && this.drawSectionLines();\n this.drawGapLines();\n constants.DISPLAY_TABLE_MEASUREMENTS && this.drawTableLines();\n this.drawClothesRailLines();\n\n if (!this.active) {\n this.active = true;\n this.horizontalLine.alpha = 0;\n this.verticalLine.alpha = 0;\n this.depthLine.alpha = 0;\n this._fadeIn(this.verticalLine);\n this._fadeIn(this.horizontalLine);\n this._fadeIn(this.depthLine);\n\n [\n this.clothesRailLines,\n this.sectionLines,\n this.gapLines,\n this.tableLines,\n ].forEach(lineGroup => {\n lineGroup &&\n lineGroup.children.forEach(child => {\n child.alpha = 0;\n this._fadeIn(child);\n });\n });\n }\n }\n\n _fadeIn(sprite) {\n if (sprite.alpha < 1 && animationEnabled) {\n sprite.alpha += animationSpeed;\n if (this.active) {\n requestAnimationFrame(() => {\n this._fadeIn(sprite);\n });\n this.app.render();\n }\n } else {\n sprite.alpha = 1;\n this.app.render();\n }\n }\n\n clear() {\n if (\n this.verticalLine &&\n this.verticalLine.alpha > 0 &&\n animationEnabled &&\n !this.wallResizerActive\n ) {\n this.verticalLine.alpha -= animationSpeed;\n this.horizontalLine.alpha -= animationSpeed;\n this.depthLine.alpha -= animationSpeed;\n\n [\n this.clothesRailLines,\n this.sectionLines,\n this.gapLines,\n this.tableLines,\n ].forEach(lineGroup => {\n lineGroup &&\n lineGroup.children.forEach(child => {\n child.alpha -= animationSpeed;\n });\n });\n\n if (!this.active) {\n requestAnimationFrame(() => {\n this.clear();\n });\n if (this.app.renderer) {\n this.app.render();\n }\n }\n } else {\n this.verticalLine && this.verticalLine.destroy();\n this.verticalLine = null;\n\n this.horizontalLine && this.horizontalLine.destroy();\n this.horizontalLine = null;\n\n this.depthLine && this.depthLine.destroy();\n this.depthLine = null;\n\n [\n this.sectionLines,\n this.clothesRailLines,\n this.gapLines,\n this.tableLines,\n ].forEach(lineGroup => {\n lineGroup &&\n // need to slice these since destroying a child modifies children array\n lineGroup.children.slice().forEach(child => {\n child.destroy();\n });\n });\n }\n }\n\n update({\n active,\n wallResizerActive,\n origo,\n tac,\n room,\n ratio,\n isMobile,\n isPortrait,\n }) {\n this.isMobile = isMobile;\n this.wallResizerActive = wallResizerActive;\n this.origo = origo;\n this.tac = tac;\n this.room = room;\n this.ratio = ratio;\n this.isPortrait = isPortrait;\n\n this.offset = platform.isKiosk\n ? constants.MEASUREMENTS_OFFSET_KIOSK\n : isMobile\n ? constants.MEASUREMENTS_OFFSET_MOBILE\n : constants.MEASUREMENTS_OFFSET;\n\n this.offset += (constants.MEASUREMENTS_LINE_SERIF / 2) * ratio;\n\n if (active && tac.items.length > 0) {\n this.draw();\n } else if (this.verticalLine) {\n this.active = false;\n this.clear();\n }\n }\n}\n","import * as PIXI from 'pixi.js-legacy';\nimport Measurements from './Measurements';\n\nexport default class MeasurementLayer extends PIXI.Application {\n constructor({\n autoStart,\n autoResize,\n resolution,\n transparent,\n view,\n height,\n width,\n isMobile,\n scene,\n }) {\n super({\n autoStart,\n autoResize,\n resolution,\n transparent,\n view,\n height,\n width,\n });\n PIXI.settings.ROUND_PIXELS = true;\n // capping SPRITE_MAX_TEXTURES fixes a webgl crash in firefox on linux\n PIXI.settings.SPRITE_MAX_TEXTURES = Math.min(\n PIXI.settings.SPRITE_MAX_TEXTURES,\n 16\n );\n PIXI.settings.FAIL_IF_MAJOR_PERFORMANCE_CAVEAT = false;\n\n this.measurements = new Measurements({\n isMobile,\n app: this,\n scene,\n });\n\n this.stage.addChild(this.measurements);\n }\n\n setX(x) {\n this.measurements.x = x;\n }\n\n setY(y) {\n this.measurements.y = y;\n }\n\n update({\n origo,\n active,\n wallResizerActive,\n tac,\n room,\n ratio,\n isMobile,\n isPortrait,\n }) {\n this.measurements.update({\n origo,\n active,\n wallResizerActive,\n tac,\n room,\n ratio,\n isMobile,\n isPortrait,\n });\n this.render();\n }\n}\n","import * as PIXI from 'pixi.js-legacy';\nimport deepEqual from 'fast-deep-equal';\nimport emitter from '../emitter';\nimport offset from '../util/offset';\nimport productsService from '../services/products';\nimport constants from '../settings/constants';\nimport { DROP_ITEM, GRAB_ITEM, SCENE_REDRAWN } from '../settings/events';\nimport * as supportedEvents from '../util/supportedEvents';\nimport tacHelpers from '../state/tac/tacHelpers';\nimport * as actions from '../state/tac/tacActions.js';\nimport store from '../state';\nimport getComponent from './util/getComponent';\nimport ItemContainer from './ItemContainer';\nimport proppingService from '../services/propping';\nimport Rect from './Rect';\nimport DynamicRoom from './DynamicRoom';\nimport FixedRoom from './FixedRoom';\nimport {\n actionSetWallResizerInactive,\n actionSetProppingVisibility,\n} from '../state/scene';\nimport { isFixedRoom } from '../util/room';\nimport MeasurementLayer from './MeasurementLayer';\nimport storage from '../services/history/storage';\nimport { actionShowSceneErrors, actionHideSceneErrors } from '../state/popups';\nimport { showTacToast } from '../services/toastMaster';\nimport { thunkRemoveItem, thunkUpdateItem } from '../state/tac/tacThunks';\nimport {\n selectSceneMargins,\n selectSceneRect,\n} from '../state/scene/sceneSelectors';\nimport { applicationSettings } from '../settings/application';\n\nwindow.oncontextmenu = () => {\n emitter.emit(DROP_ITEM, {\n abort: true,\n });\n};\n\nexport default class SceneBase extends PIXI.Application {\n constructor(args) {\n PIXI.settings.ROUND_PIXELS = true;\n // capping SPRITE_MAX_TEXTURES fixes a webgl crash in firefox on linux\n PIXI.settings.SPRITE_MAX_TEXTURES = Math.min(\n PIXI.settings.SPRITE_MAX_TEXTURES,\n 16\n );\n PIXI.settings.FAIL_IF_MAJOR_PERFORMANCE_CAVEAT = false;\n super({\n autoStart: false,\n autoResize: true,\n antialias: true,\n resolution: Math.max(window.devicePixelRatio, 1.5),\n transparent: true,\n height: args.viewRect.height,\n width: args.viewRect.width,\n });\n this.onDragStart = this.onDragStart.bind(this);\n this.onDragMove = this.onDragMove.bind(this);\n this.onDragEnd = this.onDragEnd.bind(this);\n this.update = this.update.bind(this);\n this.resizeScene = this.resizeScene.bind(this);\n this.getItemBounds = this.getItemBounds.bind(this);\n this.getSprite = this.getSprite.bind(this);\n this.adaptProppings = this.adaptProppings.bind(this);\n this.activate = this.activate.bind(this);\n this.renderer.plugins.interaction.autoPreventDefault = false;\n this.renderer.view.style['touch-action'] = 'auto';\n this.stage.interactive = true;\n const { userAgent } = store.getState();\n this.room = isFixedRoom()\n ? new FixedRoom(this.view, this.render.bind(this), this.stage)\n : new DynamicRoom();\n this.stage.addChild(this.room);\n this.rectContainer = this.rectContainer || new PIXI.Container();\n this.stage.addChild(this.rectContainer);\n this.itemContainer = new ItemContainer({\n onDragStart: this.onDragStart,\n app: this,\n isMobile: userAgent.isMobile,\n });\n this.stage.addChild(this.itemContainer);\n this.measurementLayer = new MeasurementLayer({\n autoStart: false,\n autoResize: true,\n resolution: Math.max(window.devicePixelRatio, 1.5),\n transparent: true,\n height: args.viewRect.height,\n width: args.viewRect.width,\n isMobile: userAgent.isMobile,\n scene: this,\n });\n this.hintContainer = new PIXI.Container();\n this.stage.addChild(this.hintContainer);\n this.hasRendered = false;\n this.resizeScene(args);\n // TODO check that this setup works correctly on computers with touch\n // screen AND mouse\n this.stage\n .on(supportedEvents.POINTER_MOVE, this.onDragMove)\n .on(supportedEvents.POINTER_UP, this.onDragEnd)\n .on(supportedEvents.POINTER_UP_OUTSIDE, this.onDragEnd);\n\n this.activate(args);\n }\n\n activate(args) {\n if (!applicationSettings.isProd) {\n window.PIXI = this;\n }\n this.args = args;\n this.unsubscribe = store.subscribe(this.update);\n this.update();\n\n emitter.on(DROP_ITEM, this.onDragEnd);\n emitter.on(\n GRAB_ITEM,\n (product, mousePosition, mode, eventType, domSize) => {\n if (this.dragging) {\n return;\n }\n\n const canvasOffset = offset(this.view);\n\n mousePosition.x -= canvasOffset.left;\n mousePosition.y -= canvasOffset.top;\n\n const item = {\n ...product,\n x:\n (mousePosition.x - (this.room.x + this.itemContainer.x)) /\n this.room.ratio -\n product.width / 2,\n y:\n this.room.model.height -\n (mousePosition.y - (this.room.y + this.itemContainer.y)) /\n this.room.ratio -\n product.height / 2,\n z: productsService.getInitialZPos(product),\n };\n\n // Set default propping\n const singleProppingBounds = proppingService.pickPropping({\n tac: this.tac,\n item,\n });\n if (singleProppingBounds) {\n item.propping = singleProppingBounds.id;\n }\n\n this.itemArgs = {\n onDragStart: this.onDragStart,\n ratio: this.room.ratio,\n parentItem: this.room.model,\n options: this.options,\n app: this,\n };\n\n const sprite = getComponent({ ...this.itemArgs, item });\n\n sprite.dummy = true;\n\n this.onDragStart({\n mode: productsService.getDragMode(item),\n mouse: mousePosition,\n offset: {\n x: sprite.width / 2 + this.itemContainer.x,\n y: sprite.height / 2 + this.itemContainer.y,\n },\n sprite: sprite,\n domSize: domSize,\n });\n }\n );\n }\n\n deactivate() {\n emitter.off(DROP_ITEM);\n emitter.off(GRAB_ITEM);\n this.args = {};\n this.unsubscribe();\n }\n\n /**\n * Creates and returns a base 64 string based on a texture created from the scene.\n * @returns {string}\n */\n createImageStringFromTexture(\n startingX,\n startingY,\n floor,\n height,\n resolution\n ) {\n const renderTexture = this.renderer.generateTexture(\n this.stage,\n PIXI.SCALE_MODES.LINEAR,\n resolution,\n new PIXI.Rectangle(startingX, startingY, floor.width, height)\n );\n const base64Image = this.renderer.extract.base64(renderTexture);\n renderTexture.destroy(true);\n return base64Image;\n }\n\n /**\n * Base 64 encoding creates 4 characters for each 3 bytes in a string.\n * https://sv.wikipedia.org/wiki/Base64\n * It also pads the string with (equal signs) to bring it to a certain block size\n * but it is omited here because we only need an approximate number.\n * @param {string} imageBase64String\n * @returns {number}\n */\n getFileSizeOfBase64Image(imageBase64String) {\n return imageBase64String.length * (3 / 4);\n }\n\n /**\n * Returns a base64 string based on an image that is less than 1mb in size,\n * to comply with the dexf 1mb limitation on images.\n */\n getCompressedSceneImage(startingX, startingY, floor, height) {\n let resolution = 1.1;\n let sceneImage = '';\n do {\n resolution -= 0.1;\n sceneImage = this.createImageStringFromTexture(\n startingX,\n startingY,\n floor,\n height,\n resolution\n );\n } while (this.getFileSizeOfBase64Image(sceneImage) > 1 * Math.pow(10, 6));\n return sceneImage;\n }\n\n getSceneImage(limitResolution) {\n this.wallResizerActive &&\n store.dispatch(actionSetWallResizerInactive({ nonInteraction: true }));\n\n store.dispatch(actionSetProppingVisibility(false));\n this.measurementLayer.measurements.clear();\n\n const { room } = this;\n const { floor } = room;\n\n const startingY = room.y + room.wall.y;\n const height = room.height;\n const startingX = floor.x + room.x;\n\n if (limitResolution)\n return this.getCompressedSceneImage(startingX, startingY, floor, height);\n\n return this.createImageStringFromTexture(\n startingX,\n startingY,\n floor,\n height,\n this.renderer.resolution\n );\n }\n\n resizeScene({ viewRect, trashcanRect }) {\n const currentSceneRect = selectSceneRect(store.getState());\n if (this.renderer) {\n this.trashcanRect = trashcanRect;\n\n this.renderer.resize(viewRect.width, viewRect.height);\n this.measurementLayer.renderer.resize(viewRect.width, viewRect.height);\n this.trashcanBelow =\n trashcanRect.y > currentSceneRect.y + currentSceneRect.height;\n this.update(true);\n }\n }\n\n showToasts(tac) {\n const { toastFlags } = tac;\n if (!toastFlags) {\n return;\n }\n\n toastFlags.forEach(key => {\n showTacToast(key);\n });\n\n store.dispatch(actions.removeToastFlags());\n }\n\n update(force = false) {\n if (!this.renderer) {\n // sometimes the applications is destroyed before the redux subscription triggers an\n // update, so we need to ensure the renderer is not nul before try update the scene\n return;\n }\n\n const state = store.getState();\n const { userAgent, scene, popups } = state;\n const tac = state.tac.present;\n\n const options = {\n isMobile: userAgent.isMobile,\n isPortrait: userAgent.isPortrait,\n measurementsActive: scene.measurementsActive,\n wallResizerActive: scene.wallResizerActive,\n errorVisible: popups.sceneErrorVisible && !scene.wallResizerActive,\n introPopupsVisible: popups.introPopupsVisible,\n hasShownPegboardHint: popups.hasShownPegboardHint,\n popups: popups.scenePopups,\n firstRender: !this.hasRendered,\n showPropping: scene.showPropping,\n };\n\n if (force || tac !== this.tac || !deepEqual(options, this.options)) {\n // Scene has changed so cached image is no longer valid\n storage.session.removeItem(\n constants.SESSION_STORAGE.CHECKOUT_IMAGE_CACHE_KEY\n );\n\n const instantErrors =\n tac?.errors?.filter(error => error.options?.showInstantly) || [];\n if (instantErrors.length && !popups.sceneErrorVisible) {\n store.dispatch(actionShowSceneErrors(instantErrors));\n } else if (!instantErrors.length && popups.sceneErrorVisible) {\n store.dispatch(actionHideSceneErrors());\n }\n\n tac && this.showToasts(tac);\n\n const leftDepth =\n Math.cos(constants.OBLIQUE_ANGLE) * constants.ROOM_DEPTH * 0.29;\n\n if (\n !(this.wallResizerActive && scene.wallResizerActive) ||\n !this.canvasMargins\n ) {\n this.canvasMargins = selectSceneMargins(store.getState());\n }\n\n this.wallResizerActive = scene.wallResizerActive;\n this.measurementsActive = scene.measurementsActive;\n\n this.room.update();\n\n this.itemContainer.x = this.hintContainer.x = isFixedRoom()\n ? this.room.x + this.room.wall.x\n : this.room.x + leftDepth * this.room.ratio;\n this.measurementLayer.setX(this.itemContainer.x);\n\n this.itemContainer.y = this.hintContainer.y = isFixedRoom()\n ? this.room.y +\n this.room.wall.y +\n this.canvasMargins.top * this.room.ratio\n : this.room.y + this.canvasMargins.top;\n this.measurementLayer.setY(this.itemContainer.y);\n\n this.tac = tac;\n this.options = options;\n\n if (this.tac) {\n this.itemContainer.update({\n tac,\n options,\n room: this.room.model,\n ratio: this.room.ratio,\n wallResizerActive: options.wallResizerActive,\n });\n\n this.measurementLayer.update({\n origo: this.itemContainer.origo,\n active: options.measurementsActive,\n wallResizerActive: options.wallResizerActive,\n tac,\n room: this.room.model,\n ratio: this.room.ratio,\n isMobile: options.isMobile,\n isPortrait: options.isPortrait,\n });\n\n this.adaptProppings();\n\n this.hasRendered = true;\n // tell extendable items about it\n emitter.emit(SCENE_REDRAWN, tac);\n }\n\n this.render();\n }\n }\n\n adaptProppings(movingItem, sprite) {\n const { dragging } = this;\n const movingParent = sprite?.parentItem || { x: 0, y: 0, z: 0 };\n const gMovingItem = movingItem\n ? {\n ...movingItem,\n x: movingParent.x,\n y: movingParent.y,\n z: movingParent.z,\n }\n : movingItem;\n const itemsToAdapt = tacHelpers.getProppingItemsToAdapt(\n this.tac,\n gMovingItem,\n dragging && dragging.item,\n dragging && dragging.originalItem\n );\n\n itemsToAdapt.forEach(item => {\n const sprite = this.itemContainer.getSprite(item.itemid);\n sprite &&\n sprite.adaptPropping &&\n sprite.adaptPropping(movingItem, dragging?.sprite);\n });\n }\n\n withoutClothesRails(item, filter) {\n const crids = tacHelpers\n .getClothesRails(item)\n .filter(productsService.isMultiParentProduct)\n .filter(cr => cr.logic && cr.logic.extendable)\n .map(item => item.itemid);\n const removeCrids = crids\n .map(crid => this.itemContainer.getSprite(crid))\n .filter(filter)\n .map(cr => cr.item.itemid);\n return {\n item: tacHelpers.filterTac(item, removeCrids),\n removed: removeCrids,\n };\n }\n\n crFilters = {\n hidden: cr => cr.adjustableContainer && cr.adjustableContainer.alpha === 0,\n all: () => true,\n };\n\n isInTrashcan(e) {\n const mouse = e && e.data && e.data.global;\n if (!mouse) {\n return false;\n }\n const trash = this.trashcanRect;\n\n return (\n mouse.x > trash.x &&\n mouse.x < trash.x + trash.width &&\n mouse.y > trash.y &&\n mouse.y < trash.y + trash.height\n );\n }\n\n isInScene(e) {\n const { offset, sprite } = this.dragging;\n\n const mouse = e && e.data && e.data.global;\n\n if (!mouse) {\n return false;\n }\n\n const roomBounds = this.room.getBounds();\n\n if (this.trashcanBelow) {\n const spriteVerticalLimit =\n this.itemContainer.y +\n (mouse.y - offset.y) +\n sprite.getBounds().height / 4;\n\n return spriteVerticalLimit < roomBounds.y + roomBounds.height;\n } else {\n const spriteHorizontalLimit =\n this.itemContainer.x +\n (mouse.x - offset.x) +\n sprite.getBounds().width / 4;\n\n return spriteHorizontalLimit < roomBounds.x + roomBounds.width;\n }\n }\n getItemBounds(item) {\n return this.itemContainer.getItemBounds(item);\n }\n\n getSprite(itemid) {\n if (itemid.id && productsService.isSection(itemid.id)) {\n // this is an item, not an itemid\n const item = itemid;\n const tacItem = tacHelpers.getItem(this.tac, item.itemid);\n if (tacItem) {\n return this.itemContainer.getSprite(item.itemid);\n }\n // not in tac so probably a fake item, let's try and find it\n const actualItem = this.tac.items.find(\n cand => cand.id === item.id && cand.x === item.x && cand.y === item.y\n );\n if (actualItem) {\n return this.itemContainer.getSprite(actualItem.itemid);\n }\n }\n return this.itemContainer.getSprite(itemid);\n }\n\n removeItem(item, reason) {\n store.dispatch(thunkRemoveItem(item, undefined, reason));\n }\n\n updateItem(item, parent, meta) {\n store.dispatch(thunkUpdateItem(item, parent, meta));\n }\n\n __debug__drawRects(rects, color, options) {\n if (options && options.clear) {\n this.rectContainer.removeChildren();\n }\n return rects.map(irect => {\n const rect = { ...irect };\n\n rect.y =\n this.room.height -\n (rect.y + this.itemContainer.y + rect.height) * this.room.ratio -\n this.room.y +\n this.itemContainer.y -\n (this.room.height - this.room.skirt.y) +\n 33;\n rect.x =\n (rect.x -\n this.itemContainer.origo.x -\n this.room.x +\n this.itemContainer.x +\n 500) *\n this.room.ratio;\n\n const sprite = new Rect({\n item: rect,\n ratio: this.room.ratio,\n });\n this.rectContainer.addChild(sprite);\n sprite.drawOutline(color);\n return sprite;\n });\n }\n\n __debug__drawPoly(poly, color, options) {\n if (options && options.clear) {\n this.rectContainer.removeChildren();\n }\n\n const localy = y => {\n return (\n this.room.height -\n (y + this.itemContainer.y) * this.room.ratio -\n this.room.y +\n this.itemContainer.y -\n (this.room.height - this.room.skirt.y) +\n 33\n );\n };\n\n const localx = x => {\n return (\n (x -\n this.itemContainer.origo.x -\n this.room.x +\n this.itemContainer.x +\n 500) *\n this.room.ratio\n );\n };\n\n const canvas = new PIXI.Graphics();\n\n canvas.lineStyle(2, color);\n\n poly.regions.forEach(region => {\n const points = region.map(coords => [\n localx(coords[0]),\n localy(coords[1]),\n ]);\n canvas.drawPolygon(new PIXI.Polygon(points.flat()));\n });\n this.rectContainer.addChild(canvas);\n }\n}\n","import { updateMultipleItems } from '../../state/tac/tacReducer/updateMultipleItems';\nimport updateDependentItems from '../../state/tac/tacReducer/updateDependentItems';\nimport constants from '../../settings/constants';\nimport geometry from './geometry';\nimport tacHelpers from '../../state/tac/tacHelpers';\nimport { isSection } from '../../services/products';\nimport { round } from '../../util/round';\nimport productService from '../../services/products';\n\nexport function supersection_drawSuggestions(supersection, scene) {\n const updTac = updateMultipleItems(scene.tac, supersection);\n const fixedTac = updateDependentItems(updTac, {\n keepBrackets: true,\n });\n const realItemsIds = scene.tac.items.map(item => item.itemid);\n\n fixedTac.items\n .filter(item => !realItemsIds.includes(item.itemid))\n .forEach(tempItem => {\n // tell section that it should draw a suggestion box on itself\n tempItem.isTempItem = true;\n });\n\n scene.itemContainer.update({\n room: scene.room.model,\n options: scene.itemContainerOptions,\n ratio: scene.room.ratio,\n tac: fixedTac,\n });\n}\n\nexport function supersection_relativePos(item, grabbedItem, drawPos) {\n return {\n x: item.x - grabbedItem.x + drawPos.x,\n y: item.y - grabbedItem.y + drawPos.y,\n z: item.z - grabbedItem.z + drawPos.z,\n };\n}\n\nexport function supersection_recalc(\n supersection,\n grabbedItem,\n newItem,\n scene,\n options\n) {\n const updItem = { ...newItem };\n // the grabbed item is _always_ a section,\n // so it's safe to do this x adjustment\n updItem.x += productService.getPostWidth() / 2;\n\n const boundingSupersection = supersection.map(item => {\n if (isSection(item)) {\n const bounds = geometry.mergeKids(\n item,\n tacHelpers.getRects(item, null, scene.tac, true)\n );\n return Object.assign({}, item, bounds);\n }\n return item;\n });\n const lowestY = Math.min.apply(\n null,\n boundingSupersection.map(item => item.y)\n );\n\n const bottomPadding = grabbedItem.y - lowestY;\n const gridY = scene.getGrid(constants.DRAG_MODE.FLOAT).y;\n\n if (!options?.lockedY) {\n updItem.y = Math.max(\n updItem.y,\n gridY.offset + bottomPadding,\n gridY.offset - bottomPadding\n );\n // now newItem.y might be off grid so readjust\n updItem.y = scene.getClosestGridPos(\n updItem.y - gridY.step / 2,\n constants.DRAG_MODE.FLOAT,\n 'y',\n round\n );\n }\n\n const items = boundingSupersection.map(item => ({\n ...item,\n ...supersection_relativePos(item, grabbedItem, updItem),\n }));\n return {\n items,\n item: { x: updItem.x, y: updItem.y },\n };\n}\n","import _ from 'lodash';\nimport polybool from 'polybooljs';\nimport emitter from '../emitter';\nimport geometry from './util/geometry';\nimport productsService from '../services/products';\nimport swiperService from '../services/swiper';\nimport { floor, round, ceil } from '../util/round';\nimport constants from '../settings/constants';\nimport { TRASH_CAN_HOVER, ITEM_MOVING } from '../settings/events';\nimport tacHelpers from '../state/tac/tacHelpers';\nimport store from '../state';\nimport {\n isStandAlone,\n isFloorStanding,\n isWallMounted,\n} from '../services/products/models';\nimport { range } from '../state/tac/range';\nimport { snapPadding } from './util/snapPadding';\nimport SceneBase from './SceneBase';\nimport idGenerator from '../util/aactools/idGenerator';\nimport {\n supersection_drawSuggestions,\n supersection_relativePos,\n supersection_recalc,\n} from './util/supersection';\nimport { selectCuttableMountingRailHintCheckPending } from '../state/popups/popupsSelectors';\nimport { thunkHandleMountingRailPopup } from '../state/popups/popupsThunks';\nimport {\n thunkAddItem,\n thunkUpdateItem,\n thunkUpdateMultiple,\n} from '../state/tac/tacThunks';\nimport { ITEMS } from '../constants';\n\nexport default class Scene extends SceneBase {\n GRID_TYPES = {\n DYNAMIC_GRID: 'DYNAMIC_GRID',\n GRID: 'GRID',\n };\n\n // calc dragmode and slots\n buildDragEnv(item, slotItem, fitItem, draw = false) {\n draw && this.rectContainer.removeChildren();\n let slots;\n let mode;\n let rects;\n let space = this.room.getSpace(this.tac);\n\n const dependentItems = range.getDependentItems(item, this.tac);\n const fittingTac = tacHelpers.filterTac(\n this.tac,\n dependentItems.map(item => item.itemid)\n );\n\n const slotOptions = {\n extendabilityLocked: slotItem.logic?.extendabilityLockedOnDrag,\n };\n\n const parentalSlots = tacHelpers.getOpenSlots(\n fittingTac,\n slotItem,\n slotOptions\n );\n if (parentalSlots.length) {\n this.itemContainer.drawDropAreas(parentalSlots);\n }\n\n if (isStandAlone(item)) {\n rects = tacHelpers.getRects(fittingTac, fitItem, this.tac);\n space = tacHelpers.getSpaceForItem(space, item);\n let roomSlots = tacHelpers.getRoomSlots(\n space,\n rects,\n fitItem,\n slotItem,\n this.tac\n );\n draw && this.__debug__drawRects(roomSlots);\n if (\n productsService.isSection(item) ||\n productsService.isFrame(item) ||\n productsService.isShelvingUnit(item)\n ) {\n // something with making addonshelves on dragged item\n // be a part of the colliding item\n roomSlots = roomSlots\n .map(slot =>\n tacHelpers.modifySlot(\n slot,\n fitItem,\n rects,\n this.room.model,\n this.itemContainer.origo\n )\n )\n .filter(Boolean);\n }\n\n slots = roomSlots.concat(parentalSlots);\n mode = parentalSlots.length\n ? constants.DRAG_MODE.MIXED\n : constants.DRAG_MODE.FLOAT;\n } else if (tacHelpers.isSuperSectionHandle(item)) {\n mode = constants.DRAG_MODE.MULTI;\n } else {\n slots = parentalSlots;\n mode = constants.DRAG_MODE.INSERT;\n }\n\n draw && this.__debug__drawRects(slots, 0xff0000);\n\n return { slots, mode, space, rects };\n }\n\n onDragStart({ mode, sprite, mouse, offset, domSize }) {\n if (this.dragging) {\n return;\n }\n const state = store.getState();\n const { userAgent, scene, popups } = state;\n const tac = state.tac.present;\n\n this.tac = tac;\n this.itemContainerOptions = {\n isMobile: userAgent.isMobile,\n isPortrait: userAgent.isPortrait,\n measurementsActive: scene.measurementsActive,\n errorVisible: popups.sceneErrorVisible,\n introPopupsVisible: popups.introPopupsVisible,\n hasShownPegboardHint: popups.hasShownPegboardHint,\n popups: popups.scenePopups,\n firstRender: !this.hasRendered,\n showPropping: scene.showPropping,\n };\n\n const item = sprite.item;\n\n const fitItem = this.withoutClothesRails(item, this.crFilters.hidden).item;\n\n this.args.onDragStart({ mode, sprite, item });\n const naked = this.withoutClothesRails(item, this.crFilters.all);\n\n const env = this.buildDragEnv(item, naked.item, fitItem);\n let slots;\n\n if (naked.removed.length && item.itemid) {\n const nakedEnv = this.buildDragEnv(item, naked.item, naked.item);\n const gItemPos = tacHelpers.getGlobalCoords(item, this.tac);\n const gItem = { ...fitItem, ...gItemPos };\n const currentSlot = geometry.closestCollidingRect(gItem, env.slots);\n\n const mergedSlots = currentSlot\n ? nakedEnv.slots\n .filter(slot => !geometry.contains(slot, currentSlot))\n .concat(currentSlot)\n : nakedEnv.slots;\n\n slots = mergedSlots;\n } else {\n slots = env.slots;\n }\n\n const rects = env.rects;\n mode = env.mode;\n\n if (mode & constants.DRAG_MODE.MIXED) {\n if (slots.length === 0) {\n if (!sprite.parent) {\n sprite.destroy();\n }\n\n return;\n }\n const newItem = { ...item };\n\n if (sprite.parent !== this.itemContainer) {\n if (sprite.parentItem && sprite.parentItem !== this.room.model) {\n offset.x += this.itemContainer.x;\n offset.y += this.itemContainer.y;\n\n newItem.x += sprite.parentItem.x || 0;\n newItem.y += sprite.parentItem.y || 0;\n\n sprite.x = mouse.x - offset.x;\n sprite.y = mouse.y - offset.y;\n }\n\n this.itemContainer.addChild(sprite, false);\n this.itemContainer.sortChildrenDragging(sprite, newItem, mode);\n }\n\n this.dragging = {\n item: newItem,\n originalItem: item,\n mode,\n offset,\n slots,\n space: env.space,\n sprite,\n domSize: domSize,\n rects,\n };\n } else if (mode === constants.DRAG_MODE.MULTI) {\n const ss = tacHelpers.findSuperSection(this.tac, item);\n\n const slotPolygon = tacHelpers.getSuperSectionSpace(this.tac, ss);\n\n this.dragging = {\n item: { ...item },\n supersection: _.cloneDeep(ss),\n originalItem: item,\n sprites: ss\n .filter(item => !productsService.isType(item, 'mounting-rail'))\n .map(item => this.getSprite(item.itemid)),\n sprite, // keeping this for positioning\n slotPolygon,\n mode,\n offset,\n space: env.space,\n domSize: domSize,\n rects,\n };\n }\n\n this.onDragMove({\n data: {\n global: mouse,\n },\n });\n }\n\n getNewItemTrashcanSizeScale() {\n const { domSize, sprite } = this.dragging;\n const { data, item } = sprite;\n const { ratio } = this.room;\n const zoom = swiperService.getTrashcanZoomLevel(item);\n\n if (sprite.width && sprite.height) {\n return sprite.width >= sprite.height\n ? (domSize.width * zoom) / (sprite.width / sprite.scale.x)\n : (domSize.height * zoom) / (sprite.height / sprite.scale.y);\n }\n return data.outerSize.width >= data.outerSize.height\n ? (domSize.width * zoom) / (data.outerSize.width * ratio)\n : (domSize.height * zoom) / (data.outerSize.height * ratio);\n }\n\n /**\n * Calculates and updates the scene according to the new mouse position.\n * Triggered every time the user moves while dragging.\n * @param {*} e The mouse event;\n * @returns {undefined}\n */\n onDragMove(e) {\n if (!this.dragging) {\n return;\n }\n\n if (e.data.originalEvent && e.data.originalEvent.cancelable) {\n e.data.originalEvent.preventDefault();\n }\n\n this.dragging.lastDragEventPos = {\n data: { global: e && e.data && e.data.global },\n };\n\n const { mode, slots } = this.dragging;\n const { model: room, ratio } = this.room;\n const options = {\n wallResizerActive: this.wallResizerActive,\n measurementsActive: this.measurementsActive,\n showPropping: this.itemContainerOptions.showPropping,\n };\n\n if (mode & constants.DRAG_MODE.MIXED) {\n const { offset, sprite } = this.dragging;\n const item = sprite.item;\n sprite.update({\n item: item,\n options,\n });\n\n const { data } = sprite;\n const defaultSizeScale = 1;\n const inTrash = this.isInTrashcan(e);\n\n emitter.emit(TRASH_CAN_HOVER, inTrash, !!item.itemid);\n\n let sizeScale = defaultSizeScale;\n if (inTrash && !item.itemid) {\n sizeScale = this.getNewItemTrashcanSizeScale();\n }\n\n const newPos = this.calcPos(data, e, offset, ratio, room);\n this.fitToGrid(newPos, mode, slots, round);\n\n const newItem = {\n ...item,\n x: newPos.x + this.itemContainer.origo.x,\n y: newPos.y,\n z: newPos.z,\n };\n\n if (mode & constants.DRAG_MODE.INSERT) {\n const collidingSlots = geometry\n .getCollidingRects(newItem, slots, snapPadding(mode, newItem))\n .filter(slot => !!slot.parent);\n\n if (collidingSlots.length > 0) {\n const slot = geometry.closestCollidingRect(\n geometry.pad(newItem, snapPadding(mode, newItem)),\n collidingSlots\n );\n\n Object.assign(newItem, {\n x: slot.x,\n y: slot.y,\n z: slot.z,\n width: slot.width,\n height: slot.height,\n depth: slot.depth,\n });\n\n if (\n sprite.parent === this.itemContainer ||\n newItem.x !== item.x ||\n newItem.y !== item.y ||\n tacHelpers.getParent(this.tac, item)?.itemid !== slot.parent.itemid\n ) {\n const parentSprite = this.getSprite(slot.parent.itemid);\n sizeScale = defaultSizeScale;\n sprite.scale.set(sizeScale);\n\n parentSprite.addSprite(sprite, slot, this.tac);\n\n this.adaptProppings(sprite.item, sprite);\n this.dragging.item = newItem;\n\n sprite.updateDependentItemsDragging &&\n sprite.updateDependentItemsDragging(sprite.item, slot.parent);\n this.render();\n }\n emitter.emit(ITEM_MOVING, this.tac, newItem);\n return;\n } else if (sprite.parent !== this.itemContainer) {\n const oldParent = this.itemContainer.getSprite(\n sprite.parentItem.itemid\n );\n\n const currentPos = {\n x: this.dragging.item.x,\n y: this.dragging.item.y,\n z: this.dragging.item.z,\n };\n Object.assign(\n this.dragging.item,\n this.dragging.originalItem,\n currentPos\n );\n sprite.update({\n item: this.dragging.item,\n options,\n parentItem: room,\n });\n this.itemContainer.addChild(sprite);\n\n if (oldParent && oldParent.adaptPropping) {\n oldParent.adaptPropping();\n }\n\n emitter.emit(ITEM_MOVING, this.tac, sprite.item);\n }\n }\n if (\n this.isInScene(e) &&\n !this.isInTrashcan(e) &&\n mode & constants.DRAG_MODE.FLOAT\n ) {\n if (mode & constants.DRAG_MODE.INSERT) {\n Object.assign(newItem, this.dragging.originalItem, {\n x: newItem.x,\n y: newItem.y,\n z: newItem.z,\n items:\n newItem.id === this.dragging.originalItem.id\n ? newItem.items\n : this.dragging.originalItem.items,\n });\n sprite.update({\n item: newItem,\n parentItem: room,\n options,\n });\n }\n\n if (isFloorStanding(item) && !isWallMounted(item)) {\n newItem.y = this.getSnapToFloorYPosition(item);\n }\n\n if (!slots.some(slot => geometry.contains(slot, newItem))) {\n if (mode === constants.DRAG_MODE.INSERT) {\n const collidingSlots = geometry.getCollidingRects(\n newItem,\n slots,\n snapPadding(mode, newItem)\n );\n if (collidingSlots.length !== 1) {\n return;\n }\n }\n\n Object.assign(\n newItem,\n geometry.closestPosition(\n newItem,\n slots.filter(slot => !slot.parent)\n )\n );\n }\n\n if (isWallMounted(newItem) && isFloorStanding(newItem)) {\n if (this.isPlacedOnSkirt(newItem)) {\n this.snapToSkirtOrFloor(newItem);\n }\n }\n this.fitToGrid(newItem, mode, slots);\n this.snapToPartner(newItem);\n }\n\n sprite.scale.set(sizeScale);\n sprite.x =\n (newItem.x - this.itemContainer.origo.x) * ratio -\n (data.scaledVertices[4][0] - data.scaledVertices[0][0]);\n sprite.y = (room.height - (data.faceSize.height + newItem.y)) * ratio;\n\n sprite.x += (sprite.width / sizeScale - sprite.width) / 2;\n sprite.y += (sprite.height / sizeScale - sprite.height) / 2;\n\n this.adaptProppings(newItem, sprite);\n this.dragging.item = newItem;\n if (sprite.updateDependentItemsDragging && this.hasMoved(newItem)) {\n sprite.updateDependentItemsDragging(newItem, sprite.parentItem);\n if (productsService.isType(newItem, ITEMS.SHELF_DRAWER)) {\n this.itemContainer.sortChildrenDragging(sprite, newItem, mode);\n }\n } else if (!sprite.updateDependentItemsDragging) {\n this.itemContainer.sortChildrenDragging(sprite, newItem, mode);\n }\n\n emitter.emit(ITEM_MOVING, this.tac, newItem);\n } else if (mode === constants.DRAG_MODE.MULTI) {\n const {\n supersection,\n slotPolygon,\n offset,\n item: grabbedItem,\n } = this.dragging;\n const data = this.dragging.sprite.data;\n\n const newPos = this.calcPos(data, e, offset, ratio, room);\n this.fitToGrid(newPos, constants.DRAG_MODE.FLOAT, [], round);\n\n const newItem = {\n ...grabbedItem,\n x: newPos.x + this.itemContainer.origo.x,\n y: newPos.y,\n };\n\n if (tacHelpers.hasTable({ items: supersection })) {\n newItem.y = grabbedItem.y;\n }\n\n if (this.hasMoved(newItem)) {\n const supersection = this.dragging.sprites.map(sprite => sprite.item);\n\n const recalced = supersection_recalc(\n supersection,\n grabbedItem,\n newItem,\n this,\n {\n lockedY: tacHelpers.hasTable({ items: supersection }),\n }\n );\n\n Object.assign(newItem, recalced.item);\n\n const ssPoly = geometry.rects2poly(recalced.items);\n if (polybool.intersect(ssPoly, slotPolygon).regions.length) {\n // ss collides with the walls or something on the scene, don't move it\n return;\n }\n\n const movedSupersection = supersection.map(item => ({\n ...item,\n ...supersection_relativePos(item, grabbedItem, newItem),\n }));\n\n supersection_drawSuggestions(movedSupersection, this);\n }\n this.dragging.item = newItem;\n }\n\n this.dragging.previousEventItem = Object.assign({}, this.dragging.item);\n\n this.render();\n }\n\n onDragEnd(e) {\n this.rectContainer.removeChildren();\n this.args.onDragEnd && this.args.onDragEnd();\n\n if (!this.dragging) {\n const cuttableMountingRailHintCheckPending =\n selectCuttableMountingRailHintCheckPending(store.getState());\n cuttableMountingRailHintCheckPending &&\n store.dispatch(\n thunkHandleMountingRailPopup({ omitExtendedConfClosedCheck: true })\n );\n return;\n }\n\n const { item, mode, slots, sprite } = this.dragging;\n\n sprite.clearCache && sprite.clearCache();\n\n if (mode & constants.DRAG_MODE.MIXED) {\n const noCrs = this.withoutClothesRails(item, this.crFilters.hidden);\n item.items = noCrs.item.items;\n\n if (\n (this.isInTrashcan(e) ||\n this.isInTrashcan(this.dragging.lastDragEventPos)) &&\n !e.abort\n ) {\n sprite.destroy();\n const originalItem = { ...this?.dragging?.originalItem };\n this.dragging = null;\n if (idGenerator.hasRealId(item) || item.forceRemoval) {\n const origin = 'drag';\n this.removeItem(originalItem || item, { origin: origin });\n } else {\n this.update(true);\n }\n return;\n }\n\n const isInScene =\n this.isInScene(e) || this.isInScene(this.dragging.lastDragEventPos);\n\n this.dragging = null;\n\n if (sprite.dummy) {\n sprite.destroy();\n }\n\n // dropping between swiper and scene can cause items to appear off floor.\n // only applies to float and not mixed mode\n\n const badFloorDrop =\n !isWallMounted(item) &&\n mode === constants.DRAG_MODE.FLOAT &&\n item.y > constants.PAD_HEIGHT &&\n !item.items?.some(item => item.y < 0);\n\n // for fixed scene, left corner is 0,0 so no drops outside wall.\n const badDropOutsideWall = !tacHelpers.isWithinWall(item, this.tac, true);\n\n if (!isInScene || e.abort || badDropOutsideWall || badFloorDrop) {\n if (mode === constants.DRAG_MODE.MIXED && sprite.dummy) {\n sprite.destroy();\n }\n this.update(true);\n } else if (mode & constants.DRAG_MODE.INSERT) {\n let collidingSlots = geometry.getCollidingRects(item, slots);\n\n if (collidingSlots.length > 0) {\n if (collidingSlots.some(slot => slot.parent)) {\n collidingSlots = collidingSlots.filter(slot => !!slot.parent);\n }\n const slot = geometry.closestCollidingRect(item, collidingSlots);\n if (slot.parent) {\n item.x = slot.x;\n item.y = slot.y;\n item.z = slot.z;\n item.width = slot.width;\n item.height = slot.height;\n item.depth = slot.depth;\n }\n\n const fittingProduct =\n productsService.getFit(item, {\n ...slot,\n width: item.width,\n depth: item.depth,\n height: item.height,\n color: item.filter.color,\n }) || productsService.getProduct(item.id);\n const fullFit = tacHelpers.getSwitchableItem(item, fittingProduct);\n const parentSprite =\n slot.parent && this.itemContainer.getSprite(slot.parent.itemid);\n const fosterParent =\n parentSprite &&\n parentSprite.getChildDisplacement &&\n parentSprite.getChildDisplacement(slot);\n\n const offset = tacHelpers.getGlobalCoords(\n fosterParent || slot.parent,\n this.tac\n ) || {\n x: 0,\n y: 0,\n z: 0,\n };\n\n const newItem = {\n ...item,\n ...fittingProduct,\n ...fullFit,\n x: item.x - offset.x,\n y: item.y - offset.y,\n z: item.z - offset.z,\n };\n\n if (slot.partnerSlot?.parent) {\n newItem.connectsTo = slot.partnerSlot.parent;\n }\n if (idGenerator.hasRealId(newItem)) {\n store.dispatch(\n thunkUpdateItem(newItem, fosterParent || slot.parent)\n );\n } else {\n store.dispatch(thunkAddItem(newItem, fosterParent || slot.parent));\n }\n } else {\n this.update(true);\n }\n } else if (idGenerator.hasRealId(item)) {\n store.dispatch(thunkUpdateItem(item, undefined, { tac: this.tac }));\n } else {\n store.dispatch(thunkAddItem(item, undefined, { tac: this.tac }));\n }\n } else if (mode & constants.DRAG_MODE.MULTI) {\n const isInScene =\n this.isInScene(e) || this.isInScene(this.dragging.lastDragEvent);\n if (e.abort || !isInScene) {\n this.dragging = null;\n this.update(true);\n } else {\n const ssItems = this.dragging.sprites.map(sprite => sprite.item);\n this.dragging = null;\n store.dispatch(thunkUpdateMultiple(ssItems));\n }\n }\n }\n\n hasMoved(item) {\n return (\n !this.dragging.previousEventItem ||\n !geometry.contains(item, this.dragging.previousEventItem)\n );\n }\n\n calcPos(data, event, offset, ratio, room) {\n const pos = {\n x:\n data.projectedVertices[4][0] -\n data.projectedVertices[0][0] +\n (event.data.global.x - offset.x) / ratio,\n\n y:\n room.height -\n ((event.data.global.y - offset.y) / ratio + data.faceSize.height),\n z: productsService.getInitialZPos(this.dragging?.item),\n };\n return pos;\n }\n\n getGrid(mode, item) {\n const dynamicGrid = constants.DYNAMIC_GRID && constants.DYNAMIC_GRID[mode];\n const gridType = dynamicGrid\n ? this.GRID_TYPES.DYNAMIC_GRID\n : this.GRID_TYPES.GRID;\n\n if (!dynamicGrid) {\n return { ...constants.GRID, gridType };\n }\n\n const rects = this.dragging.rects;\n if (!item || !rects) {\n return { ...dynamicGrid, gridType };\n }\n\n const closeRects = geometry.getCollidingRects(item, rects, {\n right: dynamicGrid.x.activationDistance,\n left: dynamicGrid.x.activationDistance,\n top: 5,\n bottom: 5,\n });\n /*\n ditch any objects that are covered by other objects; we only snap\n to stuff that can spawn sections\n */\n\n const usefulRects = closeRects.filter(\n rect => !geometry.obscured(rect, item, closeRects)\n );\n const closest = geometry.closestCollidingRect(item, usefulRects);\n if (!closest) {\n return { ...constants.GRID, gridType };\n }\n\n function getAdjustedGrid(dynamicGrid, origin) {\n const adjustedGrid = {\n x: { ...dynamicGrid.x },\n y: { ...dynamicGrid.y },\n };\n adjustedGrid.x.offset = origin.x % adjustedGrid.x.step;\n adjustedGrid.y.offset = origin.y % adjustedGrid.y.step;\n return adjustedGrid;\n }\n\n const fitted = usefulRects\n .map(rect => getAdjustedGrid(dynamicGrid, rect))\n .map(grid => ({\n ...item,\n x: this.getClosestGridPos(item.x, mode, 'x', round, grid),\n y: this.getClosestGridPos(item.y, mode, 'y', round, grid),\n grid,\n }));\n\n const closestFit = fitted.reduce(\n (curr, cand) => {\n if (geometry.distance(item, cand) < geometry.distance(item, curr)) {\n return cand;\n }\n return curr;\n },\n { x: Infinity, y: Infinity }\n );\n\n return { ...closestFit.grid, gridType };\n }\n\n getClosestGridPos(val, mode, axis, adjust, grid) {\n grid = grid || this.getGrid(mode);\n\n if (grid[axis].offset < 0) {\n adjust = ceil;\n }\n\n return adjust(val - grid[axis].offset, grid[axis].step) + grid[axis].offset;\n }\n\n fitToGrid(newItem, mode, slots, adjust = floor) {\n let grid = this.getGrid(mode, newItem);\n const gridSlots = slots.filter(\n slot => slot.width >= grid.x.step && slot.height >= grid.y.step\n );\n\n newItem.x = this.getClosestGridPos(newItem.x, mode, 'x', round, grid);\n newItem.y = this.getClosestGridPos(newItem.y, mode, 'y', adjust, grid);\n\n // search for a slot this many steps away (total steps both x and y)\n // The BOAXEL Uprights needing more retries to ensure that they don't get placed on top of each other\n let retries =\n mode === constants.DRAG_MODE.FLOAT &&\n grid.gridType === this.GRID_TYPES.DYNAMIC_GRID\n ? 50\n : 5;\n while (\n !slots.some(slot => geometry.contains(slot, newItem)) &&\n this.dragging &&\n geometry.contains(this.dragging.space, newItem, {\n left: grid.x.step,\n right: grid.x.step,\n top: grid.y.step,\n bottom: grid.y.step,\n }) &&\n retries--\n ) {\n grid = this.getGrid(mode, newItem);\n const closestPos = geometry.closestPosition(newItem, gridSlots);\n\n if (closestPos) {\n if (closestPos.x < newItem.x) {\n newItem.x = this.getClosestGridPos(\n newItem.x - grid.x.step,\n mode,\n 'x',\n adjust,\n grid\n );\n } else if (closestPos.x > newItem.x) {\n newItem.x = this.getClosestGridPos(\n newItem.x + grid.x.step,\n mode,\n 'x',\n adjust,\n grid\n );\n }\n\n if (closestPos.y < newItem.y) {\n newItem.y = this.getClosestGridPos(\n newItem.y - grid.y.step,\n mode,\n 'y',\n adjust,\n grid\n );\n } else if (closestPos.y > newItem.y) {\n newItem.y = this.getClosestGridPos(\n newItem.y + grid.y.step,\n mode,\n 'y',\n adjust,\n grid\n );\n }\n }\n }\n }\n\n /**\n * Gets the y-position to snap the item to.\n * @param {Object} item\n * @returns {number} The y-position that the item should be placed at to make it stand on the floor.\n */\n getSnapToFloorYPosition(item) {\n /*\n If a floor-standing item has children with a negative y,\n we need to account for that offset when \"flooring\"\n */\n const negativeChild = item.items && item.items.find(item => item.y < 0);\n return negativeChild ? -negativeChild.y : 0;\n }\n\n /**\n * Snaps the item to a partner item.\n * @example A section is placed close to another section, if its close enough it will snap to the closest section/partner.\n * @param {Object} newItem\n */\n snapToPartner(newItem) {\n const snappingPosition = tacHelpers.getSnappingPosition(\n newItem,\n this.dragging.rects\n );\n if (snappingPosition) {\n Object.assign(newItem, snappingPosition);\n }\n }\n\n /**\n * Snaps the item if its placed on the skirt area to either;\n * 1) The floor - If the item is placed below half of the skirt.\n * 2) The top of the skirt - If the item is placed above half of the skirt.\n * @param {Object} newItem\n */\n snapToSkirtOrFloor(newItem) {\n const snappingPosition = tacHelpers.getSkirtSnappingPosition(\n newItem,\n this.dragging.rects\n );\n if (snappingPosition) {\n Object.assign(newItem, snappingPosition);\n }\n }\n /**\n * Checks to see if the item is placed on the skirt or not.\n * @param {Object} item\n * @returns {Boolean} A value depending on whether the item is placed on the skirt or not.\n */\n isPlacedOnSkirt(item) {\n return item.y < constants.SKIRT_HEIGHT;\n }\n}\n","import React from 'react';\nimport classNames from 'classnames';\nimport styles from './ProductMenuCover.module.less';\nimport { Icon } from '../Icon';\nimport Transition from '../Transition';\nimport { useDispatch, useSelector } from 'react-redux';\nimport {\n selectIsMeasurementsActive,\n selectIsWallResizerActive,\n} from '../../state/scene/sceneSelectors';\nimport { actionSetWallResizerInactive } from '../../state/scene/sceneActions';\nimport { actionHideMeasurements } from '../../state/scene';\n\nexport interface Props {\n trashcan: boolean;\n show: boolean;\n enlarge: boolean;\n}\n\nconst ProductMenuCover = React.forwardRef(\n ({ trashcan, show, enlarge }, ref) => {\n ProductMenuCover.displayName = 'ProductMenuCover';\n const wallResizerActive = useSelector(selectIsWallResizerActive);\n const measurementsActive = useSelector(selectIsMeasurementsActive);\n const dispatch = useDispatch();\n const setWallResizerInactive = () =>\n dispatch(actionSetWallResizerInactive({}));\n const hideMeasurements = () => dispatch(actionHideMeasurements({}));\n\n const onClick = () => {\n wallResizerActive && setWallResizerInactive();\n measurementsActive && hideMeasurements();\n };\n\n return (\n \n \n
\n \n
\n
\n \n );\n }\n);\n\nexport default ProductMenuCover;\n","import React from 'react';\nimport { connect } from 'react-redux';\nimport PropTypes from 'prop-types';\nimport classNames from 'classnames';\n\nimport styles from './WallSizeInput.module.less';\nimport { translate } from '../../services/L10n';\nimport { t } from '../../translations';\nimport { metricToImperial, MM_PER_INCH } from '../../util/measures';\nimport { round } from '../../util/round';\nimport { asMm } from '../../util/measures';\nimport { selectUseMetric } from '../../state/dexfSettings/dexfSettingsSelectors';\nimport { selectDraggingWallResizer } from '../../state/scene/sceneSelectors';\nimport { actionSetDirtyConfiguration } from '../../state/vpc/vpcActions';\n\nconst INTEGER_REGEX = /(^\\d+$)|(^$)/;\n\nclass WallSizeInput extends React.Component {\n static propTypes = {\n min: PropTypes.number,\n max: PropTypes.number,\n val: PropTypes.number.isRequired,\n top: PropTypes.number.isRequired,\n left: PropTypes.number.isRequired,\n onComplete: PropTypes.func.isRequired,\n setDirtyConfig: PropTypes.func.isRequired,\n isHorizontal: PropTypes.bool,\n kiosk: PropTypes.bool,\n draggingWallResizer: PropTypes.bool,\n poppedOut: PropTypes.bool.isRequired,\n useMetricMeasures: PropTypes.bool.isRequired,\n };\n\n constructor(props) {\n super(props);\n this.onBlur = this.onBlur.bind(this);\n this.onComplete = this.onComplete.bind(this);\n this.onChange = this.onChange.bind(this);\n this.onFocus = this.onFocus.bind(this);\n this.onKeyUp = this.onKeyUp.bind(this);\n this.focusTimer = null;\n this.state = {\n values: WallSizeInput.getValues(props.val, true, props.useMetricMeasures),\n isFocused: false,\n };\n }\n\n componentWillUnmount() {\n clearTimeout(this.focusTimer);\n }\n\n static getDerivedStateFromProps(props, state) {\n if (!state.isFocused && asMm(state.values) !== props.val) {\n return {\n values: WallSizeInput.getValues(\n props.val,\n true,\n props.useMetricMeasures\n ),\n };\n }\n return null;\n }\n\n static getValues(val, isMM, useMetricMeasures) {\n const valueInMm = isMM ? val : asMm(val);\n if (useMetricMeasures) {\n return {\n imp: false,\n minor: !isMM && val.minor === '' ? '' : Math.round(valueInMm / 10),\n };\n }\n const imp = metricToImperial(round(valueInMm, MM_PER_INCH));\n\n return {\n imp: true,\n major: !isMM && val.major === '' ? '' : Number(imp.feet),\n minor:\n !isMM && val.minor === '' ? '' : Number(imp.inches.replace(/\\D/g, '')),\n };\n }\n\n onChange(source, event) {\n this.props.setDirtyConfig();\n if (!INTEGER_REGEX.test(event.target.value)) {\n return;\n }\n const newValues = { ...this.state.values };\n newValues[source] = event.target.value;\n\n this.setState({\n values: WallSizeInput.getValues(\n newValues,\n false,\n this.props.useMetricMeasures\n ),\n });\n }\n\n onFocus(event) {\n event.target.select();\n clearTimeout(this.focusTimer);\n this.setState({ isFocused: true });\n }\n\n onKeyUp(event) {\n if (event.key === 'Enter') {\n event.target.blur();\n }\n }\n\n render() {\n const values = this.state.values;\n return (\n \n {values.imp && (\n <>\n \n {translate(t.MEASURE_VALUE_FEET)}\n \n )}\n\n \n \n {this.props.useMetricMeasures\n ? translate(t.MEASURE_VALUE_CM)\n : translate(t.MEASURE_VALUE_IN)}\n \n \n );\n }\n\n onBlur() {\n this.onComplete();\n }\n\n onComplete() {\n this.focusTimer = setTimeout(() => {\n const clamped = Math.min(\n Math.max(asMm(this.state.values), this.props.min || -Infinity),\n this.props.max || Infinity\n );\n\n this.props.onComplete(clamped);\n\n this.setState({\n values: WallSizeInput.getValues(\n clamped,\n true,\n this.props.useMetricMeasures\n ),\n isFocused: false,\n });\n }, 50);\n }\n}\n\nexport default connect(\n state => ({\n draggingWallResizer: selectDraggingWallResizer(state),\n useMetricMeasures: selectUseMetric(state),\n }),\n dispatch => ({\n setDirtyConfig: () => dispatch(actionSetDirtyConfiguration(true)),\n })\n)(WallSizeInput);\n","function addBodyClass() {\n document.body.classList.add('drag-in-progress');\n}\n\nfunction removeBodyClass() {\n document.body.classList.remove('drag-in-progress');\n}\n\nexport default {\n addBodyClass,\n removeBodyClass,\n};\n","import React from 'react';\nimport InlineMessage from '@ingka/inline-message';\nimport infoIcon from '@ingka/ssr-icon/paths/information-circle';\nimport warningIcon from '@ingka/ssr-icon/paths/warning-triangle';\nimport PropTypes from 'prop-types';\nimport { connect } from 'react-redux';\nimport platform from '../../util/platform';\nimport { translate } from '../../services/L10n';\nimport { closeSupplyBanner } from '../../state/popups';\nimport {\n selectIsSupplyBannedEnabled,\n selectSupplyBannerMessageType,\n selectSupplyBannerLinkURL,\n} from '../../state/dexfSettings/dexfSettingsSelectors';\nimport styles from './SupplyBanner.module.less';\nimport { t } from '../../translations';\n\nclass SupplyBanner extends React.Component {\n static propTypes = {\n userAgent: PropTypes.object.isRequired,\n supplyBannerClosed: PropTypes.bool.isRequired,\n closeSupplyBanner: PropTypes.func.isRequired,\n isSupplyBannerEnabled: PropTypes.bool.isRequired,\n supplyBannerMessageType: PropTypes.string.isRequired,\n supplyBannerLinkURL: PropTypes.string,\n };\n constructor(props) {\n super(props);\n this.events = {\n onClose: () => this.props.closeSupplyBanner(),\n };\n this.state = {};\n }\n\n getTitle = () =>\n platform.isKiosk\n ? translate(t.PLANNER_BANNER_MESSAGE_TITLE_KIOSK)\n : translate(t.PLANNER_BANNER_MESSAGE_TITLE);\n\n getBody = () => {\n const isMobile = this.props.userAgent.isMobile;\n const isKiosk = platform.isKiosk;\n\n const bodyTextDesktop = translate(t.PLANNER_BANNER_MESSAGE_BODY);\n const bodyTextKiosk = translate(t.PLANNER_BANNER_MESSAGE_BODY_KIOSK);\n\n const link = {\n href: this.props.supplyBannerLinkURL,\n label: this.props.supplyBannerLinkURL\n ? translate(t.PLANNER_BANNER_MESSAGE_LINK_TEXT)\n : undefined,\n };\n\n if (isMobile) return { text: null, link };\n else if (isKiosk) return { text: bodyTextKiosk, link: undefined };\n else return { text: bodyTextDesktop, link };\n };\n\n getLayout = () => {\n switch (this.props.supplyBannerMessageType) {\n case 'Cautionary':\n return { variant: 'cautionary', icon: warningIcon };\n case 'Information':\n default:\n return { variant: 'informative', icon: infoIcon };\n }\n };\n\n shouldBannerBeDisplayed = () => {\n const { isSupplyBannerEnabled } = this.props;\n const bannerClosed = this.props.supplyBannerClosed;\n\n return isSupplyBannerEnabled && !bannerClosed;\n };\n render() {\n return this.shouldBannerBeDisplayed() ? (\n this.events.onClose()}\n />\n ) : null;\n }\n}\nexport default connect(\n state => ({\n userAgent: state.userAgent,\n supplyBannerClosed: state.popups.supplyBannerClosed,\n isSupplyBannerEnabled: selectIsSupplyBannedEnabled(state),\n supplyBannerMessageType: selectSupplyBannerMessageType(state),\n supplyBannerLinkURL: selectSupplyBannerLinkURL(state),\n }),\n dispatch => ({\n closeSupplyBanner: () => dispatch(closeSupplyBanner()),\n })\n)(SupplyBanner);\n","import React from 'react';\nimport styles from './JoyRide.module.less';\n\ninterface EventObject {\n condition: () => boolean;\n func: () => any;\n}\n\nexport interface Props {\n queue: EventObject[];\n children: any;\n}\n\nexport const JoyRide: React.FunctionComponent = ({\n queue,\n children,\n}) => {\n const [queueIndex, setQueueIndex] = React.useState(0);\n\n /**\n * Checks if all events have been set to listeners\n * @returns {boolean}\n */\n const isDone = () => queue.length - 1 < queueIndex;\n\n /**\n * Checks if condition for current index is met\n * @returns {*}\n */\n const currentConditionMet = () => queue[queueIndex]?.condition();\n\n /**\n * Check if next event is ready to be set up\n * @returns {*|boolean}\n */\n const nextEventReady = () => !isDone() && currentConditionMet();\n\n /**\n * Set check for fired event and run event\n */\n const runQueueFunction = () => {\n if (nextEventReady()) {\n queue[queueIndex].func();\n setQueueIndex(queueIndex + 1);\n }\n };\n\n return (\n
\n {children}\n
\n );\n};\n\nexport default JoyRide;\n","/* eslint no-unused-expressions: 0 */\nimport React, { FunctionComponent } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport classNames from 'classnames';\nimport Footer from '../../components/Footer/Footer';\nimport TopBar from '../../components/TopBar/TopBar';\nimport ProductMenu from '../../components/ProductMenu';\nimport ScenePopupManager from '../../components/ScenePopupManager';\nimport Scene from '../../scene';\nimport ProductMenuCover from '../../components/ProductMenuCover';\nimport tacHelpers from '../../state/tac/tacHelpers';\nimport WallSizeInput from '../../components/WallSizeInput/WallSizeInput';\nimport platform from '../../util/platform';\nimport {\n DROP_ITEM,\n IFRAME_RESIZED,\n TRASH_CAN_HOVER,\n WALL_RESIZED,\n} from '../../settings/events';\nimport draggingUtil from '../../util/dragging';\nimport * as supportedEvents from '../../util/supportedEvents';\nimport emitter from '../../emitter';\nimport constants from '../../settings/constants';\nimport { SCENE_VIEW_ID } from '../../constants';\nimport {\n selectIsWallResizerActive,\n selectSceneSheetVariant,\n selectShowSceneSheet,\n} from '../../state/scene/sceneSelectors';\nimport { selectTac, selectWallSize } from '../../state/tac/tacSelectors';\nimport styles from './SceneView.module.less';\nimport SupplyBanner from '../../components/SupplyBanner/SupplyBanner';\nimport {\n selectIsKiosk,\n selectIsMobile,\n selectIsMobilePortrait,\n selectIsTabletPortrait,\n} from '../../state/userAgent/userAgentSelectors';\nimport { findDOMNode } from 'react-dom';\nimport { actionHideSceneSheet } from '../../state/scene';\nimport { Point } from '../../generalTypes';\nimport JoyRide from '../../components/JoyRide/JoyRide';\nimport { selectFilterIntroPopupVisible } from '../../state/popups/popupsSelectors';\nimport { selectInitSuccessful } from '../../state/init/initSelectors';\nimport {\n actionHideIntroPopups,\n actionSetFilterIntroPopupBeVisible,\n} from '../../state/popups/popupsActions';\nimport { thunkSetSceneRect } from '../../state/scene/sceneThunks';\nimport { thunkSetWall } from '../../state/tac/tacThunks';\nimport store from '../../state';\nimport { SheetObject } from '../../state/sheets/sheetTypes';\nimport { actionEnqueueSheet } from '../../state/sheets/sheetActions';\n\nconst { MOUSE_SUPPORT, POINTER_SUPPORT, TOUCH_SUPPORT } = supportedEvents;\n\nexport type Props = {\n buildScene: any;\n captureScene: any;\n};\n\nconst SceneView: FunctionComponent = ({ buildScene, captureScene }) => {\n type Meta = { origin: string; isPersistent: true };\n\n const root = React.useRef(null);\n const canvasContainer = React.useRef(null);\n const productMenuCover = React.useRef(null);\n const leftContainer = React.useRef(null);\n const scene = React.useRef(null);\n\n const [isDragging, setIsDragging] = React.useState(false);\n const [showTrashcan, setShowTrashcan] = React.useState(false);\n const [enlargeTrashcan, setEnlargeTrashcan] = React.useState(false);\n const [sceneAvailable, setSceneAvailable] = React.useState(false);\n const [topWallResizerInputPosition, setTopWallResizerInputPosition] =\n React.useState<{ x: number; y: number } | null>(null);\n const [rightWallResizerInputPosition, setRightWallResizerInputPosition] =\n React.useState<{ x: number; y: number } | null>(null);\n const wallSize = useSelector(selectWallSize);\n\n const isMobile = useSelector(selectIsMobile);\n const isMobilePortrait = useSelector(selectIsMobilePortrait);\n const isTabletPortrait = useSelector(selectIsTabletPortrait);\n const isKiosk = useSelector(selectIsKiosk);\n const tac = useSelector(selectTac);\n const wallResizerActive = useSelector(selectIsWallResizerActive);\n const filterIntroPopupsVisible = useSelector(selectFilterIntroPopupVisible);\n const initSuccessful = useSelector(selectInitSuccessful);\n const showingSceneSheet = useSelector(selectShowSceneSheet);\n const sceneSheetVariant = useSelector(selectSceneSheetVariant);\n\n const dispatch = useDispatch();\n const setWall = (\n { width, height }: { width?: number; height?: number },\n meta: Meta\n ) => dispatch(thunkSetWall({ width, height }, meta));\n\n const _getRects = () => {\n if (!root.current || !canvasContainer.current)\n throw new Error('Some refs are not defined.');\n const view = root.current.getBoundingClientRect();\n const sceneDimensions = canvasContainer.current.getBoundingClientRect();\n // eslint-disable-next-line react/no-find-dom-node\n const trashcan = findDOMNode(\n productMenuCover.current\n // @ts-ignore\n )?.getBoundingClientRect();\n\n const viewRect = {\n x: view.left,\n y: view.top,\n width: view.width,\n height: view.height,\n };\n\n const sceneRect = {\n // the -1 is to account for the border around the app\n x: sceneDimensions.left - viewRect.x,\n y: sceneDimensions.top - viewRect.y,\n width: sceneDimensions.width,\n height: sceneDimensions.height,\n };\n // @ts-ignore\n store.dispatch(thunkSetSceneRect(sceneRect));\n\n const trashcanRect = {\n x: trashcan.left - viewRect.x,\n y: trashcan.top - viewRect.y,\n width: trashcan.width,\n height: trashcan.height,\n };\n\n return {\n viewRect,\n trashcanRect,\n };\n };\n\n const onDragStart = () => {\n draggingUtil.addBodyClass();\n setIsDragging(true);\n };\n\n const onDragEnd = () => {\n draggingUtil.removeBodyClass();\n setIsDragging(false);\n setShowTrashcan(false);\n setEnlargeTrashcan(false);\n };\n\n const stopDrag = (e: any) => {\n e = e.nativeEvent || e;\n\n const pixiEvent = {\n data: {\n global: {\n x: e.pageX,\n y: e.pageY,\n },\n },\n };\n\n emitter.emit(DROP_ITEM, pixiEvent);\n };\n\n const abortDrag = () => emitter.emit(DROP_ITEM, { abort: true });\n\n const resize = () => scene.current?.resizeScene(_getRects());\n\n const onTrashcanHover = (hover: boolean, fromScene: boolean) => {\n const updatedEnlargeTrashcan = hover && fromScene;\n const updatedShowTrashcan = !hover || fromScene;\n\n if (\n updatedEnlargeTrashcan !== enlargeTrashcan ||\n updatedShowTrashcan !== updatedEnlargeTrashcan\n ) {\n setEnlargeTrashcan(updatedEnlargeTrashcan);\n setShowTrashcan(updatedShowTrashcan);\n }\n };\n\n const setScene = () => (buildScene ? activateBuildScene() : generateScene());\n\n const prepScene = () => {\n if (!scene.current) return;\n\n const sceneCanvas = scene.current.view;\n const measurementsCanvas = scene.current.measurementLayer.view;\n\n sceneCanvas.className = styles.sceneCanvas;\n measurementsCanvas.className = styles.measurementLayer;\n\n canvasContainer.current?.appendChild(sceneCanvas);\n root.current?.appendChild(measurementsCanvas);\n };\n\n const preventScrollOnTouchMove = () => {\n root.current?.addEventListener(\n 'touchmove',\n event => {\n event.preventDefault();\n },\n { passive: false }\n );\n };\n\n const activateBuildScene = () => {\n if (isKiosk) {\n preventScrollOnTouchMove();\n }\n scene.current = buildScene;\n scene.current?.activate({ onDragStart, onDragEnd });\n };\n\n const generateScene = () => {\n if (isKiosk) {\n preventScrollOnTouchMove();\n }\n return (scene.current = new Scene({\n onDragStart,\n onDragEnd,\n ..._getRects(),\n }));\n };\n\n React.useEffect(() => {\n emitter.on(DROP_ITEM, onDragEnd);\n emitter.on(IFRAME_RESIZED, resize);\n emitter.on(TRASH_CAN_HOVER, onTrashcanHover);\n emitter.on(WALL_RESIZED, onWallResized);\n\n setScene();\n prepScene();\n setSceneAvailable(true);\n\n return () => {\n emitter.off(IFRAME_RESIZED, resize);\n emitter.off(TRASH_CAN_HOVER, onTrashcanHover);\n emitter.off(DROP_ITEM, onDragEnd);\n emitter.off(WALL_RESIZED, onWallResized);\n\n scene.current && scene.current.deactivate();\n dispatch(actionHideSceneSheet());\n onDragEnd();\n };\n }, []);\n\n const onNextViewClick = () => captureScene(scene.current);\n\n const forwardEvent = (e: any) => {\n const ev = e.nativeEvent || e;\n\n ev.stopPropagation();\n\n if (ev.cancelable) ev.preventDefault();\n\n const event = new ev.constructor(ev.type, ev);\n\n scene.current?.view.dispatchEvent(event);\n };\n\n const abortIfTouchExit = (event: any) => {\n const { pageX, pageY } = event.touches[0];\n const { width, height } = _getRects().viewRect;\n\n if (pageX < width && pageX > 0 && pageY < height && pageY > 0) {\n return;\n }\n abortDrag();\n };\n\n const shouldPopOut = (dimension: string, measurement: number) =>\n isMobile && measurement < constants.WALL[dimension].max * 0.5;\n\n const getTopWallResizerInputPosition = (\n wallSizeHeight: number,\n x1: number,\n x2: number,\n y1: number\n ) => ({\n x: isMobile ? x1 - 1 : x1 + (x2 - x1) / 2,\n y: y1 - +!shouldPopOut('height', wallSizeHeight),\n });\n\n const getRightWallResizerInputPosition = (\n wallSizeWidth: number,\n x2: number,\n y2: number,\n y3: number\n ) => ({\n x: x2 + +!shouldPopOut('width', wallSizeWidth),\n y: isMobile ? y3 + 1 : y2 + (y3 - y2) / 2,\n });\n\n /**\n * Returns que for joy ride\n * @returns EventObject[]\n */\n const getQueue = () => [\n {\n condition: () => initSuccessful && !showingSceneSheet,\n func: () => dispatch(actionHideIntroPopups()),\n },\n {\n condition: () => filterIntroPopupsVisible,\n func: () => dispatch(actionSetFilterIntroPopupBeVisible(false)),\n },\n ];\n\n const onWallResized = ({ points }: { points: Point[] }) => {\n const [{ x: x1, y: y1 }, { x: x2, y: y2 }, { y: y3 }] = points;\n\n setTopWallResizerInputPosition(\n getTopWallResizerInputPosition(wallSize.height, x1, x2, y1)\n );\n setRightWallResizerInputPosition(\n getRightWallResizerInputPosition(wallSize.width, x2, y2, y3)\n );\n };\n\n const onComplete = (type: string) => (value: number) =>\n setWall({ [type]: value }, { origin: 'type', isPersistent: true });\n\n const getWallResizerInput = () => {\n const limits = tacHelpers.getWallResizingLimits(tac);\n\n if (!topWallResizerInputPosition || !rightWallResizerInputPosition) return;\n\n const { y: tY, x: tX } = topWallResizerInputPosition;\n const { y: rY, x: rX } = rightWallResizerInputPosition;\n\n return (\n <>\n \n \n \n );\n };\n\n const renderWallResizerInput = () =>\n rightWallResizerInputPosition &&\n topWallResizerInputPosition &&\n wallResizerActive &&\n getWallResizerInput();\n\n const renderScenePopupManager = () =>\n sceneAvailable && (\n \n );\n\n const renderProductMenu = () =>\n !(isMobilePortrait || isTabletPortrait) && (\n \n );\n\n const renderPortraitProductMenu = () =>\n (isMobilePortrait || isTabletPortrait) && (\n
\n \n
\n );\n\n /**\n * Render sheet\n */\n const showSheet = () => {\n const sheet: SheetObject = {\n sheetType: sceneSheetVariant,\n };\n showingSceneSheet && dispatch(actionEnqueueSheet(sheet));\n };\n\n return (\n \n {showSheet()}\n \n {renderWallResizerInput()}\n
\n \n \n \n
\n\n {renderPortraitProductMenu()}\n