Good place to start. I'd keep the first JavaScript layer very small:

### 1. Connect JavaScript with the `script` tag

Place an external script near the end of the HTML `body`:

```html
<script src="./app.js"></script>
```

The `src` attribute gives the path to the JavaScript file. Placing the tag
after the page content lets those HTML elements exist before the script tries
to select them.

### 2. Variables, numbers, strings, and `console.log`

Use `let` when the value needs to change:

```javascript
let score = 0;

score = 10;
```

And learn `const` alongside it for values you don't reassign:

```javascript
const name = "Trent";
```

A useful rule: **use `const` by default; use `let` when reassignment is needed.** Avoid old-style `var` for now.

Numbers do not use quotation marks. Strings are text surrounded by quotation
marks:

```javascript
const quantity = 3;
const product = "notebook";
```

Use `console.log()` to inspect a value in the browser's developer console:

```javascript
console.log(quantity);
console.log(product);
```

The `+` operator adds numbers, but concatenates when a string is involved:

```javascript
console.log(2 + 3);                    // 5
console.log("2" + 3);                  // "23"
console.log("Quantity: " + quantity);  // "Quantity: 3"
```

JavaScript evaluates these operations from left to right:

```javascript
console.log("Total: " + 2 + 3);   // "Total: 23"
console.log("Total: " + (2 + 3)); // "Total: 5"
```

Use parentheses when numbers must be added before they are joined to a string.

### 3. Functions

Functions package reusable behavior:

```javascript
function greet(name) {
    return "Hello " + name;
}

const message = greet("Trent");
```

Understand these four pieces first:

**parameters → arguments → function body → return value**

### 4. Basic DOM selection

By ID:

```javascript
const title = document.getElementById("title");
```

By class:

```javascript
const cards = document.getElementsByClassName("card");
```

Important difference:

```javascript
getElementById()
```

returns **one element** (or `null`).

```javascript
getElementsByClassName()
```

returns an **HTMLCollection of potentially many elements**.

Notice it's `getElementsByClassName` — **Elements** is plural.

### 5. Change DOM content

```javascript
const title = document.getElementById("title");

title.textContent = "New title";
```

The property is spelled **`innerText`**, not `innerTxt`. It differs from
`textContent`:

- `textContent` reads all text in the element's DOM, including hidden text.
- `innerText` reads text as it is visibly rendered. It respects CSS and may
  calculate layout.

For example:

```html
<div id="message">Visible <span style="display: none;">secret</span></div>
```

```javascript
const message = document.getElementById("message");

console.log(message.textContent); // "Visible secret"
console.log(message.innerText);   // "Visible"
```

Rendered block elements also produce line breaks with `innerText`:

```html
<div id="items"><div>One</div><div>Two</div></div>
```

```javascript
const items = document.getElementById("items");

console.log(items.textContent); // "OneTwo"
console.log(items.innerText);   // "One\nTwo"
```

Use `textContent` for predictable text reading and updates. Use `innerText`
when you specifically need the text a user can see.

Or styling:

```javascript
title.style.color = "red";
```

### 6. Events

This should come almost immediately afterward:

```javascript
const button = document.getElementById("myButton");

button.addEventListener("click", function () {
    console.log("Clicked!");
});
```

That gives you your first complete mental model:

**Find an HTML element → listen for something → run a function → change something.**

That's probably enough JavaScript to focus on initially before adding arrays, objects, loops, `querySelector`, etc.

## Phase 2

Phase 2 adds collections, simple structured data, decisions, repetition, and
selected methods from the built-in `Math` object.

### 7. Arrays

An array stores an ordered list of values. Its first value is at index `0`:

```javascript
const teams = ["Lions", "Hawks", "Bulls"];

console.log(teams[0]);    // "Lions"
console.log(teams.length); // 3
```

Use these methods to change either end of an array:

```javascript
const players = ["Ava", "Noah"];

players.push("Mia");     // add to the end
players.pop();           // remove from the end
players.unshift("Leo"); // add to the beginning
players.shift();         // remove from the beginning
```

`push()` and `unshift()` return the new array length. `pop()` and `shift()`
return the removed value, or `undefined` when the array is empty.

### 8. Basic objects

An object groups related values under property names:

```javascript
const player = {
  name: "Jordan",
  team: "Comets",
  points: 18,
  isStarter: true
};

console.log(player.name);
console.log(player.points);
console.log(player["team"]);
```

Use dot notation for ordinary property names. Bracket notation is useful when
the property name comes from another value. Phase 2 uses data properties only;
object functions (methods) come later.

### 9. Booleans and decisions

A boolean is either `true` or `false`:

```javascript
const isMember = true;
const isBlocked = false;
```

Use `if`, `else if`, and `else` to choose which code runs:

```javascript
const score = 82;

if (score >= 90) {
  console.log("Excellent");
} else if (score >= 60) {
  console.log("Passed");
} else {
  console.log("Try again");
}
```

### 10. Comparison and logical operators

Comparisons produce boolean values:

| Operator | Meaning |
| --- | --- |
| `===` | equal value and equal type |
| `!==` | different value or different type |
| `>` | greater than |
| `<` | less than |
| `>=` | greater than or equal to |
| `<=` | less than or equal to |

Prefer strict equality (`===` and `!==`) so JavaScript does not silently
convert values before comparing them.

Logical operators combine or reverse boolean expressions:

```javascript
const age = 20;
const hasTicket = true;
const hasPass = false;

const canEnter = age >= 18 && hasTicket;       // AND: both must be true
const hasAccess = hasTicket || hasPass;        // OR: either may be true
const needsTicket = !hasTicket;                // NOT: reverses the boolean
```

### 11. `for` loops

A `for` loop repeats while its condition remains true:

```javascript
const scores = [12, 18, 15];
let total = 0;

for (let index = 0; index < scores.length; index = index + 1) {
  total = total + scores[index];
}

console.log(total); // 45
```

The loop starts at index `0`, continues while the index is less than the array
length, and increases the index after each pass.

### 12. The `Math` object

The built-in `Math` object provides number utilities:

```javascript
console.log(Math.floor(4.8)); // 4: rounds down
console.log(Math.ceil(4.2));  // 5: rounds up
console.log(Math.random());   // random number from 0 up to, but not including, 1
```

