Skip to content

Feat: show a live count next to counter checklist tasks - #1218

Merged
repile[bot] merged 5 commits into
developmentfrom
feature/live-character-counter
Oct 20, 2026
Merged

repile[bot] merged 5 commits into
developmentfrom
feature/live-character-counter

Conversation

@stevejburge

Copy link
Copy Markdown
Member

What

Shows the current value of counter checklist tasks in the editor, updating live as the author types.

Resolves #1097 and #467.

Why

Counter tasks only reported pass or fail. A writer could see that the excerpt was too short, with no idea by how much. #467 put it well: writers "are finding it difficult to estimate how much they've written" — and explicitly asked for it beyond the excerpt.

How

Adds PP_Checklists.update_live_count(), which renders the value as a small badge in two places:

  • the metabox list item, for the Classic editor
  • the requirement label in ppChecklists.requirements, which is what the Gutenberg sidebar renders (no source change needed there — it already renders labels via dangerouslySetInnerHTML and re-syncs whenever pp-checklists.requirements-updated fires, which every counter's tic handler already triggers)

The original label is cached as pp_base_label, so repeated updates replace the badge rather than stacking it.

Coverage

Wired into every counter task that has a single meaningful value, in both editors: excerpt/title character counts, word count, tag/category counts, hierarchical and non-hierarchical taxonomy counts, and internal/external link counts.

image_alt_count is deliberately excluded: it validates each image's alt text separately (Array.every()), so there is no single number to display.

Open question for reviewers

The badge is currently always on. If you'd rather it were opt-in, it would be easy to gate behind a setting in the Appearance section — say the word and I'll add it.

Testing

  • Verified the badge text for min-only, max-only, and range-configured counters (a min-only counter used to render as (42 (min 50)) — double-parenthesized — now renders as (42, min 50)).
  • Verified the badge is excluded from the Gutenberg sidebar's alphabetical-sort key (normalizeLabelForSort), so two counter tasks sharing a label (via the label-rename setting) no longer swap position on every keystroke as their live counts change.
  • Rebuilt gutenberg-panel.min.js via webpack in development mode, matching how the checked-in bundle is already built.

Not yet exercised in a live WordPress install — the Gutenberg sidebar path in particular deserves a manual look before merge.

stevejburge and others added 2 commits August 26, 2026 13:40
Resolves #1097 and #467. Counter tasks (excerpt/title/word count, tag
and category count, taxonomy counts, internal/external link count)
only showed pass/fail, giving no sense of how close the content was to
the configured range. This adds a small live badge, e.g. "(42 / 60)",
next to the task label, updating on the same tic/content-change cycle
each counter already uses.

Two shared helpers in meta-box.js:
- format_live_count(count, min, max): "count / max" when a max is
  configured, "count (min N)" for a minimum-only rule, else just the
  count.
- update_live_count(id, text): updates the badge in the metabox DOM
  and appends it to ppChecklists.requirements[id].label (caching the
  original label first, so repeated updates replace rather than stack
  the badge).

Wired into all 9 counter-type tasks across both the classic editor and
Gutenberg code paths (11 call sites): tags, categories, hierarchical
and non-hierarchical taxonomies, excerpt and title character counts,
word count, and internal/external link counts.

image_alt_count is intentionally NOT covered: it validates each
image's alt text length separately (Array.every()), so there is no
single number to show.

No changes needed in gutenberg-panel.jsx: the sidebar already renders
requirement labels via dangerouslySetInnerHTML and re-syncs whenever
'pp-checklists.requirements-updated' fires, which every tic handler
already triggers after a status check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two issues caught in review before opening the PR:

- format_live_count()'s min-only branch returned an already-
  parenthesized string ("42 (min 50)"), which update_live_count()
  then wrapped in another pair of parens for both the label and the
  DOM badge, producing "(42 (min 50))" for every min-only counter task
  (a common configuration - filled_excerpt, title_count, and most of
  the count-type tasks in this plugin are typically configured with
  only a minimum). Changed to a bare "42, min 50" so the single
  outer wrap in update_live_count produces clean text.

