import {html, css, PropertyValueMap, TemplateResult, PropertyValues} from "lit" import {styleMap} from "lit/directives/style-map.js" import {LitElementWw, option, action} from "@webwriter/lit" import {customElement, property, queryAssignedElements, query} from "lit/decorators.js" import SlButton from "@shoelace-style/shoelace/dist/components/button/button.component.js" import SlIcon from "@shoelace-style/shoelace/dist/components/icon/icon.component.js" import SlRadio from "@shoelace-style/shoelace/dist/components/radio/radio.component.js" import SlCheckbox from "@shoelace-style/shoelace/dist/components/checkbox/checkbox.component.js" import SlRadioButton from "@shoelace-style/shoelace/dist/components/radio-button/radio-button.component.js" import SlRadioGroup from "@shoelace-style/shoelace/dist/components/radio-group/radio-group.component.js" import IconPlusSquare from "bootstrap-icons/icons/plus-square.svg" import IconPlusCircle from "bootstrap-icons/icons/plus-circle.svg" import type { WebwriterChoiceItem } from "./webwriter-choice-item.js" import "@shoelace-style/shoelace/dist/themes/light.css" import MiniMasonry from "minimasonry" import LOCALIZE from "../../localization/generated" import {msg} from "@lit/localize" export function shuffle(a: T[]) { for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } declare global {interface HTMLElementTagNameMap { "webwriter-choice": WebwriterChoice; }} @customElement("webwriter-choice") export class WebwriterChoice extends LitElementWw { localize = LOCALIZE @property({type: String, attribute: true, reflect: true}) @option({ type: "select", options: [ {value: "truefalse", label: {"en": "True/False"}}, {value: "single", label: {"en": "Single Choice"}}, {value: "multiple", label: {"en": "Multiple Choice"}}, ] }) accessor mode: "truefalse" | "single" | "multiple" = "single" get layout(): "list" | "tiles" { return this.children?.item(0)?.getAttribute("layout") as any ?? "list" } @property({type: String, attribute: true, reflect: true}) //@ts-ignore @option({ type: "select", options: [ {value: "list", label: {"en": "List"}}, {value: "tiles", label: {"en": "Tiles"}} ], }) set layout(value) { this.querySelectorAll("webwriter-choice-item").forEach(el => el.setAttribute("layout", value)) this.requestUpdate("layout") } @property({type: Boolean, attribute: true, reflect: true}) @option({ type: Boolean, label: {"en": "Random Choice Order", "de": "Zufällige Reihenfolge"} }) accessor randomOrder = false @property({type: String, attribute: true, reflect: true}) @option({ type: "select", label: {"de": "Lösung anzeigen", "en": "Show solution"}, options: [ {value: "right", label: {"en": "Right answers", "de": "Korrekte Antworten"}}, {value: "both", label: {"en": "all (indicator)", "de": "Alle (Indikator)"}}, {value: "full", label: {"en": "all (conclusive)", "de": "Alle (vollständig)"}} ], }) accessor showSolution = "right" get solution() { const validIDs = this.items.filter(item => item.valid).map(item => item.id) return validIDs.length? validIDs: undefined } set solution(value: string[]) { this.items.forEach(item => value.includes(item.id)? item.valid=true: null) } reportSolution() { console.log(this.solution, this.items) let itemActive: boolean = false this.items.forEach(item => {itemActive = itemActive || item.active}) if(!itemActive){ this.items.forEach(item => {item.valid = undefined; item.active = false; item.style.pointerEvents = "auto"}) return } if(!this.solution) { this.items.forEach(item => item.valid = true) return } let wrongSolution: boolean = false this.items.forEach(item => { console.log(!(item.active === item.valid), item.active, item.valid) item.valid = (this.solution ?? []).includes(item.id) if(item.active != item.valid){ wrongSolution = true if(this.showSolution != "full"){ this.items.forEach(item => { item.showSolution = false }) } } item.style.pointerEvents = "none" } ) if(wrongSolution){ if(this.showSolution != "right"){ this.style.backgroundColor = "#F9B5C4" } }else{ this.style.backgroundColor = "#BCE194" } } reset() { this.items.forEach(item => {item.valid = undefined; item.active = false; item.showSolution = true; item.style.pointerEvents = "auto", this.style.backgroundColor = ""}) if (this.randomOrder) this.shuffleItems() } static scopedElements = { "sl-button": SlButton, "sl-icon": SlIcon, "sl-radio": SlRadio, "sl-checkbox": SlCheckbox, "sl-radio-button": SlRadioButton, "sl-radio-group": SlRadioGroup } @query("slot") accessor slotEl: HTMLSlotElement @action() addItem() { /* if(this.ownerDocument.getSelection().containsNode(this, true)) { this.ownerDocument.getSelection().modify("move", "backward", "character") } setTimeout(() => {*/ const choiceItem = this.ownerDocument.createElement("webwriter-choice-item") const p = this.ownerDocument.createElement("p") choiceItem.appendChild(p) choiceItem.setAttribute("layout", this.layout) this.appendChild(choiceItem) this.ownerDocument.getSelection().setBaseAndExtent(p, 0, p, 0) //}) } shuffleItems() { const items = this.slotEl.assignedElements() as WebwriterChoiceItem[] const n = items.length const nums = shuffle([...(new Array(n)).keys()]) items.forEach((el, i) => el.style.order = String(nums[i])) } connectedCallback(): void { super.connectedCallback() const observer = new MutationObserver(() => { if(!this.contentEditable && this.randomOrder) { this.shuffleItems() } }) observer.observe(this, {childList: true}) } protected firstUpdated(_changedProperties: PropertyValues): void { if(!this.contentEditable && this.randomOrder) { this.shuffleItems() } } static styles = css` :host { display: flex !important; flex-direction: column; align-items: flex-start; gap: 0.5rem; } :host([layout=tiles]) { flex-direction: row; flex-wrap: wrap; gap: 15px; & ::slotted(*) { aspect-ratio: 1; min-width: 125px; width: 125px; max-width: 350px; min-height: 125px; height: 125px; max-height: 350px; overflow: hidden; resize: both; border: 2px solid var(--sl-color-gray-500); border-radius: 5px; } } sl-button::part(label) { padding: 0; display: flex; flex-direction: row; align-items: center; } sl-button::part(base) { border: none; background: transparent; } sl-icon { width: 19px; height: 19px; } sl-icon::part(svg) { overflow: visible; } :host(:not([contenteditable=true]):not([contenteditable=""])) .author-only { display: none !important; } #add-option span { margin-inline-start: 0.5em; } :host(:is([contenteditable=true], [contenteditable=""])) ::slotted(*) { order: unset !important; } :host(:not([mode=multiple])) { --webwriter-choice-radius: 100%; } #add-option:not(:hover)::part(base) { color: darkgray; } :host([layout=tiles]) #add-option { width: 125px; height: 125px; overflow: hidden; border: 2px solid var(--sl-color-gray-300); border-radius: 5px; padding: 5px; display: flex; align-items: center; justify-content: center; } :host([mode=truefalse]) #add-option { display: none; } ` @queryAssignedElements() accessor items: WebwriterChoiceItem[] #value: number[] = [] get value() { return this.#value } @property({type: Array, attribute: false}) set value(value: number[]) { this.#value = value this.items.forEach((item, i) => { this.isContentEditable ? item.valid = this.#value.includes(i) : item.active = this.#value.includes(i) }) } private applyValue() { this.#value = this.items .map((item, i) => [item.valid, i] as [boolean, number]) .filter(([valid]) => valid) .map(([_, i]) => i) } handleControlChange = (e: CustomEvent) => { if(this.mode === "single") { const item = e.target as WebwriterChoiceItem const otherItems = this.items.filter(el => el !== item) otherItems.forEach(el => this.isContentEditable? el.valid = false: el.active = false) } this.applyValue() this.dispatchEvent(new CustomEvent("ww-answer-change", { detail: {value: this.value}, bubbles: true, composed: true })) } handleSlotChange(e: Event) { this.requestUpdate() } protected willUpdate(changed: PropertyValueMap) { if(changed.has("mode") && this.mode === "single") { this.items .filter(el => this.isContentEditable? el.valid: el.active) .slice(1) .forEach(el => this.isContentEditable? el.valid = false: el.active = false) } if(changed.has("mode") && this.mode === "truefalse") { this.items.forEach(el => el.remove()) } } @query("#items-slot") accessor itemsSlotEl: HTMLSlotElement render() { return html` ${this.mode === "truefalse" ? html` ${msg("True")} ${msg("False")} ` : html` ` } this.addItem()}> ${msg("Add Option")} ` } }