@lume/live-code
Version:
A `<live-code>` element for editable code with live output.
161 lines (130 loc) • 3.32 kB
text/typescript
import {Element, element, stringAttribute, type ElementAttributes} from '@lume/element'
import html from 'solid-js/html'
import {createEffect, onCleanup} from 'solid-js'
export class OutputViewErrorEvent extends ErrorEvent {
override error
constructor(error: unknown) {
super('error', {})
this.error = error
}
}
type OutputViewAttributes = 'value' | 'mode'
export
('output-view')
class OutputView extends Element {
override hasShadow = false
value = ''
mode: 'script>iframe' | 'html>iframe' = 'html>iframe'
#iframe!: HTMLIFrameElement
override connectedCallback() {
super.connectedCallback()
createEffect(() => {
if (!this.value) return // f.e. empty string
this.#renderCode()
})
window.addEventListener('message', this.#handleMessage)
}
#handleError(error: unknown) {
this.dispatchEvent(new OutputViewErrorEvent(error))
}
#handleMessage = (msg: MessageEvent<{error: unknown}>) => {
if (!(this.#iframe && msg.source === this.#iframe.contentWindow)) return
if (msg.data && msg.data.error) {
this.#handleError(msg.data.error)
}
}
#renderCode() {
switch (this.mode) {
case 'script>iframe': {
const html = /*html*/ `
<html>
<head></head>
<body>
<script>${iframeErrorHandler.toString()}</script>
<script>${this.value}</script>
</body>
</html>
`
const url = URL.createObjectURL(new Blob([html], {type: 'text/html'}))
this.#iframe.src = url
onCleanup(() => URL.revokeObjectURL(url))
break
}
case 'html>iframe': {
const html = /*html*/ `<script>${iframeErrorHandler.toString()}</script>` + this.value
const url = URL.createObjectURL(new Blob([html], {type: 'text/html'}))
this.#iframe.src = url
onCleanup(() => URL.revokeObjectURL(url))
break
}
}
}
override template = () => html`
<div class="live-code-preview">
${() => (this.value ? html`<iframe ref=${(e: HTMLIFrameElement) => (this.#iframe = e)}></iframe>` : null)}
</div>
`
override css = /*css*/ `
:host { display: contents; }
.live-code-preview iframe {
display: block;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
border: none;
}
.live-code-preview div {
height: 100%;
}
.hidden {
display: none !important;
}
`
}
declare module 'solid-js' {
namespace JSX {
interface IntrinsicElements {
'output-view': ElementAttributes<OutputView, OutputViewAttributes>
}
}
}
declare global {
interface HTMLElementTagNameMap {
'output-view': OutputView
}
}
const iframeErrorHandler = /*js*/ `
function onError(e) {
console.error('Error in <live-code> iframe:', e)
let error;
if (
window.PromiseRejectionEvent &&
e instanceof PromiseRejectionEvent
) {
if (e.reason instanceof Error) {
error = {
message: e.reason.message,
stack: e.reason.stack
};
} else {
error = {
message: e.reason
};
}
} else if (e.error) {
error = {
message: e.error.message,
stack: e.error.stack
};
} else {
error = {
message: e.message
};
}
const parentWindow = window.parent || window.opener;
parentWindow.postMessage({ error });
}
window.addEventListener("error", onError);
window.addEventListener("unhandledrejection", onError);
`