import { html, css, LitElement, unsafeCSS, PropertyValues } from "lit"; import { repeat } from "lit/directives/repeat.js"; import { LitElementWw } from "@webwriter/lit"; import { customElement, property } from "lit/decorators.js"; // Shoelace Imports import "@shoelace-style/shoelace/dist/themes/light.css"; import { SlButton, SlDivider, SlIcon, SlIconButton, SlInput, SlOption, SlSelect, } from "@shoelace-style/shoelace"; //CSS import styles from "./branch-node-detail-view.styles"; //Tabler import plus from "@tabler/icons/outline/plus.svg"; import minus from "@tabler/icons/outline/minus.svg"; import circleDashedX from "@tabler/icons/outline/circle-dashed-x.svg"; import circleDashedCheck from "@tabler/icons/outline/circle-dashed-check.svg"; import arrowsSplit2 from "@tabler/icons/outline/arrows-split-2.svg"; import gripHorizontal from "@tabler/icons/outline/grip-horizontal.svg"; import percentage from "@tabler/icons/outline/percentage.svg"; import { NodeOutputSelect } from "../../node-output-select/node-output-select"; import { WebWriterQuizSelect } from "../../component-select/webwriter-quiz-select/webwriter-quiz-select"; import { WebWriterQuizTasksSelect } from "../../component-select/webwriter-quiz-tasks-select/webwriter-quiz-tasks-select"; import { ToggleTextInput } from "../../toggle-text-input/toggle-text-input"; import { NodeConnectionList } from "../../node-connection-list/node-connection-list"; import { provide, consume, createContext } from "@lit/context"; import { editorState, GamebookEditorState, } from "../../../utils/gamebook-editor-state-context"; @customElement("branch-node-detail-view") export class BranchNodeDetailView extends LitElementWw { //registering custom elements used in the widget static get scopedElements() { return { "sl-button": SlButton, "sl-icon-button": SlIconButton, "sl-icon": SlIcon, "sl-select": SlSelect, "sl-input": SlInput, "sl-option": SlOption, "sl-divider": SlDivider, "node-output-select": NodeOutputSelect, "webwriter-quiz-select": WebWriterQuizSelect, "toggle-text-input": ToggleTextInput, "node-connection-list": NodeConnectionList, "webwriter-quiz-tasks-select": WebWriterQuizTasksSelect, }; } //import CSS static styles = [styles]; //public properties are part of the component's public API @property({ type: Boolean }) accessor ruleDrag = false; private draggedIndex = -1; @property({ type: Number }) accessor hoveredDividerIndex = -1; @consume({ context: editorState, subscribe: true }) @property({ type: Object, attribute: true, reflect: false }) public accessor editorStore = new GamebookEditorState("Default"); // // protected firstUpdated(_changedProperties: PropertyValues): void {} render() { let isConnected = this.editorStore.selectedContainer?.incomingContainerId !== -1; return html`
this.renameNode(string)} >

Branch

