AutorÃa | Ultima modificación | Ver Log |
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Provides an in browser PDF editor.
*
* @module moodle-assignfeedback_editpdf-editor
*/
/**
* Class representing a list of comments.
*
* @namespace M.assignfeedback_editpdf
* @class comment
* @param M.assignfeedback_editpdf.editor editor
* @param Int gradeid
* @param Int pageno
* @param Int x
* @param Int y
* @param Int width
* @param String colour
* @param String rawtext
*/
var COMMENT = function(editor, gradeid, pageno, x, y, width, colour, rawtext) {
/**
* Reference to M.assignfeedback_editpdf.editor.
* @property editor
* @type M.assignfeedback_editpdf.editor
* @public
*/
this.editor = editor;
/**
* Grade id
* @property gradeid
* @type Int
* @public
*/
this.gradeid = gradeid || 0;
/**
* X position
* @property x
* @type Int
* @public
*/
this.x = parseInt(x, 10) || 0;
/**
* Y position
* @property y
* @type Int
* @public
*/
this.y = parseInt(y, 10) || 0;
/**
* Comment width
* @property width
* @type Int
* @public
*/
this.width = parseInt(width, 10) || 0;
/**
* Comment rawtext
* @property rawtext
* @type String
* @public
*/
this.rawtext = rawtext || '';
/**
* Comment page number
* @property pageno
* @type Int
* @public
*/
this.pageno = pageno || 0;
/**
* Comment background colour.
* @property colour
* @type String
* @public
*/
this.colour = colour || 'yellow';
/**
* Reference to M.assignfeedback_editpdf.drawable
* @property drawable
* @type M.assignfeedback_editpdf.drawable
* @public
*/
this.drawable = false;
/**
* Boolean used by a timeout to delete empty comments after a short delay.
* @property deleteme
* @type Boolean
* @public
*/
this.deleteme = false;
/**
* Reference to the link that opens the menu.
* @property menulink
* @type Y.Node
* @public
*/
this.menulink = null;
/**
* Reference to the dialogue that is the context menu.
* @property menu
* @type M.assignfeedback_editpdf.dropdown
* @public
*/
this.menu = null;
/**
* Clean a comment record, returning an oject with only fields that are valid.
* @public
* @method clean
* @return {}
*/
this.clean = function() {
return {
gradeid: this.gradeid,
x: parseInt(this.x, 10),
y: parseInt(this.y, 10),
width: parseInt(this.width, 10),
rawtext: this.rawtext,
pageno: parseInt(this.pageno, 10),
colour: this.colour
};
};
/**
* Draw a comment.
* @public
* @method draw_comment
* @param boolean focus - Set the keyboard focus to the new comment if true
* @return M.assignfeedback_editpdf.drawable
*/
this.draw = function(focus) {
var drawable = new M.assignfeedback_editpdf.drawable(this.editor),
node,
drawingcanvas = this.editor.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
container,
label,
marker,
menu,
position,
scrollheight;
// Lets add a contenteditable div.
node = Y.Node.create('<textarea/>');
container = Y.Node.create('<div class="commentdrawable"/>');
label = Y.Node.create('<label/>');
marker = Y.Node.create('<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 13 13" ' +
'preserveAspectRatio="xMinYMin meet">' +
'<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" ' +
'fill="currentColor" opacity="0.9" stroke="rgb(153, 153, 153)" stroke-width="0.5"/></svg>');
menu = Y.Node.create('<a href="#"><img src="' + M.util.image_url('t/contextmenu', 'core') + '"/></a>');
this.menulink = menu;
container.append(label);
label.append(node);
container.append(marker);
container.setAttribute('tabindex', '-1');
label.setAttribute('tabindex', '0');
node.setAttribute('tabindex', '-1');
menu.setAttribute('tabindex', '0');
if (!this.editor.get('readonly')) {
container.append(menu);
} else {
node.setAttribute('readonly', 'readonly');
}
if (this.width < 100) {
this.width = 100;
}
position = this.editor.get_window_coordinates(new M.assignfeedback_editpdf.point(this.x, this.y));
node.setStyles({
width: this.width + 'px',
backgroundColor: COMMENTCOLOUR[this.colour],
color: COMMENTTEXTCOLOUR
});
drawingcanvas.append(container);
container.setStyle('position', 'absolute');
container.setX(position.x);
container.setY(position.y);
drawable.store_position(container, position.x, position.y);
drawable.nodes.push(container);
node.set('value', this.rawtext);
scrollheight = node.get('scrollHeight');
node.setStyles({
'height': scrollheight + 'px',
'overflow': 'hidden'
});
marker.setStyle('color', COMMENTCOLOUR[this.colour]);
this.attach_events(node, menu);
if (focus) {
node.focus();
} else if (editor.collapsecomments) {
container.addClass('commentcollapsed');
}
this.drawable = drawable;
return drawable;
};
/**
* Delete an empty comment if it's menu hasn't been opened in time.
* @method delete_comment_later
*/
this.delete_comment_later = function() {
if (this.deleteme && !this.is_menu_active()) {
this.remove();
}
};
/**
* Returns true if the menu is active, false otherwise.
*
* @return bool true if menu is active, else false.
*/
this.is_menu_active = function() {
return this.menu !== null && this.menu.get('visible');
};
/**
* Comment nodes have a bunch of event handlers attached to them directly.
* This is all done here for neatness.
*
* @protected
* @method attach_comment_events
* @param node - The Y.Node representing the comment.
* @param menu - The Y.Node representing the menu.
*/
this.attach_events = function(node, menu) {
var container = node.ancestor('div'),
label = node.ancestor('label'),
marker = label.next('svg');
// Function to collapse a comment to a marker icon.
node.collapse = function(delay) {
node.collapse.delay = Y.later(delay, node, function() {
if (editor.collapsecomments && !this.is_menu_active()) {
container.addClass('commentcollapsed');
}
}.bind(this));
}.bind(this);
// Function to expand a comment.
node.expand = function() {
if (node.getData('dragging') !== true) {
if (node.collapse.delay) {
node.collapse.delay.cancel();
}
container.removeClass('commentcollapsed');
}
};
// Expand comment on mouse over (under certain conditions) or click/tap.
container.on('mouseenter', function() {
if (editor.currentedit.tool === 'comment' || editor.currentedit.tool === 'select' || this.editor.get('readonly')) {
node.expand();
}
}, this);
container.on('click|tap', function() {
node.expand();
node.focus();
}, this);
// Functions to capture reverse tabbing events.
node.on('keyup', function(e) {
if (e.keyCode === 9 && e.shiftKey && menu.getAttribute('tabindex') === '0') {
// User landed here via Shift+Tab (but not from this comment's menu).
menu.focus();
}
menu.setAttribute('tabindex', '0');
}, this);
menu.on('keydown', function(e) {
if (e.keyCode === 9 && e.shiftKey) {
// User is tabbing back to the comment node from its own menu.
menu.setAttribute('tabindex', '-1');
}
}, this);
// Comment becomes "active" on label or menu focus.
label.on('focus', function() {
node.active = true;
if (node.collapse.delay) {
node.collapse.delay.cancel();
}
// Give comment a tabindex to prevent focus outline being suppressed.
node.setAttribute('tabindex', '0');
// Expand comment and pass focus to it.
node.expand();
node.focus();
// Now remove label tabindex so user can reverse tab past it.
label.setAttribute('tabindex', '-1');
}, this);
menu.on('focus', function() {
node.active = true;
if (node.collapse.delay) {
node.collapse.delay.cancel();
}
this.deleteme = false;
// Restore label tabindex so user can tab back to it from menu.
label.setAttribute('tabindex', '0');
}, this);
// Always restore the default tabindex states when moving away.
node.on('blur', function() {
node.setAttribute('tabindex', '-1');
}, this);
label.on('blur', function() {
label.setAttribute('tabindex', '0');
}, this);
// Collapse comment on mouse out if not currently active.
container.on('mouseleave', function() {
if (editor.collapsecomments && node.active !== true) {
node.collapse(400);
}
}, this);
// Collapse comment on blur.
container.on('blur', function() {
node.active = false;
node.collapse(800);
}, this);
if (!this.editor.get('readonly')) {
// Save the text on blur.
node.on('blur', function() {
// Save the changes back to the comment.
this.rawtext = node.get('value');
this.width = parseInt(node.getStyle('width'), 10);
// Trim.
if (this.rawtext.replace(/^\s+|\s+$/g, "") === '') {
// Delete empty comments.
this.deleteme = true;
Y.later(400, this, this.delete_comment_later);
}
this.editor.save_current_page();
this.editor.editingcomment = false;
}, this);
// For delegated event handler.
menu.setData('comment', this);
node.on('keyup', function() {
node.setStyle('height', 'auto');
var scrollheight = node.get('scrollHeight'),
height = parseInt(node.getStyle('height'), 10);
// Webkit scrollheight fix.
if (scrollheight === height + 8) {
scrollheight -= 8;
}
node.setStyle('height', scrollheight + 'px');
});
node.on('gesturemovestart', function(e) {
if (editor.currentedit.tool === 'select') {
e.preventDefault();
if (editor.collapsecomments) {
node.setData('offsetx', 8);
node.setData('offsety', 8);
} else {
node.setData('offsetx', e.clientX - container.getX());
node.setData('offsety', e.clientY - container.getY());
}
}
});
node.on('gesturemove', function(e) {
if (editor.currentedit.tool === 'select') {
var x = e.clientX - node.getData('offsetx'),
y = e.clientY - node.getData('offsety'),
newlocation,
windowlocation,
bounds;
if (node.getData('dragging') !== true) {
// Collapse comment during move.
node.collapse(0);
node.setData('dragging', true);
}
newlocation = this.editor.get_canvas_coordinates(new M.assignfeedback_editpdf.point(x, y));
bounds = this.editor.get_canvas_bounds(true);
bounds.x = 0;
bounds.y = 0;
bounds.width -= 24;
bounds.height -= 24;
// Clip to the window size - the comment icon size.
newlocation.clip(bounds);
this.x = newlocation.x;
this.y = newlocation.y;
windowlocation = this.editor.get_window_coordinates(newlocation);
container.setX(windowlocation.x);
container.setY(windowlocation.y);
this.drawable.store_position(container, windowlocation.x, windowlocation.y);
}
}, null, this);
node.on('gesturemoveend', function() {
if (editor.currentedit.tool === 'select') {
if (node.getData('dragging') === true) {
node.setData('dragging', false);
}
this.editor.save_current_page();
}
}, null, this);
marker.on('gesturemovestart', function(e) {
if (editor.currentedit.tool === 'select') {
e.preventDefault();
node.setData('offsetx', e.clientX - container.getX());
node.setData('offsety', e.clientY - container.getY());
node.expand();
}
});
marker.on('gesturemove', function(e) {
if (editor.currentedit.tool === 'select') {
var x = e.clientX - node.getData('offsetx'),
y = e.clientY - node.getData('offsety'),
newlocation,
windowlocation,
bounds;
if (node.getData('dragging') !== true) {
// Collapse comment during move.
node.collapse(100);
node.setData('dragging', true);
}
newlocation = this.editor.get_canvas_coordinates(new M.assignfeedback_editpdf.point(x, y));
bounds = this.editor.get_canvas_bounds(true);
bounds.x = 0;
bounds.y = 0;
bounds.width -= 24;
bounds.height -= 24;
// Clip to the window size - the comment icon size.
newlocation.clip(bounds);
this.x = newlocation.x;
this.y = newlocation.y;
windowlocation = this.editor.get_window_coordinates(newlocation);
container.setX(windowlocation.x);
container.setY(windowlocation.y);
this.drawable.store_position(container, windowlocation.x, windowlocation.y);
}
}, null, this);
marker.on('gesturemoveend', function() {
if (editor.currentedit.tool === 'select') {
if (node.getData('dragging') === true) {
node.setData('dragging', false);
}
this.editor.save_current_page();
}
}, null, this);
this.menu = new M.assignfeedback_editpdf.commentmenu({
buttonNode: this.menulink,
comment: this
});
}
};
/**
* Delete a comment.
* @method remove
*/
this.remove = function() {
var i = 0;
var comments;
comments = this.editor.pages[this.editor.currentpage].comments;
for (i = 0; i < comments.length; i++) {
if (comments[i] === this) {
comments.splice(i, 1);
this.drawable.erase();
this.editor.save_current_page();
return;
}
}
};
/**
* Event handler to remove a comment from the users quicklist.
*
* @protected
* @method remove_from_quicklist
*/
this.remove_from_quicklist = function(e, quickcomment) {
e.preventDefault();
e.stopPropagation();
this.menu.hide();
this.editor.quicklist.remove(quickcomment);
};
/**
* A quick comment was selected in the list, update the active comment and redraw the page.
*
* @param Event e
* @protected
* @method set_from_quick_comment
*/
this.set_from_quick_comment = function(e, quickcomment) {
e.preventDefault();
this.menu.hide();
this.deleteme = false;
this.rawtext = quickcomment.rawtext;
this.width = quickcomment.width;
this.colour = quickcomment.colour;
this.editor.save_current_page();
this.editor.redraw();
this.node = this.drawable.nodes[0].one('textarea');
this.node.ancestor('div').removeClass('commentcollapsed');
this.node.focus();
};
/**
* Event handler to add a comment to the users quicklist.
*
* @protected
* @method add_to_quicklist
*/
this.add_to_quicklist = function(e) {
e.preventDefault();
this.menu.hide();
this.editor.quicklist.add(this);
};
/**
* Draw the in progress edit.
*
* @public
* @method draw_current_edit
* @param M.assignfeedback_editpdf.edit edit
*/
this.draw_current_edit = function(edit) {
var drawable = new M.assignfeedback_editpdf.drawable(this.editor),
shape,
bounds;
bounds = new M.assignfeedback_editpdf.rect();
bounds.bound([edit.start, edit.end]);
// We will draw a box with the current background colour.
shape = this.editor.graphic.addShape({
type: Y.Rect,
width: bounds.width,
height: bounds.height,
fill: {
color: COMMENTCOLOUR[edit.commentcolour]
},
x: bounds.x,
y: bounds.y
});
drawable.shapes.push(shape);
return drawable;
};
/**
* Promote the current edit to a real comment.
*
* @public
* @method init_from_edit
* @param M.assignfeedback_editpdf.edit edit
* @return bool true if comment bound is more than min width/height, else false.
*/
this.init_from_edit = function(edit) {
var bounds = new M.assignfeedback_editpdf.rect();
bounds.bound([edit.start, edit.end]);
// Minimum comment width.
if (bounds.width < 100) {
bounds.width = 100;
}
// Save the current edit to the server and the current page list.
this.gradeid = this.editor.get('gradeid');
this.pageno = this.editor.currentpage;
this.x = bounds.x;
this.y = bounds.y;
this.width = bounds.width;
this.colour = edit.commentcolour;
this.rawtext = '';
return (bounds.has_min_width() && bounds.has_min_height());
};
/**
* Update comment position when rotating page.
* @public
* @method updatePosition
*/
this.updatePosition = function() {
var node = this.drawable.nodes[0].one('textarea');
var container = node.ancestor('div');
var newlocation = new M.assignfeedback_editpdf.point(this.x, this.y);
var windowlocation = this.editor.get_window_coordinates(newlocation);
container.setX(windowlocation.x);
container.setY(windowlocation.y);
this.drawable.store_position(container, windowlocation.x, windowlocation.y);
};
};
M.assignfeedback_editpdf = M.assignfeedback_editpdf || {};
M.assignfeedback_editpdf.comment = COMMENT;