diff --git a/CHANGELOG.md b/CHANGELOG.md index b83dea8..7681ca8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +### 2.0.0-dev - TBD + +- Renamed phpBB Topic Prefixes to phpBB Topic Tags and replaced single text prefixes embedded in titles with relational topic metadata. +- Added multiple color-coded tags per topic with automatic badge text contrast and badges throughout forum, topic, MCP, and search displays. +- Added centralized ACP management for tag text, color, order, status, and availability across multiple forums. +- Redesigned ACP management with a responsive tag catalog, sticky editor, searchable forum assignments, drag-and-drop ordering, and AJAX updates. +- Added accessible tag selection when creating topics or editing their first posts. +- Added clickable filters that combine multiple tags using AND matching in forum lists, category Active Topics, and SQL-backed keyword or author search results shown as topics or posts. +- Preserved tags through topic moves, splits, forks, and merges, including tags retained after moving into forums where they are unavailable for new assignments. +- Added a safe, restartable upgrade that converts legacy prefixes and assignments into tags while removing matching prefix text from topic titles and first-post subjects. +- Added automatic splitting of complete adjacent bracket sequences and consolidation of duplicate legacy definitions with matching names and enabled states, while preserving malformed formats and same-name definitions with conflicting states. +- Required legacy upgrades to run with the board disabled and directed administrators to back up the database first. +- Added an interactive CLI for splitting tags into multiple separate tags or merging one tag into another. +- Raised minimum requirements to phpBB 3.3.5 and PHP 7.2. +- Note: implementations for showing tags on UCP main page, bookmarks, and subscriptions will take effect after phpBB adds new proposed events (targeted for 3.3.19). + ### 1.0.2 - 2025-12-17 - Strengthened compatibility with both phpBB3 and phpBB4-alpha-1. diff --git a/README.md b/README.md index 1d07044..3df366b 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,49 @@ -# phpBB Topic Prefixes +# phpBB Topic Tags -This is the repository for the development of the Topic Prefixes phpBB extension. +This is the repository for development of phpBB Topic Tags, formerly named Topic Prefixes. [![Build Status](https://github.com/phpbb-extensions/topicprefixes/actions/workflows/tests.yml/badge.svg)](https://github.com/phpbb-extensions/topicprefixes/actions) [![codecov](https://codecov.io/gh/phpbb-extensions/topicprefixes/branch/master/graph/badge.svg?token=Dc0GWOeQWj)](https://codecov.io/gh/phpbb-extensions/topicprefixes) -The phpBB Topic Prefixes extension allows phpBB board administrators to create topic prefix tags that users can easily add to their topic titles. Features include: +The phpBB Topic Tags extension provides administrator-curated topic tags. Tags are structured topic metadata and are displayed separately from topic titles as colored badges. Features include: -- Create unique and unlimited prefixes for each forum in your phpBB board. -- Admins can sort, enable and disable prefix tags. -- Users will see a topic prefix menu when creating a new topic in a forum that uses topic prefixes. -- Topic prefixes can easily be changed when a user is editing their original first post. -- Because the topic prefix is added directly to the topic title, if an admin disables or deletes a topic prefix, existing topic titles using the deleted prefix will not be changed. +- Create, edit, order, enable, disable, and color topic tags. +- Make each tag available in one or more forums. +- Assign multiple tags while creating a topic or editing its first post. +- Display readable colored badges in topic lists, topic pages, and search results. +- Filter forum and category Active Topics lists by one or more tags using AND semantics. +- Filter keyword and author search results shown as topics or posts when using phpBB native, MySQL fulltext, or PostgreSQL search; Sphinx search remains display-only. +- Upgrade existing title prefixes safely into relational tag assignments. - This is the same extension currently in use at phpbb.com in the Extensions and Styles in development forums. +## Upgrading from 1.x + +Before enabling version 2, make a full database backup and disable the board in +General > Board settings. The extension blocks a legacy upgrade while the board +is enabled. + +Migration splits complete adjacent bracket sequences such as `[3.3][DEV]` into +separate tags. Legacy prefixes duplicated within or across multiple forums are +consolidated into one tag available in each applicable forum when their decoded +names and enabled states match. Other combined formats and same-name definitions with +different enabled states remain separate. This upgrade is one-way; restore the +pre-upgrade database backup to undo it. + +## Repairing topic tags + +Use the interactive command to split one tag into multiple separate tags or +merge one tag into another existing tag. Splitting can repair combined formats +that could not be handled automatically, such as `(A)(B)` or `A|B`: + +```shell +php bin/phpbbcli.php topicprefixes:repair-tags +``` + +Use `--tag-id=ID` to repair one source tag. Back up the database and disable the +board before running the command; the command enforces both an interactive +backup confirmation and maintenance mode. Every proposed change is previewed +and must be confirmed before any data changes. + 📦 [Download](https://www.phpbb.com/customise/db/extension/topicprefixes/) the latest release of this extension. 🐞 [Report bugs](https://github.com/phpbb-extensions/topicprefixes/issues) to our Issue Tracker. diff --git a/acp/topic_prefixes_module.php b/acp/topic_prefixes_module.php index e13021b..d3b45f7 100644 --- a/acp/topic_prefixes_module.php +++ b/acp/topic_prefixes_module.php @@ -33,7 +33,7 @@ public function main() $lang->add_lang('acp/forums'); $lang->add_lang('acp_topic_prefixes', 'phpbb/topicprefixes'); $this->tpl_name = 'acp_topic_prefixes'; - $this->page_title = 'TOPIC_PREFIXES'; + $this->page_title = 'TOPIC_TAGS'; $admin_controller = $phpbb_container->get('phpbb.topicprefixes.admin_controller'); $admin_controller->set_u_action($this->u_action)->main(); diff --git a/adm/style/acp_topic_prefixes.css b/adm/style/acp_topic_prefixes.css index 3c73b32..aacc130 100644 --- a/adm/style/acp_topic_prefixes.css +++ b/adm/style/acp_topic_prefixes.css @@ -1,12 +1,725 @@ -.tp-toggle-on, -.tp-toggle-off { - font-weight: 900; +.topic-tag-app { + --tp-accent: #105289; + --tp-border: #D6D9DC; + --tp-muted: #66727D; + --tp-panel: #FFFFFF; + --tp-soft: #F4F6F8; } -.tp-toggle-on:before { - content: "\f205"; +.topic-tag-app [hidden] { + display: none !important; } -.tp-toggle-off:before { - content: "\f204"; +.topic-tag-app *, +.topic-tag-app *::before, +.topic-tag-app *::after { + box-sizing: border-box; +} + +.topic-tag-page-header, +.topic-tag-catalog-header, +.topic-tag-inspector-header, +.topic-tag-forum-header, +.topic-tag-enabled-field { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.topic-tag-page-header h1, +.topic-tag-catalog-header h2, +.topic-tag-inspector-header h2 { + margin: 0; +} + +.topic-tag-page-header p, +.topic-tag-catalog-header p { + margin: 4px 0 0; +} + +.topic-tag-new { + flex: 0 0 auto; +} + +.topic-tag-form-error { + border-radius: 4px; + margin: 12px 0; + padding: 10px 12px; +} + +.topic-tag-notice { + align-items: center; + background: #E7F3E7; + border: 1px solid #8CC68C; + border-radius: 6px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + color: #234D20; + display: flex; + gap: 12px; + max-width: min(380px, calc(100vw - 32px)); + opacity: 0; + padding: 11px 12px 11px 14px; + pointer-events: none; + position: fixed; + right: 16px; + top: 16px; + transform: translateY(-12px) scale(0.98); + transition: opacity 160ms ease, transform 160ms ease; + z-index: 10000; +} + +.topic-tag-notice.is-visible { + opacity: 1; + pointer-events: auto; + transform: translateY(0) scale(1); +} + +#topic-tag-notice-message { + flex: 1 1 auto; +} + +.topic-tag-notice.is-error, +.topic-tag-form-error { + background: #FBE9EC; + border: 1px solid #D7A0AA; + color: #8A1F36; +} + +.topic-tag-notice-close { + background: transparent !important; + border-color: transparent !important; + color: currentColor !important; + flex: 0 0 auto; +} + +.tp-button { + align-items: center; + appearance: none; + background: #FFFFFF; + border: 1px solid #BCC4CB; + border-radius: 6px; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.06); + color: #34424E !important; + cursor: pointer; + display: inline-flex; + font-family: inherit; + font-size: 12px; + font-weight: 600; + gap: 6px; + height: 32px; + justify-content: center; + line-height: 1; + margin: 0; + padding: 0 12px; + text-decoration: none !important; + transition: background-color 120ms ease, border-color 120ms ease, box-shadow 120ms ease, color 120ms ease, transform 120ms ease; + vertical-align: middle; +} + +.tp-button:hover { + background: #F5F8FA; + border-color: #8C99A3; + color: #1E2B35 !important; + text-decoration: none !important; +} + +.tp-button:active { + box-shadow: none; + transform: translateY(1px); +} + +.tp-button:focus, +.tp-button:focus-visible { + border-color: var(--tp-accent); + box-shadow: 0 0 0 3px rgba(16, 82, 137, 0.2); + outline: none; + text-decoration: none !important; +} + +.tp-button[disabled], +.tp-button[aria-disabled="true"] { + cursor: not-allowed; + opacity: 0.48; + pointer-events: none; +} + +.tp-button-primary { + background: linear-gradient(#1C6AA5, #105289); + border-color: #0C4677; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.16); + color: #FFFFFF !important; +} + +.tp-button-primary:hover, +.tp-button-primary:focus { + background: linear-gradient(#2377B5, #0E4777); + border-color: #083B65; + color: #FFFFFF !important; +} + +.tp-button-secondary { + background: #FFFFFF; +} + +.tp-button-icon { + flex: 0 0 30px; + height: 30px; + padding: 0; + width: 30px; +} + +.tp-button-icon .icon, +.tp-button-icon .fa { + margin: 0; +} + +.tp-button-quiet { + background: transparent; + border-color: transparent; + box-shadow: none; +} + +.topic-tag-workspace { + align-items: start; + display: grid; + gap: 18px; + grid-template-columns: minmax(0, 3fr) minmax(320px, 2fr); + margin-top: 18px; +} + +.topic-tag-catalog, +.topic-tag-inspector { + background: var(--tp-panel); + border: 1px solid var(--tp-border); + border-radius: 6px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.topic-tag-catalog-header, +.topic-tag-inspector-header { + border-bottom: 1px solid var(--tp-border); + padding: 16px; +} + +.topic-tag-catalog-header { + align-items: flex-start; + flex-wrap: wrap; +} + +.topic-tag-catalog-header h2, +.topic-tag-inspector-header h2 { + color: #26333D; + font-size: 1.25rem; +} + +.topic-tag-catalog-header p, +.topic-tag-help, +.topic-tag-eyebrow, +.topic-tag-forum-summary { + color: var(--tp-muted); +} + +.topic-tag-filters, +.topic-tag-forum-tools, +.topic-tag-color-control { + display: flex; + align-items: center; + gap: 8px; +} + +.topic-tag-filters select, +.topic-tag-filters input, +.topic-tag-forum-tools input, +.topic-tag-color-control input[type="text"] { + margin: 0; +} + +.topic-tag-search { + position: relative; +} + +.topic-tag-search .fa { + color: var(--tp-muted); + left: 9px; + position: absolute; + top: 50%; + transform: translateY(-50%); +} + +.topic-tag-search input { + padding-left: 28px; +} + +.topic-tag-order-hint { + background: #FFF6D8; + border-radius: 3px; + color: #6B5312 !important; + flex-basis: 100%; + padding: 7px 9px; +} + +.topic-tag-list { + min-height: 54px; +} + +.topic-tag-row-template { + display: none !important; +} + +.topic-tag-item { + align-items: center; + border-bottom: 1px solid #E7E9EB; + display: grid; + gap: 10px; + grid-template-columns: 24px minmax(150px, 1fr) auto auto; + min-height: 64px; + padding: 9px 12px; + transition: background-color 120ms ease, box-shadow 120ms ease, opacity 120ms ease; +} + +.topic-tag-item:last-child { + border-bottom: 0; +} + +.topic-tag-item:hover, +.topic-tag-item.is-selected { + background: #F5F9FC; +} + +.topic-tag-item.is-selected { + box-shadow: inset 3px 0 0 var(--tp-accent); +} + +.topic-tag-item.is-disabled .topic-tag-summary { + opacity: 0.7; +} + +.topic-tag-item.is-dragging { + opacity: 0.45; +} + +.topic-tag-item.is-drag-over { + box-shadow: inset 0 3px 0 var(--tp-accent); +} + +.topic-tag-drag { + color: #8B969E; + cursor: grab; + font-size: 14px; +} + +.topic-tag-drag:active { + cursor: grabbing; +} + +.topic-tag-summary { + align-items: flex-start; + color: inherit; + display: flex; + flex-direction: column; + gap: 5px; + min-width: 0; + text-decoration: none; +} + +.topic-tag-summary:hover, +.topic-tag-summary:focus, +.topic-tag-summary:active, +.topic-tag-toggle:hover, +.topic-tag-toggle:focus, +.topic-tag-toggle:active, +.topic-tag-actions a:hover, +.topic-tag-actions a:focus, +.topic-tag-actions a:active { + text-decoration: none !important; +} + +.topic-tag { + background-image: linear-gradient(to top, transparent, rgba(255, 255, 255, 0.35)); + border-radius: 3px; + display: inline-block; + font-weight: 600; + line-height: 1.5; + max-width: 100%; + overflow-wrap: anywhere; + padding: 2px 8px; +} + +.topic-tag-forum-summary { + display: flex; + font-size: 11px; + gap: 7px; + min-width: 0; +} + +.topic-tag-forum-preview { + display: flex; + gap: 4px; + min-width: 0; + overflow: hidden; + white-space: nowrap; +} + +.topic-tag-forum-preview span:not(:last-child)::after { + content: ","; +} + +.topic-tag-toggle { + align-items: center; + color: #48545E; + display: flex; + font-size: 11px; + gap: 6px; + text-decoration: none; +} + +.topic-tag-switch { + background: #8A949C; + border-radius: 10px; + display: inline-block; + height: 18px; + position: relative; + transition: background-color 120ms ease; + width: 32px; +} + +.topic-tag-switch::after { + background: #FFFFFF; + border-radius: 50%; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35); + content: ""; + height: 14px; + left: 2px; + position: absolute; + top: 2px; + transition: transform 120ms ease; + width: 14px; +} + +.topic-tag-toggle[aria-checked="true"] .topic-tag-switch { + background: #2E7D32; +} + +.topic-tag-toggle[aria-checked="true"] .topic-tag-switch::after { + transform: translateX(14px); +} + +.topic-tag-toggle.is-saving, +.topic-tag-actions.is-saving { + opacity: 0.55; + pointer-events: none; +} + +.topic-tag-actions, +.topic-tag-order-actions { + display: flex; + gap: 5px; + white-space: nowrap; +} + +.topic-tag-actions a { + text-decoration: none !important; +} + +.topic-tag-drag-image { + background: #FFFFFF; + border: 1px solid var(--tp-accent); + border-radius: 6px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); + color: #26333D; + font-weight: 600; + left: -10000px; + max-width: 280px; + overflow: hidden; + padding: 10px 14px; + position: fixed; + text-overflow: ellipsis; + top: -10000px; + white-space: nowrap; +} + +.topic-tag-inspector { + max-height: calc(100vh - 24px); + overflow: auto; + position: sticky; + top: 12px; +} + +.topic-tag-inspector form { + margin: 0; +} + +.topic-tag-inspector-header { + align-items: flex-start; + position: sticky; + top: 0; + z-index: 3; + background: var(--tp-panel); +} + +.topic-tag-eyebrow { + font-size: 10px; + font-weight: bold; + letter-spacing: 0.08em; + margin: 0 0 4px; + text-transform: uppercase; +} + +.topic-tag-preview { + flex: 0 1 auto; + margin-top: 2px; +} + +.topic-tag-form-error { + margin: 14px 16px 0; +} + +.topic-tag-field { + border-bottom: 1px solid #ECEEEF; + padding: 14px 16px; +} + +.topic-tag-field > label, +.topic-tag-forum-header label, +.topic-tag-enabled-field label { + display: block; + font-weight: bold; + margin-bottom: 5px; +} + +.topic-tag-help { + display: block; + font-size: 11px; + line-height: 1.4; + margin-bottom: 7px; +} + +.topic-tag-field input[type="text"] { + max-width: none; + width: 100%; +} + +.topic-tag-color, +.topic-tag-color:hover, +.topic-tag-color:focus { + background: #FFFFFF; + border: 1px solid #AFAEAA; + border-radius: 4px; + color: inherit; + cursor: pointer; + height: 32px; + margin: 0; + padding: 3px; + width: 52px; +} + +.topic-tag-color-control input[type="text"] { + font-family: monospace; + max-width: 96px; + text-transform: uppercase; +} + +.topic-tag-input-error { + color: #A51D36; + display: block; + font-size: 11px; + font-weight: 600; + margin-top: 6px; +} + +#tag_color_text[aria-invalid="true"] { + border-color: #C94B62; + box-shadow: 0 0 0 2px rgba(201, 75, 98, 0.15); +} + +.topic-tag-color::-webkit-color-swatch-wrapper { + padding: 0; +} + +.topic-tag-color::-webkit-color-swatch, +.topic-tag-color::-moz-color-swatch { + border: 0; + border-radius: 2px; +} + +.topic-tag-enabled-field > div { + min-width: 0; +} + +.topic-tag-checkbox { + height: 20px; + width: 20px; +} + +.topic-tag-forum-header { + align-items: flex-start; +} + +.topic-tag-forum-tools { + flex-wrap: wrap; + margin: 10px 0 8px; +} + +.topic-tag-forum-tools .topic-tag-search { + flex: 1 1 150px; +} + +.topic-tag-forum-tools .topic-tag-search input { + width: 100%; +} + +.topic-tag-forum-picker { + border: 1px solid var(--tp-border); + border-radius: 4px; + max-height: min(34vh, 320px); + overflow: auto; +} + +.topic-tag-forum-group, +.topic-tag-forum-option { + border-bottom: 1px solid #ECEEEF; + display: block; + margin: 0; + padding: 7px 10px 7px calc(10px + var(--forum-depth) * 14px); +} + +.topic-tag-forum-group { + background: var(--tp-soft); + color: #3B4650; + font-size: 11px; + font-weight: bold; + text-transform: uppercase; +} + +.topic-tag-forum-option { + cursor: pointer; + font-weight: normal; +} + +.topic-tag-forum-option:hover { + background: #F5F9FC; +} + +.topic-tag-forum-option:last-child, +.topic-tag-forum-group:last-child { + border-bottom: 0; +} + +.topic-tag-forum-option input { + margin-right: 7px; +} + +.topic-tag-inspector-actions { + align-items: center; + background: var(--tp-soft); + bottom: 0; + display: flex; + gap: 8px; + padding: 12px 16px; + position: sticky; + z-index: 3; +} + +.topic-tag-dirty { + color: #7A5610; + font-size: 11px; + margin-left: auto; +} + +.topic-tags-empty { + color: var(--tp-muted); + padding: 32px 16px; + text-align: center; +} + +.topic-tag-js-only { + display: none; +} + +.topic-tag-app.is-js .topic-tag-js-only { + display: initial; +} + +.topic-tag-app.is-js .topic-tag-page-header .topic-tag-js-only, +.topic-tag-app.is-js .topic-tag-drag, +.topic-tag-app.is-js .topic-tag-filters, +.topic-tag-app.is-js .topic-tag-forum-tools, +.topic-tag-app.is-js .topic-tag-dirty { + display: flex; +} + +.sr-only { + clip: rect(0, 0, 0, 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; +} + +@media (max-width: 980px) { + .topic-tag-workspace { + display: flex; + flex-direction: column-reverse; + } + + .topic-tag-catalog, + .topic-tag-inspector { + width: 100%; + } + + .topic-tag-inspector { + max-height: none; + position: static; + } + + .topic-tag-inspector-header, + .topic-tag-inspector-actions { + position: static; + } +} + +@media (max-width: 620px) { + .topic-tag-page-header, + .topic-tag-catalog-header { + align-items: stretch; + flex-direction: column; + } + + .topic-tag-filters, + .topic-tag-filters label, + .topic-tag-filters input, + .topic-tag-filters select { + width: 100%; + } + + .topic-tag-item { + grid-template-columns: 20px minmax(120px, 1fr) auto; + } + + .topic-tag-toggle { + grid-column: 2; + } + + .topic-tag-actions { + grid-column: 3; + grid-row: 1 / span 2; + } + + .topic-tag-state, + .topic-tag-forum-preview { + display: none; + } +} + +@media (prefers-reduced-motion: reduce) { + .topic-tag-notice, + .tp-button, + .topic-tag-item, + .topic-tag-switch, + .topic-tag-switch::after { + transition: none; + } } diff --git a/adm/style/acp_topic_prefixes.html b/adm/style/acp_topic_prefixes.html index 9677ca3..e643449 100644 --- a/adm/style/acp_topic_prefixes.html +++ b/adm/style/acp_topic_prefixes.html @@ -2,70 +2,197 @@ -

