mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
feat: new resize
This commit is contained in:
parent
68b60d9766
commit
fd8ea597ca
6 changed files with 163 additions and 75 deletions
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
41
src/utils/hooks.ts
Normal 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 '),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue