Make studio.ui available before the UI is loaded
This was a regression introduced by b83164f26f
This commit is contained in:
parent
bcfb91fbb7
commit
35fe1c375c
5 changed files with 104 additions and 67 deletions
11
packages/playground/src/shared/remote/index.html
Normal file
11
packages/playground/src/shared/remote/index.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Theatre.js Playground</title>
|
||||||
|
<script src="./index.tsx" type="module"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,6 +1,6 @@
|
||||||
import Scrub from '@theatre/studio/Scrub'
|
import Scrub from '@theatre/studio/Scrub'
|
||||||
import type {StudioHistoricState} from '@theatre/studio/store/types/historic'
|
import type {StudioHistoricState} from '@theatre/studio/store/types/historic'
|
||||||
import type UI from '@theatre/studio/UI'
|
import UI from '@theatre/studio/UI/UI'
|
||||||
import type {Pointer, Ticker} from '@theatre/dataverse'
|
import type {Pointer, Ticker} from '@theatre/dataverse'
|
||||||
import {Atom, PointerProxy, pointerToPrism} from '@theatre/dataverse'
|
import {Atom, PointerProxy, pointerToPrism} from '@theatre/dataverse'
|
||||||
import type {
|
import type {
|
||||||
|
@ -32,9 +32,6 @@ const DEFAULT_PERSISTENCE_KEY = 'theatre-0.4'
|
||||||
|
|
||||||
export type CoreExports = typeof _coreExports
|
export type CoreExports = typeof _coreExports
|
||||||
|
|
||||||
const UIConstructorModule =
|
|
||||||
typeof window !== 'undefined' ? import('./UI').then((M) => M.default) : null
|
|
||||||
|
|
||||||
const STUDIO_NOT_INITIALIZED_MESSAGE = `You seem to have imported '@theatre/studio' but haven't initialized it. You can initialize the studio by:
|
const STUDIO_NOT_INITIALIZED_MESSAGE = `You seem to have imported '@theatre/studio' but haven't initialized it. You can initialize the studio by:
|
||||||
\`\`\`
|
\`\`\`
|
||||||
import studio from '@theatre/studio'
|
import studio from '@theatre/studio'
|
||||||
|
@ -64,16 +61,9 @@ studio.initialize()
|
||||||
`
|
`
|
||||||
|
|
||||||
export class Studio {
|
export class Studio {
|
||||||
protected _ui: UI | null = null
|
readonly ui: UI
|
||||||
// this._uiInitDeferred.promise will resolve once this._ui is set
|
// this._uiInitDeferred.promise will resolve once this._ui is set
|
||||||
private _uiInitDeferred = defer()
|
|
||||||
get ui() {
|
|
||||||
if (!this._ui) {
|
|
||||||
debugger
|
|
||||||
throw new Error(`Studio.ui called before UI is initialized`)
|
|
||||||
}
|
|
||||||
return this._ui
|
|
||||||
}
|
|
||||||
readonly publicApi: IStudio
|
readonly publicApi: IStudio
|
||||||
readonly address: {studioId: string}
|
readonly address: {studioId: string}
|
||||||
readonly _projectsProxy: PointerProxy<Record<ProjectId, Project>> =
|
readonly _projectsProxy: PointerProxy<Record<ProjectId, Project>> =
|
||||||
|
@ -130,17 +120,7 @@ export class Studio {
|
||||||
this.address = {studioId: nanoid(10)}
|
this.address = {studioId: nanoid(10)}
|
||||||
this.publicApi = new TheatreStudio(this)
|
this.publicApi = new TheatreStudio(this)
|
||||||
|
|
||||||
// initialize UI if we're in the browser
|
this.ui = new UI(this)
|
||||||
if (process.env.NODE_ENV !== 'test' && typeof window !== 'undefined') {
|
|
||||||
UIConstructorModule!
|
|
||||||
.then((M) => {
|
|
||||||
this._ui = new M(this)
|
|
||||||
this._uiInitDeferred.resolve(null)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(`Failed initializing the UI at @theatre/studio.`, error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this._attachToIncomingProjects()
|
this._attachToIncomingProjects()
|
||||||
this.paneManager = new PaneManager(this)
|
this.paneManager = new PaneManager(this)
|
||||||
|
@ -215,13 +195,15 @@ export class Studio {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'test' && typeof window !== 'undefined') {
|
||||||
|
await this.ui.ready
|
||||||
|
}
|
||||||
|
|
||||||
this._initializedDeferred.resolve()
|
this._initializedDeferred.resolve()
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
this._uiInitDeferred.promise.then(() => {
|
|
||||||
this.ui.render()
|
this.ui.render()
|
||||||
checkForUpdates()
|
checkForUpdates()
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
70
theatre/studio/src/UI/UI.ts
Normal file
70
theatre/studio/src/UI/UI.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import type {Studio} from '@theatre/studio/Studio'
|
||||||
|
import {val} from '@theatre/dataverse'
|
||||||
|
|
||||||
|
const NonSSRBitsClass =
|
||||||
|
typeof window !== 'undefined'
|
||||||
|
? import('./UINonSSRBits').then((M) => M.default)
|
||||||
|
: null
|
||||||
|
|
||||||
|
export default class UI {
|
||||||
|
private _rendered = false
|
||||||
|
private _nonSSRBits = NonSSRBitsClass
|
||||||
|
? NonSSRBitsClass.then((NonSSRBitsClass) => new NonSSRBitsClass())
|
||||||
|
: Promise.reject()
|
||||||
|
readonly ready: Promise<void> = this._nonSSRBits.then(
|
||||||
|
() => undefined,
|
||||||
|
() => undefined,
|
||||||
|
)
|
||||||
|
|
||||||
|
constructor(readonly studio: Studio) {}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this._rendered) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this._rendered = true
|
||||||
|
|
||||||
|
this._nonSSRBits.then((b) => {
|
||||||
|
b.render()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.studio.transaction(({drafts}) => {
|
||||||
|
drafts.ahistoric.visibilityState = 'everythingIsHidden'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
restore() {
|
||||||
|
this.render()
|
||||||
|
this.studio.transaction(({drafts}) => {
|
||||||
|
drafts.ahistoric.visibilityState = 'everythingIsVisible'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get isHidden() {
|
||||||
|
return (
|
||||||
|
val(this.studio.atomP.ahistoric.visibilityState) === 'everythingIsHidden'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderToolset(toolsetId: string, htmlNode: HTMLElement) {
|
||||||
|
let shouldUnmount = false
|
||||||
|
|
||||||
|
let unmount: null | (() => void) = null
|
||||||
|
|
||||||
|
this._nonSSRBits.then((nonSSRBits) => {
|
||||||
|
if (shouldUnmount) return // unmount requested before the toolset is mounted, so, abort
|
||||||
|
unmount = nonSSRBits.renderToolset(toolsetId, htmlNode)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (unmount) {
|
||||||
|
unmount()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (shouldUnmount) return
|
||||||
|
shouldUnmount = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,20 +2,17 @@ import UIRoot from '@theatre/studio/UIRoot/UIRoot'
|
||||||
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import type {Studio} from './Studio'
|
import {getMounter} from '@theatre/studio/utils/renderInPortalInContext'
|
||||||
import {val} from '@theatre/dataverse'
|
import {withStyledShadow} from '@theatre/studio/css'
|
||||||
import {getMounter} from './utils/renderInPortalInContext'
|
import ExtensionToolbar from '@theatre/studio/toolbars/ExtensionToolbar/ExtensionToolbar'
|
||||||
import {withStyledShadow} from './css'
|
|
||||||
import ExtensionToolbar from './toolbars/ExtensionToolbar/ExtensionToolbar'
|
|
||||||
|
|
||||||
export default class UI {
|
export default class UINonSSRBits {
|
||||||
readonly containerEl = document.createElement('div')
|
readonly containerEl = document.createElement('div')
|
||||||
private _rendered = false
|
|
||||||
private _renderTimeout: NodeJS.Timer | undefined = undefined
|
private _renderTimeout: NodeJS.Timer | undefined = undefined
|
||||||
private _documentBodyUIIsRenderedIn: HTMLElement | undefined = undefined
|
private _documentBodyUIIsRenderedIn: HTMLElement | undefined = undefined
|
||||||
readonly containerShadow: ShadowRoot & HTMLElement
|
readonly containerShadow: ShadowRoot & HTMLElement
|
||||||
|
|
||||||
constructor(readonly studio: Studio) {
|
constructor() {
|
||||||
// @todo we can't bootstrap Theatre.js (as in, to design Theatre.js using theatre), if we rely on IDed elements
|
// @todo we can't bootstrap Theatre.js (as in, to design Theatre.js using theatre), if we rely on IDed elements
|
||||||
this.containerEl.id = 'theatrejs-studio-root'
|
this.containerEl.id = 'theatrejs-studio-root'
|
||||||
|
|
||||||
|
@ -49,15 +46,6 @@ export default class UI {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this._rendered) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this._rendered = true
|
|
||||||
|
|
||||||
this._render()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _render() {
|
|
||||||
const renderCallback = () => {
|
const renderCallback = () => {
|
||||||
if (!document.body) {
|
if (!document.body) {
|
||||||
this._renderTimeout = setTimeout(renderCallback, 5)
|
this._renderTimeout = setTimeout(renderCallback, 5)
|
||||||
|
@ -66,30 +54,14 @@ export default class UI {
|
||||||
this._renderTimeout = undefined
|
this._renderTimeout = undefined
|
||||||
this._documentBodyUIIsRenderedIn = document.body
|
this._documentBodyUIIsRenderedIn = document.body
|
||||||
this._documentBodyUIIsRenderedIn.appendChild(this.containerEl)
|
this._documentBodyUIIsRenderedIn.appendChild(this.containerEl)
|
||||||
ReactDOM.render(React.createElement(UIRoot), this.containerShadow)
|
ReactDOM.render(
|
||||||
|
React.createElement(UIRoot, {containerShadow: this.containerShadow}),
|
||||||
|
this.containerShadow,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
this._renderTimeout = setTimeout(renderCallback, 10)
|
this._renderTimeout = setTimeout(renderCallback, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
hide() {
|
|
||||||
this.studio.transaction(({drafts}) => {
|
|
||||||
drafts.ahistoric.visibilityState = 'everythingIsHidden'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
restore() {
|
|
||||||
this.render()
|
|
||||||
this.studio.transaction(({drafts}) => {
|
|
||||||
drafts.ahistoric.visibilityState = 'everythingIsVisible'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
get isHidden() {
|
|
||||||
return (
|
|
||||||
val(this.studio.atomP.ahistoric.visibilityState) === 'everythingIsHidden'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderToolset(toolsetId: string, htmlNode: HTMLElement) {
|
renderToolset(toolsetId: string, htmlNode: HTMLElement) {
|
||||||
const s = getMounter()
|
const s = getMounter()
|
||||||
|
|
|
@ -46,7 +46,9 @@ const INTERNAL_LOGGING = /Playground.+Theatre\.js/.test(
|
||||||
(typeof document !== 'undefined' ? document?.title : null) ?? '',
|
(typeof document !== 'undefined' ? document?.title : null) ?? '',
|
||||||
)
|
)
|
||||||
|
|
||||||
export default function UIRoot() {
|
export default function UIRoot(props: {
|
||||||
|
containerShadow: ShadowRoot & HTMLElement
|
||||||
|
}) {
|
||||||
const studio = getStudio()
|
const studio = getStudio()
|
||||||
const [portalLayerRef, portalLayer] = useRefAndState<HTMLDivElement>(
|
const [portalLayerRef, portalLayer] = useRefAndState<HTMLDivElement>(
|
||||||
undefined as $IntentionalAny,
|
undefined as $IntentionalAny,
|
||||||
|
@ -87,7 +89,7 @@ export default function UIRoot() {
|
||||||
target={
|
target={
|
||||||
window.__IS_VISUAL_REGRESSION_TESTING === true
|
window.__IS_VISUAL_REGRESSION_TESTING === true
|
||||||
? undefined
|
? undefined
|
||||||
: getStudio()!.ui.containerShadow
|
: props.containerShadow
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
|
|
Loading…
Reference in a new issue