The method is named `Math.ceil()`, not `Math.ceiling()`.

To generate a random whole number from `1` through `6`:

```javascript
const roll = Math.floor(Math.random() * 6) + 1;
```

### 13. Return statements

`return` sends a value back to the code that called a function and immediately
ends that function call:

```javascript
function canPlay(age, hasPermission) {
  if (age >= 18 || hasPermission) {
    return true;
  }

  return false;
}

const allowed = canPlay(16, true);
```

Keep calculation and decision functions pure when practical: use their
parameters, return a result, and leave DOM updates to separate code.

## Phase 3

Phase 3 adds richer DOM work, form selects, template strings, objects in arrays,
JSON, and browser storage.

### 14. Reading and writing form control values

Form controls store their current choice in the `value` property:

```javascript
const nameInput = document.getElementById("name");

console.log(nameInput.value);
nameInput.value = "Jordan";
```

Use `value` for `input` and `select` controls. Use `textContent` or
`innerHTML` for ordinary elements.

### 15. The `select` element

A `select` offers a fixed list of choices with nested `option` elements:

```html
<label for="focus">Focus area</label>
<select id="focus" name="focus">
  <option value="">Choose a focus</option>
  <option value="Shooting">Shooting</option>
  <option value="Defense">Defense</option>
</select>
```

Read and write the selected option with `value`. Use `selectedIndex` when you
need the position of the choice. Use `options` to inspect every option:

```javascript
const focusSelect = document.getElementById("focus");

console.log(focusSelect.value);
console.log(focusSelect.selectedIndex);
console.log(focusSelect.options.length);

focusSelect.value = "Defense";
focusSelect.selectedIndex = 0;
```

You can also build options in JavaScript:

```javascript
const option = document.createElement("option");
option.value = "Conditioning";
option.textContent = "Conditioning";
focusSelect.appendChild(option);
```

Prefer a `select` when the allowed values are known in advance. Prefer a text
`input` when the user may type any value.

### 16. `innerHTML` versus `textContent`

`textContent` sets or reads plain text. `innerHTML` sets or reads HTML markup:

```javascript
const panel = document.getElementById("panel");

panel.textContent = "Safe plain text";
panel.innerHTML = "<strong>Trusted</strong> markup";
```

Prefer `textContent` when you only need text. Use `innerHTML` only for markup
you control. Do not insert untrusted user text as HTML.

### 17. Function parameters

Parameters receive values when a function is called:

```javascript
function makeLabel(title, count) {
  return title + " (" + count + ")";
}

const label = makeLabel("Notes", 3);
```

Name parameters clearly. Pass arguments in the same order the parameters are
declared.

### 18. Template strings

Template strings use backticks and `${}` to insert values:

```javascript
const name = "Ava";
const score = 18;

const summary = `${name} scored ${score} points.`;
```

They keep string building readable when you combine several values.

### 19. More DOM selection

Phase 1 introduced `getElementById` and `getElementsByClassName`. Phase 3 adds
tag and CSS-selector methods:

```javascript
const title = document.getElementById("title");
const notes = document.getElementsByClassName("note");
const paragraphs = document.getElementsByTagName("p");
const firstCard = document.querySelector(".card");
const allCards = document.querySelectorAll(".card");
```

| Method | Result |
| --- | --- |
| `getElementById("title")` | One element, or `null` |
| `getElementsByClassName("note")` | Live HTMLCollection of matching classes |
| `getElementsByTagName("p")` | Live HTMLCollection of every `p` |
| `querySelector(".card")` | First matching element, or `null` |
| `querySelectorAll(".card")` | Static NodeList of all matches |

`getElementById` and `getElementsByClassName` take plain names with no `#` or
`.`. `querySelector` and `querySelectorAll` accept CSS selectors such as `#id`,
`.class`, and `section p`.

When teaching selectors, you may display sample markup with `pre` and `code` so
learners can compare the HTML source with live matches.
### 20. Creating and removing elements

Build new nodes with `createElement()`, insert them with `append()` or
`prepend()`, and take them off the page with `remove()`:

```javascript
const list = document.getElementById("items");
const item = document.createElement("li");

item.textContent = "Practice drills";

list.append(item);   // add as the last child
// list.prepend(item); // add as the first child instead

item.remove();
```

- `createElement("li")` builds a new element that is not on the page yet.
- `append()` inserts at the **end** of the parent’s children.
- `prepend()` inserts at the **start** of the parent’s children.
- `remove()` takes that element off the page.

`appendChild()` still works and returns the inserted node; prefer `append()` /
`prepend()` in new code — they also accept text strings and multiple arguments.

**Performance rule:** keep DOM writes outside the loop. Touching the live page on
every pass (reflow/repaint) is slower than building work in memory, then
updating the DOM once.

With `innerHTML`, build the markup string in the loop, then assign once:

```javascript
const list = document.getElementById("items");
let html = "";

for (let index = 0; index < players.length; index = index + 1) {
  html += `<li>${players[index].name}</li>`;
}

list.innerHTML = html;
```

Avoid `innerHTML +=` inside the loop: each `+=` reads the current HTML, rebuilds
it, and writes it back.

With `createElement`, build nodes off the page (or on a
`DocumentFragment`), then append once:

```javascript
const list = document.getElementById("items");
const fragment = document.createDocumentFragment();

for (let index = 0; index < players.length; index = index + 1) {
  const item = document.createElement("li");
  item.textContent = players[index].name;
  fragment.append(item);
}

list.append(fragment);
```

`DocumentFragment` is a lightweight container that is not in the document. One
`append(fragment)` moves all of its children onto the page in a single update.

### 21. Objects in arrays

Arrays can store objects as items:

```javascript
const roster = [
  { name: "Jordan", points: 18 },
  { name: "Ava", points: 22 }
];

console.log(roster[0].name);
console.log(roster[1].points);
```

Loop through the array to read each object and update the page.

### 22. Basic JSON

JSON is a text format for data. Convert between JavaScript values and JSON
strings with:

```javascript
const players = [
  { name: "Jordan", points: 18 },
  { name: "Ava", points: 22 }
];

const asText = JSON.stringify(players);
const asValue = JSON.parse(asText);
```

