/*!     jquery-comments.js 1.5.0
 *
 *     (c) 2017 Joona Tykkyläinen, Viima Solutions Oy
 *     jquery-comments may be freely distributed under the MIT license.
 *     For all details and documentation:
 *     http://viima.github.io/jquery-comments/
 */

(function (factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['jquery'], factory);
    } else if (typeof module === 'object' && module.exports) {
        // Node/CommonJS
        module.exports = function(root, jQuery) {
            if (jQuery === undefined) {
                // require('jQuery') returns a factory that requires window to
                // build a jQuery instance, we normalize how we use modules
                // that require this pattern but the window provided is a noop
                // if it's defined (how jquery works)
                if (typeof window !== 'undefined') {
                    jQuery = require('jquery');
                }
                else {
                    jQuery = require('jquery')(root);
                }
            }
            factory(jQuery);
            return jQuery;
        };
    } else {
        // Browser globals
        factory(jQuery);
    }
}(function($) {

    var Comments = {

        // Instance variables
        // ==================

        $el: null,
        commentsById: {},
        dataFetched: false,
        currentSortKey: '',
        options: {},
        events: {
            // Close dropdowns
            'click': 'closeDropdowns',

            // Paste attachments
            'paste' : 'preSavePastedAttachments',

            // Save comment on keydown
            'keydown [contenteditable]' : 'saveOnKeydown',

            // Listening changes in contenteditable fields (due to input event not working with IE)
            'focus [contenteditable]' : 'saveEditableContent',
            'keyup [contenteditable]' : 'checkEditableContentForChange',
            'paste [contenteditable]' : 'checkEditableContentForChange',
            'input [contenteditable]' : 'checkEditableContentForChange',
            'blur [contenteditable]' : 'checkEditableContentForChange',

            // Navigation
            'click .navigation li[data-sort-key]' : 'navigationElementClicked',
            'click .navigation li.title' : 'toggleNavigationDropdown',

            // Main comenting field
            'click .commenting-field.main .textarea': 'showMainCommentingField',
            'click .commenting-field.main .close' : 'hideMainCommentingField',

            // All commenting fields
            'click .commenting-field .textarea' : 'increaseTextareaHeight',
            'change .commenting-field .textarea' : 'increaseTextareaHeight textareaContentChanged',
            'click .commenting-field:not(.main) .close' : 'removeCommentingField',

            // Edit mode actions
            'click .commenting-field .send.enabled' : 'postComment',
            'click .commenting-field .update.enabled' : 'putComment',
            'click .commenting-field .delete.enabled' : 'deleteComment',
            'click .commenting-field .attachments .attachment .delete' : 'preDeleteAttachment',
            'change .commenting-field .upload.enabled input[type="file"]' : 'fileInputChanged',

            // Other actions
            'click li.comment button.upvote' : 'upvoteComment',
            'click li.comment button.delete.enabled' : 'deleteComment',
            'click li.comment .hashtag' : 'hashtagClicked',
            'click li.comment .ping' : 'pingClicked',

            // Other
            'click li.comment ul.child-comments .toggle-all': 'toggleReplies',
            'click li.comment button.reply': 'replyButtonClicked',
            'click li.comment button.edit': 'editButtonClicked',

            // Drag & dropping attachments
            'dragenter' : 'showDroppableOverlay',

            'dragenter .droppable-overlay' : 'handleDragEnter',
            'dragleave .droppable-overlay' : 'handleDragLeaveForOverlay',
            'dragenter .droppable-overlay .droppable' : 'handleDragEnter',
            'dragleave .droppable-overlay .droppable' : 'handleDragLeaveForDroppable',

            'dragover .droppable-overlay' : 'handleDragOverForOverlay',
            'drop .droppable-overlay' : 'handleDrop',

            // Prevent propagating the click event into buttons under the autocomplete dropdown
            'click .dropdown.autocomplete': 'stopPropagation',
            'mousedown .dropdown.autocomplete': 'stopPropagation',
            'touchstart .dropdown.autocomplete': 'stopPropagation',
        },


        // Default options
        // ===============

        getDefaultOptions: function() {
            return {

                // User
                profilePictureURL: '',
                currentUserIsAdmin: false,
                currentUserId: null,

                // Font awesome icon overrides
                spinnerIconURL: '',
                upvoteIconURL: '',
                replyIconURL: '',
                uploadIconURL: '',
                attachmentIconURL: '',
                noCommentsIconURL: '',
                closeIconURL: '',

                // Strings to be formatted (for example localization)
                textareaPlaceholderText: '댓글추가',
                newestText: '최신순',
                oldestText: '등록순',
                popularText: '인기글',
                attachmentsText: '첨부',
                sendText: '댓글',
                replyText: '댓글',
                editText: '수정',
                editedText: '수정:',
                youText: '홍길동',
                saveText: '저장',
                deleteText: '삭제',
                newText: 'N',
                viewAllRepliesText: '댓글 __replyCount__개 모두보기',
                hideRepliesText: '댓글 접기',
                noCommentsText: '댓글 없음',
                noAttachmentsText: '첨부 없음',
                attachmentDropText: 'Drop files here',
                textFormatter: function(text) {return text},

                // Functionalities
                enableReplying: true,
                enableEditing: true,
                enableUpvoting: true,
                enableDeleting: true,
                enableAttachments: false,
                enableHashtags: false,
                enablePinging: false,
                enableDeletingCommentWithReplies: false,
                enableNavigation: true,
                postCommentOnEnter: false,
                forceResponsive: false,
                readOnly: false,
                defaultNavigationSortKey: 'newest',

                // Colors
                highlightColor: '#0049d3',
                deleteButtonColor: '#a8b1c7',

                scrollContainer: this.$el,
                roundProfilePictures: false,
                textareaRows: 2,
                textareaRowsOnFocus: 2,
                textareaMaxRows: 5,
                maxRepliesVisible: 5,

                fieldMappings: {
                    id: 'id',
                    parent: 'parent',
                    created: 'created',
                    modified: 'modified',
                    content: 'content',
                    attachments: 'attachments',
                    pings: 'pings',
                    creator: 'creator',
                    fullname: 'fullname',
                    profilePictureURL: 'profile_picture_url',
                    isNew: 'is_new',
                    createdByAdmin: 'created_by_admin',
                    createdByCurrentUser: 'created_by_current_user',
                    upvoteCount: 'upvote_count',
                    userHasUpvoted: 'user_has_upvoted'
                },

                searchUsers: function(term, success, error) {success([])},
                getComments: function(success, error) {success([])},
                postComment: function(commentJSON, success, error) {success(commentJSON)},
                putComment: function(commentJSON, success, error) {success(commentJSON)},
                deleteComment: function(commentJSON, success, error) {success()},
                upvoteComment: function(commentJSON, success, error) {success(commentJSON)},
                validateAttachments: function(attachments, callback) {return callback(attachments)},
                hashtagClicked: function(hashtag) {},
                pingClicked: function(userId) {},
                refresh: function() {},
                timeFormatter: function(time) {return new Date(time).toLocaleDateString()}
            }
        },


        // Initialization
        // ==============

        init: function(options, el) {
            this.$el = $(el);
            this.$el.addClass('jquery-comments');
            this.undelegateEvents();
            this.delegateEvents();

            // Detect mobile devices
            (function(a){(jQuery.browser=jQuery.browser||{}).mobile=/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))})(navigator.userAgent||navigator.vendor||window.opera);
            if($.browser.mobile) this.$el.addClass('mobile');

            // Init options
            this.options = $.extend(true, {}, this.getDefaultOptions(), options);;

            // Read-only mode
            if(this.options.readOnly) this.$el.addClass('read-only');

            // Set initial sort key
            this.currentSortKey = this.options.defaultNavigationSortKey;

            // Create CSS declarations for highlight color
            this.createCssDeclarations();

            // Fetching data and rendering
            this.fetchDataAndRender();
        },

        delegateEvents: function() {
            this.bindEvents(false);
        },

        undelegateEvents: function() {
            this.bindEvents(true);
        },

        bindEvents: function(unbind) {
            var bindFunction = unbind ? 'off' : 'on';
            for (var key in this.events) {
                var eventName = key.split(' ')[0];
                var selector = key.split(' ').slice(1).join(' ');
                var methodNames = this.events[key].split(' ');

                for(var index in methodNames) {
                    if(methodNames.hasOwnProperty(index)) {
                        var method = this[methodNames[index]];

                        // Keep the context
                        method = $.proxy(method, this);

                        if (selector == '') {
                            this.$el[bindFunction](eventName, method);
                        } else {
                            this.$el[bindFunction](eventName, selector, method);
                        }
                    }
                }
            }
        },


        // Basic functionalities
        // =====================

        fetchDataAndRender: function () {
            var self = this;

            this.commentsById = {};

            this.$el.empty();
            this.createHTML();

            // Comments
            // ========

            this.options.getComments(function(commentsArray) {

                // Convert comments to custom data model
                var commentModels = commentsArray.map(function(commentsJSON){
                    return self.createCommentModel(commentsJSON)
                });

                // Sort comments by date (oldest first so that they can be appended to the data model
                // without caring dependencies)
                self.sortComments(commentModels, 'oldest');

                $(commentModels).each(function(index, commentModel) {
                    self.addCommentToDataModel(commentModel);
                });

                // Mark data as fetched
                self.dataFetched = true;

                // Render
                self.render();
            });
        },

        fetchNext: function() {
            var self = this;

            // Loading indicator
            var spinner = this.createSpinner();
            this.$el.find('ul#comment-list').append(spinner);

            var success = function (commentModels) {
                $(commentModels).each(function(index, commentModel) {
                    self.createComment(commentModel);
                });
                spinner.remove();
            }

            var error = function() {
                spinner.remove();
            }

            this.options.getComments(success, error);
        },

        createCommentModel: function(commentJSON) {
            var commentModel = this.applyInternalMappings(commentJSON);
            commentModel.childs = [];
            commentModel.hasAttachments = function() {
                return commentModel.attachments.length > 0;
            }
            return commentModel;
        },

        addCommentToDataModel: function(commentModel) {
            if(!(commentModel.id in this.commentsById)) {
                this.commentsById[commentModel.id] = commentModel;

                // Update child array of the parent (append childs to the array of outer most parent)
                if(commentModel.parent) {
                    var outermostParent = this.getOutermostParent(commentModel.parent);
                    outermostParent.childs.push(commentModel.id);
                }
            }
        },

        updateCommentModel: function(commentModel) {
            $.extend(this.commentsById[commentModel.id], commentModel);
        },

        render: function() {
            var self = this;

            // Prevent re-rendering if data hasn't been fetched
            if(!this.dataFetched) return;

            // Show active container
            this.showActiveContainer();

            // Create comments and attachments
            this.createComments();
            if(this.options.enableAttachments && this.options.enableNavigation) this.createAttachments();

            // Remove spinner
            this.$el.find('> .spinner').remove();

            this.options.refresh();
        },

        showActiveContainer: function() {
            var activeNavigationEl = this.$el.find('.navigation li[data-container-name].active');
            var containerName = activeNavigationEl.data('container-name');
            var containerEl = this.$el.find('[data-container="' + containerName + '"]');
            containerEl.siblings('[data-container]').hide();
            containerEl.show();
        },

        createComments: function() {
            var self = this;

            // Create the list element before appending to DOM in order to reach better performance
            this.$el.find('#comment-list').remove();
            var commentList = $('<ul/>', {
                id: 'comment-list',
                'class': 'main'
            });

            // Divide commments into main level comments and replies
            var mainLevelComments = [];
            var replies = [];
            $(this.getComments()).each(function(index, commentModel) {
                if(commentModel.parent == null) {
                    mainLevelComments.push(commentModel);
                } else {
                    replies.push(commentModel);
                }
            });

            // Append main level comments
            this.sortComments(mainLevelComments, this.currentSortKey);
            $(mainLevelComments).each(function(index, commentModel) {
                self.addComment(commentModel, commentList);
            });

            // Append replies in chronological order
            this.sortComments(replies, 'oldest');
            $(replies).each(function(index, commentModel) {
                self.addComment(commentModel, commentList);
            });

            // Appned list to DOM
            this.$el.find('[data-container="comments"]').prepend(commentList);
        },

        createAttachments: function() {
            var self = this;

            // Create the list element before appending to DOM in order to reach better performance
            this.$el.find('#attachment-list').remove();
            var attachmentList = $('<ul/>', {
                id: 'attachment-list',
                'class': 'main'
            });

            var attachments = this.getAttachments();
            this.sortComments(attachments, 'newest');
            $(attachments).each(function(index, commentModel) {
                self.addAttachment(commentModel, attachmentList);
            });

            // Appned list to DOM
            this.$el.find('[data-container="attachments"]').prepend(attachmentList);
        },

        addComment: function(commentModel, commentList, prependComment) {
            commentList = commentList || this.$el.find('#comment-list');
            var commentEl = this.createCommentElement(commentModel);

            // Case: reply
            if(commentModel.parent) {
                var directParentEl = commentList.find('.comment[data-id="'+commentModel.parent+'"]');

                // Re-render action bar of direct parent element
                this.reRenderCommentActionBar(commentModel.parent);

                // Force replies into one level only
                var outerMostParent = directParentEl.parents('.comment').last();
                if(outerMostParent.length == 0) outerMostParent = directParentEl;

                // Append element to DOM
                var childCommentsEl = outerMostParent.find('.child-comments');
                var commentingField = childCommentsEl.find('.commenting-field');
                if(commentingField.length) {
                    commentingField.before(commentEl)
                } else {
                    childCommentsEl.append(commentEl);
                }

                // Update toggle all -button
                this.updateToggleAllButton(outerMostParent);

            // Case: main level comment
            } else {
                if(prependComment) {
                    commentList.prepend(commentEl);
                } else {
                    commentList.append(commentEl);
                }
            }
        },

        addAttachment: function(commentModel, commentList) {
            commentList = commentList || this.$el.find('#attachment-list');
            var commentEl = this.createCommentElement(commentModel);
            commentList.prepend(commentEl);
        },

        removeComment: function(commentId) {
            var self = this;
            var commentModel = this.commentsById[commentId];

            // Remove child comments recursively
            var childComments = this.getChildComments(commentModel.id);
            $(childComments).each(function(index, childComment) {
                self.removeComment(childComment.id);
            });

            // Update the child array of outermost parent
            if(commentModel.parent) {
                var outermostParent = this.getOutermostParent(commentModel.parent);
                var indexToRemove = outermostParent.childs.indexOf(commentModel.id);
                outermostParent.childs.splice(indexToRemove, 1);
            }

            // Remove the comment from data model
            delete this.commentsById[commentId];

            var commentElements = this.$el.find('li.comment[data-id="'+commentId+'"]');
            var parentEl = commentElements.parents('li.comment').last();

            // Remove the element
            commentElements.remove();

            // Update the toggle all button
            this.updateToggleAllButton(parentEl);
        },

        preDeleteAttachment: function(ev) {
            var commentingField = $(ev.currentTarget).parents('.commenting-field').first()
            var attachmentEl = $(ev.currentTarget).parents('.attachment').first();
            attachmentEl.remove();

            // Check if save button needs to be enabled
            this.toggleSaveButton(commentingField);
        },

        preSaveAttachments: function(files, commentingField) {
            var self = this;

            if(files.length) {

                // Elements
                if(!commentingField) commentingField = this.$el.find('.commenting-field.main');
                var uploadButton = commentingField.find('.control-row .upload');
                var isReply = !commentingField.hasClass('main');
                var attachmentsContainer = commentingField.find('.control-row .attachments');

                // Create attachment models
                var attachments = $(files).map(function(index, file){
                    return {
                        mime_type: file.type,
                        file: file
                    }
                });

                // Filter out already added attachments
                var existingAttachments = this.getAttachmentsFromCommentingField(commentingField);
                attachments = attachments.filter(function(index, attachment) {
                    var duplicate = false;

                    // Check if the attacment name and size matches with already added attachment
                    $(existingAttachments).each(function(index, existingAttachment) {
                        if(attachment.file.name == existingAttachment.file.name && attachment.file.size == existingAttachment.file.size) {
                            duplicate = true;
                        }
                    });

                    return !duplicate;
                });

                // Ensure that the main commenting field is shown if attachments were added to that
                if(commentingField.hasClass('main')) {
                    commentingField.find('.textarea').trigger('click');
                }

                // Set button state to loading
                this.setButtonState(uploadButton, false, true);

                // Validate attachments
                this.options.validateAttachments(attachments, function(validatedAttachments) {

                    if(validatedAttachments.length) {

                        // Create attachment tags
                        $(validatedAttachments).each(function(index, attachment) {
                            var attachmentTag = self.createAttachmentTagElement(attachment, true);
                            attachmentsContainer.append(attachmentTag);
                        });

                        // Check if save button needs to be enabled
                        self.toggleSaveButton(commentingField);
                    }

                    // Reset button state
                    self.setButtonState(uploadButton, true, false);
                });
            }

            // Clear the input field
            uploadButton.find('input').val('');
        },

        updateToggleAllButton: function(parentEl) {
            // Don't hide replies if maxRepliesVisible is null or undefined
            if (this.options.maxRepliesVisible == null) return;

            var childCommentsEl = parentEl.find('.child-comments');
            var childComments = childCommentsEl.find('.comment').not('.hidden');
            var toggleAllButton = childCommentsEl.find('li.toggle-all');
            childComments.removeClass('togglable-reply');

            // Select replies to be hidden
            if (this.options.maxRepliesVisible === 0) {
                var togglableReplies = childComments;
            } else {
                var togglableReplies = childComments.slice(0, -this.options.maxRepliesVisible);
            }

            // Add identifying class for hidden replies so they can be toggled
            togglableReplies.addClass('togglable-reply');

            // Show all replies if replies are expanded
            if(toggleAllButton.find('span.text').text() == this.options.textFormatter(this.options.hideRepliesText)) {
                togglableReplies.addClass('visible');
            }

            // Make sure that toggle all button is present
            if(childComments.length > this.options.maxRepliesVisible) {

                // Append button to toggle all replies if necessary
                if(!toggleAllButton.length) {

                    toggleAllButton = $('<li/>', {
                        'class': 'toggle-all highlight-font-bold'
                    });
                    var toggleAllButtonText = $('<span/>', {
                        'class': 'text'
                    });
                    var caret = $('<span/>', {
                        'class': 'caret'
                    });

                    // Append toggle button to DOM
                    toggleAllButton.append(toggleAllButtonText).append(caret);
                    childCommentsEl.prepend(toggleAllButton);
                }

                // Update the text of toggle all -button
                this.setToggleAllButtonText(toggleAllButton, false);

            // Make sure that toggle all button is not present
            } else {
                toggleAllButton.remove();
            }
        },

        updateToggleAllButtons: function() {
            var self = this;
            var commentList = this.$el.find('#comment-list');

            // Fold comments, find first level children and update toggle buttons
            commentList.find('.comment').removeClass('visible');
            commentList.children('.comment').each(function(index, el) {
                self.updateToggleAllButton($(el));
            });
        },

        sortComments: function (comments, sortKey) {
            var self = this;

            // Sort by popularity
            if(sortKey == 'popularity') {
                comments.sort(function(commentA, commentB) {
                    var pointsOfA = commentA.childs.length;
                    var pointsOfB = commentB.childs.length;

                    if(self.options.enableUpvoting) {
                        pointsOfA += commentA.upvoteCount;
                        pointsOfB += commentB.upvoteCount;
                    }

                    if(pointsOfB != pointsOfA) {
                        return pointsOfB - pointsOfA;

                    } else {
                        // Return newer if popularity is the same
                        var createdA = new Date(commentA.created).getTime();
                        var createdB = new Date(commentB.created).getTime();
                        return createdB - createdA;
                    }
                });

            // Sort by date
            } else {
                comments.sort(function(commentA, commentB) {
                    var createdA = new Date(commentA.created).getTime();
                    var createdB = new Date(commentB.created).getTime();
                    if(sortKey == 'oldest') {
                        return createdA - createdB;
                    } else {
                        return createdB - createdA;
                    }
                });
            }
        },

        sortAndReArrangeComments: function(sortKey) {
            var commentList = this.$el.find('#comment-list');

            // Get main level comments
            var mainLevelComments = this.getComments().filter(function(commentModel){return !commentModel.parent});
            this.sortComments(mainLevelComments, sortKey);

            // Rearrange the main level comments
            $(mainLevelComments).each(function(index, commentModel) {
                var commentEl = commentList.find('> li.comment[data-id='+commentModel.id+']');
                commentList.append(commentEl);
            });
        },

        showActiveSort: function() {
            var activeElements = this.$el.find('.navigation li[data-sort-key="' + this.currentSortKey + '"]');

            // Indicate active sort
            this.$el.find('.navigation li').removeClass('active');
            activeElements.addClass('active');

            // Update title for dropdown
            var titleEl = this.$el.find('.navigation .title');
            if(this.currentSortKey != 'attachments') {
                titleEl.addClass('active');
                titleEl.find('header').html(activeElements.first().html());
             } else {
                var defaultDropdownEl = this.$el.find('.navigation ul.dropdown').children().first();
                titleEl.find('header').html(defaultDropdownEl.html());
             }

            // Show active container
            this.showActiveContainer();
        },

        forceResponsive: function() {
            this.$el.addClass('responsive');
        },

        // Event handlers
        // ==============

        closeDropdowns: function() {
            this.$el.find('.dropdown').hide();
        },

        preSavePastedAttachments: function(ev) {
            var clipboardData = ev.originalEvent.clipboardData;
            var files = clipboardData.files;

            // Browsers only support pasting one file
            if(files && files.length == 1) {

                // Select correct commenting field
                var commentingField;
                var parentCommentingField = $(ev.target).parents('.commenting-field').first(); 
                if(parentCommentingField.length) {
                    commentingField = parentCommentingField;
                }

                this.preSaveAttachments(files, commentingField);
                ev.preventDefault();
            }
        },

        saveOnKeydown: function(ev) {
            // Save comment on cmd/ctrl + enter
            if(ev.keyCode == 13) {
                var metaKey = ev.metaKey || ev.ctrlKey;
                if(this.options.postCommentOnEnter || metaKey) {
                    var el = $(ev.currentTarget);
                    el.siblings('.control-row').find('.save').trigger('click');
                    ev.stopPropagation();
                    ev.preventDefault();
                }
            }
        },

        saveEditableContent: function(ev) {
            var el = $(ev.currentTarget);
            el.data('before', el.html());
        },

        checkEditableContentForChange: function(ev) {
            var el = $(ev.currentTarget);

            // Fix jquery-textcomplete on IE, empty text nodes will break up the autocomplete feature
            $(el[0].childNodes).each(function() {
                if(this.nodeType == Node.TEXT_NODE && this.length == 0 && this.removeNode) this.removeNode();
            });

            if (el.data('before') != el.html()) {
                el.data('before', el.html());
                el.trigger('change');
            }
        },

        navigationElementClicked: function(ev) {
            var navigationEl = $(ev.currentTarget);
            var sortKey = navigationEl.data().sortKey;

            // Sort the comments if necessary
            if(sortKey == 'attachments') {
                this.createAttachments();
            } else {
                this.sortAndReArrangeComments(sortKey);
            }

            // Save the current sort key
            this.currentSortKey = sortKey;
            this.showActiveSort();
        },

        toggleNavigationDropdown: function(ev) {
            // Prevent closing immediately
            ev.stopPropagation();

            var dropdown = $(ev.currentTarget).find('~ .dropdown');
            dropdown.toggle();
        },

        showMainCommentingField: function(ev) {
            var mainTextarea = $(ev.currentTarget);
            mainTextarea.siblings('.control-row').show();
            mainTextarea.parent().find('.close').show();
            mainTextarea.parent().find('.upload.inline-button').hide();
            mainTextarea.focus();
        },

        hideMainCommentingField: function(ev) {
            var closeButton = $(ev.currentTarget);
            var commentingField = this.$el.find('.commenting-field.main');
            var mainTextarea = commentingField.find('.textarea');
            var mainControlRow = commentingField.find('.control-row');

            // Clear text area
            this.clearTextarea(mainTextarea);

            // Clear attachments
            commentingField.find('.attachments').empty();

            // Toggle save button
            this.toggleSaveButton(commentingField);

            // Adjust height
            this.adjustTextareaHeight(mainTextarea, false);

            mainControlRow.hide();
            closeButton.hide();
            mainTextarea.parent().find('.upload.inline-button').show();
            mainTextarea.blur();
        },

        increaseTextareaHeight: function(ev) {
            var textarea = $(ev.currentTarget);
            this.adjustTextareaHeight(textarea, true);
        },

        textareaContentChanged: function(ev) {
            var textarea = $(ev.currentTarget);

            // Update parent id if reply-to tag was removed
            if(!textarea.find('.reply-to.tag').length) {
                var commentId = textarea.attr('data-comment');

                // Case: editing comment
                if(commentId) {
                    var parentComments = textarea.parents('li.comment');
                    if(parentComments.length > 1) {
                        var parentId = parentComments.last().data('id');
                        textarea.attr('data-parent', parentId);
                    }

                // Case: new comment
                } else {
                    var parentId = textarea.parents('li.comment').last().data('id');
                    textarea.attr('data-parent', parentId);
                }
            }

            // Move close button if scrollbar is visible
            var commentingField = textarea.parents('.commenting-field').first();
            if(textarea[0].scrollHeight > textarea.outerHeight()) {
                commentingField.addClass('commenting-field-scrollable');
            } else {
                commentingField.removeClass('commenting-field-scrollable');
            }

            // Check if save button needs to be enabled
            this.toggleSaveButton(commentingField);
        },

        toggleSaveButton: function(commentingField) {
            var textarea = commentingField.find('.textarea');
            var saveButton = textarea.siblings('.control-row').find('.save');

            var content = this.getTextareaContent(textarea, true);
            var attachments = this.getAttachmentsFromCommentingField(commentingField);
            var enabled;

            // Case: existing comment
            if(commentModel = this.commentsById[textarea.attr('data-comment')]) {

                // Case: parent changed
                var contentChanged = content != commentModel.content;
                var parentFromModel;
                if(commentModel.parent) {
                    parentFromModel = commentModel.parent.toString();
                }

                // Case: parent changed
                var parentChanged = textarea.attr('data-parent') != parentFromModel;

                // Case: attachments changed
                var attachmentsChanged = false;
                if(this.options.enableAttachments) {
                    var savedAttachmentIds = commentModel.attachments.map(function(attachment){return attachment.id});
                    var currentAttachmentIds = attachments.map(function(attachment){return attachment.id});
                    attachmentsChanged = !this.areArraysEqual(savedAttachmentIds, currentAttachmentIds);
                }

                enabled = contentChanged || parentChanged || attachmentsChanged;

            // Case: new comment
            } else {
                enabled = Boolean(content.length) || Boolean(attachments.length);
            }

            saveButton.toggleClass('enabled', enabled);
        },

        removeCommentingField: function(ev) {
            var closeButton = $(ev.currentTarget);

            // Remove edit class from comment if user was editing the comment
            var textarea = closeButton.siblings('.textarea');
            if(textarea.attr('data-comment')) {
                closeButton.parents('li.comment').first().removeClass('edit');
            }

            // Remove the field
            var commentingField = closeButton.parents('.commenting-field').first();
            commentingField.remove();
        },

        postComment: function(ev) {
            var self = this;
            var sendButton = $(ev.currentTarget);
            var commentingField = sendButton.parents('.commenting-field').first();

            // Set button state to loading
            this.setButtonState(sendButton, false, true);

            // Create comment JSON
            var commentJSON = this.createCommentJSON(commentingField);

            // Reverse mapping
            commentJSON = this.applyExternalMappings(commentJSON);

            var success = function(commentJSON) {
                self.createComment(commentJSON);
                commentingField.find('.close').trigger('click');

                // Reset button state
                self.setButtonState(sendButton, false, false);
            };

            var error = function() {

                // Reset button state
                self.setButtonState(sendButton, true, false);
            };

            this.options.postComment(commentJSON, success, error);
        },

        createComment: function(commentJSON) {
            var commentModel = this.createCommentModel(commentJSON);
            this.addCommentToDataModel(commentModel);

            // Add comment element
            var commentList = this.$el.find('#comment-list');
            var prependComment = this.currentSortKey == 'newest';
            this.addComment(commentModel, commentList, prependComment);

            if(this.currentSortKey == 'attachments' && commentModel.hasAttachments()) {
                this.addAttachment(commentModel);
            }
        },

        putComment: function(ev) {
            var self = this;
            var saveButton = $(ev.currentTarget);
            var commentingField = saveButton.parents('.commenting-field').first();
            var textarea = commentingField.find('.textarea');

            // Set button state to loading
            this.setButtonState(saveButton, false, true);

            // Use a clone of the existing model and update the model after succesfull update
            var commentJSON =  $.extend({}, this.commentsById[textarea.attr('data-comment')]);
            $.extend(commentJSON, {
                parent: textarea.attr('data-parent') || null,
                content: this.getTextareaContent(textarea),
                pings: this.getPings(textarea),
                modified: new Date().getTime(),
                attachments: this.getAttachmentsFromCommentingField(commentingField)
            });

            // Reverse mapping
            commentJSON = this.applyExternalMappings(commentJSON);

            var success = function(commentJSON) {
                // The outermost parent can not be changed by editing the comment so the childs array
                // of parent does not require an update

                var commentModel = self.createCommentModel(commentJSON);

                // Delete childs array from new comment model since it doesn't need an update
                delete commentModel['childs'];
                self.updateCommentModel(commentModel);

                // Close the editing field
                commentingField.find('.close').trigger('click');

                // Re-render the comment
                self.reRenderComment(commentModel.id);

                // Reset button state
                self.setButtonState(saveButton, false, false);
            };

            var error = function() {

                // Reset button state
                self.setButtonState(saveButton, true, false);
            };

            this.options.putComment(commentJSON, success, error);
        },

        deleteComment: function(ev) {
            var self = this;
            var deleteButton = $(ev.currentTarget);
            var commentEl = deleteButton.parents('.comment').first();
            var commentJSON =  $.extend({}, this.commentsById[commentEl.attr('data-id')]);
            var commentId = commentJSON.id;
            var parentId = commentJSON.parent;

            // Set button state to loading
            this.setButtonState(deleteButton, false, true);

            // Reverse mapping
            commentJSON = this.applyExternalMappings(commentJSON);

            var success = function() {
                self.removeComment(commentId);
                if(parentId) self.reRenderCommentActionBar(parentId);

                // Reset button state
                self.setButtonState(deleteButton, false, false);
            };

            var error = function() {

                // Reset button state
                self.setButtonState(deleteButton, true, false);
            };

            this.options.deleteComment(commentJSON, success, error);
        },

        hashtagClicked: function(ev) {
            var el = $(ev.currentTarget);
            var value = el.attr('data-value');
            this.options.hashtagClicked(value);
        },

        pingClicked: function(ev) {
            var el = $(ev.currentTarget);
            var value = el.attr('data-value');
            this.options.pingClicked(value);
        },

        fileInputChanged: function(ev, files) {
            var files = ev.currentTarget.files;
            var commentingField = $(ev.currentTarget).parents('.commenting-field').first();
            this.preSaveAttachments(files, commentingField);
        },

        upvoteComment: function(ev) {
            var self = this;
            var commentEl = $(ev.currentTarget).parents('li.comment').first();
            var commentModel = commentEl.data().model;

            // Check whether user upvoted the comment or revoked the upvote
            var previousUpvoteCount = commentModel.upvoteCount;
            var newUpvoteCount;
            if(commentModel.userHasUpvoted) {
                newUpvoteCount = previousUpvoteCount - 1;
            } else {
                newUpvoteCount = previousUpvoteCount + 1;
            }

            // Show changes immediately
            commentModel.userHasUpvoted = !commentModel.userHasUpvoted;
            commentModel.upvoteCount = newUpvoteCount;
            this.reRenderUpvotes(commentModel.id);

            // Reverse mapping
            var commentJSON = $.extend({}, commentModel);
            commentJSON = this.applyExternalMappings(commentJSON);

            var success = function(commentJSON) {
                var commentModel = self.createCommentModel(commentJSON);
                self.updateCommentModel(commentModel);
                self.reRenderUpvotes(commentModel.id);
            };

            var error = function() {

                // Revert changes
                commentModel.userHasUpvoted = !commentModel.userHasUpvoted;
                commentModel.upvoteCount = previousUpvoteCount;
                self.reRenderUpvotes(commentModel.id);
            };

            this.options.upvoteComment(commentJSON, success, error);
        },

        toggleReplies: function(ev) {
            var el = $(ev.currentTarget);
            el.siblings('.togglable-reply').toggleClass('visible');
            this.setToggleAllButtonText(el, true);
        },

        replyButtonClicked: function(ev) {
            var replyButton = $(ev.currentTarget);
            var outermostParent = replyButton.parents('li.comment').last();
            var parentId = replyButton.parents('.comment').first().data().id;


            // Remove existing field
            var replyField = outermostParent.find('.child-comments > .commenting-field');
            if(replyField.length) replyField.remove();
            var previousParentId = replyField.find('.textarea').attr('data-parent');

            // Create the reply field (do not re-create)
            if(previousParentId != parentId) {
                replyField = this.createCommentingFieldElement(parentId);
                outermostParent.find('.child-comments').append(replyField);

                // Move cursor to end
                var textarea = replyField.find('.textarea');
                this.moveCursorToEnd(textarea);

                // Ensure element stays visible
                this.ensureElementStaysVisible(replyField);
            }
        },

        editButtonClicked: function(ev) {
            var editButton = $(ev.currentTarget);
            var commentEl = editButton.parents('li.comment').first();
            var commentModel = commentEl.data().model;
            commentEl.addClass('edit');

            // Create the editing field
            var editField = this.createCommentingFieldElement(commentModel.parent, commentModel.id);
            commentEl.find('.comment-wrapper').first().append(editField);

            // Append original content
            var textarea = editField.find('.textarea');
            textarea.attr('data-comment', commentModel.id);

            // Escaping HTML
            textarea.append(this.getFormattedCommentContent(commentModel, true));

            // Move cursor to end
            this.moveCursorToEnd(textarea);

            // Ensure element stays visible
            this.ensureElementStaysVisible(editField);
        },

        showDroppableOverlay: function(ev) {
            if(this.options.enableAttachments) {
                this.$el.find('.droppable-overlay').css('top', this.$el[0].scrollTop);
                this.$el.find('.droppable-overlay').show();
                this.$el.addClass('drag-ongoing');
            }
        },

        handleDragEnter: function(ev) {
            var count = $(ev.currentTarget).data('dnd-count') || 0;
            count++;
            $(ev.currentTarget).data('dnd-count', count);
            $(ev.currentTarget).addClass('drag-over');
        },

        handleDragLeave: function(ev, callback) {
            var count = $(ev.currentTarget).data('dnd-count');
            count--;
            $(ev.currentTarget).data('dnd-count', count);

            if(count == 0) {
                $(ev.currentTarget).removeClass('drag-over');
                if(callback) callback();
            }
        },

        handleDragLeaveForOverlay: function(ev) {
            var self = this;
            this.handleDragLeave(ev, function() {
                self.hideDroppableOverlay();
            });
        },

        handleDragLeaveForDroppable: function(ev) {
            this.handleDragLeave(ev);
        },

        handleDragOverForOverlay: function(ev) {
            ev.stopPropagation();
            ev.preventDefault();
            ev.originalEvent.dataTransfer.dropEffect = 'copy';
        },

        hideDroppableOverlay: function() {
            this.$el.find('.droppable-overlay').hide();
            this.$el.removeClass('drag-ongoing');
        },

        handleDrop: function(ev) {
            ev.preventDefault();

            // Reset DND counts
            $(ev.target).trigger('dragleave');

            // Hide the overlay and upload the files
            this.hideDroppableOverlay();
            this.preSaveAttachments(ev.originalEvent.dataTransfer.files);
        },

        stopPropagation: function(ev) {
            ev.stopPropagation();
        },


        // HTML elements
        // =============

        createHTML: function() {
            var self = this;

            // Commenting field
            var mainCommentingField = this.createMainCommentingFieldElement();
            this.$el.append(mainCommentingField);

            // Hide control row and close button
            var mainControlRow = mainCommentingField.find('.control-row');
            mainControlRow.hide();
            mainCommentingField.find('.close').hide();

            // Navigation bar
            if (this.options.enableNavigation) {
                this.$el.append(this.createNavigationElement());
                this.showActiveSort();
            }

            // Loading spinner
            var spinner = this.createSpinner();
            this.$el.append(spinner);

            // Comments container
            var commentsContainer = $('<div/>', {
                'class': 'data-container',
                'data-container': 'comments'
            });
            this.$el.append(commentsContainer);

            // "No comments" placeholder
            var noComments = $('<div/>', {
                'class': 'no-comments no-data',
                text: this.options.textFormatter(this.options.noCommentsText)
            });
            var noCommentsIcon = $('<i/>', {
                'class': 'fa fa-comments fa-2x'
            });
            if(this.options.noCommentsIconURL.length) {
                noCommentsIcon.css('background-image', 'url("'+this.options.noCommentsIconURL+'")');
                noCommentsIcon.addClass('image');
            }
            noComments.prepend($('<br/>')).prepend(noCommentsIcon);
            commentsContainer.append(noComments);

            // Attachments
            if(this.options.enableAttachments) {

                // Attachments container
                var attachmentsContainer = $('<div/>', {
                    'class': 'data-container',
                    'data-container': 'attachments'
                });
                this.$el.append(attachmentsContainer);

                // "No attachments" placeholder
                var noAttachments = $('<div/>', {
                    'class': 'no-attachments no-data',
                    text: this.options.textFormatter(this.options.noAttachmentsText)
                });
                var noAttachmentsIcon = $('<i/>', {
                    'class': 'fa fa-paperclip fa-2x'
                });
                if(this.options.attachmentIconURL.length) {
                    noAttachmentsIcon.css('background-image', 'url("'+this.options.attachmentIconURL+'")');
                    noAttachmentsIcon.addClass('image');
                }
                noAttachments.prepend($('<br/>')).prepend(noAttachmentsIcon);
                attachmentsContainer.append(noAttachments);


                // Drag & dropping attachments
                var droppableOverlay = $('<div/>', {
                    'class': 'droppable-overlay'
                });

                var droppableContainer = $('<div/>', {
                    'class': 'droppable-container'
                });

                var droppable = $('<div/>', {
                    'class': 'droppable'
                });

                var uploadIcon = $('<i/>', {
                    'class': 'fa fa-paperclip fa-4x'
                });
                if(this.options.uploadIconURL.length) {
                    uploadIcon.css('background-image', 'url("'+this.options.uploadIconURL+'")');
                    uploadIcon.addClass('image');
                }

                var dropAttachmentText = $('<div/>', {
                    text: this.options.textFormatter(this.options.attachmentDropText)
                });
                droppable.append(uploadIcon);
                droppable.append(dropAttachmentText);

                droppableOverlay.html(droppableContainer.html(droppable)).hide();
                this.$el.append(droppableOverlay);
            }
        },

        createProfilePictureElement: function(src, userId) {
            if(src) {
              var profilePicture = $('<div/>').css({
                  'background-image': 'url(' + src + ')'
                });
            } else {
                var profilePicture = $('<i/>', {
                    'class': 'fa fa-user'
                });
            }
            profilePicture.addClass('profile-picture');
            profilePicture.attr('data-user-id', userId);
            if(this.options.roundProfilePictures) profilePicture.addClass('round');
            return profilePicture;
        },

        createMainCommentingFieldElement: function() {
            return this.createCommentingFieldElement(undefined, undefined, true);
        },

        createCommentingFieldElement: function(parentId, existingCommentId, isMain) {
            var self = this;

            var profilePictureURL;
            var userId;
            var attachments;

            // Commenting field
            var commentingField = $('<div/>', {
                'class': 'commenting-field'
            });
            if(isMain) commentingField.addClass('main');

            // Comment was modified, use existing data
            if(existingCommentId) {
                profilePictureURL = this.commentsById[existingCommentId].profilePictureURL;
                userId = this.commentsById[existingCommentId].creator;
                attachments = this.commentsById[existingCommentId].attachments;

            // New comment was created
            } else {
                profilePictureURL = this.options.profilePictureURL;
                userId = this.options.creator;
                attachments = [];
            }

            var profilePicture = this.createProfilePictureElement(profilePictureURL, userId);

            // New comment
            var textareaWrapper = $('<div/>', {
                'class': 'textarea-wrapper'
            });

            // Control row
            var controlRow = $('<div/>', {
                'class': 'control-row'
            });

            // Textarea
            var textarea = $('<div/>', {
                'class': 'textarea',
                'data-placeholder': this.options.textFormatter(this.options.textareaPlaceholderText),
                contenteditable: true
            });

            // Setting the initial height for the textarea
            this.adjustTextareaHeight(textarea, false);

            // Close button
            var closeButton = this.createCloseButton();
            closeButton.addClass('inline-button');

            // Save button
            var saveButtonClass = existingCommentId ? 'update' : 'send';
            var saveButtonText = existingCommentId ? this.options.textFormatter(this.options.saveText) : this.options.textFormatter(this.options.sendText);
            var saveButton = $('<span/>', {
                'class': saveButtonClass + ' save highlight-background',
                'text': saveButtonText
            });
            saveButton.data('original-content', saveButtonText);
            controlRow.append(saveButton);

            // Delete button
            if(existingCommentId && this.isAllowedToDelete(existingCommentId)) {

                // Delete button
                var deleteButtonText = this.options.textFormatter(this.options.deleteText);
                var deleteButton = $('<span/>', {
                    'class': 'delete enabled',
                    text: deleteButtonText
                }).css('background-color', this.options.deleteButtonColor);
                deleteButton.data('original-content', deleteButtonText);
                controlRow.append(deleteButton);
            }

            if(this.options.enableAttachments) {

                // Upload buttons
                // ==============

                var uploadButton = $('<span/>', {
                    'class': 'enabled upload'
                });
                var uploadIcon = $('<i/>', {
                    'class': 'fa fa-paperclip'
                });
                var fileInput = $('<input/>', {
                    'type': 'file',
                    'multiple': 'multiple',
                    'data-role': 'none' // Prevent jquery-mobile for adding classes
                });

                if(this.options.uploadIconURL.length) {
                    uploadIcon.css('background-image', 'url("'+this.options.uploadIconURL+'")');
                    uploadIcon.addClass('image');
                }
                uploadButton.append(uploadIcon).append(fileInput);

                // Main upload button
                var mainUploadButton = uploadButton.clone();
                mainUploadButton.data('original-content', mainUploadButton.children());
                controlRow.append(mainUploadButton);

                // Inline upload button for main commenting field
                if(isMain) {
                    textareaWrapper.append(uploadButton.clone().addClass('inline-button'));
                }

                // Attachments container
                // =====================

                var attachmentsContainer = $('<div/>', {
                    'class': 'attachments',
                });
                $(attachments).each(function(index, attachment) {
                    var attachmentTag = self.createAttachmentTagElement(attachment, true);
                    attachmentsContainer.append(attachmentTag);
                });
                controlRow.append(attachmentsContainer);
            }


            // Populate the element
            textareaWrapper.append(closeButton).append(textarea).append(controlRow);
            commentingField.append(profilePicture).append(textareaWrapper);


            if(parentId) {

                // Set the parent id to the field if necessary
                textarea.attr('data-parent', parentId);

                // Append reply-to tag if necessary
                var parentModel = this.commentsById[parentId];
                if(parentModel.parent) {
                    textarea.html('&nbsp;');    // Needed to set the cursor to correct place

                    // Creating the reply-to tag
                    var replyToName = '@' + parentModel.fullname;
                    var replyToTag = this.createTagElement(replyToName, 'reply-to', parentModel.creator, {
                        'data-user-id': parentModel.creator
                    });
                    textarea.prepend(replyToTag);
                }
            }

            // Pinging users
            if(this.options.enablePinging) {
                textarea.textcomplete([{
                    match: /(^|\s)@([^@]*)$/i,
                    index: 2,
                    search: function (term, callback) {
                        term = self.normalizeSpaces(term);

                        // Return empty array on error
                        var error = function() {
                            callback([]);
                        }

                        self.options.searchUsers(term, callback, error);
                    },
                    template: function(user) {
                        var wrapper = $('<div/>');

                        var profilePictureEl = self.createProfilePictureElement(user.profile_picture_url);

                        var detailsEl = $('<div/>', {
                            'class': 'details',
                        });
                        var nameEl = $('<div/>', {
                            'class': 'name',
                        }).html(user.fullname);

                        var emailEl = $('<div/>', {
                            'class': 'email',
                        }).html(user.email);

                        if (user.email) {
                            detailsEl.append(nameEl).append(emailEl);
                        } else {
                            detailsEl.addClass('no-email')
                            detailsEl.append(nameEl)
                        }

                        wrapper.append(profilePictureEl).append(detailsEl);
                        return wrapper.html();
                    },
                    replace: function (user) {
                        var tag = self.createTagElement('@' + user.fullname, 'ping', user.id, {
                            'data-user-id': user.id
                        });
                        return ' ' + tag[0].outerHTML + ' ';
                    },
                }], {
                    appendTo: '.jquery-comments',
                    dropdownClassName: 'dropdown autocomplete',
                    maxCount: 5,
                    rightEdgeOffset: 0,
                    debounce: 250
                });


                // OVERIDE TEXTCOMPLETE DROPDOWN POSITIONING

                $.fn.textcomplete.Dropdown.prototype.render = function(zippedData) {
                    var contentsHtml = this._buildContents(zippedData);
                    var unzippedData = $.map(zippedData, function (d) { return d.value; });
                    if (zippedData.length) {
                      var strategy = zippedData[0].strategy;
                      if (strategy.id) {
                        this.$el.attr('data-strategy', strategy.id);
                      } else {
                        this.$el.removeAttr('data-strategy');
                      }
                      this._renderHeader(unzippedData);
                      this._renderFooter(unzippedData);
                      if (contentsHtml) {
                        this._renderContents(contentsHtml);
                        this._fitToBottom();
                        this._fitToRight();
                        this._activateIndexedItem();
                      }
                      this._setScroll();
                    } else if (this.noResultsMessage) {
                      this._renderNoResultsMessage(unzippedData);
                    } else if (this.shown) {
                      this.deactivate();
                    }

                    // CUSTOM CODE
                    // ===========

                    // Adjust vertical position
                    var top = parseInt(this.$el.css('top')) + self.options.scrollContainer.scrollTop();
                    this.$el.css('top', top);

                    // Adjust horizontal position
                    var originalLeft = this.$el.css('left');
                    this.$el.css('left', 0);    // Left must be set to 0 in order to get the real width of the el
                    var maxLeft = self.$el.width() - this.$el.outerWidth();
                    var left = Math.min(maxLeft, parseInt(originalLeft));
                    this.$el.css('left', left);

                    // ===========
                }


                // OVERIDE TEXTCOMPLETE CONTENTEDITABLE SKIPSEARCH FUNCTION WHEN USING ALT + backspace

                $.fn.textcomplete.ContentEditable.prototype._skipSearch = function(clickEvent) {
                    switch (clickEvent.keyCode) {
                        case 9:  // TAB
                        case 13: // ENTER
                        case 16: // SHIFT
                        case 17: // CTRL
                        //case 18: // ALT
                        case 33: // PAGEUP
                        case 34: // PAGEDOWN
                        case 40: // DOWN
                        case 38: // UP
                        case 27: // ESC
                            return true;
                    }
                    if (clickEvent.ctrlKey) switch (clickEvent.keyCode) {
                        case 78: // Ctrl-N
                        case 80: // Ctrl-P
                            return true;
                    }
                }
            }

            return commentingField;
        },

        createNavigationElement: function() {
            var navigationEl = $('<ul/>', {
                'class': 'navigation'
            });
            var navigationWrapper = $('<div/>', {
                'class': 'navigation-wrapper'
            });
            navigationEl.append(navigationWrapper);

            // Newest
            var newest = $('<li/>', {
                text: this.options.textFormatter(this.options.newestText),
                'data-sort-key': 'newest',
                'data-container-name': 'comments'
            });

            // Oldest
            var oldest = $('<li/>', {
                text: this.options.textFormatter(this.options.oldestText),
                'data-sort-key': 'oldest',
                'data-container-name': 'comments'
            });

            // Popular
            var popular = $('<li/>', {
                text: this.options.textFormatter(this.options.popularText),
                'data-sort-key': 'popularity',
                'data-container-name': 'comments'
            });

            // Attachments
            var attachments = $('<li/>', {
                text: this.options.textFormatter(this.options.attachmentsText),
                'data-sort-key': 'attachments',
                'data-container-name': 'attachments'
            });

            // Attachments icon
            var attachmentsIcon = $('<i/>', {
                'class': 'fa fa-paperclip'
            });
            if(this.options.attachmentIconURL.length) {
                attachmentsIcon.css('background-image', 'url("'+this.options.attachmentIconURL+'")');
                attachmentsIcon.addClass('image');
            }
            attachments.prepend(attachmentsIcon);


            // Responsive navigation
            var dropdownNavigationWrapper = $('<div/>', {
                'class': 'navigation-wrapper responsive'
            });
            var dropdownNavigation = $('<ul/>', {
                'class': 'dropdown'
            });
            var dropdownTitle = $('<li/>', {
                'class': 'title'
            });
            var dropdownTitleHeader = $('<header/>');

            dropdownTitle.append(dropdownTitleHeader);
            dropdownNavigationWrapper.append(dropdownTitle);
            dropdownNavigationWrapper.append(dropdownNavigation);
            navigationEl.append(dropdownNavigationWrapper);


            // Populate elements
            navigationWrapper.append(newest).append(oldest);
            dropdownNavigation.append(newest.clone()).append(oldest.clone());

            if(this.options.enableReplying || this.options.enableUpvoting) {
                navigationWrapper.append(popular);
                dropdownNavigation.append(popular.clone());
            }
            if(this.options.enableAttachments) {
                navigationWrapper.append(attachments);
                dropdownNavigationWrapper.append(attachments.clone());
            }

            if(this.options.forceResponsive) this.forceResponsive();
            return navigationEl;
        },

        createSpinner: function(inline) {
            var spinner = $('<div/>', {
                'class': 'spinner'
            });
            if(inline) spinner.addClass('inline');

            var spinnerIcon = $('<i/>', {
                'class': 'fa fa-spinner fa-spin'
            });
            if(this.options.spinnerIconURL.length) {
                spinnerIcon.css('background-image', 'url("'+this.options.spinnerIconURL+'")');
                spinnerIcon.addClass('image');
            }
            spinner.html(spinnerIcon);
            return spinner;
        },

        createCloseButton: function(className) {
            var closeButton = $('<span/>', {
                'class': className || 'close'
            });

            var icon = $('<i/>', {
                'class': 'bi bi-x'
            });
            if(this.options.closeIconURL.length) {
                icon.css('background-image', 'url("'+this.options.closeIconURL+'")');
                icon.addClass('image');
            }

            closeButton.html(icon);

            return closeButton;
        },

        createCommentElement: function(commentModel) {

            // Comment container element
            var commentEl = $('<li/>', {
                'data-id': commentModel.id,
                'class': 'comment'
            }).data('model', commentModel);

            if(commentModel.createdByCurrentUser) commentEl.addClass('by-current-user');
            if(commentModel.createdByAdmin) commentEl.addClass('by-admin');

            // Child comments
            var childComments = $('<ul/>', {
                'class': 'child-comments'
            });

            // Comment wrapper
            var commentWrapper = this.createCommentWrapperElement(commentModel);

            commentEl.append(commentWrapper);
            if(commentModel.parent == null) commentEl.append(childComments);
            return commentEl;
        },

        createCommentWrapperElement: function(commentModel) {
            var self = this;

            var commentWrapper = $('<div/>', {
                'class': 'comment-wrapper'
            });

            // Profile picture
            var profilePicture = this.createProfilePictureElement(commentModel.profilePictureURL, commentModel.creator);

            // Time
            var time = $('<time/>', {
                text: this.options.timeFormatter(commentModel.created),
                'data-original': commentModel.created
            });

            // Comment header element
            var commentHeaderEl = $('<div/>', {
                'class': 'comment-header',
            });

            // Name element
            var nameEl = $('<span/>', {
                'class': 'name',
                'data-user-id': commentModel.creator,
                'text': commentModel.createdByCurrentUser ? this.options.textFormatter(this.options.youText) : commentModel.fullname
            });
            commentHeaderEl.append(nameEl);


            // Highlight admin names
            if(commentModel.createdByAdmin) nameEl.addClass('highlight-font-bold');

            // Show reply-to name if parent of parent exists
            if(commentModel.parent) {
                var parent = this.commentsById[commentModel.parent];
                if(parent.parent) {
                    var replyTo = $('<span/>', {
                        'class': 'reply-to',
                        'text': parent.fullname,
                        'data-user-id': parent.creator
                    });

                    // reply icon
                    var replyIcon = $('<i/>', {
                        'class': 'fa fa-share'
                    });
                    if(this.options.replyIconURL.length) {
                        replyIcon.css('background-image', 'url("'+this.options.replyIconURL+'")');
                        replyIcon.addClass('image');
                    }

                    replyTo.prepend(replyIcon);
                    commentHeaderEl.append(replyTo);
                }
            }

            // New tag
            if(commentModel.isNew) {
                var newTag = $('<span/>', {
                    'class': 'new highlight-background',
                    text: this.options.textFormatter(this.options.newText)
                });
                commentHeaderEl.append(newTag);
            }

            // Wrapper
            var wrapper = $('<div/>', {
                'class': 'wrapper'
            });

            // Content
            // =======

            var content = $('<div/>', {
                'class': 'content'
            });
            content.html(this.getFormattedCommentContent(commentModel));

            // Edited timestamp
            if(commentModel.modified && commentModel.modified != commentModel.created) {
                var editedTime = this.options.timeFormatter(commentModel.modified);
                var edited = $('<time/>', {
                    'class': 'edited',
                    text: this.options.textFormatter(this.options.editedText) + ' ' + editedTime,
                    'data-original': commentModel.modified
                });
                content.append(edited);
            }


            // Attachments
            // ===========

            var attachments = $('<div/>', {
                'class': 'attachments'
            });
            var attachmentPreviews = $('<div/>', {
                'class': 'previews'
            });
            var attachmentTags = $('<div/>', {
                'class': 'tags'
            });
            attachments.append(attachmentPreviews).append(attachmentTags);

            if(this.options.enableAttachments && commentModel.hasAttachments()) {
                $(commentModel.attachments).each(function(index, attachment) {
                    var format = undefined;
                    var type = undefined;

                    // Type and format
                    if(attachment.mime_type) {
                        var mimeTypeParts = attachment.mime_type.split('/');
                        if(mimeTypeParts.length == 2) {
                            format = mimeTypeParts[1];
                            type = mimeTypeParts[0];
                        }
                    }

                    // Preview
                    if(type == 'image' || type == 'video') {
                        var previewRow = $('<div/>');

                        // Preview element
                        var preview = $('<a/>', {
                            'class': 'preview',
                            href: attachment.file,
                            target: '_blank'
                        });
                        previewRow.html(preview);

                        // Case: image preview
                        if(type == 'image') {
                            var image = $('<img/>', {
                                src: attachment.file
                            });
                            preview.html(image);

                        // Case: video preview
                        } else {
                            var video = $('<video/>', {
                                src: attachment.file,
                                type: attachment.mime_type,
                                controls: 'controls'
                            });
                            preview.html(video);
                        }
                        attachmentPreviews.append(previewRow);
                    }

                    // Tag element
                    var attachmentTag = self.createAttachmentTagElement(attachment, false);
                    attachmentTags.append(attachmentTag);
                });
            }


            // Actions
            // =======

            var actions = $('<span/>', {
                'class': 'actions'
            });

            // Separator
            var separator = $('<span/>', {
                'class': 'separator',
                text: '·'
            });

            // Reply
            var reply = $('<button/>', {
                'class': 'action reply',
                'type': 'button',
                text: this.options.textFormatter(this.options.replyText)
            });

            // Upvote icon
            var upvoteIcon = $('<i/>', {
                'class': 'bi bi-heart-fill'
            });
            if(this.options.upvoteIconURL.length) {
                upvoteIcon.css('background-image', 'url("'+this.options.upvoteIconURL+'")');
                upvoteIcon.addClass('image');
            }

            // Upvotes
            var upvotes = this.createUpvoteElement(commentModel);

            // Append buttons for actions that are enabled
            if(this.options.enableReplying) actions.append(reply);
            if(this.options.enableUpvoting) actions.append(upvotes);

            if(commentModel.createdByCurrentUser || this.options.currentUserIsAdmin) {
                var editButton = $('<button/>', {
                    'class': 'action edit',
                    text: this.options.textFormatter(this.options.editText)
                });
                actions.append(editButton);
            }

            // Append separators between the actions
            actions.children().each(function(index, actionEl) {
                if(!$(actionEl).is(':last-child')) {
                    $(actionEl).after(separator.clone());
                }
            });

            wrapper.append(content);
            wrapper.append(attachments);
            wrapper.append(actions);
            commentWrapper.append(profilePicture).append(time).append(commentHeaderEl).append(wrapper);
            return commentWrapper;
        },

        createUpvoteElement: function(commentModel) {
            // Upvote icon
            var upvoteIcon = $('<i/>', {
                'class': 'bi bi-heart-fill'
            });
            if(this.options.upvoteIconURL.length) {
                upvoteIcon.css('background-image', 'url("'+this.options.upvoteIconURL+'")');
                upvoteIcon.addClass('image');
            }

            // Upvotes
            var upvoteEl = $('<button/>', {
                'class': 'action upvote' + (commentModel.userHasUpvoted ? ' highlight-font' : '')
            }).append($('<span/>', {
                text: commentModel.upvoteCount,
                'class': 'upvote-count'
            })).append(upvoteIcon);

            return upvoteEl;
        },

        createTagElement: function(text, extraClasses, value, extraAttributes) {
            var tagEl = $('<input/>', {
                'class': 'tag',
                'type': 'button',
                'data-role': 'none',
            });
            if(extraClasses) tagEl.addClass(extraClasses);
            tagEl.val(text);
            tagEl.attr('data-value', value);
            if (extraAttributes) tagEl.attr(extraAttributes);
            return tagEl;
        },

        createAttachmentTagElement: function(attachment, deletable) {
            
            // Tag element
            var attachmentTag = $('<a/>', {
                'class': 'tag attachment',
                'target': '_blank'
            });

            // Set href attribute if not deletable
            if(!deletable) {
                attachmentTag.attr('href', attachment.file);
            }

            // Bind data
            attachmentTag.data({
                id: attachment.id,
                mime_type: attachment.mime_type,
                file: attachment.file,
            });

            // File name
            var fileName = '';

            // Case: file is file object
            if(attachment.file instanceof File) {
                fileName = attachment.file.name;

            // Case: file is URL
            } else {
                var parts = attachment.file.split('/');
                var fileName = parts[parts.length - 1];
                fileName = fileName.split('?')[0];
                fileName = decodeURIComponent(fileName);
            }

            // Attachment icon
            var attachmentIcon = $('<i/>', {
                'class': 'fa fa-paperclip'
            });
            if(this.options.attachmentIconURL.length) {
                attachmentIcon.css('background-image', 'url("'+this.options.attachmentIconURL+'")');
                attachmentIcon.addClass('image');
            }

            // Append content
            attachmentTag.append(attachmentIcon, fileName);

            // Add delete button if deletable
            if(deletable) {
                attachmentTag.addClass('deletable');

                // Append close button
                var closeButton = this.createCloseButton('delete');
                attachmentTag.append(closeButton);
            }

            return attachmentTag;
        },

        reRenderComment: function(id) {
            var commentModel = this.commentsById[id];
            var commentElements = this.$el.find('li.comment[data-id="'+commentModel.id+'"]');

            var self = this;
            commentElements.each(function(index, commentEl) {
                var commentWrapper = self.createCommentWrapperElement(commentModel);
                $(commentEl).find('.comment-wrapper').first().replaceWith(commentWrapper);
            });
        },

        reRenderCommentActionBar: function(id) {
            var commentModel = this.commentsById[id];
            var commentElements = this.$el.find('li.comment[data-id="'+commentModel.id+'"]');

            var self = this;
            commentElements.each(function(index, commentEl) {
                var commentWrapper = self.createCommentWrapperElement(commentModel);
                $(commentEl).find('.actions').first().replaceWith(commentWrapper.find('.actions'));
            });
        },

        reRenderUpvotes: function(id) {
            var commentModel = this.commentsById[id];
            var commentElements = this.$el.find('li.comment[data-id="'+commentModel.id+'"]');

            var self = this;
            commentElements.each(function(index, commentEl) {
                var upvotes = self.createUpvoteElement(commentModel);
                $(commentEl).find('.upvote').first().replaceWith(upvotes);
            });
        },


        // Styling
        // =======

        createCssDeclarations: function() {

            // Remove previous css-declarations
            $('head style.jquery-comments-css').remove();

            // Navigation underline
            this.createCss('.jquery-comments ul.navigation li.active:after {background: '
                + this.options.highlightColor  + ' !important;',
                +'}');

            // Dropdown active element
            this.createCss('.jquery-comments ul.navigation ul.dropdown li.active {background: '
                + this.options.highlightColor  + ' !important;',
                +'}');

            // Background highlight
            this.createCss('.jquery-comments .highlight-background {background: '
                + this.options.highlightColor  + ' !important;',
                +'}');

            // Font highlight
            this.createCss('.jquery-comments .highlight-font {color: '
                + this.options.highlightColor + ' !important;'
                +'}');
            this.createCss('.jquery-comments .highlight-font-bold {color: '
                + this.options.highlightColor + ' !important;'
                + 'font-weight: bold;'
                +'}');
        },

        createCss: function(css) {
            var styleEl = $('<style/>', {
                type: 'text/css',
                'class': 'jquery-comments-css',
                text: css
            });
            $('head').append(styleEl);
        },


        // Utilities
        // =========

        getComments: function() {
            var self = this;
            return Object.keys(this.commentsById).map(function(id){return self.commentsById[id]});
        },

        getChildComments: function(parentId) {
            return this.getComments().filter(function(comment){return comment.parent == parentId});
        },

        getAttachments: function() {
            return this.getComments().filter(function(comment){return comment.hasAttachments()});
        },

        getOutermostParent: function(directParentId) {
            var parentId = directParentId;
            do {
                var parentComment = this.commentsById[parentId];
                parentId = parentComment.parent;
            } while(parentComment.parent != null);
            return parentComment;
        },

        createCommentJSON: function(commentingField) {
            var textarea = commentingField.find('.textarea');
            var time = new Date().toISOString();

            var commentJSON = {
                id: 'c' +  (this.getComments().length + 1),   // Temporary id
                parent: textarea.attr('data-parent') || null,
                created: time,
                modified: time,
                content: this.getTextareaContent(textarea),
                pings: this.getPings(textarea),
                fullname: this.options.textFormatter(this.options.youText),
                profilePictureURL: this.options.profilePictureURL,
                createdByCurrentUser: true,
                upvoteCount: 0,
                userHasUpvoted: false,
                attachments: this.getAttachmentsFromCommentingField(commentingField)
            };
            return commentJSON;
        },

        isAllowedToDelete: function(commentId) {
            if(this.options.enableDeleting) {
                var isAllowedToDelete = true;
                if(!this.options.enableDeletingCommentWithReplies) {
                    $(this.getComments()).each(function(index, comment) {
                        if(comment.parent == commentId) isAllowedToDelete = false;
                    });
                }
                return isAllowedToDelete;
            }
            return false;
        },

        setToggleAllButtonText: function(toggleAllButton, toggle) {
            var self = this;
            var textContainer = toggleAllButton.find('span.text');
            var caret = toggleAllButton.find('.caret');

            var showExpandingText = function() {
                var text = self.options.textFormatter(self.options.viewAllRepliesText);
                var replyCount = toggleAllButton.siblings('.comment').not('.hidden').length;
                text = text.replace('__replyCount__', replyCount);
                textContainer.text(text);
            };

            var hideRepliesText = this.options.textFormatter(this.options.hideRepliesText);

            if(toggle) {

                // Toggle text
                if(textContainer.text() == hideRepliesText) {
                    showExpandingText();
                } else {
                    textContainer.text(hideRepliesText);
                }
                // Toggle direction of the caret
                caret.toggleClass('up');

            } else {

                // Update text if necessary
                if(textContainer.text() != hideRepliesText) {
                    showExpandingText();
                }
            }
        },

        setButtonState: function(button, enabled, loading) {
            button.toggleClass('enabled', enabled);
            if(loading) {
                button.html(this.createSpinner(true));
            } else {
                button.html(button.data('original-content'));
            }
        },

        adjustTextareaHeight: function(textarea, focus) {
            var textareaBaseHeight = 2.2;
            var lineHeight = 1.4;

            var setRows = function(rows) {
                var height = textareaBaseHeight + (rows - 1) * lineHeight;
                textarea.css('height', height + 'em');
            };

            textarea = $(textarea);
            var rowCount = focus == true ? this.options.textareaRowsOnFocus : this.options.textareaRows;
            do {
                setRows(rowCount);
                rowCount++;
                var isAreaScrollable = textarea[0].scrollHeight > textarea.outerHeight();
                var maxRowsUsed = this.options.textareaMaxRows == false ?
                    false : rowCount > this.options.textareaMaxRows;
            } while(isAreaScrollable && !maxRowsUsed);
        },

        clearTextarea: function(textarea) {
            textarea.empty().trigger('input');
        },

        getTextareaContent: function(textarea, humanReadable) {
            var textareaClone = textarea.clone();

            // Remove reply-to tag
            textareaClone.find('.reply-to.tag').remove();

            // Replace tags with text values
            textareaClone.find('.tag.hashtag').replaceWith(function(){
                return humanReadable ? $(this).val() : '#' + $(this).attr('data-value');
            });
            textareaClone.find('.tag.ping').replaceWith(function(){
                return humanReadable ? $(this).val() : '@' + $(this).attr('data-value');
            });

            var ce = $('<pre/>').html(textareaClone.html());
            ce.find('div, p, br').replaceWith(function() { return '\n' + this.innerHTML; });

            // Trim leading spaces
            var text = ce.text().replace(/^\s+/g, '');

            // Normalize spaces
            var text = this.normalizeSpaces(text);
            return text;
        },

        getFormattedCommentContent: function(commentModel, replaceNewLines) {
            var html = this.escape(commentModel.content);
            html = this.linkify(html);
            html = this.highlightTags(commentModel, html);
            if(replaceNewLines) html = html.replace(/(?:\n)/g, '<br>');
            return html;
        },

        // Return pings in format
        //  {
        //      id1: userFullname1,
        //      id2: userFullname2,
        //      ...
        //  }
        getPings: function(textarea) {
            var pings = {};
            textarea.find('.ping').each(function(index, el){
                var id = parseInt($(el).attr('data-value'));
                var value = $(el).val();
                pings[id] = value.slice(1);
            });
            return pings;
        },

        getAttachmentsFromCommentingField: function(commentingField) {
            var attachments = commentingField.find('.attachments .attachment').map(function(){
                return $(this).data();
            }).toArray();

            return attachments;
        },

        moveCursorToEnd: function(el) {
            el = $(el)[0];

            // Trigger input to adjust size
            $(el).trigger('input');

            // Scroll to bottom
            $(el).scrollTop(el.scrollHeight);

            // Move cursor to end
            if (typeof window.getSelection != 'undefined' && typeof document.createRange != 'undefined') {
                var range = document.createRange();
                range.selectNodeContents(el);
                range.collapse(false);
                var sel = window.getSelection();
                sel.removeAllRanges();
                sel.addRange(range);
            } else if (typeof document.body.createTextRange != 'undefined') {
                var textRange = document.body.createTextRange();
                textRange.moveToElementText(el);
                textRange.collapse(false);
                textRange.select();
            }

            // Focus
            el.focus();
        },

        ensureElementStaysVisible: function(el) {
            var maxScrollTop = el.position().top;
            var minScrollTop = el.position().top + el.outerHeight() - this.options.scrollContainer.outerHeight();

            // Case: element hidden above scoll area
            if(this.options.scrollContainer.scrollTop() > maxScrollTop) {
                this.options.scrollContainer.scrollTop(maxScrollTop);

            // Case: element hidden below scoll area
            } else if(this.options.scrollContainer.scrollTop() < minScrollTop) {
                this.options.scrollContainer.scrollTop(minScrollTop);
            }

        },

        escape: function(inputText) {
            return $('<pre/>').text(this.normalizeSpaces(inputText)).html();
        },

        normalizeSpaces: function(inputText) {
            return inputText.replace(new RegExp('\u00a0', 'g'), ' ');   // Convert non-breaking spaces to reguar spaces
        },

        after: function(times, func) {
            var self = this;
            return function() {
                times--;
                if (times == 0) {
                    return func.apply(self, arguments);
                }
            }
        },

        highlightTags: function(commentModel, html) {
            if(this.options.enableHashtags) html = this.highlightHashtags(commentModel, html);
            if(this.options.enablePinging) html = this.highlightPings(commentModel, html);
            return html;
        },

        highlightHashtags: function(commentModel, html) {
            var self = this;

            if(html.indexOf('#') != -1) {

                var __createTag = function(tag) {
                    var tag = self.createTagElement('#' + tag, 'hashtag', tag);
                    return tag[0].outerHTML;
                }

                var regex = /(^|\s)#([a-z\u00C0-\u00FF\d-_]+)/gim;
                html = html.replace(regex, function($0, $1, $2){
                    return $1 + __createTag($2);
                });
            }
            return html;
        },

        highlightPings: function(commentModel, html) {
            var self = this;

            if(html.indexOf('@') != -1) {

                var __createTag = function(pingText, userId) {
                    var tag = self.createTagElement(pingText, 'ping', userId, {
                        'data-user-id': userId
                    });
                    return tag[0].outerHTML;
                }

                $(Object.keys(commentModel.pings)).each(function(index, userId) {
                    var fullname = commentModel.pings[userId];
                    var pingText = '@' + fullname;
                    html = html.replace(new RegExp(pingText, 'g'), __createTag(pingText, userId));
                });
            }
            return html;
        },

        linkify: function(inputText) {
            var replacedText, replacePattern1, replacePattern2, replacePattern3;

            // URLs starting with http://, https://, ftp:// or file://
            replacePattern1 = /(\b(https?|ftp|file):\/\/[-A-ZÄÖÅ0-9+&@#\/%?=~_|!:,.;{}]*[-A-ZÄÖÅ0-9+&@#\/%=~_|{}])/gim;
            replacedText = inputText.replace(replacePattern1, '<a href="$1" target="_blank">$1</a>');

            // URLs starting with "www." (without // before it, or it would re-link the ones done above).
            replacePattern2 = /(^|[^\/f])(www\.[-A-ZÄÖÅ0-9+&@#\/%?=~_|!:,.;{}]*[-A-ZÄÖÅ0-9+&@#\/%=~_|{}])/gim;
            replacedText = replacedText.replace(replacePattern2, '$1<a href="https://$2" target="_blank">$2</a>');

            // Change email addresses to mailto: links.
            replacePattern3 = /(([A-ZÄÖÅ0-9\-\_\.])+@[A-ZÄÖÅ\_]+?(\.[A-ZÄÖÅ]{2,6})+)/gim;
            replacedText = replacedText.replace(replacePattern3, '<a href="mailto:$1" target="_blank">$1</a>');

            // If there are hrefs in the original text, let's split
            // the text up and only work on the parts that don't have urls yet.
            var count = inputText.match(/<a href/g) || [];

            if (count.length > 0) {
                // Keep delimiter when splitting
                var splitInput = inputText.split(/(<\/a>)/g);
                for (var i = 0 ; i < splitInput.length ; i++) {
                    if (splitInput[i].match(/<a href/g) == null) {
                        splitInput[i] = splitInput[i]
                            .replace(replacePattern1, '<a href="$1" target="_blank">$1</a>')
                            .replace(replacePattern2, '$1<a href="https://$2" target="_blank">$2</a>')
                            .replace(replacePattern3, '<a href="mailto:$1" target="_blank">$1</a>');
                    }
                }
                var combinedReplacedText = splitInput.join('');
                return combinedReplacedText;
            } else {
                return replacedText;
            }
        },

        waitUntil: function(condition, callback) {
            var self = this;

            if(condition()) {
                callback();
            } else {
                setTimeout(function() {
                    self.waitUntil(condition, callback);
                }, 100);
            }
        },

        areArraysEqual: function(array1, array2) {

            // Case: arrays are different sized
            if(array1.length != array2.length) {
                return false;

            // Case: arrays are equal sized
            } else {
                array1.sort();
                array2.sort();

                for(var i=0; i < array1.length; i++) {
                    if(array1[i] != array2[i]) return false;
                }

                return true;
            }
        },

        applyInternalMappings: function(commentJSON) {
            // Inverting field mappings
            var invertedMappings = {};
            var mappings = this.options.fieldMappings;
            for (var prop in mappings) {
                if(mappings.hasOwnProperty(prop)) {
                    invertedMappings[mappings[prop]] = prop;
                }
            }

            return this.applyMappings(invertedMappings, commentJSON);
        },

        applyExternalMappings: function(commentJSON) {
            var mappings = this.options.fieldMappings;
            return this.applyMappings(mappings, commentJSON);
        },

        applyMappings: function(mappings, commentJSON) {
            var result = {};

            for(var key1 in commentJSON) {
                if(key1 in mappings) {
                    var key2 = mappings[key1];
                    result[key2] = commentJSON[key1];
                }
            }
            return result;
        }

    };

    $.fn.comments = function(options) {
        return this.each(function() {
            var comments = Object.create(Comments);
            $.data(this, 'comments', comments);
            comments.init(options || {}, this);
        });
    };
}));
