'use strict'; /** * Licensed Materials - Property of IBM * IBM Cognos Products: Storytelling * (C) Copyright IBM Corp. 2014, 2020 * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp. */ define(['baglass/core-client/js/core-client/ui/core/View', 'baglass/core-client/js/core-client/utils/BrowserUtils', 'baglass/core-client/js/core-client/utils/ContentFormatter', 'baglass/core-client/js/core-client/utils/dom-utils', 'baglass/core-client/js/core-client/utils/Utils', 'text!./templates/FilmStripView.html', 'text!./templates/SceneCell.html', 'text!./templates/AddSceneCell.html', 'text!./templates/OverviewCell.html', 'jquery', 'underscore', 'doT', '../nls/StringResources', 'gemini/app/ui/dialogs/RenameDialog', 'text!../layout/templates/sceneLayoutListing.json', 'storytelling-ui/storytelling-ui.min', './TimelineView', 'gemini/app/ui/dnd/DnDHelper', '../lib/@ba-ui-toolkit/ba-graphics/dist/icons-js/chevron-down_16', '../lib/@ba-ui-toolkit/ba-graphics/dist/icons-js/add_16', '../lib/@ba-ui-toolkit/ba-graphics/dist/icons-js/maximize_16', '../lib/@ba-ui-toolkit/ba-graphics/dist/icons-js/overflow-menu--horizontal_16', '../lib/@ba-ui-toolkit/ba-graphics/dist/illustrations-js/visualization-widget_128', 'react', 'react-dom', 'hammerjs', 'jquery.hammer'], function (View, BrowserUtils, ContentFormatter, DomUtils, Utils, FilmStripViewTemplate, SceneCellTemplate, AddSceneCellTemplate, OverviewCellTemplate, $, _, dot, stringResources, RenameDialog, sceneLayoutListing, StorytellingUI, TimelineView, DnDHelper, ChevronDownIcon, addIcon, maximizeIcon, menuOverflowIcon, visualizationIcon, React, ReactDOM) { var FilmStripView = View.extend({ templateString: FilmStripViewTemplate, collapseDuration: 400, enlargeDuration: 400, events: { 'keydown .scene, .overview, .addScene': 'onKeydown' }, init: function init(options) { FilmStripView.inherited('init', this, arguments); this.dashboardApi = options.dashboardApi; this.controller = options.controller; // StoryPaneController this.timelineController = this.controller.getTimelineController(); this.canvasController = this.timelineController.canvasController; this.glassContext = options.glassContext; this.dndManager = options.dndManager; this.services = options.services; this._dropZones = []; this.logger = this.dashboardApi.getGlassCoreSvc('.Logger'); this.translationService = this.dashboardApi.getDashboardCoreSvc('TranslationService'); this.nextPreviousDuration = 900; this.nextPreviousDelay = this.nextPreviousDuration / 6; this.forceReFocus = false; this.flyoutHolder = document.createElement('div'); this.flyoutHolder.classList.add('flyout-holder'); }, /** * Renders the film strip * * @returns */ render: function render() { var _this = this; this.$el.addClass('filmstrip'); this.contentEl = this.el.parentNode; // FIXME: This line is bad. Filmstrip is technically reaching out of its scope this.controller.on('change:pageSize', this.onPageSizeChange, this); this.controller.on('change:showOverviews', this.onShowOverviewsChanged, this); this.controller.on('layoutType:changed', this.onLayoutTypeChanged, this); this.controller.on('mode:change', this.onModeChanged, this); this.controller.on('scene:add', this.onSceneAdded, this); this.controller.on('scene:collapse', this.onSceneCollapse, this); this.controller.on('scene:expand', this.onSceneExpand, this); this.controller.on('scene:remove', this.onSceneRemoved, this); this.controller.on('scene:select', this.onSceneSelected, this); this.controller.model.on('change:layout', this.onChangeLayout, this); this.timelineController.on('scene:reorder', this.onSceneReorder, this); this.canvasController.addRemoveNotifier.on('widget:addDone', this.onWidgetAdded, this); this.canvasController.addRemoveNotifier.on('widget:removeDone', this.onWidgetRemoved, this); var sHtml = this.dotTemplate({}); this.$el.html(sHtml); this.$sceneSequencer = this.$el.find('.sceneSequencer'); this.$sceneList = this.$el.find('.sceneList'); this.$sceneSequencer.hammer().on('mousedown', '.scene', this.onSceneContextMenu.bind(this)).on('clicktap', '.scene', this.onSceneClick.bind(this)).on('dragstart', '.scene', this.onSceneDragStart.bind(this)) // hammer .on('hold', '.scene', this.onSelectedSceneHold.bind(this)) // hammer .on('primaryaction', 'span.overflow', this.onSceneOverflowClick.bind(this)).on('primaryaction', 'span.expandScene', this.onExpandSceneClick.bind(this)); this.cellTemplate = dot.template(SceneCellTemplate); this.controller.getScenes().forEach(function (item, index) { var $cell = _this.renderSceneCell(item, index); _this.$sceneList.append($cell); _this.middleShortenSceneTitle(_this._getSceneSelector(item.id)); }); this.updateOverviewButton(); this._setupFilmStripDropZone(); this._toggleModeChanged(this.controller.isAuthoring()); return Promise.all([this._addExpandSceneCoachmark(), this._addOverviewSceneCoachmark()]); }, _scaleCell: function _scaleCell($cell, pageSize) { if (pageSize && pageSize.width && pageSize.height) { var DEFAULT_SCENE_HEIGHT = 125; $cell.css({ width: DEFAULT_SCENE_HEIGHT * pageSize.width / pageSize.height + 'px', height: DEFAULT_SCENE_HEIGHT + 'px' }); } }, renderSceneCell: function renderSceneCell(sceneModel, index) { var _this2 = this; var sHtml = this.cellTemplate({ id: sceneModel.id, index: index, title: sceneModel.get('title'), expandSceneLabel: stringResources.get('storySceneExpand'), sceneActionMenuLabel: stringResources.get('storySceneActionMenu'), selected: index === this.controller.getSelectedSceneIndex(), widgetIndicated: this.controller.isScenePopulated(sceneModel.id) }); var $cell = $(sHtml); this.translationService.registerView({ view: $cell, model: sceneModel }); Utils.setIcon($cell.find('.expandScene'), maximizeIcon.default.id); Utils.setIcon($cell.find('.overflow'), menuOverflowIcon.default.id); Utils.setIcon($cell.find('.widgetIndicator'), visualizationIcon.default.id); sceneModel.on('change:title', function () { _this2.onSceneLabelChange(sceneModel); }); this._scaleCell($cell, this.controller.model.layout.pageSize); this._setupDropZones($cell); return $cell; }, _setupDropZones: function _setupDropZones($cell) { var _this3 = this; var cell = $cell[0]; var dropZone = this.dndManager.addDropTarget(cell, { accepts: function accepts(dragObject) { return dragObject.type === 'sceneCell' || dragObject.type === 'widget'; }, onDragEnter: function onDragEnter(dragObject, dropNode) { if (dragObject.type === 'widget') { dropNode.classList.add('dropZone'); } }, onDragMove: function onDragMove(dragObject, dropNode) { if (dragObject.type !== 'sceneCell') { return; } _this3._dropNodeModelId = dropNode.dataset.modelId; var dropModelIndex = _this3.controller.getSceneIndex(_this3._dropNodeModelId); if (_this3._dragModelIndex > dropModelIndex) { _this3.$sceneList[0].insertBefore(_this3._cloneDragEl, dropNode); } else { _this3.$sceneList[0].insertBefore(_this3._cloneDragEl, dropNode.nextSibling); } }, onDrop: function onDrop(dragObject) { if (dragObject.type === 'widget') { DnDHelper.handleWidgetDrop(_this3.dashboardApi.getCanvas(), $cell.attr('data-model-id'), dragObject); $cell[0].classList.remove('dropZone'); } }, onDragLeave: function onDragLeave(dragObject, dropNode) { if (dragObject.type === 'widget') { dropNode.classList.remove('dropZone'); } } }); this._dropZones.push({ dropZone: dropZone, node: cell }); }, /** * A method to setup a dropzone on the entire filmstrip to handle drag-and-drop events * on scenes that are dropped outside the sceneList container. */ _setupFilmStripDropZone: function _setupFilmStripDropZone() { var _this4 = this; var filmstrip = this.el; var sceneList = this.$sceneList[0]; var dropZone = this.dndManager.addDropTarget(filmstrip, { accepts: function accepts(dragObject) { return dragObject.type === 'sceneCell'; }, onDragStart: function onDragStart() { _this4.sceneListBounds = sceneList.getBoundingClientRect(); }, onDragMove: function onDragMove(dragObject) { var firstScene = sceneList.firstChild; var lastScene = sceneList.lastChild; if (dragObject.position.x < _this4.sceneListBounds.left) { if (firstScene !== _this4._cloneDragEl) { _this4._dropNodeModelId = firstScene.dataset.modelId; sceneList.insertBefore(_this4._cloneDragEl, firstScene); } } else if (dragObject.position.x > _this4.sceneListBounds.left + _this4.sceneListBounds.width) { if (lastScene !== _this4._cloneDragEl) { _this4._dropNodeModelId = lastScene.dataset.modelId; sceneList.appendChild(_this4._cloneDragEl); } } } }); this._dropZones.push({ dropZone: dropZone, node: filmstrip }); }, /** * Create the "add scene" cell that lives at the end of the filmstrip * but before the end overview scene if it exists and is turned on. */ renderAddSceneCell: function renderAddSceneCell() { var addCellTemplate = dot.template(AddSceneCellTemplate); var sHtml = addCellTemplate({ addTitle: stringResources.get('addSceneBtnTitle'), chevronTitle: stringResources.get('addSceneChevronBtnTitle') }); this.$addSceneCell = $(sHtml); // Place the 'add scene' cell after the 'sceneSequencer' container this.$sceneSequencer.after(this.$addSceneCell); this._scaleCell(this.$addSceneCell, this.controller.model.layout.pageSize); var $plusContainer = this.$addSceneCell.find('.addContainer'); Utils.setIcon($plusContainer, addIcon.default.id); $plusContainer.on('click', this.onAddSceneClick.bind(this)); var $chevronContainer = this.$addSceneCell.find('.chevron'); Utils.setIcon($chevronContainer, ChevronDownIcon.default.id); $chevronContainer.on('primaryaction', this.onAddSceneOverflowClick.bind(this)); }, onSceneLabelChange: function onSceneLabelChange(model) { var sceneSelector = this._getSceneSelector(model.id); this._getTitleEl(sceneSelector).textContent = model.get('title') || ''; this.middleShortenSceneTitle(sceneSelector); }, middleShortenSceneTitle: function middleShortenSceneTitle(sceneSelector) { var sceneTitleEl = this._getTitleEl(sceneSelector); ContentFormatter.middleShortenString(sceneTitleEl); }, _getTitleEl: function _getTitleEl(sceneSelector) { return this.el.querySelector(sceneSelector + ' .pageTitle'); }, onShowOverviewsChanged: function onShowOverviewsChanged() { this.updateOverviewButton(); }, updateOverviewButton: function updateOverviewButton(updateIcon) { if (this.controller.showAuthoringOverview()) { if (!this._$overviewStart) { this._$overviewStart = this._renderOverviewButton(-1); this.$el.prepend(this._$overviewStart); this._scaleCell(this._$overviewStart, this.controller.model.layout.pageSize); } else if (updateIcon) { this._updateOverviewIcon(this.$el.find('.overview.start .layoutIcon'), -1); } // This bit is for toggling the scene number in the overview frame to show if it is shown or not. var $pageNum = this._$overviewStart.find('.sceneSubContainer .pageInfo'); if (this.controller.showStartOverview()) { $pageNum.show(); } else { $pageNum.hide(); } } else { if (this._$overviewStart) { this._$overviewStart.remove(); this._$overviewStart = null; } } if (this.controller.showEndOverview()) { if (!this._$overviewEnd) { this._$overviewEnd = this._renderOverviewButton(-2); this.$el.append(this._$overviewEnd); this._scaleCell(this._$overviewEnd, this.controller.model.layout.pageSize); } else if (updateIcon) { this._updateOverviewIcon(this.$el.find('.overview.end .layoutIcon'), -2); } } else { if (this._$overviewEnd) { this._$overviewEnd.remove(); this._$overviewEnd = null; } } }, /** * Render an overview scene cell * @param {Number} index The index value that relates to which overview scene to handle rendering of */ _renderOverviewButton: function _renderOverviewButton(index) { // if index is -1, this is the beginning overview button (overviewId should be 0) // if index is -2, this is the end overview button (overviewId should be # scenes + 1) var label = this._getOverviewLabel(index); if (!this.overviewCellTemplate) { this.overviewCellTemplate = dot.template(OverviewCellTemplate); } var sHtml = this.overviewCellTemplate({ title: label, selected: index === this.controller.getSelectedSceneIndex(), index: this._getOverviewIndex(index) }); var $el = $(sHtml); if (index === -1) { $el.addClass('start').on('primaryaction', this.onOverviewStartClick.bind(this)); } else if (index === -2) { $el.addClass('end').on('primaryaction', this.onOverviewEndClick.bind(this)); } this._updateOverviewIcon($el.find('.layoutIcon'), index); return $el; }, _getOverviewLabel: function _getOverviewLabel(index) { var label = void 0; if (index === -1) { label = stringResources.get('sceneListStart'); } else if (index === -2) { label = stringResources.get('sceneListEnd'); } else { label = stringResources.get('overviewBtnLabel'); } return label; }, _getOverviewIndex: function _getOverviewIndex(index) { var $scenes = this.$sceneList.children(); var overviewIndex = null; if (index === -1) { overviewIndex = 0; } else if (index === -2) { overviewIndex = $scenes.length + 1; } return overviewIndex; }, _updateOverviewIcon: function _updateOverviewIcon($layoutIcon, index) { var navModel = this.controller.getNavModel(); var iconName = navModel.indexOf('panAndZoom') >= 0 ? 'dashboard-' + navModel : null; var label = this._getOverviewLabel(index); if (iconName) { $layoutIcon.empty(); Utils.setIcon($layoutIcon, iconName, label); } }, remove: function remove() { var _this5 = this; if (this.controller) { this.controller.off('change:pageSize', this.onPageSizeChange, this); this.controller.off('change:showOverviews', this.onShowOverviewsChanged, this); this.controller.off('layoutType:changed', this.onLayoutTypeChanged, this); this.controller.off('mode:change', this.onModeChanged, this); this.controller.off('scene:add', this.onSceneAdded, this); this.controller.off('scene:collapse', this.onSceneCollapse, this); this.controller.off('scene:expand', this.onSceneExpand, this); this.controller.off('scene:remove', this.onSceneRemoved, this); this.controller.off('scene:select', this.onSceneSelected, this); var scenes = this.controller.getScenes() || []; scenes.forEach(function (scene) { scene.off('change:title', null, _this5); }); if (this.controller.model) { this.controller.model.off('change:layout', this.onChangeLayout, this); } } this.timelineController.off('scene:reorder', this.onSceneReorder, this); // This check is here because there is a destruction path that causes the // addRemoveNotifier to be unset before execution gets here if (this.canvasController && this.canvasController.addRemoveNotifier) { this.canvasController.addRemoveNotifier.off('widget:addDone', this.onWidgetAdded, this); this.canvasController.addRemoveNotifier.off('widget:removeDone', this.onWidgetRemoved, this); } if (this._dropZones) { this._dropZones.forEach(function (zone) { if (zone.dropZone && zone.node) { zone.dropZone.remove(); // Remove scenes from the filmstrip if (zone.node !== _this5.el) { zone.node.parentNode.removeChild(zone.node); } } }); this._dropZones = []; } FilmStripView.inherited('remove', this, arguments); }, onAddSceneClick: function onAddSceneClick() { var _this6 = this; this.controller.addScene().catch(function (error) { _this6.logger.error('Unable to add new scene to story', error); }); }, onSceneClick: function onSceneClick(event) { event.stopPropagation(); this._closeFlyout(); var index = this.$sceneList.children().index(event.currentTarget); var modelId = event.currentTarget.dataset.modelId; this.controller.selectScene({ index: index, modelId: modelId }); }, onOverviewStartClick: function onOverviewStartClick(event) { event.stopPropagation(); this.controller.selectScene({ index: -1 }); }, onOverviewEndClick: function onOverviewEndClick(event) { event.stopPropagation(); this.controller.selectScene({ index: -2 }); }, onExpandSceneClick: function onExpandSceneClick(event) { event.stopPropagation(); this.controller.expandScene(); }, /** * Controller events */ onModeChanged: function onModeChanged(event) { this._toggleModeChanged(event.authoring); this.updateOverviewButton(); }, /** * @param {LayoutEvent} event This is the event triggered by StoryPaneController.onAddLayout() */ onSceneAdded: function onSceneAdded(event) { this._addScene(event.scene, event.index, event.insertBefore); this._refreshCellPageNumbers(); }, onSceneRemoved: function onSceneRemoved(event) { this._removeScene(event.id); this._refreshCellPageNumbers(); this.updateOverviewButton(); }, onSceneSelected: function onSceneSelected(event) { this._selectScene(event); this._refreshTimeline(); }, onSceneExpand: function onSceneExpand(event) { this._expandScene(this.$sceneList.children().eq(event.index)); }, onSceneCollapse: function onSceneCollapse(event) { this._collapseScene(this.$sceneList.children().eq(event.index)); }, onLayoutTypeChanged: function onLayoutTypeChanged() { this.updateOverviewButton(true); }, onPageSizeChange: function onPageSizeChange(pageSize) { var _this7 = this; this.$sceneList.children().each(function (index, cell) { _this7._scaleCell($(cell), pageSize); }); this._scaleCell(this.$addSceneCell, pageSize); if (this._$overviewStart) { this._scaleCell(this._$overviewStart, pageSize); } if (this._$overviewEnd) { this._scaleCell(this._$overviewEnd, pageSize); } }, onSceneContextMenu: function onSceneContextMenu(event) { // right-click if (event.which === 3) { this.onSceneOverflowClick(event); } }, onSceneOverflowClick: function onSceneOverflowClick(event) { event.stopPropagation(); if (this.preventContext || !this.controller.isAuthoring()) { this.preventContext = false; } else { if (this._flyout) { this._closeFlyout(); return; } var $target = $(event.currentTarget).closest('[data-model-id]'); var sceneId = $target.data('modelId'); var options = { name: this.controller.getScene(this.controller.getSceneIndex(sceneId)).get('title'), type: 'Scene', allowEmpty: true, id: _.uniqueId('renameDialog') }; var renameDialog = new RenameDialog(options, function (newName) { this.controller.renameScene(sceneId, newName); }.bind(this)); var renameDialogCallback = function renameDialogCallback() { renameDialog.open(); }; // Making sure the anchor element is always the scene var anchorElement = event.currentTarget; if (anchorElement.parentNode.id === sceneId + '_tablabel') { anchorElement = anchorElement.parentNode; } this.$el.append(this.flyoutHolder); this._flyout = React.createElement(StorytellingUI.SceneCellFlyout, { sceneId: sceneId, controller: this.controller, anchorElement: anchorElement, closeFlyout: this._closeFlyout.bind(this), renameDialog: renameDialogCallback }); ReactDOM.render(this._flyout, this.flyoutHolder); } }, onAddSceneOverflowClick: function onAddSceneOverflowClick(event) { //TODO: Find a better way to access this element var target = event.currentTarget.children[0]; var options = { target: event.currentTarget, onOpen: function onOpen() { // can't use .classList.add() because it is not supported in IE11 for SVG elements target.setAttribute('class', 'svgIcon selected'); }, onClose: function () { // can't use .classList.remove() because it is not supported in IE11 for SVG elements target.setAttribute('class', 'svgIcon'); this._closeFlyout(); }.bind(this) }; // Toggle open/close of add scene flyout if (this._flyout) { options.onClose(); } else { this._showContextMenu(options); } }, _showContextMenu: function _showContextMenu(options) { if (this.preventContext || !this.controller.isAuthoring()) { this.preventContext = false; } else if (this.controller.isAuthoring()) { this.$el.append(this.flyoutHolder); this._flyout = React.createElement(StorytellingUI.AddSceneFlyout, { anchorElement: options.target, closeFlyout: options.onClose, onOpen: options.onOpen, onLayoutSelection: this.controller.addScene.bind(this.controller), stringResources: this.services.getSvcSync('.StringResources'), sceneLayoutListing: JSON.parse(sceneLayoutListing).sceneLayouts }); ReactDOM.render(this._flyout, this.flyoutHolder); } }, _closeFlyout: function _closeFlyout() { if (this._flyout) { ReactDOM.unmountComponentAtNode(this.flyoutHolder); this.$el.find('.flyout-holder').remove(); this._flyout = null; this.preventContext = false; } }, /** * Event handler for 'scene:reorder' event from TimelineController */ onSceneReorder: function onSceneReorder(event) { var swapElements = function swapElements(element, target) { if (element !== target) { var parent = element.parentNode; var placeholder = parent.insertBefore(document.createTextNode(''), element); parent.insertBefore(element, target); parent.insertBefore(target, placeholder); parent.removeChild(placeholder); } }; // Get the updated list of scenes (after they've been reordered) // and update the FilmStrip's scene list var updatedSceneList = event.model.getParent().items; _.each(updatedSceneList, function (scene, index) { var $currentCell = this.$sceneList.children().eq(index); var $cell = this.$sceneList.find(this._getSceneSelector(scene.id)); swapElements($cell[0], $currentCell[0]); }, this); // Remove any coach marks from the scene when it is moved var $scene = this.$sceneList.find('#' + event.model.id + '_tablabel>.expandScene'); $scene.find('.coachMark').remove(); this._refreshCellPageNumbers(); }, /** * The method used when listening for the 'dragstart' (hammer) event * @param {Event} event A mouse event */ onSceneDragStart: function onSceneDragStart(event) { if (!DomUtils.isPointerTouch(event) && this.controller.isAuthoring()) { this._startReorderScenes(event); } }, /** * The method used when listening for the 'hold' (hammer) event * @param {Event} event A mouse event */ onSelectedSceneHold: function onSelectedSceneHold(event) { if (event.currentTarget.classList.contains('selected') && this.controller.isAuthoring()) { this._shakeOnHold(event.currentTarget); this._startReorderScenes(event); } }, /** * @private * @param {Event} event The event generated from a mouse drag or touch hold */ _startReorderScenes: function _startReorderScenes(event) { var _this8 = this; var sceneEl = event.currentTarget; var $sceneEl = $(sceneEl); var $avatar = this._makeAvatar($sceneEl); this._cloneDragEl = $avatar[0].cloneNode(); this._cloneDragEl.classList.remove('sceneMoveAvatar'); this._cloneDragEl.classList.add('scenePlaceholder'); this._dragModelIndex = this.controller.getSceneIndex(event.currentTarget.dataset.modelId); var sceneModel = this.controller.getSceneById(event.currentTarget.dataset.modelId); var targetSelected = sceneEl.classList.contains('selected'); /* Make sure the avatar is aligned with the labels*/ var offset = $sceneEl.offset(); var eventPos = DomUtils.getEventPos(event); var avatarYOffset = offset.top - eventPos.pageY; var avatarXOffset = offset.left - eventPos.pageX; this.dndManager.startDrag({ event: event, type: 'sceneCell', data: sceneModel, avatar: $avatar[0], moveXThreshold: 20, // move horizontally 20 pixels to enable dragging. This will allow vertical scrolling/panning avatarYOffset: avatarYOffset, avatarXOffset: avatarXOffset, restrictToXAxis: true, callerCallbacks: { onDragStart: function onDragStart() { _this8.$el.addClass('noScroll'); _this8._dropNodeModelId = null; _this8.$sceneList[0].insertBefore(_this8._cloneDragEl, sceneEl); sceneEl.style.display = 'none'; sceneEl.classList.remove('selected'); sceneEl.classList.remove('shake'); }, onDragDone: function onDragDone() { if (_this8._dropNodeModelId) { var dropModelIndex = _this8.controller.getSceneIndex(_this8._dropNodeModelId); var dropModel = _this8.controller.getSceneById(_this8._dropNodeModelId); _this8.beforeModelId = dropModel.getNextSiblingId(); if (_this8._dragModelIndex > dropModelIndex) { _this8.beforeModelId = dropModel.id; } } _this8.$sceneList[0].insertBefore(sceneEl, _this8._cloneDragEl); if (BrowserUtils.isIE11()) { _this8.$sceneList[0].removeChild(_this8._cloneDragEl); } else { _this8._cloneDragEl.remove(); } sceneEl.style.display = ''; if (targetSelected) { sceneEl.classList.add('selected'); } _this8.el.classList.remove('noScroll'); if (_this8._dropNodeModelId) { _this8.controller.moveViewBefore(sceneModel, _this8.beforeModelId); } _this8.beforeModelId = null; _this8._cloneDragEl = null; _this8._dropNodeModelId = null; return true; } } }); }, /** * Clone an element and append it to the body of the document * @private * @param {jQuery} $target A jQuery object representing an element on the page that will be cloned * * @return {jQuery} The cloned element as a jquery object */ _makeAvatar: function _makeAvatar($target) { var $avatar = $target.clone().addClass('sceneMoveAvatar'); var avatar = $avatar[0]; // Remove some unneeded attributes (from the clone) before placing the element on the page avatar.removeAttribute('id'); avatar.removeAttribute('tabindex'); avatar.removeAttribute('role'); avatar.removeAttribute('data-model-id'); $avatar.find('.overflow').remove(); $avatar.find('.expandScene').remove(); $avatar.find('.pageInfo').remove(); document.body.appendChild(avatar); return $avatar; }, onWidgetAdded: function onWidgetAdded(event) { var sceneId = this._getWidgetSceneId(event.id); this._widgetIndicated(sceneId); }, onWidgetRemoved: function onWidgetRemoved(event) { var sceneId = this._getWidgetSceneId(event.id); this._widgetIndicated(sceneId); }, /* * Helpers */ _getWidgetSceneId: function _getWidgetSceneId(widgetId) { var result = void 0; if (this.controller && this.controller.model) { var parent = this.controller.model.layout.findTopLevelParentItem(widgetId); if (parent) { result = this.controller.getSceneIndex(parent.id) > -1 ? parent.id : null; } } return result; }, _widgetIndicated: function _widgetIndicated(sceneId) { var toggle; if (sceneId) { toggle = this.controller.isScenePopulated(sceneId); this.$el.find(this._getSceneSelector(sceneId) + ' .widgetIndicator').toggleClass('widgetIndicated', toggle); } else { toggle = this.timelineController.getTimelineEpisodeCount() > 0; this.$el.find('.scene.selected .widgetIndicator').toggleClass('widgetIndicated', toggle); } }, _widgetIndicatedMove: function _widgetIndicatedMove(moveSceneId, $cell) { //update widget indicator for selected scene this._widgetIndicated(); //update widget indicator for scene widget is moved to var toggle = this.controller.isScenePopulated(moveSceneId); $cell.find('.widgetIndicator').toggleClass('widgetIndicated', toggle); }, _findTopLevelParentItem: function _findTopLevelParentItem(value) { var parents = []; if (value && value.parameter && value.parameter.updateArray) { var array = value.parameter.updateArray; parents = _.map(array, function (element) { var parent = this.controller.model.layout.findTopLevelParentItem(element.parentId); return parent && parent.id && !this.controller.isOverview() ? parent.id : element.parentId; }.bind(this)); } return parents; }, onChangeLayout: function onChangeLayout(event) { this._updateWidgetIndicator(event); if (this.timelineView) { this._showTimeline(0); } }, _updateWidgetIndicator: function _updateWidgetIndicator(event) { var prevParentId = this._findTopLevelParentItem(event.prevValue); var newParentId = this._findTopLevelParentItem(event.value); if (prevParentId.length === 0 || newParentId.length === 0 || _.intersection(prevParentId, newParentId).length !== 0) { return; } for (var i = 0; i < prevParentId.length; i++) { this._widgetIndicated(prevParentId[i]); } for (i = 0; i < newParentId.length; i++) { this._widgetIndicated(newParentId[i]); } }, _toggleModeChanged: function _toggleModeChanged(authoringFlag) { this.$el.toggleClass('authoring', authoringFlag); var $addSceneEl = this.$el.find('.addScene'); if (authoringFlag) { if ($addSceneEl.length === 0) { this.renderAddSceneCell(); } } else { // Otherwise remove the add scene cell and its events $addSceneEl.off().remove(); } // Close timeline if user is leaving authoring mode if (!authoringFlag && this.timelineView) { this.controller.collapseScene(); } }, _getSceneSelector: function _getSceneSelector(id) { return '#' + id + '_tablabel'; }, /** * Add a scene cell to the appropriate place in the sceneList * @private */ _addScene: function _addScene(scene, index, insertBeforeId) { var $cell = this.renderSceneCell(scene, index); if (insertBeforeId) { this.$sceneList.find(this._getSceneSelector(insertBeforeId)).before($cell); } else { this.$sceneList.find('.scene:last-child').after($cell); } this._addExpandSceneCoachmark(); }, _removeScene: function _removeScene(id) { // When a scene is removed from the film strip, we lose focus of the film strip. // selectScene() only focuses on a scene if the film strip is focused, so we force refocus. this.forceReFocus = true; var sceneSelector = this._getSceneSelector(id); var sceneCell = this.$sceneList[0].querySelector(sceneSelector); this._dropZones = this._dropZones.filter(function (zone) { if (zone.node === sceneCell) { zone.dropZone.remove(); zone.node.parentNode.removeChild(zone.node); return false; } return true; }); this.translationService.deregisterView(id); }, _refreshCellPageNumbers: function _refreshCellPageNumbers() { var $scenes = this.$sceneList.children(); for (var i = 0; i < $scenes.length; i++) { $scenes.eq(i).find('.pageNumber')[0].setAttribute('data-index', i + 1); } // if beginning and end overviews exist, reset their id // this will change once we allow enabling/disabling each overview var $overviews = this.$el.find('.overview'); if ($overviews.length >= 2) { $overviews.eq(0).find('.pageNumber')[0].setAttribute('data-index', 0); $overviews.eq(1).find('.pageNumber')[0].setAttribute('data-index', $scenes.length + 1); } }, /** * Add an empty div with 100% width to force the scroll view to be scrollable. * * Reason: scrolling of the scroll view is necessary during the animation to make it look slick. * When the scroll view's content is not scrollable (ie. 1 or 2 scenes), then the scroll animation * can only occur once the resizing cell is large enough to introduce scroll in the parent. This * results in a jarring animation. */ _createAnimationDummyDiv: function _createAnimationDummyDiv() { return $('
').css({ width: '100%', height: '1px', display: 'inline-block' }); }, _expandScene: function _expandScene($scene) { var _this9 = this; var scrollLeft = this.$el.scrollLeft(); var position = $scene.position(); var marginLeft = parseInt($scene.css('margin-left'), 10); var originalWidth = $scene.css('width'); var originalHeight = $scene.css('height'); var $sceneElements = $scene.find('.sceneOverlay, .widgetIndicator'); var duration = this.enlargeDuration; // Add an empty div with 100% width to force the scroll view to be scrollable. var dummyDiv = this._createAnimationDummyDiv().appendTo(this.$el); // remove coachmark this._removeExpandSceneCoachmark(); // close flyout this._closeFlyout(); this.$el // Issue on Firefox: scroll location changes when appending items. Reset the scroll. .scrollLeft(scrollLeft) // Prevent scrollbars from showing during the animation. .css('overflow-x', 'hidden'); // The animation is about to begin. $scene.addClass('animating'); // Fade out the overlay elements and the widgetIndicator element $sceneElements.fadeOut(duration / 3); // Animate the cell to fit the full width and height. $scene.animate({ width: this.$el.outerWidth(true), height: this.$el.outerHeight(true), opacity: 0 }, { duration: duration, complete: function complete() { _this9._showTimeline(duration / 4, function () { // Hide the film strip view as we are now showing the timeline view. _this9.$el.hide(); // The animation is finished. $scene.removeClass('animating'); // Reset the cell size. $scene.css({ width: originalWidth, height: originalHeight, opacity: 1 }); // Reset the display overlays and widgetIndicator $sceneElements.css('display', ''); // Remove the dummy div. dummyDiv.remove(); }); } }); // Animate to the end scroll position. this.$el.animate({ scrollLeft: position.left + marginLeft + scrollLeft }, duration); }, _collapseScene: function _collapseScene($scene) { var _this10 = this; this.$el.show(); var scrollLeft = this.$el.scrollLeft(); var fullWidth = this.$el.outerWidth(true); var fullHeight = this.$el.outerHeight(true); var marginLeft = parseInt($scene.css('margin-left'), 10); var originalWidth = $scene.css('width'); var originalHeight = $scene.css('height'); var duration = this.collapseDuration; // Add an empty div with 100% width to force the view to be scrollable. var $dummyDiv = this._createAnimationDummyDiv().appendTo(this.$sceneList); // Issue on Firefox: scroll location changes when appending items. Reset the scroll. this.$el.scrollLeft(scrollLeft); // The animation is about to begin. $scene.addClass('animating'); // Fade-out and remove the timeline. if (this.timelineView) { this.timelineView.fadeOut({ complete: function complete() { _this10.timelineView.remove(); _this10.timelineView = null; }, duration: duration / 4 }); } // Enlarge the overlays and widgetIndicator after the cell animation completes. $scene.find('.sceneOverlay, .widgetIndicator').css({ transform: 'scale(0)', opacity: 0 }).delay(duration).animate({ transform: 1, opacity: 1 }, { step: function step(now, fx) { if (fx.prop === 'transform') { this.style.transform = 'scale(' + now + ')'; } }, complete: function complete() { this.style.transform = null; this.style.opacity = null; }, duration: duration / 2 }); // Prior to animation: set the resize cell to the full width and height. $scene.css({ width: fullWidth, height: fullHeight, opacity: 0 }); // Prior to animation: set the scroll so that cell is aligned in the page. scrollLeft = this.$el.scrollLeft(); this.$el.scrollLeft($scene.position().left + marginLeft + scrollLeft); // Animate back to the original cell size. $scene.animate({ width: originalWidth, height: originalHeight, opacity: 1 }, { duration: duration, complete: function complete() { // The animation is finished. $scene.removeClass('animating'); // Reset scrollbars. _this10.$el.css('overflow-x', 'auto'); // Remove the dummy div. $dummyDiv.remove(); } }); // Animate to the end scroll position. this.$el.animate({ scrollLeft: scrollLeft - parseInt(this.$sceneList.css('margin-left'), 10) }, duration); }, _refreshTimeline: function _refreshTimeline() { var _this11 = this; if (this.timelineView) { this.timelineView.fadeOut({ complete: function complete() { // Guard against concurrent dismissal of the timeline, for instance. if (_this11.timelineView) { _this11._showTimeline(_this11.enlargeDuration / 4); } } }); } }, _showTimeline: function _showTimeline(duration, complete) { if (this.timelineView) { this.timelineView.remove(); this.timelineView = null; } if (this.timelineController.getCurrentScene() !== null) { this._createTimeline(); this.timelineView.renderFadeIn({ complete: complete, duration: duration }); } else { // If execution gets in here it means one of two things: // 1. User navigated to an overview (in GJ) // 2. The bug in IE when converting caused current scene data to be lost var sceneIndex = this.controller.currentSceneIndex; if (sceneIndex === -1) { sceneIndex = 0; } else if (sceneIndex === -2) { sceneIndex = this.controller.getSceneCount() - 1; } this._collapseScene(this.$sceneList.children().eq(sceneIndex)); } }, _createTimeline: function _createTimeline() { var timelineNode = document.createElement('div'); this.contentEl.appendChild(timelineNode); // FIXME: This line is bad. Filmstrip is technically reaching out of its scope this.timelineView = new TimelineView({ el: timelineNode, storyController: this.controller, controller: this.timelineController, dashboardApi: this.dashboardApi, dndManager: this.dndManager, glassContext: this.glassContext, services: this.services }); }, _removeExpandSceneCoachmark: function _removeExpandSceneCoachmark() { // following line is document scoped because we may need to remove in many opened stories. // After some testing it seems like glass now removes an inactive perspective's HTML from the DOM // so this document scoped query isn't needed anymore as it doesn't work as desired var $firstScenes = $('.expandScene.coachMarkContainer'); if ($firstScenes.find('.coachMark:visible').length) { var coachMarkReadId = $firstScenes.attr('id'); var persistence = this.dashboardApi.getGlassCoreSvc('.CoachMarkService').getPersistence(); persistence.marksAsRead(coachMarkReadId); $firstScenes.find('.coachMark').hide(); } }, _addExpandSceneCoachmark: function _addExpandSceneCoachmark() { var $target = this.$sceneList.find('.expandScene').first(); var options = { id: 'com.ibm.bi.dashboard.filmstrip.expandScene', domElement: $target[0], title: stringResources.get('expandSceneCoachmarkTitle'), contents: stringResources.get('expandSceneCoachmarkContents') }; this.dashboardApi.prepareGlassOptions(options); var coachMarkApi = this.dashboardApi.getFeature('CoachMark'); return coachMarkApi.addCoachMark(options); }, _addOverviewSceneCoachmark: function _addOverviewSceneCoachmark() { var $target = this.$el.find('.overview').first(); if ($target.length === 1) { var options = { id: 'com.ibm.bi.dashboard.filmstrip.overview', domElement: $target[0], title: stringResources.get('overviewCoachmarkTitle'), contents: stringResources.get('overviewCoachmarkContents') }; this.dashboardApi.prepareGlassOptions(options); var coachMarkApi = this.dashboardApi.getFeature('CoachMark'); return coachMarkApi.addCoachMark(options); } return Promise.resolve(); }, _selectScene: function _selectScene(event) { this._closeFlyout(); var sceneId = event.scene && event.scene.id; if (!sceneId) { return; } var $scene = void 0; if (sceneId === 'start' || sceneId === 'end') { $scene = this.$el.find('.overview.' + sceneId); } else { $scene = this.$sceneList.find(this._getSceneSelector(sceneId)); } if ($scene.length !== 1) { return; } this.$el.find('.overview.selected, .scene.selected').removeClass('selected').attr('tabindex', -1); $scene.addClass('selected').attr('tabindex', 0); if (this.forceReFocus || $.contains(this.el, document.activeElement) && $scene.is(':visible')) { // If we are focused inside the film strip or if the force refocus flag is set, move the focus. $scene.focus(); this.forceReFocus = false; } var scroller = this._getSceneScrollPosition($scene); this.$el.scrollLeft(scroller.scrollLeft).animate({ scrollLeft: scroller.position }, 400); }, /** * @param {Element} target The DOM element to add the shake class to */ _shakeOnHold: function _shakeOnHold(target) { this._closeFlyout(); this.preventContext = true; target.classList.add('shake'); this.el.classList.add('noScroll'); // note: we use "one" here to make sure it only runs once (for each event type) $(target).one('mouseup touchup', function () { target.classList.remove('shake'); this.el.classList.remove('noScroll'); this.preventContext = false; }.bind(this)); }, _wrapEvent: function _wrapEvent(event) { return { code: event.keyCode || event.charCode, modifier: event.ctrlKey || event.metaKey, altKey: event.altKey, shiftKey: event.shiftKey, originalEvent: event }; }, onKeydown: function onKeydown(event) { var wrappedEvent = this._wrapEvent(event); if (this._shouldMoveSceneLeft(wrappedEvent)) { // Ctrl-Left/Ctrl-Down this._moveSceneLeft(this._getSceneIdFromElement(event)); } else if (this._shouldMoveSceneRight(wrappedEvent)) { // Ctrl-Right/Ctrl-Up this._moveSceneRight(this._getSceneIdFromElement(event)); } else if (this._shouldNavigateLeft(wrappedEvent)) { // Left or Down this.controller.previousScene(); } else if (this._shouldNavigateRight(wrappedEvent)) { // Right or Up this.controller.nextScene(); } else if (this._shouldShowContextMenu(wrappedEvent)) { // Shift+F10 - Show toolbar this.onSceneOverflowClick(event); } else if (this._shouldDeleteScene(wrappedEvent)) { // Delete or Backspace (With no modifier keys) this.controller.deleteScene(this._getSceneIdFromElement(event)); } else if (this._shouldPerformEnter(wrappedEvent)) { // Enter $(event.target).click(); } else if (this._shouldPerformExpand(wrappedEvent)) { //F10 - expand the scene this.controller.expandScene(); } else if (this._shouldPerformTogglePlayPause(wrappedEvent)) { // Spacebar - toggle play/pause this.controller.togglePlayPause(); } else { // Don't stop propagation or prevent default. return; } event.stopPropagation(); event.preventDefault(); }, _getSceneIdFromElement: function _getSceneIdFromElement(event) { return event.currentTarget.dataset.modelId; }, _shouldMoveSceneLeft: function _shouldMoveSceneLeft(event) { return event.modifier && (event.code === 37 || event.code === 38); //Ctrl-Left/Ctrl-Down }, _shouldMoveSceneRight: function _shouldMoveSceneRight(event) { return event.modifier && (event.code === 39 || event.code === 40); //Ctrl-Right/Ctrl-Up }, // Left or Down _shouldNavigateLeft: function _shouldNavigateLeft(event) { return event.code === 37 || event.code === 38; }, // Right or Up _shouldNavigateRight: function _shouldNavigateRight(event) { return event.code === 39 || event.code === 40; }, // Delete or Backspace (With no modifier keys) _shouldDeleteScene: function _shouldDeleteScene(event) { var sceneId = this._getSceneIdFromElement(event.originalEvent), canDelete = sceneId && this.controller.isAuthoring() && this.controller.getSceneCount() > 1, isDeleteOrBackspace = event.code === 46 || event.code === 8, notCtrlOrAlt = !event.modifier && !event.altKey; return isDeleteOrBackspace && notCtrlOrAlt && canDelete; }, // Shift-F10 when scene on scene _shouldShowContextMenu: function _shouldShowContextMenu(event) { return event.shiftKey && event.code === 121; }, _shouldPerformEnter: function _shouldPerformEnter(event) { return event.code === 13 && !$(event.originalEvent.target).hasClass('coachMark'); }, // F10 in windows when scene on scene _shouldPerformExpand: function _shouldPerformExpand(event) { return event.code === 121; }, _shouldPerformTogglePlayPause: function _shouldPerformTogglePlayPause(event) { return event.code === 32; }, _moveSceneLeft: function _moveSceneLeft(sceneId) { if (this.controller.isAuthoring() && sceneId) { this.controller.moveSceneLeft(sceneId); } }, _moveSceneRight: function _moveSceneRight(sceneId) { if (this.controller.isAuthoring() && sceneId) { this.controller.moveSceneRight(sceneId); } }, _getSceneScrollPosition: function _getSceneScrollPosition($scene) { var scrollLeft = this.$el.scrollLeft(); var position = $scene.position(); var marginLeft = parseInt($scene.css('margin-left'), 10); var width = this.$el.outerWidth(); var left = position.left, right = position.left + $scene.outerWidth(true); if (left < 0) { left += scrollLeft - marginLeft - 30; } else if (right > width) { left = scrollLeft + marginLeft + 30 + (right - width); } else { left = scrollLeft; } if ($scene.index() === this.$sceneList.children().length - 1) { left += $scene.width() * 2; } return { position: left, scrollLeft: scrollLeft }; } }); return FilmStripView; }); //# sourceMappingURL=FilmStripView.js.map