- The live badge is appended into req.label, which
  gutenberg-panel.jsx's normalizeLabelForSort() also uses to build the
  alphabetical-sort key when "Checklist task sort order" is set to
  Alphabetical. Since the badge's digits change as the user types, two
  counter tasks that happen to share a label (e.g. after using the
  per-task label rename feature) could visibly swap positions in the
  sidebar on every keystroke. normalizeLabelForSort() now strips the
  .pp-checklists-live-count element before extracting the sort text.

Rebuilt gutenberg-panel.min.js via webpack (NODE_ENV=development, to
match how the checked-in bundle is already built - production mode
produces a fully minified single line that would otherwise replace the
whole file). gutenberg-warning.min.js is untouched, matching its
unmodified .jsx source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@stevejburge

Copy link
Copy Markdown
Member Author

Merge-order note: this PR and #1217 (internal-links fix) both insert a new method at the same spot in modules/checklists/assets/js/meta-box.js (right after check_valid_quantity) — format_live_count/update_live_count here, get_internal_link_hosts there. Whichever merges second will hit a real git conflict against development, not an auto-mergeable one (verified with git merge-tree).

It's mechanical to resolve — the two additions don't call each other or share state, so it's just a manual splice keeping both methods — but flagging it now so it isn't a surprise during merge.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 23aaddabdc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

req.pp_base_label = req.label;
}

req.label = req.pp_base_label + ' <span class="pp-checklists-live-count">(' + displayText + ')</span>';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update Gutenberg labels without mutating existing state

In Gutenberg, updateRequirements() stores the original ppChecklists.requirements objects directly in component state. Mutating req.label here therefore also changes this.state.requirements before the requirements-updated hook calculates prevHash; the previous and next hashes match and no setState() occurs. For counters updated by the periodic TIC handlers, such as title, excerpt, tags, and categories, the sidebar can retain the previous count until an unrelated React render. Build a new requirement object or otherwise perform an immutable state update so the hook reliably renders the new badge.

Useful? React with 👍 / 👎.

var min = parseInt(config.value[0]),
max = parseInt(config.value[1]);

PP_Checklists.update_live_count(requirementId, PP_Checklists.format_live_count(count, min, max));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Initialize counter caches so zero values render

When a post initially has zero words, this call is skipped because lastCount starts at 0 and the equality guard returns before reaching it. The same initial-zero suppression affects Gutenberg internal/external links and Classic word/external-link counts, leaving those tasks without any badge until the count first becomes nonzero. Initialize the caches to an unset sentinel or force the first refresh so the promised current value includes zero.

Useful? React with 👍 / 👎.

