import {css, html, PropertyValues} from "lit" import {LitElementWw, option} from "@webwriter/lit" import {customElement, property, query, queryAssignedElements} from "lit/decorators.js" import SlButton from "@shoelace-style/shoelace/dist/components/button/button.component.js" import SlButtonGroup from "@shoelace-style/shoelace/dist/components/button-group/button-group.component.js" import SlDropdown from "@shoelace-style/shoelace/dist/components/dropdown/dropdown.component.js" import SlMenu from "@shoelace-style/shoelace/dist/components/menu/menu.component.js" import SlMenuItem from "@shoelace-style/shoelace/dist/components/menu-item/menu-item.component.js" import SlIcon from "@shoelace-style/shoelace/dist/components/icon/icon.component.js" import IconUIChecksGrid from "bootstrap-icons/icons/ui-checks-grid.svg" import Icon123 from "bootstrap-icons/icons/123.svg" import IconCardText from "bootstrap-icons/icons/card-text.svg" import IconHighlighter from "bootstrap-icons/icons/highlighter.svg" import IconSubtract from "bootstrap-icons/icons/subtract.svg" import IconBodyText from "bootstrap-icons/icons/body-text.svg" import IconMic from "bootstrap-icons/icons/mic.svg" import IconImage from "bootstrap-icons/icons/image.svg" import IconSearch from "bootstrap-icons/icons/search.svg" import IconGrid3x3Gap from "bootstrap-icons/icons/grid-3x3-gap.svg" import "@shoelace-style/shoelace/dist/themes/light.css" import type { WebwriterTask } from "./webwriter-task.js" import LOCALIZE from "../../localization/generated" import {msg} from "@lit/localize" 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-quiz": WebwriterQuiz; }} @customElement("webwriter-quiz") export class WebwriterQuiz extends LitElementWw { localize = LOCALIZE get answerTypes() { return { "webwriter-choice": { label: msg("Choice"), icon: IconUIChecksGrid }, "webwriter-order": { label: msg("Order"), icon: Icon123 }, "webwriter-text": { label: msg("Text"), icon: IconCardText }, "webwriter-mark": { label: msg("Mark"), icon: IconHighlighter },/* "webwriter-pairing": { label: this.msg("Pairing"), icon: IconSubtract }, "webwriter-cloze": { label: this.msg("Cloze"), icon: IconBodyText },*/ "webwriter-speech": { label: msg("Speech"), //advanced: true, icon: IconMic }, /*"webwriter-wordsearch": { label: this.msg("Word Search"), advanced: true, icon: IconSearch }, "webwriter-memory": { label: this.msg("Memory"), advanced: true, icon: IconGrid3x3Gap }*/ } } static scopedElements = { "sl-button": SlButton, "sl-button-group": SlButtonGroup, "sl-icon": SlIcon, "sl-dropdown": SlDropdown, "sl-menu": SlMenu, "sl-menu-item": SlMenuItem, } addTask(answerTypeName: string) { const task = this.ownerDocument.createElement("webwriter-task") as WebwriterTask task.setAttribute("counter", this.counter) const prompt = this.ownerDocument.createElement("webwriter-task-prompt") const p = this.ownerDocument.createElement("p") prompt.append(p) prompt.slot = "prompt" const answer = this.ownerDocument.createElement(answerTypeName) task.appendChild(prompt) task.appendChild(answer) this.appendChild(task) document.getSelection().setBaseAndExtent(p, 0, p, 0) } static styles = css` :host { border-top: 1px solid darkgray; border-bottom: 1px solid darkgray; padding: 1ch; display: flex !important; flex-direction: column; gap: 2rem; counter-reset: task; } :host(:not([contenteditable=true]):not([contenteditable=""])) .author-only { display: none; } :host(:is([contenteditable=true], [contenteditable=""])) .user-only { display: none; } :host(:is([contenteditable=true], [contenteditable=""])) ::slotted(*) { order: unset !important; } sl-button-group::part(base) { display: flex; } sl-button-group > *:not(sl-dropdown) { flex-grow: 1; } sl-dropdown[data-empty] { display: none; } sl-button:not([caret])::part(label) { padding: 0; display: flex; flex-direction: row; width: 100%; justify-content: space-evenly; align-items: center; } sl-icon { width: 18px; height: 18px; } sl-menu::part(menu) { z-index: 10000; } .user-actions { & #submit { flex-grow: 3; } & #reset { flex-grow: 1; } } ` @property({type: Boolean, attribute: true, reflect: true}) @option({type: Boolean, label: {"en": "Random Task Order"}}) accessor randomOrder = false get counter(): "number" | "roman" | "roman-capitalized" | "alphabetical" | "alphabetical-capitalized" { return this.tasks[0]?.counter } @property({attribute: true}) //@ts-ignore @option({ type: "select", label: { "en": "Counter" }, options: [ {value: undefined, label: {"en": "None"}}, {value: "number", label: {"en": "1. 2. 3."}}, {value: "roman", label: {"en": "i. ii. iii."}}, {value: "roman-capitalized", label: {"en": "I. II. III."}}, {value: "alphabetical", label: {"en": "a. b. c."}}, {value: "alphabetical-capitalized", label: {"en": "A. B. C."}}, ] }) set counter(value) { this.tasks.forEach(el => el.counter = value) } shuffleTasks() { const n = this.tasks.length const nums = shuffle([...(new Array(n)).keys()]) this.tasks.forEach((el, i) => el.style.order = String(nums[i])) } handleSubmit(e: Event) { this.submitted = true this.dispatchEvent(new Event("submit")) this.tasks.forEach(task => task.handleSubmit()) } handleReset = () => { this.tasks.forEach(task => task.handleReset()) this.requestUpdate() this.submitted = false } observer: MutationObserver connectedCallback(): void { super.connectedCallback() this.observer = new MutationObserver(() => { if(!this.contentEditable && this.randomOrder) { this.shuffleTasks() } }) this.observer.observe(this, {childList: true}) } disconnectedCallback(): void { super.disconnectedCallback() this.observer.disconnect() } @query("slot") accessor slotEl: HTMLSlotElement @queryAssignedElements() accessor tasks: WebwriterTask[] @property({type: Boolean, attribute: true, reflect: true}) accessor submitted = false get isChanged() { return this.tasks.some(task => task.isChanged) } render() { const basicAnswerTypes = Object.keys(this.answerTypes).filter(k => !this.answerTypes[k]?.advanced) const otherAnswerTypes = Object.keys(this.answerTypes).filter(k => this.answerTypes[k]?.advanced) return html` this.requestUpdate()}> ${msg("Submit")} ${msg("Reset")} ${basicAnswerTypes.map(k => html` this.addTask(k)}> ${this.answerTypes[k].label} `)} ${otherAnswerTypes.map(k => html` this.addTask(k)}> ${this.answerTypes[k].label} `)} ` } }