Skip to content

feat(toolbarSelect): DRY up the toolbar <select>-change boilerplate#17

Merged
JohnMcLear merged 1 commit into
mainfrom
feat/toolbar-select
May 15, 2026
Merged

feat(toolbarSelect): DRY up the toolbar <select>-change boilerplate#17
JohnMcLear merged 1 commit into
mainfrom
feat/toolbar-select

Conversation

@JohnMcLear
Copy link
Copy Markdown
Member

Summary

Three core-adjacent plugins — ep_font_color, ep_font_size, ep_headings2 — all implement the same toolbar <select> change handler by hand:

hs.on('change', function () {
  const value = $(this).val();
  const intValue = parseInt(value, 10);
  if (!isNaN(intValue)) {
    context.ace.callWithAce((ace) => ace.ace_doX(intValue), 'op', true);
    hs.val('dummy');
    context.ace.focus();
  }
});

This PR adds a shared toolbarSelect({selector, context, invoke}) helper that handles the four steps — value coercion, callWithAce wrap, sentinel reset, and editor-focus restore — so each plugin can drop the boilerplate.

const {toolbarSelect} = require('ep_plugin_helpers/toolbar-select');

exports.postAceInit = (hookName, context) => {
  toolbarSelect({
    selector: '#font-size, select.size-selection',
    context,
    invoke: (ace, value) => ace.ace_doInsertsizes(value),
    op: 'insertsize',
  });
};

Why focus restoration matters

The context.ace.focus() call in each plugin is intentional and load-bearing: without it, picking from the toolbar leaves focus stuck on the <select> (or its nice-select wrapper), and the next keystroke goes there instead of the pad. The existing select_focus_restore.spec.ts test in core covers this for ep_headings2.

This helper restores focus unconditionally — even when the coerced value is unusable — so an accidental pick on a non-numeric option still leaves the user typing back in the pad. That preserves the WCAG-relevant property that the per-plugin .focus() calls already provided (context: ether/etherpad#7255).

API

Field Required Default Description
selector yes jQuery selector for the toolbar <select>.
context yes The postAceInit context — must expose context.ace.callWithAce and context.ace.focus.
invoke yes (ace, coercedValue) => void. Runs inside callWithAce so the edit joins the undo stack.
op no 'toolbarSelect' callWithAce label.
coerce no 'int' 'int' | 'number' | 'string' | 'identity' or a custom (raw) => coerced | null. Returning null skips the edit but still restores focus.
resetValue no 'dummy' Value written back to the select so the same option can be picked again.
onAfterChange no (coercedValue) => void, called after focus restoration. Errors swallowed and logged.

Client-only — imported via the sub-path ep_plugin_helpers/toolbar-select, not via the package root, so esbuild doesn't pull any server-only deps into the pad bundle (same pattern as pad-toggle / pad-select).

Tests

12 new mocha tests cover:

  • Config validation (missing selector, bad context, non-function invoke, empty op, unknown coerce token, non-function onAfterChange)
  • Int coercion + reset + invoke ordering
  • Focus runs even when coercion returns null
  • Custom function coercers
  • Custom resetValue
  • onAfterChange invocation + error swallowing

All 103 tests pass (12 new + 91 existing).

Version bump

0.5.3 → 0.6.0 — new public API surface, no behavior change for existing helpers.

Follow-up PRs

Once this merges and ships to npm:

  • ep_font_color → migrate to toolbarSelect
  • ep_font_size → migrate to toolbarSelect
  • ep_headings2 → migrate to toolbarSelect

🤖 Generated with Claude Code

Three Etherpad plugins (ep_font_color, ep_font_size, ep_headings2) all
implement the same toolbar-select boilerplate by hand:

  hs.on('change', function () {
    const value = $(this).val();
    const intValue = parseInt(value, 10);
    if (!isNaN(intValue)) {
      context.ace.callWithAce((ace) => ace.ace_doX(intValue), 'op', true);
      hs.val('dummy');
      context.ace.focus();
    }
  });

This commit adds a shared `toolbarSelect({selector, context, invoke})`
helper that handles all four steps — value coercion, callWithAce wrap,
sentinel reset, and (most importantly for a11y) editor-focus restore so
the next keystroke doesn't get swallowed by the toolbar control.

Focus restoration runs unconditionally, even when the coerced value is
unusable, so an accidental pick on a non-numeric option still leaves
the user typing back in the pad rather than stuck on the select. This
preserves the WCAG-relevant property that the per-plugin .focus() calls
already provided — see ether/etherpad#7255 for the broader context.

Client-only module — imported via the sub-path
`ep_plugin_helpers/toolbar-select`, not via the package root, so
esbuild doesn't pull any server-only deps into the pad bundle (same
pattern as pad-toggle / pad-select).

