feat: new resize

This commit is contained in:
EnixCoda 2019-08-10 18:59:44 +08:00
parent 68b60d9766
commit fd8ea597ca
No known key found for this signature in database
GPG key ID: 0C1A07377913A1DD
6 changed files with 163 additions and 75 deletions

View file

@ -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<Props> {
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<Props>) {
const [size, setSize] = React.useState(baseSize)
render() {
const { className, children } = this.props
const { size } = this.state
return (
<div className={cx('gitako-position-wrapper', className)}>
<ResizeHandler onResize={this.onResize} size={size} />
<div className={'gitako-position-content'} style={{ width: size }}>
{children}
</div>
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 (
<div className={cx('gitako-position-wrapper', className)}>
<div className={'gitako-position-content'} style={{ width: size }}>
{children}
</div>
)
}
<HorizontalResizeHandler
onResize={size => {
if (size < window.innerWidth - MINIMAL_CONTENT_VIEWPORT_WIDTH) setSize(size)
}}
size={size}
/>
</div>
)
}

View file

@ -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<Props> {
export default class HorizontalResizeHandler extends React.PureComponent<Props> {
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<Props> {
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()
}

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -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)
}

41
src/utils/hooks.ts Normal file
View file

@ -0,0 +1,41 @@
import * as React from 'react'
import { createStyleSheet, setStyleSheetMedia } from './general'
export function useWindowSize(
callback: (width: number) => void,
deps?: ReadonlyArray<any> | 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<HTMLStyleElement>()
// 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 '),
)
}
}