| 1 |
/** |
| 2 |
* Forumax Inline Edit - Topic |
| 3 |
* |
| 4 |
* Intercepts the Edit link in the admin dropdown, |
| 5 |
* swaps the topic title and content with a visual TinyMCE editor, |
| 6 |
* and saves changes via AJAX without page reload. |
| 7 |
* |
| 8 |
* @package Forumax |
| 9 |
*/ |
| 10 |
; (function ($) { |
| 11 |
|
| 12 |
'use strict'; |
| 13 |
|
| 14 |
/** |
| 15 |
* TinyMCE editor ID used for the inline editor instance. |
| 16 |
* |
| 17 |
* @type {string} |
| 18 |
*/ |
| 19 |
var EDITOR_ID = 'frmx-edit-content'; |
| 20 |
|
| 21 |
/** |
| 22 |
* Inline edit state management. |
| 23 |
*/ |
| 24 |
var FrmxInlineEdit = { |
| 25 |
|
| 26 |
isEditing: false, |
| 27 |
|
| 28 |
/** |
| 29 |
* Initialize inline edit functionality. |
| 30 |
*/ |
| 31 |
init: function () { |
| 32 |
this.bindEvents(); |
| 33 |
}, |
| 34 |
|
| 35 |
/** |
| 36 |
* Bind event listeners. |
| 37 |
*/ |
| 38 |
bindEvents: function () { |
| 39 |
var self = this; |
| 40 |
|
| 41 |
// Intercept the Edit option in the topic admin dropdown (nice-select). |
| 42 |
// nice-select does NOT copy data-action attributes, so we find the |
| 43 |
// corresponding <option> by index from the original <select>. |
| 44 |
$(document).on('click', '.topic-content-wrapper .bbp-admin-links-dropdown .nice-select .list .option', function (e) { |
| 45 |
var $option = $(this); |
| 46 |
var optIndex = $option.index(); |
| 47 |
var $dropdown = $option.closest('.bbp-admin-links-dropdown'); |
| 48 |
var $origOpt = $dropdown.find('select option').eq(optIndex); |
| 49 |
var action = $origOpt.data('action'); |
| 50 |
|
| 51 |
if (action === 'edit') { |
| 52 |
e.preventDefault(); |
| 53 |
e.stopPropagation(); |
| 54 |
|
| 55 |
var $wrapper = $dropdown.closest('.topic-content-wrapper'); |
| 56 |
|
| 57 |
// Close the nice-select dropdown |
| 58 |
$dropdown.find('.nice-select').removeClass('open'); |
| 59 |
|
| 60 |
if (!self.isEditing) { |
| 61 |
self.startEditing($wrapper); |
| 62 |
} |
| 63 |
|
| 64 |
return false; |
| 65 |
} |
| 66 |
}); |
| 67 |
|
| 68 |
// Intercept changes on the original <select> (fallback for non-nice-select). |
| 69 |
// Scoped to topic-content-wrapper so it does not catch reply dropdowns. |
| 70 |
$(document).on('change', '.topic-content-wrapper .bbp-admin-dropdown-menu', function () { |
| 71 |
var $selected = $(this).find('option:selected'); |
| 72 |
var action = $selected.data('action'); |
| 73 |
|
| 74 |
if (action === 'edit') { |
| 75 |
$(this).val(''); |
| 76 |
|
| 77 |
var $dropdown = $(this).closest('.bbp-admin-links-dropdown'); |
| 78 |
var $wrapper = $dropdown.closest('.topic-content-wrapper'); |
| 79 |
|
| 80 |
if (!self.isEditing) { |
| 81 |
self.startEditing($wrapper); |
| 82 |
} |
| 83 |
|
| 84 |
return false; |
| 85 |
} |
| 86 |
}); |
| 87 |
}, |
| 88 |
|
| 89 |
/** |
| 90 |
* Start inline editing mode. |
| 91 |
* |
| 92 |
* @param {jQuery} $wrapper - The .topic-content-wrapper element. |
| 93 |
*/ |
| 94 |
startEditing: function ($wrapper) { |
| 95 |
var self = this; |
| 96 |
var topicId = frmxInlineEdit.topic_id; |
| 97 |
|
| 98 |
if (!topicId) { |
| 99 |
return; |
| 100 |
} |
| 101 |
|
| 102 |
self.isEditing = true; |
| 103 |
|
| 104 |
// Show loading state |
| 105 |
var $titleEl = $wrapper.find('.q-title h1'); |
| 106 |
var $contentEl = $wrapper.find('.content.editor-content'); |
| 107 |
|
| 108 |
// $titleEl.css('opacity', '0.5'); |
| 109 |
$contentEl.css('opacity', '0.5'); |
| 110 |
|
| 111 |
// Fetch raw content via AJAX |
| 112 |
$.ajax({ |
| 113 |
url: frmxInlineEdit.ajaxurl, |
| 114 |
type: 'POST', |
| 115 |
data: { |
| 116 |
action: 'forumax_get_topic_content', |
| 117 |
nonce: frmxInlineEdit.nonce, |
| 118 |
topic_id: topicId |
| 119 |
}, |
| 120 |
success: function (response) { |
| 121 |
if (response.success) { |
| 122 |
self.renderEditor($wrapper, response.data); |
| 123 |
} else { |
| 124 |
self.showNotice($wrapper, response.data.message, 'error'); |
| 125 |
self.isEditing = false; |
| 126 |
} |
| 127 |
|
| 128 |
$titleEl.css('opacity', ''); |
| 129 |
$contentEl.css('opacity', ''); |
| 130 |
}, |
| 131 |
error: function () { |
| 132 |
self.showNotice($wrapper, frmxInlineEdit.i18n.error, 'error'); |
| 133 |
self.isEditing = false; |
| 134 |
$titleEl.css('opacity', ''); |
| 135 |
$contentEl.css('opacity', ''); |
| 136 |
} |
| 137 |
}); |
| 138 |
}, |
| 139 |
|
| 140 |
/** |
| 141 |
* Render the inline editor form with TinyMCE visual editor. |
| 142 |
* |
| 143 |
* @param {jQuery} $wrapper - The .topic-content-wrapper element. |
| 144 |
* @param {Object} data - { title, content } from the server. |
| 145 |
*/ |
| 146 |
renderEditor: function ($wrapper, data) { |
| 147 |
var self = this; |
| 148 |
|
| 149 |
var $qTitle = $wrapper.find('.q-title'); |
| 150 |
var $contentEl = $wrapper.find('.content.editor-content'); |
| 151 |
|
| 152 |
// Store original HTML for cancel |
| 153 |
self.originalTitleHtml = $qTitle.html(); |
| 154 |
self.originalContentHtml = $contentEl.html(); |
| 155 |
|
| 156 |
// Replace title with input |
| 157 |
var titleInput = '<div class="frmx-inline-edit-title">' + |
| 158 |
'<label for="frmx-edit-title" class="screen-reader-text">' + frmxInlineEdit.i18n.title_label + '</label>' + |
| 159 |
'<input type="text" id="frmx-edit-title" class="frmx-edit-title-input" value="' + self.escapeAttr(data.title) + '" />' + |
| 160 |
'</div>'; |
| 161 |
$qTitle.html(titleInput); |
| 162 |
|
| 163 |
// Replace content with a textarea that TinyMCE will enhance |
| 164 |
var editorHtml = '<div class="frmx-inline-edit-content">' + |
| 165 |
'<label for="' + EDITOR_ID + '" class="screen-reader-text">' + frmxInlineEdit.i18n.content_label + '</label>' + |
| 166 |
'<textarea id="' + EDITOR_ID + '" class="frmx-edit-content-textarea" rows="12">' + data.content + '</textarea>' + |
| 167 |
'<div class="frmx-inline-edit-actions">' + |
| 168 |
'<button type="button" class="frmx-edit-save">' + frmxInlineEdit.i18n.save + '</button>' + |
| 169 |
'<button type="button" class="frmx-edit-cancel">' + frmxInlineEdit.i18n.cancel + '</button>' + |
| 170 |
'</div>' + |
| 171 |
'</div>'; |
| 172 |
$contentEl.html(editorHtml); |
| 173 |
|
| 174 |
// Add editing class for styling |
| 175 |
$wrapper.addClass('frmx-is-editing'); |
| 176 |
|
| 177 |
// Initialize TinyMCE visual editor on the textarea |
| 178 |
self.initTinyMCE(); |
| 179 |
|
| 180 |
// Focus the title input |
| 181 |
$wrapper.find('.frmx-edit-title-input').focus(); |
| 182 |
|
| 183 |
// Bind save/cancel |
| 184 |
$contentEl.find('.frmx-edit-save').on('click', function () { |
| 185 |
self.saveEdit($wrapper); |
| 186 |
}); |
| 187 |
|
| 188 |
$contentEl.find('.frmx-edit-cancel').on('click', function () { |
| 189 |
self.cancelEdit($wrapper); |
| 190 |
}); |
| 191 |
|
| 192 |
// Keyboard shortcuts on the title input |
| 193 |
$wrapper.find('.frmx-edit-title-input').on('keydown', function (e) { |
| 194 |
// Ctrl+Enter / Cmd+Enter to save |
| 195 |
if ((e.ctrlKey || e.metaKey) && e.keyCode === 13) { |
| 196 |
e.preventDefault(); |
| 197 |
self.saveEdit($wrapper); |
| 198 |
} |
| 199 |
// Escape to cancel |
| 200 |
if (e.keyCode === 27) { |
| 201 |
e.preventDefault(); |
| 202 |
self.cancelEdit($wrapper); |
| 203 |
} |
| 204 |
}); |
| 205 |
}, |
| 206 |
|
| 207 |
/** |
| 208 |
* Initialize TinyMCE on the editor textarea. |
| 209 |
* |
| 210 |
* Uses the WordPress wp.editor API for consistent behavior. |
| 211 |
*/ |
| 212 |
initTinyMCE: function () { |
| 213 |
var self = this; |
| 214 |
|
| 215 |
// Remove any existing editor instance first |
| 216 |
if (typeof wp !== 'undefined' && wp.editor) { |
| 217 |
wp.editor.remove(EDITOR_ID); |
| 218 |
} |
| 219 |
|
| 220 |
// Force Visual mode — set the WP user preference before initialization. |
| 221 |
if (typeof setUserSetting === 'function') { |
| 222 |
setUserSetting('editor', 'tinymce'); |
| 223 |
} |
| 224 |
|
| 225 |
// Use wp.editor.initialize for proper TinyMCE setup |
| 226 |
if (typeof wp !== 'undefined' && wp.editor && wp.editor.initialize) { |
| 227 |
wp.editor.initialize(EDITOR_ID, { |
| 228 |
tinymce: { |
| 229 |
wpautop: true, |
| 230 |
toolbar1: 'bold,italic,underline,strikethrough,|,bullist,numlist,|,blockquote,|,link,unlink,|,undo,redo', |
| 231 |
toolbar2: '', |
| 232 |
height: 250, |
| 233 |
statusbar: false, |
| 234 |
resize: true, |
| 235 |
setup: function (editor) { |
| 236 |
// Ensure Visual mode is active after editor initialises. |
| 237 |
editor.on('init', function () { |
| 238 |
self.forceVisualMode(EDITOR_ID); |
| 239 |
}); |
| 240 |
|
| 241 |
// Ctrl+Enter / Cmd+Enter to save from inside the editor |
| 242 |
editor.on('keydown', function (e) { |
| 243 |
if ((e.ctrlKey || e.metaKey) && e.keyCode === 13) { |
| 244 |
e.preventDefault(); |
| 245 |
var $wrapper = $('#' + EDITOR_ID).closest('.topic-content-wrapper'); |
| 246 |
self.saveEdit($wrapper); |
| 247 |
} |
| 248 |
if (e.keyCode === 27) { |
| 249 |
e.preventDefault(); |
| 250 |
var $wrapper = $('#' + EDITOR_ID).closest('.topic-content-wrapper'); |
| 251 |
self.cancelEdit($wrapper); |
| 252 |
} |
| 253 |
}); |
| 254 |
} |
| 255 |
}, |
| 256 |
quicktags: true, |
| 257 |
mediaButtons: false |
| 258 |
}); |
| 259 |
|
| 260 |
// Also force Visual mode immediately after initialize call. |
| 261 |
this.forceVisualMode(EDITOR_ID); |
| 262 |
} |
| 263 |
}, |
| 264 |
|
| 265 |
/** |
| 266 |
* Force the editor wrapper into Visual (TinyMCE) mode. |
| 267 |
* Directly manipulates DOM classes and visibility as a |
| 268 |
* reliable alternative to switchEditors.go(). |
| 269 |
* |
| 270 |
* @param {string} editorId - The editor textarea ID. |
| 271 |
*/ |
| 272 |
forceVisualMode: function (editorId) { |
| 273 |
var $wrap = $('#wp-' + editorId + '-wrap'); |
| 274 |
console.log('[forceVisualMode] editorId:', editorId, '| #wp-' + editorId + '-wrap found:', $wrap.length); |
| 275 |
if (!$wrap.length) { |
| 276 |
$wrap = $('#' + editorId).closest('.wp-editor-wrap'); |
| 277 |
console.log('[forceVisualMode] fallback .closest found:', $wrap.length); |
| 278 |
} |
| 279 |
|
| 280 |
if (!$wrap.length) { |
| 281 |
console.warn('[forceVisualMode] No wrapper found for', editorId); |
| 282 |
return; |
| 283 |
} |
| 284 |
|
| 285 |
console.log('[forceVisualMode] wrapper classes BEFORE:', $wrap.attr('class')); |
| 286 |
console.log('[forceVisualMode] quicktags found:', $wrap.find('.quicktags-toolbar').length, '| offsetHeight:', $wrap.find('.quicktags-toolbar')[0] ? $wrap.find('.quicktags-toolbar')[0].offsetHeight : 'N/A'); |
| 287 |
console.log('[forceVisualMode] mce-tinymce found:', $wrap.find('.mce-tinymce').length); |
| 288 |
|
| 289 |
// Swap classes to Visual mode. |
| 290 |
$wrap.removeClass('html-active').addClass('tmce-active'); |
| 291 |
|
| 292 |
// Hide the quicktags toolbar. |
| 293 |
$wrap.find('.quicktags-toolbar').css({ |
| 294 |
display: 'none', |
| 295 |
height: '0', |
| 296 |
padding: '0', |
| 297 |
overflow: 'hidden' |
| 298 |
}); |
| 299 |
|
| 300 |
// Set the Visual tab as active. |
| 301 |
$wrap.find('.switch-tmce').attr('aria-pressed', 'true'); |
| 302 |
$wrap.find('.switch-html').attr('aria-pressed', 'false'); |
| 303 |
|
| 304 |
// Show the TinyMCE container, hide the raw textarea. |
| 305 |
$wrap.find('.mce-tinymce').show(); |
| 306 |
$wrap.find('#' + editorId).hide(); |
| 307 |
|
| 308 |
console.log('[forceVisualMode] wrapper classes AFTER:', $wrap.attr('class')); |
| 309 |
console.log('[forceVisualMode] quicktags offsetHeight AFTER:', $wrap.find('.quicktags-toolbar')[0] ? $wrap.find('.quicktags-toolbar')[0].offsetHeight : 'N/A'); |
| 310 |
}, |
| 311 |
|
| 312 |
/** |
| 313 |
* Get the content from TinyMCE or fallback textarea. |
| 314 |
* |
| 315 |
* @return {string} The editor content. |
| 316 |
*/ |
| 317 |
getEditorContent: function () { |
| 318 |
// Try TinyMCE first |
| 319 |
if (typeof tinyMCE !== 'undefined') { |
| 320 |
var editor = tinyMCE.get(EDITOR_ID); |
| 321 |
if (editor && !editor.isHidden()) { |
| 322 |
return editor.getContent(); |
| 323 |
} |
| 324 |
} |
| 325 |
|
| 326 |
// Fallback to textarea value (text mode) |
| 327 |
return $('#' + EDITOR_ID).val(); |
| 328 |
}, |
| 329 |
|
| 330 |
/** |
| 331 |
* Destroy the TinyMCE editor instance. |
| 332 |
*/ |
| 333 |
destroyEditor: function () { |
| 334 |
if (typeof wp !== 'undefined' && wp.editor) { |
| 335 |
wp.editor.remove(EDITOR_ID); |
| 336 |
} |
| 337 |
}, |
| 338 |
|
| 339 |
/** |
| 340 |
* Save the inline edit via AJAX. |
| 341 |
* |
| 342 |
* @param {jQuery} $wrapper - The .topic-content-wrapper element. |
| 343 |
*/ |
| 344 |
saveEdit: function ($wrapper) { |
| 345 |
var self = this; |
| 346 |
var topicId = frmxInlineEdit.topic_id; |
| 347 |
var newTitle = $wrapper.find('.frmx-edit-title-input').val().trim(); |
| 348 |
var newContent = self.getEditorContent().trim(); |
| 349 |
|
| 350 |
if (!newTitle) { |
| 351 |
self.showNotice($wrapper, frmxInlineEdit.i18n.title_required, 'error'); |
| 352 |
$wrapper.find('.frmx-edit-title-input').focus(); |
| 353 |
return; |
| 354 |
} |
| 355 |
|
| 356 |
if (!newContent) { |
| 357 |
self.showNotice($wrapper, frmxInlineEdit.i18n.content_required, 'error'); |
| 358 |
return; |
| 359 |
} |
| 360 |
|
| 361 |
// Disable buttons and show saving state |
| 362 |
var $saveBtn = $wrapper.find('.frmx-edit-save'); |
| 363 |
var $cancelBtn = $wrapper.find('.frmx-edit-cancel'); |
| 364 |
$saveBtn.prop('disabled', true).text(frmxInlineEdit.i18n.saving); |
| 365 |
$cancelBtn.prop('disabled', true); |
| 366 |
|
| 367 |
$.ajax({ |
| 368 |
url: frmxInlineEdit.ajaxurl, |
| 369 |
type: 'POST', |
| 370 |
data: { |
| 371 |
action: 'forumax_save_topic_edit', |
| 372 |
nonce: frmxInlineEdit.nonce, |
| 373 |
topic_id: topicId, |
| 374 |
title: newTitle, |
| 375 |
content: newContent |
| 376 |
}, |
| 377 |
success: function (response) { |
| 378 |
if (response.success) { |
| 379 |
// Destroy TinyMCE before replacing HTML |
| 380 |
self.destroyEditor(); |
| 381 |
|
| 382 |
// Update the displayed content |
| 383 |
var $qTitle = $wrapper.find('.q-title'); |
| 384 |
var $contentEl = $wrapper.find('.content.editor-content'); |
| 385 |
|
| 386 |
// Restore title area with new title |
| 387 |
$qTitle.html(self.originalTitleHtml); |
| 388 |
$qTitle.find('h1').contents().first().replaceWith(response.data.title); |
| 389 |
|
| 390 |
// Restore content area with new rendered content |
| 391 |
$contentEl.html(response.data.content); |
| 392 |
|
| 393 |
$wrapper.removeClass('frmx-is-editing'); |
| 394 |
self.isEditing = false; |
| 395 |
|
| 396 |
self.showNotice($wrapper, response.data.message, 'success'); |
| 397 |
|
| 398 |
// Re-initialize nice-select on admin dropdown if it exists |
| 399 |
if (typeof $.fn.frmxNiceSelect === 'function') { |
| 400 |
$wrapper.find('select.bbp-admin-dropdown-menu').each(function () { |
| 401 |
var $select = $(this); |
| 402 |
$select.nextAll('.nice-select, .frmx-nice-select').remove(); |
| 403 |
$select.frmxNiceSelect(); |
| 404 |
$select.next('.frmx-nice-select').removeClass('nice-select'); |
| 405 |
}); |
| 406 |
} |
| 407 |
if (typeof window.frmxEnhanceAdminDropdowns === 'function') { |
| 408 |
window.frmxEnhanceAdminDropdowns(); |
| 409 |
} |
| 410 |
} else { |
| 411 |
self.showNotice($wrapper, response.data.message, 'error'); |
| 412 |
$saveBtn.prop('disabled', false).text(frmxInlineEdit.i18n.save); |
| 413 |
$cancelBtn.prop('disabled', false); |
| 414 |
} |
| 415 |
}, |
| 416 |
error: function () { |
| 417 |
self.showNotice($wrapper, frmxInlineEdit.i18n.error, 'error'); |
| 418 |
$saveBtn.prop('disabled', false).text(frmxInlineEdit.i18n.save); |
| 419 |
$cancelBtn.prop('disabled', false); |
| 420 |
} |
| 421 |
}); |
| 422 |
}, |
| 423 |
|
| 424 |
/** |
| 425 |
* Cancel inline editing and restore original content. |
| 426 |
* |
| 427 |
* @param {jQuery} $wrapper - The .topic-content-wrapper element. |
| 428 |
*/ |
| 429 |
cancelEdit: function ($wrapper) { |
| 430 |
// Destroy TinyMCE before replacing HTML |
| 431 |
this.destroyEditor(); |
| 432 |
|
| 433 |
var $qTitle = $wrapper.find('.q-title'); |
| 434 |
var $contentEl = $wrapper.find('.content.editor-content'); |
| 435 |
|
| 436 |
$qTitle.html(this.originalTitleHtml); |
| 437 |
$contentEl.html(this.originalContentHtml); |
| 438 |
|
| 439 |
$wrapper.removeClass('frmx-is-editing'); |
| 440 |
this.isEditing = false; |
| 441 |
}, |
| 442 |
|
| 443 |
/** |
| 444 |
* Show a temporary notice message. |
| 445 |
* |
| 446 |
* @param {jQuery} $wrapper - The wrapper element. |
| 447 |
* @param {string} message - The message text. |
| 448 |
* @param {string} type - 'success' or 'error'. |
| 449 |
*/ |
| 450 |
showNotice: function ($wrapper, message, type) { |
| 451 |
// Remove any existing notices |
| 452 |
$wrapper.find('.frmx-inline-edit-notice').remove(); |
| 453 |
|
| 454 |
var noticeClass = 'frmx-inline-edit-notice frmx-notice-' + type; |
| 455 |
var $notice = $('<div class="' + noticeClass + '">' + message + '</div>'); |
| 456 |
|
| 457 |
$wrapper.find('.q-title').after($notice); |
| 458 |
|
| 459 |
// Auto-dismiss after 4 seconds |
| 460 |
setTimeout(function () { |
| 461 |
$notice.fadeOut(300, function () { |
| 462 |
$(this).remove(); |
| 463 |
}); |
| 464 |
}, 4000); |
| 465 |
}, |
| 466 |
|
| 467 |
/** |
| 468 |
* Escape HTML entities for safe attribute insertion. |
| 469 |
* |
| 470 |
* @param {string} str - The string to escape. |
| 471 |
* @return {string} Escaped string. |
| 472 |
*/ |
| 473 |
escapeAttr: function (str) { |
| 474 |
return str |
| 475 |
.replace(/&/g, '&') |
| 476 |
.replace(/"/g, '"') |
| 477 |
.replace(/'/g, ''') |
| 478 |
.replace(/</g, '<') |
| 479 |
.replace(/>/g, '>'); |
| 480 |
} |
| 481 |
}; |
| 482 |
|
| 483 |
$(document).ready(function () { |
| 484 |
// Only initialize on single topic pages |
| 485 |
if (typeof frmxInlineEdit !== 'undefined') { |
| 486 |
if ($('.topic-content-wrapper').length) { |
| 487 |
FrmxInlineEdit.init(); |
| 488 |
} |
| 489 |
if ($('.forum-comment').length) { |
| 490 |
FrmxReplyInlineEdit.init(); |
| 491 |
} |
| 492 |
} |
| 493 |
}); |
| 494 |
|
| 495 |
/** |
| 496 |
* Reply inline edit state management. |
| 497 |
* |
| 498 |
* Intercepts the Edit link in reply admin dropdowns, |
| 499 |
* swaps the reply content with a visual TinyMCE editor, |
| 500 |
* and saves changes via AJAX without page reload. |
| 501 |
*/ |
| 502 |
var FrmxReplyInlineEdit = { |
| 503 |
|
| 504 |
/** |
| 505 |
* Track which reply is currently being edited (reply ID or false). |
| 506 |
* |
| 507 |
* @type {number|boolean} |
| 508 |
*/ |
| 509 |
editingReplyId: false, |
| 510 |
|
| 511 |
/** |
| 512 |
* Initialize reply inline edit functionality. |
| 513 |
*/ |
| 514 |
init: function () { |
| 515 |
this.bindEvents(); |
| 516 |
}, |
| 517 |
|
| 518 |
/** |
| 519 |
* Bind event listeners for reply edit interception. |
| 520 |
*/ |
| 521 |
bindEvents: function () { |
| 522 |
var self = this; |
| 523 |
|
| 524 |
// Intercept the Edit option in reply admin dropdown (nice-select). |
| 525 |
// nice-select does NOT copy data-action attributes, so we find the |
| 526 |
// corresponding <option> by index from the original <select>. |
| 527 |
$(document).on('click', '.reply-admin-links .bbp-admin-links-dropdown .nice-select .list .option, .reply-admin-links .bbp-admin-links-dropdown .frmx-nice-select .list .option', function (e) { |
| 528 |
var $option = $(this); |
| 529 |
var optIndex = $option.index(); |
| 530 |
var $dropdown = $option.closest('.bbp-admin-links-dropdown'); |
| 531 |
var $origOpt = $dropdown.find('select option').eq(optIndex); |
| 532 |
var action = $origOpt.data('action'); |
| 533 |
|
| 534 |
if (action === 'edit') { |
| 535 |
e.preventDefault(); |
| 536 |
e.stopPropagation(); |
| 537 |
|
| 538 |
var $reply = $dropdown.closest('.forum-comment'); |
| 539 |
|
| 540 |
// Close the nice-select dropdown |
| 541 |
$dropdown.find('.nice-select, .frmx-nice-select').removeClass('open'); |
| 542 |
|
| 543 |
if (!self.editingReplyId) { |
| 544 |
self.startEditing($reply); |
| 545 |
} |
| 546 |
|
| 547 |
return false; |
| 548 |
} |
| 549 |
}); |
| 550 |
|
| 551 |
// Intercept changes on the original <select> (fallback for non-nice-select). |
| 552 |
$(document).on('change', '.reply-admin-links .bbp-admin-dropdown-menu', function () { |
| 553 |
var $selected = $(this).find('option:selected'); |
| 554 |
var action = $selected.data('action'); |
| 555 |
|
| 556 |
if (action === 'edit') { |
| 557 |
$(this).val(''); |
| 558 |
|
| 559 |
var $dropdown = $(this).closest('.bbp-admin-links-dropdown'); |
| 560 |
var $reply = $dropdown.closest('.forum-comment'); |
| 561 |
|
| 562 |
if (!self.editingReplyId) { |
| 563 |
self.startEditing($reply); |
| 564 |
} |
| 565 |
|
| 566 |
return false; |
| 567 |
} |
| 568 |
}); |
| 569 |
}, |
| 570 |
|
| 571 |
/** |
| 572 |
* Get the reply ID from a .forum-comment element. |
| 573 |
* |
| 574 |
* @param {jQuery} $reply - The .forum-comment element. |
| 575 |
* @return {number} The reply ID. |
| 576 |
*/ |
| 577 |
getReplyId: function ($reply) { |
| 578 |
// The reply container has id="post-{id}" |
| 579 |
var postId = $reply.attr('id'); |
| 580 |
if (postId) { |
| 581 |
return parseInt(postId.replace('post-', ''), 10); |
| 582 |
} |
| 583 |
return 0; |
| 584 |
}, |
| 585 |
|
| 586 |
/** |
| 587 |
* Get the TinyMCE editor ID for a specific reply. |
| 588 |
* |
| 589 |
* @param {number} replyId - The reply ID. |
| 590 |
* @return {string} The editor textarea ID. |
| 591 |
*/ |
| 592 |
getEditorId: function (replyId) { |
| 593 |
return 'frmx-reply-edit-content-' + replyId; |
| 594 |
}, |
| 595 |
|
| 596 |
/** |
| 597 |
* Track whether content has been modified during editing. |
| 598 |
* |
| 599 |
* @type {boolean} |
| 600 |
*/ |
| 601 |
contentModified: false, |
| 602 |
|
| 603 |
/** |
| 604 |
* Start inline editing mode for a reply. |
| 605 |
* |
| 606 |
* @param {jQuery} $reply - The .forum-comment element. |
| 607 |
*/ |
| 608 |
startEditing: function ($reply) { |
| 609 |
var self = this; |
| 610 |
var replyId = self.getReplyId($reply); |
| 611 |
|
| 612 |
if (!replyId) { |
| 613 |
return; |
| 614 |
} |
| 615 |
|
| 616 |
self.editingReplyId = replyId; |
| 617 |
self.contentModified = false; |
| 618 |
|
| 619 |
// Show loading spinner overlay |
| 620 |
var $contentEl = $reply.find('.comment-content').first(); |
| 621 |
var $spinner = $('<div class="frmx-edit-loading">' + |
| 622 |
'<div class="frmx-edit-loading-spinner"></div>' + |
| 623 |
'<span>' + frmxInlineEdit.i18n.loading + '</span>' + |
| 624 |
'</div>'); |
| 625 |
$contentEl.css({ position: 'relative', minHeight: '80px' }); |
| 626 |
$contentEl.append($spinner); |
| 627 |
$contentEl.children().not('.frmx-edit-loading').css('opacity', '0.35'); |
| 628 |
|
| 629 |
// Fetch raw content via AJAX |
| 630 |
$.ajax({ |
| 631 |
url: frmxInlineEdit.ajaxurl, |
| 632 |
type: 'POST', |
| 633 |
data: { |
| 634 |
action: 'forumax_get_reply_content', |
| 635 |
nonce: frmxInlineEdit.nonce, |
| 636 |
reply_id: replyId |
| 637 |
}, |
| 638 |
success: function (response) { |
| 639 |
$spinner.remove(); |
| 640 |
$contentEl.css({ position: '', minHeight: '' }); |
| 641 |
|
| 642 |
// Always restore children opacity before handing off to renderEditor |
| 643 |
// so originalContentHtml is stored without the inline opacity style. |
| 644 |
$contentEl.children().css('opacity', ''); |
| 645 |
|
| 646 |
if (response.success) { |
| 647 |
self.renderEditor($reply, response.data); |
| 648 |
} else { |
| 649 |
self.showNotice($reply, response.data.message, 'error'); |
| 650 |
self.editingReplyId = false; |
| 651 |
} |
| 652 |
}, |
| 653 |
error: function () { |
| 654 |
$spinner.remove(); |
| 655 |
$contentEl.css({ position: '', minHeight: '' }); |
| 656 |
self.showNotice($reply, frmxInlineEdit.i18n.error, 'error'); |
| 657 |
self.editingReplyId = false; |
| 658 |
$contentEl.children().css('opacity', ''); |
| 659 |
} |
| 660 |
}); |
| 661 |
}, |
| 662 |
|
| 663 |
/** |
| 664 |
* Render the inline editor form for a reply with TinyMCE visual editor. |
| 665 |
* |
| 666 |
* @param {jQuery} $reply - The .forum-comment element. |
| 667 |
* @param {Object} data - { content } from the server. |
| 668 |
*/ |
| 669 |
renderEditor: function ($reply, data) { |
| 670 |
var self = this; |
| 671 |
var replyId = self.getReplyId($reply); |
| 672 |
var editorId = self.getEditorId(replyId); |
| 673 |
|
| 674 |
var $contentEl = $reply.find('.comment-content').first(); |
| 675 |
|
| 676 |
// Store original HTML for cancel |
| 677 |
self.originalContentHtml = $contentEl.html(); |
| 678 |
|
| 679 |
// Build enhanced editor UI with header bar and footer |
| 680 |
var isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; |
| 681 |
var modKey = isMac ? '⌘' : 'Ctrl'; |
| 682 |
var editorHtml = '<div class="frmx-inline-edit-content frmx-reply-inline-edit" role="form" aria-label="' + frmxInlineEdit.i18n.editing_reply + '">' + |
| 683 |
// Header bar |
| 684 |
'<div class="frmx-edit-header">' + |
| 685 |
'<span class="frmx-edit-header-label">' + |
| 686 |
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>' + |
| 687 |
frmxInlineEdit.i18n.editing_reply + |
| 688 |
'</span>' + |
| 689 |
'<span class="frmx-edit-header-shortcut">' + |
| 690 |
'<kbd>' + modKey + '</kbd><kbd>Enter</kbd> ' + frmxInlineEdit.i18n.to_save + |
| 691 |
'<span class="frmx-edit-shortcut-sep" aria-hidden="true">·</span>' + |
| 692 |
'<kbd>Esc</kbd> ' + frmxInlineEdit.i18n.to_cancel + |
| 693 |
'</span>' + |
| 694 |
'</div>' + |
| 695 |
// Editor textarea |
| 696 |
'<label for="' + editorId + '" class="screen-reader-text">' + frmxInlineEdit.i18n.content_label + '</label>' + |
| 697 |
'<textarea id="' + editorId + '" class="frmx-edit-content-textarea" rows="8">' + data.content + '</textarea>' + |
| 698 |
// Footer actions |
| 699 |
'<div class="frmx-inline-edit-actions">' + |
| 700 |
'<div class="frmx-edit-actions-left">' + |
| 701 |
'<span class="frmx-edit-unsaved-hint" role="status" aria-live="polite"></span>' + |
| 702 |
'<span class="frmx-edit-char-count" aria-hidden="true"></span>' + |
| 703 |
'</div>' + |
| 704 |
'<div class="frmx-edit-actions-right">' + |
| 705 |
'<button type="button" class="frmx-edit-cancel">' + frmxInlineEdit.i18n.cancel + '</button>' + |
| 706 |
'<button type="button" class="frmx-edit-save">' + |
| 707 |
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>' + |
| 708 |
frmxInlineEdit.i18n.save + |
| 709 |
'</button>' + |
| 710 |
'</div>' + |
| 711 |
// Inline discard confirmation (hidden by default) |
| 712 |
'<div class="frmx-edit-discard-prompt" role="alertdialog" aria-hidden="true">' + |
| 713 |
'<span class="frmx-edit-discard-text">' + frmxInlineEdit.i18n.discard_confirm + '</span>' + |
| 714 |
'<div class="frmx-edit-discard-actions">' + |
| 715 |
'<button type="button" class="frmx-edit-discard-keep">' + frmxInlineEdit.i18n.keep_editing + '</button>' + |
| 716 |
'<button type="button" class="frmx-edit-discard-confirm">' + frmxInlineEdit.i18n.discard + '</button>' + |
| 717 |
'</div>' + |
| 718 |
'</div>' + |
| 719 |
'</div>' + |
| 720 |
'</div>'; |
| 721 |
|
| 722 |
// Animate: wrap content, collapse, insert editor, expand |
| 723 |
$contentEl.css('overflow', 'hidden'); |
| 724 |
var origHeight = $contentEl.outerHeight(); |
| 725 |
$contentEl.html(editorHtml); |
| 726 |
|
| 727 |
// Add editing class for styling |
| 728 |
$reply.addClass('frmx-is-editing'); |
| 729 |
|
| 730 |
// Animate expand |
| 731 |
var $editWrapper = $contentEl.find('.frmx-reply-inline-edit'); |
| 732 |
$editWrapper.css({ opacity: 0 }); |
| 733 |
|
| 734 |
// Initialize TinyMCE visual editor on the textarea |
| 735 |
self.initTinyMCE(editorId); |
| 736 |
|
| 737 |
// Fade in the edit wrapper |
| 738 |
requestAnimationFrame(function () { |
| 739 |
$editWrapper.css({ opacity: 1, transition: 'opacity 0.25s ease' }); |
| 740 |
$contentEl.css('overflow', ''); |
| 741 |
}); |
| 742 |
|
| 743 |
// Track content modifications |
| 744 |
self.contentModified = false; |
| 745 |
|
| 746 |
// Bind save/cancel |
| 747 |
$contentEl.find('.frmx-edit-save').on('click', function () { |
| 748 |
self.saveEdit($reply); |
| 749 |
}); |
| 750 |
|
| 751 |
$contentEl.find('.frmx-edit-cancel').on('click', function () { |
| 752 |
if (self.contentModified) { |
| 753 |
self.showDiscardPrompt($reply); |
| 754 |
return; |
| 755 |
} |
| 756 |
self.cancelEdit($reply); |
| 757 |
}); |
| 758 |
|
| 759 |
// Inline discard prompt actions |
| 760 |
$contentEl.find('.frmx-edit-discard-keep').on('click', function () { |
| 761 |
self.hideDiscardPrompt($reply); |
| 762 |
}); |
| 763 |
|
| 764 |
$contentEl.find('.frmx-edit-discard-confirm').on('click', function () { |
| 765 |
self.cancelEdit($reply); |
| 766 |
}); |
| 767 |
}, |
| 768 |
|
| 769 |
/** |
| 770 |
* Show the inline discard-changes confirmation prompt. |
| 771 |
* |
| 772 |
* @param {jQuery} $reply - The .forum-comment element. |
| 773 |
*/ |
| 774 |
showDiscardPrompt: function ($reply) { |
| 775 |
var $actions = $reply.find('.frmx-inline-edit-actions'); |
| 776 |
$actions.addClass('frmx-show-discard'); |
| 777 |
$actions.find('.frmx-edit-discard-prompt').attr('aria-hidden', 'false'); |
| 778 |
$actions.find('.frmx-edit-discard-keep').focus(); |
| 779 |
}, |
| 780 |
|
| 781 |
/** |
| 782 |
* Hide the inline discard-changes confirmation prompt. |
| 783 |
* |
| 784 |
* @param {jQuery} $reply - The .forum-comment element. |
| 785 |
*/ |
| 786 |
hideDiscardPrompt: function ($reply) { |
| 787 |
var $actions = $reply.find('.frmx-inline-edit-actions'); |
| 788 |
$actions.removeClass('frmx-show-discard'); |
| 789 |
$actions.find('.frmx-edit-discard-prompt').attr('aria-hidden', 'true'); |
| 790 |
}, |
| 791 |
|
| 792 |
/** |
| 793 |
* Update the live character counter in the editor footer. |
| 794 |
* |
| 795 |
* @param {jQuery} $reply - The .forum-comment element. |
| 796 |
* @param {string} editorId - The editor textarea ID. |
| 797 |
*/ |
| 798 |
updateCharCount: function ($reply, editorId) { |
| 799 |
var raw = this.getEditorContent(editorId); |
| 800 |
var plain = raw.replace(/<[^>]*>/g, '').replace(/ /g, ' ').trim(); |
| 801 |
var count = plain.length; |
| 802 |
var label = count === 1 ? '1 character' : count + ' characters'; |
| 803 |
$reply.find('.frmx-edit-char-count').text(label); |
| 804 |
}, |
| 805 |
|
| 806 |
/** |
| 807 |
* Initialize TinyMCE on the reply editor textarea. |
| 808 |
* |
| 809 |
* @param {string} editorId - The textarea element ID. |
| 810 |
*/ |
| 811 |
initTinyMCE: function (editorId) { |
| 812 |
var self = this; |
| 813 |
|
| 814 |
// Remove any existing editor instance first |
| 815 |
if (typeof wp !== 'undefined' && wp.editor) { |
| 816 |
wp.editor.remove(editorId); |
| 817 |
} |
| 818 |
|
| 819 |
// Force Visual mode — set the WP user preference before initialization. |
| 820 |
if (typeof setUserSetting === 'function') { |
| 821 |
setUserSetting('editor', 'tinymce'); |
| 822 |
} |
| 823 |
|
| 824 |
// Use wp.editor.initialize for proper TinyMCE setup |
| 825 |
if (typeof wp !== 'undefined' && wp.editor && wp.editor.initialize) { |
| 826 |
wp.editor.initialize(editorId, { |
| 827 |
tinymce: { |
| 828 |
wpautop: true, |
| 829 |
toolbar1: 'bold,italic,underline,strikethrough,|,bullist,numlist,|,blockquote,|,link,unlink,|,undo,redo', |
| 830 |
toolbar2: '', |
| 831 |
height: 180, |
| 832 |
statusbar: false, |
| 833 |
resize: true, |
| 834 |
setup: function (editor) { |
| 835 |
// Auto-focus the editor and move caret to the end on init. |
| 836 |
editor.on('init', function () { |
| 837 |
// Ensure Visual mode is active after editor initialises. |
| 838 |
self.forceVisualMode(editorId); |
| 839 |
|
| 840 |
editor.focus(); |
| 841 |
editor.selection.select(editor.getBody(), true); |
| 842 |
editor.selection.collapse(false); |
| 843 |
|
| 844 |
var $reply = $('#' + editorId).closest('.forum-comment'); |
| 845 |
self.updateCharCount($reply, editorId); |
| 846 |
}); |
| 847 |
|
| 848 |
// Track content modifications + live character count. |
| 849 |
editor.on('input change keyup', function () { |
| 850 |
var $reply = $('#' + editorId).closest('.forum-comment'); |
| 851 |
|
| 852 |
if (!self.contentModified) { |
| 853 |
self.contentModified = true; |
| 854 |
$reply.find('.frmx-edit-unsaved-hint').text(frmxInlineEdit.i18n.unsaved_changes); |
| 855 |
} |
| 856 |
|
| 857 |
self.updateCharCount($reply, editorId); |
| 858 |
}); |
| 859 |
|
| 860 |
// Ctrl+Enter / Cmd+Enter to save from inside the editor |
| 861 |
editor.on('keydown', function (e) { |
| 862 |
if ((e.ctrlKey || e.metaKey) && e.keyCode === 13) { |
| 863 |
e.preventDefault(); |
| 864 |
var $reply = $('#' + editorId).closest('.forum-comment'); |
| 865 |
self.saveEdit($reply); |
| 866 |
} |
| 867 |
if (e.keyCode === 27) { |
| 868 |
e.preventDefault(); |
| 869 |
var $reply = $('#' + editorId).closest('.forum-comment'); |
| 870 |
if (self.contentModified) { |
| 871 |
self.showDiscardPrompt($reply); |
| 872 |
return; |
| 873 |
} |
| 874 |
self.cancelEdit($reply); |
| 875 |
} |
| 876 |
}); |
| 877 |
} |
| 878 |
}, |
| 879 |
quicktags: true, |
| 880 |
mediaButtons: false |
| 881 |
}); |
| 882 |
|
| 883 |
// Also force Visual mode immediately after initialize call. |
| 884 |
this.forceVisualMode(editorId); |
| 885 |
} |
| 886 |
}, |
| 887 |
|
| 888 |
/** |
| 889 |
* Force the editor wrapper into Visual (TinyMCE) mode. |
| 890 |
* Directly manipulates DOM classes and visibility as a |
| 891 |
* reliable alternative to switchEditors.go(). |
| 892 |
* |
| 893 |
* @param {string} editorId - The editor textarea ID. |
| 894 |
*/ |
| 895 |
forceVisualMode: function (editorId) { |
| 896 |
var $wrap = $('#wp-' + editorId + '-wrap'); |
| 897 |
if (!$wrap.length) { |
| 898 |
$wrap = $('#' + editorId).closest('.wp-editor-wrap'); |
| 899 |
} |
| 900 |
|
| 901 |
if (!$wrap.length) { |
| 902 |
return; |
| 903 |
} |
| 904 |
|
| 905 |
// Swap classes to Visual mode. |
| 906 |
$wrap.removeClass('html-active').addClass('tmce-active'); |
| 907 |
|
| 908 |
// Hide the quicktags toolbar. |
| 909 |
$wrap.find('.quicktags-toolbar').css({ |
| 910 |
display: 'none', |
| 911 |
height: '0', |
| 912 |
padding: '0', |
| 913 |
overflow: 'hidden' |
| 914 |
}); |
| 915 |
|
| 916 |
// Set the Visual tab as active. |
| 917 |
$wrap.find('.switch-tmce').attr('aria-pressed', 'true'); |
| 918 |
$wrap.find('.switch-html').attr('aria-pressed', 'false'); |
| 919 |
|
| 920 |
// Show the TinyMCE container, hide the raw textarea. |
| 921 |
$wrap.find('.mce-tinymce').show(); |
| 922 |
$wrap.find('#' + editorId).hide(); |
| 923 |
}, |
| 924 |
|
| 925 |
/** |
| 926 |
* Get the content from TinyMCE or fallback textarea for a reply. |
| 927 |
* |
| 928 |
* @param {string} editorId - The editor textarea ID. |
| 929 |
* @return {string} The editor content. |
| 930 |
*/ |
| 931 |
getEditorContent: function (editorId) { |
| 932 |
// Try TinyMCE first |
| 933 |
if (typeof tinyMCE !== 'undefined') { |
| 934 |
var editor = tinyMCE.get(editorId); |
| 935 |
if (editor && !editor.isHidden()) { |
| 936 |
return editor.getContent(); |
| 937 |
} |
| 938 |
} |
| 939 |
|
| 940 |
// Fallback to textarea value (text mode) |
| 941 |
return $('#' + editorId).val(); |
| 942 |
}, |
| 943 |
|
| 944 |
/** |
| 945 |
* Destroy the TinyMCE editor instance for a reply. |
| 946 |
* |
| 947 |
* @param {string} editorId - The editor textarea ID. |
| 948 |
*/ |
| 949 |
destroyEditor: function (editorId) { |
| 950 |
if (typeof wp !== 'undefined' && wp.editor) { |
| 951 |
wp.editor.remove(editorId); |
| 952 |
} |
| 953 |
}, |
| 954 |
|
| 955 |
/** |
| 956 |
* Save the inline reply edit via AJAX. |
| 957 |
* |
| 958 |
* @param {jQuery} $reply - The .forum-comment element. |
| 959 |
*/ |
| 960 |
saveEdit: function ($reply) { |
| 961 |
var self = this; |
| 962 |
var replyId = self.getReplyId($reply); |
| 963 |
var editorId = self.getEditorId(replyId); |
| 964 |
var newContent = self.getEditorContent(editorId).trim(); |
| 965 |
|
| 966 |
if (!newContent) { |
| 967 |
self.showNotice($reply, frmxInlineEdit.i18n.content_required, 'error'); |
| 968 |
return; |
| 969 |
} |
| 970 |
|
| 971 |
// Disable buttons and show saving state |
| 972 |
var $saveBtn = $reply.find('.frmx-edit-save'); |
| 973 |
var $cancelBtn = $reply.find('.frmx-edit-cancel'); |
| 974 |
$saveBtn.prop('disabled', true).html( |
| 975 |
'<svg class="frmx-btn-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10" stroke-dasharray="31.4 31.4" stroke-linecap="round"/></svg>' + |
| 976 |
frmxInlineEdit.i18n.saving |
| 977 |
); |
| 978 |
$cancelBtn.prop('disabled', true); |
| 979 |
|
| 980 |
$.ajax({ |
| 981 |
url: frmxInlineEdit.ajaxurl, |
| 982 |
type: 'POST', |
| 983 |
data: { |
| 984 |
action: 'forumax_save_reply_edit', |
| 985 |
nonce: frmxInlineEdit.nonce, |
| 986 |
reply_id: replyId, |
| 987 |
content: newContent |
| 988 |
}, |
| 989 |
success: function (response) { |
| 990 |
if (response.success) { |
| 991 |
// Destroy TinyMCE before replacing HTML |
| 992 |
self.destroyEditor(editorId); |
| 993 |
|
| 994 |
// Animate back to display content |
| 995 |
var $contentEl = $reply.find('.comment-content').first(); |
| 996 |
$contentEl.css('opacity', '0'); |
| 997 |
|
| 998 |
setTimeout(function () { |
| 999 |
$contentEl.html(response.data.content); |
| 1000 |
$reply.removeClass('frmx-is-editing'); |
| 1001 |
$contentEl.css({ opacity: 1, transition: 'opacity 0.25s ease' }); |
| 1002 |
}, 150); |
| 1003 |
|
| 1004 |
self.editingReplyId = false; |
| 1005 |
self.contentModified = false; |
| 1006 |
|
| 1007 |
self.showNotice($reply, response.data.message, 'success'); |
| 1008 |
|
| 1009 |
// Re-initialize nice-select on admin dropdown if it exists |
| 1010 |
if (typeof $.fn.frmxNiceSelect === 'function') { |
| 1011 |
$reply.find('select.bbp-admin-dropdown-menu').each(function () { |
| 1012 |
var $select = $(this); |
| 1013 |
$select.nextAll('.nice-select, .frmx-nice-select').remove(); |
| 1014 |
$select.frmxNiceSelect(); |
| 1015 |
$select.next('.frmx-nice-select').removeClass('nice-select'); |
| 1016 |
}); |
| 1017 |
} |
| 1018 |
if (typeof window.frmxEnhanceAdminDropdowns === 'function') { |
| 1019 |
window.frmxEnhanceAdminDropdowns(); |
| 1020 |
} |
| 1021 |
|
| 1022 |
// Re-initialize nested reply buttons if they exist |
| 1023 |
self.reinitNestedReplyButtons($reply); |
| 1024 |
} else { |
| 1025 |
self.showNotice($reply, response.data.message, 'error'); |
| 1026 |
$saveBtn.prop('disabled', false).text(frmxInlineEdit.i18n.save); |
| 1027 |
$cancelBtn.prop('disabled', false); |
| 1028 |
} |
| 1029 |
}, |
| 1030 |
error: function () { |
| 1031 |
self.showNotice($reply, frmxInlineEdit.i18n.error, 'error'); |
| 1032 |
$saveBtn.prop('disabled', false).text(frmxInlineEdit.i18n.save); |
| 1033 |
$cancelBtn.prop('disabled', false); |
| 1034 |
} |
| 1035 |
}); |
| 1036 |
}, |
| 1037 |
|
| 1038 |
/** |
| 1039 |
* Cancel inline editing and restore original reply content. |
| 1040 |
* |
| 1041 |
* @param {jQuery} $reply - The .forum-comment element. |
| 1042 |
*/ |
| 1043 |
cancelEdit: function ($reply) { |
| 1044 |
var self = this; |
| 1045 |
var editorId = self.getEditorId(self.getReplyId($reply)); |
| 1046 |
|
| 1047 |
// Destroy TinyMCE before replacing HTML |
| 1048 |
self.destroyEditor(editorId); |
| 1049 |
|
| 1050 |
var $contentEl = $reply.find('.comment-content').first(); |
| 1051 |
|
| 1052 |
// Animate out |
| 1053 |
$contentEl.css('opacity', '0'); |
| 1054 |
|
| 1055 |
setTimeout(function () { |
| 1056 |
$contentEl.html(self.originalContentHtml); |
| 1057 |
$reply.removeClass('frmx-is-editing'); |
| 1058 |
$contentEl.css({ opacity: 1, transition: 'opacity 0.25s ease' }); |
| 1059 |
}, 150); |
| 1060 |
|
| 1061 |
self.editingReplyId = false; |
| 1062 |
self.contentModified = false; |
| 1063 |
}, |
| 1064 |
|
| 1065 |
/** |
| 1066 |
* Show a temporary notice message for a reply. |
| 1067 |
* |
| 1068 |
* @param {jQuery} $reply - The .forum-comment element. |
| 1069 |
* @param {string} message - The message text. |
| 1070 |
* @param {string} type - 'success' or 'error'. |
| 1071 |
*/ |
| 1072 |
showNotice: function ($reply, message, type) { |
| 1073 |
// Remove any existing notices |
| 1074 |
$reply.find('.frmx-inline-edit-notice').remove(); |
| 1075 |
|
| 1076 |
var noticeClass = 'frmx-inline-edit-notice frmx-notice-' + type; |
| 1077 |
var $notice = $('<div class="' + noticeClass + '">' + message + '</div>'); |
| 1078 |
|
| 1079 |
$reply.find('.comment-content').first().before($notice); |
| 1080 |
|
| 1081 |
// Auto-dismiss after 4 seconds |
| 1082 |
setTimeout(function () { |
| 1083 |
$notice.fadeOut(300, function () { |
| 1084 |
$(this).remove(); |
| 1085 |
}); |
| 1086 |
}, 4000); |
| 1087 |
}, |
| 1088 |
|
| 1089 |
/** |
| 1090 |
* Re-initialize nested reply buttons after content update. |
| 1091 |
* |
| 1092 |
* This ensures the "Reply" button works correctly after inline editing. |
| 1093 |
* |
| 1094 |
* @param {jQuery} $reply - The .forum-comment element. |
| 1095 |
*/ |
| 1096 |
reinitNestedReplyButtons: function ($reply) { |
| 1097 |
// Only proceed if bbPress reply system is loaded and nested replies is enabled. |
| 1098 |
if (typeof addReply === 'undefined' || ! addReply.moveForm) { |
| 1099 |
return; |
| 1100 |
} |
| 1101 |
|
| 1102 |
// Find all reply-to links in the reply. |
| 1103 |
$reply.find('a.bbp-reply-to-link').each(function () { |
| 1104 |
var $link = $(this); |
| 1105 |
|
| 1106 |
// Extract the reply ID from the link's onclick or data attributes. |
| 1107 |
var replyId = $link.data('reply-id'); |
| 1108 |
|
| 1109 |
// If no data attribute, try to extract from onclick attribute. |
| 1110 |
if (!replyId) { |
| 1111 |
var onclick = $link.attr('onclick'); |
| 1112 |
if (onclick) { |
| 1113 |
var match = onclick.match(/addReply\.moveForm\([^,]+,\s*['"](\d+)['"]/); |
| 1114 |
if (match) { |
| 1115 |
replyId = match[1]; |
| 1116 |
} |
| 1117 |
} |
| 1118 |
} |
| 1119 |
|
| 1120 |
// If we still don't have a reply ID, try to get it from the parent post ID. |
| 1121 |
if (!replyId) { |
| 1122 |
var $post = $link.closest('[id^="post-"]'); |
| 1123 |
if ($post.length) { |
| 1124 |
replyId = $post.attr('id').replace('post-', ''); |
| 1125 |
} |
| 1126 |
} |
| 1127 |
|
| 1128 |
if (replyId) { |
| 1129 |
// Remove any existing onclick to avoid duplicates. |
| 1130 |
$link.removeAttr('onclick'); |
| 1131 |
|
| 1132 |
// Attach the click handler. |
| 1133 |
$link.on('click', function (e) { |
| 1134 |
e.preventDefault(); |
| 1135 |
|
| 1136 |
// Get topic ID from localized script if available. |
| 1137 |
var topicId = (typeof frmxInlineEdit !== 'undefined' && frmxInlineEdit.topic_id) |
| 1138 |
? frmxInlineEdit.topic_id |
| 1139 |
: 0; |
| 1140 |
|
| 1141 |
// Use bbPress's addReply.moveForm to move the reply form. |
| 1142 |
// Parameters: anchor, replyId, respondId, postId |
| 1143 |
addReply.moveForm( |
| 1144 |
'bbp-reply-to-link-' + replyId, |
| 1145 |
replyId, |
| 1146 |
'new-post', |
| 1147 |
topicId |
| 1148 |
); |
| 1149 |
|
| 1150 |
return false; |
| 1151 |
}); |
| 1152 |
} |
| 1153 |
}); |
| 1154 |
} |
| 1155 |
}; |
| 1156 |
|
| 1157 |
})(jQuery); |
| 1158 |
|