refactor: order-independent scene setters, fragment-safe style extraction
This commit is contained in:
parent
a7e463cb94
commit
14916e6416
3 changed files with 71 additions and 42 deletions
18
src/main.ts
18
src/main.ts
|
|
@ -141,20 +141,22 @@ async function rebuildPreview(): Promise<void> {
|
||||||
compileError.hidden = true
|
compileError.hidden = true
|
||||||
compileError.textContent = ''
|
compileError.textContent = ''
|
||||||
|
|
||||||
if (runtime) {
|
if (!runtime) {
|
||||||
runtime.setTemplate(result.template)
|
|
||||||
runtime.setSlides(result.slides)
|
|
||||||
// Follow the slide being edited; the deck may just have grown.
|
|
||||||
if (currentSlide >= 0) {
|
|
||||||
runtime.goTo(currentSlide)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
runtime = createRuntime(stage, result.slides, {
|
runtime = createRuntime(stage, result.slides, {
|
||||||
template: result.template,
|
template: result.template,
|
||||||
onCurrentChange: (index) => setSlide(index)
|
onCurrentChange: (index) => setSlide(index)
|
||||||
})
|
})
|
||||||
|
// A reload restores the slide you were on (history state): open
|
||||||
|
// the editor and the selection there too.
|
||||||
|
setSlide(runtime.current)
|
||||||
}
|
}
|
||||||
|
runtime.setTemplate(result.template)
|
||||||
|
runtime.setSlides(result.slides)
|
||||||
runtime.setCss(result.css)
|
runtime.setCss(result.css)
|
||||||
|
// Follow the slide being edited; the deck may just have grown.
|
||||||
|
if (currentSlide >= 0) {
|
||||||
|
runtime.goTo(currentSlide)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Half-typed Less should not blank the preview: keep the last good
|
// Half-typed Less should not blank the preview: keep the last good
|
||||||
// output on screen and report what broke.
|
// output on screen and report what broke.
|
||||||
|
|
|
||||||
|
|
@ -57,21 +57,25 @@ export interface CompiledPresentation {
|
||||||
* removed from the markup because their raw Less must never reach the DOM —
|
* removed from the markup because their raw Less must never reach the DOM —
|
||||||
* the compiled CSS is injected separately, at `<?= css ?>`. A style tag that
|
* the compiled CSS is injected separately, at `<?= css ?>`. A style tag that
|
||||||
* *is* that injection point is left alone.
|
* *is* that injection point is left alone.
|
||||||
|
*
|
||||||
|
* Parsed as a fragment rather than a full document: as a document a leading
|
||||||
|
* `<style>` would land in `<head>` and be lost from the body output, taking
|
||||||
|
* the injection point with it.
|
||||||
*/
|
*/
|
||||||
export function extractStyles(markup: string): { html: string; style: string } {
|
export function extractStyles(markup: string): { html: string; style: string } {
|
||||||
if (!markup.includes('<style')) {
|
if (!markup.includes('<style')) {
|
||||||
return { html: markup, style: '' }
|
return { html: markup, style: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const hidden = markup
|
const fragment = document.createElement('div')
|
||||||
|
fragment.innerHTML = markup
|
||||||
.split(HTML_PLACEHOLDER)
|
.split(HTML_PLACEHOLDER)
|
||||||
.join(HTML_SENTINEL)
|
.join(HTML_SENTINEL)
|
||||||
.split(CSS_PLACEHOLDER)
|
.split(CSS_PLACEHOLDER)
|
||||||
.join(CSS_SENTINEL)
|
.join(CSS_SENTINEL)
|
||||||
const parsed = new DOMParser().parseFromString(hidden, 'text/html')
|
|
||||||
|
|
||||||
const blocks: string[] = []
|
const blocks: string[] = []
|
||||||
for (const element of Array.from(parsed.querySelectorAll('style'))) {
|
for (const element of Array.from(fragment.querySelectorAll('style'))) {
|
||||||
const source = element.textContent ?? ''
|
const source = element.textContent ?? ''
|
||||||
if (source.includes(CSS_SENTINEL)) {
|
if (source.includes(CSS_SENTINEL)) {
|
||||||
// This tag is where the compiled CSS gets injected, not authoring.
|
// This tag is where the compiled CSS gets injected, not authoring.
|
||||||
|
|
@ -81,7 +85,7 @@ export function extractStyles(markup: string): { html: string; style: string } {
|
||||||
element.remove()
|
element.remove()
|
||||||
}
|
}
|
||||||
|
|
||||||
const html = parsed.body.innerHTML
|
const html = fragment.innerHTML
|
||||||
.split(HTML_SENTINEL)
|
.split(HTML_SENTINEL)
|
||||||
.join('<?= html ?>')
|
.join('<?= html ?>')
|
||||||
.split(CSS_SENTINEL)
|
.split(CSS_SENTINEL)
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,16 @@ export interface Runtime {
|
||||||
previous(): void
|
previous(): void
|
||||||
/** Swap the deck in place (after recompiling) without resetting the view. */
|
/** Swap the deck in place (after recompiling) without resetting the view. */
|
||||||
setSlides(slides: readonly CompiledSlide[]): void
|
setSlides(slides: readonly CompiledSlide[]): void
|
||||||
/** Set the global slide; a template re-renders the scene around the slides. */
|
/**
|
||||||
|
* Set the global slide; a template re-renders the scene around the slides.
|
||||||
|
* Order-independent: a scene re-render re-applies the stored stylesheet.
|
||||||
|
*/
|
||||||
setTemplate(template: string): void
|
setTemplate(template: string): void
|
||||||
/** Inject the compiled stylesheet at the scene's `<?= css ?>`, or in the head. */
|
/**
|
||||||
|
* Set the compiled stylesheet: injected at the scene's `<?= css ?>`, or in
|
||||||
|
* the head when the scene has no injection point. Stored, so re-renders of
|
||||||
|
* the scene re-apply it — safe to call in any order with the other setters.
|
||||||
|
*/
|
||||||
setCss(css: string): void
|
setCss(css: string): void
|
||||||
readonly current: number
|
readonly current: number
|
||||||
readonly slides: readonly RuntimeSlide[]
|
readonly slides: readonly RuntimeSlide[]
|
||||||
|
|
@ -194,6 +201,7 @@ export function createRuntime(
|
||||||
let generation = 0
|
let generation = 0
|
||||||
let destroyed = false
|
let destroyed = false
|
||||||
let template = options.template ?? ''
|
let template = options.template ?? ''
|
||||||
|
let css = ''
|
||||||
let mountPoint: HTMLElement = container
|
let mountPoint: HTMLElement = container
|
||||||
|
|
||||||
container.setAttribute('data-psslides-runtime', '')
|
container.setAttribute('data-psslides-runtime', '')
|
||||||
|
|
@ -359,33 +367,30 @@ export function createRuntime(
|
||||||
window.addEventListener('popstate', onPopState)
|
window.addEventListener('popstate', onPopState)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the global slide. A template (`<?= html ?>` inside) is rendered once
|
* Render the global slide as the scene (or reset to plain slides when it
|
||||||
* as the scene — slides are mounted where the placeholder was, `<?= css ?>`
|
* is not a template) and re-apply the stored stylesheet to whatever the
|
||||||
* is filled by setCss. Anything else leaves the slide tree alone; its
|
* new scene offers: its `<?= css ?>`, or the head as a fallback.
|
||||||
* markup was already merged into the slides by the compiler.
|
|
||||||
*/
|
*/
|
||||||
function setTemplate(value: string): void {
|
function renderShell(): void {
|
||||||
if (destroyed || value === template) {
|
// Start from a clean container so a previous scene never lingers
|
||||||
return
|
// after the template goes away. The slide elements survive as
|
||||||
|
// objects; the caller re-homes them into the fresh mount point.
|
||||||
|
container.replaceChildren()
|
||||||
|
mountPoint = container
|
||||||
|
if (HTML_PLACEHOLDER.test(template)) {
|
||||||
|
container.innerHTML = template.split(HTML_PLACEHOLDER).join(MOUNT_POINT)
|
||||||
|
mountPoint = container.querySelector<HTMLElement>(MOUNT_SELECTOR) ?? container
|
||||||
}
|
}
|
||||||
template = value
|
applyCss()
|
||||||
renderShell()
|
|
||||||
// The shell was rebuilt, but the slide elements themselves survive as
|
|
||||||
// objects: re-home them instead of recreating anything.
|
|
||||||
mountPoint.append(...records.map((record) => record.element))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inject the compiled stylesheet. A `<style>` holding `<?= css ?>` inside
|
* Put the stored stylesheet in exactly one place: a `<style>` holding
|
||||||
* the container takes it in place — tagged `data-ps-css` so recompiles
|
* `<?= css ?>` inside the scene takes it in place — tagged `data-ps-css`
|
||||||
* keep aiming at it once the placeholder text is gone. With no such target
|
* so recompiles keep aiming at it once the placeholder text is gone. With
|
||||||
* the runtime keeps one stylesheet in the document head.
|
* no such target the runtime keeps one stylesheet in the document head.
|
||||||
*/
|
*/
|
||||||
function setCss(css: string): void {
|
function applyCss(): void {
|
||||||
if (destroyed) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let injected = false
|
let injected = false
|
||||||
for (const style of Array.from(container.querySelectorAll('style'))) {
|
for (const style of Array.from(container.querySelectorAll('style'))) {
|
||||||
const isTarget =
|
const isTarget =
|
||||||
|
|
@ -403,19 +408,35 @@ export function createRuntime(
|
||||||
if (fallbackSheet) {
|
if (fallbackSheet) {
|
||||||
fallbackSheet.textContent = ''
|
fallbackSheet.textContent = ''
|
||||||
}
|
}
|
||||||
} else {
|
} else if (css || fallbackSheet) {
|
||||||
headSheet().textContent = css
|
headSheet().textContent = css
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Render the global slide as the scene; plain markup leaves the DOM alone. */
|
function setCss(value: string): void {
|
||||||
function renderShell(): void {
|
if (destroyed) {
|
||||||
mountPoint = container
|
|
||||||
if (!template || !HTML_PLACEHOLDER.test(template)) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
container.innerHTML = template.split(HTML_PLACEHOLDER).join(MOUNT_POINT)
|
css = value
|
||||||
mountPoint = container.querySelector<HTMLElement>(MOUNT_SELECTOR) ?? container
|
applyCss()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the global slide. A template (`<?= html ?>` inside) is rendered once
|
||||||
|
* as the scene — slides are mounted where the placeholder was, and the
|
||||||
|
* stored stylesheet is re-applied where `<?= css ?>` is. Anything else
|
||||||
|
* leaves the slide tree alone; its markup was already merged into the
|
||||||
|
* slides by the compiler.
|
||||||
|
*/
|
||||||
|
function setTemplate(value: string): void {
|
||||||
|
if (destroyed || value === template) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
template = value
|
||||||
|
renderShell()
|
||||||
|
// The shell was rebuilt, but the slide elements themselves survive as
|
||||||
|
// objects: re-home them instead of recreating anything.
|
||||||
|
mountPoint.append(...records.map((record) => record.element))
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -443,6 +464,8 @@ export function createRuntime(
|
||||||
window.removeEventListener('popstate', onPopState)
|
window.removeEventListener('popstate', onPopState)
|
||||||
container.replaceChildren()
|
container.replaceChildren()
|
||||||
container.removeAttribute('data-psslides-runtime')
|
container.removeAttribute('data-psslides-runtime')
|
||||||
|
fallbackSheet?.remove()
|
||||||
|
fallbackSheet = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue