'use strict'; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /** * Licensed Materials - Property of IBM * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2014, 2022 * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp. */ define(['../../lib/@waca/core-client/js/core-client/ui/core/View', '../../lib/@waca/core-client/js/core-client/utils/Utils', '../../lib/@waca/core-client/js/core-client/utils/BrowserUtils', 'underscore', 'jquery', 'hammerjs', 'text!./templates/Splitter.html'], function (View, CoreUtils, BrowserUtils, _, $, Hammer, Template) { var Splitter = function (_View) { _inherits(Splitter, _View); function Splitter() { _classCallCheck(this, Splitter); for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _possibleConstructorReturn(this, _View.call.apply(_View, [this].concat(args))); } /** * Create a splitter pane. Support attributes: * * items: * A list of item objects containing View objects and handle details: * [{ * handleIcon: , * handleClass: , * handleOpenTitle: , * handleCloseTitle: , * view: , * hidden: * },...] * * When hidden is true the item is not shown * * @param options */ Splitter.prototype.init = function init() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; this.templateString = Template; if (options.$el && options.$el.length > 0) { //Note that this DOM node gets passed in so this view does not really own it //therefore ensure the remove function must not clean it up, not sure why it was done like thiss this.el = options.$el[0]; } _View.prototype.init.call(this, options); this.content = options.content; this.handlers = options.handlers || { getParentSize: function getParentSize() { return { height: Infinity, // and beyond.. I had to width: Infinity }; } }; // Iterate over the items given to us and make sure we have some sane defaults this.items = (options.items || []).map(function (item) { if (item.defaultTabIndex === undefined) { item.defaultTabIndex = item.hidden ? '-1' : '0'; } return item; }); this.panel = options.panel; this.currentIndex = -1; this.isDragStartCalled = false; if (BrowserUtils.isIPad()) { //Add a touchmove handler to the document on iPad to kill default page scroll when dragging. document.addEventListener('touchmove', this.preventPageScrollDrag.bind(this), { passive: false }); } // FIXME: Why pass instance property to instance method? this.overrideGlobalMaxSize = this.getOverrideGlobalMaxSize(this.items); this._setupEvent(); this.$el.children().addClass('splitterPane'); var sHtml = this.dotTemplate({ handles: this.items }); this.$el.children(this.panel).before(sHtml); this.$el.find('.handle').each(function () { var icon = this.dataset.icon || ''; CoreUtils.setIcon($(this), icon); }); this.$splitterBar = this.$el.find('.splitterBar'); var visibleItems = _.filter(this.items, function (item) { return !item.hidden; }); if (visibleItems.length === 0) { this.$splitterBar.find('.splitterSizeHandle').hide(); } if (this._shouldHideHandle()) { // The height of the splitter bar is 0px by default // Need to set the height in order to show splitterSizeHandle this.$splitterBar.addClass('showSplitterSizeHandle'); } _.each(this.items, function (item) { if (item.hidden || this._shouldHideHandle()) { this.$splitterBar.find('.' + item.handleClass).hide(); } }, this); this.$handles = this.$splitterBar.find('.handle'); this.$handles.each(function (index, value) { $(value).attr('title', this.items[index].handleOpenTitle); $(value).attr('aria-label', this.items[index].handleOpenTitle); }.bind(this)); this.renderedMap = {}; $(window).off('resize.splitterResize' + this.viewId).on('resize.splitterResize' + this.viewId, this.onResize.bind(this)); $.each(this.$splitterBar.find('.handle, .splitterSizeHandle'), function (i, e) { Hammer(e).on('dragstart', this.onHandleDragStart.bind(this)).on('dragup dragdown', this.onHandleDragDownUp.bind(this)).on('dragend', this.onHandleDragEnd.bind(this)).on('tap', this.onHandleClick.bind(this)); $(e).on('keydown', this.onHandleKeyDown.bind(this)); }.bind(this)); }; Splitter.prototype.preventPageScrollDrag = function preventPageScrollDrag(evt) { if (this.isDragStartCalled) { evt.preventDefault(); } }; Splitter.prototype._setupEvent = function _setupEvent() { //This class does not own the items views so no need to unregister handlers them during remove since the views unregister handlers themselves when they get remove (this.items || []).forEach(function (item, index) { var _this2 = this; var openPaneAction = this.toggleItem.bind(this, index, true); item.view.on('openPane', function () { openPaneAction(); }); item.view.on('closePane', function () { // TODO: stub, implement as needed }); item.view.on('hidePane', function () { // TODO: stub, implement as needed }); item.view.on('showPane', function () { // TODO: stub, implement as needed }); item.view.on('disableHandle', function () { _this2._toggleItemHandle(index, true); }); item.view.on('enableHandle', function () { _this2._toggleItemHandle(index, false); }); // disableAndSwitchHandle is to be used when we want to automatically // switch off the current handle to the next or previous one // If we do switch we save where we came from to restore if/when // the 'restoreHandle' event is triggered item.view.on('disableAndSwitchHandle', function () { _this2._preservedIndex = null; item.view.trigger('disableHandle'); var $handle = _this2.$handles.eq(index); if ($handle.hasClass('selected')) { $handle.removeClass('selected'); // Find next viable handle to switch to here var nextHandleIndex = _this2._getNextAvailableHandle(index); _this2.toggleItem(nextHandleIndex, true); // preserve the handle index the user was on to automatically put them back if required _this2._preservedIndex = index; } }); // re-enable the handle that was disabled and if there wasn't a use action // on the splitter handles between the 'disableAndSwitchHandle' and // 'restoreHandle' events then we set the user back to the preserved // index handle automatically. item.view.on('restoreHandle', function () { item.view.trigger('enableHandle'); if (_this2._preservedIndex === index) { _this2.toggleItem(_this2._preservedIndex, true); _this2._preservedIndex = null; } }); }, this); }; Splitter.prototype._shouldHideHandle = function _shouldHideHandle() { return this.items && this.items.length === 1; }; /** * @param {number} index An number value relating to the position the elemtn is in the DOM relative to its parent element * @param {boolean} [state=false] A value to pass to the jquery 'toggleClass' function. Used to determine whether the class * should be added or removed. Default set to false in case it is called in error we don't deny user access to parts of the UI */ Splitter.prototype._toggleItemHandle = function _toggleItemHandle(index) { var state = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; this.$handles.eq(index).toggleClass('disabled blockOnExpandView', state); }; Splitter.prototype._getNextAvailableHandle = function _getNextAvailableHandle(index) { if (index < 0) { index = 0; } else if (index >= this.items.length) { index = this.items.length - 1; } return index < this.items.length - 1 ? index + 1 : index - 1; }; Splitter.prototype.getOverrideGlobalMaxSize = function getOverrideGlobalMaxSize(items) { var overrideGlobalMaxSize = Infinity; (items || []).forEach(function (item) { if (item.overrideGlobalMaxSize) { if (overrideGlobalMaxSize !== Infinity) { console.warn('Multiple modules are trying to override the global max splitter size.'); } overrideGlobalMaxSize = item.overrideGlobalMaxSize; } }, this); return overrideGlobalMaxSize; }; Splitter.prototype.remove = function remove() { //@todo jQuery does not seem to work with remove event listener here $(window).off('resize.splitterResize' + this.viewId); if (BrowserUtils.isIPad()) { //Add a touchmove handler to the document on iPad to kill default page scroll when dragging. document.removeEventListener('touchmove', this.preventPageScrollDrag.bind(this), { passive: false }); } var sizeHandles = this.$splitterBar.find('.handle, .splitterSizeHandle'); if (sizeHandles instanceof $ && sizeHandles.length > 0) { // Be aware of that sizeHandles is a JQuery object. It's not safe to use `for...of...`. `.each()` or `jQuery.each` would be safer. for (var i = 0; i < sizeHandles.length; i++) { var $handle = $(sizeHandles[i]); $handle.off('dragstart dragup dragdown dragend tap keydown'); } } this.$el.children().removeClass('splitterPane'); $('.splitterBar', this.$el).remove(); this.content = null; this.handlers = null; this.items = null; this.panel = null; this.$splitterBar = null; //This view does not own this DOM node so just set it to null here //Because this don't call the base class remove since this base will invoke this.$el.remove() which is not what this //view wants since it does not not the this.$el node this.$el = null; if (this.off) { this.off(); } }; /** * Splitter bar click handler * * @param evt */ Splitter.prototype.onHandleClick = function onHandleClick(evt) { var _this3 = this; var result = void 0; if (!this._isAnimating) { var index = 0; if (!this._shouldHideHandle()) { index = this.getHandleIndexFromNode($(evt.currentTarget)); } result = this.toggleItem(index).then(function () { if (_this3.currentIndex != null) { _this3.items[_this3.currentIndex].view.trigger('splitterPanel:visibleChange'); } }); } return result ? result : Promise.resolve(); }; /** * Splitter bar keydown handler * * @param evt */ Splitter.prototype.onHandleKeyDown = function onHandleKeyDown(evt) { var _map = { 13: this.onHandleClick.bind(this, evt), 40: this._handleUpDownKey.bind(this, evt, 1), 38: this._handleUpDownKey.bind(this, evt, -1) }; var result; if (evt.keyCode in _map) { result = _map[evt.keyCode].call(this); } return result ? result : Promise.resolve(); }; Splitter.prototype._handleUpDownKey = function _handleUpDownKey(event, direction) { event.stopPropagation(); var sizePropName = this._getSizePropName(); this.prevSize = this.$splitterBar.prev()[sizePropName](); this.nextSize = this.panel[sizePropName](); this._adjustSize(direction, 10, this.currentIndex); }; Splitter.prototype.isOpen = function isOpen() { if (this.$splitterBar && this.$splitterBar.hasClass) { return this.$splitterBar.hasClass('open'); } return false; }; /** * Toggle the specified handle open or closed. * * @param index - the index of the handle * @param toggle - an optional value of whether to show or hide the pane. Leaving this parameter blank will * simply toggle the pane between show or hide. * @param options - an optional object to carry extra options. */ Splitter.prototype.toggleItem = function toggleItem(index, toggle, options) { var _this4 = this; // Do nothing when the index is out of bounds. if (index < 0 || index >= this.items.length) { return Promise.resolve(false); } var isOpen = this.isOpen(); if (toggle === undefined) { if (index === this.currentIndex) { // Only toggle the current index. toggle = !isOpen; } else { // Always expand. toggle = true; } } var height = this._getHeight(index, toggle, isOpen, options); if (height <= 0 && this.items[index]) { this.items[index].view.trigger('splitterPanel:hide', options); } this.$handles.each(function (index, value) { $(value).attr('title', this.items[index] && this.items[index].handleOpenTitle); $(value).attr('aria-label', this.items[index] && this.items[index].handleOpenTitle); }.bind(this)); if (height > 0) { var handle = this.$handles.eq(index); handle.attr('title', this.items[index].handleCloseTitle); handle.attr('aria-label', this.items[index].handleCloseTitle); } else { this.$handles.eq(index).removeClass('selected'); this.currentIndex = null; this._preservedIndex = null; } this._isAnimating = true; this.$splitterBar.addClass('animating'); return this.setPaneSize(height).finally(function () { _this4.$splitterBar.removeClass('animating'); _this4._isAnimating = false; return height; }); }; Splitter.prototype._getHeight = function _getHeight(index, toggle, isOpen, options) { var height; if (toggle && index !== null) { if (isOpen && this.items[this.currentIndex]) { height = Math.max(this.items[this.currentIndex].size, this.items[index].view.minimumSize || -8); } else { height = this.items[index].size || this.items[index].initialSize; } // If no height is given, use max height. if (!height) { height = '100%'; } //use the global override if the size hasn't been set by tbe user if (this.overrideGlobalMaxSize !== Infinity && !this.items[index].size) { height = this.overrideGlobalMaxSize; } // Toggling on, load the correct panel. this.selectHandle(index, options); } else { height = 0; } return height; }; /** * Set the pane size * * @param $splitterBar - splitter bar that controls the pane * @param size */ Splitter.prototype.setPaneSize = function setPaneSize(size) { var _this5 = this; var sizePropName = this._getSizePropName(); if (size !== '100%' && !!this.isRestrictToParentSize) { var parentSize = this.handlers.getParentSize(); size = Math.round(Math.min(size, parentSize[sizePropName])); } var open = parseInt(size, 10) > 0; var animation = {}; animation[sizePropName] = size; return new Promise(function (resolve, reject) { try { _this5.panel.animate(animation, 300, function () { try { if (!open) { _this5.$splitterBar.toggleClass('open', false); } else { _this5.$splitterBar.toggleClass('open', true); if (_this5.items[_this5.currentIndex]) { // Store this height. _this5.items[_this5.currentIndex].size = size; } } $(window).resize(); } catch (err) { console.error(err); } resolve(size); }); } catch (error) { reject(error); } }); }; Splitter.prototype.selectHandle = function selectHandle(index, options) { if (this.currentIndex !== index) { this.loadPanel(index); var $handles = this.$splitterBar.find('.handle'); var $handle = $handles.eq(index); if (!$handle.hasClass('selected')) { $handles.removeClass('selected'); $handle.addClass('selected'); this._preservedIndex = null; if (this.items[index] && this.items[index].view) { this.items[index].view.trigger('splitterPanel:show', options); } } } }; Splitter.prototype.loadPanel = function loadPanel(index) { this.currentIndex = index; var panel = this.items[index]; var panelView = panel.view; if (!this.renderedMap[index]) { panelView.render(); panelView.$el.appendTo(this.panel); this.renderedMap[index] = true; } else { panelView.$el.css('visibility', 'visible'); panelView.$el.css('z-index', '1'); if (this.$splitterBar.hasClass('open')) { panelView.$el.css('opacity', 0); panelView.$el.animate({ opacity: 1 }, 300); } else { this.panel.children().not(panelView.$el).css('visibility', 'hidden'); panelView.$el.css('opacity', 1); } } var children = this.panel.children().not(panelView.$el); children.animate({ opacity: 0 }, 300, function () { children.css('visibility', 'hidden'); children.css('z-index', '-1'); }); }; /** * @return The gripper handle index that matches the selected node */ Splitter.prototype.getHandleIndexFromNode = function getHandleIndexFromNode(node) { return this.$splitterBar.find('.handle').index(node); }; /** * Splitter bar dragStart handler * @param evt */ Splitter.prototype.onHandleDragStart = function onHandleDragStart(evt) { this.isDragStartCalled = true; this.$el.addClass('resizing'); var sizePropName = this._getSizePropName(); this.prevSize = this.$splitterBar.prev()[sizePropName](); this.nextSize = this.panel[sizePropName](); // Sometimes the hammer event doesn't have a gesture member. if (evt.gesture) { evt.gesture.preventDefault(); } var index = this.getHandleIndexFromNode($(evt.currentTarget)); if (this._shouldHideHandle()) { index = 0; } if (index < 0 || index >= this.items.length) { index = this.currentIndex; } this.selectHandle(index); if (!this.$splitterBar.hasClass('open')) { this.$splitterBar.toggleClass('open', true); } }; /** * Splitter bar drag up/down handler * @param evt */ Splitter.prototype.onHandleDragDownUp = function onHandleDragDownUp(evt) { var isMovingBackward = evt.gesture.direction === 'up' || evt.gesture.direction === 'left'; var direction = isMovingBackward ? -1 : 1; var index = this.getHandleIndexFromNode($(evt.currentTarget)); if (this._shouldHideHandle()) { index = 0; } if (index < 0 || index >= this.items.length) { this._adjustSize(direction, evt.gesture.distance, this.currentIndex); } else { this._adjustSize(direction, evt.gesture.distance, index); } this.lastDragEvent = evt; }; Splitter.prototype._adjustSize = function _adjustSize(direction, distance) { var index = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; if (!this.isOpen() && (direction === 1 || direction === -1)) { return; } var sizePropName = this._getSizePropName(); var newPosition = this.nextSize - direction * distance; if (newPosition < this._calculateSizeLimit() && newPosition > 0) { //241557 using keyboard, navigating to data tray and using up/down keys to adjust height, results in blank data tray this.selectHandle(index); this.panel.css(sizePropName, Math.round(newPosition)); } else if (newPosition <= 0 && !this._isAnimating) { this.close(); } // RTC issue #50066 - this next line is needed to prevent a Firefox/JQuery issue where the splitter jumps to the top of the page when dragging. this.panel.css(sizePropName); $(window).resize(); }; Splitter.prototype._calculateSizeLimit = function _calculateSizeLimit() { var parentSize = this.handlers.getParentSize(); var containerHeight = parentSize.height; var limit = containerHeight - 160; // FIXME: where does this magical number come from? return limit; }; Splitter.prototype.onResize = function onResize() { if (this.isOpen()) { var sizePropName = this._getSizePropName(); var sizeLimit = this._calculateSizeLimit(); if (this.panel[sizePropName]() > sizeLimit) { this.panel.css(sizePropName, Math.round(sizeLimit)); } } }; /** * Splitter bar drag end handler * @param evt */ Splitter.prototype.onHandleDragEnd = function onHandleDragEnd() { this.$el.removeClass('resizing'); var sizePropName = this._getSizePropName(); if (this.lastDragEvent && !this._isAnimating) { var gesture = this.lastDragEvent.gesture; var isResizingForward = gesture.direction === 'down' || gesture.direction === 'right'; var velocity = gesture.velocityY; var isSwipe = this.enableSwipe && velocity > 1.5; if (isSwipe) { this._handleSwipe(this.$splitterBar, isResizingForward); } else { this._handleMinSize(this.$splitterBar, sizePropName, isResizingForward); // Save the current height. if (this.currentIndex !== null) { this.items[this.currentIndex].size = this.panel.height(); if (this.items[this.currentIndex].size > 0) { this.items[this.currentIndex].view.trigger('splitterPanel:visibleChange'); } } } } this.isDragStartCalled = false; }; Splitter.prototype._handleSwipe = function _handleSwipe($splitterBar, isResizingForward) { this.setPaneSize($splitterBar, isResizingForward ? 0 : '100%'); }; Splitter.prototype._handleMinSize = function _handleMinSize($splitterBar, sizePropName, isResizingForward) { var _minSize = typeof this.minSize === 'function' ? this.minSize() : this.minSize; if ($splitterBar.prev()[sizePropName]() < _minSize) { // if previous pane ends up being less than our minimun size // set it to 0 if we are expanding 'next' pane or set it to minSize if we are reducing 'next' pane var parentSize = this.handlers.getParentSize(); this.setPaneSize($splitterBar, isResizingForward ? Math.round(parentSize[sizePropName] - _minSize) : '100%'); } else if (this.panel[sizePropName]() < _minSize) { // Pane is less than _minSize - if we are expanding the pane, set it to _minSize, otherwise we collapse it. this.setPaneSize($splitterBar, isResizingForward ? 0 : _minSize); } else { var size = this.panel.css(sizePropName); $splitterBar.toggleClass('open', parseInt(size, 10) > 1); } }; Splitter.prototype._getSizePropName = function _getSizePropName() { return 'height'; }; Splitter.prototype.open = function open() { return this.toggleItem(this.currentIndex, true); }; Splitter.prototype.close = function close() { var item = this.items[this.currentIndex]; if (item) { item.size = 0; } return this.toggleItem(this.currentIndex, false); }; Splitter.prototype.hideHandle = function hideHandle(handleClass) { var $handle = this.$splitterBar.find('.' + handleClass); var $splitterSizeHandle = this.$splitterBar.find('.splitterSizeHandle'); if (!this._shouldHideHandle()) { $handle.hide(); $handle.attr('tabindex', '-1'); } else { $splitterSizeHandle.attr('tabindex', '-1'); } var item = _.findWhere(this.items, { handleClass: handleClass }) || {}; item.hidden = true; var visibleItems = _.filter(this.items, function (item) { return !item.hidden; }); if (visibleItems.length === 0) { $splitterSizeHandle.hide(); } }; Splitter.prototype.hide = function hide() { var _this6 = this; return new Promise(function (resolve, reject) { try { _this6.$splitterBar.fadeOut(250, function () { resolve(); }); } catch (error) { reject(error); } }); }; Splitter.prototype.showHandle = function showHandle(handleClass) { var $splitterSizeHandle = this.$splitterBar.find('.splitterSizeHandle'); $splitterSizeHandle.show(); var $handle = this.$splitterBar.find('.' + handleClass); if (!this._shouldHideHandle()) { $handle.show(); $handle.attr('tabindex', '0'); } else { $splitterSizeHandle.attr('tabindex', '0'); } var item = _.findWhere(this.items, { handleClass: handleClass }) || {}; item.hidden = false; }; Splitter.prototype.show = function show() { var _this7 = this; return new Promise(function (resolve, reject) { try { _this7.$splitterBar.fadeIn(250, function () { resolve(); }); } catch (error) { reject(error); } }); }; return Splitter; }(View); /** * A jQuery function that can be used to enable a splitter pane */ $.fn.splitter = function (options) { this.destroySplitter(); this.data('splitter', new Splitter(_.extend({ $el: this }, options))); return this; }; /** * A jQuery function that can be destroy to remove a splitter pane */ $.fn.destroySplitter = function () { var splitter = this.data('splitter'); if (splitter) { splitter.remove(); this.data('splitter', null); } return this; }; return Splitter; }); //# sourceMappingURL=Splitter.js.map