`JSON.stringify()` turns a value into a string. `JSON.parse()` turns a JSON
string back into a value.

### 23. `localStorage`

`localStorage` saves key/value strings in the browser:

```javascript
const notes = [
  { title: "Warm-up", body: "Stretch first" }
];

localStorage.setItem("notes", JSON.stringify(notes));

const savedText = localStorage.getItem("notes");
const savedNotes = JSON.parse(savedText);

localStorage.removeItem("notes");
```

Always store strings. Use `JSON.stringify()` before saving arrays or objects,
and `JSON.parse()` after loading them. Check for `null` when nothing is saved
yet.

## Phase 4

Phase 4 teaches changing how elements look and reading or writing HTML
attributes: inline styles, CSS classes, generic attributes, and `data-*`
values.

### 24. The `.style` property

`.style` sets one CSS property at a time on an element. Property names use
camelCase in JavaScript (`backgroundColor`), not CSS kebab-case
(`background-color`):

```javascript
const preview = document.getElementById("preview");

preview.style.color = "#0f766e";
preview.style.fontSize = "28px";
preview.style.backgroundColor = "#ecfeff";
```

These writes become **inline** styles on the element. Prefer `.style` for a
single quick change. Prefer CSS classes when several properties should change
together.

### 25. `classList`

`classList` adds, removes, or checks CSS class names without rewriting the
whole `class` attribute:

```javascript
const card = document.getElementById("player-card");

card.classList.add("is-starter");
card.classList.remove("is-bench");
card.classList.toggle("is-highlighted");

const isStarter = card.classList.contains("is-starter");
```

- `add()` puts a class on the element (no duplicate if it is already there).
- `remove()` takes a class off.
- `toggle()` adds the class when missing and removes it when present.
- `contains()` returns `true` or `false`.

Keep visual themes in CSS. Use JavaScript only to flip class names.

### 26. `getAttribute()` and `setAttribute()`

Use these methods to read or write any HTML attribute by name:

```javascript
const link = document.getElementById("court-link");
const tipoff = document.getElementById("tipoff");

console.log(link.getAttribute("href"));
link.setAttribute("href", "https://example.com");
link.setAttribute("target", "_blank");

tipoff.setAttribute("disabled", "disabled");
tipoff.removeAttribute("disabled");
```

`getAttribute()` returns a string, or `null` when the attribute is missing.
Use attributes for generic HTML attrs such as `href`, `alt`, and `title`. Prefer
`classList` for classes and `dataset` for `data-*` values.

### 27. `dataset`

Custom `data-*` attributes map to the `dataset` object. Kebab-case names become
camelCase properties (`data-jersey-number` → `dataset.jerseyNumber`):

```html
<article
  class="roster-card"
  data-name="Ava"
  data-position="Guard"
  data-points="22"
  data-jersey-number="7"
></article>
```

```javascript
const card = document.querySelector(".roster-card");

console.log(card.dataset.name);          // "Ava"
console.log(card.dataset.position);      // "Guard"
console.log(card.dataset.points);        // "22"
console.log(card.dataset.jerseyNumber);  // "7"

card.dataset.points = "24";
```

All `dataset` values are strings. `getAttribute("data-points")` reads the same
value; `dataset` is the shorter form for `data-*` work.

## Phase 5

Phase 5 deepens events: listening for more than one event type, reading which
element was involved, and stopping the browser’s default action.

### 28. `addEventListener()` (deeper)

Phase 1 introduced `addEventListener` for clicks. The same method works for many
event types. The first argument is the event name as a string; the second is the
function to run:

```javascript
const tipoff = document.getElementById("tipoff");
const nameInput = document.getElementById("player-name");

tipoff.addEventListener("click", function () {
  console.log("Tip off clicked");
});

nameInput.addEventListener("input", function () {
  console.log("Typing: " + nameInput.value);
});
```

Common types for this phase: `"click"`, `"input"`, `"submit"`, `"mouseover"`.
You can attach more than one listener to the same element.

### 29. `event.target`

The handler receives an **event object**. `event.target` is the element that
actually triggered the event (useful when one listener sits on a parent and
several children can be clicked):

```javascript
const board = document.getElementById("play-board");

board.addEventListener("click", function (event) {
  console.log(event.target);
  console.log(event.target.textContent);
});
```

`event.currentTarget` is the element that has the listener (`board` above).
`event.target` may be a child inside that element.

For delegation, prefer `event.target.closest(selector)` so a click on nested
markup still finds the intended control or row (see also §33):

```javascript
expenseBody.addEventListener("click", function (event) {
  const btn = event.target.closest("button[data-id]");
  if (!btn) return;
  // handle the row action using btn.dataset.id
});
```

Bare `event.target` breaks when the user clicks a child (an icon `span`, text
node wrapper, or nested element) instead of the element you expected. Lab:
`phase5/06-delegation-closest.html` compares bare `dataset.id` with
`closest("[data-id]")` side by side.

### 30. `preventDefault()`

Some elements have built-in browser behavior: a form `submit` reloads the page;
a link navigates away. Call `event.preventDefault()` to keep the page in place
and handle the action in JavaScript:

```javascript
const form = document.getElementById("signup-form");
const link = document.getElementById("leave-court");

form.addEventListener("submit", function (event) {
  event.preventDefault();
  console.log("Form stayed on the page");
});

link.addEventListener("click", function (event) {
  event.preventDefault();
  console.log("Link did not navigate");
});
```

Use `preventDefault()` when you need to validate input, save data, or update the
DOM without a full page reload.

**Required for desks, web apps, games, and `projects/`:** whenever JavaScript owns
the outcome, cancel the browser default first.

| Situation | Call `preventDefault()` on… |
| --- | --- |
| In-page form (save, filter, wizard, desk, project app) | `"submit"` — every time, even if there is no submit button (Enter in a field can still submit) |
| Control styled as a link (`href="#"` / `javascript:`) | `"click"` |
| Drag-and-drop file zone | `"dragover"` and `"drop"` (see §39) |
| Game / app keyboard that would scroll or activate the page (Space, arrows, Tab traps you handle yourself) | `"keydown"` / `"keyup"` for those keys |

