2451176bf83070afe74900e7f0808478fee27794
[moodle.git] / mod / assign / feedback / editpdf / yui / src / editor / js / comment.js
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/>.
15 /* global SELECTOR, COMMENTCOLOUR, COMMENTTEXTCOLOUR */
17 /**
18  * Provides an in browser PDF editor.
19  *
20  * @module moodle-assignfeedback_editpdf-editor
21  */
23 /**
24  * Class representing a list of comments.
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  */
37 var COMMENT = function(editor, gradeid, pageno, x, y, width, colour, rawtext) {
39     /**
40      * Reference to M.assignfeedback_editpdf.editor.
41      * @property editor
42      * @type M.assignfeedback_editpdf.editor
43      * @public
44      */
45     this.editor = editor;
47     /**
48      * Grade id
49      * @property gradeid
50      * @type Int
51      * @public
52      */
53     this.gradeid = gradeid || 0;
55     /**
56      * X position
57      * @property x
58      * @type Int
59      * @public
60      */
61     this.x = parseInt(x, 10) || 0;
63     /**
64      * Y position
65      * @property y
66      * @type Int
67      * @public
68      */
69     this.y = parseInt(y, 10) || 0;
71     /**
72      * Comment width
73      * @property width
74      * @type Int
75      * @public
76      */
77     this.width = parseInt(width, 10) || 0;
79     /**
80      * Comment rawtext
81      * @property rawtext
82      * @type String
83      * @public
84      */
85     this.rawtext = rawtext || '';
87     /**
88      * Comment page number
89      * @property pageno
90      * @type Int
91      * @public
92      */
93     this.pageno = pageno || 0;
95     /**
96      * Comment background colour.
97      * @property colour
98      * @type String
99      * @public
100      */
101     this.colour = colour || 'yellow';
103     /**
104      * Reference to M.assignfeedback_editpdf.drawable
105      * @property drawable
106      * @type M.assignfeedback_editpdf.drawable
107      * @public
108      */
109     this.drawable = false;
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;
119     /**
120      * Reference to the link that opens the menu.
121      * @property menulink
122      * @type Y.Node
123      * @public
124      */
125     this.menulink = null;
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;
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 {
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,
148             pageno: parseInt(this.pageno, 10),
149             colour: this.colour
150         };
151     };
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,
163             drawingregion = this.editor.get_dialogue_element(SELECTOR.DRAWINGREGION),
164             container,
165             label,
166             marker,
167             menu,
168             position,
169             scrollheight;
171         // Lets add a contenteditable div.
172         node = Y.Node.create('<textarea/>');
173         container = Y.Node.create('<div class="commentdrawable"/>');
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>');
179         menu = Y.Node.create('<a href="#"><img src="' + M.util.image_url('t/contextmenu', 'core') + '"/></a>');
181         this.menulink = menu;
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');
188         menu.setAttribute('tabindex', '0');
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         }
199         position = this.editor.get_window_coordinates(new M.assignfeedback_editpdf.point(this.x, this.y));
200         node.setStyles({
201             width: this.width + 'px',
202             backgroundColor: COMMENTCOLOUR[this.colour],
203             color: COMMENTTEXTCOLOUR
204         });
206         drawingregion.append(container);
207         container.setStyle('position', 'absolute');
208         container.setX(position.x);
209         container.setY(position.y);
210         drawable.store_position(container, position.x, position.y);
211         drawable.nodes.push(container);
212         node.set('value', this.rawtext);
213         scrollheight = node.get('scrollHeight');
214         node.setStyles({
215             'height': scrollheight + 'px',
216             'overflow': 'hidden'
217         });
218         marker.setStyle('color', COMMENTCOLOUR[this.colour]);
219         this.attach_events(node, menu);
220         if (focus) {
221             node.focus();
222         } else if (editor.collapsecomments) {
223             container.addClass('commentcollapsed');
224         }
225         this.drawable = drawable;
228         return drawable;
229     };
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     };
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) {
251         var container = node.ancestor('div'),
252             label = node.ancestor('label'),
253             marker = label.next('svg');
255         // Function to collapse a comment to a marker icon.
256         node.collapse = function(delay) {
257             node.collapse.delay = Y.later(delay, node, function() {
258                 if (editor.collapsecomments) {
259                     container.addClass('commentcollapsed');
260                 }
261             });
262         };
264         // Function to expand a comment.
265         node.expand = function() {
266             if (node.getData('dragging') !== true) {
267                 if (node.collapse.delay) {
268                     node.collapse.delay.cancel();
269                 }
270                 container.removeClass('commentcollapsed');
271             }
272         };
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();
278             }
279         }, this);
280         container.on('click|tap', function() {
281             node.expand();
282             node.focus();
283         }, this);
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);
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             }
306             // Give comment a tabindex to prevent focus outline being suppressed.
307             node.setAttribute('tabindex', '0');
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();
318             }
319             this.deleteme = false;
320             // Restore label tabindex so user can tab back to it from menu.
321             label.setAttribute('tabindex', '0');
322         }, this);
324         // Always restore the default tabindex states when moving away.
325         node.on('blur', function() {
326             node.setAttribute('tabindex', '-1');
327         }, this);
328         label.on('blur', function() {
329             label.setAttribute('tabindex', '0');
330         }, this);
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);
336             }
337         }, this);
339         // Collapse comment on blur.
340         container.on('blur', function() {
341             node.active = false;
342             node.collapse(800);
343         }, this);
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);
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);
362             // For delegated event handler.
363             menu.setData('comment', this);
365             node.on('keyup', function() {
366                 node.setStyle('height', 'auto');
367                 var scrollheight = node.get('scrollHeight'),
368                     height = parseInt(node.getStyle('height'), 10);
370                 // Webkit scrollheight fix.
371                 if (scrollheight === height + 8) {
372                     scrollheight -= 8;
373                 }
374                 node.setStyle('height', scrollheight + 'px');
375             });
377             node.on('gesturemovestart', function(e) {
378                 if (editor.currentedit.tool === 'select') {
379                     e.preventDefault();
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                     }
387                 }
388             });
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;
397                     if (node.getData('dragging') !== true) {
398                         // Collapse comment during move.
399                         node.collapse(0);
400                         node.setData('dragging', true);
401                     }
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;
408                     bounds.width -= 24;
409                     bounds.height -= 24;
410                     // Clip to the window size - the comment icon size.
411                     newlocation.clip(bounds);
413                     this.x = newlocation.x;
414                     this.y = newlocation.y;
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);
422             node.on('gesturemoveend', function() {
423                 if (editor.currentedit.tool === 'select') {
424                     if (node.getData('dragging') === true) {
425                         node.setData('dragging', false);
426                     }
427                     this.editor.save_current_page();
428                 }
429             }, null, this);
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) {
439                 if (editor.currentedit.tool === 'select') {
440                     var x = e.clientX - node.getData('offsetx'),
441                         y = e.clientY - node.getData('offsety'),
442                         newlocation,
443                         windowlocation,
444                         bounds;
446                     if (node.getData('dragging') !== true) {
447                         // Collapse comment during move.
448                         node.collapse(100);
449                         node.setData('dragging', true);
450                     }
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;
457                     bounds.width -= 24;
458                     bounds.height -= 24;
459                     // Clip to the window size - the comment icon size.
460                     newlocation.clip(bounds);
462                     this.x = newlocation.x;
463                     this.y = newlocation.y;
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);
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);
480             this.menu = new M.assignfeedback_editpdf.commentmenu({
481                 buttonNode: this.menulink,
482                 comment: this
483             });
484         }
485     };
487     /**
488      * Delete a comment.
489      * @method remove
490      */
491     this.remove = function() {
492         var i = 0;
493         var comments;
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     };
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) {
513         e.preventDefault();
514         e.stopPropagation();
516         this.menu.hide();
518         this.editor.quicklist.remove(quickcomment);
519     };
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) {
529         e.preventDefault();
531         this.menu.hide();
532         this.deleteme = false;
534         this.rawtext = quickcomment.rawtext;
535         this.width = quickcomment.width;
536         this.colour = quickcomment.colour;
538         this.editor.save_current_page();
540         this.editor.redraw();
542         this.node = this.drawable.nodes[0].one('textarea');
543         this.node.ancestor('div').removeClass('commentcollapsed');
544         this.node.focus();
545     };
547     /**
548      * Event handler to add a comment to the users quicklist.
549      *
550      * @protected
551      * @method add_to_quicklist
552      */
553     this.add_to_quicklist = function(e) {
554         e.preventDefault();
555         this.menu.hide();
556         this.editor.quicklist.add(this);
557     };
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;
571         bounds = new M.assignfeedback_editpdf.rect();
572         bounds.bound([edit.start, edit.end]);
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         });
586         drawable.shapes.push(shape);
588         return drawable;
589     };
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
597      * @return bool true if comment bound is more than min width/height, else false.
598      */
599     this.init_from_edit = function(edit) {
600         var bounds = new M.assignfeedback_editpdf.rect();
601         bounds.bound([edit.start, edit.end]);
603         // Minimum comment width.
604         if (bounds.width < 100) {
605             bounds.width = 100;
606         }
608         // Save the current edit to the server and the current page list.
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 = '';
618         return (bounds.has_min_width() && bounds.has_min_height());
619     };
621 };
623 M.assignfeedback_editpdf = M.assignfeedback_editpdf || {};
624 M.assignfeedback_editpdf.comment = COMMENT;