// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License.txt in the project root for license information. /// import Animations = require('../../Animations'); import _Base = require('../../Core/_Base'); import _BaseUtils = require('../../Core/_BaseUtils'); import _Control = require('../../Utilities/_Control'); import _Dispose = require('../../Utilities/_Dispose'); import _ElementUtilities = require('../../Utilities/_ElementUtilities'); import _ErrorFromName = require('../../Core/_ErrorFromName'); import _Events = require('../../Core/_Events'); import _Global = require('../../Core/_Global'); import _Hoverable = require('../../Utilities/_Hoverable'); import _LightDismissService = require('../../_LightDismissService'); import Promise = require('../../Promise'); import _OpenCloseMachine = require('../../Utilities/_OpenCloseMachine'); import _TransitionAnimation = require('../../Animations/_TransitionAnimation'); require(["require-style!less/styles-splitview"]); require(["require-style!less/colors-splitview"]); "use strict"; // // Implementation Overview // // SplitView's responsibilities are divided into the following: // // Open/close state management // This involves firing the beforeopen, afteropen, beforeclose, and afterclose events. // It also involves making sure the control behaves properly when things happen in a // variety of states such as: // - open is called while the control is already open // - close is called while the control is in the middle of opening // - dispose is called within a beforeopen event handler // The SplitView relies on the _OpenCloseMachine component for most of this // state management. The contract is: // - The SplitView is responsible for specifying how to play the open and close // animations // - The _OpenCloseMachine is responsible for everything else including: // - Ensuring that these animations get run at the appropriate times // - Tracking the current state of the control and ensuring the right thing happens // when a method is called // - Firing the events // // Light dismiss // The SplitView's pane is light dismissable when the pane is open and the SplitView // is configured to openedDisplayMode:overlay. This means that the pane can be closed // thru a variety of cues such as tapping off of the pane, pressing the escape key, // and resizing the window. SplitView relies on the _LightDismissService component for // most of this functionality. The only pieces the SplitView is responsible for are: // - Describing what happens when a light dismiss is triggered on the SplitView. // - Describing how the SplitView should take/restore focus when it becomes the // topmost light dismissable. // // Open/close animations // Much of the SplitView's implementation is dedicated to playing the open and close // animations. The general approach is for the SplitView to calculate the current and // final sizes and positions of its pane and content elements. Then it creates a CSS // transition to animate the control between these two visual states. // // One tricky part of the animation is that the SplitView creates an animation that looks // like the pane is changing its width/height. You cannot animate width/height changes in CSS // so this animation is actually an illusion. It involves animating 2 elements, one which // clips the other, to give the illusion that an element is resizing. This logic is carried // out by Animations._resizeTransition. Take a look at that method for more details. // // Update DOM // SplitView follows the Update DOM pattern. For more information about this pattern, see: // https://github.com/winjs/winjs/wiki/Update-DOM-Pattern // // Note that the SplitView reads from the DOM when it needs to measure the position and size // of its pane and content elements. When possible, it caches this information and reads from // the cache instead of the DOM. This minimizes the performance cost. // // Outside of updateDom, SplitView writes to the DOM in a couple of places: // - The initializeDom function runs during construction and creates the // initial version of the SplitView's DOM. // - During animations, the animations take ownership of the DOM and turn // updateDom into a no-op. When the animation completes, updateDom begins // running again. This disabling of updateDom during animations is carried // out by _OpenCloseMachine. // interface IRect { left: number; top: number; contentWidth: number; contentHeight: number totalWidth: number; totalHeight: number; } export interface IThickness { content: number; total: number; } var transformNames = _BaseUtils._browserStyleEquivalents["transform"]; var Strings = { get duplicateConstruction() { return "Invalid argument: Controls may only be instantiated one time for each DOM element"; } }; var ClassNames = { splitView: "win-splitview", pane: "win-splitview-pane", content: "win-splitview-content", // closed/opened paneClosed: "win-splitview-pane-closed", paneOpened: "win-splitview-pane-opened", _panePlaceholder: "win-splitview-paneplaceholder", _paneOutline: "win-splitview-paneoutline", _tabStop: "win-splitview-tabstop", _paneWrapper: "win-splitview-panewrapper", _contentWrapper: "win-splitview-contentwrapper", _animating: "win-splitview-animating", // placement _placementLeft: "win-splitview-placementleft", _placementRight: "win-splitview-placementright", _placementTop: "win-splitview-placementtop", _placementBottom: "win-splitview-placementbottom", // closed display mode _closedDisplayNone: "win-splitview-closeddisplaynone", _closedDisplayInline: "win-splitview-closeddisplayinline", // opened display mode _openedDisplayInline: "win-splitview-openeddisplayinline", _openedDisplayOverlay: "win-splitview-openeddisplayoverlay" }; var EventNames = { beforeOpen: "beforeopen", afterOpen: "afteropen", beforeClose: "beforeclose", afterClose: "afterclose" }; var Dimension = { width: "width", height: "height" }; var ClosedDisplayMode = { /// /// When the pane is closed, it is not visible and doesn't take up any space. /// none: "none", /// /// When the pane is closed, it occupies space leaving less room for the SplitView's content. /// inline: "inline" }; var OpenedDisplayMode = { /// /// When the pane is open, it occupies space leaving less room for the SplitView's content. /// inline: "inline", /// /// When the pane is open, it doesn't take up any space and it is light dismissable. /// overlay: "overlay" }; var PanePlacement = { /// /// Pane is positioned left of the SplitView's content. /// left: "left", /// /// Pane is positioned right of the SplitView's content. /// right: "right", /// /// Pane is positioned above the SplitView's content. /// top: "top", /// /// Pane is positioned below the SplitView's content. /// bottom: "bottom" }; var closedDisplayModeClassMap = {}; closedDisplayModeClassMap[ClosedDisplayMode.none] = ClassNames._closedDisplayNone; closedDisplayModeClassMap[ClosedDisplayMode.inline] = ClassNames._closedDisplayInline; var openedDisplayModeClassMap = {}; openedDisplayModeClassMap[OpenedDisplayMode.overlay] = ClassNames._openedDisplayOverlay; openedDisplayModeClassMap[OpenedDisplayMode.inline] = ClassNames._openedDisplayInline; var panePlacementClassMap = {}; panePlacementClassMap[PanePlacement.left] = ClassNames._placementLeft; panePlacementClassMap[PanePlacement.right] = ClassNames._placementRight; panePlacementClassMap[PanePlacement.top] = ClassNames._placementTop; panePlacementClassMap[PanePlacement.bottom] = ClassNames._placementBottom; // Versions of add/removeClass that are no ops when called with falsy class names. function addClass(element: HTMLElement, className: string): void { className && _ElementUtilities.addClass(element, className); } function removeClass(element: HTMLElement, className: string): void { className && _ElementUtilities.removeClass(element, className); } function rectToThickness(rect: IRect, dimension: string): IThickness { return (dimension === Dimension.width) ? { content: rect.contentWidth, total: rect.totalWidth }: { content: rect.contentHeight, total: rect.totalHeight }; } /// /// /// Displays a SplitView which renders a collapsable pane next to arbitrary HTML content. /// /// /// /// /// ]]> /// Raised just before opening the pane. Call preventDefault on this event to stop the pane from opening. /// Raised immediately after the pane is fully opened. /// Raised just before closing the pane. Call preventDefault on this event to stop the pane from closing. /// Raised immediately after the pane is fully closed. /// The entire SplitView control. /// The element which hosts the SplitView's pane. /// The element which hosts the SplitView's content. /// /// export class SplitView { /// /// Display options for a SplitView's pane when it is closed. /// static ClosedDisplayMode = ClosedDisplayMode; /// /// Display options for a SplitView's pane when it is open. /// static OpenedDisplayMode = OpenedDisplayMode; /// /// Placement options for a SplitView's pane. /// static PanePlacement = PanePlacement; static supportedForProcessing: boolean = true; private static _ClassNames = ClassNames; private _disposed: boolean; private _machine: _OpenCloseMachine.OpenCloseMachine; _dom: { root: HTMLElement; pane: HTMLElement; startPaneTab: HTMLElement; endPaneTab: HTMLElement; paneOutline: HTMLElement; paneWrapper: HTMLElement; // Shouldn't have any margin, padding, or border. panePlaceholder: HTMLElement; // Shouldn't have any margin, padding, or border. content: HTMLElement; contentWrapper: HTMLElement; // Shouldn't have any margin, padding, or border. }; private _dismissable: _LightDismissService.LightDismissableElement; private _isOpenedMode: boolean; // Is ClassNames.paneOpened present on the SplitView? private _rtl: boolean; private _cachedHiddenPaneThickness: IThickness; private _lowestPaneTabIndex: number; private _highestPaneTabIndex: number; private _updateTabIndicesThrottled: Function; constructor(element?: HTMLElement, options: any = {}) { /// /// /// Creates a new SplitView control. /// /// /// The DOM element that hosts the SplitView control. /// /// /// An object that contains one or more property/value pairs to apply to the new control. /// Each property of the options object corresponds to one of the control's properties or events. /// Event names must begin with "on". For example, to provide a handler for the beforeclose event, /// add a property named "onbeforeclose" to the options object and set its value to the event handler. /// /// /// The new SplitView. /// /// // Check to make sure we weren't duplicated if (element && element["winControl"]) { throw new _ErrorFromName("WinJS.UI.SplitView.DuplicateConstruction", Strings.duplicateConstruction); } this._initializeDom(element || _Global.document.createElement("div")); this._machine = new _OpenCloseMachine.OpenCloseMachine({ eventElement: this._dom.root, onOpen: () => { this._cachedHiddenPaneThickness = null; var hiddenPaneThickness = this._getHiddenPaneThickness(); this._isOpenedMode = true; this._updateDomImpl(); _ElementUtilities.addClass(this._dom.root, ClassNames._animating); return this._playShowAnimation(hiddenPaneThickness).then(() => { _ElementUtilities.removeClass(this._dom.root, ClassNames._animating); }); }, onClose: () => { _ElementUtilities.addClass(this._dom.root, ClassNames._animating); return this._playHideAnimation(this._getHiddenPaneThickness()).then(() => { _ElementUtilities.removeClass(this._dom.root, ClassNames._animating); this._isOpenedMode = false; this._updateDomImpl(); }); }, onUpdateDom: () => { this._updateDomImpl(); }, onUpdateDomWithIsOpened: (isOpened: boolean) => { this._isOpenedMode = isOpened; this._updateDomImpl(); } }); // Initialize private state. this._disposed = false; this._dismissable = new _LightDismissService.LightDismissableElement({ element: this._dom.paneWrapper, tabIndex: -1, onLightDismiss: () => { this.closePane(); }, onTakeFocus: (useSetActive) => { this._dismissable.restoreFocus() || _ElementUtilities._tryFocusOnAnyElement(this._dom.pane, useSetActive); } }); this._cachedHiddenPaneThickness = null; // Initialize public properties. this.paneOpened = false; this.closedDisplayMode = ClosedDisplayMode.inline; this.openedDisplayMode = OpenedDisplayMode.overlay; this.panePlacement = PanePlacement.left; _Control.setOptions(this, options); // Exit the Init state. _ElementUtilities._inDom(this._dom.root).then(() => { this._rtl = _ElementUtilities._getComputedStyle(this._dom.root).direction === 'rtl'; this._updateTabIndices(); this._machine.exitInit(); }); } /// get element(): HTMLElement { return this._dom.root; } /// get paneElement(): HTMLElement { return this._dom.pane; } /// get contentElement(): HTMLElement { return this._dom.content; } private _closedDisplayMode: string; /// /// Gets or sets the display mode of the SplitView's pane when it is hidden. /// get closedDisplayMode(): string { return this._closedDisplayMode; } set closedDisplayMode(value: string) { if (ClosedDisplayMode[value] && this._closedDisplayMode !== value) { this._closedDisplayMode = value; this._cachedHiddenPaneThickness = null; this._machine.updateDom(); } } private _openedDisplayMode: string; /// /// Gets or sets the display mode of the SplitView's pane when it is open. /// get openedDisplayMode(): string { return this._openedDisplayMode; } set openedDisplayMode(value: string) { if (OpenedDisplayMode[value] && this._openedDisplayMode !== value) { this._openedDisplayMode = value; this._cachedHiddenPaneThickness = null; this._machine.updateDom(); } } private _panePlacement: string; /// /// Gets or sets the placement of the SplitView's pane. /// get panePlacement(): string { return this._panePlacement; } set panePlacement(value: string) { if (PanePlacement[value] && this._panePlacement !== value) { this._panePlacement = value; this._cachedHiddenPaneThickness = null; this._machine.updateDom(); } } /// get paneOpened(): boolean { return this._machine.opened; } set paneOpened(value: boolean) { this._machine.opened = value; } dispose(): void { /// /// /// Disposes this control. /// /// if (this._disposed) { return; } this._disposed = true; this._machine.dispose(); _LightDismissService.hidden(this._dismissable); _Dispose._disposeElement(this._dom.pane); _Dispose._disposeElement(this._dom.content); } openPane(): void { /// /// /// Opens the SplitView's pane. /// /// this._machine.open(); } closePane(): void { /// /// /// Closes the SplitView's pane. /// /// this._machine.close(); } private _initializeDom(root: HTMLElement): void { // The first child is the pane var paneEl = root.firstElementChild || _Global.document.createElement("div"); _ElementUtilities.addClass(paneEl, ClassNames.pane); if (!paneEl.hasAttribute("tabIndex")) { paneEl.tabIndex = -1; } // All other children are members of the content var contentEl = _Global.document.createElement("div"); _ElementUtilities.addClass(contentEl, ClassNames.content); var child = paneEl.nextSibling; while (child) { var sibling = child.nextSibling; contentEl.appendChild(child); child = sibling; } var startPaneTabEl = _Global.document.createElement("div"); startPaneTabEl.className = ClassNames._tabStop; _ElementUtilities._ensureId(startPaneTabEl); var endPaneTabEl = _Global.document.createElement("div"); endPaneTabEl.className = ClassNames._tabStop; _ElementUtilities._ensureId(endPaneTabEl); // paneOutline's purpose is to render an outline around the pane in high contrast mode var paneOutlineEl = _Global.document.createElement("div"); paneOutlineEl.className = ClassNames._paneOutline; // paneWrapper's purpose is to clip the pane during the pane resize animation var paneWrapperEl = _Global.document.createElement("div"); paneWrapperEl.className = ClassNames._paneWrapper; paneWrapperEl.appendChild(startPaneTabEl); paneWrapperEl.appendChild(paneEl); paneWrapperEl.appendChild(paneOutlineEl); paneWrapperEl.appendChild(endPaneTabEl); var panePlaceholderEl = _Global.document.createElement("div"); panePlaceholderEl.className = ClassNames._panePlaceholder; // contentWrapper is an extra element we need to allow heights to be specified as percentages (e.g. height: 100%) // for elements within the content area. It works around this Chrome bug: // Issue 428049: 100% height doesn't work on child of a definite-flex-basis flex item (in vertical flex container) // https://code.google.com/p/chromium/issues/detail?id=428049 // The workaround is that putting a position: absolute element (_dom.content) within the flex item (_dom.contentWrapper) // allows percentage heights to work within the absolutely positioned element (_dom.content). var contentWrapperEl = _Global.document.createElement("div"); contentWrapperEl.className = ClassNames._contentWrapper; contentWrapperEl.appendChild(contentEl); root["winControl"] = this; _ElementUtilities.addClass(root, ClassNames.splitView); _ElementUtilities.addClass(root, "win-disposable"); this._dom = { root: root, pane: paneEl, startPaneTab: startPaneTabEl, endPaneTab: endPaneTabEl, paneOutline: paneOutlineEl, paneWrapper: paneWrapperEl, panePlaceholder: panePlaceholderEl, content: contentEl, contentWrapper: contentWrapperEl }; _ElementUtilities._addEventListener(paneEl, "keydown", this._onKeyDown.bind(this)); _ElementUtilities._addEventListener(startPaneTabEl, "focusin", this._onStartPaneTabFocusIn.bind(this)); _ElementUtilities._addEventListener(endPaneTabEl, "focusin", this._onEndPaneTabFocusIn.bind(this)); } private _onKeyDown(eventObject: KeyboardEvent) { if (eventObject.keyCode === _ElementUtilities.Key.tab) { this._updateTabIndices(); } } private _onStartPaneTabFocusIn(eventObject: FocusEvent) { _ElementUtilities._focusLastFocusableElement(this._dom.pane); } private _onEndPaneTabFocusIn(eventObject: FocusEvent) { _ElementUtilities._focusFirstFocusableElement(this._dom.pane); } private _measureElement(element: HTMLElement): IRect { var style = _ElementUtilities._getComputedStyle(element); var position = _ElementUtilities._getPositionRelativeTo(element, this._dom.root); var marginLeft = parseInt(style.marginLeft, 10); var marginTop = parseInt(style.marginTop, 10); return { left: position.left - marginLeft, top: position.top - marginTop, contentWidth: _ElementUtilities.getContentWidth(element), contentHeight: _ElementUtilities.getContentHeight(element), totalWidth: _ElementUtilities.getTotalWidth(element), totalHeight: _ElementUtilities.getTotalHeight(element) }; } private _setContentRect(contentRect: IRect) { var contentWrapperStyle = this._dom.contentWrapper.style; contentWrapperStyle.left = contentRect.left + "px"; contentWrapperStyle.top = contentRect.top + "px"; contentWrapperStyle.height = contentRect.contentHeight + "px"; contentWrapperStyle.width = contentRect.contentWidth + "px"; } // Overridden by tests. private _prepareAnimation(paneRect: IRect, contentRect: IRect): void { var paneWrapperStyle = this._dom.paneWrapper.style; paneWrapperStyle.position = "absolute"; paneWrapperStyle.left = paneRect.left + "px"; paneWrapperStyle.top = paneRect.top + "px"; paneWrapperStyle.height = paneRect.totalHeight + "px"; paneWrapperStyle.width = paneRect.totalWidth + "px"; var contentWrapperStyle = this._dom.contentWrapper.style; contentWrapperStyle.position = "absolute"; this._setContentRect(contentRect); } // Overridden by tests. private _clearAnimation(): void { var paneWrapperStyle = this._dom.paneWrapper.style; paneWrapperStyle.position = ""; paneWrapperStyle.left = ""; paneWrapperStyle.top = ""; paneWrapperStyle.height = ""; paneWrapperStyle.width = ""; paneWrapperStyle[transformNames.scriptName] = ""; var contentWrapperStyle = this._dom.contentWrapper.style; contentWrapperStyle.position = ""; contentWrapperStyle.left = ""; contentWrapperStyle.top = ""; contentWrapperStyle.height = ""; contentWrapperStyle.width = ""; contentWrapperStyle[transformNames.scriptName] = ""; var paneStyle = this._dom.pane.style; paneStyle.height = ""; paneStyle.width = ""; paneStyle[transformNames.scriptName] = ""; } private _getHiddenContentRect(shownContentRect: IRect, hiddenPaneThickness: IThickness, shownPaneThickness: IThickness): IRect { if (this.openedDisplayMode === OpenedDisplayMode.overlay) { return shownContentRect; } else { var placementRight = this._rtl ? PanePlacement.left : PanePlacement.right; var multiplier = this.panePlacement === placementRight || this.panePlacement === PanePlacement.bottom ? 0 : 1; var paneDiff = { content: shownPaneThickness.content - hiddenPaneThickness.content, total: shownPaneThickness.total - hiddenPaneThickness.total }; return this._horizontal ? { left: shownContentRect.left - multiplier * paneDiff.total, top: shownContentRect.top, contentWidth: shownContentRect.contentWidth + paneDiff.content, contentHeight: shownContentRect.contentHeight, totalWidth: shownContentRect.totalWidth + paneDiff.total, totalHeight: shownContentRect.totalHeight } : { left: shownContentRect.left, top: shownContentRect.top - multiplier * paneDiff.total, contentWidth: shownContentRect.contentWidth, contentHeight: shownContentRect.contentHeight + paneDiff.content, totalWidth: shownContentRect.totalWidth, totalHeight: shownContentRect.totalHeight + paneDiff.total } } } private get _horizontal(): boolean { return this.panePlacement === PanePlacement.left || this.panePlacement === PanePlacement.right; } private _getHiddenPaneThickness(): IThickness { if (this._cachedHiddenPaneThickness === null) { if (this._closedDisplayMode === ClosedDisplayMode.none) { this._cachedHiddenPaneThickness = { content: 0, total: 0 }; } else { if (this._isOpenedMode) { _ElementUtilities.removeClass(this._dom.root, ClassNames.paneOpened); _ElementUtilities.addClass(this._dom.root, ClassNames.paneClosed); } var size = this._measureElement(this._dom.pane); this._cachedHiddenPaneThickness = rectToThickness(size, this._horizontal ? Dimension.width : Dimension.height); if (this._isOpenedMode) { _ElementUtilities.removeClass(this._dom.root, ClassNames.paneClosed); _ElementUtilities.addClass(this._dom.root, ClassNames.paneOpened); } } } return this._cachedHiddenPaneThickness; } // Should be called while SplitView is rendered in its opened mode // Overridden by tests. private _playShowAnimation(hiddenPaneThickness: IThickness): Promise { var dim = this._horizontal ? Dimension.width : Dimension.height; var shownPaneRect = this._measureElement(this._dom.pane); var shownContentRect = this._measureElement(this._dom.content); var shownPaneThickness = rectToThickness(shownPaneRect, dim); var hiddenContentRect = this._getHiddenContentRect(shownContentRect, hiddenPaneThickness, shownPaneThickness); this._prepareAnimation(shownPaneRect, hiddenContentRect); var playPaneAnimation = (): Promise => { var placementRight = this._rtl ? PanePlacement.left : PanePlacement.right; // What percentage of the size change should be skipped? (e.g. let's do the first // 30% of the size change instantly and then animate the other 70%) var animationOffsetFactor = 0.3; var from = hiddenPaneThickness.total + animationOffsetFactor * (shownPaneThickness.total - hiddenPaneThickness.total); return Animations._resizeTransition(this._dom.paneWrapper, this._dom.pane, { from: from, to: shownPaneThickness.total, actualSize: shownPaneThickness.total, dimension: dim, anchorTrailingEdge: this.panePlacement === placementRight || this.panePlacement === PanePlacement.bottom }); }; var playShowAnimation = (): Promise => { if (this.openedDisplayMode === OpenedDisplayMode.inline) { this._setContentRect(shownContentRect); } return playPaneAnimation(); }; return playShowAnimation().then(() => { this._clearAnimation(); }); } // Should be called while SplitView is rendered in its opened mode // Overridden by tests. private _playHideAnimation(hiddenPaneThickness: IThickness): Promise { var dim = this._horizontal ? Dimension.width : Dimension.height; var shownPaneRect = this._measureElement(this._dom.pane); var shownContentRect = this._measureElement(this._dom.content); var shownPaneThickness = rectToThickness(shownPaneRect, dim); var hiddenContentRect = this._getHiddenContentRect(shownContentRect, hiddenPaneThickness, shownPaneThickness); this._prepareAnimation(shownPaneRect, shownContentRect); var playPaneAnimation = (): Promise => { var placementRight = this._rtl ? PanePlacement.left : PanePlacement.right; // What percentage of the size change should be skipped? (e.g. let's do the first // 30% of the size change instantly and then animate the other 70%) var animationOffsetFactor = 0.3; var from = shownPaneThickness.total - animationOffsetFactor * (shownPaneThickness.total - hiddenPaneThickness.total); return Animations._resizeTransition(this._dom.paneWrapper, this._dom.pane, { from: from, to: hiddenPaneThickness.total, actualSize: shownPaneThickness.total, dimension: dim, anchorTrailingEdge: this.panePlacement === placementRight || this.panePlacement === PanePlacement.bottom }); }; var playHideAnimation = (): Promise => { if (this.openedDisplayMode === OpenedDisplayMode.inline) { this._setContentRect(hiddenContentRect); } return playPaneAnimation(); }; return playHideAnimation().then(() => { this._clearAnimation(); }); } // _updateTabIndices and _updateTabIndicesImpl are used in tests private _updateTabIndices() { if (!this._updateTabIndicesThrottled) { this._updateTabIndicesThrottled = _BaseUtils._throttledFunction(100, this._updateTabIndicesImpl.bind(this)); } this._updateTabIndicesThrottled(); } private _updateTabIndicesImpl() { var tabIndex = _ElementUtilities._getHighAndLowTabIndices(this._dom.pane); this._highestPaneTabIndex = tabIndex.highest; this._lowestPaneTabIndex = tabIndex.lowest; this._machine.updateDom(); } // State private to _updateDomImpl. No other method should make use of it. // // Nothing has been rendered yet so these are all initialized to undefined. Because // they are undefined, the first time _updateDomImpl is called, they will all be // rendered. private _updateDomImpl_rendered = { paneIsFirst: undefined, isOpenedMode: undefined, closedDisplayMode: undefined, openedDisplayMode: undefined, panePlacement: undefined, panePlaceholderWidth: undefined, panePlaceholderHeight: undefined, isOverlayShown: undefined, startPaneTabIndex: undefined, endPaneTabIndex: undefined }; private _updateDomImpl(): void { var rendered = this._updateDomImpl_rendered; var paneShouldBeFirst = this.panePlacement === PanePlacement.left || this.panePlacement === PanePlacement.top; if (paneShouldBeFirst !== rendered.paneIsFirst) { // TODO: restore focus if (paneShouldBeFirst) { this._dom.root.appendChild(this._dom.panePlaceholder); this._dom.root.appendChild(this._dom.paneWrapper); this._dom.root.appendChild(this._dom.contentWrapper); } else { this._dom.root.appendChild(this._dom.contentWrapper); this._dom.root.appendChild(this._dom.paneWrapper); this._dom.root.appendChild(this._dom.panePlaceholder); } } rendered.paneIsFirst = paneShouldBeFirst; if (rendered.isOpenedMode !== this._isOpenedMode) { if (this._isOpenedMode) { _ElementUtilities.removeClass(this._dom.root, ClassNames.paneClosed); _ElementUtilities.addClass(this._dom.root, ClassNames.paneOpened); } else { _ElementUtilities.removeClass(this._dom.root, ClassNames.paneOpened); _ElementUtilities.addClass(this._dom.root, ClassNames.paneClosed); } } rendered.isOpenedMode = this._isOpenedMode; if (rendered.panePlacement !== this.panePlacement) { removeClass(this._dom.root, panePlacementClassMap[rendered.panePlacement]); addClass(this._dom.root, panePlacementClassMap[this.panePlacement]); rendered.panePlacement = this.panePlacement; } if (rendered.closedDisplayMode !== this.closedDisplayMode) { removeClass(this._dom.root, closedDisplayModeClassMap[rendered.closedDisplayMode]); addClass(this._dom.root, closedDisplayModeClassMap[this.closedDisplayMode]); rendered.closedDisplayMode = this.closedDisplayMode; } if (rendered.openedDisplayMode !== this.openedDisplayMode) { removeClass(this._dom.root, openedDisplayModeClassMap[rendered.openedDisplayMode]); addClass(this._dom.root, openedDisplayModeClassMap[this.openedDisplayMode]); rendered.openedDisplayMode = this.openedDisplayMode; } var isOverlayShown = this._isOpenedMode && this.openedDisplayMode === OpenedDisplayMode.overlay; var startPaneTabIndex = isOverlayShown ? this._lowestPaneTabIndex : -1; var endPaneTabIndex = isOverlayShown ? this._highestPaneTabIndex : -1; if (rendered.startPaneTabIndex !== startPaneTabIndex) { this._dom.startPaneTab.tabIndex = startPaneTabIndex; if (startPaneTabIndex === -1) { this._dom.startPaneTab.removeAttribute("x-ms-aria-flowfrom"); } else { this._dom.startPaneTab.setAttribute("x-ms-aria-flowfrom", this._dom.endPaneTab.id); } rendered.startPaneTabIndex = startPaneTabIndex; } if (rendered.endPaneTabIndex !== endPaneTabIndex) { this._dom.endPaneTab.tabIndex = endPaneTabIndex; if (endPaneTabIndex === -1) { this._dom.endPaneTab.removeAttribute("aria-flowto"); } else { this._dom.endPaneTab.setAttribute("aria-flowto", this._dom.startPaneTab.id); } rendered.endPaneTabIndex = endPaneTabIndex; } // panePlaceholder's purpose is to take up the amount of space occupied by the // hidden pane while the pane is shown in overlay mode. Without this, the content // would shift as the pane shows and hides in overlay mode. var width: string, height: string; if (isOverlayShown) { var hiddenPaneThickness = this._getHiddenPaneThickness(); if (this._horizontal) { width = hiddenPaneThickness.total + "px"; height = ""; } else { width = ""; height = hiddenPaneThickness.total + "px"; } } else { width = ""; height = ""; } if (rendered.panePlaceholderWidth !== width || rendered.panePlaceholderHeight !== height) { var style = this._dom.panePlaceholder.style; style.width = width; style.height = height; rendered.panePlaceholderWidth = width; rendered.panePlaceholderHeight = height; } if (rendered.isOverlayShown !== isOverlayShown) { if (isOverlayShown) { _LightDismissService.shown(this._dismissable); } else { _LightDismissService.hidden(this._dismissable); } rendered.isOverlayShown = isOverlayShown; } } } _Base.Class.mix(SplitView, _Events.createEventProperties( EventNames.beforeOpen, EventNames.afterOpen, EventNames.beforeClose, EventNames.afterClose )); _Base.Class.mix(SplitView, _Control.DOMEventMixin);