Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions specs/README.md
Original file line number Diff line number Diff line change
@@ -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.
45 changes: 45 additions & 0 deletions specs/check.md
Original file line number Diff line number Diff line change
@@ -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
<a href="#context" data-toggle="checkboxes" data-action="check">Check all</a>
```

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)
75 changes: 75 additions & 0 deletions specs/data-api.md
Original file line number Diff line number Diff line change
@@ -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="<method>"` becomes a trigger. When clicked, the plugin invokes `<method>` 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 `<a>`) is prevented. If the trigger element is a checkbox, the default action is not prevented so the checkbox remains interactive.

### Examples

```html
<!-- Trigger using href hash -->
<a href="#my-list" data-toggle="checkboxes" data-action="check">Check all</a>

<!-- Trigger using data-context -->
<button data-toggle="checkboxes" data-context="#my-list" data-action="uncheck">Uncheck all</button>

<div id="my-list">
<input type="checkbox"> ...
</div>
```

## 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-<method>="<value>" → $(element).checkboxes('<method>', <value>)
```

This allows `max` and `range` to be configured declaratively:

```html
<!-- Enables range selection on #my-list at page load -->
<div id="my-list" data-toggle="checkboxes" data-range="true">
<input type="checkbox"> ...
</div>

<!-- Limits #my-list to 3 checked checkboxes at page load -->
<div id="my-list" data-toggle="checkboxes" data-max="3">
<input type="checkbox"> ...
</div>
```

## 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('<method>', 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`)*
54 changes: 54 additions & 0 deletions specs/max.md
Original file line number Diff line number Diff line change
@@ -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
<div data-toggle="checkboxes" data-max="3">
...
</div>
```

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)
42 changes: 42 additions & 0 deletions specs/plugin.md
Original file line number Diff line number Diff line change
@@ -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`
57 changes: 57 additions & 0 deletions specs/range.md
Original file line number Diff line number Diff line change
@@ -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
<div data-toggle="checkboxes" data-range="true">
...
</div>
```

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)
47 changes: 47 additions & 0 deletions specs/toggle.md
Original file line number Diff line number Diff line change
@@ -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
<a href="#context" data-toggle="checkboxes" data-action="toggle">Toggle all</a>
```

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)
Loading
Loading