forked from theatre-js/theatre
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create a notification system that can display notifications in Theatr…
…e.js' Studio (theatre-js#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
- Loading branch information
1 parent
ef5752c
commit 62bc12a
Showing
14 changed files
with
818 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 changes: 150 additions & 0 deletions
150
packages/playground/src/shared/notifications/useDrag.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import logger from './logger' | ||
import * as globalVariableNames from './globalVariableNames' | ||
|
||
export type Notification = {title: string; message: string} | ||
export type NotificationType = 'info' | 'success' | 'warning' | ||
export type Notify = ( | ||
/** | ||
* The title of the notification. | ||
*/ | ||
title: string, | ||
/** | ||
* The message of the notification. | ||
*/ | ||
message: string, | ||
/** | ||
* An array of doc pages to link to. | ||
*/ | ||
docs?: {url: string; title: string}[], | ||
/** | ||
* Whether duplicate notifications should be allowed. | ||
*/ | ||
allowDuplicates?: boolean, | ||
) => void | ||
export type Notifiers = { | ||
/** | ||
* Show a success notification. | ||
*/ | ||
success: Notify | ||
/** | ||
* Show a warning notification. | ||
* | ||
* Say what happened in the title. | ||
* In the message, start with 1) a reassurance, then 2) explain why it happened, and 3) what the user can do about it. | ||
*/ | ||
warning: Notify | ||
/** | ||
* Show an info notification. | ||
*/ | ||
info: Notify | ||
} | ||
|
||
const createHandler = | ||
(type: NotificationType): Notify => | ||
(...args) => { | ||
if (type === 'warning') { | ||
logger.warn(args[1]) | ||
} | ||
|
||
// @ts-ignore | ||
return window?.[globalVariableNames.notifications]?.notify[type](...args) | ||
} | ||
|
||
export const notify: Notifiers = { | ||
warning: createHandler('warning'), | ||
success: createHandler('success'), | ||
info: createHandler('info'), | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.