diff --git a/specs/README.md b/specs/README.md new file mode 100644 index 0000000..05e92fd --- /dev/null +++ b/specs/README.md @@ -0,0 +1,50 @@ +# Functional Specifications + +This directory contains the functional specifications for the `checkboxes.js` jQuery plugin. Each file describes **what** a feature must do — the observable behaviour, edge cases, and invariants — independently of implementation details. + +## Who is this for? + +- Contributors adding or changing behaviour: read the spec first, then update tests, then code. +- Reviewers: verify that a PR's code and tests match what the spec says. +- Users: understand exactly what to expect from each method. + +## Relationship to other documentation + +| This directory | `tests/specs/` | `docs/index.html` | +|---|---|---| +| Prose description of expected behaviour | Executable verification of behaviour | Interactive demos and usage examples | +| Source of truth for *what* | Source of truth for *how it's verified* | Source of truth for *how to use it* | + +## Files + +| Spec | Feature | Test file | +|---|---|---| +| [plugin.md](plugin.md) | Plugin instantiation, jQuery integration, no-conflict | `tests/specs/jquery_checkboxes_spec.js` | +| [check.md](check.md) | `check()` — check all eligible checkboxes | `tests/specs/jquery_checkboxes_check_spec.js` | +| [uncheck.md](uncheck.md) | `uncheck()` — uncheck all eligible checkboxes | `tests/specs/jquery_checkboxes_uncheck_spec.js` | +| [toggle.md](toggle.md) | `toggle()` — flip state of all eligible checkboxes | `tests/specs/jquery_checkboxes_toggle_spec.js` | +| [max.md](max.md) | `max(n)` — limit how many checkboxes can be checked | `tests/specs/jquery_checkboxes_max_spec.js` | +| [range.md](range.md) | `range(enable)` — Shift+click range selection | `tests/specs/jquery_checkboxes_range_spec.js` | +| [data-api.md](data-api.md) | Declarative Data API via `data-*` attributes | *(no dedicated test file yet)* | + +## Cross-cutting invariant: the "visible AND enabled" gate + +`check()`, `uncheck()`, and `toggle()` all share the same eligibility rule: a checkbox is only affected if it is both **visible** (not hidden via CSS) and **not disabled**. A checkbox that fails either condition is left completely unchanged. + +The `mixed.html` fixture concretely captures all four combinations: + +| Checkbox | Visible | Enabled | Affected by bulk methods? | +|---|---|---|---| +| `#5` | No (display:none) | Yes | No | +| `#6` | No (display:none) | Yes | No | +| `#7` | Yes | No (disabled) | No | +| `#8` | Yes | No (disabled) | No | +| `#1–#4, #9–#10` | Yes | Yes | **Yes** | + +## Keeping specs in sync + +When changing behaviour: +1. Update the relevant spec file first. +2. Update or add tests in `tests/specs/` to match. +3. Change the implementation in `src/jquery.checkboxes.js`. +4. Update `docs/index.html` if the user-facing API changed. diff --git a/specs/check.md b/specs/check.md new file mode 100644 index 0000000..5b3a1d0 --- /dev/null +++ b/specs/check.md @@ -0,0 +1,45 @@ +# `check()` + +> Sets all eligible checkboxes in the context to the checked state. + +## Signature + +```js +$(context).checkboxes('check'); +``` + +No parameters. + +## Behavior + +1. Every checkbox within the context that is both **visible** and **not disabled** is set to the checked state (`checked = true`). +2. A `change` event is triggered on each affected checkbox. + +## Edge Cases + +- **Disabled checkboxes** — their state is left unchanged (whether previously checked or unchecked). +- **Hidden checkboxes** (`display:none` or otherwise not `:visible`) — their state is left unchanged. +- **Already-checked checkboxes** — remain checked; the `change` event is still triggered on them. +- **Empty context** — no checkboxes are found; no events are fired; no error is thrown. + +## Constraints + +- Both conditions must hold simultaneously: a visible-but-disabled checkbox is skipped, and a hidden-but-enabled checkbox is also skipped. + +## Events + +- `change` is triggered on every checkbox whose state was updated. + +## Data API + +```html +Check all +``` + +See [data-api.md](data-api.md) for full Data API behaviour. + +## Related + +- Test file: `tests/specs/jquery_checkboxes_check_spec.js` +- Fixture: `tests/fixtures/checked.html` +- Docs section: `docs/index.html` (Checking all checkboxes) diff --git a/specs/data-api.md b/specs/data-api.md new file mode 100644 index 0000000..4eec39a --- /dev/null +++ b/specs/data-api.md @@ -0,0 +1,75 @@ +# Data API + +> Declarative interface that wires up `checkboxes.js` behaviour through HTML `data-*` attributes, requiring no JavaScript. + +## Overview + +The Data API provides two integration points: + +1. **Click handler** — responds to clicks on trigger elements to invoke a method on a target context. +2. **DOM-ready scanner** — reads `data-*` attributes on context elements at page load and calls the corresponding methods automatically. + +## Click Handler + +Any element with `data-toggle="checkboxes"` and `data-action=""` becomes a trigger. When clicked, the plugin invokes `` on the target context. + +### Identifying the context + +The target context is resolved in this order: + +1. The value of the `data-context` attribute on the trigger element (used as a jQuery selector). +2. The hash fragment of the trigger element's `href` attribute (e.g. `href="#my-list"` → `$('#my-list')`). + +`data-context` takes priority over `href`. + +### Default action prevention + +If the trigger element is **not** a checkbox itself, the browser's default action for that element (e.g. navigation for an ``) is prevented. If the trigger element is a checkbox, the default action is not prevented so the checkbox remains interactive. + +### Examples + +```html + +Check all + + + + +
+ ... +
+``` + +## DOM-Ready Scanner + +At DOM ready the plugin scans every element matching `[data-toggle^=checkboxes]` and reads all its `data-*` attributes (the `toggle` attribute itself is excluded). Each attribute is treated as a method call: + +``` +data-="" → $(element).checkboxes('', ) +``` + +This allows `max` and `range` to be configured declaratively: + +```html + +
+ ... +
+ + +
+ ... +
+``` + +## Constraints + +- The click handler is attached once at plugin load time as a **delegated** listener on `document`. It works for elements present in the DOM at any time, including those added dynamically after page load. +- The DOM-ready scanner runs **once**. Elements added dynamically after DOM ready are not processed by the scanner. If you add a `[data-toggle^=checkboxes]` element later, call `$(element).checkboxes('', value)` manually. +- If neither `data-context` nor `href` resolves to a non-empty jQuery set, or if `data-action` is absent, the click handler does nothing. +- Unknown `data-action` values are silently ignored (the plugin's method dispatch no-ops on unknown names). + +## Related + +- Source: `src/jquery.checkboxes.js` (`dataApiClickHandler`, `dataApiDomReadyHandler`) +- *(No dedicated test file yet — `data-api` behaviour is covered informally by the interactive demos in `docs/index.html`)* diff --git a/specs/max.md b/specs/max.md new file mode 100644 index 0000000..720365b --- /dev/null +++ b/specs/max.md @@ -0,0 +1,54 @@ +# `max(n)` + +> Limits how many checkboxes can be checked simultaneously in a context by disabling all unchecked ones once the limit is reached. + +## Signature + +```js +$(context).checkboxes('max', n); +``` + +| Parameter | Type | Required | Description | +|---|---|---|---| +| `n` | `number` | Yes | Maximum number of checkboxes that may be checked at the same time. Pass `0` (or any non-positive value) to remove the limit. | + +## Behavior + +1. After `max(n)` is called with a positive integer `n`, a `click` listener is attached to the context. +2. After each click on any checkbox in the context, if the number of `:checked` checkboxes equals `n`, every unchecked checkbox in the context is disabled (`disabled = true`). +3. If the count of `:checked` checkboxes drops below `n`, every unchecked checkbox is re-enabled (`disabled = false`). +4. Calling `max(0)` (or any non-positive value) removes the click listener. Checkboxes that were disabled by the max mechanism are **not** automatically re-enabled when the limit is removed. +5. Calling `max(n)` a second time replaces the previous listener; only the most recent value of `n` is enforced. + +## Edge Cases + +- **Setting max when checkboxes are already checked** — the limit is enforced only on subsequent click events; the existing state at the time `max()` is called is not validated retroactively. +- **Unchecking when at the limit** — the click listener fires, the count drops below `n`, and unchecked checkboxes are re-enabled immediately. +- **Checkboxes that are already natively disabled** — the max mechanism writes `disabled = true/false` to them too, which can overwrite their original disabled state. +- **Clicking beyond the limit** — once all unchecked checkboxes are disabled, further clicks cannot increase the checked count. + +## Constraints + +- **`click`-only enforcement**: the limit is applied via a `click` event listener. Programmatic calls to `check()` or `toggle()` bypass the listener entirely and can result in more than `n` checkboxes being checked. +- The limit applies to the entire context, not per-row or per-group. +- The enforcement condition is `=== n` (strict equality, not `>= n`). If programmatic manipulation produces a count above `n`, unchecked checkboxes are not disabled until the count drops to exactly `n` through subsequent clicks. + +## Events + +No additional events are fired by the `max` mechanism itself. Normal browser `click` and `change` events on checkboxes are unaffected. + +## Data API + +```html +
+ ... +
+``` + +The `data-max` attribute is read once at DOM-ready by the Data API scanner and calls `max(3)` on the element. See [data-api.md](data-api.md). + +## Related + +- Test file: `tests/specs/jquery_checkboxes_max_spec.js` +- Fixture: `tests/fixtures/unchecked.html` +- Docs section: `docs/index.html` (Limit max number of checked checkboxes) diff --git a/specs/plugin.md b/specs/plugin.md new file mode 100644 index 0000000..4268710 --- /dev/null +++ b/specs/plugin.md @@ -0,0 +1,42 @@ +# Plugin + +> The `checkboxes` jQuery plugin registers a `Checkboxes` instance on a context element and exposes its methods through the standard jQuery plugin interface. + +## Signature + +```js +$(context).checkboxes(method [, ...args]); +``` + +| Parameter | Type | Required | Description | +|---|---|---|---| +| `method` | `string` | No | Name of the method to invoke (`'check'`, `'uncheck'`, `'toggle'`, `'max'`, `'range'`). | +| `...args` | any | No | Arguments forwarded to the method. | + +## Behavior + +1. Calling `$(context).checkboxes(method)` on a jQuery-wrapped element creates a `Checkboxes` instance for that element and stores it in jQuery's internal data store under the key `'checkboxes'`. +2. The same instance is reused on every subsequent call — the instance is created once per element. +3. The named `method` is looked up on the instance and called with any extra arguments. Unrecognised method names are silently ignored. +4. `$.fn.checkboxes` is chainable: it returns the original jQuery object so calls can be chained. +5. `$.fn.checkboxes.Constructor` holds a reference to the `Checkboxes` class. + +## Edge Cases + +- **Empty selection** (`$().checkboxes()`) — no instance is created; no error is thrown; the empty jQuery set is returned. +- **Unknown method name** — silently ignored; the instance is still created and stored. +- **Method called before instance exists** — the instance is created on the first call, regardless of the method name. + +## No-Conflict + +`$.fn.checkboxes.noConflict()` restores the previous value of `$.fn.checkboxes` (if another plugin occupied that name before this one was loaded) and returns the plugin function so it can be assigned to a different name: + +```js +let checkboxes = $.fn.checkboxes.noConflict(); +$.fn.myCheckboxes = checkboxes; +``` + +## Related + +- Test file: `tests/specs/jquery_checkboxes_spec.js` +- Source: `src/jquery.checkboxes.js` diff --git a/specs/range.md b/specs/range.md new file mode 100644 index 0000000..6ade728 --- /dev/null +++ b/specs/range.md @@ -0,0 +1,57 @@ +# `range(enable)` + +> Enables Shift+click range selection: holding Shift while clicking a checkbox checks or unchecks all visible checkboxes between the last-clicked checkbox and the current one. + +## Signature + +```js +$(context).checkboxes('range', enable); +``` + +| Parameter | Type | Required | Description | +|---|---|---|---| +| `enable` | `boolean` | Yes | `true` to enable range selection; `false` to disable it. | + +## Behavior + +1. After `range(true)` is called, a `click` listener is attached to the context. +2. Every click on a checkbox (Shift held or not) updates an internal **anchor** — the last-clicked checkbox. +3. When the user Shift+clicks a checkbox and an anchor exists, all visible checkboxes between the anchor and the clicked checkbox (inclusive, in DOM order) are set to the same checked state as the clicked checkbox. +4. Disabled checkboxes within the range are skipped; their state is not changed. +5. A `change` event is triggered on each checkbox whose state was updated by the range operation. +6. Calling `range(false)` removes the click listener; subsequent Shift+clicks behave as normal browser checkbox clicks. + +## Edge Cases + +- **First click without Shift** — recorded as the new anchor; no range action; normal checkbox toggle occurs. +- **Shift+click with no existing anchor** — no range action; the clicked checkbox toggles normally and becomes the new anchor. +- **Range of one** — Shift+clicking the same checkbox as the anchor affects only that checkbox. +- **Reverse direction** — Shift+clicking a checkbox that appears before the anchor in the DOM still selects the full range between them. Direction does not matter. +- **Hidden checkboxes in the range** — excluded from range computation. The range is calculated over the index of `:checkbox:visible` elements, so hidden checkboxes do not occupy a slot. +- **Disabled checkboxes in the range** — included in the index range but their state is not changed. +- **Calling `range(true)` twice** — attaches a second listener on top of the first; range actions fire twice per click. Call `range(false)` first to reset, or rely on the Data API which calls it only once. + +## Constraints + +- The range is computed using the visible-checkbox index at the moment of the Shift+click. Checkboxes added or removed between the anchor click and the Shift+click may shift indices and produce unexpected ranges. +- The state applied to the entire range is the **final checked state of the Shift+clicked checkbox** (i.e., the state after the browser has toggled it), not the state of the anchor. + +## Events + +- `change` is triggered on each checkbox in the range whose state was modified. + +## Data API + +```html +
+ ... +
+``` + +The `data-range` attribute is read once at DOM-ready by the Data API scanner and calls `range(true)` on the element. See [data-api.md](data-api.md). + +## Related + +- Test file: `tests/specs/jquery_checkboxes_range_spec.js` *(existence only — Shift+click behaviour has no automated test yet)* +- Fixture: `tests/fixtures/mixed.html` +- Docs section: `docs/index.html` (Range selection of checkboxes) diff --git a/specs/toggle.md b/specs/toggle.md new file mode 100644 index 0000000..3fa903d --- /dev/null +++ b/specs/toggle.md @@ -0,0 +1,47 @@ +# `toggle()` + +> Flips the checked state of every eligible checkbox in the context individually. + +## Signature + +```js +$(context).checkboxes('toggle'); +``` + +No parameters. + +## Behavior + +1. Every checkbox within the context that is both **visible** and **not disabled** has its state inverted: checked becomes unchecked, unchecked becomes checked. +2. Each checkbox is evaluated and flipped **independently** — the result depends on that checkbox's own current state, not on the majority state of the context. +3. A `change` event is triggered on the full set of affected checkboxes after all states have been flipped. + +## Edge Cases + +- **Disabled checkboxes** — their state is left unchanged. +- **Hidden checkboxes** (`display:none` or otherwise not `:visible`) — their state is left unchanged. +- **Mixed state** — a context with 3 checked and 2 unchecked eligible checkboxes will result in 3 unchecked and 2 checked; there is no "majority wins" logic. +- **Empty context** — no checkboxes are found; no events are fired; no error is thrown. + +## Constraints + +- Both visibility and enabled conditions must hold simultaneously (same gate as `check()` and `uncheck()`). +- The `change` event fires once on the set, after all individual flips are done — not once per checkbox. + +## Events + +- `change` is triggered on the set of affected checkboxes (fired once after all flips). + +## Data API + +```html +Toggle all +``` + +See [data-api.md](data-api.md) for full Data API behaviour. + +## Related + +- Test file: `tests/specs/jquery_checkboxes_toggle_spec.js` +- Fixture: `tests/fixtures/mixed.html` +- Docs section: `docs/index.html` (Toggling all checkboxes) diff --git a/specs/uncheck.md b/specs/uncheck.md new file mode 100644 index 0000000..abd6bdf --- /dev/null +++ b/specs/uncheck.md @@ -0,0 +1,45 @@ +# `uncheck()` + +> Sets all eligible checkboxes in the context to the unchecked state. + +## Signature + +```js +$(context).checkboxes('uncheck'); +``` + +No parameters. + +## Behavior + +1. Every checkbox within the context that is both **visible** and **not disabled** is set to the unchecked state (`checked = false`). +2. A `change` event is triggered on each affected checkbox. + +## Edge Cases + +- **Disabled checkboxes** — their state is left unchanged (whether previously checked or unchecked). +- **Hidden checkboxes** (`display:none` or otherwise not `:visible`) — their state is left unchanged. +- **Already-unchecked checkboxes** — remain unchecked; the `change` event is still triggered on them. +- **Empty context** — no checkboxes are found; no events are fired; no error is thrown. + +## Constraints + +- Both conditions must hold simultaneously: a visible-but-disabled checkbox is skipped, and a hidden-but-enabled checkbox is also skipped. + +## Events + +- `change` is triggered on every checkbox whose state was updated. + +## Data API + +```html +Uncheck all +``` + +See [data-api.md](data-api.md) for full Data API behaviour. + +## Related + +- Test file: `tests/specs/jquery_checkboxes_uncheck_spec.js` +- Fixture: `tests/fixtures/mixed.html` +- Docs section: `docs/index.html` (Unchecking all checkboxes)