Also prefer `type="button"` on non-submit controls inside a `<form>` so a stray
Enter does not trigger navigation. The Phase 5 lab
`03-prevent-default` is the only place that intentionally omits
`preventDefault()` so you can compare both paths.

Projects under `projects/` (CRM, Expense Dashboard, Job Tracker, Trip Planner,
Draft Desk, Post Board) must keep `event.preventDefault()` on every form
`submit` handler and on any fake links or drag handlers they add.

## Phase 6

Phase 6 teaches walking the DOM tree: up to a parent, down to children, up to
the nearest matching ancestor, and sideways to the next sibling element.
Building and removing nodes (`createElement`, `append`, `prepend`, `remove`)
was covered in Phase 3 §20 — use those tools when you need to change the tree
you are walking.

### 31. `parentElement`

`parentElement` is the element one level up from the current element:

```javascript
const badge = document.getElementById("jersey-badge");
const card = badge.parentElement;

console.log(card.id);
card.classList.add("is-selected");
```

If there is no parent element, the value is `null`.

### 32. `children`

`children` is a live list of **element** children only (not text nodes):

```javascript
const roster = document.getElementById("roster");
const kids = roster.children;

console.log(kids.length);
console.log(kids[0].textContent);
```

Loop with a normal `for` loop. Prefer `children` when you only care about
elements, not every node inside the parent.

### 33. `closest()`

`closest()` walks **up** from an element (including itself) and returns the
nearest ancestor that matches a CSS selector, or `null`:

```javascript
const button = document.getElementById("remove-player");
const card = button.closest(".player-card");

if (card !== null) {
  card.classList.add("is-selected");
}
```

Useful when a click lands on a nested control and you need the wrapping card or
row.

### 34. `nextElementSibling`

`nextElementSibling` is the next **element** sibling (skips text and comments).
`previousElementSibling` goes the other way:

```javascript
const first = document.getElementById("player-a");
const second = first.nextElementSibling;

if (second !== null) {
  second.classList.add("is-highlighted");
}
```

If there is no next element sibling, the value is `null`.

## Phase 7

Phase 7 puts forms to work in the browser: live output, the Constraint
Validation API, field presentation toggles, file lists, drag-and-drop, multi-step
wizards, and a settings-style capstone. It builds on Phase 3 form values, Phase 4
`classList`, and Phase 5 `preventDefault()`.

### 35. Live `input` events and `output`

The `input` event fires as the user changes a control. Pair a range (or other
control) with an `output` element and update its text:

```javascript
const range = document.getElementById("confidence");
const output = document.getElementById("confidence-out");

range.addEventListener("input", function () {
  output.textContent = range.value + " out of 10";
});
```

### 36. Constraint Validation API

Native constraints (`required`, `type`, `min`, `max`, `minlength`) expose a
ValidityState. Common methods:

```javascript
form.checkValidity();   // true/false, no UI
form.reportValidity();  // shows browser messages when invalid
input.setCustomValidity("Enter a usable email.");
input.setCustomValidity(""); // clear custom error
```

Listen for `submit`, call `preventDefault()`, refresh custom messages, then
decide whether to continue.

**Postel's Law (be liberal in what you accept):** before failing a field, trim
whitespace, normalise casing where safe (for example emails), and tolerate
harmless formatting differences. Reserve hard errors for values that still cannot
work after that cleanup.

### 37. Field presentation with `classList` and `disabled`

Toggle success or error chrome with classes, and disable a control when needed:

```javascript
field.classList.add("is-bad");
field.classList.remove("is-ok");
input.disabled = true;
```

Keep a text message beside the field; do not rely on colour alone.

### 38. Reading `input.files`

A file input exposes a `FileList` on `files`. Each `File` has `name` and `size`.
Listing files locally does not upload them:

```javascript
const files = input.files;
for (let i = 0; i < files.length; i++) {
  console.log(files[i].name, files[i].size);
}
```

### 39. Drag and drop files

Call `preventDefault()` on `dragover` and `drop`. Read files from
`event.dataTransfer.files`:

```javascript
zone.addEventListener("dragover", function (event) {
  event.preventDefault();
});

zone.addEventListener("drop", function (event) {
  event.preventDefault();
  const files = event.dataTransfer.files;
});
```

Always provide a **pointer alternative** to drag-and-drop (WCAG 2.2 Dragging
Movements): a native file input, button, or other single-pointer control that
can choose the same files without dragging. The Phase 7 dropzone lab pairs the
zone with `<input type="file">` for that reason.

### 40. Multi-step form state

Keep a step index, show one panel at a time (`hidden`), update progress UI, and
validate only the current step's fields before advancing. Finish with a final
`submit` handler that still calls `preventDefault()` for an in-page save.

**Doherty threshold:** aim to respond within **≤ 400 ms**. If work will take
longer (network, heavy validation, multi-file reads), show progress immediately —
a step indicator, `role="status"` message, or disabled-with-busy control — so the
UI never feels frozen. Visible progress also satisfies Goal-Gradient / Zeigarnik
(people finish flows they can see advancing).

## Phase 8

Phase 8 introduces **third-party form UI libraries** after you can handle native
forms (Phase 7). Labs pin CDN builds from jsDelivr. Prefer enhancing a real
`<select>`, `<input>`, or `<textarea>` (or syncing values into one). Destroy
instances before re-init. Dropzone labs never upload to a network. TinyMCE uses
the self-hosted CDN build with `license_key: "gpl"` (not Tiny Cloud).

### 41. Pinned CDN load and destroy

Load CSS and the library script **before** your lesson script. Pin versions in
the URL. Keep a variable for the instance and call `destroy()` before creating
another on the same element:

```javascript
if (choicesInstance) {
  choicesInstance.destroy();
  choicesInstance = null;
}
choicesInstance = new Choices(select, { shouldSort: false });
```

Picmo lessons use `type="module"` and jsDelivr `+esm` imports.

### 42. Choices.js

Enhance a native select. Common options: `searchEnabled`, `removeItemButton`,
`maxItemCount`, `placeholderValue`. Read with `getValue(true)`; set with
`setChoiceByValue`. Listen on the original select for `addItem`, `removeItem`,
and `choice`.

