From 2907a160ce7ca6908b53576bac80c6a2b6fae381 Mon Sep 17 00:00:00 2001 From: Sally McGrath Date: Thu, 23 Jan 2025 12:22:16 +0000 Subject: [PATCH 1/6] bare bones labeller we should probably do some little components like this -- sorting and classifying are really powerful --- common-theme/assets/scripts/label-items.js | 93 +++++++++++++++++++ .../layouts/shortcodes/label-items.html | 47 ++++++++++ 2 files changed, 140 insertions(+) create mode 100644 common-theme/assets/scripts/label-items.js create mode 100644 common-theme/layouts/shortcodes/label-items.html diff --git a/common-theme/assets/scripts/label-items.js b/common-theme/assets/scripts/label-items.js new file mode 100644 index 000000000..c9098cbc1 --- /dev/null +++ b/common-theme/assets/scripts/label-items.js @@ -0,0 +1,93 @@ +//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(); + } + + connectedCallback() { + this.render(); + this.init(); + } + + render() { + this.shadowRoot.innerHTML = ` +
+ +

Drag the labels on to the items

+ +

+ +

+
+ `; + } + + 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) { + const labels = labelsContainer.querySelectorAll("[data-label]"); + + labels.forEach((label) => { + label.setAttribute("draggable", "true"); + label.addEventListener("dragstart", (e) => { + e.dataTransfer.setData("text/plain", label.dataset.label); + }); + }); + } + + setupContent(contentContainer) { + const items = contentContainer.querySelectorAll("[data-item]"); + + items.forEach((item) => { + if (item.dataset.correct) { + this.correctAnswers.set(item.dataset.item, item.dataset.correct); + } + + item.addEventListener("dragover", (e) => e.preventDefault()); + item.addEventListener("drop", (e) => { + e.preventDefault(); + const labelId = e.dataTransfer.getData("text/plain"); + this.handleDrop(labelId, item); + }); + }); + } + + handleDrop(labelId, itemElement) { + const itemId = itemElement.dataset.item; + const isCorrect = this.correctAnswers.get(itemId) === labelId; + + itemElement.dataset.status = isCorrect ? "correct" : "incorrect"; + itemElement.classList.toggle("is-good", isCorrect); + itemElement.classList.toggle("is-bad", !isCorrect); + + // Update feedback slot content + const feedbackSlot = this.shadowRoot.querySelector('slot[name="feedback"]'); + const feedback = feedbackSlot.assignedElements()[0]; + if (feedback) { + feedback.textContent = isCorrect ? "Sorted!" : "Try again"; + } + } +} + +customElements.define("label-items", LabelItems); diff --git a/common-theme/layouts/shortcodes/label-items.html b/common-theme/layouts/shortcodes/label-items.html new file mode 100644 index 000000000..02f0c0509 --- /dev/null +++ b/common-theme/layouts/shortcodes/label-items.html @@ -0,0 +1,47 @@ +{{/* 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 >}} +*/}} +{{/* 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 }} + + + +
+ {{ range (uniq $labels) }} + + {{ end }} +
+
+ {{ range $index, $item := $processedItems }} +
+ {{ .content | $.Page.RenderString }} +
+ {{ end }} +
+
+
+ +{{ $labelItems := resources.Get "scripts/label-items.js" | resources.Minify }} + From af75ba2de4463fc235bbdbde2769d6f4b9eba198 Mon Sep 17 00:00:00 2001 From: Sally McGrath Date: Thu, 23 Jan 2025 12:28:48 +0000 Subject: [PATCH 2/6] always forget it doesn't do that by default --- common-theme/assets/scripts/label-items.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common-theme/assets/scripts/label-items.js b/common-theme/assets/scripts/label-items.js index c9098cbc1..5690820c5 100644 --- a/common-theme/assets/scripts/label-items.js +++ b/common-theme/assets/scripts/label-items.js @@ -50,8 +50,10 @@ class LabelItems extends HTMLElement { 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"; }); }); } From ee048e4066caeefb6b6bd067bb024109f9bf3f58 Mon Sep 17 00:00:00 2001 From: Sally McGrath Date: Thu, 23 Jan 2025 12:31:28 +0000 Subject: [PATCH 3/6] is it even cyf if there's no emoji? --- common-theme/assets/scripts/label-items.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common-theme/assets/scripts/label-items.js b/common-theme/assets/scripts/label-items.js index 5690820c5..177a0cc7b 100644 --- a/common-theme/assets/scripts/label-items.js +++ b/common-theme/assets/scripts/label-items.js @@ -19,7 +19,7 @@ class LabelItems extends HTMLElement { this.shadowRoot.innerHTML = `
-

Drag the labels on to the items

+

👆🏾 Drag the labels on to the items 👇🏽

