MDL-55322 assignfeedback_editpdf: Prevent scroll when moving comments
[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: this.currentpage,
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             menu,
166             position,
167             scrollheight;
169         // Lets add a contenteditable div.
170         node = Y.Node.create('<textarea/>');
171         container = Y.Node.create('<div class="commentdrawable"/>');
172         menu = Y.Node.create('<a href="#"><img src="' + M.util.image_url('t/contextmenu', 'core') + '"/></a>');
174         this.menulink = menu;
175         container.append(node);
177         if (!this.editor.get('readonly')) {
178             container.append(menu);
179         } else {
180             node.setAttribute('readonly', 'readonly');
181         }
182         if (this.width < 100) {
183             this.width = 100;
184         }
186         position = this.editor.get_window_coordinates(new M.assignfeedback_editpdf.point(this.x, this.y));
187         node.setStyles({
188             width: this.width + 'px',
189             backgroundColor: COMMENTCOLOUR[this.colour],
190             color: COMMENTTEXTCOLOUR
191         });
193         drawingregion.append(container);
194         container.setStyle('position', 'absolute');
195         container.setX(position.x);
196         container.setY(position.y);
197         drawable.store_position(container, position.x, position.y);
198         drawable.nodes.push(container);
199         node.set('value', this.rawtext);
200         scrollheight = node.get('scrollHeight');
201         node.setStyles({
202             'height': scrollheight + 'px',
203             'overflow': 'hidden'
204         });
205         if (!this.editor.get('readonly')) {
206             this.attach_events(node, menu);
207         }
208         if (focus) {
209             node.focus();
210         }
211         this.drawable = drawable;
214         return drawable;
215     };
217     /**
218      * Delete an empty comment if it's menu hasn't been opened in time.
219      * @method delete_comment_later
220      */
221     this.delete_comment_later = function() {
222         if (this.deleteme) {
223             this.remove();
224         }
225     };
227     /**
228      * Comment nodes have a bunch of event handlers attached to them directly.
229      * This is all done here for neatness.
230      *
231      * @protected
232      * @method attach_comment_events
233      * @param node - The Y.Node representing the comment.
234      * @param menu - The Y.Node representing the menu.
235      */
236     this.attach_events = function(node, menu) {
237         // Save the text on blur.
238         node.on('blur', function() {
239             // Save the changes back to the comment.
240             this.rawtext = node.get('value');
241             this.width = parseInt(node.getStyle('width'), 10);
243             // Trim.
244             if (this.rawtext.replace(/^\s+|\s+$/g, "") === '') {
245                 // Delete empty comments.
246                 this.deleteme = true;
247                 Y.later(400, this, this.delete_comment_later);
248             }
249             this.editor.save_current_page();
250             this.editor.editingcomment = false;
251         }, this);
253         // For delegated event handler.
254         menu.setData('comment', this);
256         node.on('keyup', function() {
257             var scrollheight = node.get('scrollHeight'),
258                 height = parseInt(node.getStyle('height'), 10);
260             // Webkit scrollheight fix.
261             if (scrollheight === height + 8) {
262                 scrollheight -= 8;
263             }
264             node.setStyle('height', scrollheight + 'px');
266         });
268         node.on('gesturemovestart', function(e) {
269             if (editor.currentedit.tool === 'select') {
270                 e.preventDefault();
271                 node.setData('dragging', true);
272                 node.setData('offsetx', e.clientX - node.getX());
273                 node.setData('offsety', e.clientY - node.getY());
274             }
275         });
276         node.on('gesturemoveend', function() {
277             if (editor.currentedit.tool === 'select') {
278                 node.setData('dragging', false);
279                 this.editor.save_current_page();
280             }
281         }, null, this);
282         node.on('gesturemove', function(e) {
283             if (editor.currentedit.tool === 'select') {
284                 var x = e.clientX - node.getData('offsetx'),
285                     y = e.clientY - node.getData('offsety'),
286                     nodewidth,
287                     nodeheight,
288                     newlocation,
289                     windowlocation,
290                     bounds;
292                 nodewidth = parseInt(node.getStyle('width'), 10);
293                 nodeheight = parseInt(node.getStyle('height'), 10);
295                 newlocation = this.editor.get_canvas_coordinates(new M.assignfeedback_editpdf.point(x, y));
296                 bounds = this.editor.get_canvas_bounds(true);
297                 bounds.x = 0;
298                 bounds.y = 0;
300                 bounds.width -= nodewidth + 42;
301                 bounds.height -= nodeheight + 8;
302                 // Clip to the window size - the comment size.
303                 newlocation.clip(bounds);
305                 this.x = newlocation.x;
306                 this.y = newlocation.y;
308                 windowlocation = this.editor.get_window_coordinates(newlocation);
309                 node.ancestor().setX(windowlocation.x);
310                 node.ancestor().setY(windowlocation.y);
311                 this.drawable.store_position(node.ancestor(), windowlocation.x, windowlocation.y);
312             }
313         }, null, this);
315         this.menu = new M.assignfeedback_editpdf.commentmenu({
316             buttonNode: this.menulink,
317             comment: this
318         });
319     };
321     /**
322      * Delete a comment.
323      * @method remove
324      */
325     this.remove = function() {
326         var i = 0;
327         var comments;
329         comments = this.editor.pages[this.editor.currentpage].comments;
330         for (i = 0; i < comments.length; i++) {
331             if (comments[i] === this) {
332                 comments.splice(i, 1);
333                 this.drawable.erase();
334                 this.editor.save_current_page();
335                 return;
336             }
337         }
338     };
340     /**
341      * Event handler to remove a comment from the users quicklist.
342      *
343      * @protected
344      * @method remove_from_quicklist
345      */
346     this.remove_from_quicklist = function(e, quickcomment) {
347         e.preventDefault();
349         this.menu.hide();
351         this.editor.quicklist.remove(quickcomment);
352     };
354     /**
355      * A quick comment was selected in the list, update the active comment and redraw the page.
356      *
357      * @param Event e
358      * @protected
359      * @method set_from_quick_comment
360      */
361     this.set_from_quick_comment = function(e, quickcomment) {
362         e.preventDefault();
364         this.menu.hide();
366         this.rawtext = quickcomment.rawtext;
367         this.width = quickcomment.width;
368         this.colour = quickcomment.colour;
370         this.editor.save_current_page();
372         this.editor.redraw();
373     };
375     /**
376      * Event handler to add a comment to the users quicklist.
377      *
378      * @protected
379      * @method add_to_quicklist
380      */
381     this.add_to_quicklist = function(e) {
382         e.preventDefault();
383         this.menu.hide();
384         this.editor.quicklist.add(this);
385     };
387     /**
388      * Draw the in progress edit.
389      *
390      * @public
391      * @method draw_current_edit
392      * @param M.assignfeedback_editpdf.edit edit
393      */
394     this.draw_current_edit = function(edit) {
395         var drawable = new M.assignfeedback_editpdf.drawable(this.editor),
396             shape,
397             bounds;
399         bounds = new M.assignfeedback_editpdf.rect();
400         bounds.bound([edit.start, edit.end]);
402         // We will draw a box with the current background colour.
403         shape = this.editor.graphic.addShape({
404             type: Y.Rect,
405             width: bounds.width,
406             height: bounds.height,
407             fill: {
408                color: COMMENTCOLOUR[edit.commentcolour]
409             },
410             x: bounds.x,
411             y: bounds.y
412         });
414         drawable.shapes.push(shape);
416         return drawable;
417     };
419     /**
420      * Promote the current edit to a real comment.
421      *
422      * @public
423      * @method init_from_edit
424      * @param M.assignfeedback_editpdf.edit edit
425      * @return bool true if comment bound is more than min width/height, else false.
426      */
427     this.init_from_edit = function(edit) {
428         var bounds = new M.assignfeedback_editpdf.rect();
429         bounds.bound([edit.start, edit.end]);
431         // Minimum comment width.
432         if (bounds.width < 100) {
433             bounds.width = 100;
434         }
436         // Save the current edit to the server and the current page list.
438         this.gradeid = this.editor.get('gradeid');
439         this.pageno = this.editor.currentpage;
440         this.x = bounds.x;
441         this.y = bounds.y;
442         this.width = bounds.width;
443         this.colour = edit.commentcolour;
444         this.rawtext = '';
446         return (bounds.has_min_width() && bounds.has_min_height());
447     };
449 };
451 M.assignfeedback_editpdf = M.assignfeedback_editpdf || {};
452 M.assignfeedback_editpdf.comment = COMMENT;