diff --git a/CHANGELOG.md b/CHANGELOG.md index f353b316..dfa647ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,3 +6,7 @@ ## 2026-01-18 - Reworked Steam Workshop support detection to read installer AppIDs directly from the canonical game XMLs, ensuring admin mappings and Game Monitor buttons only appear for true Workshop-enabled titles. + +## 2026-01-19 +- Hid staging directory, install strategy, and post-install script controls from non-admins to keep panel defaults enforced for customers. +- Added the Steam Workshop mod picker, including search-backed UI, JSON state handling, and refreshed styling so customers can select mods without touching raw ID lists. diff --git a/docs/COPILOT_TODO.md b/docs/COPILOT_TODO.md index 2648f0aa..b1e56f41 100644 --- a/docs/COPILOT_TODO.md +++ b/docs/COPILOT_TODO.md @@ -1 +1,2 @@ - Auto-detect which server configs actually support Steam Workshop before showing adapter controls. +- Allow players/admins to reorder selected Workshop mods in the new picker UI so load order matches game expectations. diff --git a/modules/steam_workshop/controllers/SteamWorkshopController.php b/modules/steam_workshop/controllers/SteamWorkshopController.php index de3c2707..ec5c1c77 100644 --- a/modules/steam_workshop/controllers/SteamWorkshopController.php +++ b/modules/steam_workshop/controllers/SteamWorkshopController.php @@ -22,7 +22,13 @@ class SteamWorkshopController $isAdmin = $db->isAdmin($userId); $action = $_GET['action'] ?? 'index'; + if ($action === 'search') { + $this->handleSearch($userId, $isAdmin); + return; + } + echo ''; + echo ''; if ($action === 'save' && $_SERVER['REQUEST_METHOD'] === 'POST') { $this->handleSave($userId, $isAdmin); @@ -117,6 +123,41 @@ class SteamWorkshopController ]); } + private function handleSearch(int $userId, bool $isAdmin): void + { + header('Content-Type: application/json'); + $homeId = isset($_GET['home_id']) ? (int)$_GET['home_id'] : 0; + $query = trim((string)($_GET['q'] ?? '')); + if ($homeId <= 0) { + echo json_encode(['ok' => false, 'error' => $this->lang['error_missing_home'] ?? 'Home ID missing.']); + return; + } + if ($query === '') { + echo json_encode(['ok' => false, 'error' => $this->lang['error_missing_query'] ?? 'Enter a search term.']); + return; + } + + $home = $this->service->getHome($homeId, $userId, $isAdmin); + if ($home === null) { + echo json_encode(['ok' => false, 'error' => $this->lang['error_home_not_found'] ?? 'Home not found.']); + return; + } + + $gameKey = (string)($home['game_key'] ?? ''); + if ($gameKey === '') { + echo json_encode(['ok' => false, 'error' => $this->lang['error_home_not_found'] ?? 'Home not found.']); + return; + } + + $results = $this->service->searchWorkshopItems($gameKey, $query); + if (empty($results)) { + echo json_encode(['ok' => true, 'results' => [], 'empty' => true]); + return; + } + + echo json_encode(['ok' => true, 'results' => $results]); + } + private function applyGameAdapterOverride(array $home, array &$config): bool { $gameKey = isset($home['game_key']) ? (string)$home['game_key'] : ''; diff --git a/modules/steam_workshop/lang/en_US.php b/modules/steam_workshop/lang/en_US.php index 639ed590..5e07d6b1 100644 --- a/modules/steam_workshop/lang/en_US.php +++ b/modules/steam_workshop/lang/en_US.php @@ -16,6 +16,7 @@ return [ 'label_post_install_script' => 'Post-install script (absolute path)', 'label_mod_import' => 'Workshop IDs list (one "id,@ModName" per line)', 'hint_mod_import' => 'Paste from Modlist.txt or import from a collection. IDs are sanitized automatically.', + 'hint_admin_only' => 'Managed by your administrator.', 'adapter_locked_note' => 'This adapter is enforced for the current game type by your administrator.', 'admin_heading_game_mapping' => 'Adapter mapping by game type', 'admin_subheading_game_mapping' => 'Pick which adapter becomes the default whenever a server of that game opens the Workshop UI.', @@ -64,6 +65,25 @@ return [ 'message_adapter_saved' => 'Adapter saved.', 'message_adapter_deleted' => 'Adapter deleted.', 'error_admin_only' => 'Administrator access required.', + 'mod_picker_heading' => 'Workshop library', + 'mod_picker_hint' => 'Search Steam Workshop and add mods to keep them synced automatically.', + 'mod_picker_search_label' => 'Search Steam Workshop', + 'mod_picker_search_placeholder' => 'Example: 221100 or QoL tweaks', + 'mod_picker_search_button' => 'Search', + 'mod_picker_selected_heading' => 'Selected mods', + 'mod_picker_selected_hint' => 'Enable syncing to pull each mod before game server restarts.', + 'mod_picker_selected_empty' => 'No mods selected yet. Use the search above to add your first entry.', + 'mod_picker_results_heading' => 'Search results', + 'mod_picker_results_select' => 'Select', + 'mod_picker_results_title' => 'Title', + 'mod_picker_results_author' => 'Author', + 'mod_picker_action_add' => 'Add', + 'mod_picker_action_remove' => 'Remove', + 'mod_picker_status_loading' => 'Searching Steam Workshop…', + 'mod_picker_status_error' => 'Unable to contact the Steam Workshop. Try again in a moment.', + 'mod_picker_results_empty' => 'No workshop items matched that search.', + 'mod_picker_status_need_query' => 'Enter a Workshop ID or keyword before searching.', + 'mod_picker_toggle_label' => 'Sync', 'error_game_key_required' => 'Select a valid game key before editing the adapter.', 'error_adapter_delete_failed' => 'Adapter could not be deleted.', 'button_edit_adapter' => 'Edit', @@ -79,4 +99,5 @@ return [ 'label_adapter_hot_reload' => 'Supports hot reload', 'label_adapter_activation' => 'Activation template', 'label_adapter_notes' => 'Notes', + 'error_missing_query' => 'Enter a search term before querying the Workshop.', ]; diff --git a/modules/steam_workshop/lib/SteamWorkshopService.php b/modules/steam_workshop/lib/SteamWorkshopService.php index 9a559b07..1923deb8 100644 --- a/modules/steam_workshop/lib/SteamWorkshopService.php +++ b/modules/steam_workshop/lib/SteamWorkshopService.php @@ -145,7 +145,13 @@ class SteamWorkshopService { $input = $payload['workshop'] ?? []; $rawMods = trim((string)($input['raw_items'] ?? '')); - $items = $this->parseWorkshopItems($rawMods); + $selectedItems = $this->parseSelectedItemsJson((string)($input['selected_items'] ?? '')); + if (!empty($selectedItems)) { + $items = $selectedItems; + $rawMods = $this->serializeWorkshopItems($selectedItems); + } else { + $items = $this->parseWorkshopItems($rawMods); + } return [ 'workshop_enabled' => isset($input['workshop_enabled']) ? (bool)$input['workshop_enabled'] : false, @@ -568,6 +574,41 @@ class SteamWorkshopService return array_values(array_unique($keys)); } + public function getSteamAppIdForGameKey(string $gameKey): ?string + { + $xml = $this->loadServerConfigXml($gameKey); + if ($xml === null) { + return null; + } + + return $this->parseSteamAppIdFromConfig($xml); + } + + public function searchWorkshopItems(string $gameKey, string $query, int $limit = 12): array + { + $query = trim($query); + if ($query === '') { + return []; + } + + $appId = $this->getSteamAppIdForGameKey($gameKey); + if ($appId === null) { + return []; + } + + if (ctype_digit($query)) { + $item = $this->fetchWorkshopItemById($query); + return $item === null ? [] : [$item]; + } + + $html = $this->fetchWorkshopSearchHtml($appId, $query, $limit * 2); + if ($html === null) { + return []; + } + + return $this->extractWorkshopItemsFromHtml($html, $limit); + } + private function sanitizeInterval(?int $minutes): int { if ($minutes === null || $minutes <= 0) { @@ -617,6 +658,8 @@ class SteamWorkshopService $item['label'] = trim((string)($item['label'] ?? '')); $item['enabled'] = !empty($item['enabled']); $item['source'] = $item['source'] ?? 'manual'; + $item['author'] = trim((string)($item['author'] ?? '')); + $item['preview_url'] = trim((string)($item['preview_url'] ?? '')); return $item; }, $config['workshop_items']); @@ -735,6 +778,7 @@ class SteamWorkshopService file_put_contents($this->adapterMapFile, json_encode([], JSON_PRETTY_PRINT)); } } + public function gameSupportsWorkshop($serverXml): bool { if (!($serverXml instanceof SimpleXMLElement)) { @@ -778,6 +822,218 @@ class SteamWorkshopService return $candidate; } + private function loadServerConfigXml(string $gameKey): ?SimpleXMLElement + { + $gameKey = $this->sanitizeGameKey($gameKey); + if ($gameKey === '') { + return null; + } + + $directPath = sprintf('%s/%s.xml', $this->serverConfigDir, $gameKey); + if (is_file($directPath)) { + $xml = @simplexml_load_file($directPath); + if ($xml !== false) { + return $xml; + } + } + + foreach (glob($this->serverConfigDir . '/*.xml') as $file) { + $xml = @simplexml_load_file($file); + if ($xml === false) { + continue; + } + $configuredKey = isset($xml->game_key) ? $this->sanitizeGameKey((string)$xml->game_key) : ''; + if ($configuredKey === $gameKey) { + return $xml; + } + } + + return null; + } + + private function parseSelectedItemsJson(string $json): array + { + if ($json === '') { + return []; + } + + $decoded = json_decode($json, true); + if (!is_array($decoded)) { + return []; + } + + $result = []; + foreach ($decoded as $item) { + if (!is_array($item)) { + continue; + } + $id = preg_replace('/[^0-9]/', '', (string)($item['id'] ?? '')); + if ($id === '') { + continue; + } + $label = trim((string)($item['label'] ?? '')); + if ($label === '') { + $label = '@' . $id; + } + $result[$id] = [ + 'id' => $id, + 'label' => $label, + 'author' => trim((string)($item['author'] ?? '')), + 'preview_url' => trim((string)($item['preview_url'] ?? '')), + 'enabled' => isset($item['enabled']) ? (bool)$item['enabled'] : true, + 'source' => trim((string)($item['source'] ?? 'search')), + ]; + } + + return array_values($result); + } + + private function serializeWorkshopItems(array $items): string + { + $lines = []; + foreach ($items as $item) { + $id = preg_replace('/[^0-9]/', '', (string)($item['id'] ?? '')); + if ($id === '') { + continue; + } + $label = trim((string)($item['label'] ?? '')); + if ($label === '') { + $label = '@' . $id; + } + $lines[] = $id . ',' . $label; + } + + return implode(PHP_EOL, $lines); + } + + private function fetchWorkshopSearchHtml(string $appId, string $query, int $pageSize): ?string + { + $params = http_build_query([ + 'appid' => $appId, + 'searchtext' => $query, + 'numperpage' => max(5, $pageSize), + 'format' => 'json', + 'browsesort' => 'textsearch', + ]); + $url = 'https://steamcommunity.com/workshop/browse/?' . $params; + $response = $this->httpRequest($url); + if ($response === null) { + return null; + } + + $data = json_decode($response, true); + if (is_array($data) && isset($data['html'])) { + return (string)$data['html']; + } + + return $response; + } + + private function extractWorkshopItemsFromHtml(string $html, int $limit): array + { + libxml_use_internal_errors(true); + $doc = new DOMDocument(); + $doc->loadHTML('' . $html); + libxml_clear_errors(); + $xpath = new DOMXPath($doc); + $nodes = $xpath->query('//*[@data-publishedfileid]'); + $results = []; + + foreach ($nodes as $node) { + if (!($node instanceof DOMElement)) { + continue; + } + $id = $node->getAttribute('data-publishedfileid'); + if ($id === '') { + continue; + } + + $titleNode = $xpath->query('.//*[contains(@class,"workshopItemTitle")]', $node)->item(0); + $authorNode = $xpath->query('.//*[contains(@class,"workshopItemAuthorName")]', $node)->item(0); + $imgNode = $xpath->query('.//img[contains(@class,"workshopItemPreviewImage") or contains(@class,"workshopItemPreviewImageMain")]', $node)->item(0); + + $results[$id] = [ + 'id' => $id, + 'label' => trim($titleNode instanceof DOMNode ? $titleNode->textContent : ('@' . $id)), + 'author' => trim($authorNode instanceof DOMNode ? $authorNode->textContent : ''), + 'preview_url' => $imgNode instanceof DOMElement ? (string)$imgNode->getAttribute('src') : '', + 'enabled' => true, + 'source' => 'search', + ]; + + if (count($results) >= $limit) { + break; + } + } + + return array_values($results); + } + + private function fetchWorkshopItemById(string $id): ?array + { + $postData = http_build_query([ + 'itemcount' => 1, + 'publishedfileids[0]' => $id, + ]); + $response = $this->httpRequest('https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/', $postData); + if ($response === null) { + return null; + } + + $data = json_decode($response, true); + $details = $data['response']['publishedfiledetails'][0] ?? null; + if (!is_array($details) || (int)($details['result'] ?? 0) !== 1) { + return null; + } + + $title = trim((string)($details['title'] ?? '')); + if ($title === '') { + $title = '@' . $id; + } + + return [ + 'id' => $id, + 'label' => $title, + 'author' => (string)($details['creator'] ?? ''), + 'preview_url' => (string)($details['preview_url'] ?? ''), + 'enabled' => true, + 'source' => 'search', + ]; + } + + private function httpRequest(string $url, ?string $postFields = null): ?string + { + if (function_exists('curl_init')) { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_USERAGENT, 'GSP-Workshop/1.0 (+https://github.com/GameServerPanel/GSP)'); + if ($postFields !== null) { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields); + } + $response = curl_exec($ch); + curl_close($ch); + return is_string($response) ? $response : null; + } + + $contextOptions = [ + 'http' => [ + 'method' => $postFields !== null ? 'POST' : 'GET', + 'timeout' => 10, + 'header' => "User-Agent: GSP-Workshop/1.0 (+https://github.com/GameServerPanel/GSP)\r\n" . ($postFields !== null ? "Content-Type: application/x-www-form-urlencoded\r\n" : ''), + ], + ]; + if ($postFields !== null) { + $contextOptions['http']['content'] = $postFields; + } + $context = stream_context_create($contextOptions); + $result = @file_get_contents($url, false, $context); + + return $result === false ? null : $result; + } + private function buildWorkshopGroupKey(string $appId): string { return 'steamapp_' . $appId; diff --git a/modules/steam_workshop/steam_workshop.css b/modules/steam_workshop/steam_workshop.css index 89c0ac9d..6a6b6fe3 100644 --- a/modules/steam_workshop/steam_workshop.css +++ b/modules/steam_workshop/steam_workshop.css @@ -108,12 +108,22 @@ background: #fff; } +.btn.btn-xs { + padding: 0.2rem 0.45rem; + font-size: 0.85rem; +} + .btn.primary { background: #0b5ed7; border-color: #0a58ca; color: #fff; } +.btn.secondary { + background: #f7f7f7; + border-color: #c7c7c7; +} + .sw-toggle { display: flex; align-items: center; @@ -227,3 +237,171 @@ .sw-admin__mapping-actions { margin-top: 1rem; } + +.sw-picker { + display: flex; + flex-direction: column; + gap: 1rem; + border: 1px solid #dcdcdc; + border-radius: 8px; + padding: 1rem; + background: #fff; +} + +.sw-picker__header { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.sw-picker__header h4 { + margin: 0; + font-size: 1.1rem; +} + +.sw-picker__hint { + margin: 0.25rem 0 0; + color: #555; + font-size: 0.9rem; +} + +.sw-picker__search { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: flex-end; +} + +.sw-picker__search label { + flex: 1; +} + +.sw-picker__search-input { + width: 100%; + padding: 0.4rem; + border-radius: 4px; + border: 1px solid #c7c7c7; +} + +.sw-picker__status { + min-height: 1.25rem; + font-size: 0.9rem; +} + +.sw-picker__status--loading { + color: #0b5ed7; +} + +.sw-picker__status--error { + color: #c0392b; +} + +.sw-picker__status--info { + color: #555; +} + +.sw-picker__status--clear { + color: inherit; +} + +.sw-picker__selected, +.sw-picker__results { + border: 1px solid #e3e3e3; + border-radius: 6px; + padding: 0.75rem; +} + +.sw-picker__selected-header { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 0.5rem; +} + +.sw-picker__selected-header small { + color: #666; +} + +.sw-picker__chip-list { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-top: 0.5rem; +} + +.sw-picker__chip { + display: flex; + justify-content: space-between; + gap: 0.75rem; + border: 1px solid #d0dae9; + border-radius: 6px; + padding: 0.5rem 0.75rem; + background: #f8fafd; +} + +.sw-picker__chip-text { + display: flex; + flex-direction: column; + gap: 0.1rem; +} + +.sw-picker__chip-text span { + color: #555; + font-size: 0.85rem; +} + +.sw-picker__chip-controls { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.sw-picker__chip-remove { + border: 1px solid #c0392b; + background: #fff5f4; + color: #c0392b; + border-radius: 4px; + padding: 0.25rem 0.6rem; + cursor: pointer; +} + +.sw-picker__toggle { + display: inline-flex; + align-items: center; + gap: 0.35rem; + font-size: 0.85rem; +} + +.sw-picker__results-table { + width: 100%; + border-collapse: collapse; +} + +.sw-picker__results-table th, +.sw-picker__results-table td { + border: 1px solid #e3e3e3; + padding: 0.4rem; + vertical-align: top; +} + +.sw-picker__result-meta { + color: #666; + font-size: 0.85rem; +} + +.sw-picker__results-table-wrapper { + overflow-x: auto; +} + +.sw-picker__action { + padding: 0.25rem 0.5rem; + border-radius: 4px; + background: #0b5ed7; + border: 1px solid #0a58ca; + color: #fff; +} + +.sw-picker__empty { + color: #777; + font-size: 0.9rem; +} diff --git a/modules/steam_workshop/steam_workshop.js b/modules/steam_workshop/steam_workshop.js new file mode 100644 index 00000000..19c5cf52 --- /dev/null +++ b/modules/steam_workshop/steam_workshop.js @@ -0,0 +1,346 @@ +(function () { + 'use strict'; + + var Picker = /** @class */ (function () { + function Picker(root) { + this.root = root; + this.endpoint = root.getAttribute('data-endpoint') || ''; + this.lang = { + add: root.getAttribute('data-lang-add') || 'Add', + remove: root.getAttribute('data-lang-remove') || 'Remove', + loading: root.getAttribute('data-lang-loading') || 'Loading…', + error: root.getAttribute('data-lang-error') || 'Something went wrong.', + empty: root.getAttribute('data-lang-empty') || 'No results found.', + query: root.getAttribute('data-lang-query') || 'Enter a Workshop ID or keyword.', + sync: root.getAttribute('data-lang-sync') || 'Sync', + }; + this.selectedInput = root.querySelector('.js-sw-selected-input'); + this.selectedList = root.querySelector('.js-sw-selected-list'); + this.resultsBody = root.querySelector('.js-sw-results'); + this.statusEl = root.querySelector('.js-sw-picker-status'); + this.searchForm = root.querySelector('.js-sw-search-form'); + this.searchInput = root.querySelector('.js-sw-search-input'); + this.searchButton = root.querySelector('.js-sw-search-button'); + this.state = { + selected: this.readInitialSelection(), + }; + this.lastResults = []; + this.bindEvents(); + this.renderSelected(); + } + Picker.prototype.readInitialSelection = function () { + if (!this.selectedInput) { + return []; + } + try { + var parsed = JSON.parse(this.selectedInput.value || '[]'); + if (Array.isArray(parsed)) { + return parsed.filter(function (item) { return item && item.id; }) + .map(function (item) { return ({ + id: String(item.id), + label: String(item.label || ('@' + item.id)), + author: String(item.author || ''), + preview_url: String(item.preview_url || ''), + enabled: !(item.enabled === false || item.enabled === 'false' || item.enabled === 0 || item.enabled === '0'), + source: String(item.source || 'manual'), + }); }); + } + } + catch (err) { + console.warn('Invalid Workshop JSON state', err); + } + return []; + }; + Picker.prototype.bindEvents = function () { + var _this = this; + if (this.searchForm && this.searchForm.tagName === 'FORM') { + this.searchForm.addEventListener('submit', function (event) { + event.preventDefault(); + _this.performSearch(); + }); + } + if (this.searchButton) { + this.searchButton.addEventListener('click', function (event) { + event.preventDefault(); + _this.performSearch(); + }); + } + if (this.searchInput && (!this.searchForm || this.searchForm.tagName !== 'FORM')) { + this.searchInput.addEventListener('keydown', function (event) { + if (event.key === 'Enter') { + event.preventDefault(); + _this.performSearch(); + } + }); + } + if (this.selectedList) { + this.selectedList.addEventListener('click', function (event) { + var target = event.target; + if (!(target instanceof HTMLElement)) { + return; + } + if (target.matches('.js-sw-remove')) { + var id = target.getAttribute('data-id'); + if (id) { + _this.removeSelected(id); + } + } + }); + this.selectedList.addEventListener('change', function (event) { + var target = event.target; + if (!(target instanceof HTMLInputElement)) { + return; + } + if (target.matches('.js-sw-toggle')) { + var id = target.getAttribute('data-id'); + if (id) { + _this.toggleSelected(id, target.checked); + } + } + }); + } + if (this.resultsBody) { + this.resultsBody.addEventListener('click', function (event) { + var target = event.target; + if (!(target instanceof HTMLElement)) { + return; + } + if (target.matches('.js-sw-add')) { + var payload = target.getAttribute('data-payload'); + if (payload) { + try { + var data = JSON.parse(payload); + _this.addSelected(data); + } + catch (err) { + console.warn('Invalid payload', err); + } + } + } + }); + } + }; + Picker.prototype.performSearch = function () { + var _this = this; + if (!this.endpoint || !this.searchInput) { + return; + } + var term = this.searchInput.value.trim(); + if (!term) { + this.setStatus(this.lang.query, 'error'); + return; + } + if (this.isSearching) { + return; + } + this.isSearching = true; + this.setStatus(this.lang.loading, 'loading'); + var url = this.endpoint + '&q=' + encodeURIComponent(term); + fetch(url, { + headers: { 'Accept': 'application/json' }, + }) + .then(function (response) { + if (!response.ok) { + throw new Error('HTTP ' + response.status); + } + return response.json(); + }) + .then(function (data) { + if (!data || data.ok === false) { + var message = (data && data.error) ? data.error : _this.lang.error; + _this.setStatus(message, 'error'); + _this.renderResults([]); + return; + } + if (Array.isArray(data.results) && data.results.length) { + _this.setStatus('', 'clear'); + _this.renderResults(data.results); + } + else { + _this.setStatus(_this.lang.empty, 'info'); + _this.renderResults([]); + } + }) + .catch(function (error) { + console.error('Workshop search failed', error); + _this.setStatus(_this.lang.error, 'error'); + _this.renderResults([]); + }) + .finally(function () { + _this.isSearching = false; + }); + }; + Picker.prototype.setStatus = function (message, kind) { + if (!this.statusEl) { + return; + } + this.statusEl.textContent = message || ''; + this.statusEl.className = 'sw-picker__status js-sw-picker-status' + (kind ? ' sw-picker__status--' + kind : ''); + }; + Picker.prototype.renderResults = function (results) { + if (!this.resultsBody) { + return; + } + this.resultsBody.innerHTML = ''; + if (!Array.isArray(results) || !results.length) { + this.lastResults = []; + return; + } + this.lastResults = results.slice(); + var _loop_1 = function (item) { + var normalized = { + id: String(item.id), + label: String(item.label || ('@' + item.id)), + author: String(item.author || ''), + preview_url: String(item.preview_url || ''), + enabled: true, + source: String(item.source || 'search'), + }; + var row = document.createElement('tr'); + var selectCell = document.createElement('td'); + var action = document.createElement('button'); + action.type = 'button'; + action.className = 'btn btn-xs sw-picker__action js-sw-add'; + action.textContent = this_1.lang.add; + action.setAttribute('data-payload', JSON.stringify(normalized)); + if (this_1.isSelected(normalized.id)) { + action.disabled = true; + } + selectCell.appendChild(action); + var titleCell = document.createElement('td'); + titleCell.innerHTML = '' + this_1.escape(normalized.label) + '
'; + var authorCell = document.createElement('td'); + authorCell.textContent = normalized.author; + row.appendChild(selectCell); + row.appendChild(titleCell); + row.appendChild(authorCell); + this_1.resultsBody.appendChild(row); + }; + var this_1 = this; + for (var _i = 0, results_1 = results; _i < results_1.length; _i++) { + var item = results_1[_i]; + _loop_1(item); + } + }; + Picker.prototype.isSelected = function (id) { + return this.state.selected.some(function (item) { return item.id === id; }); + }; + Picker.prototype.addSelected = function (item) { + if (!item || !item.id || this.isSelected(String(item.id))) { + return; + } + this.state.selected.push({ + id: String(item.id), + label: String(item.label || ('@' + item.id)), + author: String(item.author || ''), + preview_url: String(item.preview_url || ''), + enabled: true, + source: String(item.source || 'search'), + }); + this.persist(); + this.renderSelected(); + this.renderResults(this.lastResults || []); + }; + Picker.prototype.removeSelected = function (id) { + var next = this.state.selected.filter(function (item) { return item.id !== id; }); + this.state.selected = next; + this.persist(); + this.renderSelected(); + this.renderResults(this.lastResults || []); + }; + Picker.prototype.toggleSelected = function (id, enabled) { + var changed = false; + this.state.selected = this.state.selected.map(function (item) { + if (item.id === id) { + changed = true; + return { + id: item.id, + label: item.label, + author: item.author, + preview_url: item.preview_url, + enabled: enabled, + source: item.source, + }; + } + return item; + }); + if (changed) { + this.persist(); + } + }; + Picker.prototype.renderSelected = function () { + if (!this.selectedList) { + return; + } + this.selectedList.innerHTML = ''; + if (!this.state.selected.length) { + var emptyText = this.selectedList.getAttribute('data-empty-text') || ''; + if (emptyText) { + var empty = document.createElement('div'); + empty.className = 'sw-picker__empty'; + empty.textContent = emptyText; + this.selectedList.appendChild(empty); + } + return; + } + var this_2 = this; + for (var _i = 0, _a = this.state.selected; _i < _a.length; _i++) { + var item = _a[_i]; + var chip = document.createElement('div'); + chip.className = 'sw-picker__chip'; + chip.innerHTML = '