/** * emojiarea - A rich textarea control that supports emojis, WYSIWYG-style. * Copyright (c) 2012 DIY Co * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this * file except in compliance with the License. You may obtain a copy of the License at: * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF * ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. * * @author Brian Reavis */ /** * This file also contains some modifications by Igor Zhukov in order to add custom scrollbars to EmojiMenu * See keyword `MODIFICATION` in source code. */ (function($, window, document) { var ELEMENT_NODE = 1; var TEXT_NODE = 3; var TAGS_BLOCK = ['p', 'div', 'pre', 'form']; var KEY_ESC = 27; var KEY_TAB = 9; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*! MODIFICATION START Options 'spritesheetPath', 'spritesheetDimens', 'iconSize' added by Andre Staltz. */ $.emojiarea = { path: '', spritesheetPath: '', spritesheetDimens: [], iconSize: 20, icons: {}, defaults: { button: null, buttonLabel: 'Emojis', buttonPosition: 'after' } }; /*! MODIFICATION END */ $.fn.emojiarea = function(options) { options = $.extend({}, $.emojiarea.defaults, options); return this.each(function() { var $textarea = $(this); if ('contentEditable' in document.body && options.wysiwyg !== false) { new EmojiArea_WYSIWYG($textarea, options); } else { new EmojiArea_Plain($textarea, options); } }); }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var util = {}; util.restoreSelection = (function() { if (window.getSelection) { return function(savedSelection) { var sel = window.getSelection(); sel.removeAllRanges(); for (var i = 0, len = savedSelection.length; i < len; ++i) { sel.addRange(savedSelection[i]); } }; } else if (document.selection && document.selection.createRange) { return function(savedSelection) { if (savedSelection) { savedSelection.select(); } }; } })(); util.saveSelection = (function() { if (window.getSelection) { return function() { var sel = window.getSelection(), ranges = []; if (sel.rangeCount) { for (var i = 0, len = sel.rangeCount; i < len; ++i) { ranges.push(sel.getRangeAt(i)); } } return ranges; }; } else if (document.selection && document.selection.createRange) { return function() { var sel = document.selection; return (sel.type.toLowerCase() !== 'none') ? sel.createRange() : null; }; } })(); util.replaceSelection = (function() { if (window.getSelection) { return function(content) { var range, sel = window.getSelection(); var node = typeof content === 'string' ? document.createTextNode(content) : content; if (sel.getRangeAt && sel.rangeCount) { range = sel.getRangeAt(0); range.deleteContents(); range.insertNode(document.createTextNode(' ')); range.insertNode(node); range.setStart(node, 0); window.setTimeout(function() { range = document.createRange(); range.setStartAfter(node); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); }, 0); } } } else if (document.selection && document.selection.createRange) { return function(content) { var range = document.selection.createRange(); if (typeof content === 'string') { range.text = content; } else { range.pasteHTML(content.outerHTML); } } } })(); util.insertAtCursor = function(text, el) { text = ' ' + text; var val = el.value, endIndex, startIndex, range; if (typeof el.selectionStart != 'undefined' && typeof el.selectionEnd != 'undefined') { startIndex = el.selectionStart; endIndex = el.selectionEnd; el.value = val.substring(0, startIndex) + text + val.substring(el.selectionEnd); el.selectionStart = el.selectionEnd = startIndex + text.length; } else if (typeof document.selection != 'undefined' && typeof document.selection.createRange != 'undefined') { el.focus(); range = document.selection.createRange(); range.text = text; range.select(); } }; util.extend = function(a, b) { if (typeof a === 'undefined' || !a) { a = {}; } if (typeof b === 'object') { for (var key in b) { if (b.hasOwnProperty(key)) { a[key] = b[key]; } } } return a; }; util.escapeRegex = function(str) { return (str + '').replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); }; util.htmlEntities = function(str) { return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var EmojiArea = function() {}; EmojiArea.prototype.setup = function() { var self = this; this.$editor.on('focus', function() { self.hasFocus = true; }); this.$editor.on('blur', function() { self.hasFocus = false; }); this.setupButton(); }; EmojiArea.prototype.setupButton = function() { var self = this; var $button; if (this.options.button) { $button = $(this.options.button); } else if (this.options.button !== false) { $button = $(''); $button.html(this.options.buttonLabel); $button.addClass('emoji-button'); $button.attr({title: this.options.buttonLabel}); this.$editor[this.options.buttonPosition]($button); } else { $button = $(''); } $button.on('click', function(e) { EmojiMenu.show(self); e.stopPropagation(); }); this.$button = $button; }; /*! MODIFICATION START This function was modified by Andre Staltz so that the icon is created from a spritesheet. */ EmojiArea.createIcon = function(emoji) { var category = emoji[0]; var row = emoji[1]; var column = emoji[2]; var name = emoji[3]; var filename = $.emojiarea.spritesheetPath; var xoffset = -($.emojiarea.iconSize * column); var yoffset = -($.emojiarea.iconSize * row); var scaledWidth = ($.emojiarea.spritesheetDimens[category][1] * $.emojiarea.iconSize); var scaledHeight = ($.emojiarea.spritesheetDimens[category][0] * $.emojiarea.iconSize); var style = 'display:inline-block;'; style += 'width:' + $.emojiarea.iconSize + 'px;'; style += 'height:' + $.emojiarea.iconSize + 'px;'; style += 'background:url(\'' + filename.replace('!',category) + '\') ' + xoffset + 'px ' + yoffset + 'px no-repeat;'; style += 'background-size:' + scaledWidth + 'px ' + scaledHeight + 'px;'; return '' + util.htmlEntities(name) + ''; }; /*! MODIFICATION END */ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * Editor (plain-text) * * @constructor * @param {object} $textarea * @param {object} options */ var EmojiArea_Plain = function($textarea, options) { this.options = options; this.$textarea = $textarea; this.$editor = $textarea; this.setup(); }; EmojiArea_Plain.prototype.insert = function(emoji) { if (!$.emojiarea.icons.hasOwnProperty(emoji)) return; util.insertAtCursor(emoji, this.$textarea[0]); this.$textarea.trigger('change'); }; EmojiArea_Plain.prototype.val = function() { return this.$textarea.val(); }; util.extend(EmojiArea_Plain.prototype, EmojiArea.prototype); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * Editor (rich) * * @constructor * @param {object} $textarea * @param {object} options */ var EmojiArea_WYSIWYG = function($textarea, options) { var self = this; this.options = options || {}; this.$textarea = $textarea; this.$editor = $('
').addClass('emoji-wysiwyg-editor'); this.$editor.text($textarea.val()); this.$editor.attr({contenteditable: 'true'}); /*! MODIFICATION START Following code was modified by Igor Zhukov, in order to improve rich text paste */ var changeEvents = 'blur change'; if (!this.options.norealTime) { changeEvents += ' keyup'; } this.$editor.on(changeEvents, function(e) { return self.onChange.apply(self, [e]); }); this.$editor.on('paste', function(e) { return self.onPaste.apply(self, [e]); }); /*! MODIFICATION END */ this.$editor.on('mousedown focus', function() { document.execCommand('enableObjectResizing', false, false); }); this.$editor.on('blur', function() { document.execCommand('enableObjectResizing', true, true); }); var html = this.$editor.text(); var emojis = $.emojiarea.icons; for (var key in emojis) { if (emojis.hasOwnProperty(key)) { /* MODIFICATION: Following line was modified by Andre Staltz, to use new implementation of createIcon function.*/ html = html.replace(new RegExp(util.escapeRegex(key), 'g'), EmojiArea.createIcon(emojis[key])); } } this.$editor.html(html); $textarea.hide().after(this.$editor); this.setup(); /* MODIFICATION: Following line was modified by Igor Zhukov, in order to improve emoji insert behaviour */ $(document.body).on('mousedown', function() { if (self.hasFocus) { self.selection = util.saveSelection(); } }); }; /*! MODIFICATION START Following code was modified by Igor Zhukov, in order to improve rich text paste */ EmojiArea_WYSIWYG.prototype.onPaste = function(e) { var cData = (e.originalEvent || e).clipboardData, items = cData && cData.items || [], i; for (i = 0; i < items.length; i++) { if (items[i].kind == 'file') { e.preventDefault(); return true; } } var text = (e.originalEvent || e).clipboardData.getData('text/plain'), self = this; setTimeout(function () { self.onChange(); }, 0); if (text.length) { document.execCommand('insertText', false, text); return cancelEvent(e); } return true; }; /*! MODIFICATION END */ EmojiArea_WYSIWYG.prototype.onChange = function(e) { this.$textarea.val(this.val()).trigger('change'); }; EmojiArea_WYSIWYG.prototype.insert = function(emoji) { var content; /* MODIFICATION: Following line was modified by Andre Staltz, to use new implementation of createIcon function.*/ var $img = $(EmojiArea.createIcon($.emojiarea.icons[emoji])); if ($img[0].attachEvent) { $img[0].attachEvent('onresizestart', function(e) { e.returnValue = false; }, false); } this.$editor.trigger('focus'); if (this.selection) { util.restoreSelection(this.selection); } try { util.replaceSelection($img[0]); } catch (e) {} /*! MODIFICATION START Following code was modified by Igor Zhukov, in order to improve selection handling */ var self = this; setTimeout(function () { self.selection = util.saveSelection(); }, 100); /*! MODIFICATION END */ this.onChange(); }; EmojiArea_WYSIWYG.prototype.val = function() { var lines = []; var line = []; var flush = function() { lines.push(line.join('')); line = []; }; var sanitizeNode = function(node) { if (node.nodeType === TEXT_NODE) { line.push(node.nodeValue); } else if (node.nodeType === ELEMENT_NODE) { var tagName = node.tagName.toLowerCase(); var isBlock = TAGS_BLOCK.indexOf(tagName) !== -1; if (isBlock && line.length) flush(); if (tagName === 'img') { var alt = node.getAttribute('alt') || ''; if (alt) line.push(alt); return; } else if (tagName === 'br') { flush(); } var children = node.childNodes; for (var i = 0; i < children.length; i++) { sanitizeNode(children[i]); } if (isBlock && line.length) flush(); } }; var children = this.$editor[0].childNodes; for (var i = 0; i < children.length; i++) { sanitizeNode(children[i]); } if (line.length) flush(); return lines.join('\n'); }; util.extend(EmojiArea_WYSIWYG.prototype, EmojiArea.prototype); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * Emoji Dropdown Menu * * @constructor * @param {object} emojiarea */ var EmojiMenu = function() { var self = this; var $body = $(document.body); var $window = $(window); this.visible = false; this.emojiarea = null; this.$menu = $('
'); this.$menu.addClass('emoji-menu'); this.$menu.hide(); /*! MODIFICATION START Following code was modified by Igor Zhukov, in order to add scrollbars and tail to EmojiMenu Also modified by Andre Staltz, to include tabs for categories, on the menu header. */ this.$itemsTailWrap = $('
').appendTo(this.$menu); this.$categoryTabs = $('' + '' + '' + '' + '' + '' + '
').appendTo(this.$itemsTailWrap); this.$itemsWrap = $('
').appendTo(this.$itemsTailWrap); this.$items = $('
').appendTo(this.$itemsWrap); $('
').appendTo(this.$menu); /*! MODIFICATION END */ $body.append(this.$menu); /*! MODIFICATION: Following line is added by Igor Zhukov, in order to add scrollbars to EmojiMenu */ this.$itemsWrap.nanoScroller({preventPageScrolling: true, tabIndex: -1}); $body.on('keydown', function(e) { if (e.keyCode === KEY_ESC || e.keyCode === KEY_TAB) { self.hide(); } }); $body.on('mouseup', function(e) { /*! MODIFICATION START Following code was added by Igor Zhukov, in order to prevent close on click on EmojiMenu scrollbar */ e = e.originalEvent || e; var target = e.originalTarget || e.target || window; while (target && target != window) { target = target.parentNode; if (target == self.$menu[0] || self.emojiarea && target == self.emojiarea.$button[0]) { return; } } /*! MODIFICATION END */ self.hide(); }); $window.on('resize', function() { if (self.visible) self.reposition(); }); this.$menu.on('mouseup', 'a', function(e) { e.stopPropagation(); return false; }); this.$menu.on('click', 'a', function(e) { /*! MODIFICATION START Following code was modified by Andre Staltz, to capture clicks on category tabs and change the category selection. */ if ($(this).hasClass('emoji-menu-tab')) { if (self.getTabIndex(this) !== self.currentCategory) { self.selectCategory(self.getTabIndex(this)); } return false; } /*! MODIFICATION END */ var emoji = $('.label', $(this)).text(); window.setTimeout(function() { self.onItemSelected(emoji); /*! MODIFICATION START Following code was modified by Igor Zhukov, in order to close only on ctrl-, alt- emoji select */ if (e.ctrlKey || e.metaKey) { self.hide(); } /*! MODIFICATION END */ }, 0); e.stopPropagation(); return false; }); /* MODIFICATION: Following line was modified by Andre Staltz, in order to select a default category. */ this.selectCategory(0); }; /*! MODIFICATION START Following code was added by Andre Staltz, to implement category selection. */ EmojiMenu.prototype.getTabIndex = function(tab) { return this.$categoryTabs.find('.emoji-menu-tab').index(tab); }; EmojiMenu.prototype.selectCategory = function(category) { var self = this; this.$categoryTabs.find('.emoji-menu-tab').each(function(index) { if (index === category) { this.className += '-selected'; } else { this.className = this.className.replace('-selected', ''); } }); this.currentCategory = category; this.load(category); this.$itemsWrap.nanoScroller({ scroll: 'top' }); }; /*! MODIFICATION END */ EmojiMenu.prototype.onItemSelected = function(emoji) { this.emojiarea.insert(emoji); }; /* MODIFICATION: The following function argument was modified by Andre Staltz, in order to load only icons from a category. */ EmojiMenu.prototype.load = function(category) { var html = []; var options = $.emojiarea.icons; var path = $.emojiarea.path; if (path.length && path.charAt(path.length - 1) !== '/') { path += '/'; } for (var key in options) { /* MODIFICATION: The following 2 lines were modified by Andre Staltz, in order to load only icons from the specified category. */ if (options.hasOwnProperty(key) && options[key][0] === category) { html.push('' + EmojiArea.createIcon(options[key]) + '' + util.htmlEntities(key) + ''); } } this.$items.html(html.join('')); /*! MODIFICATION: Following 4 lines were added by Igor Zhukov, in order to add scrollbars to EmojiMenu */ var self = this; setTimeout(function () { self.$itemsWrap.nanoScroller(); }, 100); }; EmojiMenu.prototype.reposition = function() { var $button = this.emojiarea.$button; var offset = $button.offset(); offset.top += $button.outerHeight(); offset.left += Math.round($button.outerWidth() / 2); this.$menu.css({ top: offset.top, left: offset.left }); }; EmojiMenu.prototype.hide = function(callback) { if (this.emojiarea) { this.emojiarea.menu = null; this.emojiarea.$button.removeClass('on'); this.emojiarea = null; } this.visible = false; this.$menu.hide(); }; EmojiMenu.prototype.show = function(emojiarea) { /* MODIFICATION: Following line was modified by Igor Zhukov, in order to improve EmojiMenu behaviour */ if (this.emojiarea && this.emojiarea === emojiarea) return this.hide(); emojiarea.$button.addClass('on'); this.emojiarea = emojiarea; this.emojiarea.menu = this; this.reposition(); this.$menu.show(); this.visible = true; }; EmojiMenu.show = (function() { var menu = null; return function(emojiarea) { menu = menu || new EmojiMenu(); menu.show(emojiarea); }; })(); })(jQuery, window, document);