### 43. Flatpickr

Attach to a text input. Useful options: `altInput` / `altFormat` / `dateFormat`,
`enableTime`, `noCalendar`, `mode: "range"` or `"multiple"`, `minDate`,
`maxDate`, `disable`, and `inline`.

### 44. TinyMCE (self-hosted)

```javascript
tinymce.init({
  selector: "#notes",
  license_key: "gpl",
  base_url: "https://cdn.jsdelivr.net/npm/tinymce@7.6.1",
  suffix: ".min",
});
```

Use `getContent` / `setContent`, `editor.mode.set("readonly"|"design")`, and
`editor.save()` or `tinymce.triggerSave()` so the underlying textarea stays in
sync for form data. Treat editor HTML as trusted demo content; prefer
`textContent` for summaries elsewhere.

For blogs and articles, enable a wider plugin set (`advlist`, `lists`, `link`,
`image`, `media`, `table`, `codesample`, `fullscreen`, `preview`, `wordcount`,
and related tools) plus a wrapping toolbar with `blocks`, alignment, lists, and
media. Keep `automatic_uploads: false` in workbook labs so images use URLs only.

### 45. Dropzone.js (local queue only)

```javascript
Dropzone.autoDiscover = false;
const zone = new Dropzone("#dz-form", {
  url: "/no-upload",
  autoProcessQueue: false,
  acceptedFiles: "image/*",
  maxFiles: 2,
  maxFilesize: 1,
});
```

Handle `addedfile`, `removedfile`, and `error`. Do not process a real upload in
these labs.

### 46. Picmo

Picmo is published as an ES module. Prefer a normal script tag (works with
`file://`) and load it with dynamic `import()` from the CDN:

```javascript
const { createPicker } = await import(
  "https://cdn.jsdelivr.net/npm/picmo@5.8.5/+esm"
);

const picker = createPicker({ rootElement: panel, showVariants: false });
picker.addEventListener("emoji:select", function (selection) {
  preview.textContent = selection.emoji;
});
```

Do not use `<script type="module" src="./local.js">` for these labs — browsers
block local module scripts under the `file://` protocol.

Listen for `emoji:select`. To insert into a field, splice `selection.emoji` at
`selectionStart` / `selectionEnd`.

For a popup UX, mount the picker in a panel and toggle the `hidden` attribute.
Prefer that pattern in this workbook over `@picmo/popup-picker` — the popup
package’s CDN builds are unreliable as local ESM dependencies, and
`triggerElement` does not open the popup by itself.

### 47. Vue 3 and React equivalents

Vanilla labs stay the teaching surface. When you move to SPA stacks, these are
the usual counterparts (wrappers still need mount/destroy lifecycle, same idea
as §41):

| Vanilla (this phase) | Vue 3 | React |
| --- | --- | --- |
| Choices.js | `@vueform/multiselect` or `vue-multiselect`; or Choices in `onMounted` / `onBeforeUnmount` | `react-select`; or Choices in `useEffect` cleanup |
| Flatpickr | `vue-flatpickr-component` | `react-flatpickr` |
| TinyMCE | `@tinymce/tinymce-vue` | `@tinymce/tinymce-react` |
| Dropzone.js | Dropzone wrapper, or `vue3-dropzone` / FilePond (`@pqina/vue-filepond`) | `react-dropzone` (different API) or Dropzone on a ref |
| Picmo | No official package — mount in `onMounted`; alt: `emoji-mart-vue-fast` | Mount in `useEffect`; alts: `emoji-picker-react`, `emoji-mart` |

## Phase 9 — Interactive Tables

Phase 9 adds search, sort, and pagination to existing HTML tables with
**List.js**, then vanilla bulk select inspired by admin product lists. CSS
Chapters 19–20 supply the static chrome; this phase wires behaviour. Prefer
enhancing markup that already has captions, headers, and scoped cells.

### 48. List.js on existing tables

Pin List.js 2.3.1 from jsDelivr and load it before your lesson script:

```html
<script src="https://cdn.jsdelivr.net/npm/list.js@2.3.1/dist/list.min.js"></script>
```

Mark sortable or searchable cells with classes that match `valueNames`. Put
`class="list"` on the `tbody`. Headers use `class="sort"` and `data-sort`:

```javascript
const peopleList = new List("people-list", {
  valueNames: ["name", "email", "age"],
  page: 5,
  pagination: true,
});
```

An input with `class="search"` filters automatically. Call `show(i, page)` for
prev/next paging. Listen with `list.on("updated", handler)`.

For numeric columns stored as display text (`$39`), sort via an attribute:

```javascript
valueNames: [{ name: "price", attr: "data-price" }]
```

### 49. Bulk select without a framework

Use a master checkbox, row checkboxes with `data-row` JSON (or ids), and toggle
a toolbar between a default action (New) and bulk actions. Collect selections
with `Array.from(checks).filter(...).map(...)`. Keep apply actions demo-safe
(log or status text; do not hard-delete rows in the workbook).

### 50. Vue 3 and React table equivalents

List.js mutates existing DOM rows. In SPA stacks, prefer a headless table engine
and render rows yourself:

| Vanilla (this phase) | Vue 3 | React |
| --- | --- | --- |
| List.js | `@tanstack/vue-table` | `@tanstack/react-table` |
| Bulk select (vanilla) | Row selection feature in TanStack Table | Same (`rowSelection` state) |

TanStack Table does not ship UI. You keep the semantic `<table>` markup and
styles from the CSS workbook.

## Phase 10 — Runtime and Language

Phase 10 steps back from DOM libraries into how JavaScript schedules work and
how newer language features compose. Labs stay interactive browser pages with
an Important JavaScript source panel. The import/export lesson needs a local
HTTP server because browsers block ES modules under `file://`.

### 51. `setTimeout` and `setInterval`

Schedule work for later:

```javascript
const later = setTimeout(function () {
  status.textContent = "Done.";
}, 1500);

clearTimeout(later);

const id = setInterval(function () {
  ticks = ticks + 1;
}, 1000);

clearInterval(id);
```

`setTimeout` runs once after the delay. `setInterval` repeats until you clear
it. Both return an id you pass to `clearTimeout` / `clearInterval`.

