@webwriter/branching-scenario
Version:
Create adaptable learning experiences by authoring a gamebook, where you present the student with choices on how to continue.
363 lines (311 loc) • 10.2 kB
text/typescript
import { html, css } from "lit";
import { LitElementWw } from "@webwriter/lit";
import {
property,
query,
queryAssignedElements,
} from "lit/decorators.js";
import { msg, localized } from "@lit/localize";
import { WebWriterGamebookButton } from "../webwriter-gamebook-button/webwriter-gamebook-button.component";
import { WebWriterGamebookBranchButton } from "../webwriter-gamebook-branch-button/webwriter-gamebook-branch-button.component";
import { WebWriterGamebookOptions } from "../../components/options-panel/webwriter-gamebook-options";
//Shoelace
import { SlDialog } from "@shoelace-style/shoelace";
/**
* Represents a popup dialog in the gamebook
*/
()
export class WebWriterGamebookPopup extends LitElementWw {
/**
* import CSS
* @internal
*/
static get styles() {
return css`
:host(:not([contenteditable="true"]):not([contenteditable=""])) .page {
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 10px; /* Adjust the value to your desired spacing */
box-sizing: border-box;
width: 100%;
height: auto;
padding: 0px;
}
:host([contenteditable="true"]) .page,
:host([contenteditable=""]) .page {
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 10px; /* Adjust the value to your desired spacing */
box-sizing: border-box;
width: 100%;
height: auto;
padding: 20px;
}
:host(:not([contenteditable="true"]):not([contenteditable=""]))
.author-only {
display: none;
}
/* Base style for the close button visibility */
sl-dialog::part(close-button) {
display: var(--close-button-display, flex);
}
/* Hide the close button when the class is applied */
.hide-close-button {
--close-button-display: none;
}
sl-dialog::part(base) {
position: absolute; /* Make the overlay absolute */
}
sl-dialog::part(overlay) {
position: absolute; /* Make the overlay absolute */
background-color: rgba(0, 0, 0, 0.7); /* Semi-transparent background */
}
`;
}
/**
* registering custom elements used in the widget
* @internal
*/
static get scopedElements() {
return {
"sl-dialog": SlDialog,
//"webwriter-gamebook-button": WebWriterGamebookButton,
"webwriter-gamebook-options": WebWriterGamebookOptions,
};
}
/** associated node id */
({ type: Number, attribute: true, reflect: true })
accessor drawflowNodeId;
/** @internal */
({ type: Number, attribute: true, reflect: true })
accessor tabIndex = -1;
/** The buttons in the popup */
({
flatten: true,
selector: "webwriter-gamebook-button, webwriter-gamebook-branch-button",
})
accessor buttons;
/** The title of the popup */
({ type: String, attribute: true, reflect: true })
accessor pageTitle;
/** The content of the popup */
({
flatten: true,
})
accessor slotContent: Array<HTMLElement>;
/** The Shoelace dialog element */
("#dialog") accessor dialog: SlDialog;
/** The title label of the dialog */
({ type: String, attribute: true, reflect: true })
accessor titleLabel = "Dialog";
/** Whether the header should be disabled */
({ type: Boolean, attribute: true, reflect: true })
accessor noHeader = false;
/** Whether closing the dialog should be prevented */
({ type: Boolean, attribute: true, reflect: true })
accessor preventClosing = true;
/** The id of the branch connected to the page */
({ type: Number, attribute: true, reflect: true })
accessor branchesOff = -1;
// Create an observer instance linked to the callback function
private mutationObserver: MutationObserver;
/*
*/
constructor() {
super();
this.mutationObserver = new MutationObserver(this.mutationCallback);
}
/*
*/
connectedCallback() {
super.connectedCallback();
this.addEventListener("keydown", this._handleKeydown, true);
const fixLayoutShiftSheet = new CSSStyleSheet();
fixLayoutShiftSheet.replaceSync("html.sl-scroll-lock { scrollbar-gutter: auto !important }");
this.addEventListener("sl-show", () => {
if (this.ownerDocument.defaultView.innerWidth <= this.ownerDocument.documentElement.clientWidth) {
this.ownerDocument.adoptedStyleSheets.push(fixLayoutShiftSheet);
}
});
this.addEventListener("sl-hide", () => {
setTimeout(() => {
this.ownerDocument.adoptedStyleSheets = this.ownerDocument.adoptedStyleSheets.filter(
(sheet) => sheet !== fixLayoutShiftSheet
);
}, 300);
});
}
/*
*/
disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener("keydown", this._handleKeydown);
}
/*
* Handles CTRL+A to select all content within the page instead of the whole widget
*/
private _handleKeydown = (event: KeyboardEvent) => {
if ((event.metaKey || event.ctrlKey) && event.key === "a") {
event.preventDefault();
event.stopPropagation();
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(this);
selection?.removeAllRanges();
selection?.addRange(range);
}
};
/*
*/
protected firstUpdated(_changedProperties: any): void {
// Options for the observer (which mutations to observe)
const config = { attributes: true, childList: true, subtree: true };
this.mutationObserver.observe(this, config);
const slot = this.shadowRoot.querySelector("slot");
const assignedElements = slot.assignedElements();
if (assignedElements.length == 0) {
const par = document.createElement("p");
par.textContent = msg("Write something here...");
this.appendChild(par);
}
if (this.preventClosing && this.dialog) {
// Prevent the dialog from closing when the user clicks on the overlay
this.dialog.addEventListener("sl-request-close", (event) => {
if (
event.detail.source === "overlay" ||
event.detail.source === "close-button" ||
event.detail.source === "keyboard"
) {
event.preventDefault();
}
});
this.dialog.classList.add("hide-close-button");
}
}
/*
*/
render() {
return html` ${this.isContentEditable
? html`
<slot class="page"></slot>
<webwriter-gamebook-options
class="author-only"
part="options"
></webwriter-gamebook-options>
`
: html`
<sl-dialog
id="dialog"
style="--width: 100%;"
label=${this.titleLabel}
?no-header=${this.noHeader ? true : false}
><slot class="page"></slot
></sl-dialog>
`}`;
}
/*
*/
public hide() {
this.style.display = "none";
this.stopAllMedia();
}
/**
* Enables the popup
*/
public show() {
this.style.display = "block";
}
/**
* Stops all HTML media elements in the popup
*/
private stopAllMedia() {
const mediaElements = this.slotContent.filter((el) =>
el.matches("audio, video")
) as (HTMLAudioElement | HTMLVideoElement)[];
mediaElements.forEach((media) => {
media.pause();
media.currentTime = 0;
});
}
/**
* Hides the popup
*/
public hideDialog() {
this.dialog.hide();
this.style.display = "none";
}
/**
* Shows the popup
*/
public showDialog() {
this.style.display = "block";
this.dialog.show();
}
/*
*/
private mutationCallback = (mutationList: MutationRecord[]) => {
mutationList.forEach(({ type, removedNodes, attributeName }) => {
if (type === "childList") {
removedNodes.forEach((node) => {
const element = node as HTMLElement;
const nodeName = element.nodeName.toLowerCase();
const isSelectedNode = element.classList?.contains(
"ProseMirror-selectednode"
);
const dispatchEventIfNeeded = (eventName: string, detail: any) => {
const event = new CustomEvent(eventName, {
detail,
bubbles: true,
composed: true,
});
this.dispatchEvent(event);
};
if (element.classList?.contains("ww-widget")) {
if (
nodeName === "webwriter-gamebook-button" ||
nodeName === "webwriter-gamebook-branch-button"
) {
const connButton = node as
| WebWriterGamebookButton
| WebWriterGamebookBranchButton;
if (connButton.identifier !== "x") {
dispatchEventIfNeeded("buttonDeleted", {
identifier: connButton.identifier,
});
}
} else if (
nodeName === "webwriter-quiz" ||
nodeName === "webwriter-task"
) {
if (this.branchesOff !== -1) {
dispatchEventIfNeeded("quizElementDeleted", {
containerId: this.branchesOff,
id: element.id,
isQuiz: nodeName === "webwriter-quiz",
});
}
}
}
});
} else if (type === "attributes" && attributeName === "class") {
if (this.classList.contains("ProseMirror-selectednode")) {
const event = new CustomEvent("nodeWwSelected", {
detail: { nodeId: this.drawflowNodeId },
bubbles: true,
composed: true,
});
this.dispatchEvent(event);
}
}
});
// Check if there is at least one paragraph <p> element in the container
const paragraphs = this.querySelectorAll("p");
if (paragraphs.length === 0) {
const par = document.createElement("p");
par.textContent = msg("Write something here...");
this.appendChild(par);
}
};
}