diff --git a/common-theme/assets/scripts/label-items.js b/common-theme/assets/scripts/label-items.js new file mode 100644 index 000000000..009d014fb --- /dev/null +++ b/common-theme/assets/scripts/label-items.js @@ -0,0 +1,133 @@ +//basically matches keys (labels) to values - can have multiple values on a key. +// this is for classifying activities +// sorting and classifying are fundamental learning strategies +// it's how we make sense of things! +// TODO : make the inverse where we group items into labelled buckets +class LabelItems extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this.correctAnswers = new Map(); + this.playerAnswers = new Map(); + this.labels = []; + this.items = []; + } + + connectedCallback() { + this.render(); + this.init(); + } + + render() { + this.shadowRoot.innerHTML = ` + +
+ + + +

+ +

+
+ `; + } + + init() { + const labelsSlot = this.shadowRoot.querySelector('slot[name="labels"]'); + const contentSlot = this.shadowRoot.querySelector('slot[name="content"]'); + + labelsSlot.addEventListener("slotchange", () => { + const labels = labelsSlot.assignedElements()[0]; + if (!labels) return; + this.setupLabels(labels); + }); + + contentSlot.addEventListener("slotchange", () => { + const content = contentSlot.assignedElements()[0]; + if (!content) return; + this.setupContent(content); + }); + } + + setupLabels(labelsContainer) { + this.labels = labelsContainer.querySelectorAll("[data-label]"); + + this.labels.forEach((label) => { + label.setAttribute("draggable", "true"); + label.style.cursor = "grab"; + label.addEventListener("dragstart", (e) => { + e.dataTransfer.setData("text/plain", label.dataset.label); + label.style.cursor = "grabbing"; + }); + }); + } + + setupContent(contentContainer) { + this.items = contentContainer.querySelectorAll("[data-item]"); + + this.items.forEach((item) => { + this.correctAnswers.set(item.dataset.item, item.dataset.correct); + this.playerAnswers.set(item.dataset.item, false); // we always start off wrong + + item.addEventListener("dragover", (e) => e.preventDefault()); + item.addEventListener("drop", (e) => { + e.preventDefault(); + const labelId = e.dataTransfer.getData("text/plain"); + this.handleDrop(labelId, item); + }); + }); + } + + makeLabel(labelId) { + // make a label + const label = document.createElement("span"); + label.textContent = labelId; + label.classList.add("c-label"); + return label; + } + + handleDrop(labelId, itemElement) { + const itemId = itemElement.dataset.item; + const isCorrect = this.correctAnswers.get(itemId) === labelId; + + this.playerAnswers.set(itemId, isCorrect ? true : false); + + itemElement.dataset.status = isCorrect ? true : false; + itemElement.classList.toggle("is-good", isCorrect); + itemElement.classList.toggle("is-bad", !isCorrect); + + // remove old labels + itemElement.querySelectorAll(".c-label").forEach((label) => label.remove()); + + // add this label + itemElement.appendChild(this.makeLabel(labelId)); + + this.updateFeedback(isCorrect); + } + + updateFeedback(isCorrect) { + const feedback = this.shadowRoot + .querySelector('slot[name="feedback"]') + .assignedElements()[0]; + if (!feedback) return; + + const correctCount = [...this.playerAnswers.values()].filter( + (state) => state + ).length; + const totalItems = this.items.length; + + if (correctCount === totalItems) { + feedback.textContent = "🎉 You've sorted them all!"; + } else { + feedback.textContent = `${ + isCorrect + ? "✅ You got that one right! " + : "❌ You got that one wrong. " + } ${correctCount} of ${totalItems} labelled correctly`; + } + } +} + +customElements.define("label-items", LabelItems); diff --git a/common-theme/assets/styles/04-components/label.scss b/common-theme/assets/styles/04-components/label.scss index 428ffe285..83c98befd 100644 --- a/common-theme/assets/styles/04-components/label.scss +++ b/common-theme/assets/styles/04-components/label.scss @@ -4,6 +4,10 @@ font: 600 var(--theme-type-size--6) system-ui; border: 1px solid; border-radius: 1em; + + &:not(:has(.c-label__name)) { + padding: 0.125em 0.5em; + } &__name { display: inline-block; padding: 0.125em 0.5em; diff --git a/common-theme/layouts/shortcodes/label-items.html b/common-theme/layouts/shortcodes/label-items.html new file mode 100644 index 000000000..75b75bd2b --- /dev/null +++ b/common-theme/layouts/shortcodes/label-items.html @@ -0,0 +1,54 @@ +{{/* example usage: attach a label to delimit each item + {{}} +[LABEL=Label-1]Item here [LABEL=Label-2]Item here anything you like can be +multiple lines [LABEL=Label-1] Another item here +{{< /label-items >}} +*/}} +{{ $heading := .Get "heading" | default "👆🏾 Drag the labels on to the items 👇🏽" }} +{{/* Split content into items by label, using a similar pattern to TABS and COLUMNS */}} +{{ $content := .Inner | strings.TrimSpace }} +{{ $items := split $content "[LABEL=" }} + +{{ $labels := slice }} +{{/* Need to clean up the brackets and stuff */}} +{{ $processedItems := slice }} + +{{ range $index, $item := $items }} + {{ if $labelEnd := index (findRE `^([^\]]+)\]` $item) 0 }} + {{ $label := strings.TrimSuffix "]" $labelEnd }} + {{ $labels = $labels | append $label }} + + {{ $itemContent := strings.TrimPrefix $labelEnd $item | strings.TrimSpace }} + {{ if $itemContent }} + {{ $processedItems = $processedItems | append (dict "label" $label "content" $itemContent) }} + {{ end }} + {{ end }} + +{{ end }} + +{{ $labels = $labels | uniq | shuffle }} + + +
+ +
+ {{ range $labels }} + + {{ end }} +
+

{{ $heading }}

+
+ {{ range $index, $item := $processedItems }} +
+ {{ .content | $.Page.RenderString }} +
+ {{ end }} +
+
+
+
+{{ $labelItems := resources.Get "scripts/label-items.js" | resources.Minify }} +