office-ui-fabric-react
Version: 
Reusable React components for building experiences for Office 365.
412 lines (410 loc) • 19.6 kB
JavaScript
define(["require", "exports", "tslib", "react", "../../Utilities", "./SelectionLayout", "./interfaces"], function (require, exports, tslib_1, React, Utilities_1, SelectionLayout_1, interfaces_1) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    // Selection definitions:
    //
    // Anchor index: the point from which a range selection starts.
    // Focus index: the point from which layout movement originates from.
    //
    // These two can differ. Tests:
    //
    // If you start at index 5
    // Shift click to index 10
    //    The focus is 10, the anchor is 5.
    // If you shift click at index 0
    //    The anchor remains at 5, the items between 0 and 5 are selected and everything else is cleared.
    // If you click index 8
    //    The anchor and focus are set to 8.
    var SELECTION_DISABLED_ATTRIBUTE_NAME = 'data-selection-disabled';
    var SELECTION_INDEX_ATTRIBUTE_NAME = 'data-selection-index';
    var SELECTION_TOGGLE_ATTRIBUTE_NAME = 'data-selection-toggle';
    var SELECTION_INVOKE_ATTRIBUTE_NAME = 'data-selection-invoke';
    var SELECTALL_TOGGLE_ALL_ATTRIBUTE_NAME = 'data-selection-all-toggle';
    var SelectionZone = (function (_super) {
        tslib_1.__extends(SelectionZone, _super);
        function SelectionZone() {
            return _super !== null && _super.apply(this, arguments) || this;
        }
        SelectionZone.prototype.componentDidMount = function () {
            var win = Utilities_1.getWindow(this.refs.root);
            var scrollElement = Utilities_1.findScrollableParent(this.refs.root);
            // Track the latest modifier keys globally.
            this._events.on(win, 'keydown keyup', this._updateModifiers);
            this._events.on(scrollElement, 'click', this._tryClearOnEmptyClick);
        };
        SelectionZone.prototype.render = function () {
            return (React.createElement("div", tslib_1.__assign({ className: 'ms-SelectionZone', ref: 'root', onKeyDown: this._onKeyDown, onMouseDown: this._onMouseDown, onClick: this._onClick, role: 'presentation', onDoubleClick: this._onDoubleClick, onContextMenu: this._onContextMenu }, {
                onMouseDownCapture: this._onMouseDownCapture,
                onFocusCapture: this._onFocus
            }), this.props.children));
        };
        /**
         * In some cases, the consuming scenario requires to set focus on a row without having SelectionZone
         * react to the event. Note that focus events in IE <= 11 will occur asynchronously after .focus() has
         * been called on an element, so we need a flag to store the idea that we will bypass the "next"
         * focus event that occurs. This method does that.
         */
        SelectionZone.prototype.ignoreNextFocus = function () {
            this._shouldHandleFocus = false;
        };
        SelectionZone.prototype._onMouseDownCapture = function (ev) {
            if (document.activeElement !== ev.target && !Utilities_1.elementContains(document.activeElement, ev.target)) {
                this.ignoreNextFocus();
            }
        };
        /**
         * When we focus an item, for single/multi select scenarios, we should try to select it immediately
         * as long as the focus did not originate from a mouse down/touch event. For those cases, we handle them
         * specially.
         */
        SelectionZone.prototype._onFocus = function (ev) {
            var target = ev.target;
            var _a = this.props, selection = _a.selection, selectionMode = _a.selectionMode;
            var isToggleModifierPressed = this._isCtrlPressed || this._isMetaPressed;
            if (this._shouldHandleFocus && selectionMode !== interfaces_1.SelectionMode.none) {
                var isToggle = this._hasAttribute(target, SELECTION_TOGGLE_ATTRIBUTE_NAME);
                var itemRoot = this._findItemRoot(target);
                if (!isToggle && itemRoot) {
                    var index = this._getItemIndex(itemRoot);
                    if (isToggleModifierPressed) {
                        // set anchor only.
                        selection.setIndexSelected(index, selection.isIndexSelected(index), true);
                    }
                    else {
                        this._onItemSurfaceClick(ev, index);
                    }
                }
            }
            this._shouldHandleFocus = false;
        };
        SelectionZone.prototype._onMouseDown = function (ev) {
            this._updateModifiers(ev);
            var target = ev.target;
            var itemRoot = this._findItemRoot(target);
            while (target !== this.refs.root) {
                if (this._hasAttribute(target, SELECTALL_TOGGLE_ALL_ATTRIBUTE_NAME)) {
                    break;
                }
                else if (itemRoot) {
                    if (this._hasAttribute(target, SELECTION_TOGGLE_ATTRIBUTE_NAME)) {
                        break;
                    }
                    else if (this._hasAttribute(target, SELECTION_INVOKE_ATTRIBUTE_NAME)) {
                        this._onInvokeMouseDown(ev, this._getItemIndex(itemRoot));
                        break;
                    }
                    else if (target === itemRoot) {
                        break;
                    }
                }
                target = Utilities_1.getParent(target);
            }
        };
        SelectionZone.prototype._onClick = function (ev) {
            this._updateModifiers(ev);
            var target = ev.target;
            var itemRoot = this._findItemRoot(target);
            // No-op if selection is disabled
            if (this._isSelectionDisabled(target)) {
                return;
            }
            while (target !== this.refs.root) {
                if (this._hasAttribute(target, SELECTALL_TOGGLE_ALL_ATTRIBUTE_NAME)) {
                    this._onToggleAllClick(ev);
                    break;
                }
                else if (itemRoot) {
                    var index = this._getItemIndex(itemRoot);
                    if (this._hasAttribute(target, SELECTION_TOGGLE_ATTRIBUTE_NAME)) {
                        if (this._isShiftPressed) {
                            this._onItemSurfaceClick(ev, index);
                        }
                        else {
                            this._onToggleClick(ev, index);
                        }
                        break;
                    }
                    else if (this._hasAttribute(target, SELECTION_INVOKE_ATTRIBUTE_NAME)) {
                        this._onInvokeClick(ev, index);
                        break;
                    }
                    else if (target === itemRoot) {
                        this._onItemSurfaceClick(ev, index);
                        break;
                    }
                }
                target = Utilities_1.getParent(target);
            }
        };
        SelectionZone.prototype._onContextMenu = function (ev) {
            var target = ev.target;
            var _a = this.props, onItemContextMenu = _a.onItemContextMenu, selection = _a.selection;
            if (onItemContextMenu) {
                var itemRoot = this._findItemRoot(target);
                if (itemRoot) {
                    var index = this._getItemIndex(itemRoot);
                    onItemContextMenu(selection.getItems()[index], index, ev.nativeEvent);
                    ev.preventDefault();
                }
            }
        };
        SelectionZone.prototype._isSelectionDisabled = function (target) {
            while (target !== this.refs.root) {
                if (this._hasAttribute(target, SELECTION_DISABLED_ATTRIBUTE_NAME)) {
                    return true;
                }
                target = Utilities_1.getParent(target);
            }
            return false;
        };
        /**
         * In multi selection, if you double click within an item's root (but not within the invoke element or input elements),
         * we should execute the invoke handler.
         */
        SelectionZone.prototype._onDoubleClick = function (ev) {
            var target = ev.target;
            if (this._isSelectionDisabled(target)) {
                return;
            }
            var _a = this.props, selectionMode = _a.selectionMode, onItemInvoked = _a.onItemInvoked;
            var itemRoot = this._findItemRoot(target);
            if (itemRoot && onItemInvoked && selectionMode !== interfaces_1.SelectionMode.none && !this._isInputElement(target)) {
                var index = this._getItemIndex(itemRoot);
                while (target !== this.refs.root) {
                    if (this._hasAttribute(target, SELECTION_TOGGLE_ATTRIBUTE_NAME) ||
                        this._hasAttribute(target, SELECTION_INVOKE_ATTRIBUTE_NAME)) {
                        break;
                    }
                    else if (target === itemRoot) {
                        this._onInvokeClick(ev, index);
                        break;
                    }
                    target = Utilities_1.getParent(target);
                }
                target = Utilities_1.getParent(target);
            }
        };
        SelectionZone.prototype._onKeyDown = function (ev) {
            this._updateModifiers(ev);
            var target = ev.target;
            if (this._isSelectionDisabled(target)) {
                return;
            }
            var _a = this.props, selection = _a.selection, selectionMode = _a.selectionMode;
            var isSelectAllKey = ev.which === Utilities_1.KeyCodes.a && (this._isCtrlPressed || this._isMetaPressed);
            var isClearSelectionKey = ev.which === Utilities_1.KeyCodes.escape;
            // Ignore key downs from input elements.
            if (this._isInputElement(target)) {
                // A key was pressed while an item in this zone was focused.
                this._shouldHandleFocus = true;
                return;
            }
            // If ctrl-a is pressed, select all (if all are not already selected.)
            if (isSelectAllKey && selectionMode === interfaces_1.SelectionMode.multiple && !selection.isAllSelected()) {
                selection.setAllSelected(true);
                ev.stopPropagation();
                ev.preventDefault();
                return;
            }
            // If escape is pressed, clear selection (if any are selected.)
            if (isClearSelectionKey && selection.getSelectedCount() > 0) {
                selection.setAllSelected(false);
                ev.stopPropagation();
                ev.preventDefault();
                return;
            }
            var itemRoot = this._findItemRoot(target);
            // If a key was pressed within an item, we should treat "enters" as invokes and "space" as toggle
            if (itemRoot) {
                var index = this._getItemIndex(itemRoot);
                while (target !== this.refs.root) {
                    if (this._hasAttribute(target, SELECTION_TOGGLE_ATTRIBUTE_NAME)) {
                        // For toggle elements, assuming they are rendered as buttons, they will generate a click event,
                        // so we can no-op for any keydowns in this case.
                        break;
                    }
                    else if ((ev.which === Utilities_1.KeyCodes.enter || ev.which === Utilities_1.KeyCodes.space) &&
                        (target.tagName === 'BUTTON' || target.tagName === 'A' || target.tagName === 'INPUT')) {
                        return false;
                    }
                    else if (target === itemRoot) {
                        if (ev.which === Utilities_1.KeyCodes.enter) {
                            this._onInvokeClick(ev, index);
                            ev.preventDefault();
                            return;
                        }
                        else if (ev.which === Utilities_1.KeyCodes.space) {
                            this._onToggleClick(ev, index);
                            ev.preventDefault();
                            return;
                        }
                        break;
                    }
                    target = Utilities_1.getParent(target);
                }
            }
            // A key was pressed while an item in this zone was focused.
            this._shouldHandleFocus = true;
        };
        SelectionZone.prototype._onToggleAllClick = function (ev) {
            var _a = this.props, selection = _a.selection, selectionMode = _a.selectionMode;
            if (selectionMode === interfaces_1.SelectionMode.multiple) {
                selection.toggleAllSelected();
                ev.stopPropagation();
                ev.preventDefault();
            }
        };
        SelectionZone.prototype._onToggleClick = function (ev, index) {
            var _a = this.props, selection = _a.selection, selectionMode = _a.selectionMode;
            if (selectionMode === interfaces_1.SelectionMode.multiple) {
                selection.toggleIndexSelected(index);
            }
            else if (selectionMode === interfaces_1.SelectionMode.single) {
                var isSelected = selection.isIndexSelected(index);
                selection.setChangeEvents(false);
                selection.setAllSelected(false);
                selection.setIndexSelected(index, !isSelected, true);
                selection.setChangeEvents(true);
            }
            else {
                return;
            }
            ev.stopPropagation();
            // NOTE: ev.preventDefault is not called for toggle clicks, because this will kill the browser behavior
            // for checkboxes if you use a checkbox for the toggle.
        };
        SelectionZone.prototype._onInvokeClick = function (ev, index) {
            var _a = this.props, selection = _a.selection, onItemInvoked = _a.onItemInvoked;
            if (onItemInvoked) {
                onItemInvoked(selection.getItems()[index], index, ev.nativeEvent);
                ev.preventDefault();
                ev.stopPropagation();
            }
        };
        SelectionZone.prototype._onItemSurfaceClick = function (ev, index) {
            var _a = this.props, selection = _a.selection, selectionMode = _a.selectionMode;
            var isToggleModifierPressed = this._isCtrlPressed || this._isMetaPressed;
            if (selectionMode === interfaces_1.SelectionMode.multiple) {
                if (this._isShiftPressed) {
                    selection.selectToIndex(index, !isToggleModifierPressed);
                }
                else if (isToggleModifierPressed) {
                    selection.toggleIndexSelected(index);
                }
                else {
                    this._clearAndSelectIndex(index);
                }
            }
            else if (selectionMode === interfaces_1.SelectionMode.single) {
                this._clearAndSelectIndex(index);
            }
        };
        SelectionZone.prototype._onInvokeMouseDown = function (ev, index) {
            var selection = this.props.selection;
            // Only do work if item is not selected.
            if (selection.isIndexSelected(index)) {
                return;
            }
            this._clearAndSelectIndex(index);
        };
        SelectionZone.prototype._tryClearOnEmptyClick = function (ev) {
            if (!this.props.selectionPreservedOnEmptyClick &&
                this._isNonHandledClick(ev.target)) {
                this.props.selection.setAllSelected(false);
            }
        };
        SelectionZone.prototype._clearAndSelectIndex = function (index) {
            var selection = this.props.selection;
            var isAlreadySingleSelected = selection.getSelectedCount() === 1 && selection.isIndexSelected(index);
            if (!isAlreadySingleSelected) {
                selection.setChangeEvents(false);
                selection.setAllSelected(false);
                selection.setIndexSelected(index, true, true);
                selection.setChangeEvents(true);
            }
        };
        /**
         * We need to track the modifier key states so that when focus events occur, which do not contain
         * modifier states in the Event object, we know how to behave.
         */
        SelectionZone.prototype._updateModifiers = function (ev) {
            this._isShiftPressed = ev.shiftKey;
            this._isCtrlPressed = ev.ctrlKey;
            this._isMetaPressed = ev.metaKey;
        };
        SelectionZone.prototype._findItemRoot = function (target) {
            var selection = this.props.selection;
            while (target !== this.refs.root) {
                var indexValue = target.getAttribute(SELECTION_INDEX_ATTRIBUTE_NAME);
                var index = Number(indexValue);
                if (indexValue !== null && index >= 0 && index < selection.getItems().length) {
                    break;
                }
                target = Utilities_1.getParent(target);
            }
            if (target === this.refs.root) {
                return undefined;
            }
            return target;
        };
        SelectionZone.prototype._getItemIndex = function (itemRoot) {
            return Number(itemRoot.getAttribute(SELECTION_INDEX_ATTRIBUTE_NAME));
        };
        SelectionZone.prototype._hasAttribute = function (element, attributeName) {
            var isToggle = false;
            while (!isToggle && element !== this.refs.root) {
                isToggle = element.getAttribute(attributeName) === 'true';
                element = Utilities_1.getParent(element);
            }
            return isToggle;
        };
        SelectionZone.prototype._isInputElement = function (element) {
            return element.tagName === 'INPUT' || element.tagName === 'TEXTAREA';
        };
        SelectionZone.prototype._isNonHandledClick = function (element) {
            var doc = Utilities_1.getDocument();
            if (doc && element) {
                while (element && element !== doc.documentElement) {
                    if (Utilities_1.isElementTabbable(element)) {
                        return false;
                    }
                    element = Utilities_1.getParent(element);
                }
            }
            return true;
        };
        return SelectionZone;
    }(Utilities_1.BaseComponent));
    SelectionZone.defaultProps = {
        layout: new SelectionLayout_1.SelectionLayout(interfaces_1.SelectionDirection.vertical),
        isMultiSelectEnabled: true,
        isSelectedOnFocus: true,
        selectionMode: interfaces_1.SelectionMode.multiple
    };
    tslib_1.__decorate([
        Utilities_1.autobind
    ], SelectionZone.prototype, "ignoreNextFocus", null);
    tslib_1.__decorate([
        Utilities_1.autobind
    ], SelectionZone.prototype, "_onMouseDownCapture", null);
    tslib_1.__decorate([
        Utilities_1.autobind
    ], SelectionZone.prototype, "_onFocus", null);
    tslib_1.__decorate([
        Utilities_1.autobind
    ], SelectionZone.prototype, "_onMouseDown", null);
    tslib_1.__decorate([
        Utilities_1.autobind
    ], SelectionZone.prototype, "_onClick", null);
    tslib_1.__decorate([
        Utilities_1.autobind
    ], SelectionZone.prototype, "_onContextMenu", null);
    tslib_1.__decorate([
        Utilities_1.autobind
    ], SelectionZone.prototype, "_onDoubleClick", null);
    tslib_1.__decorate([
        Utilities_1.autobind
    ], SelectionZone.prototype, "_onKeyDown", null);
    exports.SelectionZone = SelectionZone;
});
//# sourceMappingURL=SelectionZone.js.map