PluginProbe
PowerPress Podcasting plugin by Blubrry / trunk
PowerPress Podcasting plugin by Blubrry vtrunk
11.17.9 11.17.8 11.17.7 11.17.6 11.17.4 11.17.3 11.17.2 11.17.1 11.17 11.16.11 11.16.10 11.16.9 11.16.8 11.16.7 11.16.6 11.16.5 11.16.4 11.16.3 11.16.2 11.16.1 11.9.13 11.9.14 11.9.15 11.9.16 11.9.17 All 383 releases
powerpress / js / network.js

network.js in PowerPress Podcasting plugin by Blubrry trunk, at js/network.js

726 lines 24.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * PowerPress Network Admin
3 *
4 * TODO: convert var -> const/let
5 *
6 * SECTIONS:
7 * LOCALIZATION
8 * DIALOG MODAL
9 * PAGE TYPE CONFIG
10 * FORM SUBMISSION
11 * TAB NAVIGATION
12 * PAGE HANDLING
13 * LIST MANAGEMENT
14 * SEARCH / FILTER
15 * APPLICATION APPROVAL
16 * PROGRAM MANAGEMENT
17 * SETTINGS
18 * INIT
19 * EVENT DELEGATION
20 */
21 import { ppCopyText, showToast, initCharCounter } from './utils/dom-utils.js';
22 import { escapeHtml } from './utils/sanitize.js';
23
24 // ============
25 // LOCALIZATION
26 // ============
27
28 // wp.i18n wrapper w/ graceful fallback
29 var __ = (typeof wp !== 'undefined' && wp.i18n && wp.i18n.__)
30 ? wp.i18n.__
31 : function(s) { return s; };
32
33 // DIALOG MODAL
34
35 function ppnDialog(el) {
36 var dialogId = el.dataset.ppnDialog || el.getAttribute('data-ppn-dialog');
37 if (!dialogId) return;
38 var dialog = document.getElementById(dialogId);
39 if (dialog && typeof dialog.showModal === 'function') {
40 dialog.showModal();
41 }
42 }
43
44 function ppnDialogClose(el) {
45 var dialog = el.closest('dialog');
46 if (dialog) dialog.close();
47 }
48
49
50 // ================
51 // PAGE TYPE CONFIG
52 // ================
53
54 /** @type {Object<string, {shortcode: function, defaultTitle: function, hasTargetId: boolean}>} */
55 var PAGE_TYPES = {
56 Program: {
57 shortcode: function(id) { return `[ppn-program id = ${id}]`; },
58 defaultTitle: function(id) { return `program id = ${id}`; },
59 hasTargetId: true
60 },
61 List: {
62 shortcode: function(id) { return `[ppn-gridview id="${id}" rows="100" cols="3"]`; },
63 defaultTitle: function(id) { return `list id = ${id}`; },
64 hasTargetId: true
65 },
66 Application: {
67 shortcode: function() { return '[ppn-application]'; },
68 defaultTitle: function() { return 'Application Page'; },
69 hasTargetId: false
70 },
71 Homepage: {
72 shortcode: function() { return '[ppn-gridview id="all" rows="100" cols="3"]\n\n[ppn-list id="all" style="detailed"]'; },
73 defaultTitle: function() { return 'Network Page' },
74 hasTargetId: false
75 }
76 };
77
78
79 // ===============
80 // FORM SUBMISSION
81 // ===============
82
83 /** post formdata to current page, returns true on success */
84 async function _ppnPost(body) {
85 try {
86 var resp = await fetch(window.location.href, { method: 'POST', body: body });
87 if (!resp.ok) throw new Error('request failed');
88 return true;
89 } catch (e) {
90 alert(__('Something went wrong. Please refresh and try again.'));
91 return false;
92 }
93 }
94
95 function _appendHidden(form, name, value) {
96 var input = document.createElement('input');
97 input.type = 'hidden';
98 input.name = name;
99 if (value !== undefined) input.value = value;
100 form.appendChild(input);
101 }
102
103 /** generic action: ajax mode (fetch + reload tab) or navigate mode (form submit) */
104 async function _ppnAction(el) {
105 // 1) RESOLVE FORM
106 var formId = el.dataset.form;
107 var form = formId ? document.getElementById(formId) : el.closest('form');
108 if (!form) return;
109
110 // 2) CONFIRM
111 if (el.hasAttribute('data-confirm')) {
112 var msg = el.dataset.confirm || __('Are you sure?');
113 if (!confirm(msg)) return;
114 }
115
116 // 3) SET FIELD VALUES
117 if (el.dataset.setField) {
118 var target = document.getElementById(el.dataset.setField);
119 if (target) target.value = el.dataset.setValue || '';
120 }
121
122 // 4) APPEND DATA-FIELDS ("key:val,key:val")
123 if (el.dataset.fields) {
124 el.dataset.fields.split(',').forEach(function(pair) {
125 var parts = pair.split(':');
126 var existing = form.querySelector('[name="' + parts[0] + '"]');
127 if (existing) {
128 existing.value = parts[1] || '';
129 } else {
130 _appendHidden(form, parts[0], parts[1] || '');
131 }
132 });
133 }
134
135 // 5) APPEND FLAGS + NONCE
136 if (el.dataset.change === 'true' && !form.querySelector('[name="changeOrCreate"]')) {
137 _appendHidden(form, 'changeOrCreate', 'true');
138 }
139 if (typeof ppnNonce !== 'undefined' && !form.querySelector('[name="_ppn_nonce"]')) {
140 _appendHidden(form, '_ppn_nonce', ppnNonce);
141 }
142
143 // 6) NAVIGATE MODE: set action url + submit
144 if (el.dataset.navigate) {
145 var tab = el.dataset.tab ? '&tab=' + el.dataset.tab : '';
146 form.setAttribute('action', `?page=network-plugin&status=${el.dataset.navigate}${tab}`);
147 form.submit();
148 return;
149 }
150
151 // 7) AJAX MODE: post + reload tab
152 var ok = await _ppnPost(new FormData(form));
153 if (ok && el.dataset.tab) _reloadTab(el.dataset.tab);
154 }
155
156
157 // ==============
158 // TAB NAVIGATION
159 // ==============
160
161 function showPPNTab(application) {
162 var x = document.getElementsByClassName("tabContent");
163 for (var i = 0; i < x.length; i++) {
164 x[i].style.display = "none";
165 }
166 var tabContent = document.getElementById(application);
167 if (!tabContent) return;
168 tabContent.style.display = "block";
169 document.querySelectorAll('.tabActive').forEach(function(el) {
170 el.classList.add('tabInactive');
171 el.classList.remove('tabActive');
172 });
173 var tab = document.getElementById(`${application}Tab`);
174 if (tab) {
175 tab.classList.remove('tabInactive');
176 tab.classList.add('tabActive');
177 }
178 }
179
180
181 // =============
182 // PAGE HANDLING
183 // =============
184
185 /** ajax: link or create a page via ppn_page_action */
186 async function _ppnPageAction(el) {
187 const mode = el.dataset.mode;
188 const target = el.dataset.target || '';
189 const body = new FormData();
190 body.append('action', 'ppn_page_action');
191 body.append('nonce', ppnNonce);
192
193 // 1) BUILD REQUEST
194 if (mode === 'link') {
195 const form = el.dataset.form ? document.getElementById(el.dataset.form) : el.closest('form');
196 if (!form) return;
197 for (const [key, val] of new FormData(form)) body.append(key, val);
198 body.append('mode', 'link');
199 } else {
200 const config = PAGE_TYPES[target];
201 if (!config) return;
202 body.append('mode', 'create');
203 body.append('target', target);
204 body.append('content', config.shortcode(el.dataset.id));
205 body.append('pageTitle', el.dataset.title || config.defaultTitle(el.dataset.id));
206 if (config.hasTargetId && el.dataset.id) body.append('targetId', el.dataset.id);
207 }
208
209 // 2) FETCH
210 let result;
211 try {
212 const resp = await fetch(ajaxurl, { method: 'POST', body: body });
213 const data = await resp.json();
214 if (!data.success) throw new Error();
215 result = data.data;
216 } catch (e) {
217 alert(__('Something went wrong. Please refresh and try again.'));
218 return;
219 }
220
221 const dialog = document.querySelector('dialog[open]');
222 if (dialog) dialog.close();
223
224 // 3) UPDATE UI
225 switch (mode) {
226 case 'link':
227 case 'dialog': {
228 const linkRow = document.getElementById('ppn-page-link-row');
229 if (!linkRow) {
230 const tabMap = { Program: 'programs', List: 'groups', Application: 'applications' };
231 _reloadTab(tabMap[target || body.get('target')]);
232 break;
233 }
234
235 const input = document.getElementById('ppn-page-link-input');
236 if (input) input.value = result.permalink;
237
238 linkRow.querySelectorAll('.ppn-page-link-view').forEach(el => el.remove());
239
240 const viewLink = document.createElement('a');
241 viewLink.href = result.permalink;
242 viewLink.target = '_blank';
243 viewLink.className = 'ppn-page-link-view';
244 viewLink.title = __('View Page');
245 viewLink.innerHTML = '<i class="material-icons-outlined">open_in_new</i>';
246 linkRow.appendChild(viewLink);
247
248 const editLink = document.createElement('a');
249 editLink.href = result.edit_url;
250 editLink.target = '_blank';
251 editLink.className = 'ppn-page-link-view';
252 editLink.title = __('Edit Page');
253 editLink.innerHTML = '<i class="material-icons-outlined">edit</i>';
254 linkRow.appendChild(editLink);
255
256 const linkBtn = document.getElementById('ppn-link-page-btn');
257 if (linkBtn) linkBtn.textContent = __('Change Page');
258 break;
259 }
260
261 case 'list':
262 _reloadTab({ Program: 'programs', List: 'groups', Application: 'applications' }[target]);
263 break;
264
265 case 'group': {
266 const status = document.getElementById(`ppn-show-status-${el.dataset.id}`);
267 if (status) {
268 status.className = 'ppn-page-status';
269 status.innerHTML = `<a class="ppn-page-status--linked" target="_blank" href="${result.permalink}">${__('View Page')}</a>`;
270 }
271 el.remove();
272 break;
273 }
274
275 case 'singleton': {
276 const link = document.createElement('a');
277 link.className = 'button';
278 link.href = result.edit_url;
279 link.target = '_blank';
280 link.textContent = el.dataset.editLabel || __('Edit Page');
281 el.replaceWith(link);
282 break;
283 }
284 }
285 }
286
287
288 // ===============
289 // LIST MANAGEMENT
290 // ===============
291
292 /** toggle show in/out of group list, update sidebar preview */
293 function updateListOfShows(el) {
294 var inList = el.checked;
295 var title = el.dataset.title;
296 var list = document.getElementById('shows-in-group');
297 if (!list) return;
298
299 ppnMarkUnsaved();
300
301 if (inList) {
302 var empty = list.querySelector('.ppn-manage__show-item--empty');
303 if (empty) empty.remove();
304
305 const programId = el.dataset.programId;
306 var li = document.createElement('li');
307 li.className = 'ppn-manage__show-item';
308 li.dataset.title = title;
309 li.textContent = title;
310
311 if (el.dataset.hasPage === '0') {
312 const status = document.createElement('span');
313 status.id = `ppn-show-status-${programId}`;
314 status.className = 'ppn-page-status ppn-page-status--missing';
315 status.textContent = __('Not Linked');
316 li.appendChild(status);
317
318 const btn = document.createElement('button');
319 btn.type = 'button';
320 btn.className = 'ppn-icon-btn';
321 btn.dataset.ppnAction = 'ppnPageAction';
322 btn.dataset.mode = 'group';
323 btn.dataset.target = 'Program';
324 btn.dataset.id = el.dataset.programId;
325 btn.dataset.title = title;
326 btn.innerHTML = '<i class="material-icons-outlined">note_add</i>';
327 li.appendChild(btn);
328 }
329
330 list.appendChild(li);
331 } else {
332 list.querySelectorAll('.ppn-manage__show-item').forEach(function(item) {
333 if (item.dataset.title === title) item.remove();
334 });
335 if (!list.querySelector('.ppn-manage__show-item')) {
336 var emptyLi = document.createElement('li');
337 emptyLi.className = 'ppn-manage__show-item--empty';
338 emptyLi.textContent = 'No shows in this group yet';
339 list.appendChild(emptyLi);
340 }
341 }
342 }
343
344 function ppnMarkUnsaved() {
345 var indicator = document.getElementById('ppn-unsaved-indicator');
346 if (indicator) indicator.style.display = '';
347 }
348
349
350 // ===============
351 // SEARCH / FILTER
352 // ===============
353
354 /** filter show checkboxes by title (managelist.php) */
355 function filterShows(el) {
356 var searchText = el.value.toLowerCase();
357 var rows = document.querySelectorAll('.show-row');
358 rows.forEach(function(row) {
359 var title = (row.getAttribute('data-title') || '').toLowerCase();
360 row.style.display = title.includes(searchText) ? '' : 'none';
361 });
362 }
363
364 /** filter list rows by data-title within target container, skip dividers */
365 function filterList(el) {
366 var searchText = el.value.toLowerCase();
367 var container = document.getElementById(el.dataset.ppnTarget);
368 if (!container) return;
369
370 // skip empty rows + dividers
371 var rows = container.querySelectorAll('.ppn-list__row:not(.ppn-list__divider):not(.ppn-list__empty)');
372 rows.forEach(function(row) {
373 var title = (row.getAttribute('data-title') || '').toLowerCase();
374 row.style.display = title.includes(searchText) ? '' : 'none';
375 });
376 }
377
378
379 // ====================
380 // APPLICATION APPROVAL
381 // ====================
382
383 function _appStatusSelectHtml(applicantId, selectedValue) {
384 var safeId = escapeHtml(String(applicantId));
385 var attrs = `data-ppn-action="approveProgram" data-applicant-id="${safeId}"`;
386 var pSel = selectedValue === '0' ? ' selected' : '';
387 var aSel = selectedValue === '1' ? ' selected' : '';
388 var rSel = selectedValue === '-1' ? ' selected' : '';
389 return `<select ${attrs} class="application-dropdown"><option value="0"${pSel}>${__('Pending')}</option><option value="1"${aSel}>${__('Approve')}</option><option value="-1"${rSel}>${__('Reject')}</option></select>`;
390 }
391
392 function _appStatusLabelHtml(selectedValue) {
393 var label = selectedValue === '1' ? __('Approved') : (selectedValue === '-1' ? __('Rejected') : __('Pending'));
394 var mod = selectedValue === '1' ? 'approved' : (selectedValue === '-1' ? 'rejected' : 'pending');
395 return `<span class="ppn-app-status-label ppn-app-status-label--${mod}">${label}</span>`;
396 }
397
398 /** approve/reject/undo/delete applicant via ajax, sync table + modal */
399 async function approveProgram(el) {
400 var applicantId = el.dataset.applicantId;
401 var isDelete = el.dataset.delete === 'true';
402
403 // 1) DELETE VIA BUTTON
404 if (isDelete) {
405 if (!confirm(__('Are you sure you want to delete this application?'))) return;
406
407 var body = new FormData();
408 body.append('appAction', 'delete');
409 body.append('applicantId', applicantId);
410 body.append('changeOrCreate', 'true');
411 if (typeof ppnNonce !== 'undefined') body.append('_ppn_nonce', ppnNonce);
412
413 if (!await _ppnPost(body)) return;
414
415 // remove row + close/remove dialog
416 var tableCell = document.getElementById(`app-status-${applicantId}`);
417 if (tableCell) {
418 var row = tableCell.closest('.ppn-list__row');
419 if (row) row.remove();
420 }
421 var openDialog = document.querySelector(`dialog#ppn-app-${applicantId}`);
422 if (openDialog && openDialog.open) openDialog.close();
423 var modalWrap = document.getElementById(`ppn-app-${applicantId}`);
424 if (modalWrap) modalWrap.remove();
425 return;
426 }
427
428 // 2) STATUS CHANGE VIA SELECT
429 if (el.tagName !== 'SELECT') return;
430 var selectedValue = el.value;
431 var action;
432 if (selectedValue === '1') action = 'approve';
433 else if (selectedValue === '-1') action = 'disapprove';
434 else action = 'undo';
435
436 var body = new FormData();
437 body.append('appAction', action);
438 body.append('applicantId', applicantId);
439 body.append('changeOrCreate', 'true');
440 if (typeof ppnNonce !== 'undefined') body.append('_ppn_nonce', ppnNonce);
441
442 if (!await _ppnPost(body)) return;
443
444 // 3) SYNC UI: table label + modal select, reload shows tab
445 var tableCell = document.getElementById(`app-status-${applicantId}`);
446 var modalCell = document.getElementById(`app-modal-status-${applicantId}`);
447 if (tableCell) tableCell.innerHTML = _appStatusLabelHtml(selectedValue);
448 if (modalCell) modalCell.innerHTML = _appStatusSelectHtml(applicantId, selectedValue);
449 _reloadTab('programs');
450 }
451
452 /** refetch page html and swap tab content by id */
453 async function _reloadTab(tabId) {
454 try {
455 // 1) BUILD URL w/ tab param
456 var tabParam = tabId === 'programs' ? 'shows' : tabId;
457 var url = window.location.href.split('#')[0];
458 if (url.indexOf('tab=') === -1) {
459 url += (url.indexOf('?') === -1 ? '?' : '&') + 'tab=' + tabParam;
460 }
461
462 // 2) FETCH + PARSE response html
463 var resp = await fetch(url, { credentials: 'same-origin' });
464 if (!resp.ok) return;
465 var doc = new DOMParser().parseFromString(await resp.text(), 'text/html');
466
467 // 3) SWAP tab content or full reload as fallback
468 var fresh = doc.getElementById(tabId);
469 var current = document.getElementById(tabId);
470 if (fresh && current) {
471 current.querySelectorAll('dialog[open]').forEach(function(d) { d.close(); });
472 current.innerHTML = fresh.innerHTML;
473 } else {
474 location.reload();
475 }
476 } catch (e) {
477 // silent fail, updates on next page load
478 }
479 }
480
481
482 // ==================
483 // PROGRAM MANAGEMENT
484 // ==================
485
486 /** open page-select dialog pre-filled w/ program shortcode */
487 function editPageForProgram(el) {
488 var programId = el.dataset.programId;
489 var programTitle = el.dataset.programTitle || '';
490 document.getElementById('select-page-target-id').value = programId;
491 document.getElementById('page-select-ppn').value = el.dataset.linkPage;
492 document.getElementById('ppn-program-shortcode').value = `[ppn-program id="${programId}"]`;
493 // populate create-new-page btn w/ program context
494 var createBtn = document.getElementById('ppn-create-page-btn');
495 if (createBtn) {
496 createBtn.dataset.id = programId;
497 createBtn.dataset.title = programTitle;
498 }
499
500 const editLink = document.getElementById('ppn-edit-page-link');
501 if (editLink) {
502 if (el.dataset.linkPage) {
503 editLink.href = `post.php?post=${el.dataset.linkPage}&action=edit`;
504 editLink.style.display = '';
505 } else {
506 editLink.style.display = 'none';
507 }
508 }
509
510 var dialog = document.getElementById('selectPageBox');
511 if (dialog) dialog.showModal();
512 }
513
514 function addToGroup(el) {
515 var programId = el.dataset.programId;
516 var input = document.getElementById('add-program-to-group');
517 if (input) input.value = programId;
518 var dialog = document.getElementById('addToGroup');
519 if (dialog) dialog.showModal();
520 }
521
522
523 // ========
524 // SETTINGS
525 // ========
526
527 /** save tos url via fetch, show saved/error feedback */
528 function saveTosUrl(el) {
529 const form = document.getElementById(el.dataset.formId);
530 if (!form) return;
531 const btn = form.querySelector('button[data-ppn-action="saveTosUrl"]');
532 if (!btn) return;
533 const originalText = btn.textContent;
534
535 btn.textContent = 'Saving...';
536 btn.disabled = true;
537
538 fetch(window.location.href, {
539 method: 'POST',
540 body: new FormData(form)
541 }).then(function(response) {
542 if (response.ok) {
543 btn.textContent = 'Saved!';
544
545 // clear error message if existing
546 const existing = document.getElementById('ppn-tos-error');
547 if (existing) existing.remove();
548
549 setTimeout(function() {
550 btn.textContent = originalText;
551 btn.disabled = false;
552 }, 2000);
553 } else {
554 btn.textContent = 'Error';
555 btn.disabled = false;
556 const existing = document.getElementById('ppn-tos-error');
557 if (existing) existing.remove();
558 const errDiv = document.createElement('div');
559 errDiv.id = 'ppn-tos-error';
560 errDiv.className = 'error powerpress-error inline';
561 errDiv.textContent = 'Please enter a valid URL for your Terms of Service';
562 form.parentNode.insertBefore(errDiv, form);
563
564 setTimeout(function() {
565 btn.textContent = originalText;
566 btn.disabled = false;
567 }, 2000);
568 }
569 }).catch(function() {
570 btn.textContent = 'Error';
571 btn.disabled = false;
572 });
573 }
574
575
576 // ====
577 // INIT
578 // ====
579
580 // strip irrelevant fields from submit application form
581 function ppnInitSubmitApp() {
582 var feedUrl = document.getElementById('feedUrl');
583 if (!feedUrl) return;
584 if (!feedUrl.hasAttribute('readonly')) {
585 var addInfo = document.getElementById('addInfo');
586 if (addInfo) addInfo.remove();
587 } else {
588 var find = document.getElementById('findProgram');
589 var back = document.getElementById('backProgram');
590 if (find) find.remove();
591 if (back) back.remove();
592 }
593 }
594
595 async function addProgramToNetwork(el) {
596 const icon = document.getElementById(el.id);
597
598 // ignore clicks on shows that have already been added
599 if(!icon.src.includes('circlecheck_blue')){
600 const body = new FormData();
601 body.append('action', 'add_program_to_network');
602 body.append('nonce', ppnNonce);
603 body.append('program_id', el.dataset.programId);
604 body.append('network_id', el.dataset.networkId);
605 body.append('program_title', el.dataset.title || '');
606
607 const resp = await fetch(ajaxurl, { method: 'POST', body: body });
608 const data = await resp.json();
609
610 if(data.success === true){
611 icon.src = el.dataset.updatedSrc;
612 if(data.data && data.data.message) showToast(data.data.message, { id: 'ppn-add-toast' });
613 } else {
614 // do we need any error messaging?
615 }
616 }
617 }
618
619
620 // ================
621 // EVENT DELEGATION
622 // ================
623
624 document.addEventListener('DOMContentLoaded', function() {
625 // data-ppn-action -> handler map
626 var handlers = {
627 ppnAction: _ppnAction,
628 ppnPageAction: _ppnPageAction,
629 showPPNTab: function(el) { showPPNTab(el.dataset.tab); },
630 approveProgram: approveProgram,
631 editPageForProgram: editPageForProgram,
632 addToGroup: addToGroup,
633 ppCopyText: function(el) { ppCopyText(el.dataset.inputId, el); },
634 ppnDialog: ppnDialog,
635 ppnDialogClose: ppnDialogClose,
636 saveTosUrl: saveTosUrl,
637 updateListOfShows: updateListOfShows,
638 filterShows: filterShows,
639 filterList: filterList,
640 addProgramToNetwork: addProgramToNetwork
641 };
642
643 function dispatch(action, el) {
644 if (handlers[action]) handlers[action](el);
645 }
646
647 // CLICK DELEGATION (backdrop close, toggle, action dispatch)
648 document.addEventListener('click', function(e) {
649 // backdrop click closes open dialogs
650 if (e.target.tagName === 'DIALOG' && e.target.open) {
651 e.target.close();
652 return;
653 }
654
655 // collapse/expand toggle
656 var toggle = e.target.closest('.ppn-toggle');
657 if (toggle) {
658 var collapsed = toggle.classList.toggle('ppn-toggle--collapsed');
659 var chevron = toggle.querySelector('.ppn-toggle__chevron');
660 if (chevron) chevron.textContent = collapsed ? 'expand_more' : 'expand_less';
661 return;
662 }
663
664 // action dispatch
665 var el = e.target.closest('[data-ppn-action]');
666 if (!el) return;
667 var action = el.dataset.ppnAction;
668 // skip change/keyup only handlers
669 if (action === 'updateListOfShows' || action === 'filterShows' || action === 'filterList') return;
670 // selects handle via change event
671 if (el.tagName === 'SELECT') return;
672 e.preventDefault();
673 dispatch(action, el);
674 });
675
676 // CHANGE DELEGATION
677 document.addEventListener('change', function(e) {
678 var el = e.target.closest('[data-ppn-action]');
679 if (!el) return;
680 dispatch(el.dataset.ppnAction, el);
681 });
682
683 // KEYUP DELEGATION
684 document.addEventListener('keyup', function(e) {
685 var el = e.target.closest('[data-ppn-action]');
686 if (!el) return;
687 dispatch(el.dataset.ppnAction, el);
688 });
689
690 // NETWORK SHOW SEARCH ENTER KEY
691 var searchInput = document.getElementById('network-show-search-input');
692 if (searchInput) {
693 searchInput.addEventListener('keydown', function(e) {
694 if (e.key === 'Enter') {
695 e.preventDefault();
696 if (typeof submitNetworkShowSearch === 'function') {
697 submitNetworkShowSearch();
698 }
699 }
700 });
701 }
702
703 // TOS URL ENTER KEY
704 const tosForm = document.getElementById('tosUrlForm');
705 if (tosForm) {
706 tosForm.addEventListener('submit', function(e) {
707 e.preventDefault();
708 const btn = tosForm.querySelector('[data-ppn-action="saveTosUrl"]');
709 if (btn) btn.click();
710 });
711 }
712
713 // AUTO-INIT
714 ppnInitSubmitApp();
715
716 document.querySelectorAll('[data-char-counter]').forEach(function(input) {
717 var warnAt = input.dataset.charWarn ? parseInt(input.dataset.charWarn, 10) : null;
718 initCharCounter(input.id, input.dataset.charCounter, warnAt);
719 });
720
721 var editForm = document.getElementById('editForm') || document.getElementById('createForm');
722 if (editForm) {
723 editForm.addEventListener('input', ppnMarkUnsaved);
724 }
725 });
726