{{ lang('TOPIC_PREFIXES') }}

+
+
+
+

{{ lang('TOPIC_TAGS') }}

+

{{ lang('TOPIC_TAGS_EXPLAIN') }}

+

{{ lang('TOPIC_TAGS_REPAIR_CLI') }} php bin/phpbbcli.php topicprefixes:repair-tags

+
+ +
-

{{ lang('TOPIC_PREFIXES_EXPLAIN') }}

+ -
-
-
-
-
- - -
-
-
-
+
+
+
+
+

{{ lang('TOPIC_TAG_CATALOG') }}

+

{{ lang('TOPIC_TAG_RESULT_COUNT', loops.tags|length) }}

+
+
+ + +
+ +
-{% if FORUM_ID %} - - - - - - - - - - - {% for prefix in loops.prefixes %} - - - - - - {% else %} - - - - {% endfor %} - -
{{ lang('TOPIC_PREFIX_TAG') }}{{ lang('TOPIC_PREFIX_ENABLED') }}{{ lang('ACTION') }}
{{ prefix.PREFIX_TAG }} - - + - - {{ ICON_MOVE_UP }} - - {{ ICON_MOVE_DOWN }} - {{ ICON_DELETE }} -
{{ lang('TOPIC_PREFIXES_EMPTY') }}
+ +
+ {% else %} +
{{ lang('TOPIC_TAGS_EMPTY') }}
+ {% endfor %} +
+ + + + +
+
+
+
+