From 48d2bc68d654bfd84ba9ce69e9995dbb67a5feae Mon Sep 17 00:00:00 2001 From: Sally McGrath Date: Thu, 23 Jan 2025 15:48:45 +0000 Subject: [PATCH 4/6] add/rm labels on drop also allow a heading --- common-theme/assets/scripts/label-items.js | 19 ++++++++++++++++++- .../assets/styles/04-components/label.scss | 4 ++++ .../layouts/shortcodes/label-items.html | 4 +++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/common-theme/assets/scripts/label-items.js b/common-theme/assets/scripts/label-items.js index 177a0cc7b..8a496e632 100644 --- a/common-theme/assets/scripts/label-items.js +++ b/common-theme/assets/scripts/label-items.js @@ -17,9 +17,12 @@ class LabelItems extends HTMLElement { render() { this.shadowRoot.innerHTML = ` +
-

👆🏾 Drag the labels on to the items 👇🏽

+

@@ -75,6 +78,14 @@ class LabelItems extends HTMLElement { }); } + 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; @@ -83,6 +94,12 @@ class LabelItems extends HTMLElement { 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)); + // Update feedback slot content const feedbackSlot = this.shadowRoot.querySelector('slot[name="feedback"]'); const feedback = feedbackSlot.assignedElements()[0]; 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 index 02f0c0509..499a911b2 100644 --- a/common-theme/layouts/shortcodes/label-items.html +++ b/common-theme/layouts/shortcodes/label-items.html @@ -1,10 +1,11 @@ {{/* 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=" }} @@ -33,6 +34,7 @@ {{ end }}

+

{{ $heading }}

{{ range $index, $item := $processedItems }}
From 5f5a34d97830ccd38268d648d85376a4fdf77b30 Mon Sep 17 00:00:00 2001 From: Sally McGrath Date: Fri, 24 Jan 2025 13:43:23 +0000 Subject: [PATCH 5/6] add a load of feedback and therefore mini state tracker also pop it in a block --- common-theme/assets/scripts/label-items.js | 47 ++++++++++++++----- .../layouts/shortcodes/label-items.html | 37 ++++++++------- 2 files changed, 54 insertions(+), 30 deletions(-) diff --git a/common-theme/assets/scripts/label-items.js b/common-theme/assets/scripts/label-items.js index 8a496e632..009d014fb 100644 --- a/common-theme/assets/scripts/label-items.js +++ b/common-theme/assets/scripts/label-items.js @@ -8,6 +8,9 @@ class LabelItems extends HTMLElement { super(); this.attachShadow({ mode: "open" }); this.correctAnswers = new Map(); + this.playerAnswers = new Map(); + this.labels = []; + this.items = []; } connectedCallback() { @@ -49,9 +52,9 @@ class LabelItems extends HTMLElement { } setupLabels(labelsContainer) { - const labels = labelsContainer.querySelectorAll("[data-label]"); + this.labels = labelsContainer.querySelectorAll("[data-label]"); - labels.forEach((label) => { + this.labels.forEach((label) => { label.setAttribute("draggable", "true"); label.style.cursor = "grab"; label.addEventListener("dragstart", (e) => { @@ -62,12 +65,11 @@ class LabelItems extends HTMLElement { } setupContent(contentContainer) { - const items = contentContainer.querySelectorAll("[data-item]"); + this.items = contentContainer.querySelectorAll("[data-item]"); - items.forEach((item) => { - if (item.dataset.correct) { - this.correctAnswers.set(item.dataset.item, item.dataset.correct); - } + 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) => { @@ -90,7 +92,9 @@ class LabelItems extends HTMLElement { const itemId = itemElement.dataset.item; const isCorrect = this.correctAnswers.get(itemId) === labelId; - itemElement.dataset.status = isCorrect ? "correct" : "incorrect"; + 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); @@ -100,11 +104,28 @@ class LabelItems extends HTMLElement { // add this label itemElement.appendChild(this.makeLabel(labelId)); - // Update feedback slot content - const feedbackSlot = this.shadowRoot.querySelector('slot[name="feedback"]'); - const feedback = feedbackSlot.assignedElements()[0]; - if (feedback) { - feedback.textContent = isCorrect ? "Sorted!" : "Try again"; + 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`; } } } diff --git a/common-theme/layouts/shortcodes/label-items.html b/common-theme/layouts/shortcodes/label-items.html index 499a911b2..144f302e5 100644 --- a/common-theme/layouts/shortcodes/label-items.html +++ b/common-theme/layouts/shortcodes/label-items.html @@ -28,22 +28,25 @@ {{ end }} - -
- {{ range (uniq $labels) }} - - {{ end }} -
-

{{ $heading }}

-
- {{ range $index, $item := $processedItems }} -
- {{ .content | $.Page.RenderString }} -
- {{ end }} -
-
-
- +
+ +
+ {{ range (uniq $labels) }} + + {{ end }} +
+

{{ $heading }}

+
+ {{ range $index, $item := $processedItems }} +
+ {{ .content | $.Page.RenderString }} +
+ {{ end }} +
+
+
+
{{ $labelItems := resources.Get "scripts/label-items.js" | resources.Minify }} From f2fef94927daf6eb48fc7e765518b52932bc5be7 Mon Sep 17 00:00:00 2001 From: Sally McGrath Date: Fri, 24 Jan 2025 13:56:41 +0000 Subject: [PATCH 6/6] shuffle labels --- common-theme/layouts/shortcodes/label-items.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common-theme/layouts/shortcodes/label-items.html b/common-theme/layouts/shortcodes/label-items.html index 144f302e5..75b75bd2b 100644 --- a/common-theme/layouts/shortcodes/label-items.html +++ b/common-theme/layouts/shortcodes/label-items.html @@ -27,11 +27,13 @@ {{ end }} +{{ $labels = $labels | uniq | shuffle }} +
- {{ range (uniq $labels) }} + {{ range $labels }} {{ end }}