1/** 2 * emojiarea - A rich textarea control that supports emojis, WYSIWYG-style. 3 * Copyright (c) 2012 DIY Co 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 6 * file except in compliance with the License. You may obtain a copy of the License at: 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under 10 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 * ANY KIND, either express or implied. See the License for the specific language 12 * governing permissions and limitations under the License. 13 * 14 * @author Brian Reavis <[email protected]> 15 */ 16 17(function($, window, document) { 18 19 var ELEMENT_NODE = 1; 20 var TEXT_NODE = 3; 21 var TAGS_BLOCK = ['p', 'div', 'pre', 'form']; 22 var KEY_ESC = 27; 23 var KEY_TAB = 9; 24 25 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 26 27 $.emojiarea = { 28 path: '', 29 icons: {}, 30 defaults: { 31 button: null, 32 buttonLabel: 'Emojis', 33 buttonPosition: 'after', 34 contentEditable: 'always' // always - contentEditable always creates, empty - check if textarea is empty, any other - no contentEditable 35 } 36 }; 37 38 $.fn.emojiarea = function(options) { 39 options = $.extend({}, $.emojiarea.defaults, options); 40 return this.each(function() { 41 var $textarea = $(this); 42 43 var wysiwyg = false; 44 if (options.contentEditable == 'always' || (options.contentEditable == 'empty' && $textarea.val().length)) { 45 wysiwyg = true; 46 } 47 48 if ('contentEditable' in document.body && options.wysiwyg !== false && wysiwyg) { 49 new EmojiArea_WYSIWYG($textarea, options); 50 } else { 51 new EmojiArea_Plain($textarea, options); 52 } 53 }); 54 }; 55 56 $.fn.emoji = function() { 57 this.each(function () { 58 var html = $(this).html(); 59 var emojis = $.emojiarea.icons; 60 //console.log(html); 61 //debugger; 62 for (var key in emojis) { 63 if (emojis.hasOwnProperty(key)) { 64 html = html.replace(new RegExp(util.escapeRegex(key), 'g'), EmojiArea.createIcon(key)); 65 } 66 } 67 68 $(this).html(html); 69 }); 70 }; 71 72 $.emoji = function(html) { 73 if (html) { 74 var emojis = $.emojiarea.icons; 75 76 for (var key in emojis) { 77 if (emojis.hasOwnProperty(key)) { 78 html = html.replace(new RegExp(util.escapeRegex(key), 'g'), EmojiArea.createIcon(key)); 79 } 80 } 81 82 return html; 83 } else { 84 return html; 85 } 86 }; 87 88 $.fn.emojiButton = function() { 89 this.each(function () { 90 $(this).after('<div class="emoji-button"><span class="fa fa-smile-o fa-lg"></span></div>'); 91 $(this).next('.emoji-button').andSelf().wrapAll('<div class="emoji-container" />'); 92 }); 93 }; 94 95 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 96 97 var util = {}; 98 99 util.restoreSelection = (function() { 100 if (window.getSelection) { 101 return function(savedSelection) { 102 var sel = window.getSelection(); 103 sel.removeAllRanges(); 104 for (var i = 0, len = savedSelection.length; i < len; ++i) { 105 sel.addRange(savedSelection[i]); 106 } 107 }; 108 } else if (document.selection && document.selection.createRange) { 109 return function(savedSelection) { 110 if (savedSelection) { 111 savedSelection.select(); 112 } 113 }; 114 } 115 })(); 116 117 util.saveSelection = (function() { 118 if (window.getSelection) { 119 return function() { 120 var sel = window.getSelection(), ranges = []; 121 if (sel.rangeCount) { 122 for (var i = 0, len = sel.rangeCount; i < len; ++i) { 123 ranges.push(sel.getRangeAt(i)); 124 } 125 } 126 return ranges; 127 }; 128 } else if (document.selection && document.selection.createRange) { 129 return function() { 130 var sel = document.selection; 131 return (sel.type.toLowerCase() !== 'none') ? sel.createRange() : null; 132 }; 133 } 134 })(); 135 136 util.replaceSelection = (function() { 137 if (window.getSelection) { 138 return function(content) { 139 var range, sel = window.getSelection(); 140 var node = typeof content === 'string' ? document.createTextNode(content) : content; 141 if (sel.getRangeAt && sel.rangeCount) { 142 range = sel.getRangeAt(0); 143 range.deleteContents();
144 range.insertNode(document.createTextNode(' ')); 145 range.insertNode(node); 146 range.setStart(node, 0); 147 148 window.setTimeout(function() { 149 range = document.createRange(); 150 range.setStartAfter(node); 151 range.collapse(false); 152 sel.removeAllRanges(); 153 sel.addRange(range); 154 }, 0); 155 } 156 } 157 } else if (document.selection && document.selection.createRange) { 158 return function(content) { 159 var range = document.selection.createRange(); 160 if (typeof content === 'string') { 161 range.text = content; 162 } else { 163 range.pasteHTML(content.outerHTML); 164 } 165 } 166 } 167 })(); 168 169 util.insertAtCursor = function(text, el) { 170 text = ' ' + text; 171 var val = el.value, endIndex, startIndex, range; 172 if (typeof el.selectionStart != 'undefined' && typeof el.selectionEnd != 'undefined') { 173 startIndex = el.selectionStart; 174 endIndex = el.selectionEnd; 175 el.value = val.substring(0, startIndex) + text + val.substring(el.selectionEnd); 176 el.selectionStart = el.selectionEnd = startIndex + text.length; 177 } else if (typeof document.selection != 'undefined' && typeof document.selection.createRange != 'undefined') { 178 el.focus(); 179 range = document.selection.createRange(); 180 range.text = text; 181 range.select(); 182 } 183 }; 184 185 util.extend = function(a, b) { 186 if (typeof a === 'undefined' || !a) { a = {}; } 187 if (typeof b === 'object') { 188 for (var key in b) { 189 if (b.hasOwnProperty(key)) { 190 a[key] = b[key]; 191 } 192 } 193 } 194 return a; 195 }; 196 197 util.escapeRegex = function(str) { 198 return (str + '').replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); 199 }; 200 201 util.htmlEntities = function(str) { 202 return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); 203 }; 204 205 util.nl2br = function(str) { 206 return str.replace(/(\r\n|\n\r|\r|\n)/g, "<br>"); 207 }; 208 209 util.removeBr = function(str) { 210 return str.replace(/<br>/g, "\r"); 211 }; 212 213 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 214 215 var EmojiArea = function() {}; 216 217 EmojiArea.prototype.setup = function() { 218 var self = this; 219 220 this.$editor.on('focus', function() { self.hasFocus = true; }); 221 this.$editor.on('blur', function() { self.hasFocus = false; }); 222 223 this.setupButton(); 224 }; 225 226 EmojiArea.prototype.setupButton = function() { 227 var self = this; 228 var $button; 229 230 if (this.options.button) { 231 $button = this.$textarea.siblings(this.options.button); 232 } else if (this.options.button !== false) { 233 $button = $('<a href="javascript:void(0)">'); 234 $button.html(this.options.buttonLabel); 235 $button.addClass('emoji-button'); 236 $button.attr({title: this.options.buttonLabel}); 237 this.$editor[this.options.buttonPosition]($button); 238 } else { 239 $button = $(''); 240 } 241 242 $button.on('click', function(e) { 243 EmojiMenu.show(self); 244 e.stopPropagation(); 245 }); 246 247 this.$button = $button; 248 }; 249 250 EmojiArea.createIcon = function(emoji) { 251 return '<span class="smile-box">'+ $.emojiarea.entities[emoji] +'</span>'; 252 }; 253 254 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 255 256 /** 257 * Editor (plain-text) 258 * 259 * @constructor 260 * @param {object} $textarea 261 * @param {object} options 262 */ 263 264 var EmojiArea_Plain = function($textarea, options) { 265 this.options = options; 266 this.$textarea = $textarea; 267 this.$editor = $textarea; 268 this.setup(); 269 }; 270 271 EmojiArea_Plain.prototype.insert = function(emoji) { 272 if (!$.emojiarea.icons.hasOwnProperty(emoji)) return; 273 util.insertAtCursor(emoji, this.$textarea[0]); 274 this.$textarea.trigger('change'); 275 }; 276 277 EmojiArea_Plain.prototype.val = function() { 278 return this.$textarea.val(); 279 }; 280 281 util.extend(EmojiArea_Plain.prototype, EmojiArea.prototype); 282 283 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 284 285 /** 286 * Editor (rich) 287 * 288 * @constructor 289 * @param {object} $textarea 290 * @param {object} options 291 */ 292 293 var EmojiArea_WYSIWYG = function($textarea, options) { 294 var self = this; 295 296 this.options = options; 297 this.$textarea = $textarea; 298 this.$editor = $('<div>') 299 .addClass('emoji-wysiwyg-editor') 300 .css({ 301 minHeight: $textarea.css('height'), 302 maxHeight: $textarea.css('max-height') 303 }); 304 this.$editor.html($textarea.val()); 305 306 this.$editor.attr({contenteditable: 'true'}); 307 this.$editor.on('blur keyup paste', function() { return self.onChange.apply(self, arguments); }); 308 this.$editor.on('mousedown focus', function() { document.execCommand('enableObjectResizing', false, false); }); 309 this.$editor.on('blur', function() { document.execCommand('enableObjectResizing', true, true); }); 310 311 var html = util.nl2br(this.$editor.html()); 312 313 var emojis = $.emojiarea.icons; 314 for (var key in emojis) { 315 if (emojis.hasOwnProperty(key)) { 316 html = html.replace(new RegExp(util.escapeRegex(key), 'g'), EmojiArea.createIcon(key)); 317 } 318 } 319 this.$editor.html(html); 320 321 $textarea.hide().after(this.$editor); 322 323 this.setup(); 324 325 this.$button.on('mousedown', function() { 326 if (self.hasFocus) { 327 self.selection = util.saveSelection(); 328 } 329 }); 330 331 this.$button.trigger('click'); 332 333 this.$editor.height('auto'); 334 }; 335 336 EmojiArea_WYSIWYG.prototype.onChange = function() { 337 this.$textarea.val(util.removeBr(this.val())).trigger('change'); 338 }; 339 340 EmojiArea_WYSIWYG.prototype.insert = function(emoji) { 341 var content; 342 var $img = $(EmojiArea.createIcon(emoji)); 343 if ($img[0].attachEvent) { 344 $img[0].attachEvent('onresizestart', function(e) { e.returnValue = false; }, false); 345 } 346 347 this.$editor.trigger('focus'); 348 if (this.selection) { 349 util.restoreSelection(this.selection); 350 } 351 try { util.replaceSelection($img[0]); } catch (e) {} 352 this.onChange(); 353 }; 354 355 EmojiArea_WYSIWYG.prototype.val = function() { 356 var lines = []; 357 var line = []; 358 359 var flush = function() { 360 lines.push(line.join('')); 361 line = []; 362 }; 363 364 var sanitizeNode = function(node) { 365 if (node.nodeType === TEXT_NODE) { 366 line.push(node.nodeValue); 367 } else if (node.nodeType === ELEMENT_NODE) { 368 var tagName = node.tagName.toLowerCase(); 369 var isBlock = TAGS_BLOCK.indexOf(tagName) !== -1; 370 371 if (isBlock && line.length) flush(); 372 373 if (tagName === 'img') { 374 var alt = node.getAttribute('alt') || ''; 375 if (alt) line.push(alt); 376 return; 377 } else if (tagName === 'br') { 378 flush(); 379 } 380 381 var children = node.childNodes; 382 for (var i = 0; i < children.length; i++) { 383 sanitizeNode(children[i]); 384 } 385 386 if (isBlock && line.length) flush(); 387 } 388 }; 389 390 var children = this.$editor[0].childNodes; 391 for (var i = 0; i < children.length; i++) { 392 sanitizeNode(children[i]); 393 } 394 395 if (line.length) flush(); 396 397 return lines.join('\n'); 398 }; 399 400 util.extend(EmojiArea_WYSIWYG.prototype, EmojiArea.prototype); 401 402 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 403 404 /** 405 * Emoji Dropdown Menu 406 * 407 * @constructor 408 * @param {object} emojiarea 409 */ 410 var EmojiMenu = function() { 411 var self = this; 412 var $body = $(document.body); 413 var $window = $(window); 414 415 this.visible = false;
416 this.emojiarea = null; 417 this.$menu = $('<div>'); 418 this.$menu.addClass('emoji-menu'); 419 this.$menu.hide(); 420 this.$items = $('<div>').appendTo(this.$menu); 421 422 $body.append(this.$menu); 423 424 $body.on('keydown', function(e) { 425 if (e.keyCode === KEY_ESC || e.keyCode === KEY_TAB) { 426 self.hide(); 427 } 428 }); 429 430 $body.on('mouseup', function() { 431 self.hide(); 432 }); 433 434 $window.on('resize', function() { 435 if (self.visible) self.reposition(); 436 }); 437 438 this.$menu.on('mouseup', 'a', function(e) { 439 e.stopPropagation(); 440 return false; 441 }); 442 443 this.$menu.on('click', 'a', function(e) { 444 var emoji = $('.label', $(this)).text(); 445 window.setTimeout(function() { 446 self.onItemSelected.apply(self, [emoji]); 447 }, 0); 448 e.stopPropagation(); 449 return false; 450 }); 451 452 this.load(); 453 }; 454 455 EmojiMenu.prototype.onItemSelected = function(emoji) { 456 this.emojiarea.insert(emoji); 457 this.hide(); 458 }; 459 460 EmojiMenu.prototype.load = function() { 461 var html = []; 462 var options = $.emojiarea.icons; 463 var path = $.emojiarea.path; 464 if (path.length && path.charAt(path.length - 1) !== '/') { 465 path += '/'; 466 } 467 468 for (var key in options) { 469 if (options.hasOwnProperty(key)) { 470 var filename = options[key]; 471 html.push('<a href="javascript:void(0)" title="' + util.htmlEntities(key) + '">' + EmojiArea.createIcon(key) + '<span class="label">' + util.htmlEntities(key) + '</span></a>'); 472 } 473 } 474 475 this.$items.html(html.join('')); 476 }; 477 478 EmojiMenu.prototype.reposition = function() { 479 var $button = this.emojiarea.$button; 480 var offset = $button.offset(); 481 482 //offset.left += Math.round($button.outerWidth() / 2); 483 var dW = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); 484 var dH = Math.max(document.documentElement.clientHeight, window.innerHeight || 0, $(document).height()); 485 var mW = this.$menu.outerWidth(); 486 var mH = this.$menu.outerHeight(); 487 488 this.$menu.css({ 489 top: (offset.top + mH) < dH ? offset.top + $button.outerHeight() : (offset.top - mH - Math.round($button.outerHeight()/2.4)) 490 ,left: (offset.left + $button.outerWidth() + mW) > dW ? 491 (offset.left - Math.round($button.outerWidth()*2) - Math.round(mW/2)) : 492 (offset.left - Math.round($button.outerWidth()*2) + Math.round(mW/2.4)) 493 }); 494 }; 495 496 EmojiMenu.prototype.hide = function(callback) { 497 if (this.emojiarea) { 498 this.emojiarea.menu = null; 499 this.emojiarea.$button.removeClass('on'); 500 this.emojiarea = null; 501 } 502 this.visible = false; 503 this.$menu.hide(); 504 }; 505 506 EmojiMenu.prototype.show = function(emojiarea) { 507 if (this.emojiarea && this.emojiarea === emojiarea) return; 508 this.emojiarea = emojiarea; 509 this.emojiarea.menu = this; 510 511 this.reposition(); 512 this.$menu.show(); 513 this.visible = true; 514 }; 515 516 EmojiMenu.show = (function() { 517 var menu = null; 518 return function(emojiarea) { 519 menu = menu || new EmojiMenu(); 520 menu.show(emojiarea); 521 }; 522 })(); 523 524})(jQuery, window, document);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.