From 13d2dea935fd55100a91a9cd130ae1b48e99c544 Mon Sep 17 00:00:00 2001 From: Steve Burge Date: Wed, 26 Aug 2026 13:15:12 -0400 Subject: [PATCH 01/52] feat: list Required Words in the Pro requirements teaser Adds the teaser entry for the new Pro "Required Words" requirement, alongside the existing Prohibited Words entry, so free users see it in the Pro feature list and the requirement is labelled correctly. Pairs with the publishpress-checklists-pro branch feature/required-words-checklist. Co-Authored-By: Claude Opus 4.8 --- modules/checklists/pro-requirements.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/checklists/pro-requirements.php b/modules/checklists/pro-requirements.php index bafd4d4f..5a7d77f2 100644 --- a/modules/checklists/pro-requirements.php +++ b/modules/checklists/pro-requirements.php @@ -123,6 +123,13 @@ 'group' => 'content', 'label' => __('Prohibited words', 'publishpress-checklists'), ], + [ + 'id' => 'required_words', + 'type' => 'multiple', + 'support' => 'editor', + 'group' => 'content', + 'label' => __('Required words', 'publishpress-checklists'), + ], [ 'id' => 'publish_time_exact', 'type' => 'time', From e065aced2dedd92fee98716b3c5e70ceaf0b4a5d Mon Sep 17 00:00:00 2001 From: Steve Burge Date: Wed, 26 Aug 2026 13:26:52 -0400 Subject: [PATCH 02/52] fix: count internal/external links against the site's real host, not the admin host Resolves #439. On sites where the front-end runs on a different host than wp-admin (e.g. website.com vs wp.website.com), the internal-links editor check counted zero internal links, no matter the content: JS extract_internal_links()/extract_external_links() defaulted their host to window.location.host, which in wp-admin is the admin host, never the host content actually links to. - localize the site's real hosts (home_url + site_url) as ppChecklists.internal_link_hosts and match against all of them, in both the editor JS and the PHP-side validation, via a new publishpress_checklists_internal_link_hosts filter for setups where the front-end is neither home_url() nor site_url(). - fix Internal_links::extract_internal_links(): `strpos(...) == false` is true when a link starts with the host (strpos returns 0), so such links were wrongly excluded. Changed to `!== false`. Passing an explicit $website/website still restricts matching to that one host, so existing callers of these methods are unaffected. Co-Authored-By: Claude Opus 4.8 --- core/Requirement/External_links.php | 27 ++++++++++++--- core/Requirement/Internal_links.php | 27 ++++++++++++--- modules/checklists/assets/js/meta-box.js | 43 +++++++++++++++++++----- modules/checklists/checklists.php | 33 ++++++++++++++++++ 4 files changed, 112 insertions(+), 18 deletions(-) diff --git a/core/Requirement/External_links.php b/core/Requirement/External_links.php index 41643469..e2364577 100644 --- a/core/Requirement/External_links.php +++ b/core/Requirement/External_links.php @@ -100,9 +100,19 @@ public function get_current_status($post, $option_value) */ public function extract_external_links($content, $external_links = array(), $website = '') { - //website host - if (!$website) { - $website = parse_url(home_url())['host']; + //website host(s). A single $website keeps working for backward + //compatibility with existing callers; otherwise fall back to every + //host the site is reachable on (front-end and wp-admin can differ). + if ($website) { + $websites = [$website]; + } else { + $websites = apply_filters( + 'publishpress_checklists_internal_link_hosts', + array_values(array_unique(array_filter([ + parse_url(home_url(), PHP_URL_HOST), + parse_url(site_url(), PHP_URL_HOST), + ]))) + ); } //remove images from content @@ -120,8 +130,15 @@ public function extract_external_links($content, $external_links = array(), $web if (in_array($current_extension, $image_extension)) { continue; } - //skip if link point to the current website host - if (strpos($current_link, $website) !== false) { + //skip if link points to one of the site's own hosts + $is_internal_link = false; + foreach ($websites as $host) { + if ($host !== '' && strpos($current_link, $host) !== false) { + $is_internal_link = true; + break; + } + } + if ($is_internal_link) { continue; } //add valid link to array diff --git a/core/Requirement/Internal_links.php b/core/Requirement/Internal_links.php index 5ed067db..327f980a 100644 --- a/core/Requirement/Internal_links.php +++ b/core/Requirement/Internal_links.php @@ -113,9 +113,19 @@ public function extract_internal_links( $internal_links = array(), $website = '' ) { - //website host - if (!$website) { - $website = parse_url(home_url())['host']; + //website host(s). A single $website keeps working for backward + //compatibility with existing callers; otherwise fall back to every + //host the site is reachable on (front-end and wp-admin can differ). + if ($website) { + $websites = [$website]; + } else { + $websites = apply_filters( + 'publishpress_checklists_internal_link_hosts', + array_values(array_unique(array_filter([ + parse_url(home_url(), PHP_URL_HOST), + parse_url(site_url(), PHP_URL_HOST), + ]))) + ); } //remove images from content @@ -207,8 +217,15 @@ function ($match) use (&$links) { if (in_array($current_extension, $image_extension)) { continue; } - //skip if link has different host than current website - if (strpos($current_link, $website) == false) { + //skip if link doesn't match any of the site's hosts + $is_internal_link = false; + foreach ($websites as $host) { + if ($host !== '' && strpos($current_link, $host) !== false) { + $is_internal_link = true; + break; + } + } + if (!$is_internal_link) { continue; } //add valid link to array diff --git a/modules/checklists/assets/js/meta-box.js b/modules/checklists/assets/js/meta-box.js index 16251434..2058c882 100644 --- a/modules/checklists/assets/js/meta-box.js +++ b/modules/checklists/assets/js/meta-box.js @@ -624,6 +624,27 @@ return is_valid; }, + /** + * Returns the hosts that should be treated as internal for the + * internal/external links checks, localized server-side as + * ppChecklists.internal_link_hosts. Falls back to the host this script + * is running on if that wasn't localized (e.g. an older cached copy of + * the object), matching the previous single-host behaviour. + * + * @return {Array} + */ + get_internal_link_hosts: function () { + if ( + typeof ppChecklists !== 'undefined' && + Array.isArray(ppChecklists.internal_link_hosts) && + ppChecklists.internal_link_hosts.length > 0 + ) { + return ppChecklists.internal_link_hosts; + } + + return [window.location.host]; + }, + /** * Check for internal link from content and return result as array * @@ -634,11 +655,15 @@ * * @param {String} content * @param {Array} links - * @param {String} website + * @param {Array} websites Hosts treated as internal. Defaults to the + * site's real hosts (localized as + * ppChecklists.internal_link_hosts), which is + * not necessarily the host this admin script is + * running on. * * @return {Array} */ - extract_internal_links: function (content, links = [], website = window.location.host) { + extract_internal_links: function (content, links = [], websites = PP_Checklists.get_internal_link_hosts()) { var link; if (content) { //remove image inside tags so we don't count them as link @@ -656,8 +681,8 @@ for (link of content) { //skip if link is image if (link.match(/\.(jpeg|jpg|gif|png|svg)$/)) continue; - //skip if link has different host than current website - if (link.indexOf(website) < 0) continue; + //skip if link doesn't match any of the site's hosts + if (!websites.some(function (website) { return link.indexOf(website) >= 0; })) continue; //add valid link to array links.push(link); } @@ -677,11 +702,13 @@ * * @param {String} content * @param {Array} links - * @param {String} website + * @param {Array} websites Hosts treated as internal (i.e. excluded from + * the external count). Defaults to the site's + * real hosts, see get_internal_link_hosts(). * * @return {Array} */ - extract_external_links: function (content, links = [], website = window.location.host) { + extract_external_links: function (content, links = [], websites = PP_Checklists.get_internal_link_hosts()) { var link, match, regex = /(.*?)\<\/a\>/gi; @@ -692,8 +719,8 @@ //skip if link is image if (link.match(/\.(jpeg|jpg|gif|png|svg)$/)) continue; - //skip if link point to the current website host - if (link.indexOf(website) > 0) continue; + //skip if link points to one of the site's own hosts + if (websites.some(function (website) { return link.indexOf(website) > 0; })) continue; //add valid link to array links.push(link); } diff --git a/modules/checklists/checklists.php b/modules/checklists/checklists.php index ad697390..fdbfa498 100644 --- a/modules/checklists/checklists.php +++ b/modules/checklists/checklists.php @@ -993,6 +993,7 @@ public function display_meta_box($post) 'complete' => $complete_icon, 'incomplete' => $incomplete_icon, ], + 'internal_link_hosts' => $this->get_internal_link_hosts(), ] ); @@ -1037,6 +1038,38 @@ public function display_meta_box($post) ); } + /** + * Returns the hosts that should be treated as "internal" for the + * internal/external links checklist items. Includes the front-end + * site host and the host WordPress core is installed on, since they + * can differ (e.g. a front-end domain separate from wp-admin). + * + * @return array + */ + private function get_internal_link_hosts() + { + $hosts = []; + + foreach ([home_url(), site_url()] as $url) { + $host = parse_url($url, PHP_URL_HOST); + if (!empty($host)) { + $hosts[] = $host; + } + } + + $hosts = array_values(array_unique($hosts)); + + /** + * Filters the hosts treated as "internal" for the internal and + * external links checklist items. Useful when the front-end runs + * on a host that is neither home_url() nor site_url(), such as a + * headless or multi-domain setup. + * + * @param array $hosts + */ + return apply_filters('publishpress_checklists_internal_link_hosts', $hosts); + } + private function is_gutenberg_active() { $gutenberg = false; From 620080488f529582ddeb07e835ee4436ea037f5e Mon Sep 17 00:00:00 2001 From: Steve Burge Date: Wed, 26 Aug 2026 13:40:31 -0400 Subject: [PATCH 03/52] feat: show a live count next to counter checklist tasks 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 --- .../assets/css/post-editor-checklists.css | 7 ++ modules/checklists/assets/js/meta-box.js | 103 ++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/modules/checklists/assets/css/post-editor-checklists.css b/modules/checklists/assets/css/post-editor-checklists.css index 16d563ba..a82d6327 100644 --- a/modules/checklists/assets/css/post-editor-checklists.css +++ b/modules/checklists/assets/css/post-editor-checklists.css @@ -305,3 +305,10 @@ ul#pp-checklists-req-box span.required { pointer-events: none; filter: drop-shadow(0 4px 8px rgba(223, 0, 0, 0.18)); } + +.pp-checklists-live-count { + font-weight: normal; + opacity: 0.65; + font-size: 0.9em; + margin-left: 2px; +} diff --git a/modules/checklists/assets/js/meta-box.js b/modules/checklists/assets/js/meta-box.js index 16251434..dac9d82f 100644 --- a/modules/checklists/assets/js/meta-box.js +++ b/modules/checklists/assets/js/meta-box.js @@ -624,6 +624,78 @@ return is_valid; }, + /** + * Formats a live count value for display next to a counter task, + * showing progress against the configured min/max when one is set. + * + * @param {Number} count + * @param {Number} min_value + * @param {Number} max_value + * + * @return {String} + */ + format_live_count: function (count, min_value, max_value) { + min_value = parseInt(min_value) || 0; + max_value = parseInt(max_value) || 0; + + if (max_value > 0) { + return count + ' / ' + max_value; + } + + if (min_value > 0) { + return count + ' (min ' + min_value + ')'; + } + + return String(count); + }, + + /** + * Shows the current live value next to a counter task, both in the + * metabox list item and in the requirement's label so the Gutenberg + * sidebar (which renders ppChecklists.requirements[id].label) picks it + * up too. The original label is cached so repeated updates replace the + * badge instead of stacking it. + * + * @param {String} requirementId + * @param {String} displayText + * + * @return {void} + */ + update_live_count: function (requirementId, displayText) { + var req = (typeof ppChecklists !== 'undefined') ? ppChecklists.requirements[requirementId] : null; + + if (!req) { + return; + } + + if (typeof req.pp_base_label === 'undefined') { + req.pp_base_label = req.label; + } + + req.label = req.pp_base_label + ' (' + displayText + ')'; + + var $statusLabel = $('#pp-checklists-req-' + requirementId).find('.status-label').first(); + + if ($statusLabel.length === 0) { + return; + } + + var $badge = $statusLabel.find('.pp-checklists-live-count'); + + if ($badge.length === 0) { + $badge = $(''); + var $required = $statusLabel.find('.required').first(); + + if ($required.length > 0) { + $badge.insertBefore($required); + } else { + $statusLabel.append($badge); + } + } + + $badge.text('(' + displayText + ')'); + }, + /** * Check for internal link from content and return result as array * @@ -1182,6 +1254,11 @@ if (typeof obj !== 'undefined') { count = obj.length; + PP_Checklists.update_live_count( + requirementId, + PP_Checklists.format_live_count(count, min_value, max_value), + ); + $element.trigger( PP_Checklists.EVENT_UPDATE_REQUIREMENT_STATE, PP_Checklists.check_valid_quantity(count, min_value, max_value), @@ -1364,6 +1441,11 @@ if (typeof obj !== 'undefined') { count = obj.length; + PP_Checklists.update_live_count( + requirementId, + PP_Checklists.format_live_count(count, min_value, max_value), + ); + $element.trigger( PP_Checklists.EVENT_UPDATE_REQUIREMENT_STATE, PP_Checklists.check_valid_quantity(count, min_value, max_value), @@ -1542,6 +1624,7 @@ if (lastHierCounts[requirementId] === count) { return; } + PP_Checklists.update_live_count(requirementId, PP_Checklists.format_live_count(count, min, max)); $element.trigger( PP_Checklists.EVENT_UPDATE_REQUIREMENT_STATE, PP_Checklists.check_valid_quantity(count, min, max), @@ -1589,6 +1672,7 @@ if (lastNonHierCounts[requirementId] === count) { return; } + PP_Checklists.update_live_count(requirementId, PP_Checklists.format_live_count(count, min, max)); $element.trigger( PP_Checklists.EVENT_UPDATE_REQUIREMENT_STATE, PP_Checklists.check_valid_quantity(count, min, max), @@ -1636,6 +1720,8 @@ var min = parseInt(config.value[0]); var max = parseInt(config.value[1]); + PP_Checklists.update_live_count(requirementId, PP_Checklists.format_live_count(count, min, max)); + $element.trigger( PP_Checklists.EVENT_UPDATE_REQUIREMENT_STATE, PP_Checklists.check_valid_quantity(count, min, max), @@ -1677,6 +1763,11 @@ if (typeof obj !== 'undefined') { count = obj.length; + PP_Checklists.update_live_count( + requirementId, + PP_Checklists.format_live_count(count, min_value, max_value), + ); + $element.trigger( PP_Checklists.EVENT_UPDATE_REQUIREMENT_STATE, PP_Checklists.check_valid_quantity(count, min_value, max_value), @@ -1724,6 +1815,8 @@ 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)); + $element.trigger( PP_Checklists.EVENT_UPDATE_REQUIREMENT_STATE, PP_Checklists.check_valid_quantity(count, min, max), @@ -1781,6 +1874,8 @@ 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)); + $element.trigger( PP_Checklists.EVENT_UPDATE_REQUIREMENT_STATE, PP_Checklists.check_valid_quantity(count, min, max), @@ -1855,6 +1950,8 @@ 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)); + $element.trigger( PP_Checklists.EVENT_UPDATE_REQUIREMENT_STATE, PP_Checklists.check_valid_quantity(count, min, max), @@ -1916,6 +2013,8 @@ 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)); + $element.trigger( PP_Checklists.EVENT_UPDATE_REQUIREMENT_STATE, PP_Checklists.check_valid_quantity(count, min, max), @@ -2054,6 +2153,8 @@ 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)); + $element.trigger( PP_Checklists.EVENT_UPDATE_REQUIREMENT_STATE, PP_Checklists.check_valid_quantity(count, min, max), @@ -2112,6 +2213,8 @@ 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)); + $element.trigger( PP_Checklists.EVENT_UPDATE_REQUIREMENT_STATE, PP_Checklists.check_valid_quantity(count, min, max), From 9a5ddb49619a0c7d9853eda5069ebec2689c61f2 Mon Sep 17 00:00:00 2001 From: Steve Burge Date: Wed, 26 Aug 2026 13:42:50 -0400 Subject: [PATCH 04/52] feat: allow multi-line titles for custom checklist tasks Resolves #453. Custom task titles were a single-line , so longer instructions had to be squeezed onto one line. - Custom_item::get_setting_title_html() now renders a ', $name, - esc_attr($this->get_title()), esc_attr($this->name), - esc_html__('Enter name of custom task', 'publishpress-checklists') + esc_html__('Enter name of custom task', 'publishpress-checklists'), + esc_textarea($this->get_title()) ); $html .= sprintf( diff --git a/modules/checklists/assets/css/post-editor-checklists.css b/modules/checklists/assets/css/post-editor-checklists.css index 16d563ba..8c22cc18 100644 --- a/modules/checklists/assets/css/post-editor-checklists.css +++ b/modules/checklists/assets/css/post-editor-checklists.css @@ -213,6 +213,7 @@ ul#pp-checklists-req-box span.required { .pp-checklists-req.pp-checklists-custom-item .status-label { padding-left: 1px; + white-space: pre-line; } #pp-checklists-sidebar-content .pp-checklists-req.pp-checklists-custom-item .status-label { diff --git a/modules/checklists/assets/js/global-checklists.js b/modules/checklists/assets/js/global-checklists.js index 33c134ee..7d8f68e3 100644 --- a/modules/checklists/assets/js/global-checklists.js +++ b/modules/checklists/assets/js/global-checklists.js @@ -277,7 +277,7 @@ var $table = $('#pp-checklists-requirements'), $tr = $(''), $td = null, - $titleField = type == 'openai' ? $('', - $name, + '', + esc_attr($id), + esc_html__('OpenAI task prompt', 'publishpress-checklists'), + esc_attr($id), + esc_attr($name), esc_attr($this->name), esc_html__('Enter OpenAI task prompt', 'publishpress-checklists'), esc_attr($this->get_title()) @@ -142,9 +146,10 @@ public function get_setting_field_html($css_class = '') $html = parent::get_setting_field_html(esc_attr($css_class)); $html .= sprintf( - '', + '', esc_attr($this->name), - __('Remove', 'publishpress-checklists') + __('Remove', 'publishpress-checklists'), + __('Remove custom task', 'publishpress-checklists') ); return $html; diff --git a/core/Requirement/Pro_Requirement.php b/core/Requirement/Pro_Requirement.php index 041a3532..48f0008b 100644 --- a/core/Requirement/Pro_Requirement.php +++ b/core/Requirement/Pro_Requirement.php @@ -95,10 +95,11 @@ public function get_setting_field_html($css_class = '') if ($this->config['type'] === 'time') { $value = get_post_meta(get_the_ID(), $this->config['field_key'], true); return sprintf( - '', + '', esc_attr($this->config['field_key']), esc_attr($value), - esc_attr($css_class) + esc_attr($css_class), + esc_attr(sprintf(__('Set time for %s', 'publishpress-checklists'), $this->get_label())) ); } if ($this->config['type'] === 'counter') { @@ -107,19 +108,21 @@ public function get_setting_field_html($css_class = '') $max = $this->config['max'] ?? ''; $id_key = esc_attr($this->config['id']); return sprintf( - '
' . - '
', + '
' . + '
', esc_html__('Min', 'publishpress-checklists'), esc_html__('Max', 'publishpress-checklists'), esc_attr($min), esc_attr($css_class), - esc_attr($max) + esc_attr($max), + $id_key ); } if ($this->config['type'] === 'multiple') { return sprintf( - '', - esc_attr($css_class) + '', + esc_attr($css_class), + esc_attr(sprintf(__('Select options for %s', 'publishpress-checklists'), $this->get_label())) ); } return parent::get_setting_field_html($css_class); diff --git a/modules/checklists/assets/css/admin-pages.css b/modules/checklists/assets/css/admin-pages.css index 7362e5b0..ee2d5739 100644 --- a/modules/checklists/assets/css/admin-pages.css +++ b/modules/checklists/assets/css/admin-pages.css @@ -238,6 +238,23 @@ textarea.pp-checklists-custom-item-title { line-height: 20px; } +.pp-custom-suggestion button { + border: 0; + padding: 0; + background: transparent; + color: #2271b1; + cursor: pointer; + font: inherit; + text-decoration: underline; +} + +.pp-custom-suggestion button:focus-visible, +.pp-checklists-remove-custom-item:focus-visible, +.pp-checklists-edit-label:focus-visible { + outline: 2px solid #2271b1; + outline-offset: 2px; +} + .pp-custom-checklists-table { margin-top: 10px; } @@ -326,6 +343,12 @@ textarea.pp-checklists-custom-item-title { position: absolute; top: 10px; right: 10px; + border: 0; + padding: 0; + background: transparent; + color: #b32d2e; + cursor: pointer; + font: inherit; } .pp-checklists-requirement-row { @@ -417,6 +440,12 @@ textarea.pp-checklists-custom-item-title { border-bottom: 1px solid #eee; } +.pp-checklists-tabs-wrapper .pp-checklists-tabs ul li a[role="tab"]:focus-visible, +#pp-checklists-post-type-filter a[role="tab"]:focus-visible { + outline: 2px solid #2271b1; + outline-offset: -2px; +} + .pp-checklists-tabs-wrapper .pp-checklists-tabs ul li { margin: 0; padding: 0; @@ -663,6 +692,9 @@ textarea.pp-checklists-custom-item-title { width: 24px; height: 24px; color: #2271b1; + border: 0; + padding: 0; + background: transparent; text-decoration: none; flex-shrink: 0; cursor: pointer; diff --git a/modules/checklists/assets/js/global-checklists.js b/modules/checklists/assets/js/global-checklists.js index 33c134ee..a9b16522 100644 --- a/modules/checklists/assets/js/global-checklists.js +++ b/modules/checklists/assets/js/global-checklists.js @@ -55,14 +55,14 @@ }); // Set the event for the post type filter - $('#pp-checklists-post-type-filter a').on('click', function (event) { + $('#pp-checklists-post-type-filter a[role="tab"]').on('click', function (event) { event.preventDefault(); // Hide all requirements except the first one (title) $('.pp-checklists-requirement-row:not(.ppch-title-group)').hide(); - var $target = $(event.toElement || event.target), - post_type = $target.attr('href').substring(1); + var $target = $(event.currentTarget), + post_type = ($target.attr('href') || '').substring(1); // Save the selected post type if (typeof saveToStorage === 'function') { @@ -72,6 +72,33 @@ show_post_type_requirements(post_type); }); + $('#pp-checklists-post-type-filter a[role="tab"]').on('keydown', function (event) { + var $tabs = $('#pp-checklists-post-type-filter a[role="tab"]'); + var currentIndex = $tabs.index(event.currentTarget); + var targetIndex = currentIndex; + + if (event.key === 'Enter' || event.key === ' ' || event.keyCode === 13 || event.keyCode === 32) { + event.preventDefault(); + $(event.currentTarget).trigger('click'); + return; + } + + if (event.key === 'ArrowRight' || event.key === 'ArrowDown' || event.keyCode === 39 || event.keyCode === 40) { + targetIndex = (currentIndex + 1) % $tabs.length; + } else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp' || event.keyCode === 37 || event.keyCode === 38) { + targetIndex = (currentIndex - 1 + $tabs.length) % $tabs.length; + } else if (event.key === 'Home' || event.keyCode === 36) { + targetIndex = 0; + } else if (event.key === 'End' || event.keyCode === 35) { + targetIndex = $tabs.length - 1; + } else { + return; + } + + event.preventDefault(); + $tabs.eq(targetIndex).trigger('click').focus(); + }); + // Set the mask for settings fields $('.pp-checklists-number').on('keypress', function (event) { var key = event.keyCode || event.which; @@ -154,34 +181,45 @@ * @param {string} post_type */ function show_post_type_requirements(post_type) { + var $postTypeTab = $('#pp-checklists-post-type-filter a[href="#' + post_type + '"]'); + if (!$postTypeTab.length) { + $postTypeTab = $('#pp-checklists-post-type-filter a[role="tab"]').first(); + post_type = ($postTypeTab.attr('href') || '').substring(1); + } + // Mark the filter as selected $('#pp-checklists-post-type-filter li.nav-tab-active').removeClass('nav-tab-active'); $('#pp-checklists-post-type-filter li.post-type-' + post_type).addClass('nav-tab-active'); + $('#pp-checklists-post-type-filter a[role="tab"]') + .attr('aria-selected', 'false') + .attr('tabindex', '-1'); + $postTypeTab.attr('aria-selected', 'true').attr('tabindex', '0'); - $('.pp-checklists-tabs-list li a').removeClass('active'); + $('.pp-checklists-tabs-list li a').removeClass('active').attr('aria-selected', 'false').attr('tabindex', '-1'); - //remove active class from all tabs - $('.pp-checklists-tabs a').removeClass('active'); - - $('.pp-checklists-tabs ul').removeClass('active'); - $('.pp-checklists-tabs ul#list-' + post_type).addClass('active'); + var $tabList = $('.pp-checklists-tabs ul').removeClass('active').attr('aria-hidden', 'true').filter('#list-' + post_type); + $tabList.addClass('active').attr('aria-hidden', 'false'); //add active class to title tab - $('.pp-checklists-tabs li:first-child a').addClass('active'); + var $firstInnerTab = $tabList.find('a[role="tab"]').first(); + $firstInnerTab.addClass('active').attr('aria-selected', 'true').attr('tabindex', '0'); - $('.pp-checklists-tab-body').hide(); - $('#pp-checklists-tab-body-' + post_type).show(); + $('.pp-checklists-tab-body').hide().attr('aria-hidden', 'true'); + $('#pp-checklists-tab-body-' + post_type) + .show() + .attr('aria-hidden', 'false') + .attr('aria-labelledby', $postTypeTab.attr('id')); const current_data_tab = $('#list-' + post_type) .find('li a.active') .attr('data-tab') || 'title'; // Hide the requirements which are not for the current post type - $('#pp-checklists-requirements tr.pp-checklists-requirement-row').hide(); + $('#pp-checklists-requirements tr.pp-checklists-requirement-row').hide().attr('aria-hidden', 'true'); // Display the correct requirements $( '#pp-checklists-requirements tr.ppch-' + current_data_tab + '-group[data-post-type="' + post_type + '"]', - ).show(); + ).show().attr('aria-hidden', 'false'); } /** @@ -190,15 +228,19 @@ * @return string */ function get_current_post_type() { - var post_type = $('#pp-checklists-post-type-filter li.nav-tab-active a').attr('href').substring(1); + var post_type = ($('#pp-checklists-post-type-filter li.nav-tab-active a').attr('href') || '').substring(1); - if (post_type === '' || post_type === false || post_type === null || typeof post_type === undefined) { + if (!post_type) { post_type = objectL10n_checklists_global_checklist.first_post_type; } return post_type; } + function announceStatus(message) { + $('#pp-checklists-status').text(message); + } + /** * Update count indicators for all tabs based on current requirement settings */ @@ -251,6 +293,7 @@ .appendTo($('#pp-checklists-requirements')); $('tr[data-id="' + id + '"]').remove(); + announceStatus(objectL10n_checklists_global_checklist[type + '_task_removed']); } /** @@ -260,7 +303,7 @@ * @param {Event} event */ function callback_remove_row(event) { - var $target = $(event.target); + var $target = $(event.currentTarget); remove_row($target.data('id'), $target.data('type')); } @@ -309,12 +352,14 @@ // Title cell $titleField .attr('name', 'publishpress_checklists_checklists_options[' + id + '_title][' + post_type + ']') + .attr('id', post_type + '-checklists-' + id + '_title') .val(title) .addClass('pp-checklists-custom-item-title') - .focus() .attr('data-id', id) + .attr('aria-label', objectL10n_checklists_global_checklist[type + '_task_title']) .attr('placeholder', objectL10n_checklists_global_checklist[type + '_enter_name']) - .appendTo($td); + .appendTo($td) + .focus(); // Suggestion if (typeof $suggestionsObject !== 'undefined') { @@ -324,13 +369,13 @@ for (var key in $suggestionsObject) { if ($suggestionsObject.hasOwnProperty(key)) { $suggestionItem.append( - '• ' + $suggestionsObject[key].label + - ' ', + ' ', ); } } @@ -342,6 +387,7 @@ $actionField .attr('name', 'publishpress_checklists_checklists_options[' + id + '_rule][' + post_type + ']') .attr('data-id', id) + .attr('aria-label', objectL10n_checklists_global_checklist.custom_task_action) .appendTo($td); $.each(objectL10n_checklists_global_checklist.rules, function (value, label) { @@ -354,6 +400,7 @@ .attr('class', 'pp-checklists-can-ignore') .attr('name', 'publishpress_checklists_checklists_options[' + id + '_can_ignore][' + post_type + '][]') .attr('multiple', 'multiple') + .attr('aria-label', objectL10n_checklists_global_checklist.custom_task_roles) .appendTo($td); $option = $('').appendTo($canIgnoreField); @@ -369,6 +416,7 @@ .attr('id', '' + post_type + '-checklists-' + id + '_editable_by') .attr('name', 'publishpress_checklists_checklists_options[' + id + '_editable_by][' + post_type + '][]') .attr('multiple', 'multiple') + .attr('aria-label', objectL10n_checklists_global_checklist.custom_task_editable_by) .appendTo($td); $option = $('').appendTo($optionsField); @@ -382,14 +430,14 @@ $optionsField.after($label); } - $a = $('') - .attr('href', 'javascript:void(0);') + $a = $('