Source: glass/views/ShareView.js

/**
 * Licensed Materials - Property of IBM
 * IBM Cognos Products: Collaboration
 * (C) Copyright IBM Corp. 2017, 2020
 *
 * US Government Users Restricted Rights - Use, duplication or disclosure
 * restricted by GSA ADP Schedule Contract with IBM Corp.
 */

define([
	'underscore',
	'jquery',
	'react',
	'react-dom',
	'ca-ui-toolkit',
	'../../lib/@waca/core-client/js/core-client/ui/AccessibleView',
	'../../lib/@waca/core-client/js/core-client/utils/BrowserUtils',
	'../../lib/@waca/core-client/js/core-client/utils/DateTimeUtils',
	'../../nls/StringResources',
	'../../api/sharing/ShareController',
	'../utils/GlassUtil'
], function (_, $, React, ReactDOM, Toolkit, AccessibleView, BrowserUtils, DateTimeUtils, StringResources, ShareController, GlassUtil) {

	'use strict';

	const SHARE_VIEW_CLASS = 'share-view ba-collab-fill-space ba-theme-default';
	var ShareView = AccessibleView.extend( /** @lends ShareView */ {

		/**
		 * @desc Constructor for ShareView.
		 * @constructs ShareView
		 * @extends AccessibleView
		 * @public
		 * @param options {Object} Options
		 * @param options.glassContext {Object} The glass context
		 */
		init: function (options) {
			_.extend(options, {
				enableTabLooping: true,
				className: 'ba-collab-fill-space'
			});

			ShareView.inherited('init', this, arguments);
			this.glassContext = options.glassContext;
			this.shareController = new ShareController(_.extend({
				errorHandler: this.displayError.bind(this),
				slideout: this.slideout
			}, options));
			this._panel = null;
			this.link = options.slideout.content.link;
			this.embedVisible = this._isEmbedEnabled() && options.slideout.content.embedVisible !== false;
			this.imageVisible = !!options.slideout.content.imageVisible;
			this._eventHandlers = this.slideout && [
				this.slideout.on('show', this.onSlideShow.bind(this)),
				this.slideout.on('hide', this.onSlideHide.bind(this))
			];

			this._logger = options.logger;
			try {
				this._instrumentationService = this.glassContext.getCoreSvc('.Instrumentation');
			} catch (e) {
				// just swallow it if the instrumentation service isn't available for some reason.
				void (e);
			}

			this.onSlideShow(); // event is not being called when slide is opened for the first time.
			this.reactContainer = null;
			this.shareStore = null;
			this.imageStore = null;
		},

		/**
		* It will be called after slideout is shown
		* @callback
		*/
		setFocus: function () {
			if (this._panel) {
				this._panel.setFocus();
			}
		},

		_onIframeFocus: function (e) {
			e.stopPropagation();
			const activeElement = document.activeElement;
			if (activeElement instanceof HTMLIFrameElement
				&& !activeElement.classList.contains('cke_panel_frame')
				&& !this.$el.get(0).contains(activeElement)
				&& this.slideout.isOpen()) {
				this.close();
			}
		},

		onSlideShow: function () {
			var $iframe = $('iframe');
			if ($iframe.length > 0 && this.slideout) {
				this._onIframeFocusFunc = this._onIframeFocus.bind(this);
				$(window).on('blur', this._onIframeFocusFunc);
			}
			if (this._panel) {
				const assetStrings = this.shareController.getAssetStrings();
				this.shareStore.setAssetType(assetStrings.assetType);
				this.shareStore.setAssetTitle(assetStrings.assetTitle);
				this.shareStore.setIsDirty(this._isDirty());
				return Promise.all([ this.refreshLink(), this.refreshIncludeImage() ])
					.then(results => {
						const link = results[0];

						// if cannot send, don't need to get connectors
						if (!this.canSend()) {
							this.shareStore.setConnectors([]);
							this.imageStore.setImageLoaded(false);
							return link;
						}

						// refresh connectors
						return this.getConnectors()
							.then((connectors) => {
								this.shareStore.setConnectors(connectors);
								this.imageStore.setImageLoaded(false);
								return link;
							});
					});
			}
			return Promise.resolve(null);
		},

		onSlideHide: function () {
			var $iframe = $('iframe');
			if ($iframe.length > 0 && this.slideout && this._onIframeFocusFunc) {
				this._onIframeFocusFunc = null;
				$(window).off('blur', this._onIframeFocusFunc);
			}
			return this.shareController.leaveShareState();
		},

		/**
		 * Sets the current perspective's link URL.
		 * @param link
		 */
		setLink: function (link) {
			if (this.shareStore) {
				this.shareStore.setLink(link);
			}
		},

		/**
		 * Refresh the current perspective's link URL.
		 * @instance
		 * @returns {Promise} resolved as an object that contains shareUrl and embedUrl
		 */
		refreshLink: function () {
			return new Promise((resolve, reject) => {
				if (this.link) {
					this.setLink(this.link);
					resolve(this.link);
				} else {
					this.shareController.getLink(this.glassContext)
						.then((link) => {
							this.setLink(link);
							resolve(link);
						})
						.catch((error) => {
							this.setLink(null);
							reject(error);
						});
				}
			});
		},

		/**
		 * Fetch user's configured product locale and Product version.
		 * @instance
		 * @returns {Promise} with the user's browser language.
		 */
		getConfig: function () {
			// The passed in version number is of the form "11.1 RX", but the knowledge center
			// uses the form "11.1.X". Until glass provides consistent versioning with the knowlege
			// center, we will hard-code the version here.
			return this.glassContext.getSvc('.UserProfile')
				.then(userProfile => {
					const language = userProfile.preferences.productLocale ? userProfile.preferences.productLocale : 'en';
					return {
						language,
						version: '11.1.0'
					};
				});
		},

		/**
		 * Capture the current perspective's screenshot image.
		 * @instance
		 * @returns {Promise} with an image string as URL.
		 */
		refreshImage: function () {
			return new Promise((resolve, reject) => {
				if (this._panel && this._panel.state.connector) {
					if (this._panel.state.connector.isImageSupported()) {
						this.shareController
							.getScreenshot(this.glassContext)
							.then((item) => {
								var img = new Image();
								img.onload = () => {
									this.imageStore.setImage(item.image, item.label, img.width, img.height);
									resolve(item);
								};
								img.src = item.image;
							})
							.catch((error) => {
								this.glassContext.showToast(StringResources.get('error_no_screenshot'), {
									type: 'error'
								});
								this.imageStore.setImage('', '', 0, 0);
								reject(error);
							});
					} else {
						var errorTxt = StringResources.get('error_screenshot_unsupported');
						this.glassContext.showToast(errorTxt, {
							type: 'warning'
						});
						reject(new Error(errorTxt));
					}
				} else {
					resolve(); // We're not trying to load a screenshot yet.
				}
			});
		},

		/**
		 * Returns if the content supports export to pdf
		 * @returns Promise
		 */
		canExportToPDF: function () {
			return this.shareController.canExportToPDF(this.glassContext).then((result) => {
				// when link is set, we were called from My/Team Content navigation.
				return result && !this.link && this.features.includes(GlassUtil.feature.EXPORT);
			});
		},

		/**
		 * Returns whether "Include image" option should be visible or hidden.
		 *
		 * @returns {Promise} that resolves to a boolean value.
		 */
		includeImage: function () {
			if (!this.imageVisible) {
				return Promise.resolve(false);
			}

			// Authoring perspective supports image capture but requires an extra check
			// of the current report format. Image capture should be enabled for HTML reports;
			// disabled for PDF reports.
			return this.shareController.canCaptureImage(this.glassContext);
		},

		/**
		 * Update "Include image" flags in the ImageStore.
		 * 
		 * One use case where it's necessary:
		 * 1. Run HTML report.
		 * 2. Open Share / Email panel ==> "Include image" should be enabled
		 * 3. Hide Share slideout.
		 * 4. Run the same report as PDF in the report viewer.
		 * 5. Open Share panel ==> "Include image" should be disabled.
		 */
		refreshIncludeImage: function() {
			if (!this.imageStore) {
				return Promise.resolve();
			}
			return this.includeImage().then(result => {
				this.imageStore.setShowIncludeImage(result);
				return result;
			});
		},

		_checkLinkShareable: function (link) {
			var pathQuery = 'pathRef';
			if (!!this.type && this.type.assetType === 'folder') {
				pathQuery = 'folder';
			}
			return !!(link && link.shareUrl && link.shareUrl.search(pathQuery + '=.my_folders') === -1);
		},

		isLinkShareable: function (link) {
			return this._isLinkEnabled() && this._checkLinkShareable(link);
		},

		canSend: function () {
			return this._isSendEnabled() && (!this.link ? true : this._checkLinkShareable(this.link));
		},

		/**
		 * Render the view.
		 * @instance
		 */
		render: function () {
			// tell html2canvas to ignore our panel when capturing the image.
			this.$el.closest('.flyoutPane').attr('data-html2canvas-ignore', 'true');

			// 1. render a spinner
			ReactDOM.render(React.createElement(Toolkit.ProgressIndicator, {
				className: 'initialCollaborationSlideoutSpinner',
				id: 'initialCollaborationSlideoutSpinner',
				size: 'large',
				style: { left: '50%', top: '50%', position: 'absolute', transform: 'translate(-50%, -50%)' }
			}), this.$el.get(0));
			return new Promise(function (resolve, reject) {

				// 2. load modules
				require(['collaboration/canvaseditor/CanvasEditor', 'collaboration-ui/collaboration-ui.min', 'ckeditor'], function (CanvasEditor, CollaborationUI) {

					// 3. get language and version, connectors and more
					Promise.all([this.getConfig(), this.getConnectors(), this.canExportToPDF(), this.refreshLink(), this.includeImage()])
						.then(([config, connectors, canExport, link, showIncludeImage]) => {
							// 4. remove the spinner
							ReactDOM.unmountComponentAtNode(this.$el.get(0));

							// 5. create the stores
							this.createStores(
								CollaborationUI.ShareStore,
								CollaborationUI.ImageStore,
								CanvasEditor,
								connectors,
								config,
								canExport,
								link,
								showIncludeImage);

							// 6. render react
							this.renderReact(CollaborationUI.CollaborationPanel);

							// all done!
							resolve();
						})
						.catch(reject);
				}.bind(this));
			}.bind(this));
		},

		_isSendEnabled: function () {
			return this.features.includes(GlassUtil.feature.SEND);
		},

		_isLinkEnabled: function () {
			return this.features.includes(GlassUtil.feature.LINK);
		},

		_isEmbedEnabled: function () {
			return this.features.includes(GlassUtil.feature.EMBED);
		},

		_isEmailLinkEnabled: function () {
			return this.features.includes(GlassUtil.feature.EMAIL_LINK) && this.glassContext.hasCapability('canIncludeLinkInEmail');
		},

		_isDirty: function() {
			return this.glassContext.currentAppView.isDirty();
		},

		createStores: function (ShareStore, ImageStore, CanvasEditor, connectors, config, canExport, link, showIncludeImage) {
			const assetStrings = this.shareController.getAssetStrings();
			const shareStore = ShareStore.create({
				isIE11: BrowserUtils.isIE11(),
				language: config.language,
				version: config.version,
				assetTitle: assetStrings.assetTitle,
				assetType: assetStrings.assetType,
				isDirty: this._isDirty(),
				objectType: this.objectType || null,
				objectId: this.objectId || null
			}, {
				CanvasEditor,
				glassContext: this.glassContext,
				$root: this.$el,
				isLinkShareable: this.isLinkShareable.bind(this),
				send: this.send.bind(this),
				generatePDF: this.generatePDF.bind(this),
				displayError: this.displayError.bind(this),
				instrument: this.instrument.bind(this),
				features: this.features,
				glassFeature: GlassUtil.feature,
				dateTimeUtils: DateTimeUtils
			});
			shareStore.enableSend(this._isSendEnabled());
			shareStore.enableLink(this._isLinkEnabled());
			shareStore.enableEmailLink(this._isEmailLinkEnabled());
			shareStore.enableEmbed(this._isEmbedEnabled());
			shareStore.enableExport(canExport);
			shareStore.setConnectors(connectors);
			shareStore.setLink(link);
			this.shareStore = shareStore;

			const imageStore = ImageStore.create({
				showIncludeImage: showIncludeImage,
				includeImage: showIncludeImage,
				image: '',
				imageLoaded: false,
				imageLabel: '',
				imageWidth: 0,
				imageHeight: 0
			}, {
				refreshImage: this.refreshImage.bind(this)
			});
			this.imageStore = imageStore;
		},

		createReactPanel: function (CollaborationPanel, options) {
			this._panel = ReactDOM.render(React.createElement(CollaborationPanel, options), this.reactContainer);
		},

		renderReact: function (CollaborationPanel) {
			const $container = $(`<div class="${SHARE_VIEW_CLASS}" tabIndex="-1"></div>`).appendTo(this.$el);
			this.enableLooping($container);

			const options = {
				shareStore: this.shareStore,
				imageStore: this.imageStore,
				nls: StringResources.get,
				panel: 'main',
				cancel: this.close.bind(this),
				embedVisible: this.embedVisible,
				contentMenuShare: !!this.link
			};

			// render the view with options
			this.reactContainer = $container.get(0);
			this.createReactPanel(CollaborationPanel, options);
			this.$el.on('escapeaction', (event) => {
				event.preventDefault();
				event.stopPropagation();
			});

			// The slideout has 'transition' css in its root DOM element, and it relies on these events to calculate animation.
			// Stop propagation to avoid issues.
			var animEvents = 'transitionend webkitTransitionEnd oTransitionEnd';
			$(this.reactContainer).off(animEvents).on(animEvents, function (event) {
				event.stopPropagation();
			});
		},

		generatePDF: function (pageSize, includeFilter) {
			if (this.shareStore && this.shareStore.canExport) {
				this.shareController.exportToPDF(this.glassContext, pageSize, includeFilter);
			}
		},

		instrument: function (action, shareType) {
			if (this._instrumentationService && this._instrumentationService.enabled) {
				return this.shareController.getInstrumentation(this.glassContext)
					.then((event) => {
						event.action = action;
						event['custom.shareType'] = shareType;
						event.type = 'Shared Object';
						event.objectType = event.objectType || this.objectType;
						// If neither of those worked, try to get the type from glass.
						if (!event.objectType && typeof this.glassContext.getCurrentContentView === 'function') {
							event.objectType = this.glassContext.getCurrentContentView().getType();
						}
						event.milestoneName = `${action}_${event.objectType}`;
						this._instrumentationService.track(event);
					});
			}
			return Promise.resolve();
		},

		getConnectors: function () {
			// don't need to load connectors if cannot send
			if (!this.canSend()) {
				return Promise.resolve([]);
			}

			return this.shareController.getConnectors()
				.catch(function (error) {
					if (this._logger) {
						this._logger.error('Error fetching connectors: ' + error);
					}
					this.glassContext.showToast(StringResources.get('error_retrieving_platforms'), {
						type: 'error'
					});
					throw error;
				}.bind(this));
		},

		/**
		 * Sends the message.
		 * @instance
		 * @param {object} payload
		 * @param {object} payload.connector
		 * @param {object} payload.data
		 * @returns {Promise}
		 */
		send: function (payload) {
			return this.shareController.send(payload.connector, payload.data)
				.then((result) => {
					// success
					void (result);
					const message = (payload.type === 'email') ? StringResources.get('toast_success_email') : StringResources.get('toast_success', {
						connector: payload.connector.getLabel()
					});
					this.glassContext.showToast(message, { type: 'success' });
				}).then(() => this.close());
		},

		/**
		 * Close the panel.
		 * @instance
		 * @returns {Promise}
		 */
		close: function () {
			return this.shareController.close()
				.then(function () {
					return this.slideout.hide({
						force: true,
						hideOnly: this.slideout.hideOnly
					});
				}.bind(this))
				.then(function () {
					var launchPoint = this.getLaunchPoint();
					if (launchPoint) {
						$(launchPoint).focus();
					}
				}.bind(this));
		},

		/**
		 * Display errors as toast message.
		 * @instance
		 * @param {object} error
		 * @throws {error}
		 */
		displayError: function (error) {
			var msg = error.message;
			if (error.connector) {
				const connectorType = error.connector.getType();
				var toastStringId = (connectorType === 'email') ? 'toast_failure_email' : 'toast_failure';
				var toastMessageData = {
					connector: error.connector.getLabel(),
					error: error.message
				};
				if (error.showContactAdmin) {
					toastStringId = (connectorType === 'email') ? 'toast_failure_detailed_email' : 'toast_failure_detailed';
					toastMessageData.contactAdmin = StringResources.get('message_contact_administrator');
				}
				msg = StringResources.get(toastStringId, toastMessageData);
			}

			this.glassContext.showToast(msg, {
				type: 'error'
			});
			throw error;
		},

		/**
		 * Cleaning up events
		 * @override
		 */
		remove: function () {
			if (this._eventHandlers) {
				this._eventHandlers.forEach(function (handler) {
					if (handler) {
						handler.remove();
					}
				});
			}
			if (this.reactContainer) {
				ReactDOM.unmountComponentAtNode(this.reactContainer);
			}
		}
	});

	return ShareView;
});