From fd8ea597ca2fb85c6f3f2457af657efbbe41b722 Mon Sep 17 00:00:00 2001 From: EnixCoda Date: Sat, 10 Aug 2019 18:59:44 +0800 Subject: [PATCH] feat: new resize --- src/components/Resizable.tsx | 64 +++++++++++++++++------ src/components/ResizeHandler.tsx | 28 +++++----- src/content.less | 88 +++++++++++++++++--------------- src/utils/DOMHelper.ts | 6 +-- src/utils/general.ts | 11 ++++ src/utils/hooks.ts | 41 +++++++++++++++ 6 files changed, 163 insertions(+), 75 deletions(-) create mode 100644 src/utils/hooks.ts diff --git a/src/components/Resizable.tsx b/src/components/Resizable.tsx index a837e2b..edf3eed 100644 --- a/src/components/Resizable.tsx +++ b/src/components/Resizable.tsx @@ -1,6 +1,8 @@ import * as React from 'react' -import ResizeHandler from 'components/ResizeHandler' +import HorizontalResizeHandler from 'components/ResizeHandler' import cx from 'utils/cx' +import { useWindowSize, useMediaStyleSheet } from 'utils/hooks' +import { bodySpacingClassName } from 'utils/DOMHelper' export type Size = number type Props = { @@ -8,23 +10,51 @@ type Props = { className?: string } -export default class Resizable extends React.PureComponent { - state = { - size: this.props.baseSize, - } +const MINIMAL_CONTENT_VIEWPORT_WIDTH = 100 +const GITHUB_WIDTH = 1020 - onResize = (size: Size) => this.setState({ size: Math.max(this.props.baseSize, size) }) +export default function Resizable({ + baseSize, + className, + children, +}: React.PropsWithChildren) { + const [size, setSize] = React.useState(baseSize) - render() { - const { className, children } = this.props - const { size } = this.state - return ( -
- -
- {children} -
+ useWindowSize( + width => { + if (size > width - MINIMAL_CONTENT_VIEWPORT_WIDTH) + setSize(width - MINIMAL_CONTENT_VIEWPORT_WIDTH) + }, + [size], + ) + + React.useEffect(() => { + document.documentElement.style.setProperty('--gitako-width', size + 'px') + }, [size]) + + useMediaStyleSheet( + `.${bodySpacingClassName} { margin-left: calc(var(--gitako-width) * 2 + 1020px - 100vw); }`, + size => [`min-width: ${size + GITHUB_WIDTH}px`, `max-width: ${size * 2 + GITHUB_WIDTH}px`], + size, + ) + + useMediaStyleSheet( + `.${bodySpacingClassName} { margin-left: var(--gitako-width); }`, + size => [`max-width: ${size + GITHUB_WIDTH}px`], + size, + ) + + return ( +
+
+ {children}
- ) - } + { + if (size < window.innerWidth - MINIMAL_CONTENT_VIEWPORT_WIDTH) setSize(size) + }} + size={size} + /> +
+ ) } diff --git a/src/components/ResizeHandler.tsx b/src/components/ResizeHandler.tsx index b1045db..c4d4582 100644 --- a/src/components/ResizeHandler.tsx +++ b/src/components/ResizeHandler.tsx @@ -1,19 +1,25 @@ import * as React from 'react' import Icon from 'components/Icon' -import Resizable, { Size } from './Resizable' +import { Size } from './Resizable' type Props = { size: Size - onResize: Resizable['onResize'] + onResize(size: Size): void style?: React.CSSProperties } -export default class ResizeHandler extends React.PureComponent { +export default class HorizontalResizeHandler extends React.PureComponent { pointerDown = false startX = 0 - delta = 0 baseSize = this.props.size + componentWillReceiveProps(nextProps: Props) { + if (!this.pointerDown) { + // update baseSize when not resizing + this.baseSize = this.props.size + } + } + subscribeEvents = () => { window.addEventListener('mousemove', this.onPointerMove) window.addEventListener('mouseup', this.onPointerUp) @@ -24,24 +30,20 @@ export default class ResizeHandler extends React.PureComponent { window.removeEventListener('mouseup', this.onPointerUp) } - onPointerDown = (e: React.MouseEvent) => { - this.pointerDown = true - const { clientX } = e + onPointerDown = ({ clientX }: React.MouseEvent) => { this.startX = clientX + this.pointerDown = true this.subscribeEvents() } - onPointerMove = (e: MouseEvent) => { + onPointerMove = ({ clientX }: MouseEvent) => { if (!this.pointerDown) return - const { clientX } = e - const { onResize } = this.props - this.delta = this.startX - clientX - onResize(this.delta + this.baseSize) + this.props.onResize(clientX - this.startX + this.baseSize) } onPointerUp = () => { this.pointerDown = false - this.baseSize = Math.max(this.baseSize + this.delta, this.props.size) + this.baseSize = this.props.size this.unsubscribeEvents() } diff --git a/src/content.less b/src/content.less index fbf5566..7c134a0 100644 --- a/src/content.less +++ b/src/content.less @@ -5,20 +5,14 @@ @min-screen-width: 1280px; @github-content-width: 1020px; @side-bar-base-width: (@min-screen-width - @github-content-width); -@width-with-gitako: (@min-screen-width + @side-bar-base-width); +@min-width-with-gitako: (@min-screen-width + @side-bar-base-width); @github-header-z-index: 32; @github-pull-request-float-header-z-index: 110; @minimal-z-index: max(@github-header-z-index, @github-pull-request-float-header-z-index) + 1; -.with-@{name}-spacing { - margin-left: @side-bar-base-width; - @media (min-width: @min-screen-width) and (max-width: @width-with-gitako) { - margin-left: ~'calc(' @width-with-gitako ~' - 100vw)'; - } - @media (min-width: @width-with-gitako) { - margin-left: 0; - } +:root { + --gitako-width: @side-bar-base-width; } .@{name}-ready { @@ -88,7 +82,6 @@ position: fixed; top: 5px; left: 0; - transform: translate((@side-bar-base-width - 30px)); z-index: @minimal-z-index; display: inline-flex; justify-content: center; @@ -98,35 +91,45 @@ height: 30px; will-change: transform; border: 1px solid transparent; - transition: all @animation-duration ease; - @media screen and (min-width: @width-with-gitako) { - transform: translate(~'calc(50vw - (' @github-content-width ~') / 2 - 30px)'); + .transform-expanded { + transform: translate(calc(var(--gitako-width) - 30px)); + } + .transform-collapsed { + transform: translate(5px); } - &:hover { - .error-message { - display: inline; - } - } + .transform-expanded; + animation: toggle-show-button-wrapper-expand @animation-duration; &.collapsed { border-color: #999999; border-radius: 3px; background: #f5f5f5; - @media screen and (max-width: @width-with-gitako) { - transform: translate(5px); - } + .transform-collapsed; + animation: toggle-show-button-wrapper-collapse @animation-duration; .action-icon { color: #0366d6d0; } } - &.error { - .action-icon { - color: #cb2431; + @keyframes toggle-show-button-wrapper-expand { + from { + .transform-collapsed; + } + to { + .transform-expanded; + } + } + + @keyframes toggle-show-button-wrapper-collapse { + from { + .transform-expanded; + } + to { + .transform-collapsed; } } @@ -138,6 +141,18 @@ transition: all @animation-duration ease; } + &:hover { + .error-message { + display: inline; + } + } + + &.error { + .action-icon { + color: #cb2431; + } + } + .error-message { display: none; margin: 0 4px; @@ -151,34 +166,23 @@ left: 0; height: 100vh; z-index: @minimal-z-index; - min-width: @side-bar-base-width; display: flex; - justify-content: flex-end; - @media screen and (min-width: @min-screen-width) { - width: ~'calc((100vw - ' @github-content-width ~') / 2)'; - } &.hidden { .hidden; } - - .gitako-position-content { - max-width: 100%; - } } .@{name}-resize-handler { - @resize-handler-width: 16px; - @side-bar-and-handler-width: (@min-screen-width + @resize-handler-width); - display: none; - width: @resize-handler-width; - background: #fafbfc; + display: flex; + align-items: center; cursor: ew-resize; user-select: none; border-left: 1px solid #e1e4e8; - - @media screen and (min-width: (@min-screen-width + @side-bar-base-width + @resize-handler-width * 2)) { - display: flex; - align-items: center; + width: 2px; + background: #e1e4e8; + &:hover { + width: 16px; + background: #fafbfc; } .grabber-icon { diff --git a/src/utils/DOMHelper.ts b/src/utils/DOMHelper.ts index 71126ae..fb4ddba 100644 --- a/src/utils/DOMHelper.ts +++ b/src/utils/DOMHelper.ts @@ -20,12 +20,12 @@ function markGitakoReadyState() { * if should show gitako, then move body right to make space for showing gitako * otherwise, hide the space */ +export const bodySpacingClassName = 'with-gitako-spacing' function setBodyIndent(shouldShowGitako: boolean) { - const spacingClassName = 'with-gitako-spacing' if (shouldShowGitako) { - document.body.classList.add(spacingClassName) + document.body.classList.add(bodySpacingClassName) } else { - document.body.classList.remove(spacingClassName) + document.body.classList.remove(bodySpacingClassName) } } diff --git a/src/utils/general.ts b/src/utils/general.ts index d4cba23..91056f3 100644 --- a/src/utils/general.ts +++ b/src/utils/general.ts @@ -82,3 +82,14 @@ export function findNode( } } } + +export function createStyleSheet(content: string) { + const style = document.createElement('style') + style.appendChild(document.createTextNode(content)) + document.head.appendChild(style) + return style +} + +export function setStyleSheetMedia(style: HTMLStyleElement, media: string) { + style.setAttribute('media', media) +} diff --git a/src/utils/hooks.ts b/src/utils/hooks.ts new file mode 100644 index 0000000..a22abe6 --- /dev/null +++ b/src/utils/hooks.ts @@ -0,0 +1,41 @@ +import * as React from 'react' +import { createStyleSheet, setStyleSheetMedia } from './general' + +export function useWindowSize( + callback: (width: number) => void, + deps?: ReadonlyArray | undefined, +) { + React.useEffect(() => { + const resizeListener: (this: Window, ev: UIEvent) => void = () => callback(window.innerWidth) + + window.addEventListener('resize', resizeListener) + return () => window.removeEventListener('resize', resizeListener) + }, deps) +} + +export function useMediaStyleSheet( + content: string, + getMediaQuery: (width: number) => string[], + size: number, +) { + const style = React.useRef() + + // this order may prevent first effect actually occur at first time + React.useEffect(() => { + setSheetMedia() + }, [size]) + React.useEffect(() => { + style.current = createStyleSheet(content) + setSheetMedia() + }, []) + + function setSheetMedia() { + if (style.current) + setStyleSheetMedia( + style.current, + getMediaQuery(size) + .map(query => `(${query})`) + .join(' and '), + ) + } +}