UNPKG

@openui5/sap.m

Version:

OpenUI5 UI Library sap.m

725 lines (632 loc) 21.8 kB
/*! * UI development toolkit for HTML5 (OpenUI5) * (c) Copyright 2009-2022 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ sap.ui.define([ './library', 'sap/ui/core/Control', 'sap/ui/core/Popup', 'sap/m/Text', 'sap/m/Button', 'sap/ui/core/ResizeHandler', 'sap/ui/Device', 'sap/ui/core/Icon', 'sap/ui/layout/VerticalLayout', './InstanceManager', 'sap/ui/core/InvisibleText', 'sap/ui/core/library', './LightBoxRenderer', "sap/ui/thirdparty/jquery" ], function( library, Control, Popup, Text, Button, ResizeHandler, Device, Icon, VerticalLayout, InstanceManager, InvisibleText, coreLibrary, LightBoxRenderer, jQuery ) { 'use strict'; // shortcut for sap.ui.core.TextAlign var TextAlign = coreLibrary.TextAlign; // shortcut for sap.m.ButtonType var ButtonType = library.ButtonType; // shortcut for sap.m.LightBoxLoadingStates var LightBoxLoadingStates = library.LightBoxLoadingStates; /** * Constructor for a new LightBox. * * @param {string} [sId] ID for the new control, generated automatically if no ID is given * @param {object} [mSettings] Initial settings for the new control * * @class * Represents a popup containing an image and a footer. * * <h3>Overview</h3> * * The purpose of the control is to display an image in its original size as long as this is possible. * On smaller screens images are scaled down to fit. * * <strong>Notes:</strong> * <ul> * <li>If the image doesn't load in 10 seconds, an error is displayed.</li> * <li>Setting the <code>imageContent</code> aggregation of the control as well as the source of the image and the title of the image is <u>mandatory</u>. * If the image source is not set, the control will not open.</li> * </ul> * <h3>Structure</h3> * * Each LightBox holds a {@link sap.m.LightBoxItem LightBoxItem} which keeps the properties of the image: * <ul> * <li> imageSrc - The source URI of the image </li> * <li> title - The title of the image </li> * <li> subtitle - The subtitle of the image </li> * <li> alt - The alt text of the image </li> * </ul> * <h3>Usage</h3> * * The most common use case is to click on an image thumbnail to view it in bigger size. * When the image that should be displayed in the control cannot be loaded, an error is displayed in the popup. * * <h3>Responsive Behavior</h3> * * On a mobile device, flipping the device to landscape will flip the lightbox and the image will be adjusted to fit the new dimensions. * * <h3>Additional Information</h3> * * Check out the <a href="/#docs/api/symbols/sap.m.LightBox.html" >API Reference</a>. * @extends sap.ui.core.Control * * @author SAP SE * @version 1.60.39 * * @constructor * @public * @alias sap.m.LightBox * @see {@link fiori:https://experience.sap.com/fiori-design-web/lightbox/ Light Box} * @ui5-metamodel This control/element also will be described in the UI5 (legacy) designtime metamodel */ var LightBox = Control.extend('sap.m.LightBox', /** @lends sap.m.LightBox.prototype */ { metadata: { interfaces: [ 'sap.ui.core.PopupInterface' ], library: 'sap.m', aggregations: { /** * Aggregation which holds data about the image and its description. Although multiple LightBoxItems * may be added to this aggregation only the first one in the list will be taken into account. * @public */ imageContent: {type: 'sap.m.LightBoxItem', multiple: true, bindable: "bindable"}, /** * The close button aggregation inside the LightBox control. This button has to have text in it. * @private */ _closeButton: {type: 'sap.m.Button', multiple: false, visibility: 'hidden'}, /** * The error icon displayed when the image could not be loaded in time. * @private */ _errorIcon: {type: 'sap.ui.core.Icon', multiple: false, visibility: 'hidden'}, /** * The main error message displayed when the image could not be loaded. * @private */ _errorTitle: {type: 'sap.m.Text', multiple: false, visibility: 'hidden'}, /** * The detailed error message displayed when the image could not be loaded. * @private */ _errorSubtitle: {type: 'sap.m.Text', multiple: false, visibility: 'hidden'}, /** * A layout control used to display the error texts when the image could not be loaded. * @private */ _verticalLayout: {type: 'sap.ui.layout.VerticalLayout', multiple: false, visibility: 'hidden'}, /** * Hidden text used for accessibility of the popup. * @private */ _invisiblePopupText: {type: "sap.ui.core.InvisibleText", multiple: false, visibility: "hidden"}, /** * BusyIndicator for loading state. * @private */ _busy: {type: "sap.m.BusyIndicator", multiple: false, visibility: "hidden"} }, events: {}, defaultAggregation: 'imageContent', designtime: "sap/m/designtime/LightBox.designtime" } }); //================================================================================ // Lifecycle methods //================================================================================ /** * Sets up the initial values of the control. * * @protected */ LightBox.prototype.init = function () { this._createPopup(); this._width = 0; //to be calculated later this._height = 0; //to be calculated later this._isRendering = true; this._resizeListenerId = null; this._$lightBox = null; this._rb = sap.ui.getCore().getLibraryResourceBundle("sap.m"); this._closeButtonText = this._rb.getText("LIGHTBOX_CLOSE_BUTTON"); // create an ARIA announcement for enlarged image if (sap.ui.getCore().getConfiguration().getAccessibility()) { this.setAggregation("_invisiblePopupText", new InvisibleText()); } }; /** * Overwrites the onBeforeRendering. * * @public */ LightBox.prototype.onBeforeRendering = function () { var oImageContent = this._getImageContent(), oNativeImage = oImageContent._getNativeImage(), sImageSrc = oImageContent.getImageSrc(), sState = oImageContent._getImageState(); this._createErrorControls(); // Prevents image having 0 width and height when the LightBox rendered // busy state first and then loaded the image in the meantime if (oNativeImage.getAttribute('src') !== sImageSrc) { oNativeImage.src = sImageSrc; } if (this._resizeListenerId) { Device.resize.detachHandler(this._onResizeHandler); ResizeHandler.deregister(this._resizeListenerId); this._resizeListenerId = null; } switch (sState) { case LightBoxLoadingStates.Loading: this._timeoutId = setTimeout(function () { oImageContent._setImageState(LightBoxLoadingStates.TimeOutError); }, 10000); break; case LightBoxLoadingStates.Loaded: clearTimeout(this._timeoutId); this._calculateSizes(oNativeImage); break; case LightBoxLoadingStates.Error: clearTimeout(this._timeoutId); break; default: break; } var oInvisiblePopupText = this.getAggregation('_invisiblePopupText'); if (oImageContent && oInvisiblePopupText) { oInvisiblePopupText.setText(this._rb.getText("LIGHTBOX_ARIA_ENLARGED", oImageContent.getTitle())); } this._isRendering = true; }; /** * Overwrites the onAfterRendering. * * @public */ LightBox.prototype.onAfterRendering = function () { this._isRendering = false; this._$lightBox = this.$(); if (!this._resizeListenerId) { this._onResizeHandler = this._onResize.bind(this); Device.resize.attachHandler(this._onResizeHandler); this._resizeListenerId = ResizeHandler.register(this, this._onResizeHandler); } }; LightBox.prototype.forceInvalidate = Control.prototype.invalidate; /** * Rerenders the LightBox. * * @public * @param {object} oOrigin Origin of the invalidation. * @returns {sap.m.LightBox} this LightBox reference for chaining. */ LightBox.prototype.invalidate = function (oOrigin) { var oImageContent = this._getImageContent(); if (this.isOpen()) { if (oImageContent && oImageContent.getImageSrc()) { this.forceInvalidate(oOrigin); } else { this.close(); } } return this; }; /** * Detaches all handlers and destroys the instance. * * @public */ LightBox.prototype.exit = function () { if (this._oPopup) { this._oPopup.detachOpened(this._fnOpened, this); this._oPopup.detachClosed(this._fnClosed, this); this._oPopup.destroy(); this._oPopup = null; } if (this._resizeListenerId) { Device.resize.detachHandler(this._onResizeHandler); ResizeHandler.deregister(this._resizeListenerId); this._resizeListenerId = null; } InstanceManager.removeLightBoxInstance(this); }; //================================================================================ // Control methods //================================================================================ /** * Opens the LightBox. * * @public * @returns {sap.m.LightBox} Pointer to the control instance for chaining. * @ui5-metamodel This method also will be described in the UI5 (legacy) designtime metamodel */ LightBox.prototype.open = function () { /** @type {sap.m.LightBoxItem} */ var imageContent = this._getImageContent(); this._oPopup.setContent(this); if (imageContent && imageContent.getImageSrc()) { this._oPopup.open(300, 'center center', 'center center', document.body, null); InstanceManager.addLightBoxInstance(this); } return this; }; /** * Returns if the LightBox is open. * * @public * @returns {boolean} Is the LightBox open */ LightBox.prototype.isOpen = function() { if (this._oPopup && this._oPopup.isOpen()) { return true; } return false; }; /** * Closes the LightBox. * * @public * @returns {sap.m.LightBox} Pointer to the control instance for chaining. * @ui5-metamodel This method also will be described in the UI5 (legacy) designtime metamodel */ LightBox.prototype.close = function () { if (this._resizeListenerId) { Device.resize.detachHandler(this._onResizeHandler); ResizeHandler.deregister(this._resizeListenerId); this._resizeListenerId = null; } this._oPopup.close(); InstanceManager.removeLightBoxInstance(this); return this; }; //================================================================================ // Private methods //================================================================================ /** * Instantiates (if not defined) and returns the close button for the LightBox. * * @private * @returns {sap.m.Button} The close button. */ LightBox.prototype._getCloseButton = function () { var closeButton = this.getAggregation('_closeButton'); if (!closeButton) { closeButton = new Button({ id: this.getId() + '-closeButton', text: this._closeButtonText, type: ButtonType.Transparent, press: function () { this.close(); }.bind(this) }); this.setAggregation('_closeButton', closeButton, true); } return closeButton; }; /** * Instantiates (if not defined) and returns the BusyIndicator for the LightBox. * * @private * @returns {sap.m.BusyIndicator} The BusyIndicator displayed while the image is loading. */ LightBox.prototype._getBusyIndicator = function () { var busyIndicator = this.getAggregation("_busy"); if (!busyIndicator) { busyIndicator = new sap.m.BusyIndicator(); this.setAggregation("_busy", busyIndicator, true); } return busyIndicator; }; /** * Forces rerendering of the control when an image loads/fails to load. * * @private * @param {string} newState The new state of the image. Possible values are: "LOADING", "LOADED" and "ERROR". */ LightBox.prototype._imageStateChanged = function (newState) { var stateUnfinished = newState === LightBoxLoadingStates.Loaded || newState === LightBoxLoadingStates.Error; if (stateUnfinished && !this._isRendering) { this.rerender(); } }; /** * Creates the popup in which the LightBox is displayed and adds event handlers. Event handlers are necessary * to close the popup when the user clicks on the overlay around the popup. * * @private */ LightBox.prototype._createPopup = function () { this._oPopup = new Popup(this, true, true); this._oPopup.attachOpened(this._fnOpened, this); this._oPopup.attachClosed(this._fnClosed, this); }; /** * Adds event listener to the blocklayer area to close the lightbox when the area is clicked. * * @private */ LightBox.prototype._fnOpened = function() { var that = this; that._onResize(); jQuery('#sap-ui-blocklayer-popup').on("click", function() { that.close(); }); }; /** * Removes the event listener. * * @private */ LightBox.prototype._fnClosed = function() { jQuery('#sap-ui-blocklayer-popup').off("click"); }; /** * Creates the controls used to display error state of the LightBox. * * @private */ LightBox.prototype._createErrorControls = function() { var resourceBundle = this._rb; var errorMessageTitle; var errorMessageSubtitle; if (this._getImageContent()._getImageState() === LightBoxLoadingStates.TimeOutError) { errorMessageTitle = resourceBundle.getText('LIGHTBOX_IMAGE_TIMED_OUT'); errorMessageSubtitle = resourceBundle.getText('LIGHTBOX_IMAGE_TIMED_OUT_DETAILS'); } else { errorMessageTitle = resourceBundle.getText('LIGHTBOX_IMAGE_ERROR'); errorMessageSubtitle = resourceBundle.getText('LIGHTBOX_IMAGE_ERROR_DETAILS'); } if (!this.getAggregation('_verticalLayout')) { var errorTitle = new Text({ text : errorMessageTitle, textAlign : TextAlign.Center }).addStyleClass("sapMLightBoxErrorTitle"), errorSubtitle = new Text({ text : errorMessageSubtitle, textAlign : TextAlign.Center }).addStyleClass("sapMLightBoxErrorSubtitle"), errorIcon = new Icon({ src : "sap-icon://picture" }).addStyleClass("sapMLightBoxErrorIcon"); this.setAggregation('_verticalLayout', new VerticalLayout({ content : [ errorIcon, errorTitle, errorSubtitle] }).addStyleClass('sapMLightBoxVerticalLayout')); } }; /** * Handles the resize of the LightBox (usually caused by window resize). * * @private */ LightBox.prototype._onResize = function () { var minimumSideOffset = calculateOffset() / 2 + 'px', top = minimumSideOffset, left = minimumSideOffset, marginTop = '', marginLeft = '', oImageContent = this._getImageContent(), lightBoxContainer = this.getDomRef(), lightBoxWidth, lightBoxHeight, minimumOffset = calculateOffset(), hcbBorderSize = 2; if (oImageContent._getImageState() === LightBoxLoadingStates.Loaded) { this._calculateSizes(oImageContent._getNativeImage()); lightBoxWidth = this._width; lightBoxHeight = this._height; this._$lightBox.width(lightBoxWidth); this._$lightBox.height(lightBoxHeight); } else { lightBoxWidth = lightBoxContainer.clientWidth; lightBoxHeight = lightBoxContainer.clientHeight; } if (window.innerWidth > lightBoxWidth + minimumOffset) { left = '50%'; marginLeft = Math.round(-lightBoxWidth / 2); } if (window.innerHeight > lightBoxHeight + minimumOffset) { top = '50%'; marginTop = Math.round(-lightBoxHeight / 2); } if (sap.ui.getCore().getConfiguration().getTheme() === 'sap_hcb') { marginTop -= hcbBorderSize; marginLeft -= hcbBorderSize; } this._$lightBox.css({ 'top' : top, 'margin-top' : marginTop, 'left' : left, 'margin-left' : marginLeft }); }; /** * Calculates the target size of the image and the lightbox based on the size of the image that will be loaded. * * @private * @param {window.Image} internalImage The javascript native object referring to the image that will be loaded. */ LightBox.prototype._calculateSizes = function (internalImage) { var iFooterHeightPx = this._calculateFooterHeightInPx(), imageMinHeight = 288 - iFooterHeightPx, // 18rem * 16px = 288px image = this._getImageContent().getAggregation("_image"), height; this._setImageSize(image, internalImage.naturalWidth, internalImage.naturalHeight); this._calculateAndSetLightBoxSize(image); height = this._pxToNumber(image.getHeight()); this.toggleStyleClass('sapMLightBoxMinSize', (height < imageMinHeight)); this._isBusy = false; }; /** * Calculates the height of the footer of the LightBox in pixels. * * @private * @returns {int} The height of the footer. */ LightBox.prototype._calculateFooterHeightInPx = function () { var compact = this.$().parents().hasClass('sapUiSizeCompact'); var subtitle = this._getImageContent().getSubtitle(); var footerHeightRem = 2.5; // base height of the footer in rem if (!compact) { footerHeightRem += 0.5; } if (subtitle) { footerHeightRem += 1.5; } return footerHeightRem * 16; // 1rem == 16px }; /** * Calculates and sets in private properties the width and height of the LightBox. * * @private * @param {sap.m.Image} image The image of the LightBoxItem. */ LightBox.prototype._calculateAndSetLightBoxSize = function (image) { var imageHeight, imageWidth, lightBoxMinWidth = (20 /*rem*/ * 16 /*px*/), lightBoxMinHeight = (18 /*rem*/ * 16 /*px*/), iFooterHeightPx = this._calculateFooterHeightInPx(); imageHeight = this._pxToNumber(image.getHeight()); imageWidth = this._pxToNumber(image.getWidth()); this._width = Math.max(lightBoxMinWidth, imageWidth); this._height = Math.max(lightBoxMinHeight, imageHeight + iFooterHeightPx); this._isLightBoxBiggerThanMinDimensions = (imageWidth >= lightBoxMinWidth) && (imageHeight >= (lightBoxMinHeight - iFooterHeightPx)); }; /** * Calculates and sets the Image size in the LightBox. * * @private * @param {sap.m.Image} image The image instance. * @param {int} imageWidth The width of the internal image. * @param {int} imageHeight The height of the internal image. */ LightBox.prototype._setImageSize = function (image, imageWidth, imageHeight) { var footerHeight = this._calculateFooterHeightInPx(), dimensions = this._getDimensions(imageWidth, imageHeight, footerHeight), width = dimensions.width + 'px', height = dimensions.height + 'px', imgDomRef = image.getDomRef(); image.setProperty('width', width, true); image.setProperty('height', height, true); if (imgDomRef) { imgDomRef.style.width = width; imgDomRef.style.height = height; } }; /** * Calculates the size for an image inside the LightBox. * * @private * @param {int} imageWidth The natural width of the loaded images in px. * @param {int} imageHeight The natural height of the loaded images in px. * @param {int} footerHeight The footer height in px. * @returns {Object} An object holding the calculated dimensions of the image */ LightBox.prototype._getDimensions = function (imageWidth, imageHeight, footerHeight) { // minimum size of the lightbox var lightboxMinWidth = 20 /*rem*/ * 16 /*px*/, lightboxMinHeight = 18 /*rem*/ * 16 /*px*/, // window size $window = jQuery(window), windowHeight = $window.height(), windowWidth = $window.width(), minimumOffset = calculateOffset(), // the size available for the image availableWidth = Math.max(windowWidth - minimumOffset, lightboxMinWidth), availableHeight = Math.max(windowHeight - minimumOffset, lightboxMinHeight), // the ratio to which the image will be scaled scaleRatio; availableHeight -= footerHeight; if (imageHeight <= availableHeight) { if (imageWidth <= availableWidth) { // do nothing, image has enough space and will be displayed as it is } else { // image is wider than the space available imageHeight *= availableWidth / imageWidth; imageWidth = availableWidth; } } else { if (imageWidth <= availableWidth) { imageWidth *= availableHeight / imageHeight; imageHeight = availableHeight; } else { scaleRatio = Math.max(imageWidth / availableWidth, imageHeight / availableHeight); imageWidth /= scaleRatio; imageHeight /= scaleRatio; } } return {width : Math.round(imageWidth), height : Math.round(imageHeight)}; }; /** * Converts size from px to a number. * * @private * @param {string} sizeToConvert The size to be converted * @returns {int} The size in number value */ LightBox.prototype._pxToNumber = function (sizeToConvert) { return (sizeToConvert.substring(0, (sizeToConvert.length - 2)) ) * 1; }; /** * Returns the first LightBoxItem in the aggregation. * * @private * @returns {sap.m.LightBoxItem|null} The first LightBoxItem in the imageContent aggregation. */ LightBox.prototype._getImageContent = function () { var rgImageContent = this.getAggregation('imageContent'); return rgImageContent && rgImageContent[0]; }; /** * Helper function for calculating offset. * * @private * @returns {int} Calculated offset. */ function calculateOffset() { var system = Device.system; if (system.desktop) { return 4 /*rem*/ * 16 /*px*/; } if (system.tablet) { return 2 /*rem*/ * 16 /*px*/; } return 0; } return LightBox; });