@andreasdymek/branching-scenario
Version:
Create adaptive learning experiences by authoring a gamebook, where you present the student with choices on how to continue. The content in the gamebook can be based on a all other WebWriter content types.
479 lines (428 loc) • 13.9 kB
text/typescript
import { html, css } from "lit";
import { LitElementWw } from "@webwriter/lit";
import { customElement, property, state } from "lit/decorators.js";
import { repeat } from "lit/directives/repeat.js";
// Shoelace Imports
import "@shoelace-style/shoelace/dist/themes/light.css";
import { SlButton, SlIcon, SlIconButton } from "@shoelace-style/shoelace";
import { NodeOutputSelect } from "../node-output-select/node-output-select";
import plus from "@tabler/icons/outline/plus.svg";
import minus from "@tabler/icons/outline/minus.svg";
import XCircleFill from "bootstrap-icons/icons/x-circle-fill.svg";
import { provide, consume, createContext } from "@lit/context";
import {
editorState,
GamebookEditorState,
} from "../../utils/gamebook-editor-state-context";
("node-connection-list")
export class NodeConnectionList extends LitElementWw {
({ type: Boolean, reflect: true }) accessor output = false;
({ type: Boolean, reflect: true }) accessor input = false;
({ type: Boolean, reflect: true }) accessor branch = false;
({ context: editorState, subscribe: true })
({ type: Object, attribute: true, reflect: false })
public accessor editorStore = new GamebookEditorState("Default");
/*
*/
// Registering custom elements used in the widget
static get scopedElements() {
return {
"sl-button": SlButton,
"sl-icon-button": SlIconButton,
"sl-icon": SlIcon,
"node-output-select": NodeOutputSelect,
};
}
/*
*/
static get styles() {
return css`
.titlebar {
display: flex;
flex-direction: row;
align-items: center;
border-bottom: 1px solid #d4d4d8;
padding-bottom: 5px;
width: 100%;
}
.titlebar p {
font-size: 12px;
color: #3f3f46;
margin: 0px;
padding: 0px;
margin-right: auto;
max-width: 150px;
min-width: 80px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.verticalStack {
display: flex;
flex-direction: column;
align-items: flex-end;
box-sizing: border-box;
height: 100%;
width: 100%;
overflow-y: scroll;
padding-right: 5px;
scrollbar-width: thin;
scrollbar-color: transparent transparent;
position: relative;
}
.item {
box-sizing: border-box;
display: flex;
flex-direction: row;
align-items: center;
gap: 5px;
width: 100%;
border-bottom: 1px solid #e4e4e7;
padding-left: 5px;
box-sizing: border-box;
}
.item p {
padding: 0px;
margin: 0px;
font-size: 12px;
}
.itemButton {
width: 100%;
}
sl-button::part(base) {
justify-content: flex-start; /* Aligns the content to the left */
text-align: left; /* Ensures text within the button is aligned left */
}
sl-button::part(label) {
max-width: 130px; /* Ensures the label takes up the full width of the button */
text-align: left; /* Aligns the text within the label to the left */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: auto;
}
sl-icon-button.minus::part(base) {
padding: 0px;
}
sl-icon-button.add::part(base) {
padding-left: 0px;
padding-top: 0px;
padding-bottom: 0px;
padding-right: 4px;
font-size: 16px;
}
.verticalStack::-webkit-scrollbar {
width: 6px; /* Width of the scrollbar */
}
.verticalStack::-webkit-scrollbar-track {
background: transparent; /* Hide the track */
}
.verticalStack::-webkit-scrollbar-thumb {
background-color: #888; /* Color of the scrollbar handle */
border-radius: 10px; /* Rounded corners */
}
.verticalStack::-webkit-scrollbar-thumb:hover {
background-color: #555; /* Darker color on hover */
}
.no-node-message {
font-size: 12px;
color: darkgray;
margin: 0px;
padding-top: 10px;
}
.container {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: start;
width: 100%;
height: 100%;
}
.branch-item {
box-sizing: border-box;
display: flex;
flex-direction: row;
align-items: center;
gap: 5px;
width: 100%;
box-sizing: border-box;
}
.branch-item-container {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
margin-top: 10px;
gap: 5px;
width: 100%;
height: 100%;
text-align: center;
}
`;
}
/*
*/
renderOutputs() {
return html`
<div class="container">
<div class="titlebar">
<p>
Outputs
(${Object.keys(
this.editorStore.selectedNode.outputs
).length.toString()})
</p>
<sl-icon-button @click=${this._addOutput} src=${plus} class="add">
</sl-icon-button>
</div>
<div class="verticalStack">
${repeat(
Object.entries(this.editorStore.selectedNode.outputs),
([output_class]) =>
`${this.editorStore.selectedNode.id}-${output_class}`,
([output_class, drawflowConnection], index) => html`
<div
class="item"
@mouseenter=${() =>
this.dispatchEvent(
new CustomEvent("highlightOutput", {
detail: {
outputNodeId: this.editorStore.selectedNode.id,
outputClass: output_class,
},
bubbles: true,
composed: true,
})
)}
@mouseleave=${() =>
this.dispatchEvent(
new CustomEvent("unhighlightOutput", {
detail: {
outputNodeId: this.editorStore.selectedNode.id,
outputClass: output_class,
},
bubbles: true,
composed: true,
})
)}
>
<p style="color: gray">${index + 1}</p>
<node-output-select
.outputClass=${output_class}
in-output-list
.inOutputList=${true}
></node-output-select>
<sl-icon-button
class="minus"
src=${minus}
style="font-size: 15px;"
@click=${() => this._deleteOutput(output_class)}
></sl-icon-button>
</div>
`
)}
</div>
</div>
`;
}
/*
*/
renderInputs() {
return html`
<div class="container">
<div class="titlebar">
<p>
Incoming
(${this.editorStore.selectedNode?.inputs[
"input_1"
]?.connections.length.toString()})
</p>
</div>
<div class="verticalStack">
${this.editorStore.selectedNode?.inputs["input_1"]?.connections.map(
(connection, index) => html` <div class="item">
<p style="color: gray">${index + 1}</p>
<sl-button
class="itemButton"
variant="text"
size="small"
@mouseenter=${() =>
this.dispatchEvent(
new CustomEvent("highlightConnection", {
detail: {
outputNodeId: connection?.node,
inputNodeId: this.editorStore.selectedNode.id,
outputClass: connection?.input,
inputClass: "input_1",
highlightButton: true,
},
bubbles: true,
composed: true,
})
)}
@mouseleave=${() =>
this.dispatchEvent(
new CustomEvent("unhighlightConnection", {
detail: {
outputNodeId: connection?.node,
inputNodeId: this.editorStore.selectedNode.id,
outputClass: connection?.input,
inputClass: "input_1",
highlightButton: true,
},
bubbles: true,
composed: true,
})
)}
>
${this.editorStore.editorContent.drawflow.Home.data[
connection.node
].data.title}
</sl-button>
<sl-icon-button
src=${XCircleFill}
style="font-size: 14px; color: #71717A;"
@click=${() =>
this.dispatchEvent(
new CustomEvent("deleteConnection", {
detail: {
outputNodeId: connection?.node,
inputNodeId: this.editorStore.selectedNode.id,
outputClass: connection?.input,
inputClass: "input_1",
},
bubbles: true,
composed: true,
})
)}
></sl-icon-button>
</div>`
)}
</div>
</div>
`;
}
/*
*/
renderInputsBranch() {
const connections =
this.editorStore.selectedNode.inputs.input_1?.connections;
const length = connections ? Object.values(connections).length : 0;
return html`
<div class="container">
<div class="titlebar">
<p>Accessing</p>
</div>
<div class="verticalStack">
${length > 0
? html` ${connections.map(
(connection, index) => html`
<div class="item">
<p style="color: gray">${index + 1}</p>
<sl-button
class="itemButton"
variant="text"
size="small"
@mouseenter=${() =>
this.dispatchEvent(
new CustomEvent("highlightConnection", {
detail: {
outputNodeId: connection?.node,
inputNodeId: this.editorStore.selectedNode.id,
outputClass: connection?.input,
inputClass: "input_1",
highlightButton: true,
},
bubbles: true,
composed: true,
})
)}
@mouseleave=${() =>
this.dispatchEvent(
new CustomEvent("unhighlightConnection", {
detail: {
outputNodeId: connection?.node,
inputNodeId: this.editorStore.selectedNode.id,
outputClass: connection?.input,
inputClass: "input_1",
highlightButton: true,
},
bubbles: true,
composed: true,
})
)}
>
${
this.editorStore.editorContent.drawflow.Home.data[
connection.node
].data.title
}
</sl-button>
<sl-icon-button
src=${XCircleFill}
style="font-size: 14px; color: #71717A;"
@click=${() =>
this.dispatchEvent(
new CustomEvent("deleteConnection", {
detail: {
outputNodeId: connection?.node,
inputNodeId: this.editorStore.selectedNode.id,
outputClass: connection?.input,
inputClass: "input_1",
},
bubbles: true,
composed: true,
})
)}
></sl-icon-button>
</div>
</div>`
)}`
: html` <div class="container">
<p class="no-node-message">
Connect a node to create rules based on its content!
</p>
</div>`}
</div>
</div>
`;
}
/*
*/
render() {
if (this.output) {
return this.renderOutputs();
} else if (this.input) {
return this.renderInputs();
} else if (this.branch) {
return this.renderInputsBranch();
} else {
return html`<p>Please specify either 'input' or 'output' attribute.</p>`;
}
}
/*
*/
private _deleteOutput(output_class: string) {
this.dispatchEvent(
new CustomEvent("deleteOutput", {
detail: {
nodeId: this.editorStore.selectedNode.id,
outputClass: output_class,
},
bubbles: true,
composed: true,
})
);
}
/*
*/
private _addOutput() {
this.dispatchEvent(
new CustomEvent("addOutput", {
detail: { nodeId: this.editorStore.selectedNode.id },
bubbles: true,
composed: true,
})
);
}
}