emojionearea
Version:
WYSIWYG-like EmojiOne Converter / Picker Plugin for jQuery
644 lines (568 loc) • 25.7 kB
JavaScript
define([
'jquery',
'var/emojione',
'var/blankImg',
'var/slice',
'var/css_class',
'var/emojioneSupportMode',
'var/invisibleChar',
'function/trigger',
'function/attach',
'function/shortnameTo',
'function/pasteHtmlAtCaret',
'function/getOptions',
'function/saveSelection',
'function/restoreSelection',
'function/htmlFromText',
'function/textFromHtml',
'function/isObject',
'function/calcButtonPosition',
'function/lazyLoading',
'function/selector',
'function/div',
'function/updateRecent',
'function/getRecent',
'function/setRecent',
'function/supportsLocalStorage'
//'function/calcElapsedTime', // debug only
],
function($, emojione, blankImg, slice, css_class, emojioneSupportMode, invisibleChar, trigger, attach, shortnameTo,
pasteHtmlAtCaret, getOptions, saveSelection, restoreSelection, htmlFromText, textFromHtml, isObject,
calcButtonPosition, lazyLoading, selector, div, updateRecent, getRecent, setRecent, supportsLocalStorage)
{
return function(self, source, options) {
//calcElapsedTime('init', function() {
self.options = options = getOptions(options);
self.sprite = options.sprite && emojioneSupportMode < 3;
self.inline = options.inline === null ? source.is("INPUT") : options.inline;
self.shortnames = options.shortnames;
self.saveEmojisAs = options.saveEmojisAs;
self.standalone = options.standalone;
self.emojiTemplate = '<img alt="{alt}" class="emojione' + (self.sprite ? '-{uni}" src="' + blankImg + '"/>' : 'emoji" src="{img}" crossorigin/>');
self.emojiTemplateAlt = self.sprite ? '<i class="emojione-{uni}"/>' : '<img class="emojioneemoji" src="{img}" crossorigin/>';
self.emojiBtnTemplate = '<i class="emojibtn" role="button" data-name="{name}" title="{friendlyName}">' + self.emojiTemplateAlt + '</i>';
self.recentEmojis = options.recentEmojis && supportsLocalStorage();
var pickerPosition = options.pickerPosition;
self.floatingPicker = pickerPosition === 'top' || pickerPosition === 'bottom';
self.source = source;
if (source.is(":disabled") || source.is(".disabled")) {
self.disable();
}
var sourceValFunc = source.is("TEXTAREA") || source.is("INPUT") ? "val" : "text",
editor, button, picker, filters, filtersBtns, searchPanel, emojisList, categories, categoryBlocks, scrollArea,
tones = div('tones',
options.tones ?
function() {
this.addClass(selector('tones-' + options.tonesStyle, true));
for (var i = 0; i <= 5; i++) {
this.append($("<i/>", {
"class": "btn-tone btn-tone-" + i + (!i ? " active" : ""),
"data-skin": i,
role: "button"
}));
}
} : null
),
app = div({
"class" : css_class + ((self.standalone) ? " " + css_class + "-standalone " : " ") + (source.attr("class") || ""),
role: "application"
},
editor = self.editor = div("editor").attr({
contenteditable: (self.standalone) ? false : true,
placeholder: options.placeholder || source.data("placeholder") || source.attr("placeholder") || "",
tabindex: 0
}),
button = self.button = div('button',
div('button-open'),
div('button-close')
).attr('title', options.buttonTitle),
picker = self.picker = div('picker',
div('wrapper',
filters = div('filters'),
(options.search ?
searchPanel = div('search-panel',
div('search',
options.search ?
function() {
self.search = $("<input/>", {
"placeholder": options.searchPlaceholder || "",
"type": "text",
"class": "search"
});
this.append(self.search);
} : null
),
tones
) : null
),
scrollArea = div('scroll-area',
options.tones && !options.search ? div('tones-panel',
tones
) : null,
emojisList = div('emojis-list')
)
)
).addClass(selector('picker-position-' + options.pickerPosition, true))
.addClass(selector('filters-position-' + options.filtersPosition, true))
.addClass(selector('search-position-' + options.searchPosition, true))
.addClass('hidden')
);
if (options.search) {
searchPanel.addClass(selector('with-search', true));
}
self.searchSel = null;
editor.data(source.data());
$.each(options.attributes, function(attr, value) {
editor.attr(attr, value);
});
var mainBlock = div('category-block').attr({"data-tone": 0}).prependTo(emojisList);
$.each(options.filters, function(filter, params) {
var skin = 0;
if (filter === 'recent' && !self.recentEmojis) {
return;
}
if (filter !== 'tones') {
$("<i/>", {
"class": selector("filter", true) + " " + selector("filter-" + filter, true),
"data-filter": filter,
title: params.title
})
.wrapInner(shortnameTo(params.icon, self.emojiTemplateAlt))
.appendTo(filters);
} else if (options.tones) {
skin = 5;
} else {
return;
}
do {
var category,
items = params.emoji.replace(/[\s,;]+/g, '|');
if (skin === 0) {
category = div('category').attr({
name: filter,
"data-tone": skin
}).appendTo(mainBlock);
} else {
category = div('category-block').attr({
name: filter,
"data-tone": skin
}).appendTo(emojisList);
}
if (skin > 0) {
category.hide();
items = items.split('|').join('_tone' + skin + '|') + '_tone' + skin;
}
if (filter === 'recent') {
items = getRecent();
}
items = shortnameTo(items,
self.sprite ?
'<i class="emojibtn" role="button" data-name="{name}" title="{friendlyName}"><i class="emojione-{uni}"></i></i>' :
'<i class="emojibtn" role="button" data-name="{name}" title="{friendlyName}"><img class="emojioneemoji lazy-emoji" data-src="{img}" crossorigin/></i>',
true).split('|').join('');
category.html(items);
$('<div class="emojionearea-category-title"/>').text(params.title).prependTo(category);
} while (--skin > 0);
});
options.filters = null;
if (!self.sprite) {
self.lasyEmoji = emojisList.find(".lazy-emoji");
}
filtersBtns = filters.find(selector("filter"));
filtersBtns.eq(0).addClass("active");
categoryBlocks = emojisList.find(selector("category-block"))
categories = emojisList.find(selector("category"))
self.recentFilter = filtersBtns.filter('[data-filter="recent"]');
self.recentCategory = categories.filter("[name=recent]");
self.scrollArea = scrollArea;
if (options.container) {
$(options.container).wrapInner(app);
} else {
app.insertAfter(source);
}
if (options.hideSource) {
source.hide();
}
self.setText(source[sourceValFunc]());
source[sourceValFunc](self.getText());
calcButtonPosition.apply(self);
// if in standalone mode and no value is set, initialise with a placeholder
if (self.standalone && !self.getText().length) {
var placeholder = $(source).data("emoji-placeholder") || options.emojiPlaceholder;
self.setText(placeholder);
editor.addClass("has-placeholder");
}
// attach() must be called before any .on() methods !!!
// 1) attach() stores events into possibleEvents{},
// 2) .on() calls bindEvent() and stores handlers into eventStorage{},
// 3) bindEvent() finds events in possibleEvents{} and bind founded via jQuery.on()
// 4) attached events via jQuery.on() calls trigger()
// 5) trigger() calls handlers stored into eventStorage{}
attach(self, emojisList.find(".emojibtn"), {click: "emojibtn.click"});
attach(self, window, {resize: "!resize"});
attach(self, tones.children(), {click: "tone.click"});
attach(self, [picker, button], {mousedown: "!mousedown"}, editor);
attach(self, button, {click: "button.click"});
attach(self, editor, {paste :"!paste"}, editor);
attach(self, editor, ["focus", "blur"], function() { return self.stayFocused ? false : editor; } );
attach(self, picker, {mousedown: "picker.mousedown", mouseup: "picker.mouseup", click: "picker.click",
keyup: "picker.keyup", keydown: "picker.keydown", keypress: "picker.keypress"});
attach(self, editor, ["mousedown", "mouseup", "click", "keyup", "keydown", "keypress"]);
attach(self, picker.find(".emojionearea-filter"), {click: "filter.click"});
attach(self, source, {change: "source.change"});
if (options.search) {
attach(self, self.search, {keyup: "search.keypress", focus: "search.focus", blur: "search.blur"});
}
var noListenScroll = false;
scrollArea.on('scroll', function () {
if (!noListenScroll) {
lazyLoading.call(self);
if (scrollArea.is(":not(.skinnable)")) {
var item = categories.eq(0), scrollTop = scrollArea.offset().top;
categories.each(function (i, e) {
if ($(e).offset().top - scrollTop >= 10) {
return false;
}
item = $(e);
});
var filter = filtersBtns.filter('[data-filter="' + item.attr("name") + '"]');
if (filter[0] && !filter.is(".active")) {
filtersBtns.removeClass("active");
filter.addClass("active");
}
}
}
});
self.on("@filter.click", function(filter) {
var isActive = filter.is(".active");
if (scrollArea.is(".skinnable")) {
if (isActive) return;
tones.children().eq(0).click();
}
noListenScroll = true;
if (!isActive) {
filtersBtns.filter(".active").removeClass("active");
filter.addClass("active");
}
var headerOffset = categories.filter('[name="' + filter.data('filter') + '"]').offset().top,
scroll = scrollArea.scrollTop(),
offsetTop = scrollArea.offset().top;
scrollArea.stop().animate({
scrollTop: headerOffset + scroll - offsetTop - 2
}, 200, 'swing', function () {
lazyLoading.call(self);
noListenScroll = false;
});
})
.on("@picker.show", function() {
if (self.recentEmojis) {
updateRecent(self);
}
lazyLoading.call(self);
})
.on("@tone.click", function(tone) {
tones.children().removeClass("active");
var skin = tone.addClass("active").data("skin");
if (skin) {
scrollArea.addClass("skinnable");
categoryBlocks.hide().filter("[data-tone=" + skin + "]").show();
filtersBtns.removeClass("active");//.not('[data-filter="recent"]').eq(0).addClass("active");
} else {
scrollArea.removeClass("skinnable");
categoryBlocks.hide().filter("[data-tone=0]").show();
filtersBtns.eq(0).click();
}
lazyLoading.call(self);
if (options.search) {
self.trigger('search.keypress');
}
})
.on("@button.click", function(button) {
if (button.is(".active")) {
self.hidePicker();
} else {
self.showPicker();
self.searchSel = null;
}
})
.on("@!paste", function(editor, event) {
var pasteText = function(text) {
var caretID = "caret-" + (new Date()).getTime();
var html = htmlFromText(text, self);
pasteHtmlAtCaret(html);
pasteHtmlAtCaret('<i id="' + caretID +'"></i>');
editor.scrollTop(editorScrollTop);
var caret = $("#" + caretID),
top = caret.offset().top - editor.offset().top,
height = editor.height();
if (editorScrollTop + top >= height || editorScrollTop > top) {
editor.scrollTop(editorScrollTop + top - 2 * height/3);
}
caret.remove();
self.stayFocused = false;
calcButtonPosition.apply(self);
trigger(self, 'paste', [editor, text, html]);
};
if (event.originalEvent.clipboardData) {
var text = event.originalEvent.clipboardData.getData('text/plain');
pasteText(text);
if (event.preventDefault){
event.preventDefault();
} else {
event.stop();
}
event.returnValue = false;
event.stopPropagation();
return false;
}
self.stayFocused = true;
// insert invisible character for fix caret position
pasteHtmlAtCaret('<span>' + invisibleChar + '</span>');
var sel = saveSelection(editor[0]),
editorScrollTop = editor.scrollTop(),
clipboard = $("<div/>", {contenteditable: true})
.css({position: "fixed", left: "-999px", width: "1px", height: "1px", top: "20px", overflow: "hidden"})
.appendTo($("BODY"))
.focus();
window.setTimeout(function() {
editor.focus();
restoreSelection(editor[0], sel);
var text = textFromHtml(clipboard.html().replace(/\r\n|\n|\r/g, '<br>'), self);
clipboard.remove();
pasteText(text);
}, 200);
})
.on("@emojibtn.click", function(emojibtn) {
editor.removeClass("has-placeholder");
if (self.searchSel !== null) {
editor.focus();
restoreSelection(editor[0], self.searchSel);
self.searchSel = null;
}
if (self.standalone) {
editor.html(shortnameTo(emojibtn.data("name"), self.emojiTemplate));
self.trigger("blur");
} else {
saveSelection(editor[0]);
pasteHtmlAtCaret(shortnameTo(emojibtn.data("name"), self.emojiTemplate));
}
if (self.recentEmojis) {
setRecent(self, emojibtn.data("name"));
}
// self.search.val('').trigger("change");
self.trigger('search.keypress');
})
.on("@!resize @keyup @emojibtn.click", calcButtonPosition)
.on("@!mousedown", function(editor, event) {
if ($(event.target).hasClass('search')) {
// Allow search clicks
self.stayFocused = true;
if (self.searchSel === null) {
self.searchSel = saveSelection(editor[0]);
}
} else {
if (!app.is(".focused")) {
editor.trigger("focus");
}
event.preventDefault();
}
return false;
})
.on("@change", function() {
var html = self.editor.html().replace(/<\/?(?:div|span|p)[^>]*>/ig, '');
// clear input: chrome adds <br> when contenteditable is empty
if (!html.length || /^<br[^>]*>$/i.test(html)) {
self.editor.html(self.content = '');
}
source[sourceValFunc](self.getText());
})
.on("@source.change", function() {
self.setText(source[sourceValFunc]());
trigger('change');
})
.on("@focus", function() {
app.addClass("focused");
})
.on("@blur", function() {
app.removeClass("focused");
if (options.hidePickerOnBlur) {
self.hidePicker();
}
var content = self.editor.html();
if (self.content !== content) {
self.content = content;
trigger(self, 'change', [self.editor]);
source.trigger("blur").trigger("change");
} else {
source.trigger("blur");
}
if (options.search) {
self.search.val('');
self.trigger('search.keypress', true);
}
});
if (options.search) {
self.on("@search.focus", function() {
self.stayFocused = true;
self.search.addClass("focused");
})
.on("@search.keypress", function(hide) {
var filterBtns = picker.find(".emojionearea-filter");
var activeTone = (options.tones ? tones.find("i.active").data("skin") : 0);
var term = self.search.val().replace( / /g, "_" ).replace(/"/g, "\\\"");
if (term && term.length) {
if (self.recentFilter.hasClass("active")) {
self.recentFilter.removeClass("active").next().addClass("active");
}
self.recentCategory.hide();
self.recentFilter.hide();
categoryBlocks.each(function() {
var matchEmojis = function(category, activeTone) {
var $matched = category.find('.emojibtn[data-name*="' + term + '"]');
if ($matched.length === 0) {
if (category.data('tone') === activeTone) {
category.hide();
}
filterBtns.filter('[data-filter="' + category.attr('name') + '"]').hide();
} else {
var $notMatched = category.find('.emojibtn:not([data-name*="' + term + '"])');
$notMatched.hide();
$matched.show();
if (category.data('tone') === activeTone) {
category.show();
}
filterBtns.filter('[data-filter="' + category.attr('name') + '"]').show();
}
}
var $block = $(this);
if ($block.data('tone') === 0) {
categories.filter(':not([name="recent"])').each(function() {
matchEmojis($(this), 0);
})
} else {
matchEmojis($block, activeTone);
}
});
if (!noListenScroll) {
scrollArea.trigger('scroll');
} else {
lazyLoading.call(self);
}
} else {
updateRecent(self, true);
categoryBlocks.filter('[data-tone="' + tones.find("i.active").data("skin") + '"]:not([name="recent"])').show();
$('.emojibtn', categoryBlocks).show();
filterBtns.show();
lazyLoading.call(self);
}
})
.on("@search.blur", function() {
self.stayFocused = false;
self.search.removeClass("focused");
self.trigger("blur");
});
}
if (options.shortcuts) {
self.on("@keydown", function(_, e) {
if (!e.ctrlKey) {
if (e.which == 9) {
e.preventDefault();
button.click();
}
else if (e.which == 27) {
e.preventDefault();
if (button.is(".active")) {
self.hidePicker();
}
}
}
});
}
if (isObject(options.events) && !$.isEmptyObject(options.events)) {
$.each(options.events, function(event, handler) {
self.on(event.replace(/_/g, '.'), handler);
});
}
if (options.autocomplete) {
var autocomplete = function() {
var textcompleteOptions = {
maxCount: options.textcomplete.maxCount,
placement: options.textcomplete.placement
};
if (options.shortcuts) {
textcompleteOptions.onKeydown = function (e, commands) {
if (!e.ctrlKey && e.which == 13) {
return commands.KEY_ENTER;
}
};
}
var map = $.map(emojione.emojioneList, function (_, emoji) {
return !options.autocompleteTones ? /_tone[12345]/.test(emoji) ? null : emoji : emoji;
});
map.sort();
editor.textcomplete([
{
id: css_class,
match: /\B(:[\-+\w]*)$/,
search: function (term, callback) {
callback($.map(map, function (emoji) {
return emoji.indexOf(term) === 0 ? emoji : null;
}));
},
template: function (value) {
return shortnameTo(value, self.emojiTemplate) + " " + value.replace(/:/g, '');
},
replace: function (value) {
return shortnameTo(value, self.emojiTemplate);
},
cache: true,
index: 1
}
], textcompleteOptions);
if (options.textcomplete.placement) {
// Enable correct positioning for textcomplete
if ($(editor.data('textComplete').option.appendTo).css("position") == "static") {
$(editor.data('textComplete').option.appendTo).css("position", "relative");
}
}
};
var initAutocomplete = function() {
if (self.disabled) {
var enable = function () {
self.off('enabled', enable);
autocomplete();
};
self.on('enabled', enable);
} else {
autocomplete();
}
}
if ($.fn.textcomplete) {
initAutocomplete();
} else {
$.ajax({
url: "https://cdn.rawgit.com/yuku-t/jquery-textcomplete/v1.3.4/dist/jquery.textcomplete.js",
dataType: "script",
cache: true,
success: initAutocomplete
});
}
}
if (self.inline) {
app.addClass(selector('inline', true));
self.on("@keydown", function(_, e) {
if (e.which == 13) {
e.preventDefault();
}
});
}
if (/firefox/i.test(navigator.userAgent)) {
// disabling resize images on Firefox
document.execCommand("enableObjectResizing", false, false);
}
self.isReady = true;
self.trigger("onLoad", editor);
self.trigger("ready", editor);
//}, self.id === 1); // calcElapsedTime()
};
});