Comment on lines +641 to +642
if (max_value > 0) {
return count + ' / ' + max_value;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat a lower maximum as a minimum-only rule

For supported configurations where min_value > max_value > 0, validation and the generated requirement label intentionally treat the rule as minimum-only, but this formatter takes the max_value > 0 branch and displays values such as 42 / 20 for a minimum of 50. That badge contradicts both the task label and pass/fail behavior; mirror check_valid_quantity() by preferring the minimum-only representation when the maximum is below the minimum.

Useful? React with 👍 / 👎.

* @return {void}
*/
update_live_count: function (requirementId, displayText) {
var req = (typeof ppChecklists !== 'undefined') ? ppChecklists.requirements[requirementId] : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wire live counts into the Yoast counter tasks

When the Yoast integration is enabled, Focus_Keyword and Meta_Description are Base_counter requirements with single character counts, but their handlers in modules/yoastseo/assets/js/meta-box.js still only trigger status updates and never call this new helper. Those configured counter tasks therefore remain pass/fail-only while all core counters show a badge, so invoke update_live_count() from both Yoast handlers as well.

Useful? React with 👍 / 👎.

Comment on lines +645 to +646
if (min_value > 0) {
return count + ', min ' + min_value;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Localize the minimum-count marker

For every minimum-only counter, this newly visible user-facing text is hard-coded as English. Consequently translated editor screens show an otherwise localized requirement followed by (42, min 50). Supply the marker through the existing localized script data or a WordPress i18n call so the badge follows the active locale.

Useful? React with 👍 / 👎.

@stevejburge stevejburge added this to the 2.30 milestone Sep 1, 2026
@repile

repile Bot commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

Review: tested on a fresh SandyWP sandbox (WP 7.1.3, PHP 8.3), block editor and Classic Editor

Screenshots and recordings are linked from the Repile recorder. The links need a Repile login, so they open for the team but not anonymously.

Verdict: Not yet. The badges work, but several Codex findings reproduce, including the Gutenberg one.

Setup

Recordings

Classic editor, development (no counts):
🖼 screenshot: 1218 before

Classic editor, this PR (real wp-admin, new post):
🖼 screenshot: 1218 classic real

Block editor, this PR, after the title and content edits (badges correct once the sidebar re-renders):
🖼 screenshot: 1218 block after content

Block editor, this PR, 8 s after changing the title to 36 chars: the badge still says (2 / 60).
🖼 screenshot: 1218 stale badge

What blocks merging

  1. Confirmed (Codex P2, Gutenberg re-render): update_live_count() changes ppChecklists.requirements[id].label in place. Those are the same objects held in the sidebar's state.requirements, so getRequirementsHash() compares equal and setState() is skipped. The title badge stays at "(2 / 60)" until an unrelated change re-renders the panel (see screenshot). On development the ✓/✗ status has the same lag, since req.status is also changed in place, so the root cause predates this PR. But a stale number is exactly what this feature is meant to fix. Build new requirement objects, or force setState when labels change.
  2. Confirmed (Codex P2, zero counts): the lastCount / lastInternalCount / lastExternalCount caches start at 0, so a count of 0 never renders. On a new post, Words, Internal links and External links show no badge in either editor. Start the caches at null or -1.
  3. Confirmed by reading the code (Codex P2, min > max): format_live_count() checks max > 0 first. A min 50 / max 20 rule, which check_valid_quantity() treats as minimum-only, displays "42 / 20".
  4. Confirmed (Codex P2, i18n): "min" in (12, min 50) is hard-coded English. Localize it through ppChecklists or wp.i18n.

Notes: the Yoast focus keyword and meta description counters don't get badges (Codex P2). That's fine as a follow-up.

Reviewed on a throwaway SandyWP sandbox; nothing was pushed to this branch.

Riza and others added 3 commits October 19, 2026 20:04
Resolve the meta-box.js conflict with #1217 by keeping both
format_live_count()/update_live_count() and get_internal_link_hosts()/
normalize_link_hosts().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…slatable

- Gutenberg sidebar: keep copies of the requirement objects in state and
  re-read ppChecklists.requirements on requirements-updated, so in-place
  label/status changes from meta-box.js produce a new hash and re-render.
  The title badge no longer stays stale while typing.
- Start the word/internal/external link count caches at null so a count of
  0 renders a badge on new posts.
- format_live_count(): a max lower than the min is ignored, mirroring
  check_valid_quantity(), so min 50 / max 20 shows "42, min 50".
- Badge formats are localized (live_count_max, live_count_min) and escaped
  before going into the label HTML.
- Show the badge for the Yoast focus keyword and meta description counters.
- Rebuild gutenberg-panel.min.js.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on one line

The pre-publish panel registered its update-failed-requirements handler
under the 'publishpress/checklists' namespace, which the sidebar panel
removes when it unmounts, so the panel never rendered. Give it its own
namespace and register it on mount. Rebuild gutenberg-warning.min.js.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@repile

repile Bot commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Update: review findings fixed, tested on fresh SandyWP sandboxes (WP 7.1.3, PHP 8.3)

Verdict: Ready to merge. Every finding from the earlier review and all 5 Codex comments are fixed. Badges now update as you type in the block editor sidebar, the pre-publish panel and the Classic Editor metabox. No console errors and no PHP notices from the plugin.

What changed (pushed to this branch, no force-push)

How I tested

Two fresh sandboxes (not cloned from customer data): development first, then this branch. Classic Editor plugin installed, with the site switched between block and classic. Rules cover every counter type in Free and every rule shape:

Requirement Rule Shape
Title characters Required 10–60 min + max
Words in content Recommended min 50 min only
Excerpt characters Recommended max 100 max only
Categories Required 1–1 exact
Tags Recommended min 3 / max 2 min > max
Internal links Recommended min 2 min only
External links Recommended 1–2 min + max
Custom non-hierarchical taxonomy (Genres) max 2 max only
Custom hierarchical taxonomy (Topics) min 1 min only
Yoast focus keyword / meta description min 3 / min 50 & max 40 min only / min > max

image_alt_count is intentionally excluded (no single number). Title words, image count and the other Pro counters live in Pro and aren't part of this PR.

Driven with real key presses (Playwright + Chromium):

  • Block editor, title typed one key at a time: sidebar (4 / 60) → (15 / 60) → (37 / 60) while typing. On development nothing is shown. New post: every counter shows its 0 badge. Content, excerpt and term changes all update live: (45, min 50), (3 / 2) external links, (120 / 100) excerpt, (2 / 1) categories, (3, min 3) tags (min > max), (3 / 2) genres, (1, min 1) topics.
  • Pre-publish panel: lists the failing items with counts. Changing tags/excerpt while it's open updates it, e.g. Minimum of 3 tags (1, min 3) and Maximum of 100 characters in excerpt (150 / 100).
  • Classic Editor: title badge (5 / 60) → (16 / 60) → (32 / 60) per keystroke. Words update in both the Code and the Visual (TinyMCE) tab (48 → 51 → 53). Links, excerpt, categories ((1 / 1) → (2 / 1)), tags, genres and topics all update. The publish modal lists the counts.
  • Translation: with a test gettext filter, badges render as (0, mín. 3) / (0 de 60).
  • Yoast: typing the focus keyword updates (10, mín. 3). Meta description min 50 / max 40 shows (0, mín. 50).
  • Browser console: no errors from Checklists, only core/Yoast deprecation notices. debug.log: no Checklists entries. php -l is clean on all 164 plugin PHP files (PHP 8.3).

Evidence

Recorder links need a Repile login, so they open for the team but not anonymously.

Test site

Open a copy of the test site: each click starts a fresh copy, signed in to wp-admin as admin. It opens the block editor on the draft "Draft for live count review" with all the rules above configured.

  1. Open the Checklists sidebar (checkmark icon at the top right) if it isn't already open.
  2. Type in the title: the (n / 60) badge changes on every keystroke. Type in the paragraph to change the word count.
  3. Add tags/categories/Genres/Topics in the Post sidebar, or add an excerpt, and watch their badges.
  4. Click Publish to see the pre-publish Checklist panel with counts.
  5. For the Classic Editor: Posts → All Posts, then hover a post and choose Edit (classic editor).

Open question from the PR description (badge always on vs. opt-in setting) is still open for a product decision. It doesn't block merging.

@repile
repile Bot merged commit bad44eb into development Oct 20, 2026
repile Bot added a commit that referenced this pull request Oct 20, 2026
Brings in #1218 (live count badges). Its updateRequirements() change
stores { ...req } copies, so a custom task toggled in the block editor
Checklists panel now re-renders immediately instead of waiting for the
next unrelated change.

Conflicts: kept both the a11y strings and the live count strings in
checklists.php; rebuilt gutenberg-panel.min.js and
gutenberg-warning.min.js with webpack --mode development.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rizaardiyanto1412
rizaardiyanto1412 deleted the feature/live-character-counter branch October 26, 2026 18:42
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