### 52. Call stack, Web APIs, task queue, and the event loop

Synchronous code runs on the **call stack**. Browser timers are handed to
**Web APIs**. When a timer finishes, its callback joins the **task queue**. The
**event loop** moves the next queued callback onto the stack only after the
stack is empty:

```javascript
console.log("A");
setTimeout(function () {
  console.log("B");
}, 0);
console.log("C");
// Prints: A, C, then B
```

Promises use a different (microtask) queue; this phase focuses on timer tasks.

### 53. Hoisting and the Temporal Dead Zone

Function declarations are fully hoisted, so you can call them above their line
in the same scope. `let` and `const` are also known early, but they stay in the
**Temporal Dead Zone** until their declaration runs — reading them first throws
a `ReferenceError`.

```javascript
greet();
function greet() {
  return "Hello";
}

console.log(score); // ReferenceError
let score = 10;
```

### 54. Object destructuring

Unpack named properties into variables in one step. Rename with `:` and supply
defaults with `=`:

```javascript
const player = { name: "Jordan", number: 23 };
const { name, number: jersey, team = "Free agent" } = player;
```

### 55. `Date` and `Error`

```javascript
const now = new Date();
now.getFullYear();
now.toISOString();

const stamped = new Date("2026-09-11T09:00:00");

try {
  if (!text) {
    throw new Error("Note is required.");
  }
} catch (error) {
  status.textContent = error.message;
}
```

`new Date()` without arguments is “now”. Invalid parse results are `Invalid Date`
(check with `Number.isNaN(date.getTime())`). `Error` carries a `.message` and
`.name` you can show after `catch`.

### 56. Ternary operator

Choose one of two values inline:

```javascript
const label = score >= 60 ? "Pass" : "Retry";
```

Use ternaries for short labels or class names. Prefer `if` / `else` when there
are several branches.

### 57. `Array.filter`

`filter` returns a **new** array of items that pass a test. The original array
is unchanged:

```javascript
const kept = players.filter(function (player) {
  return player.score >= minScore;
});
```

### 58. Function parameters (defaults, rest, destructuring)

Phase 3 §17 covered basic parameters and arguments. Phase 10 adds:

```javascript
function greet(name = "friend") {
  return "Hello, " + name;
}

function sumScores(...scores) {
  let total = 0;
  scores.forEach(function (n) {
    total = total + n;
  });
  return total;
}

function makeLabel({ title, count = 0 }) {
  return title + " (" + count + ")";
}
```

Default parameters fill in missing arguments. Rest gathers leftover arguments
into an array. Destructured object parameters unpack properties at the call
boundary.

### 59. `import` / `export` and default exports

Use ES modules with `<script type="module">`. Named exports share many bindings;
a module may have **one** default export:

```javascript
// named
export function add(a, b) {
  return a + b;
}

// default
export default function greet(name) {
  return "Hello, " + name + "!";
}

import { add } from "./09-math-utils.js";
import greet from "./09-greet-default.js";
```

Named imports must match export names inside `{ }`. The default import can use
any local name. Serve module labs over HTTP — `file://` cannot load local
modules.

### 60. BigInt vs Number and numeric separators

`Number` cannot represent every integer exactly above
`Number.MAX_SAFE_INTEGER` (`9_007_199_254_740_991`). Use `BigInt` for large
whole numbers. BigInt literals end with `n`. Underscores in numeric literals are
**numeric separators** — they are ignored by the engine and exist only for
readability:

```javascript
const asNumber = 9007199254740993;  // not exact
const asBigInt = 9007199254740993n; // exact
const million = 1_000_000;
const budget = 12_500_000n;
```

You cannot mix BigInt and Number with `+` / `*` directly — convert with
`BigInt(...)` or `Number(...)` first (and know that converting a huge BigInt to
Number can lose precision).

### 61. Pre-increment

`++n` (pre-increment) adds one **first**, then yields the new value. `n++`
(post-increment) yields the **old** value, then adds one:

```javascript
let n = 5;
const pre = ++n;  // n is 6, pre is 6

let m = 5;
const post = m++; // post is 5, then m is 6
```

`--n` / `n--` work the same way for subtraction. Prefer `n = n + 1` or `n += 1`
when the distinction does not matter for readability.

### 62. Scope

**Scope** is where a name is visible:

- **Script / outer**: top-level `let` / `const` in a file are visible to
  functions declared in that same script.
- **Function**: parameters and bindings declared inside a function stay there.
- **Block**: `let` / `const` inside `{ }` (including `if` / loops) exist only in
  that block.

Inner scopes can **shadow** an outer name with a new binding of the same
identifier. That does not change the outer value.

```javascript
const score = 10;
if (true) {
  const score = 99; // shadows only inside this block
  console.log(score); // 99
}
console.log(score); // 10
```

Hoisting (§53) explains *when* a binding exists in its scope; scope explains
*where* that binding can be used.

## Phase 11 — Callbacks and Array Methods

Phase 11 teaches **function expressions** and **arrow functions**, then uses
them as callbacks for Array methods: `forEach`, `map`, `filter` (again), and
`reduce`. Defaults and rest return with these newer function shapes (see also
§58).

**Choosing a method:** side effect → `forEach`; transform every item → `map`;
keep some items → `filter`; combine into one value → `reduce`.

### 63. Function expressions

A function expression is a function value you store in a variable. Unlike a
declaration, it is **not** hoisted as a callable function before its line:

```javascript
const greet = function (name) {
  return "Hello, " + name;
};

greet("Ava");
```

You can also name the expression for clearer stack traces
(`const greet = function greetInner(name) { … }`). Call it through the outer
binding (`greet`), not the inner name.

### 64. Arrow functions

Arrows are shorter function expressions, common for callbacks:

```javascript
const label = (score) => {
  return score + " points";
};

const labelShort = (score) => score + " points";
const double = (n) => n * 2;
const add = (a, b) => a + b;
```

One parameter may omit parentheses (`n => n * 2`). Zero or two-plus parameters
need parentheses. Arrows do not hoist like declarations. They also do not
create their own `this` — leave method/`this` patterns for later. Workbook event
listeners often stay as `function () { … }`.