${this.editorStore.selectedContainer ? html`

Rules (${this.editorStore.selectedContainer.rules?.length.toString()})

this.addEmptyRule()} ?disabled=${!isConnected} >
${this.editorStore.selectedContainer?.rules?.length > 0 ? html` ${repeat( this.editorStore.selectedContainer?.rules as Rule[], (rule, index) => html`
this._onDragStart(e, index)} @dragend=${this._onDragEnd} @dragover=${(e: DragEvent) => this._onDragOver(e, index)} @dragleave=${(e: DragEvent) => this._onDragLeave(e, index)} @drop=${(e: DragEvent) => this._onDrop(e)} >

${parseInt( (rule as any).output_id.split("_")[1], 10 )}

this.updateRuleElement( index, ( e.target as WebWriterQuizSelect ).selectElement.value.toString() )} .container=${ this.editorStore.branchIncomingContainer } > ${ (rule as any).elementId !== "" && rule.elementId !== "text" && this.editorStore.branchIncomingContainer .querySelector(`#${rule.elementId}`) ?.tagName.toLowerCase() == "webwriter-quiz" ? html` this.updateRuleTasks( index, ( e.target as WebWriterQuizSelect ).selectElement.value.toString() )} .quiz=${this.editorStore.branchIncomingContainer.querySelector( `#${rule.elementId}` )} > ` : null } this.updateRuleCondition( index, (e.target as HTMLSelectElement).value )} ?disabled=${!rule.isConditionEnabled} > Correct Incorrect ${ rule.elementId !== "" ? html`${this.editorStore.branchIncomingContainer .querySelector(`#${rule.elementId}`) ?.tagName?.toLowerCase() === "webwriter-quiz" && rule.condition == "" ? html` ` : this.editorStore.branchIncomingContainer .querySelector(`#${rule.elementId}`) ?.tagName.toLowerCase() == "webwriter-quiz" && (rule.condition == "correct" || rule.condition == "incorrect") ? html` this._validateAndUpdateRuleMatch( e, index )} > ` : null}` : null } this.editorStore.selectedContainer.deleteRule( rule.output_id )} ?disabled=${!this.isConnected} >
` )}

If no rule is satisfied, go to

` : html`

No branching rules

`}
` : null} `; } /* */ private _onDragStart(event: DragEvent, index: number) { this.draggedIndex = index; const stackElement = this.shadowRoot?.getElementById( `horizontal-stack-${index}` ); if (stackElement) { stackElement.classList.add("dragging"); } this.requestUpdate(); } /* */ private _onDragEnd() { if (this.draggedIndex !== -1) { const stackElement = this.shadowRoot?.getElementById( `horizontal-stack-${this.draggedIndex}` ); if (stackElement) { stackElement.classList.remove("dragging"); } } this.draggedIndex = -1; // Reset dragged index this.hoveredDividerIndex = -1; // Reset hovered divider index this.requestUpdate(); } /* */ private _onDragOver(event: DragEvent, index: number) { event.preventDefault(); this.hoveredDividerIndex = index; this.requestUpdate(); } /* */ private _onDragLeave(event: DragEvent, index: number) { event.preventDefault(); this.hoveredDividerIndex = -1; this.requestUpdate(); } /* */ private _onDrop(event: DragEvent) { event.preventDefault(); if ( this.draggedIndex !== -1 && this.hoveredDividerIndex !== -1 && this.draggedIndex !== this.hoveredDividerIndex ) { const { selectedContainer, selectedNode } = this.editorStore; let staticCopyRules = this.editorStore.selectedContainer.rules; const hoveredRuleOutput = selectedContainer.rules[this.hoveredDividerIndex].output_id; const draggedRuleOutput = selectedContainer.rules[this.draggedIndex].output_id; const outputs = selectedNode.outputs; outputs[draggedRuleOutput].connections.forEach((connection) => { const connectionDetail = { outputNodeId: selectedNode.id, inputNodeId: connection.node, inputClass: "input_1", }; this.dispatchEvent( new CustomEvent("createConnection", { detail: { ...connectionDetail, outputClass: hoveredRuleOutput }, bubbles: true, composed: true, }) ); this.dispatchEvent( new CustomEvent("deleteConnection", { detail: { ...connectionDetail, outputClass: draggedRuleOutput }, bubbles: true, composed: true, }) ); }); // Extract output numbers const hoveredOutputNumber = parseInt(hoveredRuleOutput.split("_")[1], 10); const draggedOutputNumber = parseInt(draggedRuleOutput.split("_")[1], 10); // Calculate range and movement direction const [minOutputNumber, maxOutputNumber] = [ Math.min(hoveredOutputNumber, draggedOutputNumber), Math.max(hoveredOutputNumber, draggedOutputNumber), ]; const adjustment = hoveredOutputNumber < draggedOutputNumber ? 1 : -1; // Iterate through outputs and adjust connections Object.keys(outputs).forEach((outputClass, index) => { const outputIdNumber = parseInt(outputClass.split("_")[1], 10); // Check if the output is between the hovered and dragged, excluding the dragged one if ( outputIdNumber >= minOutputNumber && outputIdNumber <= maxOutputNumber && outputIdNumber !== draggedOutputNumber ) { const newOutputClass = `output_${outputIdNumber + adjustment}`; outputs[outputClass].connections?.forEach((connection) => { const connectionDetail = { outputNodeId: selectedNode.id, inputNodeId: connection.node, inputClass: "input_1", }; this.dispatchEvent( new CustomEvent("createConnection", { detail: { ...connectionDetail, outputClass: newOutputClass }, bubbles: true, composed: true, }) ); this.dispatchEvent( new CustomEvent("deleteConnection", { detail: { ...connectionDetail, outputClass: outputClass }, bubbles: true, composed: true, }) ); }); staticCopyRules[index].output_id = newOutputClass; } }); staticCopyRules[this.draggedIndex].output_id = hoveredRuleOutput; //Update the rules index in the rules array according to drag by removing the rule and adding it at the drop position let [draggedRule] = staticCopyRules.splice(this.draggedIndex, 1); staticCopyRules.splice(this.hoveredDividerIndex, 0, draggedRule); this.editorStore.selectedContainer.updateRules(staticCopyRules); } this._onDragEnd(); this.dispatchEvent( new CustomEvent("markOutputs", { bubbles: true, composed: true, }) ); this.editorStore.setSelectedContainer(this.editorStore.selectedContainer); this.requestUpdate(); } /* */ private _validateAndUpdateRuleMatch(e: Event, index: number) { const inputElement = e.target as SlInput; let value = inputElement.value; if (value != "") { // Remove any non-numeric characters (this makes sure input is strictly numeric) value = value.replace(/[^0-9]/g, ""); // Convert the value to a number and clamp it to the range 0-100 let numericValue = Number(value); if (numericValue < 0) numericValue = 0; if (numericValue > 100) numericValue = 100; // Update the input value with the clamped number inputElement.value = numericValue.toString(); } // Update the rule match this.editorStore.selectedContainer._updateRuleMatch( index, inputElement.value ); this.editorStore.setSelectedContainer(this.editorStore.selectedContainer); this.requestUpdate(); } /* */ private addEmptyRule() { // Step 1 this.dispatchEvent( new CustomEvent("addOutput", { detail: { nodeId: this.editorStore.selectedNode.id, }, bubbles: true, composed: true, }) ); this.editorStore.selectedContainer.addEmptyRule( this.editorStore.selectedNode ); // Step 5: Ensure the else rule is handled properly if (this.editorStore.selectedContainer.elseRule) { this.editorStore.selectedContainer._moveElseRuleToLastOutput( this.editorStore.selectedNode ); } else { this.dispatchEvent( new CustomEvent("addOutput", { detail: { nodeId: this.editorStore.selectedNode.id, }, bubbles: true, composed: true, }) ); this.editorStore.selectedContainer.addEmptyElseRule( this.editorStore.selectedNode ); } this.editorStore.setSelectedContainer(this.editorStore.selectedContainer); this.requestUpdate(); } /* */ private updateRuleElement(index, value) { this.editorStore.selectedContainer._updateRuleElement( index, value, this.editorStore.branchIncomingContainer ); this.editorStore.setSelectedContainer(this.editorStore.selectedContainer); this.requestUpdate(); } /* */ private updateRuleTasks(index, value) { this.editorStore.selectedContainer._updateRuleTasks( index, value, this.editorStore.branchIncomingContainer ); this.editorStore.setSelectedContainer(this.editorStore.selectedContainer); this.requestUpdate(); } /* */ private updateRuleCondition(index, value) { this.editorStore.selectedContainer._updateRuleCondition( index, value, this.editorStore.branchIncomingContainer ); this.editorStore.setSelectedContainer(this.editorStore.selectedContainer); this.requestUpdate(); } /* */ /* */ private renameNode(text: String) { const event = new CustomEvent("renameSelectedNode", { detail: { newTitle: text }, bubbles: true, composed: true, }); this.dispatchEvent(event); } }