/**
* 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;
});