diff --git a/CHANGELOG.md b/CHANGELOG.md index b83dea8..fefb058 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # 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. +- Added opt-in, permission-filtered tag autocomplete to advanced search, including tag-only topic search and all/any matching. +- 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.19 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..24e8fb6 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,82 @@ -# 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. +- Optionally enable tag search in ACP > General > Board features, adding a permission-filtered autocomplete to advanced search with all/any matching and tag-only topic results. +- Filter keyword and author search results shown as topics or posts when using phpBB native, MySQL fulltext, or PostgreSQL search. Sphinx supports tag-only search but not combined tag and text/author search. +- 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. + +## Repair tools + +Before applying any repair, make a current database backup and disable the +board. Each tool shows a preview and asks for confirmation before changing +anything. + +### Repair tags + +Use this command to split a combined tag into separate tags or merge one tag +into another: + +```shell +php bin/phpbbcli.php topicprefixes:repair-tags +``` + +To repair a specific tag, add `--tag-id=ID`. + +### Repair topics + +Use this command to repair missing topic tags or old prefixes, usually after +upgrading from version 1 to version 2: + +```shell +php bin/phpbbcli.php topicprefixes:repair-topics +``` + +Choose the action that matches the problem: + +- **Recover missing topic tags:** use when a topic still shows its old prefix + and has no tag. +- **Clean leftover topic titles:** use when tags migrated correctly but old + prefix text remains in topic titles. +- **Clean historical reply subjects:** optionally remove old prefixes from + replies. Reply subjects are normally kept as part of the discussion history. + +Missing-tag candidates are grouped by forum and inferred tags. You can review +each group in pages of 20, stop previewing at any page, then approve or skip the +whole group. The confirmation reports how many candidates were and were not +previewed. Each completed repair action creates one administrator-log summary. + +For title or reply cleanup, enter the old prefix exactly as shown and select the +forum to repair. If post subjects change, rebuild the search index in ACP > +Maintenance > Search index. + +Some missing information cannot be recovered from the current database. If the +tool cannot identify the old tag, use a pre-upgrade backup to determine it. + 📦 [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_info.php b/acp/topic_prefixes_info.php index 72d786c..689ea35 100644 --- a/acp/topic_prefixes_info.php +++ b/acp/topic_prefixes_info.php @@ -10,9 +10,17 @@ namespace phpbb\topicprefixes\acp; +/** + * Describe Topic Prefixes ACP module registration. + */ class topic_prefixes_info { - public function module() + /** + * Return ACP module metadata. + * + * @return array ACP module definition + */ + public function module(): array { return [ 'filename' => '\phpbb\topicprefixes\acp\topic_prefixes_module', diff --git a/acp/topic_prefixes_module.php b/acp/topic_prefixes_module.php index e13021b..f1c65cc 100644 --- a/acp/topic_prefixes_module.php +++ b/acp/topic_prefixes_module.php @@ -11,21 +11,26 @@ namespace phpbb\topicprefixes\acp; /** - * Class topic_prefixes_module + * Dispatch Topic Prefixes ACP requests to the administrator controller. */ class topic_prefixes_module { - /** @var string */ + /** @var string Module action URL */ public $u_action; + + /** @var string Template name */ public $tpl_name; + + /** @var string Page title language key */ public $page_title; /** - * Main ACP module + * Dispatch the current ACP request. * - * @throws \Exception + * @return void + * @throws \Exception When controller dispatch fails */ - public function main() + public function main(): void { global $phpbb_container; @@ -33,7 +38,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..a63e16c 100644 --- a/adm/style/acp_topic_prefixes.css +++ b/adm/style/acp_topic_prefixes.css @@ -1,12 +1,799 @@ -.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-notices { + display: flex; + flex-direction: column; + gap: 10px; + max-width: min(380px, calc(100vw - 32px)); + pointer-events: none; + position: fixed; + right: 16px; + top: 16px; + width: 100%; + z-index: 10000; +} + +.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; + opacity: 0; + padding: 11px 12px 11px 14px; + pointer-events: none; + transform: translateY(-12px) scale(0.98); + transition: opacity 160ms ease, transform 160ms ease; +} + +.topic-tag-notice.is-visible { + opacity: 1; + pointer-events: auto; + transform: translateY(0) scale(1); +} + +.topic-tag-notice > span:first-child { + 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: minmax(0, 1fr) auto auto; + min-height: 64px; + padding: 9px 12px; + transition: background-color 120ms ease, box-shadow 120ms ease, opacity 120ms ease; +} + +.topic-tag-app.is-js .topic-tag-item { + grid-template-columns: 24px minmax(0, 1fr) auto auto; +} + +.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-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; + overflow: hidden; + 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; + overflow: hidden; + width: 100%; +} + +.topic-tag-forum-summary strong { + flex: 0 0 auto; + white-space: nowrap; +} + +.topic-tag-forum-preview { + display: block; + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.topic-tag-toggle { + align-items: center; + color: #48545E; + display: flex; + font-size: 11px; + gap: 6px; + text-decoration: none; +} + +.topic-tag-state { + display: grid; +} + +.topic-tag-state-current, +.topic-tag-state::before, +.topic-tag-state::after { + grid-column: 1; + grid-row: 1; +} + +.topic-tag-state::before, +.topic-tag-state::after { + visibility: hidden; +} + +.topic-tag-state::before { + content: attr(data-enabled-label); +} + +.topic-tag-state::after { + content: attr(data-disabled-label); +} + +.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-order-actions a[aria-disabled="true"] { + cursor: default; + opacity: 0.3; + pointer-events: none; +} + +.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: 1250px) { + .topic-tag-item { + grid-template-columns: minmax(0, 1fr) auto; + } + + .topic-tag-toggle { + grid-column: 1; + grid-row: 2; + } + + .topic-tag-actions { + grid-column: 2; + grid-row: 1 / span 2; + } + + .topic-tag-app.is-js .topic-tag-item { + grid-template-columns: 24px minmax(0, 1fr) auto; + } + + .topic-tag-app.is-js .topic-tag-toggle { + grid-column: 2; + } + + .topic-tag-app.is-js .topic-tag-actions { + grid-column: 3; + } +} + +@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: minmax(120px, 1fr) auto; + } + + .topic-tag-app.is-js .topic-tag-item { + grid-template-columns: 20px minmax(120px, 1fr) auto; + } + + .topic-tag-app.is-js .topic-tag-toggle { + grid-column: 2; + } + + .topic-tag-app.is-js .topic-tag-actions { + grid-column: 3; + } + + .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..1c1ab1b 100644 --- a/adm/style/acp_topic_prefixes.html +++ b/adm/style/acp_topic_prefixes.html @@ -2,70 +2,201 @@ -

