| 1 |
(function ($) { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
/* --------------------------------------------------------------------- |
| 5 |
* Note management — works inside any ".user-notes-app" container, so the |
| 6 |
* same code drives both the profile section and the Users-list modal. |
| 7 |
* Each container carries data-user-id and data-can-delete. |
| 8 |
* ------------------------------------------------------------------- */ |
| 9 |
|
| 10 |
$(function () { |
| 11 |
$(document) |
| 12 |
.on('click', '.user-notes-add-btn', onAdd) |
| 13 |
.on('click', '.user-notes-star', onToggleStar) |
| 14 |
.on('click', '.user-notes-edit', onEditStart) |
| 15 |
.on('click', '.user-notes-delete', onDelete) |
| 16 |
// Users-list column → open modal |
| 17 |
.on('click', '.user-notes-col, .user-notes-col-add', onOpenModal); |
| 18 |
}); |
| 19 |
|
| 20 |
function i18n(key, fallback) { |
| 21 |
return (UserNotes.i18n && UserNotes.i18n[key]) || fallback; |
| 22 |
} |
| 23 |
|
| 24 |
function post(action, data) { |
| 25 |
return $.post(UserNotes.ajaxUrl, $.extend({ |
| 26 |
action: action, |
| 27 |
nonce: UserNotes.nonce |
| 28 |
}, data)); |
| 29 |
} |
| 30 |
|
| 31 |
function appOf(el) { return $(el).closest('.user-notes-app'); } |
| 32 |
function userIdOf($app) { return $app.data('user-id'); } |
| 33 |
function canDeleteOf($app) { return String($app.data('can-delete')) === '1'; } |
| 34 |
|
| 35 |
/* ---- actions -------------------------------------------------------- */ |
| 36 |
|
| 37 |
function onAdd() { |
| 38 |
var $btn = $(this); |
| 39 |
var $app = appOf($btn); |
| 40 |
var userId = userIdOf($app); |
| 41 |
var $ta = $app.find('.user-notes-new-body'); |
| 42 |
var body = $.trim($ta.val()); |
| 43 |
if (!body) return; |
| 44 |
var starred = $app.find('.user-notes-new-starred').is(':checked') ? 1 : 0; |
| 45 |
|
| 46 |
$btn.prop('disabled', true).text(i18n('saving', 'Saving…')); |
| 47 |
post('user_notes_add', { user_id: userId, body: body, starred: starred }) |
| 48 |
.done(function (res) { |
| 49 |
if (!res || !res.success) return alert(i18n('error', 'Error')); |
| 50 |
$ta.val(''); |
| 51 |
$app.find('.user-notes-new-starred').prop('checked', false); |
| 52 |
$app.find('.user-notes-empty').remove(); |
| 53 |
renderAndInsert($app, res.data); |
| 54 |
syncColumn($app); |
| 55 |
}) |
| 56 |
.fail(function () { alert(i18n('error', 'Error')); }) |
| 57 |
.always(function () { $btn.prop('disabled', false).text(i18n('addNote', 'Add Note')); }); |
| 58 |
} |
| 59 |
|
| 60 |
function onToggleStar(e) { |
| 61 |
e.preventDefault(); |
| 62 |
var $app = appOf(this); |
| 63 |
var $li = $(this).closest('.user-notes-item'); |
| 64 |
var id = $li.data('note-id'); |
| 65 |
$li.addClass('is-busy'); |
| 66 |
post('user_notes_toggle_star', { note_id: id }) |
| 67 |
.done(function (res) { |
| 68 |
if (!res || !res.success) return alert(i18n('error', 'Error')); |
| 69 |
replaceItem($app, $li, res.data); |
| 70 |
resort($app); |
| 71 |
syncColumn($app); |
| 72 |
}) |
| 73 |
.fail(function () { alert(i18n('error', 'Error')); }) |
| 74 |
.always(function () { $li.removeClass('is-busy'); }); |
| 75 |
} |
| 76 |
|
| 77 |
function onEditStart(e) { |
| 78 |
e.preventDefault(); |
| 79 |
var $app = appOf(this); |
| 80 |
var $li = $(this).closest('.user-notes-item'); |
| 81 |
if ($li.find('.user-notes-edit-area').length) return; |
| 82 |
|
| 83 |
var raw = $li.find('.user-notes-raw').text(); |
| 84 |
var $body = $li.find('.user-notes-body').hide(); |
| 85 |
var $area = $('<div class="user-notes-edit-area"></div>'); |
| 86 |
var $ta = $('<textarea rows="3"></textarea>').val(raw); |
| 87 |
var $save = $('<button type="button" class="button button-primary"></button>').text(i18n('save', 'Save')); |
| 88 |
var $cancel = $('<button type="button" class="button"></button>').text(i18n('cancel', 'Cancel')); |
| 89 |
var $actions = $('<div class="user-notes-edit-actions"></div>').append($save, $cancel); |
| 90 |
$area.append($ta, $actions); |
| 91 |
$body.after($area); |
| 92 |
$ta.focus(); |
| 93 |
|
| 94 |
$cancel.on('click', function () { $area.remove(); $body.show(); }); |
| 95 |
$save.on('click', function () { |
| 96 |
var val = $.trim($ta.val()); |
| 97 |
if (!val) return; |
| 98 |
$li.addClass('is-busy'); |
| 99 |
post('user_notes_edit', { note_id: $li.data('note-id'), body: val }) |
| 100 |
.done(function (res) { |
| 101 |
if (!res || !res.success) return alert(i18n('error', 'Error')); |
| 102 |
$area.remove(); |
| 103 |
replaceItem($app, $li, res.data); |
| 104 |
}) |
| 105 |
.fail(function () { alert(i18n('error', 'Error')); }) |
| 106 |
.always(function () { $li.removeClass('is-busy'); }); |
| 107 |
}); |
| 108 |
} |
| 109 |
|
| 110 |
function onDelete(e) { |
| 111 |
e.preventDefault(); |
| 112 |
var $app = appOf(this); |
| 113 |
if (!canDeleteOf($app)) return; |
| 114 |
if (!window.confirm(i18n('confirmDelete', 'Delete this note?'))) return; |
| 115 |
|
| 116 |
var $li = $(this).closest('.user-notes-item'); |
| 117 |
$li.addClass('is-busy'); |
| 118 |
post('user_notes_delete', { note_id: $li.data('note-id') }) |
| 119 |
.done(function (res) { |
| 120 |
if (!res || !res.success) return alert(i18n('error', 'Error')); |
| 121 |
$li.slideUp(150, function () { |
| 122 |
$(this).remove(); |
| 123 |
if (!$app.find('.user-notes-item').length) { |
| 124 |
$app.find('.user-notes-list').after('<p class="user-notes-empty">' + escHtml(i18n('noNotes', 'No notes yet.')) + '</p>'); |
| 125 |
} |
| 126 |
syncColumn($app); |
| 127 |
}); |
| 128 |
}) |
| 129 |
.fail(function () { alert(i18n('error', 'Error')); }); |
| 130 |
} |
| 131 |
|
| 132 |
/* ---- rendering ------------------------------------------------------ */ |
| 133 |
|
| 134 |
function buildItem(n, canDelete) { |
| 135 |
var star = n.starred ? 'dashicons-star-filled' : 'dashicons-star-empty'; |
| 136 |
var edited = n.edited ? '<span class="user-notes-edited" title="' + escAttr(n.updated_at) + '">(' + escHtml(i18n('edited', 'edited') + ' ' + n.updated_rel) + ')</span>' : ''; |
| 137 |
var delLink = canDelete ? ' | <a href="#" class="user-notes-delete">' + escHtml(i18n('del', 'Delete')) + '</a>' : ''; |
| 138 |
|
| 139 |
var html = '' |
| 140 |
+ '<li class="user-notes-item ' + (n.starred ? 'is-starred' : '') + '" data-note-id="' + n.id + '">' |
| 141 |
+ ' <div class="user-notes-meta">' |
| 142 |
+ ' <button type="button" class="user-notes-star" title="' + escAttr(i18n('toggleStar', 'Toggle star')) + '"><span class="dashicons ' + star + '"></span></button>' |
| 143 |
+ ' <span class="user-notes-author">' + escHtml(n.author) + '</span>' |
| 144 |
+ ' <span class="user-notes-time" title="' + escAttr(n.created_at) + '">' + escHtml(n.created_rel) + '</span>' |
| 145 |
+ ' ' + edited |
| 146 |
+ ' <span class="user-notes-actions"><a href="#" class="user-notes-edit">' + escHtml(i18n('edit', 'Edit')) + '</a>' + delLink + '</span>' |
| 147 |
+ ' </div>' |
| 148 |
+ ' <div class="user-notes-body">' + n.body_html + '</div>' |
| 149 |
+ ' <div class="user-notes-raw" style="display:none;"></div>' |
| 150 |
+ '</li>'; |
| 151 |
var $el = $(html); |
| 152 |
$el.find('.user-notes-raw').text(n.body_raw); |
| 153 |
return $el; |
| 154 |
} |
| 155 |
|
| 156 |
function renderAndInsert($app, n) { |
| 157 |
var $new = buildItem(n, canDeleteOf($app)); |
| 158 |
$app.find('.user-notes-list').prepend($new); |
| 159 |
resort($app); |
| 160 |
} |
| 161 |
|
| 162 |
function replaceItem($app, $li, n) { |
| 163 |
$li.replaceWith(buildItem(n, canDeleteOf($app))); |
| 164 |
} |
| 165 |
|
| 166 |
// Starred first, then newest (highest id) first — mirrors the server query. |
| 167 |
function resort($app) { |
| 168 |
var $list = $app.find('.user-notes-list'); |
| 169 |
var items = $list.children('.user-notes-item').get(); |
| 170 |
items.sort(function (a, b) { |
| 171 |
var as = $(a).hasClass('is-starred') ? 1 : 0; |
| 172 |
var bs = $(b).hasClass('is-starred') ? 1 : 0; |
| 173 |
if (as !== bs) return bs - as; |
| 174 |
return $(b).data('note-id') - $(a).data('note-id'); |
| 175 |
}); |
| 176 |
$.each(items, function (_, el) { $list.append(el); }); |
| 177 |
} |
| 178 |
|
| 179 |
/* ---- Users-list column sync ---------------------------------------- */ |
| 180 |
|
| 181 |
// Rebuild the Users-list cell for this user to match server output. |
| 182 |
// No-op on the profile page (no matching row exists there). |
| 183 |
function syncColumn($app) { |
| 184 |
var userId = userIdOf($app); |
| 185 |
var $cell = $('#user-' + userId + ' .column-user_notes_note, #user-' + userId + ' td.user_notes_note'); |
| 186 |
if (!$cell.length) return; |
| 187 |
|
| 188 |
var total = $app.find('.user-notes-item').length; |
| 189 |
var starred = $app.find('.user-notes-item.is-starred').length; |
| 190 |
$cell.html(columnHtml(userId, total, starred)); |
| 191 |
} |
| 192 |
|
| 193 |
function columnHtml(userId, total, starred) { |
| 194 |
if (!total) { |
| 195 |
return '<a class="user-notes-col-add" href="#" data-user-id="' + userId + '">' |
| 196 |
+ '<span class="dashicons dashicons-plus-alt2" aria-hidden="true"></span>' |
| 197 |
+ escHtml(i18n('addNote', 'Add Note')) + '</a>'; |
| 198 |
} |
| 199 |
var html = '<a class="user-notes-col" href="#" data-user-id="' + userId + '">' |
| 200 |
+ '<span class="user-notes-badge user-notes-badge-count">' |
| 201 |
+ '<span class="dashicons dashicons-admin-comments" aria-hidden="true"></span>' |
| 202 |
+ '<span class="user-notes-badge-num">' + total + '</span>' |
| 203 |
+ '</span>'; |
| 204 |
if (starred > 0) { |
| 205 |
html += '<span class="user-notes-badge user-notes-badge-star">' |
| 206 |
+ '<span class="dashicons dashicons-star-filled" aria-hidden="true"></span>' |
| 207 |
+ '<span class="user-notes-badge-num">' + starred + '</span>' |
| 208 |
+ '</span>'; |
| 209 |
} |
| 210 |
html += '</a>'; |
| 211 |
return html; |
| 212 |
} |
| 213 |
|
| 214 |
/* ---- Modal ---------------------------------------------------------- */ |
| 215 |
|
| 216 |
var $modal, $lastTrigger; |
| 217 |
|
| 218 |
function ensureModal() { |
| 219 |
if ($modal) return $modal; |
| 220 |
$modal = $('' |
| 221 |
+ '<div id="user-notes-modal" class="user-notes-modal" aria-hidden="true">' |
| 222 |
+ ' <div class="user-notes-modal-overlay"></div>' |
| 223 |
+ ' <div class="user-notes-modal-dialog" role="dialog" aria-modal="true" aria-labelledby="user-notes-modal-title">' |
| 224 |
+ ' <div class="user-notes-modal-header">' |
| 225 |
+ ' <img class="user-notes-modal-avatar" src="" alt="" />' |
| 226 |
+ ' <h2 id="user-notes-modal-title" class="user-notes-modal-title"></h2>' |
| 227 |
+ ' <button type="button" class="user-notes-modal-close" aria-label="' + escAttr(i18n('close', 'Close')) + '"><span class="dashicons dashicons-no-alt"></span></button>' |
| 228 |
+ ' </div>' |
| 229 |
+ ' <div class="user-notes-modal-body"></div>' |
| 230 |
+ ' </div>' |
| 231 |
+ '</div>' |
| 232 |
).appendTo(document.body); |
| 233 |
|
| 234 |
$modal.on('click', '.user-notes-modal-close, .user-notes-modal-overlay', closeModal); |
| 235 |
$(document).on('keydown', function (e) { |
| 236 |
if (e.key === 'Escape' && $modal && $modal.hasClass('is-open')) closeModal(); |
| 237 |
}); |
| 238 |
return $modal; |
| 239 |
} |
| 240 |
|
| 241 |
function appShell() { |
| 242 |
return '' |
| 243 |
+ '<div class="user-notes-app" data-user-id="" data-can-delete="0">' |
| 244 |
+ ' <div class="user-notes-add">' |
| 245 |
+ ' <textarea class="user-notes-new-body" rows="3" placeholder="' + escAttr(i18n('addPlaceholder', 'Add a note…')) + '"></textarea>' |
| 246 |
+ ' <div class="user-notes-add-actions">' |
| 247 |
+ ' <label><input type="checkbox" class="user-notes-new-starred" /> ' + escHtml(i18n('starThis', 'Star this note')) + '</label>' |
| 248 |
+ ' <button type="button" class="button button-primary user-notes-add-btn">' + escHtml(i18n('addNote', 'Add Note')) + '</button>' |
| 249 |
+ ' </div>' |
| 250 |
+ ' </div>' |
| 251 |
+ ' <ul class="user-notes-list"></ul>' |
| 252 |
+ ' <p class="user-notes-empty" hidden>' + escHtml(i18n('noNotes', 'No notes yet.')) + '</p>' |
| 253 |
+ '</div>'; |
| 254 |
} |
| 255 |
|
| 256 |
function onOpenModal(e) { |
| 257 |
e.preventDefault(); |
| 258 |
var userId = $(this).data('user-id'); |
| 259 |
if (!userId) return; |
| 260 |
$lastTrigger = $(this); |
| 261 |
openModal(userId); |
| 262 |
} |
| 263 |
|
| 264 |
function openModal(userId) { |
| 265 |
var $m = ensureModal(); |
| 266 |
$m.find('.user-notes-modal-title').text(i18n('loading', 'Loading…')); |
| 267 |
$m.find('.user-notes-modal-avatar').attr('src', '').hide(); |
| 268 |
$m.find('.user-notes-modal-body').html('<div class="user-notes-modal-loading"><span class="spinner is-active"></span></div>'); |
| 269 |
$m.addClass('is-open').attr('aria-hidden', 'false'); |
| 270 |
$('body').addClass('user-notes-modal-open'); |
| 271 |
|
| 272 |
post('user_notes_list', { user_id: userId }) |
| 273 |
.done(function (res) { |
| 274 |
if (!res || !res.success) { closeModal(); return alert(i18n('error', 'Error')); } |
| 275 |
var d = res.data; |
| 276 |
$m.find('.user-notes-modal-title').text(i18n('notesFor', 'Notes for %s').replace('%s', d.user_name)); |
| 277 |
if (d.avatar) $m.find('.user-notes-modal-avatar').attr('src', d.avatar).attr('alt', d.user_name).show(); |
| 278 |
|
| 279 |
var $body = $m.find('.user-notes-modal-body').html(appShell()); |
| 280 |
var $app = $body.find('.user-notes-app') |
| 281 |
.attr('data-user-id', d.user_id) |
| 282 |
.attr('data-can-delete', d.can_delete ? '1' : '0'); |
| 283 |
|
| 284 |
if (!d.can_edit) $app.find('.user-notes-add').remove(); |
| 285 |
|
| 286 |
var $list = $app.find('.user-notes-list'); |
| 287 |
if (d.notes && d.notes.length) { |
| 288 |
$.each(d.notes, function (_, n) { $list.append(buildItem(n, !!d.can_delete)); }); |
| 289 |
} else { |
| 290 |
$app.find('.user-notes-empty').prop('hidden', false); |
| 291 |
} |
| 292 |
$app.find('.user-notes-new-body').focus(); |
| 293 |
}) |
| 294 |
.fail(function () { closeModal(); alert(i18n('error', 'Error')); }); |
| 295 |
} |
| 296 |
|
| 297 |
function closeModal() { |
| 298 |
if (!$modal) return; |
| 299 |
$modal.removeClass('is-open').attr('aria-hidden', 'true'); |
| 300 |
$('body').removeClass('user-notes-modal-open'); |
| 301 |
if ($lastTrigger && $lastTrigger.length) { $lastTrigger.focus(); } |
| 302 |
} |
| 303 |
|
| 304 |
/* ---- utils ---------------------------------------------------------- */ |
| 305 |
|
| 306 |
function escHtml(s) { return $('<div/>').text(s == null ? '' : s).html(); } |
| 307 |
function escAttr(s) { return escHtml(s).replace(/"/g, '"'); } |
| 308 |
})(jQuery); |
| 309 |
|