import {css, html, PropertyValues} from "lit" import {styleMap} from "lit/directives/style-map.js" import {LitElementWw} from "@webwriter/lit" import {customElement, property, query} from "lit/decorators.js" import SlIconButton from "@shoelace-style/shoelace/dist/components/icon-button/icon-button.component.js" import IconArrowUp from "bootstrap-icons/icons/arrow-up.svg" import IconArrowDown from "bootstrap-icons/icons/arrow-down.svg" import "@shoelace-style/shoelace/dist/themes/light.css" import LOCALIZE from "../../localization/generated" import {msg} from "@lit/localize" declare global {interface HTMLElementTagNameMap { "webwriter-order-item": WebwriterOrderItem; }} function exchangeElements(a: A, b: B) { var clonedA = a.cloneNode(true) as A var clonedB = b.cloneNode(true) as B b.parentNode.replaceChild(clonedA, b) a.parentNode.replaceChild(clonedB, a) return clonedA } @customElement("webwriter-order-item") export class WebwriterOrderItem extends LitElementWw { localize = LOCALIZE static scopedElements = { "sl-icon-button": SlIconButton } @property({attribute: false, type: Boolean}) accessor showSolution = true @property({attribute: true, reflect: true, converter: {toAttribute: (v: boolean) => v? "true": "false", fromAttribute: (attr: string) => attr === "true"}}) accessor draggable = true get elementIndex() { return Array.from(this.parentElement?.children ?? []).indexOf(this) } getSibling(i: number) { return this.parentElement.children.item(i) } emitClearDropPreview = () => { this.dispatchEvent(new CustomEvent("webwriter-clear-drop-preview", {bubbles: true})) } connectedCallback(): void { super.connectedCallback() this.dropPreview = undefined this.addEventListener("dragstart", e => { this.dropPreview = undefined e.dataTransfer.setData("text/html", this.outerHTML) e.dataTransfer.setData("text/plain", this.id) // setTimeout(() => this.baseEl.style.visibility = "hidden", 0) e.stopPropagation() }) this.addEventListener("drop", e => { e.stopPropagation() const id = e.dataTransfer.getData("text/plain") const inTopHalf = e.offsetY < this.offsetHeight / 2 const inLeftHalf = e.offsetX < this.offsetWidth / 2 const el = document.querySelector(`webwriter-order-item#${id}`) const offset = (this.layout === "tiles"? inLeftHalf: inTopHalf)? 0: 1 el.dispatchEvent(new CustomEvent("ww-moveto", {bubbles: true, detail: {i: this.elementIndex + offset}})) // this.insertAdjacentHTML(inTopHalf? "beforebegin": "afterend", html) }, {passive: true}) this.addEventListener("dragover", e => { const inTopHalf = e.offsetY < this.offsetHeight / 2 const inLeftHalf = e.offsetX < this.offsetWidth / 2 if(this.layout === "tiles") { this.dropPreview = inLeftHalf? "left": "right" } else { this.dropPreview = inTopHalf? "top": "bottom" } e.preventDefault() e.stopImmediatePropagation() }) this.addEventListener("dragleave", () => this.emitClearDropPreview(), {passive: true}) document.addEventListener("dragend", (e) => { // (e.target as HTMLElement)?.remove() this.emitClearDropPreview() }, {passive: true}) } @property({type: String, attribute: true, reflect: true}) accessor dropPreview: "top" | "bottom" | "left" | "right" | undefined = undefined @property({type: String, attribute: true, reflect: true}) accessor layout: "list" | "tiles" = "list" @property({type: Number, attribute: false}) accessor validOrder: number | undefined @property({type: Boolean, attribute: true, reflect: true}) accessor hideOrderButtons = false static styles = css` :host { position: relative; } :host(:is([contenteditable=true], [contenteditable=""])) .user-only { display: none; } ::marker { font-weight: bold; } :host(:is([contenteditable=true], [contenteditable=""])) ::marker { color: var(--sl-color-success-800); } ol { padding-left: 0; margin: 0; width: 100%; position: relative; } .solution { display: flex; align-items: center; justify-content: center; position: absolute; top: -7px; left: -13px; border: 2px solid var(--sl-color-gray-500); border-radius: 100%; width: 15px; height: 15px; font-size: 10px; z-index: 1; color: white; font-weight: bold; &[data-valid] { background: var(--sl-color-success-600); } &:not([data-valid]) { background: var(--sl-color-danger-600); } } :host([layout=tiles]) { border: 2px solid var(--sl-color-gray-500); border-radius: 5px; aspect-ratio: 1; min-width: 125px; width: 125px; max-width: 350px; min-height: 125px; height: 125px; max-height: 350px; & .solution { left: unset; top: -10px; right: -10px; width: 20px; height: 20px; font-size: 15px; } & ol[part=base] { padding-left: 0; margin: 0; aspect-ratio: 1; min-width: 125px; width: 125px; max-width: 350px; min-height: 125px; height: 125px; max-height: 350px; overflow-y: auto; scrollbar-width: thin; resize: both; } & ::slotted(:is(picture, audio, video, img, iframe)) { height: 100%; width: 100%; } & ::slotted(:not(:is(picture, audio, video, img, iframe))) { margin: 5px !important; } } :host(:not([layout=tiles])) { width: 100% !important; height: unset !important; & ol[part=base] { width: 100% !important; height: auto !important; } } li { list-style-type: inherit; list-style-position: inside; flex-direction: row; align-items: center; font-size: var(--sl-input-font-size-medium); cursor: move; } :host([droppreview])::before { content: ""; position: absolute; background: var(--sl-color-primary-600); } :host([droppreview=top])::before { height: 2px; width: 100%; top: -2px; } :host([droppreview=bottom])::before { height: 2px; width: 100%; bottom: -2px; } :host([droppreview=left])::before { left: -17px; height: 100%; width: 15px; } :host([droppreview=right])::before { right: -17px; height: 100%; width: 15px; } slot { display: inline-block; width: calc(100% - var(--offset)); cursor: text; } #order-buttons { position: absolute; right: 0; top: 0; & .order-button::part(base) { padding: 0; } } :host([hideorderbuttons]) #order-buttons { display: none; } #handle { box-sizing: border-box; border-radius: 2px; position: absolute; left: 0px; bottom: 0px; width: 20px; height: 25px; display: inline-block; overflow: hidden; line-height: 5px; padding: 1.5px 1px; padding-bottom: 0; cursor: move; vertical-align: middle; font-size: 12px; font-family: sans-serif; letter-spacing: 2px; color: #cccccc; text-shadow: 1px 0 1px black; font-weight: 600; opacity: 0.95; transform: rotate(135deg); } #handle::after { content: '.. .. ..'; } :host(:not([layout=tiles])) :is(#handle, #count) { display: none; } :host(:not([layout=list])) :is(#up, #down) { display: none; } :host([layout=tiles]) #main-li { display: contents; } :host([layout=tiles]) slot { width: 100%; box-sizing: border-box; } #count { display: flex; justify-content: center; align-items: center; display: inline-block; width: 3ch; position: absolute; bottom: -14px; left: -14px; z-index: 10; & li { display: list-item; width: 4ch; height: 4ch; font-size: 12px; font-weight: bold; border-radius: 100%; border: 2px solid var(--sl-color-gray-600); color: var(--sl-color-gray-600); box-sizing: border-box; background: white; padding-left: 7px; padding-top: 4px; overflow: hidden; &[data-two-digit] { padding-left: 4px; } } } ` handleClick = (e: PointerEvent, up=false) => { if((e.target as HTMLElement).id === "up") { this.dispatchEvent(new CustomEvent("ww-moveup", {bubbles: true})) } else { this.dispatchEvent(new CustomEvent("ww-movedown", {bubbles: true})) } e.stopPropagation() } @query("[part=base]") accessor baseEl: HTMLElement @query("#content") accessor contentSlotEl: HTMLSlotElement observer: MutationObserver protected async updated(_changedProperties: PropertyValues) { if(_changedProperties.has("layout") && this.layout === "list") { // console.log("switched to list") this.syncSize(true) this.observer?.disconnect() this.requestUpdate() } else if(_changedProperties.has("layout") && this.layout === "tiles") { this.syncSize() if(!this.observer) { this.observer = new MutationObserver(() => this.syncSize()) this.observer.observe(this.baseEl, {attributeFilter: ["style"], attributes: true}) } } } protected firstUpdated(_changedProperties: PropertyValues): void { this.requestUpdate() } syncSize(clear=false) { if(this.baseEl && !clear) { this.style.width = this.baseEl.style.width this.style.height = this.baseEl.style.height } else if(this.baseEl && clear) { this.style.width = this.baseEl.style.width = null this.style.height = this.baseEl.style.height = null } } disconnectedCallback(): void { super.disconnectedCallback() this.observer?.disconnect() this.observer = undefined } get nextElementInOrder() { return Array.from(this?.parentElement?.children ?? []).find((el: HTMLElement) => { const elOrder = parseInt(getComputedStyle(el).order) return elOrder === this.elementIndex + 1 }) } get prevElementInOrder() { return Array.from(this?.parentElement?.children ?? []).find((el: HTMLElement) => { const elOrder = parseInt(getComputedStyle(el).order) return elOrder === this.elementIndex - 1 }) } get inCorrectPosition() { return this.elementIndex === this.validOrder } render() { const solution = this.validOrder !== undefined? html`${this.validOrder + 1}`: null return html`
    ${this.layout === "list" && this.showSolution ? solution: null}
  1. this.draggable = true}>
  1. = 10}>
  2. ${this.layout === "tiles" && this.showSolution? solution: null}
` } }