/** * Copyright (c) 2021 OpenLens Authors * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import "./drawer.scss"; import React from "react"; import { clipboard } from "electron"; import { createPortal } from "react-dom"; import { cssNames, noop, StorageHelper } from "../../utils"; import { Icon } from "../icon"; import { Animate, AnimateName } from "../animate"; import { history } from "../../navigation"; import { ResizeDirection, ResizeGrowthDirection, ResizeSide, ResizingAnchor } from "../resizing-anchor"; import drawerStorageInjectable, { defaultDrawerWidth, } from "./drawer-storage/drawer-storage.injectable"; import { withInjectables } from "@ogre-tools/injectable-react"; export type DrawerPosition = "top" | "left" | "right" | "bottom"; export interface DrawerProps { open: boolean; title: React.ReactNode; /** * The width or heigh (depending on `position`) of the Drawer. * * If not set then the Drawer will be resizable. */ size?: string; // e.g. 50%, 500px, etc. usePortal?: boolean; className?: string | object; contentClass?: string | object; position?: DrawerPosition; animation?: AnimateName; onClose?: () => void; toolbar?: React.ReactNode; } const defaultProps: Partial = { position: "right", animation: "slide-right", usePortal: false, onClose: noop, }; interface State { isCopied: boolean; width: number; } const resizingAnchorProps = new Map(); resizingAnchorProps.set("right", [ResizeDirection.HORIZONTAL, ResizeSide.LEADING, ResizeGrowthDirection.RIGHT_TO_LEFT]); resizingAnchorProps.set("left", [ResizeDirection.HORIZONTAL, ResizeSide.TRAILING, ResizeGrowthDirection.LEFT_TO_RIGHT]); resizingAnchorProps.set("top", [ResizeDirection.VERTICAL, ResizeSide.TRAILING, ResizeGrowthDirection.TOP_TO_BOTTOM]); resizingAnchorProps.set("bottom", [ResizeDirection.VERTICAL, ResizeSide.LEADING, ResizeGrowthDirection.BOTTOM_TO_TOP]); interface Dependencies { drawerStorage: StorageHelper<{ width: number }>; } class NonInjectedDrawer extends React.Component { static defaultProps = defaultProps as object; private mouseDownTarget: HTMLElement; private contentElem: HTMLElement; private scrollElem: HTMLElement; private scrollPos = new Map(); private stopListenLocation = history.listen(() => { this.restoreScrollPos(); }); public state = { isCopied: false, width: this.props.drawerStorage.get().width, }; componentDidMount() { // Using window target for events to make sure they will be catched after other places (e.g. Dialog) window.addEventListener("mousedown", this.onMouseDown); window.addEventListener("click", this.onClickOutside); window.addEventListener("keydown", this.onEscapeKey); window.addEventListener("click", this.fixUpTripleClick); } componentWillUnmount() { this.stopListenLocation(); window.removeEventListener("mousedown", this.onMouseDown); window.removeEventListener("click", this.onClickOutside); window.removeEventListener("click", this.fixUpTripleClick); window.removeEventListener("keydown", this.onEscapeKey); } resizeWidth = (width: number) => { this.setState({ width }); this.props.drawerStorage.merge({ width }); }; fixUpTripleClick = (ev: MouseEvent) => { // detail: A count of consecutive clicks that happened in a short amount of time if (ev.detail === 3) { const selection = window.getSelection(); selection.selectAllChildren(selection.anchorNode?.parentNode); } }; saveScrollPos = () => { if (!this.scrollElem) return; const key = history.location.key; this.scrollPos.set(key, this.scrollElem.scrollTop); }; restoreScrollPos = () => { if (!this.scrollElem) return; const key = history.location.key; this.scrollElem.scrollTop = this.scrollPos.get(key) || 0; }; onEscapeKey = (evt: KeyboardEvent) => { if (!this.props.open) { return; } if (evt.code === "Escape") { this.close(); } }; onClickOutside = (evt: MouseEvent) => { const { contentElem, mouseDownTarget, close, props: { open }} = this; if (!open || evt.defaultPrevented || contentElem.contains(mouseDownTarget)) { return; } const clickedElem = evt.target as HTMLElement; const isOutsideAnyDrawer = !clickedElem.closest(".Drawer"); if (isOutsideAnyDrawer) { close(); } this.mouseDownTarget = null; }; onMouseDown = (evt: MouseEvent) => { if (this.props.open) { this.mouseDownTarget = evt.target as HTMLElement; } }; close = () => { const { open, onClose } = this.props; if (open) onClose(); }; copyTitle = (title: string) => { const itemName = title.split(":").splice(1).join(":") || title; // copy whole if no : clipboard.writeText(itemName.trim()); this.setState({ isCopied: true }); setTimeout(() => { this.setState({ isCopied: false }); }, 3000); }; render() { const { className, contentClass, animation, open, position, title, children, toolbar, size, usePortal } = this.props; const { isCopied, width } = this.state; const copyTooltip = isCopied ? "Copied!" : "Copy"; const copyIcon = isCopied ? "done" : "content_copy"; const canCopyTitle = typeof title === "string" && title.length > 0; const [direction, placement, growthDirection] = resizingAnchorProps.get(position); const drawerSize = size || `${width}px`; const drawer = (
this.contentElem = e} >
{title} {canCopyTitle && ( this.copyTitle(title)}/> )}
{toolbar}
this.scrollElem = e} > {children}
{ !size && ( width} onDrag={this.resizeWidth} onDoubleClick={() => this.resizeWidth(defaultDrawerWidth)} minExtent={300} maxExtent={window.innerWidth * 0.9} /> ) }
); return usePortal ? createPortal(drawer, document.body) : drawer; } } export const Drawer = withInjectables( NonInjectedDrawer, { getProps: (di, props) => ({ drawerStorage: di.inject(drawerStorageInjectable), ...props, }), }, );