### 65. Default and rest parameters (expressions and arrows)

Same ideas as §58, written on expressions and arrows:

```javascript
const heading = (title = "Untitled") => "# " + title;

const totalWithBonuses = function (base, ...bonuses) {
  let sum = base;
  bonuses.forEach(function (n) {
    sum = sum + n;
  });
  return sum;
};

const multiply = (left, right = 1) => left * right;
```

Defaults fill missing arguments. Rest gathers leftover arguments into an array
(and must be last).

### 66. `Array.forEach`

Run a side effect once per item. The return value is always `undefined` — do
not use `forEach` to build a new array:

```javascript
players.forEach(function (name, index) {
  console.log(index + ": " + name);
});
```

### 67. `Array.map`

Transform every item into a **new** array of the **same length**:

```javascript
const labels = scores.map(function (n) {
  return n + " pts";
});

const doubled = scores.map((n) => n * 2);
```

The original array is unchanged.

### 68. `Array.filter` (deeper)

Same idea as §57, with richer predicates and arrow callbacks. The original
array length stays the same; the result may be shorter or empty:

```javascript
const kept = players.filter((player) => {
  return player.score >= minScore && player.active;
});
```

### 69. `Array.reduce`

Fold many values into one accumulator. Pass an initial value:

```javascript
const total = scores.reduce(function (acc, n) {
  return acc + n;
}, 0);

const counts = players.reduce((acc, player) => {
  const key = player.category;
  acc[key] = (acc[key] || 0) + 1;
  return acc;
}, {});
```

Pipelines often read `filter` → `map` → `reduce`, then `forEach` to render.

## Phase 12 — Array Queries, Copies, and Objects

Phase 12 adds everyday lookup and boolean checks, teaches **copy vs mutate**,
then covers turning objects into arrays (and back) plus object spread merges and
`flatMap`.

Modern engines also offer `toSorted` / `toReversed`; this phase teaches the
clearer **copy-then-mutate** pattern (`[...arr].sort(...)`, `arr.slice()`).

### 70. `find` and `findIndex`

```javascript
const found = players.find((player) => player.id === 3);
// object or undefined

const index = players.findIndex((player) => player.id === 3);
// number, or -1 if missing
```

### 71. `some`, `every`, and `includes`

```javascript
scores.some((n) => n >= 90);    // at least one
scores.every((n) => n >= 60);   // all must pass
roles.includes("guard");        // value present?
```

For objects, prefer `some((item) => item.id === 3)` instead of `includes`.

### 72. `slice` and spread copies

```javascript
const page = names.slice(1, 3); // new array; end not included
const copy = [...names];        // shallow copy
copy[0] = "Alex";               // original unchanged
```

### 73. Mutating `sort` / `splice` vs copy-first

```javascript
scores.sort((a, b) => a - b);           // mutates scores
const safe = [...scores].sort((a, b) => b - a);

names.splice(1, 1, "Blair");            // mutates names
const next = names.slice();
next.splice(0, 1);                      // mutate only the copy
```

### 74. `Object.keys`, `values`, and `entries`

```javascript
Object.keys(settings);
Object.values(settings);
Object.entries(settings);
// [["theme", "light"], ...]
```

### 75. `Object.fromEntries` and object spread merges

A common motivation for object spread is **base config + per-call overrides**:
keep shared defaults in one object, then spread them and override only what
changes for a given reveal or instance (the Art Studio / ScrollReveal pattern):

```javascript
const base = { distance: "50px", duration: 1000, origin: "bottom" };

ScrollReveal().reveal(".card", { ...base });
ScrollReveal().reveal(".aside", { ...base, origin: "right", delay: 500 });
```

Later keys win, so each call stays short without mutating `base`.

```javascript
const merged = { ...defaults, ...patch }; // later keys win
const again = Object.fromEntries(Object.entries(merged));
```

### 76. `flatMap`

Map each item to an array, then flatten one level:

```javascript
const tags = players.flatMap((player) => player.tags);
// like map(...).flat()
```

## Phase 13 — Promises and Async/Await

Phase 13 teaches **promises** as receipts for future values, then
**`Promise.all`** for parallel work, then **`async` / `await`** as clearer
syntax for the same model. Labs stay **offline** (delayed `setTimeout`
promises). HTTP and `fetch` come in a later phase.

Builds on Phase 10 timers / event loop / `try`/`catch` and Phase 11 callbacks.

### 77. Promise states

A promise is an object that represents work that is not finished yet:

- **pending** — waiting
- **fulfilled** — succeeded with a value
- **rejected** — failed with a reason

```javascript
const later = new Promise(function (resolve, reject) {
  setTimeout(function () {
    resolve("Scout report ready");
    // or: reject(new Error("Timeout"));
  }, 800);
});
// later is pending until resolve/reject runs
```

### 78. `.then` and `.catch`

Consume promises by registering callbacks. They run **later**, after the current
call stack (same scheduling idea as Phase 10 timers):

```javascript
lookupPlayer("Ava")
  .then(function (player) {
    console.log(player.name);
  })
  .catch(function (error) {
    console.log(error.message);
  });
```

### 79. Chaining

Return a value from `.then` to pass it forward, or return another promise to
wait for it. Prefer a flat chain over nested `.then` calls. One `.catch` at the
end covers the chain:

```javascript
fetchId(1)
  .then(function (id) {
    return fetchPlayer(id);
  })
  .then(function (player) {
    return player.name + " (" + player.rating + ")";
  })
  .then(function (label) {
    console.log(label);
  })
  .catch(function (error) {
    console.log(error.message);
  });
```

### 80. `.finally`

`.finally(handler)` runs after fulfillment **or** rejection. Use it for cleanup
(loading flags, buttons). It does not receive the value or error:

```javascript
setLoading(true);

loadReport()
  .then(function (report) {
    show(report);
  })
  .catch(function (error) {
    showError(error.message);
  })
  .finally(function () {
    setLoading(false);
  });
```

### 81. `new Promise`

The executor runs immediately. Call `resolve(value)` or `reject(error)` when
async work finishes. Helpers wrap timers so callers can use `.then`:

```javascript
function delayValue(value, ms) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(value);
    }, ms);
  });
}

function delayFail(message, ms) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      reject(new Error(message));
    }, ms);
  });
}
```

