UNPKG

chrome-devtools-frontend

Version:
899 lines (777 loc) • 33.2 kB
/* * Copyright (C) 2012 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import * as Common from '../../core/common/common.js'; import * as Platform from '../../core/platform/platform.js'; import * as ARIAUtils from './ARIAUtils.js'; import {Constraints} from './Geometry.js'; import {Events as ResizerWidgetEvents, SimpleResizerWidget, type ResizeUpdatePositionEvent} from './ResizerWidget.js'; import {ToolbarButton} from './Toolbar.js'; import {Widget} from './Widget.js'; import {Events as ZoomManagerEvents, ZoomManager} from './ZoomManager.js'; import splitWidgetStyles from './splitWidget.css.legacy.js'; export class SplitWidget extends Common.ObjectWrapper.eventMixin<EventTypes, typeof Widget>(Widget) { private sidebarElementInternal: HTMLElement; private mainElement: HTMLElement; private resizerElementInternal: HTMLElement; private resizerElementSize: number|null; private readonly resizerWidget: SimpleResizerWidget; private defaultSidebarWidth: number; private defaultSidebarHeight: number; private readonly constraintsInDip: boolean; private resizeStartSizeDIP: number; // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // eslint-disable-next-line @typescript-eslint/no-explicit-any private setting: Common.Settings.Setting<any>|null; private totalSizeCSS: number; private totalSizeOtherDimensionCSS: number; private mainWidgetInternal: Widget|null; private sidebarWidgetInternal: Widget|null; private animationFrameHandle: number; private animationCallback: (() => void)|null; private showSidebarButtonTitle: Common.UIString.LocalizedString; private hideSidebarButtonTitle: Common.UIString.LocalizedString; private shownSidebarString: Common.UIString.LocalizedString; private hiddenSidebarString: Common.UIString.LocalizedString; private showHideSidebarButton: ToolbarButton|null; private isVerticalInternal: boolean; private sidebarMinimized: boolean; private detaching: boolean; private sidebarSizeDIP: number; private savedSidebarSizeDIP: number; private secondIsSidebar: boolean; private shouldSaveShowMode: boolean; private savedVerticalMainSize: number|null; private savedHorizontalMainSize: number|null; private showModeInternal: string; private savedShowMode: string; constructor( isVertical: boolean, secondIsSidebar: boolean, settingName?: string, defaultSidebarWidth?: number, defaultSidebarHeight?: number, constraintsInDip?: boolean) { super(true); this.element.classList.add('split-widget'); this.registerRequiredCSS(splitWidgetStyles); this.contentElement.classList.add('shadow-split-widget'); this.sidebarElementInternal = this.contentElement.createChild('div', 'shadow-split-widget-contents shadow-split-widget-sidebar vbox'); this.mainElement = this.contentElement.createChild('div', 'shadow-split-widget-contents shadow-split-widget-main vbox'); (this.mainElement.createChild('slot') as HTMLSlotElement).name = 'insertion-point-main'; (this.sidebarElementInternal.createChild('slot') as HTMLSlotElement).name = 'insertion-point-sidebar'; this.resizerElementInternal = this.contentElement.createChild('div', 'shadow-split-widget-resizer'); this.resizerElementSize = null; this.resizerWidget = new SimpleResizerWidget(); this.resizerWidget.setEnabled(true); this.resizerWidget.addEventListener(ResizerWidgetEvents.ResizeStart, this.onResizeStart, this); this.resizerWidget.addEventListener(ResizerWidgetEvents.ResizeUpdatePosition, this.onResizeUpdate, this); this.resizerWidget.addEventListener(ResizerWidgetEvents.ResizeEnd, this.onResizeEnd, this); this.defaultSidebarWidth = defaultSidebarWidth || 200; this.defaultSidebarHeight = defaultSidebarHeight || this.defaultSidebarWidth; this.constraintsInDip = Boolean(constraintsInDip); this.resizeStartSizeDIP = 0; this.setting = settingName ? Common.Settings.Settings.instance().createSetting(settingName, {}) : null; this.totalSizeCSS = 0; this.totalSizeOtherDimensionCSS = 0; this.mainWidgetInternal = null; this.sidebarWidgetInternal = null; this.animationFrameHandle = 0; this.animationCallback = null; this.showSidebarButtonTitle = Common.UIString.LocalizedEmptyString; this.hideSidebarButtonTitle = Common.UIString.LocalizedEmptyString; this.shownSidebarString = Common.UIString.LocalizedEmptyString; this.hiddenSidebarString = Common.UIString.LocalizedEmptyString; this.showHideSidebarButton = null; this.isVerticalInternal = false; this.sidebarMinimized = false; this.detaching = false; this.sidebarSizeDIP = -1; this.savedSidebarSizeDIP = this.sidebarSizeDIP; this.secondIsSidebar = false; this.shouldSaveShowMode = false; this.savedVerticalMainSize = null; this.savedHorizontalMainSize = null; this.setSecondIsSidebar(secondIsSidebar); this.innerSetVertical(isVertical); this.showModeInternal = ShowMode.Both; this.savedShowMode = this.showModeInternal; // Should be called after isVertical has the right value. this.installResizer(this.resizerElementInternal); } isVertical(): boolean { return this.isVerticalInternal; } setVertical(isVertical: boolean): void { if (this.isVerticalInternal === isVertical) { return; } this.innerSetVertical(isVertical); if (this.isShowing()) { this.updateLayout(); } } private innerSetVertical(isVertical: boolean): void { this.contentElement.classList.toggle('vbox', !isVertical); this.contentElement.classList.toggle('hbox', isVertical); this.isVerticalInternal = isVertical; this.resizerElementSize = null; this.sidebarSizeDIP = -1; this.restoreSidebarSizeFromSettings(); if (this.shouldSaveShowMode) { this.restoreAndApplyShowModeFromSettings(); } this.updateShowHideSidebarButton(); // FIXME: reverse SplitWidget.isVertical meaning. this.resizerWidget.setVertical(!isVertical); this.invalidateConstraints(); } private updateLayout(animate?: boolean): void { this.totalSizeCSS = 0; // Lazy update. this.totalSizeOtherDimensionCSS = 0; // Remove properties that might affect total size calculation. this.mainElement.style.removeProperty('width'); this.mainElement.style.removeProperty('height'); this.sidebarElementInternal.style.removeProperty('width'); this.sidebarElementInternal.style.removeProperty('height'); this.innerSetSidebarSizeDIP(this.preferredSidebarSizeDIP(), Boolean(animate)); } setMainWidget(widget: Widget): void { if (this.mainWidgetInternal === widget) { return; } this.suspendInvalidations(); if (this.mainWidgetInternal) { this.mainWidgetInternal.detach(); } this.mainWidgetInternal = widget; if (widget) { widget.element.slot = 'insertion-point-main'; if (this.showModeInternal === ShowMode.OnlyMain || this.showModeInternal === ShowMode.Both) { widget.show(this.element); } } this.resumeInvalidations(); } setSidebarWidget(widget: Widget): void { if (this.sidebarWidgetInternal === widget) { return; } this.suspendInvalidations(); if (this.sidebarWidgetInternal) { this.sidebarWidgetInternal.detach(); } this.sidebarWidgetInternal = widget; if (widget) { widget.element.slot = 'insertion-point-sidebar'; if (this.showModeInternal === ShowMode.OnlySidebar || this.showModeInternal === ShowMode.Both) { widget.show(this.element); } } this.resumeInvalidations(); } mainWidget(): Widget|null { return this.mainWidgetInternal; } sidebarWidget(): Widget|null { return this.sidebarWidgetInternal; } sidebarElement(): HTMLElement { return this.sidebarElementInternal; } override childWasDetached(widget: Widget): void { if (this.detaching) { return; } if (this.mainWidgetInternal === widget) { this.mainWidgetInternal = null; } if (this.sidebarWidgetInternal === widget) { this.sidebarWidgetInternal = null; } this.invalidateConstraints(); } isSidebarSecond(): boolean { return this.secondIsSidebar; } enableShowModeSaving(): void { this.shouldSaveShowMode = true; this.restoreAndApplyShowModeFromSettings(); } showMode(): string { return this.showModeInternal; } setSecondIsSidebar(secondIsSidebar: boolean): void { if (secondIsSidebar === this.secondIsSidebar) { return; } this.secondIsSidebar = secondIsSidebar; if (!this.mainWidgetInternal || !this.mainWidgetInternal.shouldHideOnDetach()) { if (secondIsSidebar) { this.contentElement.insertBefore(this.mainElement, this.sidebarElementInternal); } else { this.contentElement.insertBefore(this.mainElement, this.resizerElementInternal); } } else if (!this.sidebarWidgetInternal || !this.sidebarWidgetInternal.shouldHideOnDetach()) { if (secondIsSidebar) { this.contentElement.insertBefore(this.sidebarElementInternal, this.resizerElementInternal); } else { this.contentElement.insertBefore(this.sidebarElementInternal, this.mainElement); } } else { console.error('Could not swap split widget side. Both children widgets contain iframes.'); this.secondIsSidebar = !secondIsSidebar; } } sidebarSide(): string|null { if (this.showModeInternal !== ShowMode.Both) { return null; } return this.isVerticalInternal ? (this.secondIsSidebar ? 'right' : 'left') : (this.secondIsSidebar ? 'bottom' : 'top'); } resizerElement(): Element { return this.resizerElementInternal; } hideMain(animate?: boolean): void { this.showOnly( this.sidebarWidgetInternal, this.mainWidgetInternal, this.sidebarElementInternal, this.mainElement, animate); this.updateShowMode(ShowMode.OnlySidebar); } hideSidebar(animate?: boolean): void { this.showOnly( this.mainWidgetInternal, this.sidebarWidgetInternal, this.mainElement, this.sidebarElementInternal, animate); this.updateShowMode(ShowMode.OnlyMain); } setSidebarMinimized(minimized: boolean): void { this.sidebarMinimized = minimized; this.invalidateConstraints(); } isSidebarMinimized(): boolean { return this.sidebarMinimized; } private showOnly( sideToShow: Widget|null, sideToHide: Widget|null, shadowToShow: Element, shadowToHide: Element, animate?: boolean): void { this.cancelAnimation(); function callback(this: SplitWidget): void { if (sideToShow) { // Make sure main is first in the children list. if (sideToShow === this.mainWidgetInternal) { this.mainWidgetInternal.show( this.element, this.sidebarWidgetInternal ? this.sidebarWidgetInternal.element : null); } else if (this.sidebarWidgetInternal) { this.sidebarWidgetInternal.show(this.element); } } if (sideToHide) { this.detaching = true; sideToHide.detach(); this.detaching = false; } this.resizerElementInternal.classList.add('hidden'); shadowToShow.classList.remove('hidden'); shadowToShow.classList.add('maximized'); shadowToHide.classList.add('hidden'); shadowToHide.classList.remove('maximized'); this.removeAllLayoutProperties(); this.doResize(); this.showFinishedForTest(); } if (animate) { this.animate(true, callback.bind(this)); } else { callback.call(this); } this.sidebarSizeDIP = -1; this.setResizable(false); } private showFinishedForTest(): void { // This method is sniffed in tests. } private removeAllLayoutProperties(): void { this.sidebarElementInternal.style.removeProperty('flexBasis'); this.mainElement.style.removeProperty('width'); this.mainElement.style.removeProperty('height'); this.sidebarElementInternal.style.removeProperty('width'); this.sidebarElementInternal.style.removeProperty('height'); this.resizerElementInternal.style.removeProperty('left'); this.resizerElementInternal.style.removeProperty('right'); this.resizerElementInternal.style.removeProperty('top'); this.resizerElementInternal.style.removeProperty('bottom'); this.resizerElementInternal.style.removeProperty('margin-left'); this.resizerElementInternal.style.removeProperty('margin-right'); this.resizerElementInternal.style.removeProperty('margin-top'); this.resizerElementInternal.style.removeProperty('margin-bottom'); } showBoth(animate?: boolean): void { if (this.showModeInternal === ShowMode.Both) { animate = false; } this.cancelAnimation(); this.mainElement.classList.remove('maximized', 'hidden'); this.sidebarElementInternal.classList.remove('maximized', 'hidden'); this.resizerElementInternal.classList.remove('hidden'); this.setResizable(true); // Make sure main is the first in the children list. this.suspendInvalidations(); if (this.sidebarWidgetInternal) { this.sidebarWidgetInternal.show(this.element); } if (this.mainWidgetInternal) { this.mainWidgetInternal.show( this.element, this.sidebarWidgetInternal ? this.sidebarWidgetInternal.element : null); } this.resumeInvalidations(); // Order widgets in DOM properly. this.setSecondIsSidebar(this.secondIsSidebar); this.sidebarSizeDIP = -1; this.updateShowMode(ShowMode.Both); this.updateLayout(animate); } setResizable(resizable: boolean): void { this.resizerWidget.setEnabled(resizable); } isResizable(): boolean { return this.resizerWidget.isEnabled(); } setSidebarSize(size: number): void { const sizeDIP = ZoomManager.instance().cssToDIP(size); this.savedSidebarSizeDIP = sizeDIP; this.saveSetting(); this.innerSetSidebarSizeDIP(sizeDIP, false, true); } sidebarSize(): number { const sizeDIP = Math.max(0, this.sidebarSizeDIP); return ZoomManager.instance().dipToCSS(sizeDIP); } /** * Returns total size in DIP. */ private totalSizeDIP(): number { if (!this.totalSizeCSS) { this.totalSizeCSS = this.isVerticalInternal ? this.contentElement.offsetWidth : this.contentElement.offsetHeight; this.totalSizeOtherDimensionCSS = this.isVerticalInternal ? this.contentElement.offsetHeight : this.contentElement.offsetWidth; } return ZoomManager.instance().cssToDIP(this.totalSizeCSS); } private updateShowMode(showMode: string): void { this.showModeInternal = showMode; this.saveShowModeToSettings(); this.updateShowHideSidebarButton(); this.dispatchEventToListeners(Events.ShowModeChanged, showMode); this.invalidateConstraints(); } private innerSetSidebarSizeDIP(sizeDIP: number, animate: boolean, userAction?: boolean): void { if (this.showModeInternal !== ShowMode.Both || !this.isShowing()) { return; } sizeDIP = this.applyConstraints(sizeDIP, userAction); if (this.sidebarSizeDIP === sizeDIP) { return; } if (!this.resizerElementSize) { this.resizerElementSize = this.isVerticalInternal ? this.resizerElementInternal.offsetWidth : this.resizerElementInternal.offsetHeight; } // Invalidate layout below. this.removeAllLayoutProperties(); // this.totalSizeDIP is available below since we successfully applied constraints. const roundSizeCSS = Math.round(ZoomManager.instance().dipToCSS(sizeDIP)); const sidebarSizeValue = roundSizeCSS + 'px'; const mainSizeValue = (this.totalSizeCSS - roundSizeCSS) + 'px'; this.sidebarElementInternal.style.flexBasis = sidebarSizeValue; // Make both sides relayout boundaries. if (this.isVerticalInternal) { this.sidebarElementInternal.style.width = sidebarSizeValue; this.mainElement.style.width = mainSizeValue; this.sidebarElementInternal.style.height = this.totalSizeOtherDimensionCSS + 'px'; this.mainElement.style.height = this.totalSizeOtherDimensionCSS + 'px'; } else { this.sidebarElementInternal.style.height = sidebarSizeValue; this.mainElement.style.height = mainSizeValue; this.sidebarElementInternal.style.width = this.totalSizeOtherDimensionCSS + 'px'; this.mainElement.style.width = this.totalSizeOtherDimensionCSS + 'px'; } // Position resizer. if (this.isVerticalInternal) { if (this.secondIsSidebar) { this.resizerElementInternal.style.right = sidebarSizeValue; this.resizerElementInternal.style.marginRight = -this.resizerElementSize / 2 + 'px'; } else { this.resizerElementInternal.style.left = sidebarSizeValue; this.resizerElementInternal.style.marginLeft = -this.resizerElementSize / 2 + 'px'; } } else { if (this.secondIsSidebar) { this.resizerElementInternal.style.bottom = sidebarSizeValue; this.resizerElementInternal.style.marginBottom = -this.resizerElementSize / 2 + 'px'; } else { this.resizerElementInternal.style.top = sidebarSizeValue; this.resizerElementInternal.style.marginTop = -this.resizerElementSize / 2 + 'px'; } } this.sidebarSizeDIP = sizeDIP; // Force layout. if (animate) { this.animate(false); } else { // No need to recalculate this.sidebarSizeDIP and this.totalSizeDIP again. this.doResize(); this.dispatchEventToListeners(Events.SidebarSizeChanged, this.sidebarSize()); } } private animate(reverse: boolean, callback?: (() => void)): void { const animationTime = 50; this.animationCallback = callback || null; let animatedMarginPropertyName: string; if (this.isVerticalInternal) { animatedMarginPropertyName = this.secondIsSidebar ? 'margin-right' : 'margin-left'; } else { animatedMarginPropertyName = this.secondIsSidebar ? 'margin-bottom' : 'margin-top'; } const marginFrom = reverse ? '0' : '-' + ZoomManager.instance().dipToCSS(this.sidebarSizeDIP) + 'px'; const marginTo = reverse ? '-' + ZoomManager.instance().dipToCSS(this.sidebarSizeDIP) + 'px' : '0'; // This order of things is important. // 1. Resize main element early and force layout. this.contentElement.style.setProperty(animatedMarginPropertyName, marginFrom); this.contentElement.style.setProperty('overflow', 'hidden'); if (!reverse) { suppressUnused(this.mainElement.offsetWidth); suppressUnused(this.sidebarElementInternal.offsetWidth); } // 2. Issue onresize to the sidebar element, its size won't change. if (!reverse && this.sidebarWidgetInternal) { this.sidebarWidgetInternal.doResize(); } // 3. Configure and run animation this.contentElement.style.setProperty('transition', animatedMarginPropertyName + ' ' + animationTime + 'ms linear'); const boundAnimationFrame = animationFrame.bind(this); let startTime: number|null = null; function animationFrame(this: SplitWidget): void { this.animationFrameHandle = 0; if (!startTime) { // Kick animation on first frame. this.contentElement.style.setProperty(animatedMarginPropertyName, marginTo); startTime = window.performance.now(); } else if (window.performance.now() < startTime + animationTime) { // Process regular animation frame. if (this.mainWidgetInternal) { this.mainWidgetInternal.doResize(); } } else { // Complete animation. this.cancelAnimation(); if (this.mainWidgetInternal) { this.mainWidgetInternal.doResize(); } this.dispatchEventToListeners(Events.SidebarSizeChanged, this.sidebarSize()); return; } this.animationFrameHandle = this.contentElement.window().requestAnimationFrame(boundAnimationFrame); } this.animationFrameHandle = this.contentElement.window().requestAnimationFrame(boundAnimationFrame); } private cancelAnimation(): void { this.contentElement.style.removeProperty('margin-top'); this.contentElement.style.removeProperty('margin-right'); this.contentElement.style.removeProperty('margin-bottom'); this.contentElement.style.removeProperty('margin-left'); this.contentElement.style.removeProperty('transition'); this.contentElement.style.removeProperty('overflow'); if (this.animationFrameHandle) { this.contentElement.window().cancelAnimationFrame(this.animationFrameHandle); this.animationFrameHandle = 0; } if (this.animationCallback) { this.animationCallback(); this.animationCallback = null; } } private applyConstraints(sidebarSize: number, userAction?: boolean): number { const totalSize = this.totalSizeDIP(); const zoomFactor = this.constraintsInDip ? 1 : ZoomManager.instance().zoomFactor(); let constraints: Constraints = this.sidebarWidgetInternal ? this.sidebarWidgetInternal.constraints() : new Constraints(); let minSidebarSize: 20|number = this.isVertical() ? constraints.minimum.width : constraints.minimum.height; if (!minSidebarSize) { minSidebarSize = MinPadding; } minSidebarSize *= zoomFactor; if (this.sidebarMinimized) { sidebarSize = minSidebarSize; } let preferredSidebarSize: 20|number = this.isVertical() ? constraints.preferred.width : constraints.preferred.height; if (!preferredSidebarSize) { preferredSidebarSize = MinPadding; } preferredSidebarSize *= zoomFactor; // Allow sidebar to be less than preferred by explicit user action. if (sidebarSize < preferredSidebarSize) { preferredSidebarSize = Math.max(sidebarSize, minSidebarSize); } preferredSidebarSize += zoomFactor; // 1 css pixel for splitter border. constraints = this.mainWidgetInternal ? this.mainWidgetInternal.constraints() : new Constraints(); let minMainSize: 20|number = this.isVertical() ? constraints.minimum.width : constraints.minimum.height; if (!minMainSize) { minMainSize = MinPadding; } minMainSize *= zoomFactor; let preferredMainSize: 20|number = this.isVertical() ? constraints.preferred.width : constraints.preferred.height; if (!preferredMainSize) { preferredMainSize = MinPadding; } preferredMainSize *= zoomFactor; const savedMainSize = this.isVertical() ? this.savedVerticalMainSize : this.savedHorizontalMainSize; if (savedMainSize !== null) { preferredMainSize = Math.min(preferredMainSize, savedMainSize * zoomFactor); } if (userAction) { preferredMainSize = minMainSize; } // Enough space for preferred. const totalPreferred = preferredMainSize + preferredSidebarSize; if (totalPreferred <= totalSize) { return Platform.NumberUtilities.clamp(sidebarSize, preferredSidebarSize, totalSize - preferredMainSize); } // Enough space for minimum. if (minMainSize + minSidebarSize <= totalSize) { const delta = totalPreferred - totalSize; const sidebarDelta = delta * preferredSidebarSize / totalPreferred; sidebarSize = preferredSidebarSize - sidebarDelta; return Platform.NumberUtilities.clamp(sidebarSize, minSidebarSize, totalSize - minMainSize); } // Not enough space even for minimum sizes. return Math.max(0, totalSize - minMainSize); } override wasShown(): void { this.forceUpdateLayout(); ZoomManager.instance().addEventListener(ZoomManagerEvents.ZoomChanged, this.onZoomChanged, this); } override willHide(): void { ZoomManager.instance().removeEventListener(ZoomManagerEvents.ZoomChanged, this.onZoomChanged, this); } override onResize(): void { this.updateLayout(); } override onLayout(): void { this.updateLayout(); } override calculateConstraints(): Constraints { if (this.showModeInternal === ShowMode.OnlyMain) { return this.mainWidgetInternal ? this.mainWidgetInternal.constraints() : new Constraints(); } if (this.showModeInternal === ShowMode.OnlySidebar) { return this.sidebarWidgetInternal ? this.sidebarWidgetInternal.constraints() : new Constraints(); } let mainConstraints: Constraints = this.mainWidgetInternal ? this.mainWidgetInternal.constraints() : new Constraints(); let sidebarConstraints: Constraints = this.sidebarWidgetInternal ? this.sidebarWidgetInternal.constraints() : new Constraints(); const min = MinPadding; if (this.isVerticalInternal) { mainConstraints = mainConstraints.widthToMax(min).addWidth(1); // 1 for splitter sidebarConstraints = sidebarConstraints.widthToMax(min); return mainConstraints.addWidth(sidebarConstraints).heightToMax(sidebarConstraints); } mainConstraints = mainConstraints.heightToMax(min).addHeight(1); // 1 for splitter sidebarConstraints = sidebarConstraints.heightToMax(min); return mainConstraints.widthToMax(sidebarConstraints).addHeight(sidebarConstraints); } private onResizeStart(): void { this.resizeStartSizeDIP = this.sidebarSizeDIP; } private onResizeUpdate(event: Common.EventTarget.EventTargetEvent<ResizeUpdatePositionEvent>): void { const offset = event.data.currentPosition - event.data.startPosition; const offsetDIP = ZoomManager.instance().cssToDIP(offset); const newSizeDIP = this.secondIsSidebar ? this.resizeStartSizeDIP - offsetDIP : this.resizeStartSizeDIP + offsetDIP; const constrainedSizeDIP = this.applyConstraints(newSizeDIP, true); this.savedSidebarSizeDIP = constrainedSizeDIP; this.saveSetting(); this.innerSetSidebarSizeDIP(constrainedSizeDIP, false, true); if (this.isVertical()) { this.savedVerticalMainSize = this.totalSizeDIP() - this.sidebarSizeDIP; } else { this.savedHorizontalMainSize = this.totalSizeDIP() - this.sidebarSizeDIP; } } private onResizeEnd(): void { this.resizeStartSizeDIP = 0; } hideDefaultResizer(noSplitter?: boolean): void { this.resizerElementInternal.classList.toggle('hidden', Boolean(noSplitter)); this.uninstallResizer(this.resizerElementInternal); this.sidebarElementInternal.classList.toggle('no-default-splitter', Boolean(noSplitter)); } installResizer(resizerElement: Element): void { this.resizerWidget.addElement((resizerElement as HTMLElement)); } uninstallResizer(resizerElement: Element): void { this.resizerWidget.removeElement((resizerElement as HTMLElement)); } hasCustomResizer(): boolean { const elements = this.resizerWidget.elements(); return elements.length > 1 || (elements.length === 1 && elements[0] !== this.resizerElementInternal); } toggleResizer(resizer: Element, on: boolean): void { if (on) { this.installResizer(resizer); } else { this.uninstallResizer(resizer); } } private settingForOrientation(): SettingForOrientation|null { const state = this.setting ? this.setting.get() : {}; return this.isVerticalInternal ? state.vertical : state.horizontal; } private preferredSidebarSizeDIP(): number { let size: number = this.savedSidebarSizeDIP; if (!size) { size = this.isVerticalInternal ? this.defaultSidebarWidth : this.defaultSidebarHeight; // If we have default value in percents, calculate it on first use. if (0 < size && size < 1) { size *= this.totalSizeDIP(); } } return size; } private restoreSidebarSizeFromSettings(): void { const settingForOrientation = this.settingForOrientation(); this.savedSidebarSizeDIP = settingForOrientation ? settingForOrientation.size : 0; } private restoreAndApplyShowModeFromSettings(): void { const orientationState = this.settingForOrientation(); this.savedShowMode = orientationState && orientationState.showMode ? orientationState.showMode : this.showModeInternal; this.showModeInternal = this.savedShowMode; switch (this.savedShowMode) { case ShowMode.Both: this.showBoth(); break; case ShowMode.OnlyMain: this.hideSidebar(); break; case ShowMode.OnlySidebar: this.hideMain(); break; } } private saveShowModeToSettings(): void { this.savedShowMode = this.showModeInternal; this.saveSetting(); } private saveSetting(): void { if (!this.setting) { return; } const state = this.setting.get(); const orientationState = (this.isVerticalInternal ? state.vertical : state.horizontal) || {}; orientationState.size = this.savedSidebarSizeDIP; if (this.shouldSaveShowMode) { orientationState.showMode = this.savedShowMode; } if (this.isVerticalInternal) { state.vertical = orientationState; } else { state.horizontal = orientationState; } this.setting.set(state); } private forceUpdateLayout(): void { // Force layout even if sidebar size does not change. this.sidebarSizeDIP = -1; this.updateLayout(); } private onZoomChanged(): void { this.forceUpdateLayout(); } createShowHideSidebarButton( showTitle: Common.UIString.LocalizedString, hideTitle: Common.UIString.LocalizedString, shownString: Common.UIString.LocalizedString, hiddenString: Common.UIString.LocalizedString): ToolbarButton { this.showSidebarButtonTitle = showTitle; this.hideSidebarButtonTitle = hideTitle; this.shownSidebarString = shownString; this.hiddenSidebarString = hiddenString; this.showHideSidebarButton = new ToolbarButton('', ''); this.showHideSidebarButton.addEventListener(ToolbarButton.Events.Click, buttonClicked, this); this.updateShowHideSidebarButton(); function buttonClicked(this: SplitWidget): void { this.toggleSidebar(); } return this.showHideSidebarButton; } toggleSidebar(): void { if (this.showModeInternal !== ShowMode.Both) { this.showBoth(true); ARIAUtils.alert(this.shownSidebarString); } else { this.hideSidebar(true); ARIAUtils.alert(this.hiddenSidebarString); } } private updateShowHideSidebarButton(): void { if (!this.showHideSidebarButton) { return; } const sidebarHidden = this.showModeInternal === ShowMode.OnlyMain; let glyph = ''; if (sidebarHidden) { glyph = this.isVertical() ? (this.isSidebarSecond() ? 'right-panel-open' : 'left-panel-open') : (this.isSidebarSecond() ? 'bottom-panel-open' : 'top-panel-open'); } else { glyph = this.isVertical() ? (this.isSidebarSecond() ? 'right-panel-close' : 'left-panel-close') : (this.isSidebarSecond() ? 'bottom-panel-close' : 'top-panel-close'); } this.showHideSidebarButton.setGlyph(glyph); this.showHideSidebarButton.setTitle(sidebarHidden ? this.showSidebarButtonTitle : this.hideSidebarButtonTitle); } } // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum ShowMode { Both = 'Both', OnlyMain = 'OnlyMain', OnlySidebar = 'OnlySidebar', } // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum Events { SidebarSizeChanged = 'SidebarSizeChanged', ShowModeChanged = 'ShowModeChanged', } export type EventTypes = { [Events.SidebarSizeChanged]: number, [Events.ShowModeChanged]: string, }; const MinPadding = 20; export interface SettingForOrientation { showMode: string; size: number; } const suppressUnused = function(_value: unknown): void {};