MDL-59691 assignfeedback_editpdf: Allow comments to collapse to top-left
[moodle.git] / mod / assign / feedback / editpdf / yui / src / editor / js / comment.js
CommitLineData
5c386472
DW
1// This file is part of Moodle - http://moodle.org/
2//
3// Moodle is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// (at your option) any later version.
7//
8// Moodle is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
ad3f8cd1 15/* global SELECTOR, COMMENTCOLOUR, COMMENTTEXTCOLOUR */
5c386472
DW
16
17/**
1f777e5c 18 * Provides an in browser PDF editor.
5c386472
DW
19 *
20 * @module moodle-assignfeedback_editpdf-editor
21 */
22
23/**
1f777e5c 24 * Class representing a list of comments.
5c386472
DW
25 *
26 * @namespace M.assignfeedback_editpdf
27 * @class comment
28 * @param M.assignfeedback_editpdf.editor editor
29 * @param Int gradeid
30 * @param Int pageno
31 * @param Int x
32 * @param Int y
33 * @param Int width
34 * @param String colour
35 * @param String rawtext
36 */
557f44d9 37var COMMENT = function(editor, gradeid, pageno, x, y, width, colour, rawtext) {
5c386472
DW
38
39 /**
40 * Reference to M.assignfeedback_editpdf.editor.
41 * @property editor
42 * @type M.assignfeedback_editpdf.editor
43 * @public
44 */
45 this.editor = editor;
46
47 /**
48 * Grade id
49 * @property gradeid
50 * @type Int
51 * @public
52 */
53 this.gradeid = gradeid || 0;
54
55 /**
56 * X position
57 * @property x
58 * @type Int
59 * @public
60 */
61 this.x = parseInt(x, 10) || 0;
62
63 /**
64 * Y position
65 * @property y
66 * @type Int
67 * @public
68 */
69 this.y = parseInt(y, 10) || 0;
70
71 /**
72 * Comment width
73 * @property width
74 * @type Int
75 * @public
76 */
77 this.width = parseInt(width, 10) || 0;
78
79 /**
80 * Comment rawtext
81 * @property rawtext
82 * @type String
83 * @public
84 */
85 this.rawtext = rawtext || '';
86
87 /**
88 * Comment page number
89 * @property pageno
90 * @type Int
91 * @public
92 */
93 this.pageno = pageno || 0;
94
95 /**
96 * Comment background colour.
97 * @property colour
98 * @type String
99 * @public
100 */
101 this.colour = colour || 'yellow';
102
103 /**
104 * Reference to M.assignfeedback_editpdf.drawable
105 * @property drawable
106 * @type M.assignfeedback_editpdf.drawable
107 * @public
108 */
109 this.drawable = false;
110
111 /**
112 * Boolean used by a timeout to delete empty comments after a short delay.
113 * @property deleteme
114 * @type Boolean
115 * @public
116 */
117 this.deleteme = false;
118
119 /**
120 * Reference to the link that opens the menu.
121 * @property menulink
122 * @type Y.Node
123 * @public
124 */
125 this.menulink = null;
126
127 /**
128 * Reference to the dialogue that is the context menu.
129 * @property menu
130 * @type M.assignfeedback_editpdf.dropdown
131 * @public
132 */
133 this.menu = null;
134
135 /**
136 * Clean a comment record, returning an oject with only fields that are valid.
137 * @public
138 * @method clean
139 * @return {}
140 */
141 this.clean = function() {
142 return {
5bb4f444
DP
143 gradeid: this.gradeid,
144 x: parseInt(this.x, 10),
145 y: parseInt(this.y, 10),
146 width: parseInt(this.width, 10),
147 rawtext: this.rawtext,
23990a0a 148 pageno: parseInt(this.pageno, 10),
5bb4f444 149 colour: this.colour
5c386472
DW
150 };
151 };
152
153 /**
154 * Draw a comment.
155 * @public
156 * @method draw_comment
157 * @param boolean focus - Set the keyboard focus to the new comment if true
158 * @return M.assignfeedback_editpdf.drawable
159 */
160 this.draw = function(focus) {
161 var drawable = new M.assignfeedback_editpdf.drawable(this.editor),
162 node,
667cec9b 163 drawingregion = this.editor.get_dialogue_element(SELECTOR.DRAWINGREGION),
5c386472 164 container,
23990a0a
TB
165 label,
166 marker,
5c386472
DW
167 menu,
168 position,
169 scrollheight;
170
171 // Lets add a contenteditable div.
172 node = Y.Node.create('<textarea/>');
173 container = Y.Node.create('<div class="commentdrawable"/>');
23990a0a
TB
174 label = Y.Node.create('<label/>');
175 marker = Y.Node.create('<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 13 13" ' +
176 'preserveAspectRatio="xMinYMin meet">' +
177 '<path d="M11 0H1C.4 0 0 .4 0 1v6c0 .6.4 1 1 1h1v4l4-4h5c.6 0 1-.4 1-1V1c0-.6-.4-1-1-1z" ' +
178 'fill="currentColor" opacity="0.9" stroke="rgb(153, 153, 153)" stroke-width="0.5"/></svg>');
5c386472
DW
179 menu = Y.Node.create('<a href="#"><img src="' + M.util.image_url('t/contextmenu', 'core') + '"/></a>');
180
181 this.menulink = menu;
23990a0a
TB
182 container.append(label);
183 label.append(node);
184 container.append(marker);
185 container.setAttribute('tabindex', '-1');
186 label.setAttribute('tabindex', '0');
187 node.setAttribute('tabindex', '-1');
e082fbd9 188 menu.setAttribute('tabindex', '0');
5c386472
DW
189
190 if (!this.editor.get('readonly')) {
191 container.append(menu);
192 } else {
193 node.setAttribute('readonly', 'readonly');
194 }
195 if (this.width < 100) {
196 this.width = 100;
197 }
198
199 position = this.editor.get_window_coordinates(new M.assignfeedback_editpdf.point(this.x, this.y));
200 node.setStyles({
201 width: this.width + 'px',
1d38083c
DW
202 backgroundColor: COMMENTCOLOUR[this.colour],
203 color: COMMENTTEXTCOLOUR
5c386472
DW
204 });
205
667cec9b 206 drawingregion.append(container);
d79d1bd8 207 container.setStyle('position', 'absolute');
5c386472
DW
208 container.setX(position.x);
209 container.setY(position.y);
50c12f01 210 drawable.store_position(container, position.x, position.y);
5c386472
DW
211 drawable.nodes.push(container);
212 node.set('value', this.rawtext);
8b766dd1 213 scrollheight = node.get('scrollHeight');
5c386472 214 node.setStyles({
5bb4f444 215 'height': scrollheight + 'px',
5c386472
DW
216 'overflow': 'hidden'
217 });
ad7c719d 218 marker.setStyle('color', COMMENTCOLOUR[this.colour]);
23990a0a 219 this.attach_events(node, menu);
5c386472
DW
220 if (focus) {
221 node.focus();
23990a0a
TB
222 } else if (editor.collapsecomments) {
223 container.addClass('commentcollapsed');
5c386472
DW
224 }
225 this.drawable = drawable;
226
227
228 return drawable;
229 };
230
231 /**
232 * Delete an empty comment if it's menu hasn't been opened in time.
233 * @method delete_comment_later
234 */
235 this.delete_comment_later = function() {
236 if (this.deleteme) {
237 this.remove();
238 }
239 };
240
241 /**
242 * Comment nodes have a bunch of event handlers attached to them directly.
243 * This is all done here for neatness.
244 *
245 * @protected
246 * @method attach_comment_events
247 * @param node - The Y.Node representing the comment.
248 * @param menu - The Y.Node representing the menu.
249 */
250 this.attach_events = function(node, menu) {
23990a0a
TB
251 var container = node.ancestor('div'),
252 label = node.ancestor('label');
253
254 // Function to collapse a comment to a marker icon.
255 node.collapse = function(delay) {
256 node.collapse.delay = Y.later(delay, node, function() {
257 container.addClass('commentcollapsed');
258 });
259 };
260
261 // Function to expand a comment.
262 node.expand = function() {
263 container.removeClass('commentcollapsed');
264 };
265
266 // Expand comment on mouse over (under certain conditions) or click/tap.
267 container.on('mouseenter', function() {
268 if (editor.currentedit.tool === 'comment' || editor.currentedit.tool === 'select' || this.editor.get('readonly')) {
269 node.expand();
270 if (node.collapse.delay) {
271 node.collapse.delay.cancel();
272 }
273 }
274 }, this);
275 container.on('click', function() {
276 node.expand();
277 node.focus();
278 if (node.collapse.delay) {
279 node.collapse.delay.cancel();
5c386472 280 }
5c386472
DW
281 }, this);
282
23990a0a
TB
283 // Functions to capture reverse tabbing events.
284 node.on('keyup', function(e) {
285 if (e.keyCode === 9 && e.shiftKey && menu.getAttribute('tabindex') === '0') {
286 // User landed here via Shift+Tab (but not from this comment's menu).
287 menu.focus();
288 }
289 menu.setAttribute('tabindex', '0');
290 }, this);
291 menu.on('keydown', function(e) {
292 if (e.keyCode === 9 && e.shiftKey) {
293 // User is tabbing back to the comment node from its own menu.
294 menu.setAttribute('tabindex', '-1');
295 }
296 }, this);
5c386472 297
23990a0a
TB
298 // Comment becomes "active" on label or menu focus.
299 label.on('focus', function() {
300 node.active = true;
301 if (node.collapse.delay) {
302 node.collapse.delay.cancel();
303 }
e082fbd9
TB
304 // Give comment a tabindex to prevent focus outline being suppressed.
305 node.setAttribute('tabindex', '0');
23990a0a
TB
306 // Expand comment and pass focus to it.
307 node.expand();
308 node.focus();
309 // Now remove label tabindex so user can reverse tab past it.
310 label.setAttribute('tabindex', '-1');
311 }, this);
312 menu.on('focus', function() {
313 node.active = true;
314 if (node.collapse.delay) {
315 node.collapse.delay.cancel();
5c386472 316 }
23990a0a
TB
317 this.deleteme = false;
318 // Restore label tabindex so user can tab back to it from menu.
319 label.setAttribute('tabindex', '0');
320 }, this);
5c386472 321
e082fbd9
TB
322 // Always restore the default tabindex states when moving away.
323 node.on('blur', function() {
324 node.setAttribute('tabindex', '-1');
325 }, this);
23990a0a
TB
326 label.on('blur', function() {
327 label.setAttribute('tabindex', '0');
328 }, this);
5c386472 329
23990a0a
TB
330 // Collapse comment on mouse out if not currently active.
331 container.on('mouseleave', function() {
332 if (editor.collapsecomments && node.active !== true) {
333 node.collapse(400);
b37b6cd8 334 }
23990a0a
TB
335 }, this);
336
337 // Collapse comment on blur.
338 container.on('blur', function() {
339 node.active = false;
340 if (editor.collapsecomments) {
341 node.collapse(800);
b37b6cd8 342 }
23990a0a 343 }, this);
5c386472 344
23990a0a
TB
345 if (!this.editor.get('readonly')) {
346 // Save the text on blur.
347 node.on('blur', function() {
348 // Save the changes back to the comment.
349 this.rawtext = node.get('value');
350 this.width = parseInt(node.getStyle('width'), 10);
351
352 // Trim.
353 if (this.rawtext.replace(/^\s+|\s+$/g, "") === '') {
354 // Delete empty comments.
355 this.deleteme = true;
356 Y.later(400, this, this.delete_comment_later);
357 }
358 this.editor.save_current_page();
359 this.editor.editingcomment = false;
360 }, this);
361
362 // For delegated event handler.
363 menu.setData('comment', this);
364
365 node.on('keyup', function() {
ee9d4a77 366 node.setStyle('height', 'auto');
23990a0a
TB
367 var scrollheight = node.get('scrollHeight'),
368 height = parseInt(node.getStyle('height'), 10);
369
370 // Webkit scrollheight fix.
371 if (scrollheight === height + 8) {
372 scrollheight -= 8;
373 }
374 node.setStyle('height', scrollheight + 'px');
375 });
376
377 node.on('gesturemovestart', function(e) {
378 if (editor.currentedit.tool === 'select') {
379 e.preventDefault();
380 node.setData('dragging', true);
381 node.setData('offsetx', e.clientX - node.getX());
382 node.setData('offsety', e.clientY - node.getY());
383 }
384 });
385 node.on('gesturemoveend', function() {
386 if (editor.currentedit.tool === 'select') {
387 node.setData('dragging', false);
388 this.editor.save_current_page();
389 }
390 }, null, this);
391 node.on('gesturemove', function(e) {
392 if (editor.currentedit.tool === 'select') {
393 var x = e.clientX - node.getData('offsetx'),
394 y = e.clientY - node.getData('offsety'),
395 nodewidth,
396 nodeheight,
397 newlocation,
398 windowlocation,
399 bounds;
400
401 nodewidth = parseInt(node.getStyle('width'), 10);
402 nodeheight = parseInt(node.getStyle('height'), 10);
403
404 newlocation = this.editor.get_canvas_coordinates(new M.assignfeedback_editpdf.point(x, y));
405 bounds = this.editor.get_canvas_bounds(true);
406 bounds.x = 0;
407 bounds.y = 0;
408
409 bounds.width -= nodewidth + 42;
410 bounds.height -= nodeheight + 8;
411 // Clip to the window size - the comment size.
412 newlocation.clip(bounds);
413
414 this.x = newlocation.x;
415 this.y = newlocation.y;
416
417 windowlocation = this.editor.get_window_coordinates(newlocation);
418 container.setX(windowlocation.x);
419 container.setY(windowlocation.y);
420 this.drawable.store_position(container, windowlocation.x, windowlocation.y);
421 }
422 }, null, this);
423
424 this.menu = new M.assignfeedback_editpdf.commentmenu({
425 buttonNode: this.menulink,
426 comment: this
427 });
428 }
5c386472
DW
429 };
430
431 /**
432 * Delete a comment.
433 * @method remove
434 */
435 this.remove = function() {
bc8b6dc6
DP
436 var i = 0;
437 var comments;
5c386472
DW
438
439 comments = this.editor.pages[this.editor.currentpage].comments;
440 for (i = 0; i < comments.length; i++) {
441 if (comments[i] === this) {
442 comments.splice(i, 1);
443 this.drawable.erase();
444 this.editor.save_current_page();
445 return;
446 }
447 }
448 };
449
450 /**
451 * Event handler to remove a comment from the users quicklist.
452 *
453 * @protected
454 * @method remove_from_quicklist
455 */
456 this.remove_from_quicklist = function(e, quickcomment) {
114913e3 457 e.preventDefault();
23990a0a 458 e.stopPropagation();
114913e3 459
5c386472
DW
460 this.menu.hide();
461
462 this.editor.quicklist.remove(quickcomment);
463 };
464
465 /**
466 * A quick comment was selected in the list, update the active comment and redraw the page.
467 *
468 * @param Event e
469 * @protected
470 * @method set_from_quick_comment
471 */
472 this.set_from_quick_comment = function(e, quickcomment) {
114913e3
DS
473 e.preventDefault();
474
5c386472 475 this.menu.hide();
23990a0a 476 this.deleteme = false;
5c386472
DW
477
478 this.rawtext = quickcomment.rawtext;
479 this.width = quickcomment.width;
480 this.colour = quickcomment.colour;
481
482 this.editor.save_current_page();
483
484 this.editor.redraw();
23990a0a
TB
485
486 this.node = this.drawable.nodes[0].one('textarea');
487 this.node.ancestor('div').removeClass('commentcollapsed');
488 this.node.focus();
5c386472
DW
489 };
490
491 /**
492 * Event handler to add a comment to the users quicklist.
493 *
494 * @protected
495 * @method add_to_quicklist
496 */
114913e3
DS
497 this.add_to_quicklist = function(e) {
498 e.preventDefault();
5c386472
DW
499 this.menu.hide();
500 this.editor.quicklist.add(this);
501 };
502
503 /**
504 * Draw the in progress edit.
505 *
506 * @public
507 * @method draw_current_edit
508 * @param M.assignfeedback_editpdf.edit edit
509 */
510 this.draw_current_edit = function(edit) {
511 var drawable = new M.assignfeedback_editpdf.drawable(this.editor),
512 shape,
513 bounds;
514
515 bounds = new M.assignfeedback_editpdf.rect();
516 bounds.bound([edit.start, edit.end]);
517
518 // We will draw a box with the current background colour.
519 shape = this.editor.graphic.addShape({
520 type: Y.Rect,
521 width: bounds.width,
522 height: bounds.height,
523 fill: {
524 color: COMMENTCOLOUR[edit.commentcolour]
525 },
526 x: bounds.x,
527 y: bounds.y
528 });
529
530 drawable.shapes.push(shape);
531
532 return drawable;
533 };
534
535 /**
536 * Promote the current edit to a real comment.
537 *
538 * @public
539 * @method init_from_edit
540 * @param M.assignfeedback_editpdf.edit edit
baf881b8 541 * @return bool true if comment bound is more than min width/height, else false.
5c386472
DW
542 */
543 this.init_from_edit = function(edit) {
544 var bounds = new M.assignfeedback_editpdf.rect();
545 bounds.bound([edit.start, edit.end]);
546
547 // Minimum comment width.
548 if (bounds.width < 100) {
549 bounds.width = 100;
550 }
551
552 // Save the current edit to the server and the current page list.
553
554 this.gradeid = this.editor.get('gradeid');
555 this.pageno = this.editor.currentpage;
556 this.x = bounds.x;
557 this.y = bounds.y;
558 this.width = bounds.width;
559 this.colour = edit.commentcolour;
560 this.rawtext = '';
baf881b8
RT
561
562 return (bounds.has_min_width() && bounds.has_min_height());
5c386472
DW
563 };
564
565};
566
567M.assignfeedback_editpdf = M.assignfeedback_editpdf || {};
568M.assignfeedback_editpdf.comment = COMMENT;