PluginProbe
User Notes / trunk
User Notes vtrunk
2.1.1 2.1.0 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 2.0.0 2.0.1
user-notes / assets / user-notes.js

user-notes.js in User Notes trunk, at assets/user-notes.js

309 lines 14.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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, '&quot;'); }
308 })(jQuery);
309