### 82. `Promise.all`

Start independent work together and wait for every promise. The result is an
array in the **same order**. Wall-clock time tracks the **slowest** task, not
the sum. If **any** promise rejects, the whole `Promise.all` rejects:

```javascript
Promise.all([
  delayValue("Ava", 400),
  delayValue("Ben", 700),
  delayValue("Cara", 500)
]).then(function (names) {
  // ["Ava", "Ben", "Cara"] — wall time ≈ 700ms
  console.log(names);
});
```

(`Promise.race`, `allSettled`, and `any` exist for other patterns; this phase
focuses on `all`.)

### 83. `async` and `await`

An `async function` always returns a promise. Inside it, `await` pauses **that
function** until the promise settles — not the whole page. Same model as
`.then`, clearer top-to-bottom reading:

```javascript
async function loadLabel() {
  const id = await fetchId();
  const player = await fetchPlayer(id);
  return player.name + " (" + player.rating + ")";
}

loadLabel().then(function (label) {
  console.log(label);
});
```

### 84. `try` / `catch` / `finally` with `await`

A rejected promise that you `await` throws into the surrounding `try` (Phase 10
Error habits):

```javascript
async function loadReport(id) {
  setLoading(true);
  try {
    const report = await fetchReport(id);
    show(report);
  } catch (error) {
    showError(error.message);
  } finally {
    setLoading(false);
  }
}
```

You can also `await Promise.all([...])` inside `try` for parallel loads.

### 85. Microtasks vs macrotasks

Promise handlers use the **microtask** queue. Timer callbacks use the
**macrotask** (task) queue. After the stack empties, microtasks run **before**
the next timer:

```javascript
console.log("A");

Promise.resolve().then(function () {
  console.log("B"); // microtask
});

console.log("C");

setTimeout(function () {
  console.log("D"); // macrotask
}, 0);

// Prints: A, C, B, D
```

## Phase 14 — HTTP and Fetch

Phase 14 is the first real API trip: **client/server**, **`fetch`**, JSON
bodies, headers, and GET/POST/PUT/DELETE against
[JSONPlaceholder](https://jsonplaceholder.typicode.com/). Builds on Phase 13
promises / `async`/`await` and Phase 3 JSON.

Labs need network access. Writes are fake-persisted by that public API.

### 86. Request, response, client, and server

The **client** (browser page) sends an HTTP **request**. The **server** returns
a **response**.

- Request: method, URL, headers, optional body
- Response: status, headers, body

Common methods: `GET` (read), `POST` (create), `PUT` (update/replace),
`DELETE` (remove).

### 87. `fetch` GET and Response

```javascript
const response = await fetch(
  "https://jsonplaceholder.typicode.com/posts/1"
);

console.log(response.status); // e.g. 200
console.log(response.ok);     // true for 200–299
```

`fetch` fulfills with a **Response**, not the parsed data.

### 88. `response.json()`

Reading JSON is a **second** promise (Phase 3 `JSON.parse` under the hood):

```javascript
const response = await fetch(url);
const data = await response.json();
```

### 89. HTTP status vs network rejection

`fetch` **rejects** on network/CORS failures. HTTP 404/500 still **fulfill**
with `ok === false`. Check status yourself:

```javascript
async function getPost(id) {
  const response = await fetch(
    "https://jsonplaceholder.typicode.com/posts/" + id
  );
  if (!response.ok) {
    throw new Error("HTTP " + response.status);
  }
  return response.json();
}
```

### 90. Request headers and `Content-Type`

```javascript
const response = await fetch(url, {
  headers: {
    Accept: "application/json",
    "Content-Type": "application/json"
  }
});

response.headers.get("content-type");
```

### 91. POST, PUT, and DELETE with JSON

```javascript
await fetch(url, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ title: "Scout", body: "Notes", userId: 1 })
});

await fetch(url + "/" + id, {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(payload)
});

await fetch(url + "/" + id, { method: "DELETE" });
```

### 92. Mapping errors to UI

Use loading / success / HTTP error / network error messages with
`try` / `catch` / `finally` (Phase 13). Treat `Error` messages starting with
`"HTTP "` as server status failures; other rejections as network problems.

## Phase 15 — Fetch Polish

Phase 15 hardens real UI work on top of Phase 14: **URLSearchParams**,
**AbortController**, a demo **Authorization** header, parallel `fetch` with
`Promise.all`, loading/empty/error states, and a simple one-retry pattern.

### 93. `URL` and `URLSearchParams`

```javascript
const url = new URL("https://jsonplaceholder.typicode.com/posts");
url.searchParams.set("userId", String(userId));

await fetch(url.toString());
url.searchParams.get("userId");
```

### 94. `AbortController`

Cancel in-flight work so a new search does not race an old response:

```javascript
let controller = null;

async function load(id) {
  if (controller) {
    controller.abort();
  }
  controller = new AbortController();
  const response = await fetch(url, { signal: controller.signal });
  // handle AbortError separately from real failures
}
```

### 95. Authorization header (pattern)

```javascript
await fetch(url, {
  headers: {
    Authorization: "Bearer " + token,
    Accept: "application/json"
  }
});
```

Demo only — do not ship real secrets in front-end source.

### 96. Parallel `fetch` with `Promise.all`

```javascript
const posts = await Promise.all([
  getPost(1),
  getPost(2),
  getPost(3)
]);
```

Same idea as Phase 13 §82, now on the network. One rejected helper fails the group.

### 97. UI states and one retry

List UIs need **loading**, **empty**, and **error** (plus data). On network
failure, retry **once**; do not retry clear HTTP 4xx-style errors you already
mapped to `"HTTP " + status`. Capstone practice: `projects/post-board/`.

Show the loading state as soon as the request starts (Doherty: **≤ 400 ms** to
visible feedback). An empty list after a successful fetch is a different state
from “still loading” — do not leave the previous rows on screen without a status
update.

```javascript
async function fetchWithRetry(url) {
  try {
    return await fetchOnce(url);
  } catch (error) {
    if (String(error.message).indexOf("HTTP ") === 0) {
      throw error;
    }
    return fetchOnce(url);
  }
}
```