{{ lang('TOPIC_PREFIXES') }}

+
+
+
+

{{ lang('TOPIC_TAGS') }}

+

{{ lang('TOPIC_TAGS_EXPLAIN') }}

+
+ {{ lang('TOPIC_TAGS_REPAIR_CLI') }} +

{{ lang('TOPIC_TAGS_REPAIR_CLI_EXPLAIN') }}
+ {{ lang('TOPIC_TAGS_REPAIR_TAGS') }} php bin/phpbbcli.php topicprefixes:repair-tags
+ {{ lang('TOPIC_TAGS_REPAIR_TOPICS') }} php bin/phpbbcli.php topicprefixes:repair-topics +

+
+
+ +
-

{{ 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..17607b6 100644 --- a/adm/style/acp_topic_prefixes.js +++ b/adm/style/acp_topic_prefixes.js @@ -1,17 +1,681 @@ -(function() { +/* global phpbb */ +(($) => { 'use strict'; - document.getElementById('select_forum').addEventListener('change', function() { - this.closest('form').submit(); + const $app = $('#topic-tag-app'); + if (!$app.length) { + return; + } + + $app.addClass('is-js'); + + const $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'), + $notices = $('#topic-tag-notices'), + $noticeTemplate = $('#topic-tag-notice-template'), + $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'); + let $selectedRow = $(), + saving = false, + dirty = false, + draggedRow = null, + dragImage = null, + dropCommitted = false, + previousOrder = [], + orderSaving = false; + + function format(template, value) { + return String(template).replace('%d', value).replace('%s', value); + } + + function removeNotice($notice, immediate) { + window.clearTimeout($notice.data('notice-timer')); + if (immediate) { + $notice.remove(); + return; + } + $notice.removeClass('is-visible'); + window.setTimeout(() => { + $notice.remove(); + }, 180); + } + + function showNotice(message, isError) { + const $notice = $noticeTemplate.clone().removeAttr('id hidden').toggleClass('is-error', !!isError) + .attr({ role: isError ? 'alert' : 'status', 'aria-live': isError ? 'assertive' : 'polite' }); + $notice.find('#topic-tag-notice-message').removeAttr('id').text(message); + $notices.prepend($notice); + $notice[0].getBoundingClientRect(); + $notice.addClass('is-visible'); + $notices.children('.topic-tag-notice').slice(4).each((index, notice) => { + removeNotice($(notice), true); + }); + if (!isError) { + $notice.data('notice-timer', window.setTimeout(() => { + removeNotice($notice, false); + }, 4000)); + } + } + + $notices.on('click', '.topic-tag-notice-close', function() { + removeNotice($(this).closest('.topic-tag-notice'), false); + }); + + function responseData(xhr) { + if (xhr.responseJSON) { + return xhr.responseJSON; + } + try { + return JSON.parse(xhr.responseText); + } catch (error) { + return {}; + } + } + + function requestError(xhr) { + const response = responseData(xhr), + message = response.message || $app.data('request-failed'); + showNotice(message, true); + return response; + } + + function contrastColor(hex) { + const clean = hex.replace('#', ''), channels = []; + if (!/^[0-9a-f]{6}$/i.test(clean)) { + return '#000000'; + } + for (let i = 0; i < 3; i++) { + const 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)); + } + const 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) { + let normalized = String(value || '').trim().toUpperCase(); + if (normalized && normalized.charAt(0) !== '#') { + normalized = '#' + normalized; + } + return normalized; + } + + function validateColor(showError) { + const 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() { + const 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 (saving) { + return false; + } + 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) { + const value = String($row.attr('data-forum-ids') || ''); + return value ? value.split(',') : []; + } + + function resetForumSearch() { + $forumSearch.val(''); + $forumPicker.children().prop('hidden', false); + } + + function updateForumCount() { + const count = $forumPicker.find('input[type="checkbox"]:checked').length; + $selectedCount.text(format($app.data('selected-count'), count)); + } + + function loadEditor($row) { + const 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((index, checkbox) => { + checkbox.checked = ids.indexOf(checkbox.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) { + const $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-current').text($toggle.data(enabled ? 'enabled-label' : 'disabled-label')); + if ($selectedRow.is($row)) { + $enabled.prop('checked', enabled); + } + } + + function forumNamesFromIds(ids) { + const names = []; + $forumPicker.find('input[type="checkbox"]').each((index, checkbox) => { + if (ids.indexOf(parseInt(checkbox.value, 10)) !== -1) { + names.push($(checkbox).siblings('span').text()); + } + }); + return names; + } + + function populateRow($row, tag) { + const names = tag.forum_names || forumNamesFromIds(tag.forum_ids), + previewText = names.join(', '), + $previewNames = $row.find('.topic-tag-forum-preview'); + $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)); + $previewNames.text(previewText); + if (previewText) { + $previewNames.attr('title', previewText); + } else { + $previewNames.removeAttr('title'); + } + $row.find('[data-tag-edit]').attr('href', tag.urls.edit); + $row.find('.topic-tag-toggle').attr('href', tag.urls.toggle); + $row.find('.topic-tag-move-up').attr('data-action-url', tag.urls.move_up); + $row.find('.topic-tag-move-down').attr('data-action-url', tag.urls.move_down); + $row.find('[data-ajax="tp_delete"]').attr('href', tag.urls.delete); + updateEnabledState($row, tag.enabled); + } + + function createRow(tag) { + const $row = $('#topic-tag-row-template').clone(); + $row.removeAttr('id hidden').removeClass('topic-tag-row-template').attr('draggable', 'false'); + populateRow($row, tag); + $list.find('.topic-tags-empty').remove(); + $list.append($row); + phpbb.ajaxify({ selector: $row.find('[data-ajax="tp_delete"]'), callback: 'tp_delete' }); + return $row; + } + + function itemRows() { + return $list.children('.topic-tag-item'); + } + + function setOrderActionState($action, disabled) { + $action.attr('aria-disabled', disabled ? 'true' : 'false'); + if (disabled) { + $action.removeAttr('href').attr('tabindex', '-1'); + } else { + $action.attr('href', $action.attr('data-action-url')).removeAttr('tabindex'); + } + } + + function updateOrderActions() { + const $rows = itemRows(), disableAll = orderSaving || $tagSearch.val().trim() || $statusFilter.val() !== 'all'; + $rows.each((index, row) => { + setOrderActionState($(row).find('.topic-tag-move-up'), disableAll || index === 0); + setOrderActionState($(row).find('.topic-tag-move-down'), disableAll || index === $rows.length - 1); + }); + } + + function updateListFilter() { + const query = $tagSearch.val().trim().toLowerCase(), status = $statusFilter.val(), + filterActive = query !== '' || status !== 'all', $rows = itemRows(); + let visible = 0; + $rows.each((index, row) => { + const $row = $(row), enabled = $row.attr('data-tag-enabled') === '1', + matchesText = String($row.attr('data-search') || '').toLowerCase().indexOf(query) !== -1, + matchesStatus = status === 'all' || (status === 'enabled' && enabled) || (status === 'disabled' && !enabled), + show = matchesText && matchesStatus; + $row.prop('hidden', !show).attr('draggable', 'false'); + $row.find('.topic-tag-drag').prop('disabled', filterActive); + if (show) { + visible++; + } + }); + $resultCount.text(format($app.data('result-count'), visible)); + $noResults.prop('hidden', !($rows.length && visible === 0)); + $orderHint.prop('hidden', !filterActive); + updateOrderActions(); + if (!$rows.length && !$list.find('.topic-tags-empty').length) { + $('
').text($app.data('empty')).appendTo($list); + } + } + + function currentOrder() { + return itemRows().map((index, row) => { + return parseInt($(row).attr('data-tag-id'), 10); + }).get(); + } + + function restoreOrder(order) { + order.forEach((id) => { + const $row = itemRows().filter((index, row) => { + return parseInt($(row).attr('data-tag-id'), 10) === id; + }).first(); + $list.append($row); + }); + } + + function persistOrder(oldOrder) { + const data = formTokenData(), $rows = itemRows(), $actions = $rows.find('.topic-tag-actions'); + orderSaving = true; + updateOrderActions(); + data.action = 'reorder'; + data.tag_ids = currentOrder(); + $actions.addClass('is-saving'); + $rows.find('.topic-tag-drag').prop('disabled', true); + $.ajax({ url: $app.data('action'), type: 'POST', data: data, cache: false }) + .done((response) => { + if (response.success) { + showNotice(response.message, false); + } + }) + .fail((xhr) => { + restoreOrder(oldOrder); + requestError(xhr); + }) + .always(() => { + orderSaving = false; + $actions.removeClass('is-saving'); + updateListFilter(); + }); + } + + function toggleRow($row, desired, source) { + const data = formTokenData(), $toggle = $row.find('.topic-tag-toggle'); + if ($toggle.hasClass('is-saving')) { + return; + } + data.action = 'toggle'; + data.tag_id = $row.attr('data-tag-id'); + data.enabled = desired ? 1 : 0; + $toggle.addClass('is-saving'); + $.ajax({ url: $app.data('action'), type: 'POST', data: data, cache: false }) + .done((response) => { + if (response.success) { + updateEnabledState($row, response.enabled); + updateListFilter(); + showNotice(response.message, false); + } + }) + .fail((xhr) => { + if (source === 'editor') { + $enabled.prop('checked', !desired); + } + requestError(xhr); + }) + .always(() => { + $toggle.removeClass('is-saving'); + }); + } + + phpbb.addAjaxCallback('tp_delete', function(response) { + const $row = $(this).closest('.topic-tag-item'); + if (!response.success) { + return; + } + if ($selectedRow.is($row)) { + loadNewEditor(); + } + $row.remove(); + updateListFilter(); + showNotice(response.message, false); }); - phpbb.addAjaxCallback('tp_toggle', function(res) { - if (typeof res.success === 'undefined' || !res.success) { + $list[0].addEventListener('click', (event) => { + if (saving) { + event.preventDefault(); + event.stopImmediatePropagation(); + return; + } + const $delete = $(event.target).closest('[data-ajax="tp_delete"]'); + if (!$delete.length) { + return; + } + const $row = $delete.closest('.topic-tag-item'); + if (dirty && $selectedRow.is($row)) { + if (!window.confirm($app.data('discard'))) { + event.preventDefault(); + event.stopImmediatePropagation(); + } + } + }, true); + + $list.on('click', '[data-tag-edit]', function(event) { + const $row = $(this).closest('.topic-tag-item'); + event.preventDefault(); + if ($selectedRow.is($row)) { return; } + confirmDiscard(() => { + loadEditor($row); + if (window.matchMedia && window.matchMedia('(max-width: 980px)').matches) { + document.querySelector('.topic-tag-inspector').scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }); + }); + + $('.topic-tag-new').on('click', () => { + confirmDiscard(() => { + loadNewEditor(); + $name.trigger('focus'); + }); + }); + + $('#topic-tag-cancel').on('click', (event) => { + event.preventDefault(); + confirmDiscard(() => { + loadNewEditor(); + }); + }); - const icon = this.querySelector('i'); - icon.classList.toggle('tp-toggle-on'); - icon.classList.toggle('tp-toggle-off'); + $list.on('click', '.topic-tag-toggle', function(event) { + const $row = $(this).closest('.topic-tag-item'); + event.preventDefault(); + toggleRow($row, $row.attr('data-tag-enabled') !== '1', 'row'); }); -})(); + + $list.on('keydown', '.topic-tag-toggle', function(event) { + if (event.key === ' ' || event.keyCode === 32) { + event.preventDefault(); + $(this).trigger('click'); + } + }); + + $enabled.on('change', function() { + if ($selectedRow.length) { + toggleRow($selectedRow, this.checked, 'editor'); + } else { + setDirty(true); + } + }); + + $name.on('input', () => { + setDirty(true); + updatePreview(); + }); + + $color.on('input change', function() { + $colorText.val(this.value.toUpperCase()); + setDirty(true); + updatePreview(); + validateColor(false); + }); + + $colorText.on('input', () => { + setDirty(true); + updatePreview(); + validateColor(false); + }).on('blur', function() { + const value = normalizeColor(this.value); + if (/^#[0-9A-F]{6}$/.test(value)) { + this.value = value; + $color.val(value); + } + validateColor(true); + updatePreview(); + }); + + $forumPicker.on('change', 'input[type="checkbox"]', () => { + setDirty(true); + updateForumCount(); + }); + + $forumSearch.on('input', function() { + const query = this.value.trim().toLowerCase(); + $forumPicker.children().each((index, forum) => { + $(forum).prop('hidden', String($(forum).attr('data-forum-name') || '').toLowerCase().indexOf(query) === -1); + }); + }); + + $('#topic-tag-select-visible').on('click', () => { + $forumPicker.find('.topic-tag-forum-option:not([hidden]) input').prop('checked', true); + setDirty(true); + updateForumCount(); + }); + + $('#topic-tag-clear-forums').on('click', () => { + $forumPicker.find('input[type="checkbox"]').prop('checked', false); + setDirty(true); + updateForumCount(); + }); + + $tagSearch.add($statusFilter).on('input change', updateListFilter); + + $list.on('click', '.topic-tag-move-up, .topic-tag-move-down', function(event) { + const $link = $(this), $row = $link.closest('.topic-tag-item'), oldOrder = currentOrder(); + if ($link.attr('aria-disabled') === 'true' || orderSaving || $tagSearch.val().trim() || $statusFilter.val() !== 'all') { + event.preventDefault(); + return; + } + event.preventDefault(); + const $sibling = $link.hasClass('topic-tag-move-up') ? $row.prev('.topic-tag-item') : $row.next('.topic-tag-item'); + if (!$sibling.length) { + return; + } + if ($link.hasClass('topic-tag-move-up')) { + $row.insertBefore($sibling); + } else { + $row.insertAfter($sibling); + } + persistOrder(oldOrder); + }); + + $list.on('mousedown', '.topic-tag-drag', function() { + if (!orderSaving && !$tagSearch.val().trim() && $statusFilter.val() === 'all') { + $(this).closest('.topic-tag-item').attr('draggable', 'true'); + } + }); + + $list.on('dragstart', '.topic-tag-item', function(event) { + if ($(this).attr('draggable') !== 'true') { + event.preventDefault(); + return; + } + draggedRow = this; + dropCommitted = false; + previousOrder = currentOrder(); + $(this).addClass('is-dragging'); + dragImage = $('
').text($(this).attr('data-tag-name')).appendTo(document.body)[0]; + event.originalEvent.dataTransfer.effectAllowed = 'move'; + event.originalEvent.dataTransfer.setData('text/plain', $(this).attr('data-tag-id')); + if (event.originalEvent.dataTransfer.setDragImage) { + event.originalEvent.dataTransfer.setDragImage(dragImage, 20, 20); + } + }); + + $list.on('dragover', '.topic-tag-item', function(event) { + if (!draggedRow || this === draggedRow) { + return; + } + event.preventDefault(); + event.originalEvent.dataTransfer.dropEffect = 'move'; + const rect = this.getBoundingClientRect(); + if (event.originalEvent.clientY < rect.top + rect.height / 2) { + $(draggedRow).insertBefore(this); + } else { + $(draggedRow).insertAfter(this); + } + }); + + $list.on('dragover', (event) => { + if (draggedRow) { + event.preventDefault(); + event.originalEvent.dataTransfer.dropEffect = 'move'; + } + }); + + $list.on('drop', (event) => { + if (!draggedRow) { + return; + } + event.preventDefault(); + event.originalEvent.dataTransfer.dropEffect = 'move'; + dropCommitted = true; + }); + + $list.on('dragend', '.topic-tag-item', function() { + const changed = previousOrder.join(',') !== currentOrder().join(','); + $(this).removeClass('is-dragging').attr('draggable', 'false'); + if (dragImage) { + document.body.removeChild(dragImage); + dragImage = null; + } + draggedRow = null; + if (!dropCommitted) { + restoreOrder(previousOrder); + } else if (changed) { + persistOrder(previousOrder); + } + dropCommitted = false; + }); + + $form.on('submit', function(event) { + const $submit = $form.find('input[type="submit"]'); + event.preventDefault(); + if (saving || orderSaving || $list.find('.is-saving').length || draggedRow) { + return; + } + $formError.prop('hidden', true).empty(); + if (!this.checkValidity()) { + this.reportValidity(); + return; + } + if (!validateColor(true)) { + $colorText.trigger('focus'); + return; + } + $colorText.val(normalizeColor($colorText.val())); + $color.val($colorText.val()); + const creating = parseInt($form.find('input[name="tag_id"]').val(), 10) === 0, + data = $form.serialize() + '&submit=1', + $origin = $selectedRow, + $controls = $app.find('input, button, select, textarea').filter(':enabled'); + saving = true; + $controls.prop('disabled', true); + $submit.prop('disabled', true); + $.ajax({ url: $form.attr('action'), type: 'POST', data: data, cache: false }) + .always(() => { + saving = false; + $controls.prop('disabled', false); + $submit.prop('disabled', false); + }) + .done((response) => { + if (!response.success) { + return; + } + if (creating) { + createRow(response.tag); + loadNewEditor(); + } else { + populateRow($origin, response.tag); + loadEditor($origin); + } + updateListFilter(); + showNotice(response.message, false); + if (creating) { + $name.trigger('focus'); + } + }) + .fail((xhr) => { + const response = responseData(xhr), + field = response.field ? document.getElementById(response.field) : null; + $formError.text(response.message || $app.data('request-failed')).prop('hidden', false); + if (field) { + field.focus(); + } + }); + }); + + $(window).on('beforeunload', () => { + if (dirty) { + return $app.data('discard'); + } + }); + + itemRows().attr('draggable', 'false'); + updateListFilter(); + updateForumCount(); + updatePreview(); + validateColor(false); + const initialId = parseInt($form.find('input[name="tag_id"]').val(), 10); + if (initialId) { + const $initial = itemRows().filter((index, row) => { + return parseInt($(row).attr('data-tag-id'), 10) === initialId; + }).first(); + if ($initial.length) { + $selectedRow = $initial.addClass('is-selected'); + } + } +})(jQuery); diff --git a/build.xml b/build.xml index b3125a4..5f8e918 100644 --- a/build.xml +++ b/build.xml @@ -1,5 +1,5 @@ - +