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 SlIcon from "@shoelace-style/shoelace/dist/components/icon/icon.component.js"
import "@shoelace-style/shoelace/dist/themes/light.css"
import { Sortable } from "@shopify/draggable"
import IconPlus from "bootstrap-icons/icons/plus.svg"
import { WebwriterOrderItem } from "./webwriter-order-item"
import { ifDefined } from "lit/directives/if-defined.js"
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-order": WebwriterOrder;
}}
@customElement("webwriter-order")
export class WebwriterOrder extends LitElementWw {
localize = LOCALIZE
static scopedElements = {
"sl-button": SlButton,
"sl-icon": SlIcon
}
static styles = css`
:host {
display: flex !important;
flex-direction: column;
align-items: flex-start;
counter-reset: orderItem;
gap: 2px;
}
:host([layout=tiles]) {
flex-direction: row;
flex-wrap: wrap;
gap: 15px;
}
:host(:is([contenteditable=true], [contenteditable=""])) {
--text-color: var(--sl-color-success-700);
}
:host ::slotted(*::part(counter)) {
counter-increment: orderItem;
content: counter(orderItem) '. ';
cursor: move;
text-align: right;
padding-right: 0.5em;
min-width: 1.5em;
color: var(--text-color, auto);
}
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;
padding: var(--sl-spacing-x-small);
padding-left: 0;
}
#add-option {
order: 2147483647;
}
#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;
}
// broken for some reason o.O
/*
:host(:not([contenteditable=true]):not([contenteditable=""])) .author-only {
display: none;
} */
`
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-order-item").forEach(el => el.setAttribute("layout", value))
this.requestUpdate("layout")
}
get hideOrderButtons() {
return !!this.items[0]?.hideOrderButtons
}
@property({type: Boolean, attribute: true, reflect: true}) //@ts-ignore
@option({type: Boolean, label: {"en": "Hide order buttons", "de": "Pfeile zum Verschieben verstecken"}})
set hideOrderButtons(value: boolean) {
this.items.forEach(item => item.hideOrderButtons = value)
}
@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"
observer: MutationObserver
connectedCallback() {
super.connectedCallback()
this.addEventListener("ww-moveup", this.handleMove)
this.addEventListener("ww-movedown", this.handleMove)
this.addEventListener("ww-moveto", this.handleMove)
this.observer = new MutationObserver(() => {
this.items.forEach(item => item.requestUpdate())
if(this.isContentEditable) {
this.solution = this.items.map(item => item.id)
}
this.clearDropPreviews()
})
this.observer.observe(this, {childList: true})
// this.shadowRoot.adoptedStyleSheets = [...this.shadowRoot.adoptedStyleSheets, this.orderSheet]
}
serializedCallback() {
this.shuffleItems()
}
disconnectedCallback(): void {
super.disconnectedCallback()
this.observer?.disconnect()
this.observer = undefined
}
orderSheet = new CSSStyleSheet()
addItem() {
const orderItem = this.ownerDocument.createElement("webwriter-order-item")
const p = this.ownerDocument.createElement("p")
orderItem.appendChild(p)
orderItem.setAttribute("layout", this.layout)
this.append(orderItem)
this.solution = [...this.#solution, orderItem.id]
this.ownerDocument.getSelection().setBaseAndExtent(p, 0, p, 0)
}
shuffleItems() {
const n = this.items.length
const nums = shuffle([...(new Array(n)).keys()])
this.innerHTML = nums.map(i => this.children.item(i).outerHTML).join("")
}
clearDropPreviews = () => {
this.items.forEach(item => item.dropPreview = undefined)
}
@query("#items-slot")
accessor itemsSlot: HTMLSlotElement
@queryAssignedElements()
accessor items: WebwriterOrderItem[]
handleKeyDown(e: KeyboardEvent) {
if(e.key == "ArrowDown") {
e.stopPropagation()
e.preventDefault()
}
else if(e.key === "ArrowUp") {
e.stopPropagation()
e.preventDefault()
}
}
/*
If contenteditable: Reorder solution -> Triggers orderSheet change
If not contenteditable: Reorder value -> Triggers orderSheet change
*/
moveChild(elem: HTMLElement, i: number) {
/*
let items = this.items.map(el => el !== elem? el: undefined)
items.splice(i, 0, elem as WebwriterOrderItem)
items = items.filter(child => child)
this.replaceChildren(...items)
this.items.forEach(item => item.requestUpdate())*/
if(i === 0) {
this.insertAdjacentElement("afterbegin", elem)
}
else if(i < this.items.length) {
this.items[i].insertAdjacentElement("beforebegin", elem)
}
else {
this.insertAdjacentElement("beforeend", elem)
}
}
handleMove = (e: CustomEvent) => {
const elem = e.target as HTMLElement
const children = Array.from(this.children)
const pos = children.indexOf(elem)
let i: number
if(e.type === "ww-moveup") {
i = Math.max(0, pos - 1)
}
else if(e.type === "ww-movedown") {
i = Math.min(children.length, pos + 2)
}
else if(e.type === "ww-moveto") {
i = e.detail.i
}
this.moveChild(elem, i)
}
#solution: string[] = []
get solution() {
return this.#solution?.length? this.#solution: undefined
}
@property({attribute: false})
set solution(value: string[]) {
this.#solution = value
if(value) {
this.dispatchEvent(new CustomEvent("ww-answer-change", {
bubbles: true,
composed: true
}))
}
}
reportSolution() {
this.solution.forEach((id, i) => (this.querySelector(`#${id}`) as any).validOrder = i)
let wrongSolution: boolean = false
this.items.forEach(item => {
if(this.items.indexOf(item) != this.solution.indexOf(item.id)){
wrongSolution = true
if(this.showSolution != "full"){
this.items.forEach(i => {
i.showSolution = false
})
}
}
})
if(wrongSolution){
if(this.showSolution != "right"){
this.style.backgroundColor = "#F9B5C4"
}
}else{
this.style.backgroundColor = "#BCE194"
}
this.items.forEach(item=>{
item.style.pointerEvents="none"
})
}
reset() {
this.solution = undefined
this.style.backgroundColor = ""
this.shuffleItems()
this.items.forEach(item=>{
item.style.pointerEvents="auto"
})
}
protected firstUpdated(_changedProperties: PropertyValues): void {
this.showSolution = this.showSolution
}
render() {
return html`
${this.hasAttribute("contenteditable")?html`
this.addItem()}>
${msg("Add Option")}
`:""}
`
}
}