Create a notification system that can display notifications in Theatre.js' Studio (#320)
* Implement an internal library for studio notifications * Improve design a little * Document code * Change relative import to absolute one * Fix tiny styling issue * Add notifications playground * Add notifications empty state and keep notifications buttons always visible Also fix a bug related to not clearing the type and uniqueness checkers. * Simplify notifications playground * Treat window as optional in case it runs in server code
This commit is contained in:
parent
ef5752cbd3
commit
62bc12ab51
14 changed files with 818 additions and 11 deletions
45
packages/playground/src/shared/notifications/Scene.tsx
Normal file
45
packages/playground/src/shared/notifications/Scene.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import React, {useLayoutEffect, useRef} from 'react'
|
||||
import type {IProject} from '@theatre/core'
|
||||
import {onChange, types} from '@theatre/core'
|
||||
|
||||
const globalConfig = {
|
||||
background: {
|
||||
type: types.stringLiteral('black', {
|
||||
black: 'black',
|
||||
white: 'white',
|
||||
dynamic: 'dynamic',
|
||||
}),
|
||||
dynamic: types.rgba(),
|
||||
},
|
||||
}
|
||||
|
||||
export const Scene: React.FC<{project: IProject}> = ({project}) => {
|
||||
// This is cheap to call and always returns the same value, so no need for useMemo()
|
||||
const sheet = project.sheet('Scene', 'default')
|
||||
const containerRef = useRef<HTMLDivElement>(null!)
|
||||
const globalObj = sheet.object('global', globalConfig)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const unsubscribeFromChanges = onChange(globalObj.props, (newValues) => {
|
||||
containerRef.current.style.background =
|
||||
newValues.background.type !== 'dynamic'
|
||||
? newValues.background.type
|
||||
: newValues.background.dynamic.toString()
|
||||
})
|
||||
return unsubscribeFromChanges
|
||||
}, [globalObj])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
right: '0',
|
||||
top: 0,
|
||||
bottom: '0',
|
||||
background: '#333',
|
||||
}}
|
||||
></div>
|
||||
)
|
||||
}
|
40
packages/playground/src/shared/notifications/index.tsx
Normal file
40
packages/playground/src/shared/notifications/index.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import studio, {notify} from '@theatre/studio'
|
||||
import {getProject} from '@theatre/core'
|
||||
import {Scene} from './Scene'
|
||||
|
||||
studio.initialize()
|
||||
|
||||
// trigger warning notification
|
||||
getProject('Sample project').sheet('Scene').sequence.play()
|
||||
|
||||
// fire an info notification
|
||||
notify.info(
|
||||
'Welcome to the notifications playground!',
|
||||
'This is a basic example of a notification! You can see the code for this notification ' +
|
||||
'(and all others) at the start of index.tsx. You can also see examples of success and warnign notifications.',
|
||||
)
|
||||
|
||||
getProject('Sample project').ready.then(() => {
|
||||
// fire a success notification on project load
|
||||
notify.success(
|
||||
'Project loaded!',
|
||||
'Now you can start calling `sequence.play()` to trigger animations. ;)',
|
||||
)
|
||||
})
|
||||
|
||||
ReactDOM.render(
|
||||
<Scene
|
||||
project={getProject('Sample project', {
|
||||
// experiments: {
|
||||
// logging: {
|
||||
// internal: true,
|
||||
// dev: true,
|
||||
// min: TheatreLoggerLevel.TRACE,
|
||||
// },
|
||||
// },
|
||||
})}
|
||||
/>,
|
||||
document.getElementById('root'),
|
||||
)
|
150
packages/playground/src/shared/notifications/useDrag.ts
Normal file
150
packages/playground/src/shared/notifications/useDrag.ts
Normal file
|
@ -0,0 +1,150 @@
|
|||
import {useLayoutEffect, useRef} from 'react'
|
||||
|
||||
const noop = () => {}
|
||||
|
||||
function createCursorLock(cursor: string) {
|
||||
const el = document.createElement('div')
|
||||
el.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 9999999;`
|
||||
|
||||
el.style.cursor = cursor
|
||||
document.body.appendChild(el)
|
||||
const relinquish = () => {
|
||||
document.body.removeChild(el)
|
||||
}
|
||||
|
||||
return relinquish
|
||||
}
|
||||
|
||||
export type UseDragOpts = {
|
||||
disabled?: boolean
|
||||
dontBlockMouseDown?: boolean
|
||||
lockCursorTo?: string
|
||||
onDragStart?: (event: MouseEvent) => void | false
|
||||
onDragEnd?: (dragHappened: boolean) => void
|
||||
onDrag: (dx: number, dy: number, event: MouseEvent) => void
|
||||
}
|
||||
|
||||
export default function useDrag(
|
||||
target: HTMLElement | undefined | null,
|
||||
opts: UseDragOpts,
|
||||
) {
|
||||
const optsRef = useRef<typeof opts>(opts)
|
||||
optsRef.current = opts
|
||||
|
||||
const modeRef = useRef<'dragStartCalled' | 'dragging' | 'notDragging'>(
|
||||
'notDragging',
|
||||
)
|
||||
|
||||
const stateRef = useRef<{
|
||||
dragHappened: boolean
|
||||
startPos: {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
}>({dragHappened: false, startPos: {x: 0, y: 0}})
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!target) return
|
||||
|
||||
const getDistances = (event: MouseEvent): [number, number] => {
|
||||
const {startPos} = stateRef.current
|
||||
return [event.screenX - startPos.x, event.screenY - startPos.y]
|
||||
}
|
||||
|
||||
let relinquishCursorLock = noop
|
||||
|
||||
const dragHandler = (event: MouseEvent) => {
|
||||
if (!stateRef.current.dragHappened && optsRef.current.lockCursorTo) {
|
||||
relinquishCursorLock = createCursorLock(optsRef.current.lockCursorTo)
|
||||
}
|
||||
if (!stateRef.current.dragHappened) stateRef.current.dragHappened = true
|
||||
modeRef.current = 'dragging'
|
||||
|
||||
const deltas = getDistances(event)
|
||||
optsRef.current.onDrag(deltas[0], deltas[1], event)
|
||||
}
|
||||
|
||||
const dragEndHandler = () => {
|
||||
removeDragListeners()
|
||||
modeRef.current = 'notDragging'
|
||||
|
||||
optsRef.current.onDragEnd &&
|
||||
optsRef.current.onDragEnd(stateRef.current.dragHappened)
|
||||
relinquishCursorLock()
|
||||
relinquishCursorLock = noop
|
||||
}
|
||||
|
||||
const addDragListeners = () => {
|
||||
document.addEventListener('mousemove', dragHandler)
|
||||
document.addEventListener('mouseup', dragEndHandler)
|
||||
}
|
||||
|
||||
const removeDragListeners = () => {
|
||||
document.removeEventListener('mousemove', dragHandler)
|
||||
document.removeEventListener('mouseup', dragEndHandler)
|
||||
}
|
||||
|
||||
const preventUnwantedClick = (event: MouseEvent) => {
|
||||
if (optsRef.current.disabled) return
|
||||
if (stateRef.current.dragHappened) {
|
||||
if (
|
||||
!optsRef.current.dontBlockMouseDown &&
|
||||
modeRef.current !== 'notDragging'
|
||||
) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
}
|
||||
stateRef.current.dragHappened = false
|
||||
}
|
||||
}
|
||||
|
||||
const dragStartHandler = (event: MouseEvent) => {
|
||||
const opts = optsRef.current
|
||||
if (opts.disabled === true) return
|
||||
|
||||
if (event.button !== 0) return
|
||||
const resultOfStart = opts.onDragStart && opts.onDragStart(event)
|
||||
|
||||
if (resultOfStart === false) return
|
||||
|
||||
if (!opts.dontBlockMouseDown) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
modeRef.current = 'dragStartCalled'
|
||||
|
||||
const {screenX, screenY} = event
|
||||
stateRef.current.startPos = {x: screenX, y: screenY}
|
||||
stateRef.current.dragHappened = false
|
||||
|
||||
addDragListeners()
|
||||
}
|
||||
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
dragStartHandler(e)
|
||||
}
|
||||
|
||||
target.addEventListener('mousedown', onMouseDown)
|
||||
target.addEventListener('click', preventUnwantedClick)
|
||||
|
||||
return () => {
|
||||
removeDragListeners()
|
||||
target.removeEventListener('mousedown', onMouseDown)
|
||||
target.removeEventListener('click', preventUnwantedClick)
|
||||
relinquishCursorLock()
|
||||
|
||||
if (modeRef.current !== 'notDragging') {
|
||||
optsRef.current.onDragEnd &&
|
||||
optsRef.current.onDragEnd(modeRef.current === 'dragging')
|
||||
}
|
||||
modeRef.current = 'notDragging'
|
||||
}
|
||||
}, [target])
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue