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);
}
}