MDL-61537 assignfeedback_editpdf: Rotate PDF page
[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 251 var container = node.ancestor('div'),
60e9f908
TB
252 label = node.ancestor('label'),
253 marker = label.next('svg');
23990a0a
TB
254
255 // Function to collapse a comment to a marker icon.
256 node.collapse = function(delay) {
257 node.collapse.delay = Y.later(delay, node, function() {
60e9f908
TB
258 if (editor.collapsecomments) {
259 container.addClass('commentcollapsed');
260 }
23990a0a
TB
261 });
262 };
263
264 // Function to expand a comment.
265 node.expand = function() {
60e9f908
TB
266 if (node.getData('dragging') !== true) {
267 if (node.collapse.delay) {
268 node.collapse.delay.cancel();
269 }
270 container.removeClass('commentcollapsed');
271 }
23990a0a
TB
272 };
273
274 // Expand comment on mouse over (under certain conditions) or click/tap.
275 container.on('mouseenter', function() {
276 if (editor.currentedit.tool === 'comment' || editor.currentedit.tool === 'select' || this.editor.get('readonly')) {
277 node.expand();
23990a0a
TB
278 }
279 }, this);
60e9f908 280 container.on('click|tap', function() {
23990a0a
TB
281 node.expand();
282 node.focus();
5c386472
DW
283 }, this);
284
23990a0a
TB
285 // Functions to capture reverse tabbing events.
286 node.on('keyup', function(e) {
287 if (e.keyCode === 9 && e.shiftKey && menu.getAttribute('tabindex') === '0') {
288 // User landed here via Shift+Tab (but not from this comment's menu).
289 menu.focus();
290 }
291 menu.setAttribute('tabindex', '0');
292 }, this);
293 menu.on('keydown', function(e) {
294 if (e.keyCode === 9 && e.shiftKey) {
295 // User is tabbing back to the comment node from its own menu.
296 menu.setAttribute('tabindex', '-1');
297 }
298 }, this);
5c386472 299
23990a0a
TB
300 // Comment becomes "active" on label or menu focus.
301 label.on('focus', function() {
302 node.active = true;
303 if (node.collapse.delay) {
304 node.collapse.delay.cancel();
305 }
e082fbd9
TB
306 // Give comment a tabindex to prevent focus outline being suppressed.
307 node.setAttribute('tabindex', '0');
23990a0a
TB
308 // Expand comment and pass focus to it.
309 node.expand();
310 node.focus();
311 // Now remove label tabindex so user can reverse tab past it.
312 label.setAttribute('tabindex', '-1');
313 }, this);
314 menu.on('focus', function() {
315 node.active = true;
316 if (node.collapse.delay) {
317 node.collapse.delay.cancel();
5c386472 318 }
23990a0a
TB
319 this.deleteme = false;
320 // Restore label tabindex so user can tab back to it from menu.
321 label.setAttribute('tabindex', '0');
322 }, this);
5c386472 323
e082fbd9
TB
324 // Always restore the default tabindex states when moving away.
325 node.on('blur', function() {
326 node.setAttribute('tabindex', '-1');
327 }, this);
23990a0a
TB
328 label.on('blur', function() {
329 label.setAttribute('tabindex', '0');
330 }, this);
5c386472 331
23990a0a
TB
332 // Collapse comment on mouse out if not currently active.
333 container.on('mouseleave', function() {
334 if (editor.collapsecomments && node.active !== true) {
335 node.collapse(400);
b37b6cd8 336 }
23990a0a
TB
337 }, this);
338
339 // Collapse comment on blur.
340 container.on('blur', function() {
341 node.active = false;
60e9f908 342 node.collapse(800);
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();
60e9f908
TB
380 if (editor.collapsecomments) {
381 node.setData('offsetx', 8);
382 node.setData('offsety', 8);
383 } else {
384 node.setData('offsetx', e.clientX - container.getX());
385 node.setData('offsety', e.clientY - container.getY());
386 }
23990a0a
TB
387 }
388 });
60e9f908
TB
389 node.on('gesturemove', function(e) {
390 if (editor.currentedit.tool === 'select') {
391 var x = e.clientX - node.getData('offsetx'),
392 y = e.clientY - node.getData('offsety'),
393 newlocation,
394 windowlocation,
395 bounds;
396
397 if (node.getData('dragging') !== true) {
398 // Collapse comment during move.
399 node.collapse(0);
400 node.setData('dragging', true);
401 }
402
403 newlocation = this.editor.get_canvas_coordinates(new M.assignfeedback_editpdf.point(x, y));
404 bounds = this.editor.get_canvas_bounds(true);
405 bounds.x = 0;
406 bounds.y = 0;
407
408 bounds.width -= 24;
409 bounds.height -= 24;
410 // Clip to the window size - the comment icon size.
411 newlocation.clip(bounds);
412
413 this.x = newlocation.x;
414 this.y = newlocation.y;
415
416 windowlocation = this.editor.get_window_coordinates(newlocation);
417 container.setX(windowlocation.x);
418 container.setY(windowlocation.y);
419 this.drawable.store_position(container, windowlocation.x, windowlocation.y);
420 }
421 }, null, this);
23990a0a
TB
422 node.on('gesturemoveend', function() {
423 if (editor.currentedit.tool === 'select') {
60e9f908
TB
424 if (node.getData('dragging') === true) {
425 node.setData('dragging', false);
426 }
23990a0a
TB
427 this.editor.save_current_page();
428 }
429 }, null, this);
60e9f908
TB
430 marker.on('gesturemovestart', function(e) {
431 if (editor.currentedit.tool === 'select') {
432 e.preventDefault();
433 node.setData('offsetx', e.clientX - container.getX());
434 node.setData('offsety', e.clientY - container.getY());
435 node.expand();
436 }
437 });
438 marker.on('gesturemove', function(e) {
23990a0a
TB
439 if (editor.currentedit.tool === 'select') {
440 var x = e.clientX - node.getData('offsetx'),
441 y = e.clientY - node.getData('offsety'),
23990a0a
TB
442 newlocation,
443 windowlocation,
444 bounds;
445
60e9f908
TB
446 if (node.getData('dragging') !== true) {
447 // Collapse comment during move.
448 node.collapse(100);
449 node.setData('dragging', true);
450 }
23990a0a
TB
451
452 newlocation = this.editor.get_canvas_coordinates(new M.assignfeedback_editpdf.point(x, y));
453 bounds = this.editor.get_canvas_bounds(true);
454 bounds.x = 0;
455 bounds.y = 0;
456
60e9f908
TB
457 bounds.width -= 24;
458 bounds.height -= 24;
459 // Clip to the window size - the comment icon size.
23990a0a
TB
460 newlocation.clip(bounds);
461
462 this.x = newlocation.x;
463 this.y = newlocation.y;
464
465 windowlocation = this.editor.get_window_coordinates(newlocation);
466 container.setX(windowlocation.x);
467 container.setY(windowlocation.y);
468 this.drawable.store_position(container, windowlocation.x, windowlocation.y);
469 }
470 }, null, this);
60e9f908
TB
471 marker.on('gesturemoveend', function() {
472 if (editor.currentedit.tool === 'select') {
473 if (node.getData('dragging') === true) {
474 node.setData('dragging', false);
475 }
476 this.editor.save_current_page();
477 }
478 }, null, this);
23990a0a
TB
479
480 this.menu = new M.assignfeedback_editpdf.commentmenu({
481 buttonNode: this.menulink,
482 comment: this
483 });
484 }
5c386472
DW
485 };
486
487 /**
488 * Delete a comment.
489 * @method remove
490 */
491 this.remove = function() {
bc8b6dc6
DP
492 var i = 0;
493 var comments;
5c386472
DW
494
495 comments = this.editor.pages[this.editor.currentpage].comments;
496 for (i = 0; i < comments.length; i++) {
497 if (comments[i] === this) {
498 comments.splice(i, 1);
499 this.drawable.erase();
500 this.editor.save_current_page();
501 return;
502 }
503 }
504 };
505
506 /**
507 * Event handler to remove a comment from the users quicklist.
508 *
509 * @protected
510 * @method remove_from_quicklist
511 */
512 this.remove_from_quicklist = function(e, quickcomment) {
114913e3 513 e.preventDefault();
23990a0a 514 e.stopPropagation();
114913e3 515
5c386472
DW
516 this.menu.hide();
517
518 this.editor.quicklist.remove(quickcomment);
519 };
520
521 /**
522 * A quick comment was selected in the list, update the active comment and redraw the page.
523 *
524 * @param Event e
525 * @protected
526 * @method set_from_quick_comment
527 */
528 this.set_from_quick_comment = function(e, quickcomment) {
114913e3
DS
529 e.preventDefault();
530
5c386472 531 this.menu.hide();
23990a0a 532 this.deleteme = false;
5c386472
DW
533
534 this.rawtext = quickcomment.rawtext;
535 this.width = quickcomment.width;
536 this.colour = quickcomment.colour;
537
538 this.editor.save_current_page();
539
540 this.editor.redraw();
23990a0a
TB
541
542 this.node = this.drawable.nodes[0].one('textarea');
543 this.node.ancestor('div').removeClass('commentcollapsed');
544 this.node.focus();
5c386472
DW
545 };
546
547 /**
548 * Event handler to add a comment to the users quicklist.
549 *
550 * @protected
551 * @method add_to_quicklist
552 */
114913e3
DS
553 this.add_to_quicklist = function(e) {
554 e.preventDefault();
5c386472
DW
555 this.menu.hide();
556 this.editor.quicklist.add(this);
557 };
558
559 /**
560 * Draw the in progress edit.
561 *
562 * @public
563 * @method draw_current_edit
564 * @param M.assignfeedback_editpdf.edit edit
565 */
566 this.draw_current_edit = function(edit) {
567 var drawable = new M.assignfeedback_editpdf.drawable(this.editor),
568 shape,
569 bounds;
570
571 bounds = new M.assignfeedback_editpdf.rect();
572 bounds.bound([edit.start, edit.end]);
573
574 // We will draw a box with the current background colour.
575 shape = this.editor.graphic.addShape({
576 type: Y.Rect,
577 width: bounds.width,
578 height: bounds.height,
579 fill: {
580 color: COMMENTCOLOUR[edit.commentcolour]
581 },
582 x: bounds.x,
583 y: bounds.y
584 });
585
586 drawable.shapes.push(shape);
587
588 return drawable;
589 };
590
591 /**
592 * Promote the current edit to a real comment.
593 *
594 * @public
595 * @method init_from_edit
596 * @param M.assignfeedback_editpdf.edit edit
baf881b8 597 * @return bool true if comment bound is more than min width/height, else false.
5c386472
DW
598 */
599 this.init_from_edit = function(edit) {
600 var bounds = new M.assignfeedback_editpdf.rect();
601 bounds.bound([edit.start, edit.end]);
602
603 // Minimum comment width.
604 if (bounds.width < 100) {
605 bounds.width = 100;
606 }
607
608 // Save the current edit to the server and the current page list.
609
610 this.gradeid = this.editor.get('gradeid');
611 this.pageno = this.editor.currentpage;
612 this.x = bounds.x;
613 this.y = bounds.y;
614 this.width = bounds.width;
615 this.colour = edit.commentcolour;
616 this.rawtext = '';
baf881b8
RT
617
618 return (bounds.has_min_width() && bounds.has_min_height());
5c386472
DW
619 };
620
9432553b
NN
621 /**
622 * Update comment position when rotating page.
623 * @public
624 * @method updatePosition
625 */
626 this.updatePosition = function() {
627 var node = this.drawable.nodes[0].one('textarea');
628 var container = node.ancestor('div');
629
630 var newlocation = new M.assignfeedback_editpdf.point(this.x, this.y);
631 var windowlocation = this.editor.get_window_coordinates(newlocation);
632
633 container.setX(windowlocation.x);
634 container.setY(windowlocation.y);
635 this.drawable.store_position(container, windowlocation.x, windowlocation.y);
636 };
637
5c386472
DW
638};
639
640M.assignfeedback_editpdf = M.assignfeedback_editpdf || {};
641M.assignfeedback_editpdf.comment = COMMENT;