ref: 81d1e4e01d7001e6a0107792e6167136787295d6
dir: /view.js/
import {BooleanSlot, Variable, List, TextParameter, BooleanParameter, Define} from "./core.js"
export let categories = ["motion", "looks", "sound", "events", "control", "sensing", "operators", "variables", "lists", "custom", "extensions"]
function toChildElement(block, child, options)
{
if (child instanceof Array) child = child[0]
if (typeof child === "string") child = document.createTextNode(child)
if (child instanceof Node) return child
return toSelectElement(block, child.options, options)
}
function toChildrenElement(block, options)
{
let inputs = []
for (let [i, reference] of block.type.references?.entries() ?? []) {
inputs.push(toReferenceElement(block, options[reference + "s"] ?? [block.names[0]], i, options))
}
for (let child of block.inputs) {
inputs.push(toReporterElement(child, options))
}
let name = options.getName?.(block.type, ...inputs) ?? block.type.name(...inputs)
return name.map(child => toChildElement(block, child, options))
}
function resizeSelect(element)
{
let div = document.createElement("div")
div.classList.add("block", "stack")
div.style.setProperty("white-space", "pre")
div.style.setProperty("position", "fixed")
div.style.setProperty("width", "max-content")
let select = document.createElement("select")
let option = document.createElement("option")
option.append(element.selectedOptions[0]?.textContent ?? "")
select.append(option)
div.append(select)
document.body.append(div)
let width = select.offsetWidth
div.remove()
element.style.setProperty("width", `${width}px`)
}
function resizeInput(input)
{
let span = document.createElement("span")
span.classList.add("input")
span.style.setProperty("white-space", "pre")
span.append(input.value)
span.style.setProperty("position", "fixed")
span.style.setProperty("width", "max-content")
document.body.append(span)
let width = span.offsetWidth
span.remove()
input.style.setProperty("width", `${width}px`)
}
function toReferenceElement(block, names, index, options)
{
let element = document.createElement("select")
for (let name of names) {
let option = document.createElement("option")
element.append(option)
option.append(name)
if (name === block.names[index]) option.selected = true
}
if (options.frozen) element.disabled = true
element.addEventListener("change", () =>
{
resizeSelect(element)
block.names[index] = element.value
})
resizeSelect(element)
return element
}
function toReporterElement(block, options)
{
if (block.type.shape === "slot") return toSlotElement(block, options)
let element = document.createElement("span")
element.classList.add("block", "reporter", categories.includes(block.type.category) ? block.type.category : "extensions")
if (block.type.shape === "reporter" && block.type.output === "boolean") {
element.classList.add("boolean")
}
if ([Variable, List, TextParameter, BooleanParameter].some(fn => fn.type === block.type)) {
element.append(block.names[0])
}
else {
let children = toChildrenElement(block, options)
element.append(...children)
}
element.dataset.id = block.id
return element
}
function toSlotElement(block, options)
{
if (block.type === BooleanSlot.type) {
let span = document.createElement("span")
span.classList.add("slot")
span.dataset.id = block.id
return span
}
if (block.type.toElement) return block.type.toElement(block, {frozen: options.frozen})
if (block.type.options) return toEnumeratedElement(block, options)
let input = document.createElement("input")
input.classList.add("input")
input.value = block.value
if (options.frozen) input.disabled = true
resizeInput(input)
input.addEventListener("input", () => resizeInput(input))
input.addEventListener("blur", () =>
{
let value = block.type.normalise(input.value)
input.value = value
block.value = value
resizeInput(input)
})
let span = document.createElement("span")
span.append(input)
span.dataset.id = block.id
return span
}
function toEnumeratedElement(block, options)
{
let element = document.createElement("select")
element.dataset.id = block.id
if (!block.type.canFit) element.classList.add("allow-block")
if (block.type.dynamic) {
let optionElement = document.createElement("option")
element.append(optionElement)
optionElement.append(options.getAddMessage?.(block.type) ?? "add another")
if (!options.addValue) optionElement.disabled = true
}
for (let option of block.type.options) {
let optionElement = document.createElement("option")
element.append(optionElement)
optionElement.append(options.getName?.(block.type, option) ?? block.type.name?.(option) ?? option)
optionElement.value = option
}
if (options.frozen) element.disabled = true
if (element.querySelectorAll("option:enabled").length <= 1) element.disabled = true
element.addEventListener("change", () =>
{
if (block.type.dynamic && element.selectedIndex === 0) {
options.addValue?.(block)
element.value = block.value
return
}
block.value = element.value
resizeSelect(element)
})
element.value = block.value
resizeSelect(element)
return element
}
function toSelectElement(block, options, options0)
{
let element = document.createElement("select")
let actions = []
for (let option of options) {
let optionElement = document.createElement("option")
element.append(optionElement)
optionElement.append(option.label)
actions.push(() =>
{
let other = option.morph(block)
if (other.type.cap && block.parent && block.parent.stack.indexOf(block) !== block.parent.stack.length - 1) {
for (let [i, option] of options.entries()) {
if (option.type === block.type) {
element.children[i].selected = true
break
}
}
return
}
let element0 = element.closest(`[data-id="${block.id}"]`)
element0.dataset.id = other.id
element0.classList.toggle("cap", Boolean(other.type.cap))
options0.replace(block, other, Boolean(option.update))
block = other
})
if (block.type === option.type) optionElement.selected = true
}
if (!options0.replace || options0.frozen) element.disabled = true
if (element.querySelectorAll("option:enabled").length <= 1) element.disabled = true
element.addEventListener("change", () =>
{
actions[element.selectedIndex]()
resizeSelect(element)
})
resizeSelect(element)
return element
}
function toSingleElement(block, options)
{
if (block.type.shape === "reporter") return toReporterElement(block, options)
if (block.type === Define.type) return toDefineElement(block, options)
if (block.type.category === "custom") return toCustomBlockElement(block, options)
let div = document.createElement("div")
div.dataset.id = block.id
div.classList.add("block", "stack", categories.includes(block.type.category) ? block.type.category : "extensions")
let line = document.createElement("div")
line.classList.add("line")
line.append(...toChildrenElement(block, options))
div.append(line)
if (block.type.hat) {
let div2 = document.createElement("div")
div2.append(div)
div.classList.add("hat")
for (let child of block.stack) div2.append(toSingleElement(child, options))
return div2
}
else {
if (block.stack) {
div.classList.add("c")
let mouth = document.createElement("div")
mouth.classList.add("mouth")
div.append(mouth)
for (let child of block.stack) mouth.append(toSingleElement(child, options))
}
}
while (block.complement) {
block = block.complement
let line = document.createElement("div")
line.classList.add("line")
line.dataset.id = block.id
div.append(line)
line.append(...toChildrenElement(block, options))
if (block.stack) {
let mouth = document.createElement("div")
mouth.classList.add("mouth")
div.append(mouth)
for (let child of block.stack) mouth.append(toSingleElement(child, options))
}
}
if (block.stack) {
let line = document.createElement("div")
line.classList.add("line")
div.append(line)
if (block.type.loop) line.append(getIcon("loop-arrow"))
}
if (block.type.cap) div.classList.add("cap")
return div
}
function toDefineElement(block, options)
{
let div = document.createElement("div")
div.dataset.id = block.id
div.classList.add("block", "stack", "define", "custom")
let line = document.createElement("div")
line.classList.add("line")
div.append(line)
let div1 = document.createElement("div")
div1.append(div)
for (let child of block.stack) div1.append(toSingleElement(child, options))
let div2 = document.createElement("div")
div2.classList.add("block", "stack", "custom", "prototype")
if (block.atomic) div2.append(getIcon("bolt"))
let line2 = document.createElement("div")
line2.classList.add("line")
div2.append(line2)
line.append(...options.getName?.(block.type, div2) ?? block.type.name(div2))
for (let part of block.names[0]) {
if (typeof part === "string") {
let span = document.createElement("span")
span.append(part)
line2.append(span)
continue
}
let element = document.createElement("span")
element.classList.add("block", "reporter", "custom", "parameter")
if (part.type === "boolean") element.classList.add("boolean")
element.append(part.name)
line2.append(element)
}
return div1
}
function toCustomBlockElement(block, options)
{
let div = document.createElement("div")
div.classList.add("block", "stack", "custom")
let line = document.createElement("div")
line.classList.add("line")
div.append(line)
div.dataset.id = block.id
let i = 0
for (let part of block.names[0]) {
if (typeof part === "string") {
line.append(part)
continue
}
let element = toReporterElement(block.inputs[i], options)
element.dataset.index = i++
line.append(element)
}
return div
}
export function toElement(block, options = {})
{
let stack
if (!block.parent || block.type.shape !== undefined) stack = [block]
else stack = block.parent.stack.slice(block.parent.stack.indexOf(block))
let div = document.createElement("div")
div.classList.add("script")
div.append(...stack.map(block => toSingleElement(block, options)))
return div
}
export function query(id, node = document)
{
return node.querySelector(`[data-id="${id}"]`)
}
export function closestScriptID(element0)
{
if (element0.closest(":enabled")) return
let element = element0.closest(".script")
if (!element) return
element = element.querySelector(".hat") ?? element.querySelector(".define")
if (!element) return
return Number(element.dataset.id)
}
export function closestBlockID(element0)
{
if (element0.closest(":enabled")) return
let element = element0.closest(".block[data-id]")
if (!element) return
return Number(element.dataset.id)
}
export function closestParameterIndex(element0)
{
let element = element0.closest(".parameter:not([data-id])")
if (!element) return
let define = element.closest(".define")
let parameters = [...define.querySelectorAll(".parameter")]
return {index: parameters.indexOf(element), id: Number(define.dataset.id), element}
}
export function closestSlotID(element0)
{
let element = element0.closest(".slot")
if (!element) return
return Number(element.dataset.id)
}
export function closestNamePartIndex(element0)
{
let element = element0.closest(".define > .line > .custom > .line > span")
if (!element) return
let block = element.closest(".define")
return {index: [...element.parentNode.childNodes].indexOf(element), id: Number(block.dataset.id)}
}
function getIcon(name)
{
let span = document.createElement("span")
span.insertAdjacentHTML("beforeend", icons[name])
let svg = span.querySelector("svg")
svg.style.setProperty("height", "1.5em")
svg.style.setProperty("vertical-align", "middle")
return span
}
let icons = {
"loop-arrow": `<svg viewbox="3 0 16 24" fill="#EEE" stroke="#444" class="loop-arrow"><path transform-origin="12 12" transform="rotate(90)" d="M10 9V7.41c0-.89-1.08-1.34-1.71-.71L3.7 11.29c-.39.39-.39 1.02 0 1.41l4.59 4.59c.63.63 1.71.19 1.71-.7V14.9c5 0 8.5 1.6 11 5.1-1-5-4-10-11-11z"/></svg>`,
bolt: `<svg viewbox="6 0 12 24" fill="#EEE" stroke="#444"><path d="M10.67,21L10.67,21c-0.35,0-0.62-0.31-0.57-0.66L11,14H7.5c-0.88,0-0.33-0.75-0.31-0.78c1.26-2.23,3.15-5.53,5.65-9.93 c0.1-0.18,0.3-0.29,0.5-0.29h0c0.35,0,0.62,0.31,0.57,0.66L13.01,10h3.51c0.4,0,0.62,0.19,0.4,0.66c-3.29,5.74-5.2,9.09-5.75,10.05 C11.07,20.89,10.88,21,10.67,21z"/></svg>`,
}
let css = `
.block {
color: #444;
pointer-events: none;
white-space: nowrap;
}
.line, .mouth::before, .reporter {
background: var(--color);
border: 2px solid #0006;
border-color: oklab(from var(--color) calc(l - 0.25) a b);
background-origin: border-box;
pointer-events: auto;
touch-action: none;
cursor: move;
}
.stack {
width: max-content;
position: relative;
}
.mouth {
width: 0;
margin-inline-start: 1.125em;
position: relative;
min-height: 2em;
}
.line {
padding: 0.5em;
border-radius: 0.5em;
margin-bottom: -2px;
position: relative;
min-height: 2em;
display: grid;
grid-auto-flow: column;
align-items: baseline;
gap: 0.5em;
justify-content: start;
min-width: 4em;
}
:not(.prototype) > .line:not(:first-child):last-child {
padding: 0 0.5em;
justify-content: end;
align-content: center;
}
.c > .line:first-child, :not(.c) > .line, .reporter {
background-image: linear-gradient(#FFF4, #0000 75%);
}
.c > .line:not(first-child):last-child {
background-image: linear-gradient(#0000 25%, #0001);
}
.c > .line:not(first-child):last-child::after {
background-image: linear-gradient(#0001, #0001);
}
.c > .line:not(:first-child):not(:dir(rtl)) {
border-top-left-radius: 0;
}
.c > .line:not(:last-child):not(:dir(rtl)) {
border-bottom-left-radius: 0;
}
.c > .line:not(:first-child):dir(rtl) {
border-top-right-radius: 0;
}
.c > .line:not(:last-child):dir(rtl) {
border-bottom-right-radius: 0;
}
.c > .line {
min-width: 10em;
}
.block select {
background: linear-gradient(#FFF4, #0000 75%) var(--color);
font: inherit;
color: inherit;
padding: 0.25em 0.5em;
border: 1px solid #0006;
border-radius: 0.5em;
cursor: pointer;
}
.block select.allow-block {
border-radius: 128em;
background: linear-gradient(#6662, #FFF4);
border-width: 2px;
}
.block select:disabled, .input:disabled {
pointer-events: none;
}
.input {
background: linear-gradient(#EEE, #FFF);
font: inherit;
color: #666;
padding: 0.25em 0.5em;
border: 2px solid #888;
border-radius: 128em;
cursor: text;
min-width: 2.5em;
text-align: center;
overflow: hidden;
}
.input:empty::before {
content: " ";
white-space: pre;
}
.line::before, .line::after {
content: "";
width: 1.5em;
height: 0.5em;
position: absolute;
inset-inline-start: 1em;
border-radius: 0 0 calc(0.5em - 2px) calc(0.5em - 2px);
}
.line::before {
top: 0;
background: linear-gradient(#0006, #0008);
background: linear-gradient(#0000, #0004) oklab(from var(--color) calc(l - 0.25) a b);
background-clip: padding-box;
width: 1.5em;
height: calc(0.5em - 2px);
}
.line::after {
top: 100%;
background-color: inherit;
z-index: 1;
border: 2px solid #0006;
border-color: oklab(from var(--color) calc(l - 0.25) a b);
border-top: none;
}
.c > .line:not(:first-child)::before,
.c > .line:not(:last-child)::after {
inset-inline-start: calc(2em + 2px);
}
.mouth::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
inset-inline-start: -1.125em;
width: 1.25em;
border-width: 0 2px;
z-index: 1;
}
.mouth:empty::before {
bottom: -2px;
}
.hat {
padding-top: 1em;
}
.hat > .line {
min-width: 8em;
}
.hat > .line:not(:dir(rtl)) {
border-top-left-radius: 0;
}
.hat > .line:dir(rtl) {
border-top-right-radius: 0;
}
.hat > .line:last-child::before {
content: "";
background: linear-gradient(#FFF4, #FFF4);
background-color: inherit;
border: 2px solid #0006;
border-color: oklab(from var(--color) calc(l - 0.25) a b);
border-bottom: none;
position: absolute;
top: auto;
inset-inline-start: -2px;
bottom: 100%;
width: 6em;
height: 1em;
border-radius: 50% 50% 0 0 / 100% 100% 0 0;
}
.define > .line:last-child::before {
content: none;
}
.define > .line {
border-top-left-radius: 1.5em;
border-top-right-radius: 1.5em;
padding: 1em;
padding-inline-start: 0.5em;
}
.prototype {
display: flex;
gap: 0.5em;
align-items: baseline;
}
.cap > .line:last-child::after {
content: none;
}
.reporter {
display: grid;
grid-auto-flow: column;
align-items: center;
padding: 0.25em 0.5em;
border-radius: 100em;
width: max-content;
grid-gap: 0.5em;
}
.boolean {
border-width: 2px 3px;
border-radius: 0;
padding: 0.25em 1em;
clip-path: polygon(0 50%, 8888em -8888em, calc(100% - 8888em) -8888em, 100% 50%, calc(100% - 8888em) 8888em, 8888em 8888em);
position: relative;
overflow: hidden;
}
.boolean::before, .boolean::after {
content: "";
border: 888em solid #0000;
border-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="4" height="4" fill="%230006"><path d="M 0 2 2 0 4 2 2 4 M 0 0 0 4 4 4 4 0" /></svg>') 50%;
position: absolute;
top: 50%;
transform: translate(0, -50%);
pointer-events: none;
width: 64em;
}
.boolean::before {
left: 0;
}
.boolean::after {
right: 0;
}
.slot {
background: linear-gradient(#0008, #0004);
clip-path: polygon(0 50%, 88em -88em, calc(100% - 88em) -88em, 100% 50%, calc(100% - 88em) 88em, 88em 88em);
width: 3em;
height: 1.5em;
display: grid;
}
.slot::before {
content: "x";
align-self: center;
color: #0000;
}
.line > input, .parameter > input {
color: inherit;
font: inherit;
background: none;
border: none;
padding: 0;
margin: 0;
min-width: 0.5em;
}
.loop-arrow:dir(rtl) {
transform: scale(-1, 1);
}
.control { --color: #FB5; }
.events { --color: #FD6; }
.looks { --color: #CBF; }
.sound { --color: #E9E; }
.variables { --color: #F94; }
.operators { --color: #8C8; }
.lists { --color: #F96; }
.motion { --color: #8BF; }
.sensing { --color: #8CE; }
.custom { --color: #F9A; }
.define, .parameter { --color: #FBC; }
.extensions { --color: #1EA; }
`
let paintCSS = `
.line {
background: none !important;
border: 2px solid #0000;
border-radius: 0;
pointer-events: visiblepainted;
}
.line::before {
content: none !important;
}
.line::after {
content: paint(stack-block) !important;
border: none;
border-radius: 0;
pointer-events: visiblepainted;
top: -2px !important;
bottom: -0.5em !important;
left: -2px !important;
right: -2px !important;
width: auto;
height: auto;
z-index: -1;
pointer-events: none;
background: none !important;
}
.line:dir(rtl)::after {
transform: scale(-1, 1);
}
.boolean {
clip-path: none;
background: paint(boolean-block) border-box !important;
border-color: #0000 !important;
}
.boolean::before, .boolean::after {
content: none;
}
.hat > .line { --hat: true; }
.cap > .line:last-child { --cap: true; }
.define > .line::after { --define: true; }
.c > .line:not(:last-child) { --mouth-below: true; }
.c > .line:not(:first-child) { --mouth-above: true; }
.hat > .line::after {
top: calc(-2px - 1em) !important;
}
.c > .line:not(:first-child)::after {
top: -0.5em !important;
}
.mouth::before {
z-index: -2;
}
.c > .line:first-child {
z-index: 4100;
}
.script {
position: relative;
}
`
if (CSS.paintWorklet) {
CSS.paintWorklet?.addModule(import.meta.resolve("./worklet.js"))
css += paintCSS
for (let i = 0 ; i <= 4000 ; i++) css += `.block.stack:nth-child(${i + 1}) { z-index: ${4000 - i}; }\n`
}
let style = document.createElement("style")
style.textContent = css
document.head.append(style)