{{ lang('TOPIC_TAG_EDITOR') }}

+

{{ lang(S_EDIT_TAG ? 'EDIT_TOPIC_TAG' : 'CREATE_TOPIC_TAG') }}

+
+ {{ TAG_NAME ?: lang('TOPIC_TAG_PREVIEW') }} +
+ + + +
+ + +
+ +
+ + {{ lang('TOPIC_TAG_COLOR_EXPLAIN') }} +
+ + +
+ +
+ +
+
+ + {{ lang('TOPIC_TAG_ENABLED_EXPLAIN') }} +
+ +
+ +
+
+
+ + {{ lang('TOPIC_TAG_FORUMS_EXPLAIN') }} +
+ +
+
+ + + +
+
+ {% for forum in loops.forums %} + {% if forum.S_POSTABLE %} + + {% else %} +
{{ forum.FORUM_NAME }}
+ {% endif %} + {% else %} +

{{ lang('TOPIC_TAG_NO_FORUMS') }}

+ {% endfor %} +
+
- -
- {{ lang('CREATE_TOPIC_PREFIX') ~ lang('COLON') }} - - - - - {{ S_FORM_TOKEN }} - {{ S_HIDDEN_FIELDS }} -
-
+
+ + {{ lang('CANCEL') }} + +
+ + + {{ S_FORM_TOKEN }} + +
+ -{% endif %} + {% INCLUDEJS '@phpbb_topicprefixes/acp_topic_prefixes.js' %} {% INCLUDECSS '@phpbb_topicprefixes/acp_topic_prefixes.css' %} diff --git a/adm/style/acp_topic_prefixes.js b/adm/style/acp_topic_prefixes.js index 5958e7c..7796972 100644 --- a/adm/style/acp_topic_prefixes.js +++ b/adm/style/acp_topic_prefixes.js @@ -1,17 +1,637 @@ -(function() { +/* global phpbb */ +(function($) { 'use strict'; - document.getElementById('select_forum').addEventListener('change', function() { - this.closest('form').submit(); + var $app = $('#topic-tag-app'); + if (!$app.length) { + return; + } + + $app.addClass('is-js'); + + var $list = $('#topic-tag-list'), + $form = $('#acp_topic_tag'), + $name = $('#tag_name'), + $color = $('#tag_color'), + $colorText = $('#tag_color_text'), + $enabled = $('#tag_enabled'), + $preview = $('#topic-tag-preview'), + $formError = $('#topic-tag-form-error'), + $notice = $('#topic-tag-notice'), + $noticeMessage = $('#topic-tag-notice-message'), + $colorError = $('#tag_color_error'), + $dirtyLabel = $('#topic-tag-dirty'), + $tagSearch = $('#topic-tag-search'), + $statusFilter = $('#topic-tag-status-filter'), + $forumSearch = $('#topic-tag-forum-search'), + $forumPicker = $('#topic-tag-forums'), + $selectedCount = $('#topic-tag-forum-selected'), + $resultCount = $('#topic-tag-result-count'), + $noResults = $('#topic-tag-no-results'), + $orderHint = $('#topic-tag-order-hint'), + $selectedRow = $(), + dirty = false, + draggedRow = null, + dragImage = null, + dropCommitted = false, + previousOrder = [], + orderSaving = false, + noticeTimer; + + function format(template, value) { + return String(template).replace('%d', value).replace('%s', value); + } + + function hideNotice() { + window.clearTimeout(noticeTimer); + $notice.removeClass('is-visible'); + noticeTimer = window.setTimeout(function() { + $notice.prop('hidden', true); + }, 180); + } + + function showNotice(message, isError) { + window.clearTimeout(noticeTimer); + $noticeMessage.text(message); + $notice.toggleClass('is-error', !!isError) + .attr({role: isError ? 'alert' : 'status', 'aria-live': isError ? 'assertive' : 'polite'}) + .prop('hidden', false) + .removeClass('is-visible'); + // Restart transition when one notification replaces another. + $notice[0].getBoundingClientRect(); + $notice.addClass('is-visible'); + if (!isError) { + noticeTimer = window.setTimeout(hideNotice, 4000); + } + } + + $('.topic-tag-notice-close').on('click', hideNotice); + + function responseData(xhr) { + if (xhr.responseJSON) { + return xhr.responseJSON; + } + try { + return JSON.parse(xhr.responseText); + } catch (error) { + return {}; + } + } + + function requestError(xhr) { + var response = responseData(xhr), + message = response.message || $app.data('request-failed'); + showNotice(message, true); + return response; + } + + function contrastColor(hex) { + var clean = hex.replace('#', ''), channels = [], luminance, whiteContrast, blackContrast, i, channel; + if (!/^[0-9a-f]{6}$/i.test(clean)) { + return '#000000'; + } + for (i = 0; i < 3; i++) { + channel = parseInt(clean.substr(i * 2, 2), 16) / 255; + channels.push(channel <= 0.03928 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4)); + } + luminance = 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2]; + whiteContrast = 1.05 / (luminance + 0.05); + blackContrast = (luminance + 0.05) / 0.05; + return whiteContrast >= blackContrast ? '#FFFFFF' : '#000000'; + } + + function normalizeColor(value) { + value = String(value || '').trim().toUpperCase(); + if (value && value.charAt(0) !== '#') { + value = '#' + value; + } + return value; + } + + function validateColor(showError) { + var valid = /^#[0-9A-F]{6}$/.test(normalizeColor($colorText.val())); + $colorText.attr('aria-invalid', valid ? 'false' : 'true'); + $colorError.prop('hidden', valid || !showError); + return valid; + } + + function updatePreview() { + var value = normalizeColor($colorText.val()), valid = /^#[0-9A-F]{6}$/.test(value); + $preview.text($name.val().trim() || $app.data('preview')); + if (valid) { + $preview.css({backgroundColor: value, color: contrastColor(value)}); + } + } + + function setDirty(value) { + dirty = !!value; + $dirtyLabel.prop('hidden', !dirty); + } + + function confirmDiscard(callback) { + if (dirty && !window.confirm($app.data('discard'))) { + return false; + } + callback(); + return true; + } + + function formTokenData() { + return { + creation_time: $form.find('input[name="creation_time"]').val(), + form_token: $form.find('input[name="form_token"]').val() + }; + } + + function forumIdsForRow($row) { + var value = String($row.attr('data-forum-ids') || ''); + return value ? value.split(',') : []; + } + + function resetForumSearch() { + $forumSearch.val(''); + $forumPicker.children().prop('hidden', false); + } + + function updateForumCount() { + var count = $forumPicker.find('input[type="checkbox"]:checked').length; + $selectedCount.text(format($app.data('selected-count'), count)); + } + + function loadEditor($row) { + var ids = forumIdsForRow($row), tagId = $row.data('tag-id'); + $selectedRow.removeClass('is-selected'); + $selectedRow = $row.addClass('is-selected'); + $form.find('input[name="tag_id"]').val(tagId); + $name.val($row.attr('data-tag-name')); + $color.val($row.attr('data-tag-color')); + $colorText.val($row.attr('data-tag-color').toUpperCase()); + $enabled.prop('checked', $row.attr('data-tag-enabled') === '1'); + $forumPicker.find('input[type="checkbox"]').each(function() { + this.checked = ids.indexOf(this.value) !== -1; + }); + $('#topic-tag-editor-title').text($app.data('edit-title')); + resetForumSearch(); + updateForumCount(); + updatePreview(); + validateColor(false); + $formError.prop('hidden', true).empty(); + setDirty(false); + } + + function loadNewEditor() { + $selectedRow.removeClass('is-selected'); + $selectedRow = $(); + $form.find('input[name="tag_id"]').val('0'); + $name.val(''); + $color.val($app.data('default-color')); + $colorText.val($app.data('default-color')); + $enabled.prop('checked', true); + $forumPicker.find('input[type="checkbox"]').prop('checked', false); + $('#topic-tag-editor-title').text($app.data('create-title')); + resetForumSearch(); + updateForumCount(); + updatePreview(); + validateColor(false); + $formError.prop('hidden', true).empty(); + setDirty(false); + } + + function updateEnabledState($row, enabled) { + var $toggle = $row.find('.topic-tag-toggle'); + $row.attr('data-tag-enabled', enabled ? '1' : '0').toggleClass('is-disabled', !enabled); + $toggle.attr('aria-checked', enabled ? 'true' : 'false'); + $toggle.find('.topic-tag-state').text($toggle.data(enabled ? 'enabled-label' : 'disabled-label')); + if ($selectedRow.is($row)) { + $enabled.prop('checked', enabled); + } + } + + function forumNamesFromIds(ids) { + var names = []; + $forumPicker.find('input[type="checkbox"]').each(function() { + if (ids.indexOf(parseInt(this.value, 10)) !== -1) { + names.push($(this).siblings('span').text()); + } + }); + return names; + } + + function populateRow($row, tag) { + var names = tag.forum_names || forumNamesFromIds(tag.forum_ids), + $previewNames = $row.find('.topic-tag-forum-preview').empty(), i; + $row.attr({ + 'data-tag-id': tag.id, + 'data-tag-name': tag.name, + 'data-tag-color': tag.color, + 'data-tag-text-color': tag.text_color, + 'data-forum-ids': tag.forum_ids.join(','), + 'data-search': (tag.name + ' ' + names.join(' ')).toLowerCase() + }); + $row.data('tag-id', tag.id); + $row.find('.topic-tag-summary .topic-tag').text(tag.name).css({backgroundColor: tag.color, color: tag.text_color}); + $row.find('.topic-tag-forum-summary strong').text(format($app.data('forum-count'), names.length)); + for (i = 0; i < Math.min(names.length, 2); i++) { + $('').text(names[i]).appendTo($previewNames); + } + if (names.length > 2) { + $('