12 new mocha tests cover config validation, int/number/string/identity
and custom-function coercers, custom resetValue, onAfterChange callback
invocation, and the focus-runs-on-failure guarantee.

Bumps minor version 0.5.3 → 0.6.0 because it's a new public API
surface; no behavior change for existing helpers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@qodo-code-review
Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@JohnMcLear JohnMcLear merged commit 201cc07 into main May 15, 2026
4 checks passed
JohnMcLear added a commit to ether/ep_font_size that referenced this pull request May 15, 2026
…elect (#142)

Replaces the hand-rolled change handler with the shared `toolbarSelect`
helper that newly ships in ep_plugin_helpers 0.6.0. Behavior is
preserved: int coercion, callWithAce wrap with the same `insertsize`
op label, `'dummy'` sentinel reset, and editor-focus restore after the
edit so the next keystroke doesn't get swallowed by the toolbar
control.

The helper restores focus unconditionally (even when the picked value
fails to coerce). For this plugin that means a slight behavioral
improvement: previously, picking the dummy/blank entry left focus
sitting on the <select>; now it always lands back in the pad. This
matches the (already unconditional) behavior in ep_headings2 and is the
WCAG-friendly default.

Depends on ep_plugin_helpers >= 0.6.0 (ether/ep_plugin_helpers#17).
Refs ether/etherpad#7255

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JohnMcLear added a commit to ether/ep_headings2 that referenced this pull request May 15, 2026
…elect (#184)

Replaces the hand-rolled change handler with the shared `toolbarSelect`
helper that newly ships in ep_plugin_helpers 0.6.0. Behavior is fully
preserved including the unconditional focus restore from #130 (the
helper guarantees focus runs regardless of whether the coerced value
was usable, matching what the original code already did).

Depends on ep_plugin_helpers >= 0.6.0 (ether/ep_plugin_helpers#17).
Refs ether/etherpad#7255

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JohnMcLear added a commit to ether/ep_font_color that referenced this pull request May 15, 2026
* feat: migrate toolbar <select> handler to ep_plugin_helpers/toolbar-select

Replaces the hand-rolled change handler with the shared `toolbarSelect`
helper that newly ships in ep_plugin_helpers 0.6.0. Behavior is
preserved: int coercion, callWithAce wrap with the same `insertColor`
op label, `'dummy'` sentinel reset, and editor-focus restore after the
edit so the next keystroke doesn't get swallowed by the toolbar
control.

The helper restores focus unconditionally (even when the picked value
fails to coerce). For this plugin that means a slight behavioral
improvement: previously, picking the dummy/blank entry left focus
sitting on the <select>; now it always lands back in the pad. This
matches the (already unconditional) behavior in ep_headings2 and is the
WCAG-friendly default.

Depends on ep_plugin_helpers >= 0.6.0 (ether/ep_plugin_helpers#17).
Refs ether/etherpad#7255

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(deps): bump ep_plugin_helpers range to ^0.6.1 (the published version)

Etherpad's plugin installer (live-plugin-manager-backed) resolves the
dependency's lower-bound version literally — `^0.6.0` makes it ask
npm for `ep_plugin_helpers@0.6.0`, which was never published (the auto
publish workflow bumped straight to `0.6.1`). Anchor to the actual
published version so the install resolves.

Same fix being applied to ep_font_size and ep_headings2.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant