/** * @author Benjamin Baer <benjamin.baer@math.uzh.ch> */ /* global $ */ /* global EventEmitter */ /* @depend QfqEvents.js */ /* @depend Alert.js */ /* @depend Comment.js */ /* @depend CommentController */ /* @depend SyntaxHighlighter */ /** * Qfq Namespace * * @namespace QfqNS */ var QfqNS = QfqNS || {}; (function (n) { 'use strict'; /** * Displays Code in a stylized fashion and allows * to write and display comments for each line of code. * * @param form Reference to the parent qfq Element * @param data Object containing the to be displayed data * @param $container Reference to the HTML Element that displays * the code correction * @param $target Reference to the HTML Element where the output (comment JSON) * should be stored. */ n.CodeCorrection = function () { this.page = {}; this.data = {}; this.eventEmitter = new EventEmitter(); this.$parent = {}; this.$target = {}; this.$rows = []; this.annotations = []; this.users = []; this.currentUser = {}; this.language = ""; this.readOnly = false; this.syntaxHighlight = {}; }; /** * Initializes the Code Correction object, fetches data from URL or API if needed */ n.CodeCorrection.prototype.initialize = function($container, page) { this.$parent = $container; this.$target = $("#" + $container.data("target")); this.data = { url: this.$parent.data("file"), text: this.$parent.data("text") }; this.page = page; this.language = this.$parent.data("highlight") || "typo3conf/ext/qfq/Resources/Public/Json/javascript.json"; this.readOnly = this.$parent.data("view-only") || false; this.currentUser = $container.data("uid"); var that = this; if (this.readOnly) { if (this.$parent.data("annotations")) { var jsonAnnotations = this.$parent.data("annotations"); this.annotations = jsonAnnotations.annotations; this.users = jsonAnnotations.users; } else { this._importFromTarget(); } } else { this._importFromTarget(); } if (this.data.url) { // Get data of a file and write it to data.text $.get(this.data.url, function(response) { that.data.text = response; that._prepareBuild(); }); } else if (this.data.text) { this._prepareBuild(); } else { console.error("[CodeCorrection] No Code to correct passed to the object."); } }; n.CodeCorrection.prototype._importFromTarget = function() { if (this.$target.val()) { var jImport = $.parseJSON(this.$target.val()); if (jImport.annotations) { this.annotations = jImport.annotations; console.log("[CodeCorrection] Imported Annotations: " + this.annotations.length); } if (jImport.users) { this.users = jImport.users; console.log("[CodeCorrection] Imported Users: " + this.users.length); } } }; n.CodeCorrection.prototype._prepareBuild = function() { var that = this; this.syntaxHighlight = new n.SyntaxHighlighter(); this.syntaxHighlight.importInstructions(this.language, function() { that._buildEditor(); }); }; /** * Breaks up the String by line and returns it as an Array * @param text Unix formatted text of the Code File * @returns {Array} Array with the Code broken up by Line */ n.CodeCorrection.prototype.createLineByLineArray = function(text) { var textArray = []; if (typeof text === 'string' || text instanceof String) { textArray = text.split("\n"); } return textArray; }; /** * Builds the Code Correction HTML Element that should be displayed to the user. * @private */ n.CodeCorrection.prototype._buildEditor = function() { var that = this; var $title = $('<div/>', { class: 'qfqCodeCorrectionTitle' }); $title.appendTo(this.$parent); var container = $('<div/>', { class: 'codeCorrectionWrap' }); var lineCount = 1; var textArray = this.createLineByLineArray(this.data.text); textArray.forEach(function(line) { that.$rows[lineCount] = that._buildLine(lineCount, line); that.$rows[lineCount].appendTo(container); lineCount++; }); container.appendTo(this.$parent); this._buildAnnotations(); }; /** * Checks which codelines have annotations and initializes a CommentController * for each of them. * @private */ n.CodeCorrection.prototype._buildAnnotations = function() { for (var i = 0; i < this.annotations.length; i++) { var annotation = this.annotations[i]; var $hook = this.$rows[annotation.lineNumber]; var commentController = this._buildCommentContainer($hook); commentController.importComments(annotation.comments, this.users); $hook.append(this._getCommentMarker(annotation.comments.length)); this._setListeners(commentController); this._setCommentController(annotation.lineNumber, commentController); } }; n.CodeCorrection.prototype._getCommentMarker = function(numberOfComments) { var container = $('<span/>', { class: "badge qfq-comment-marker", text: numberOfComments + ' ' }); container.append($('<span/>', { class: "glyphicon glyphicon-comment" })); return container; }; /** * Builds a Line as a combination of HTML Elements. Binds the necessary Events. * * @param lineCount * @param line * @returns {jQuery|HTMLElement} * @private */ n.CodeCorrection.prototype._buildLine = function(lineCount, line) { var that = this; var htmlRow = $('<div/>', { class: 'clearfix qfqCodeLine', id: 'qfqC' + lineCount }); htmlRow.on("click", function() { that._handleClick(htmlRow, lineCount);}); var htmlLineNumber = $('<div/>', { class: 'pull-left qfqLineCount', text: lineCount }); var cLine = line.replace('&', '&') .replace(';', ';') .replace('<', '<') .replace('>', '>') .replace(/\s/g, ' ') .replace('"', '"') .replace('\'', ''') .replace('\\', '\'); cLine = this.syntaxHighlight.highlightLine(cLine); var htmlCodeLine = $('<div/>', { class: 'pull-right qfqCode' }); htmlCodeLine.html(cLine); htmlLineNumber.appendTo(htmlRow); htmlCodeLine.appendTo(htmlRow); return htmlRow; }; /** * Initializes a CommentContainer at a given jQuery Hook and returns * the CommentContainer object * @param $hook * @returns {QfqNS.CommentController} * @private */ n.CodeCorrection.prototype._buildCommentContainer = function($hook) { var options = { readOnly: this.readOnly }; var commentController = new n.CommentController(); commentController.buildContainer($hook, options); commentController.setCurrentUser(this.currentUser); return commentController; }; /** * References the CommentController in this.annotations Array * for easy access later. * @param lineCount * @param commentController * @returns {boolean} * @private */ n.CodeCorrection.prototype._setCommentController = function(lineCount, commentController) { for (var i=0; i < this.annotations.length; i++) { if (this.annotations[i].lineNumber === lineCount) { this.annotations[i].commentController = commentController; return true; } } return false; }; /** * Sets listeners for events generated by the CommentController object * @param commentController * @private */ n.CodeCorrection.prototype._setListeners = function(commentController) { var that = this; commentController.on('comment.added', function(argument) { console.log("Catch event: " + that.annotations.length); console.log("With data: " + argument.data); that._handleNew(argument.data); }); commentController.on('comment.edited', function() { that._updateJSON(); }); commentController.on('comment.removed', function(e) { console.log(e); if(that._checkUserRemoval(e.data.uid)) { console.log("Removed User uid: " + e.data.uid); } that._checkLineRemoval(); that._updateJSON(); }); }; n.CodeCorrection.prototype._checkLineRemoval = function() { var removeLines = []; for (var i = 0; i < this.annotations.length; i++) { var comments = this.annotations[i].commentController.exportComments(); if(comments.length == 0) { removeLines.push(i); } } for (var ii = 0; ii < removeLines.length; ii++) { this.annotations.splice(removeLines[ii], 1); } }; n.CodeCorrection.prototype._handleNew = function(eventData) { this._addCurrentUser(); this._updateJSON(); }; n.CodeCorrection.prototype._checkUserRemoval = function(uid) { var removeUser = true; for (var i = 0; i < this.annotations.length; i++) { var comments = this.annotations[i].commentController.exportComments(); for (var ii = 0; ii < comments.length; ii++) { if(comments[ii].uid === uid) { removeUser = false; return false; } } } if (removeUser) { for (var iii = 0; iii < this.users.length; iii++) { if (this.users[iii].uid === uid) { this.users.splice(i, 1); } } } return true; }; n.CodeCorrection.prototype._addCurrentUser = function() { if (!this.checkUserExists(this.currentUser.uid)) { this.users.push(this.currentUser); } }; n.CodeCorrection.prototype.checkUserExists = function(uid) { for (var i = 0; i < this.users.length; i++) { if (this.users[i].uid === uid) { return true; } } return false; }; n.CodeCorrection.prototype._updateJSON = function() { var jexport = {}; jexport.annotations = []; for (var i = 0; i < this.annotations.length; i++) { var annotation = { lineNumber: this.annotations[i].lineNumber, comments: this.annotations[i].commentController.exportComments() }; jexport.annotations.push(annotation); } jexport.users = this.users; this.$target.val(JSON.stringify(jexport)); var that = this; if (this.page.qfqForm) { this.page.qfqForm.eventEmitter.emitEvent('form.changed', n.EventEmitter.makePayload(that, null)); this.page.qfqForm.changeHandler(); this.page.qfqForm.form.formChanged = true; } else { console.log(this.page); throw("Error: Couldn't initialize qfqForm - not possible to send form.changed event"); } }; /** * Places a comment editor under the hook. * @param $hook * @param lineCount * @private */ n.CodeCorrection.prototype._handleClick = function($hook, lineCount) { var comments = {}; if (this._hasComments(lineCount)) { comments = this._getComments(lineCount); comments.commentController.toggle(); comments.commentController.emitEvent("new"); } else { if (!this.readOnly) { comments.lineNumber = lineCount; comments.commentController = new n.CommentController(); comments.commentController.buildContainer($hook, {readOnly: this.readOnly}); comments.commentController.setCurrentUser(this.currentUser); comments.commentController.displayEditor(); this._setListeners(comments.commentController); this.annotations.push(comments); } } }; /** * Checks if a line already has comments * @param lineCount * @returns {boolean} * @private */ n.CodeCorrection.prototype._hasComments = function(lineCount) { for (var i=0; i < this.annotations.length; i++) { if (this.annotations[i].lineNumber === lineCount) { return true; } } return false; }; /** * Gets comments for a specific line or returns false if said * comments can't be found. * @param lineCount * @returns {*} * @private */ n.CodeCorrection.prototype._getComments = function(lineCount) { for (var i=0; i < this.annotations.length; i++) { if (this.annotations[i].lineNumber === lineCount) { return this.annotations[i]; } } return false; }; })(QfqNS);