FEATURE: Introduces new emoji-picker
This commit is contained in:
parent
87a1ff15fd
commit
6de258d4cf
|
@ -69,8 +69,6 @@
|
|||
//= require ./discourse/components/notifications-button
|
||||
//= require ./discourse/lib/link-mentions
|
||||
//= require ./discourse/components/site-header
|
||||
//= require ./discourse/lib/emoji/groups
|
||||
//= require ./discourse/lib/emoji/toolbar
|
||||
//= require ./discourse/components/d-editor
|
||||
//= require ./discourse/lib/screen-track
|
||||
//= require ./discourse/routes/discourse
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*global Mousetrap:true */
|
||||
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import { showSelector } from "discourse/lib/emoji/toolbar";
|
||||
import Category from 'discourse/models/category';
|
||||
import { categoryHashtagTriggerRule } from 'discourse/lib/category-hashtags';
|
||||
import { TAG_HASHTAG_POSTFIX } from 'discourse/lib/tag-hashtags';
|
||||
|
@ -346,20 +345,8 @@ export default Ember.Component.extend({
|
|||
if (v.code) {
|
||||
return `${v.code}:`;
|
||||
} else {
|
||||
showSelector({
|
||||
appendTo: self.$(),
|
||||
register,
|
||||
onSelect: title => {
|
||||
// Remove the previously type characters when a new emoji is selected from the selector.
|
||||
let selected = self._getSelected();
|
||||
let newPre = selected.pre.replace(/:[^:]+$/, ":");
|
||||
let numOfRemovedChars = selected.pre.length - newPre.length;
|
||||
selected.pre = newPre;
|
||||
selected.start -= numOfRemovedChars;
|
||||
selected.end -= numOfRemovedChars;
|
||||
self._addText(selected, `${title}:`);
|
||||
}
|
||||
});
|
||||
$editorInput.autocomplete({cancel: true});
|
||||
self.set('emojiPickerIsActive', true);
|
||||
return "";
|
||||
}
|
||||
},
|
||||
|
@ -614,6 +601,21 @@ export default Ember.Component.extend({
|
|||
},
|
||||
|
||||
actions: {
|
||||
emojiSelected(code) {
|
||||
let selected = this._getSelected();
|
||||
const captures = selected.pre.match(/\B:(\w*)$/);
|
||||
|
||||
if(_.isEmpty(captures)) {
|
||||
this._addText(selected, `:${code}:`);
|
||||
} else {
|
||||
let numOfRemovedChars = selected.pre.length - captures[1].length;
|
||||
selected.pre = selected.pre.slice(0, selected.pre.length - captures[1].length);
|
||||
selected.start -= numOfRemovedChars;
|
||||
selected.end -= numOfRemovedChars;
|
||||
this._addText(selected, `${code}:`);
|
||||
}
|
||||
},
|
||||
|
||||
toolbarButton(button) {
|
||||
const selected = this._getSelected(button.trimLeading);
|
||||
const toolbarEvent = {
|
||||
|
@ -692,11 +694,7 @@ export default Ember.Component.extend({
|
|||
},
|
||||
|
||||
emoji() {
|
||||
showSelector({
|
||||
appendTo: this.$(),
|
||||
register: this.register,
|
||||
onSelect: title => this._addText(this._getSelected(), `:${title}:`)
|
||||
});
|
||||
this.set('emojiPickerIsActive', !this.get('emojiPickerIsActive'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,448 @@
|
|||
import { observes } from "ember-addons/ember-computed-decorators";
|
||||
import { findRawTemplate } from "discourse/lib/raw-templates";
|
||||
import { emojiUrlFor } from "discourse/lib/text";
|
||||
import KeyValueStore from "discourse/lib/key-value-store";
|
||||
import { emojis } from "pretty-text/emoji/data";
|
||||
import { extendedEmojiList, isSkinTonableEmoji } from "pretty-text/emoji";
|
||||
|
||||
const recentTemplate = findRawTemplate("emoji-picker-recent");
|
||||
const pickerTemplate = findRawTemplate("emoji-picker");
|
||||
export const keyValueStore = new KeyValueStore("discourse_emojis_");
|
||||
export const EMOJI_USAGE = "emojiUsage";
|
||||
export const EMOJI_SCROLL_Y = "emojiScrollY";
|
||||
export const EMOJI_SELECTED_DIVERSITY = "emojiSelectedDiversity";
|
||||
const PER_ROW = 11;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
customEmojis: _.map(_.keys(extendedEmojiList()), function(code) {
|
||||
return { code, src: emojiUrlFor(code) };
|
||||
}),
|
||||
|
||||
$picker: Ember.computed("active", function() {
|
||||
return this.$(".emoji-picker");
|
||||
}),
|
||||
|
||||
$filter: Ember.computed("$picker", function() {
|
||||
return this.get("$picker").find(".filter");
|
||||
}),
|
||||
|
||||
$results: Ember.computed("$picker", function() {
|
||||
return this.get("$picker").find(".results");
|
||||
}),
|
||||
|
||||
$list: Ember.computed("$picker", function() {
|
||||
return this.get("$picker").find(".list");
|
||||
}),
|
||||
|
||||
willDestroyElement() {
|
||||
this._super();
|
||||
this._unbindEvents();
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
|
||||
if (!keyValueStore.getObject(EMOJI_USAGE)) {
|
||||
keyValueStore.setObject({ key: EMOJI_USAGE, value: {} });
|
||||
}
|
||||
|
||||
this.set("selectedDiversity", keyValueStore.getObject(EMOJI_SELECTED_DIVERSITY) || 1);
|
||||
},
|
||||
|
||||
didUpdateAttrs() {
|
||||
this._super();
|
||||
|
||||
if (this.get("active")) {
|
||||
this.show();
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
|
||||
@observes("filter")
|
||||
filterChanged() {
|
||||
this.get("$filter").find(".clear-filter").toggle(!_.isEmpty(this.get("filter")));
|
||||
Ember.run.debounce(this, this._filterEmojisList, 250);
|
||||
},
|
||||
|
||||
@observes("selectedDiversity")
|
||||
selectedDiversityChanged() {
|
||||
keyValueStore.setObject({key: EMOJI_SELECTED_DIVERSITY, value: this.get("selectedDiversity")});
|
||||
|
||||
$.each(this.get("$list").find(".emoji.diversity[src!='']"), (_, icon) => {
|
||||
this._updateIconSrc(icon);
|
||||
});
|
||||
|
||||
if(this.get("filter") !== "") {
|
||||
$.each(this.get("$results").find(".emoji.diversity"), (_, icon) => {
|
||||
this._updateIconSrc(icon);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@observes("recentEmojis")
|
||||
recentEmojisChanged() {
|
||||
const $recentSection = this.get("$list").find(".section[data-section='recent']");
|
||||
const $recentSectionGroup = $recentSection.find(".section-group");
|
||||
const $recentCategory = this.get("$picker").find(".category-icon a[title='recent']").parent();
|
||||
if(_.isEmpty(this.get("recentEmojis"))) {
|
||||
$recentCategory.hide();
|
||||
$recentSection.css("height", 0).hide();
|
||||
} else {
|
||||
$recentCategory.show();
|
||||
$recentSection.css("height", "auto").show();
|
||||
}
|
||||
|
||||
const recentEmojis = _.map(this.get("recentEmojis"), function(emoji) {
|
||||
return { code: emoji.title, src: emojiUrlFor(emoji.title) };
|
||||
});
|
||||
const model = { recentEmojis };
|
||||
const template = recentTemplate(model);
|
||||
$recentSectionGroup.html(template);
|
||||
this._bindHover($recentSectionGroup.find("a"));
|
||||
},
|
||||
|
||||
close() {
|
||||
this.get("$picker")
|
||||
.css({width: "", left: "", bottom: ""})
|
||||
.empty();
|
||||
this.$(".emoji-picker-modal").removeClass("fadeIn");
|
||||
|
||||
this._unbindEvents();
|
||||
},
|
||||
|
||||
show() {
|
||||
const model = { customEmojis: this.get("customEmojis") };
|
||||
const template = pickerTemplate(model);
|
||||
this.get("$picker").html(template);
|
||||
|
||||
this._bindEvents();
|
||||
|
||||
Ember.run.later(this, function() {
|
||||
this._setDiversity();
|
||||
this._positionPicker();
|
||||
this._scrollTo();
|
||||
this.recentEmojisChanged();
|
||||
});
|
||||
},
|
||||
|
||||
_bindEvents() {
|
||||
this._bindDiversityClick();
|
||||
this._bindSectionsScroll();
|
||||
this._bindEmojiClick();
|
||||
this._bindClearRecentEmojisGroup();
|
||||
this._bindResizing();
|
||||
this._bindHover();
|
||||
this._bindCategoryClick();
|
||||
this._bindModalClick();
|
||||
this._bindFilterInput();
|
||||
this._bindEscape();
|
||||
},
|
||||
|
||||
_bindEscape() {
|
||||
this.$().on("keydown", e => {
|
||||
if (e.which === 27) {
|
||||
this.set("active", false);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_bindModalClick() {
|
||||
this.$(".emoji-picker-modal").on("click", () => {
|
||||
this.set("active", false);
|
||||
});
|
||||
},
|
||||
|
||||
_unbindEvents() {
|
||||
this.$(window).off("resize");
|
||||
this.$(".emoji-picker-modal").off("click");
|
||||
Ember.$("#reply-control").off("div-resized");
|
||||
this.$().off("keydown");
|
||||
},
|
||||
|
||||
_filterEmojisList() {
|
||||
const $filter = this.get("$picker").find(".filter");
|
||||
|
||||
if (this.get("filter") === "") {
|
||||
$filter.find("input[name='filter']").val("");
|
||||
this.get("$results").empty().hide();
|
||||
this.get("$list").show();
|
||||
} else {
|
||||
const regexp = new RegExp(this.get("filter"), "g");
|
||||
const filteredCodes = _.filter(emojis, code => regexp.test(code)).slice(0, 30);
|
||||
this.get("$results").empty().html(
|
||||
_.map(filteredCodes, (code) => {
|
||||
const hasDiversity = isSkinTonableEmoji(code);
|
||||
const diversity = hasDiversity ? "diversity" : "";
|
||||
const scaledCode = this._codeWithDiversity(code, hasDiversity);
|
||||
return `<a title="${code}">
|
||||
<img src="${emojiUrlFor(scaledCode)}" data-code="${code}" class="emoji ${diversity}" />
|
||||
</a>`;
|
||||
})
|
||||
).show();
|
||||
this._bindHover(this.get("$results").find("a"));
|
||||
this._bindEmojiClick(this.get("$results"));
|
||||
this.get("$list").hide();
|
||||
}
|
||||
},
|
||||
|
||||
_bindFilterInput() {
|
||||
const $filter = this.get("$picker").find(".filter");
|
||||
const $input = $filter.find("input");
|
||||
|
||||
$input.on("input", (event) => {
|
||||
this.set("filter", event.currentTarget.value);
|
||||
});
|
||||
|
||||
$filter.find(".clear-filter").on("click", () => {
|
||||
$input.val("").focus();
|
||||
this.set("filter", "");
|
||||
return false;
|
||||
});
|
||||
},
|
||||
|
||||
_bindCategoryClick() {
|
||||
this.get("$picker").find(".category-icon").on("click", "a", (event) => {
|
||||
this.set("filter", "");
|
||||
this.get("$results").empty();
|
||||
this.get("$list").show();
|
||||
|
||||
const section = $(event.currentTarget).attr("title");
|
||||
const $section = this.get("$list").find(`.section[data-section="${section}"]`);
|
||||
const scrollTop = this.get("$list").scrollTop() +
|
||||
( $section.offset().top - this.get("$list").offset().top );
|
||||
|
||||
this._scrollTo(scrollTop);
|
||||
return false;
|
||||
});
|
||||
},
|
||||
|
||||
_bindHover(hoverables) {
|
||||
const replaceInfoContent = (html) => {
|
||||
this.get("$picker").find(".footer .info").html(html || "");
|
||||
}
|
||||
|
||||
(hoverables || this.$(".section-group a")).hover(event => {
|
||||
const $a = $(event.currentTarget);
|
||||
const code = this._codeWithDiversity($a.attr("title"), $a.find("img").hasClass("diversity"));
|
||||
const html = `<img src="${emojiUrlFor(code)}" class="emoji"> <span>:${code}:<span>`;
|
||||
replaceInfoContent(html);
|
||||
},
|
||||
() => replaceInfoContent()
|
||||
);
|
||||
},
|
||||
|
||||
_bindResizing() {
|
||||
this.$(window).on("resize", () => {
|
||||
Ember.run.debounce(this, this._positionPicker, 100);
|
||||
});
|
||||
|
||||
Ember.$("#reply-control").on("div-resized", () => {
|
||||
Ember.run.debounce(this, this._positionPicker, 100);
|
||||
});
|
||||
},
|
||||
|
||||
_bindClearRecentEmojisGroup() {
|
||||
const $recent = this.get("$picker").find(".section[data-section='recent'] .clear-recent");
|
||||
$recent.on("click", () => {
|
||||
keyValueStore.setObject({ key: EMOJI_USAGE, value: {} });
|
||||
this.set("recentEmojis", {});
|
||||
this._scrollTo(0);
|
||||
return false;
|
||||
});
|
||||
},
|
||||
|
||||
_bindEmojiClick(emojisContainer) {
|
||||
const $emojisContainer = emojisContainer || this.get("$list").find(".section-group");
|
||||
$emojisContainer.off("click").on("click", "a", e => {
|
||||
const $icon = $(e.currentTarget);
|
||||
const title = $icon.attr("title");
|
||||
const code = this._codeWithDiversity(title, $icon.find("img").hasClass("diversity"));
|
||||
|
||||
this._trackEmojiUsage(code);
|
||||
|
||||
if(this._isSmallViewport()) {
|
||||
this.set("active", false);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
},
|
||||
|
||||
_bindSectionsScroll() {
|
||||
this.get("$list").on("scroll", () => {
|
||||
Ember.run.debounce(this, this._checkVisibleSection, 150);
|
||||
Ember.run.debounce(this, this._storeScrollPosition, 50);
|
||||
});
|
||||
},
|
||||
|
||||
_checkVisibleSection() {
|
||||
const $sections = this.get("$list").find(".section");
|
||||
const sections = [];
|
||||
let cumulatedHeight = 0;
|
||||
|
||||
$.each($sections, (_, section) => {
|
||||
const $section = $(section);
|
||||
sections.push({$section, cumulatedHeight});
|
||||
cumulatedHeight += $section.innerHeight();
|
||||
});
|
||||
|
||||
let selectedSection;
|
||||
const currentScrollTop = this.get("$list").scrollTop()
|
||||
if (!_.isEmpty(this.get("recentEmojis")) && currentScrollTop === 0) {
|
||||
selectedSection = _.first(sections);
|
||||
} else if (!_.isEmpty(this.get("customEmojis")) &&
|
||||
currentScrollTop === this.get("$list")[0].scrollHeight - this.get("$list").innerHeight())
|
||||
{
|
||||
selectedSection = _.last(sections);
|
||||
} else {
|
||||
selectedSection = _.last(_.reject(sections, (section) => {
|
||||
return section.cumulatedHeight > currentScrollTop;
|
||||
}));
|
||||
}
|
||||
|
||||
if(selectedSection) {
|
||||
this.get("$picker").find(".category-icon").removeClass("current");
|
||||
this.get("$picker").find(`.category-icon a[title='${selectedSection.$section.data("section")}']`)
|
||||
.parent()
|
||||
.addClass("current");
|
||||
|
||||
if(!selectedSection.$section.hasClass("loaded")) {
|
||||
selectedSection.$section.addClass("loaded");
|
||||
this._loadVisibleEmojis(selectedSection.$section.find(".emoji[src='']"));
|
||||
}
|
||||
|
||||
//preload surrounding sections
|
||||
const selectedSectionIndex = sections.indexOf(selectedSection);
|
||||
const preloadedSection = sections[selectedSectionIndex + 1] || sections[selectedSectionIndex - 1];
|
||||
if(preloadedSection && !preloadedSection.$section.hasClass("loaded")) {
|
||||
preloadedSection.$section.addClass("loaded");
|
||||
const $visibleEmojis = preloadedSection.$section.find(".emoji[src='']");
|
||||
Ember.run.later(() => { this._loadVisibleEmojis($visibleEmojis) }, 1500);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_bindDiversityClick() {
|
||||
const $diversityScales = this.get("$picker").find(".diversity-picker .diversity-scale");
|
||||
$diversityScales.on("click", (event) => {
|
||||
const $selectedDiversity = $(event.currentTarget);
|
||||
$diversityScales.removeClass("selected");
|
||||
$selectedDiversity.addClass("selected");
|
||||
this.set("selectedDiversity", parseInt($selectedDiversity.data("level")));
|
||||
return false;
|
||||
});
|
||||
},
|
||||
|
||||
_setDiversity() {
|
||||
this.get("$picker")
|
||||
.find(`.diversity-picker .diversity-scale[data-level="${this.get("selectedDiversity")}"]`)
|
||||
.addClass("selected");
|
||||
},
|
||||
|
||||
_isSmallViewport() {
|
||||
return this.site.isMobileDevice || this.$(window).width() <= 1024 || this.$(window).height() <= 768;
|
||||
},
|
||||
|
||||
_positionPicker(){
|
||||
if(!this.get("active")) { return; }
|
||||
|
||||
let isLargePreview = this.$(window).height() -
|
||||
Ember.$(".d-header").height() -
|
||||
Ember.$("#reply-control").height() <
|
||||
this.get("$picker").height() + 16;
|
||||
|
||||
if(this._isSmallViewport()) {
|
||||
this.$(".emoji-picker-modal").addClass("fadeIn");
|
||||
this.get("$picker").css({
|
||||
width: this.site.isMobileDevice ? this.$(window).width() - 10 : 340,
|
||||
marginLeft: this.site.isMobileDevice ? -(this.$(window).width() - 10)/2 : -170,
|
||||
marginTop: -150,
|
||||
left: "50%",
|
||||
top: "50%"
|
||||
})
|
||||
} else {
|
||||
this.$(".emoji-picker-modal").removeClass("fadeIn");
|
||||
|
||||
let cssAttributes = { width: 400, marginLeft: "", marginTop: "", left: "", top: "" };
|
||||
if(isLargePreview) {
|
||||
cssAttributes.left = (Ember.$("#reply-control").width() - Ember.$(".d-editor").width() ) / 2 + Ember.$(".d-editor-preview-wrapper").position().left;
|
||||
cssAttributes.bottom = 32;
|
||||
} else {
|
||||
cssAttributes.left = (Ember.$("#reply-control").width() - Ember.$(".d-editor").width() ) / 2 + Ember.$(".d-editor").position().left;
|
||||
cssAttributes.bottom = Ember.$("#reply-control").height() - 48;
|
||||
}
|
||||
|
||||
this.get("$picker").css(cssAttributes);
|
||||
}
|
||||
|
||||
const infoMaxWidth = this.get("$picker").width() -
|
||||
this.get("$picker").find(".categories-column").width() -
|
||||
this.get("$picker").find(".diversity-picker").width() -
|
||||
32;
|
||||
this.get("$picker").find(".info").css("max-width", infoMaxWidth);
|
||||
},
|
||||
|
||||
_loadVisibleEmojis($visibleEmojis) {
|
||||
$.each($visibleEmojis, (_, icon) => {
|
||||
const $icon = $(icon);
|
||||
const code = this._codeWithDiversity($icon.parents("a").attr("title"), $icon.hasClass("diversity"))
|
||||
$icon.attr("src", emojiUrlFor(code));
|
||||
});
|
||||
},
|
||||
|
||||
_codeWithDiversity(code, diversity) {
|
||||
if(diversity && this.get("selectedDiversity") !== 1) {
|
||||
return `${code}:t${this.get("selectedDiversity")}`;
|
||||
} else {
|
||||
return code;
|
||||
}
|
||||
},
|
||||
|
||||
_storeScrollPosition() {
|
||||
keyValueStore.setObject({
|
||||
key: EMOJI_SCROLL_Y,
|
||||
value: this.get("$list").scrollTop()
|
||||
});
|
||||
},
|
||||
|
||||
_trackEmojiUsage(code) {
|
||||
const recent = keyValueStore.getObject(EMOJI_USAGE) || {};
|
||||
|
||||
if (!recent[code]) {
|
||||
// keeping title here for legacy reasons, might migrate later
|
||||
recent[code] = { title: code, usage: 0 };
|
||||
}
|
||||
recent[code]["usage"]++;
|
||||
|
||||
keyValueStore.setObject({ key: EMOJI_USAGE, value: recent });
|
||||
|
||||
this.set("recentEmojis", _.map(recent).sort(this._sortByUsage).slice(0, PER_ROW));
|
||||
|
||||
this.sendAction("emojiSelected", code);
|
||||
},
|
||||
|
||||
_sortByUsage(a, b) {
|
||||
if (a.usage > b.usage) { return -1; }
|
||||
if (b.usage > a.usage) { return 1; }
|
||||
return a.title.localeCompare(b.title);
|
||||
},
|
||||
|
||||
_scrollTo(y) {
|
||||
const yPosition = _.isUndefined(y) ? keyValueStore.getObject(EMOJI_SCROLL_Y) : y;
|
||||
|
||||
this.get("$list").scrollTop(yPosition);
|
||||
|
||||
// if we don’t actually scroll we need to force it
|
||||
if(yPosition === 0) {
|
||||
this.get("$list").scroll();
|
||||
}
|
||||
},
|
||||
|
||||
_updateIconSrc(icon) {
|
||||
const $icon = $(icon);
|
||||
const code = this._codeWithDiversity($icon.parents("a").attr("title"), true)
|
||||
$icon.attr("src", emojiUrlFor(code));
|
||||
},
|
||||
});
|
File diff suppressed because it is too large
Load Diff
|
@ -1,208 +0,0 @@
|
|||
import groups from 'discourse/lib/emoji/groups';
|
||||
import KeyValueStore from "discourse/lib/key-value-store";
|
||||
import { emojiList, isSkinTonableEmoji } from 'pretty-text/emoji';
|
||||
import { emojiUrlFor } from 'discourse/lib/text';
|
||||
import { findRawTemplate } from 'discourse/lib/raw-templates';
|
||||
|
||||
const keyValueStore = new KeyValueStore("discourse_emojis_");
|
||||
const EMOJI_USAGE = "emojiUsage";
|
||||
|
||||
let PER_ROW = 12;
|
||||
const PER_PAGE = 60;
|
||||
|
||||
let ungroupedIcons, recentlyUsedIcons;
|
||||
let selectedSkinTone = keyValueStore.getObject('selectedSkinTone') || 1;
|
||||
|
||||
if (!keyValueStore.getObject(EMOJI_USAGE)) {
|
||||
keyValueStore.setObject({key: EMOJI_USAGE, value: {}});
|
||||
}
|
||||
|
||||
function closeSelector() {
|
||||
$('.emoji-modal, .emoji-modal-wrapper').remove();
|
||||
$('body, textarea').off('keydown.emoji');
|
||||
}
|
||||
|
||||
function initializeUngroupedIcons() {
|
||||
const groupedIcons = {};
|
||||
|
||||
groups.forEach(group => {
|
||||
group.icons.forEach(icon => groupedIcons[icon] = true);
|
||||
});
|
||||
|
||||
ungroupedIcons = [];
|
||||
const emojis = emojiList();
|
||||
emojis.forEach(emoji => {
|
||||
if (groupedIcons[emoji] !== true) {
|
||||
ungroupedIcons.push(emoji);
|
||||
}
|
||||
});
|
||||
|
||||
if (ungroupedIcons.length) {
|
||||
groups.push({name: 'ungrouped', icons: ungroupedIcons});
|
||||
}
|
||||
}
|
||||
|
||||
function trackEmojiUsage(title) {
|
||||
const recent = keyValueStore.getObject(EMOJI_USAGE) || {};
|
||||
|
||||
if (!recent[title]) { recent[title] = { title: title, usage: 0 }; }
|
||||
recent[title]["usage"]++;
|
||||
|
||||
keyValueStore.setObject({key: EMOJI_USAGE, value: recent});
|
||||
|
||||
// clear the cache
|
||||
recentlyUsedIcons = null;
|
||||
}
|
||||
|
||||
function sortByUsage(a, b) {
|
||||
if (a.usage > b.usage) { return -1; }
|
||||
if (b.usage > a.usage) { return 1; }
|
||||
return a.title.localeCompare(b.title);
|
||||
}
|
||||
|
||||
function initializeRecentlyUsedIcons() {
|
||||
recentlyUsedIcons = [];
|
||||
|
||||
const usage = _.map(keyValueStore.getObject(EMOJI_USAGE)).sort(sortByUsage);
|
||||
const recent = usage.slice(0, PER_ROW);
|
||||
|
||||
if (recent.length > 0) {
|
||||
|
||||
recent.forEach(emoji => recentlyUsedIcons.push(emoji.title));
|
||||
|
||||
const recentGroup = groups.findBy('name', 'recent');
|
||||
if (recentGroup) {
|
||||
recentGroup.icons = recentlyUsedIcons;
|
||||
} else {
|
||||
groups.push({ name: 'recent', icons: recentlyUsedIcons });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toolbar(selected) {
|
||||
if (!ungroupedIcons) { initializeUngroupedIcons(); }
|
||||
if (!recentlyUsedIcons) { initializeRecentlyUsedIcons(); }
|
||||
|
||||
return groups.map((g, i) => {
|
||||
let icon = g.tabicon;
|
||||
let title = g.fullname;
|
||||
if (g.name === "recent") {
|
||||
icon = "star";
|
||||
title = "Recent";
|
||||
} else if (g.name === "ungrouped") {
|
||||
icon = g.icons[0];
|
||||
title = "Custom";
|
||||
}
|
||||
|
||||
return { src: emojiUrlFor(icon),
|
||||
title,
|
||||
groupId: i,
|
||||
selected: i === selected };
|
||||
});
|
||||
}
|
||||
|
||||
function bindEvents(page, offset, options) {
|
||||
$('.emoji-page a').click(e => {
|
||||
const title = $(e.currentTarget).attr('title');
|
||||
trackEmojiUsage(title);
|
||||
options.onSelect(title);
|
||||
closeSelector();
|
||||
return false;
|
||||
}).hover(e => {
|
||||
const title = $(e.currentTarget).attr('title');
|
||||
const html = "<img src='" + emojiUrlFor(title) + "' class='emoji'> <span>:" + title + ":<span>";
|
||||
$('.emoji-modal .info').html(html);
|
||||
}, () => $('.emoji-modal .info').html(""));
|
||||
|
||||
$('.emoji-modal .nav .next a').click(() => render(page, offset+PER_PAGE, options));
|
||||
$('.emoji-modal .nav .prev a').click(() => render(page, offset-PER_PAGE, options));
|
||||
|
||||
$('.emoji-modal .toolbar a').click(function(){
|
||||
const p = parseInt($(this).data('group-id'));
|
||||
render(p, 0, options);
|
||||
return false;
|
||||
});
|
||||
|
||||
$('.emoji-modal .tones-button').click(function(){
|
||||
selectedSkinTone = parseInt($(this).data('skin-tone'));
|
||||
keyValueStore.setObject({key: 'selectedSkinTone', value: selectedSkinTone});
|
||||
render(page, offset, options);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function render(page, offset, options) {
|
||||
keyValueStore.set({key: "emojiPage", value: page});
|
||||
keyValueStore.set({key: "emojiOffset", value: offset});
|
||||
|
||||
const toolbarItems = toolbar(page);
|
||||
const rows = [];
|
||||
let row = [];
|
||||
const icons = groups[page].icons;
|
||||
const max = offset + PER_PAGE;
|
||||
|
||||
for(let i=offset; i<max; i++){
|
||||
if(!icons[i]){ break; }
|
||||
if(row.length === (options.perRow || PER_ROW)){
|
||||
rows.push(row);
|
||||
row = [];
|
||||
}
|
||||
|
||||
let code = icons[i];
|
||||
if(selectedSkinTone !== 1 && isSkinTonableEmoji(code)) {
|
||||
code = `${code}:t${selectedSkinTone}`;
|
||||
}
|
||||
|
||||
row.push({src: emojiUrlFor(code), title: code});
|
||||
}
|
||||
rows.push(row);
|
||||
|
||||
const skinTones = [];
|
||||
const skinToneNames = ['default', 'light', 'medium-light', 'medium', 'medium-dark', 'dark'];
|
||||
for(let i=1; i<skinToneNames.length+1; i++){
|
||||
skinTones.push({
|
||||
selected: selectedSkinTone === i,
|
||||
level: i,
|
||||
className: skinToneNames[i-1]
|
||||
});
|
||||
}
|
||||
|
||||
const model = {
|
||||
toolbarItems,
|
||||
skinTones,
|
||||
rows,
|
||||
prevDisabled: offset === 0,
|
||||
nextDisabled: (max + 1) > icons.length,
|
||||
modalClass: options.modalClass
|
||||
};
|
||||
|
||||
$('.emoji-modal', options.appendTo).remove();
|
||||
const template = findRawTemplate('emoji-toolbar');
|
||||
options.appendTo.append(template(model));
|
||||
|
||||
bindEvents(page, offset, options);
|
||||
}
|
||||
|
||||
function showSelector(options) {
|
||||
options = options || {};
|
||||
options.appendTo = options.appendTo || $('body');
|
||||
|
||||
options.appendTo.append('<div class="emoji-modal-wrapper"></div>');
|
||||
$('.emoji-modal-wrapper').click(() => closeSelector());
|
||||
|
||||
if (Discourse.Site.currentProp('mobileView')) { PER_ROW = 9; }
|
||||
const page = options.page ? _.findIndex(groups, (g) => { return g.name === options.page; })
|
||||
: keyValueStore.getInt("emojiPage", 0);
|
||||
const offset = keyValueStore.getInt("emojiOffset", 0);
|
||||
|
||||
render(page, offset, options);
|
||||
|
||||
$('body, textarea').on('keydown.emoji', e => {
|
||||
if (e.which === 27) {
|
||||
closeSelector();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { showSelector };
|
|
@ -33,3 +33,5 @@
|
|||
{{plugin-outlet name="editor-preview"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{emoji-picker active=emojiPickerIsActive emojiSelected=(action 'emojiSelected')}}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
<div class='emoji-picker'></div>
|
||||
<div class='emoji-picker-modal'></div>
|
|
@ -0,0 +1,5 @@
|
|||
{{#each recentEmojis as |emoji|}}
|
||||
<a title='{{emoji.code}}'>
|
||||
<img src='{{emoji.src}}' class='emoji'>
|
||||
</a>
|
||||
{{/each}}
|
|
@ -0,0 +1,88 @@
|
|||
<div class='categories-column'>
|
||||
<div class='category-icon'>
|
||||
<a href='#' title='recent'>
|
||||
<img src='<%= Emoji.url_for("star") %>' class='emoji'>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<% JSON.parse(File.read("lib/emoji/groups.json")).each.with_index do |group, group_index| %>
|
||||
<div class='category-icon'>
|
||||
<a href='#' title='<%= group["name"] %>'>
|
||||
<img src='<%= Emoji.url_for(group["tabicon"]) %>' class='emoji'>
|
||||
</a>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if !Emoji.custom.blank? %>
|
||||
<div class='category-icon'>
|
||||
<a href='#' title='ungrouped'>
|
||||
<img src='<%= Emoji.custom.first.url %>' class='emoji'>
|
||||
</a>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class='main-column'>
|
||||
<div class='filter'>
|
||||
<input type='text' name="filter" placeholder="{{i18n 'emoji_picker.filter_placeholder'}}" autocomplete="off" autofocus/>
|
||||
<button class='clear-filter'>
|
||||
{{fa-icon 'times'}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class='results'></div>
|
||||
|
||||
<div class='list'>
|
||||
<div class='section' data-section='recent'>
|
||||
<div class='section-header'>
|
||||
<span class="title">{{i18n 'emoji_picker.recent'}}</span>
|
||||
<a href='#' class='clear-recent btn btn-default'>{{fa-icon 'trash'}}</a>
|
||||
</div>
|
||||
<div class='section-group'></div>
|
||||
</div>
|
||||
|
||||
<% JSON.parse(File.read("lib/emoji/groups.json")).each.with_index do |group, group_index| %>
|
||||
<div class='section' data-section='<%= group["name"] %>'>
|
||||
<div class='section-header'>
|
||||
<span class="title">{{i18n 'emoji_picker.<%= group["name"] %>'}}</span>
|
||||
</div>
|
||||
<div class='section-group'>
|
||||
<% group["icons"].each.with_index do |icon, icon_index| %>
|
||||
<a title='<%= icon["name"] %>'>
|
||||
<% if group_index == 0 && icon_index <= 77 %>
|
||||
<img src='<%= Emoji.url_for(icon['name']) %>' class='emoji <%= "diversity" if icon["diversity"] %>'>
|
||||
<% else %>
|
||||
<img src='' class='emoji <%= "diversity" if icon["diversity"] %>'>
|
||||
<% end %>
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
{{#if customEmojis.length}}
|
||||
<div class='section' data-section='ungrouped'>
|
||||
<div class='section-header'>
|
||||
<span class="title">{{i18n 'emoji_picker.custom'}}</span>
|
||||
</div>
|
||||
<div class='section-group'>
|
||||
{{#each customEmojis as |emoji|}}
|
||||
<a title='{{emoji.code}}'>
|
||||
<img src='{{emoji.src}}' class='emoji'>
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class='footer'>
|
||||
<div class='info'></div>
|
||||
<div class='diversity-picker'>
|
||||
<% ['default', 'light', 'medium-light', 'medium', 'medium-dark', 'dark'].each.with_index do |diversity, index| %>
|
||||
<a href='#' class='diversity-scale <%= diversity %>' data-level="<%= index + 1 %>">
|
||||
{{fa-icon "check"}}
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,45 +0,0 @@
|
|||
<div class='emoji-modal {{modalClass}}'>
|
||||
<ul class='toolbar'>
|
||||
{{#each toolbarItems as |item|}}<li><a title='{{item.title}}' {{#if item.selected}}class='selected'{{/if}} data-group-id='{{item.groupId}}'><img src='{{item.src}}' class='emoji'></a></li>{{/each}}
|
||||
</ul>
|
||||
<div class='emoji-table-wrapper'>
|
||||
<table class='emoji-page'>
|
||||
{{#each rows as |row|}}
|
||||
<tr>
|
||||
{{#each row as |item|}}
|
||||
<td><a title='{{item.title}}'><img src='{{item.src}}' class='emoji'></a></td>
|
||||
{{/each}}
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class='footer'>
|
||||
<div class='info'></div>
|
||||
<div class='tones'>
|
||||
{{#each skinTones as |skinTone|}}
|
||||
<a href='#' class='tones-button {{skinTone.className}}' data-skin-tone="{{skinTone.level}}">
|
||||
{{#if skinTone.selected}}{{fa-icon "check"}}{{/if}}
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class='nav'>
|
||||
<span class='prev'>
|
||||
{{#if prevDisabled}}
|
||||
{{fa-icon "fast-backward"}}
|
||||
{{else}}
|
||||
<a>{{fa-icon "fast-backward"}}</a>
|
||||
{{/if}}
|
||||
</span>
|
||||
<span class='next'>
|
||||
{{#if nextDisabled}}
|
||||
{{fa-icon "fast-forward"}}
|
||||
{{else}}
|
||||
<a>{{fa-icon "fast-forward"}}</a>
|
||||
{{/if}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='clearfix'></div>
|
||||
</div>
|
|
@ -1,7 +1,7 @@
|
|||
import { emojis, aliases, translations, tonableEmojis } from 'pretty-text/emoji/data';
|
||||
|
||||
// bump up this number to expire all emojis
|
||||
export const IMAGE_VERSION = "5";
|
||||
export const IMAGE_VERSION = "<%= Emoji::EMOJI_VERSION %>";
|
||||
|
||||
const extendedEmoji = {};
|
||||
|
||||
|
@ -10,10 +10,8 @@ export function registerEmoji(code, url) {
|
|||
extendedEmoji[code] = url;
|
||||
}
|
||||
|
||||
export function emojiList() {
|
||||
const result = emojis.slice(0);
|
||||
_.each(extendedEmoji, (v,k) => result.push(k));
|
||||
return result;
|
||||
export function extendedEmojiList() {
|
||||
return extendedEmoji;
|
||||
}
|
||||
|
||||
const emojiHash = {};
|
||||
|
@ -54,8 +52,7 @@ export function isCustomEmoji(code, opts) {
|
|||
|
||||
export function buildEmojiUrl(code, opts) {
|
||||
let url;
|
||||
code = code.toLowerCase();
|
||||
|
||||
code = String(code).toLowerCase();
|
||||
if (extendedEmoji.hasOwnProperty(code)) {
|
||||
url = extendedEmoji[code];
|
||||
}
|
||||
|
@ -116,10 +113,9 @@ export function emojiSearch(term, options) {
|
|||
};
|
||||
|
||||
export function isSkinTonableEmoji(term) {
|
||||
let match = term.match(/^:?(.*?):?$/);
|
||||
const match = _.compact(term.split(":"))[0];
|
||||
if (match) {
|
||||
return tonableEmojis.indexOf(match[1]) !== -1;
|
||||
} else {
|
||||
return tonableEmojis.indexOf(term) !== -1;
|
||||
return tonableEmojis.indexOf(match) !== -1;
|
||||
}
|
||||
return false;
|
||||
}
|
|
@ -1,128 +1,115 @@
|
|||
body img.emoji {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.wmd-emoji-button:before {
|
||||
content: "\f118";
|
||||
}
|
||||
|
||||
.emoji-modal {
|
||||
.emoji-picker {
|
||||
background-color: #fff;
|
||||
border: 1px solid #e9e9e9;
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||
background-clip: padding-box;
|
||||
z-index: 10000;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 445px;
|
||||
min-height: 264px;
|
||||
margin-top: -132px;
|
||||
margin-left: -222px;
|
||||
background-color: dark-light-choose(#dadada, blend-primary-secondary(5%));
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 300px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.emoji-picker .categories-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: white;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-right: 1px solid #e9e9e9;
|
||||
min-width: 36px;
|
||||
}
|
||||
|
||||
.emoji-picker .category-icon {
|
||||
display: block;
|
||||
margin: 4px auto;
|
||||
-webkit-filter: grayscale(100%);
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.emoji-picker .category-icon.current, .emoji-picker .category-icon:hover {
|
||||
-webkit-filter: grayscale(0%);
|
||||
filter: grayscale(0%);
|
||||
}
|
||||
|
||||
.emoji-picker .main-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: white;
|
||||
flex: 20;
|
||||
}
|
||||
|
||||
.emoji-picker .list {
|
||||
overflow-y: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 0px;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
table.emoji-page td {
|
||||
border: 1px solid transparent;
|
||||
background-color: dark-light-choose(white, $secondary);
|
||||
padding: 0 !important;
|
||||
.emoji-picker .list .emoji {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.emoji-page a {
|
||||
.emoji-picker .section-header {
|
||||
padding: 8px;
|
||||
display: block;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 4px;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid #e9e9e9;
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.emoji-page a:hover {
|
||||
background-color: dark-light-choose(rgb(210, 236, 252), rgb(45, 19, 3));
|
||||
.emoji-picker .section-header .title {
|
||||
}
|
||||
|
||||
.emoji-table-wrapper {
|
||||
min-width: 442px;
|
||||
min-height: 185px;
|
||||
background-color: $secondary;
|
||||
.emoji-picker .section-header .clear-recent .fa{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.emoji-modal-wrapper {
|
||||
z-index: 9999;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: dark-light-choose(0.8, 0.5);
|
||||
background-color: black;
|
||||
.section-group {
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.emoji-modal .toolbar {
|
||||
margin: 8px 0 5px;
|
||||
}
|
||||
|
||||
.emoji-modal .toolbar li {
|
||||
display: inline;
|
||||
padding-right: 1px;
|
||||
}
|
||||
|
||||
.emoji-modal .toolbar li a {
|
||||
padding: 8px;
|
||||
background-color: dark-light-choose(#dadada, blend-primary-secondary(5%));
|
||||
}
|
||||
|
||||
.emoji-modal .toolbar li a.selected {
|
||||
background-color: $secondary;
|
||||
}
|
||||
|
||||
.emoji-modal .nav span {
|
||||
color: dark-light-choose(#aaa, #555);
|
||||
}
|
||||
|
||||
.emoji-modal .nav span.next {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.emoji-modal .nav a {
|
||||
color: dark-light-choose(#333, #ccc);
|
||||
}
|
||||
|
||||
.emoji-shortname {
|
||||
.section-group a:hover, .results a:hover {
|
||||
display: inline-block;
|
||||
max-width: 200px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
vertical-align: top;
|
||||
border-radius: 50%;
|
||||
background-color: #d1f0ff;
|
||||
}
|
||||
|
||||
.emoji-modal .footer {
|
||||
display: flex;
|
||||
.emoji-picker .footer {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
flex-grow: 2;
|
||||
border-top: 1px solid #e9e9e9;
|
||||
}
|
||||
|
||||
.emoji-picker .info {
|
||||
text-overflow: ellipsis;
|
||||
max-width: 232px;
|
||||
padding-left: 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
font-weight: 700;
|
||||
max-width: 125px;
|
||||
}
|
||||
|
||||
.emoji-picker .diversity-picker {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.emoji-modal .info {
|
||||
flex: 10;
|
||||
}
|
||||
|
||||
.emoji-modal .info span {
|
||||
margin-left: 5px;
|
||||
font-weight: bold;
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
.emoji-modal .nav {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.emoji-modal .tones {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.emoji-modal .tones-button {
|
||||
.emoji-picker .diversity-picker .diversity-scale {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 5px;
|
||||
|
@ -133,14 +120,88 @@ table.emoji-page td {
|
|||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.emoji-modal .tones-button.default { background: #ffcc4d; }
|
||||
.emoji-modal .tones-button.light { background: #f7dece; }
|
||||
.emoji-modal .tones-button.medium-light { background: #f3d2a2; }
|
||||
.emoji-modal .tones-button.medium { background: #d5ab88; }
|
||||
.emoji-modal .tones-button.medium-dark { background: #af7e57; }
|
||||
.emoji-modal .tones-button.dark { background: #7c533e; }
|
||||
.emoji-picker .diversity-picker .diversity-scale.default { background: #ffcc4d; }
|
||||
.emoji-picker .diversity-picker .diversity-scale.light { background: #f7dece; }
|
||||
.emoji-picker .diversity-picker .diversity-scale.medium-light { background: #f3d2a2; }
|
||||
.emoji-picker .diversity-picker .diversity-scale.medium { background: #d5ab88; }
|
||||
.emoji-picker .diversity-picker .diversity-scale.medium-dark { background: #af7e57; }
|
||||
.emoji-picker .diversity-picker .diversity-scale.dark { background: #7c533e; }
|
||||
|
||||
.emoji-modal .tones-button i.fa {
|
||||
.emoji-picker .diversity-picker .diversity-scale.selected i {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.emoji-picker .diversity-picker i {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.emoji-picker .diversity-picker i.fa {
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
text-shadow: 0.5px 1.5px 0 rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
body img.emoji {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.wmd-emoji-button:before {
|
||||
content: "\f118";
|
||||
}
|
||||
|
||||
.emoji-picker-modal.fadeIn {
|
||||
z-index: 9999;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: .8;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.emoji-picker .filter {
|
||||
background-color: #e9e9e9;
|
||||
border-bottom: 1px solid #e9e9e9;
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.emoji-picker .filter input {
|
||||
height: 24px;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
border: 1px solid #e9e9e9;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.emoji-picker .results {
|
||||
display: none;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 4px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.emoji-picker .results .emoji {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.emoji-picker .filter .clear-filter {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 12px;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: $primary;
|
||||
outline: none;
|
||||
display: none;
|
||||
|
||||
&:hover {
|
||||
color: $tertiary;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
.emoji-table-wrapper {
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
.emoji-modal {
|
||||
width: 340px;
|
||||
margin-top: -132px;
|
||||
margin-left: -170px;
|
||||
background-color: dark-light-choose(#dadada, blend-primary-secondary(5%));
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
class Emoji
|
||||
# update this to clear the cache
|
||||
EMOJI_VERSION = "v5"
|
||||
EMOJI_VERSION = "5"
|
||||
|
||||
FITZPATRICK_SCALE ||= [ "1f3fb", "1f3fc", "1f3fd", "1f3fe", "1f3ff" ]
|
||||
|
||||
|
@ -46,15 +46,19 @@ class Emoji
|
|||
|
||||
def self.create_from_db_item(emoji)
|
||||
name = emoji["name"]
|
||||
filename = "#{emoji['filename'] || name}.png"
|
||||
filename = emoji['filename'] || name
|
||||
Emoji.new.tap do |e|
|
||||
e.name = name
|
||||
e.url = "#{Discourse.base_uri}/images/emoji/#{SiteSetting.emoji_set}/#{filename}"
|
||||
e.url = Emoji.url_for(filename)
|
||||
end
|
||||
end
|
||||
|
||||
def self.url_for(name)
|
||||
"#{Discourse.base_uri}/images/emoji/#{SiteSetting.emoji_set}/#{name}.png?v=5"
|
||||
end
|
||||
|
||||
def self.cache_key(name)
|
||||
"#{name}:#{EMOJI_VERSION}:#{Plugin::CustomEmoji.cache_key}"
|
||||
"#{name}:v#{EMOJI_VERSION}:#{Plugin::CustomEmoji.cache_key}"
|
||||
end
|
||||
|
||||
def self.clear_cache
|
||||
|
|
|
@ -1134,6 +1134,18 @@ en:
|
|||
ctrl: 'Ctrl'
|
||||
alt: 'Alt'
|
||||
|
||||
emoji_picker:
|
||||
filter_placeholder: Search an emoji
|
||||
people: People
|
||||
nature: Nature
|
||||
food: Food
|
||||
activity: Activity
|
||||
travel: Travel
|
||||
objects: Objects
|
||||
celebration: Celebration
|
||||
custom: Custom emojis
|
||||
recent: Recently used emojis
|
||||
|
||||
composer:
|
||||
emoji: "Emoji :)"
|
||||
more_emoji: "more..."
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -4,7 +4,7 @@ require "json"
|
|||
require "nokogiri"
|
||||
require "open-uri"
|
||||
|
||||
EMOJI_GROUPS_PATH ||= "app/assets/javascripts/discourse/lib/emoji/groups.js.es6"
|
||||
EMOJI_GROUPS_PATH ||= "lib/emoji/groups.json"
|
||||
|
||||
EMOJI_DB_PATH ||= "lib/emoji/db.json"
|
||||
|
||||
|
@ -432,7 +432,7 @@ def fix_incomplete_sets(emojis)
|
|||
end
|
||||
end
|
||||
|
||||
def generate_emoji_groups(emojis)
|
||||
def generate_emoji_groups(keywords)
|
||||
puts "Generating groups..."
|
||||
|
||||
list = open(EMOJI_ORDERING_URL).read
|
||||
|
@ -453,8 +453,8 @@ def generate_emoji_groups(emojis)
|
|||
|
||||
emoji_char = code_to_emoji(emoji_code)
|
||||
|
||||
if emoji = emojis[emoji_char]
|
||||
group["icons"] << emoji["name"]
|
||||
if emoji = keywords[emoji_char]
|
||||
group["icons"] << { name: emoji["name"], diversity: emoji["fitzpatrick_scale"] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -518,15 +518,7 @@ def write_js_groups(emojis, groups)
|
|||
|
||||
confirm_overwrite(EMOJI_GROUPS_PATH)
|
||||
|
||||
template = <<TEMPLATE
|
||||
// This file is generated by emoji.rake do not modify directly
|
||||
|
||||
// note that these categories are copied from Slack
|
||||
const groups = #{JSON.pretty_generate(groups)};
|
||||
|
||||
export default groups;
|
||||
TEMPLATE
|
||||
|
||||
template = JSON.pretty_generate(groups)
|
||||
FileUtils.mkdir_p(File.expand_path("..", EMOJI_GROUPS_PATH))
|
||||
File.write(EMOJI_GROUPS_PATH, template)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,259 @@
|
|||
import { acceptance } from "helpers/qunit-helpers";
|
||||
import { IMAGE_VERSION as v } from 'pretty-text/emoji';
|
||||
import {
|
||||
keyValueStore,
|
||||
EMOJI_USAGE,
|
||||
EMOJI_SCROLL_Y,
|
||||
EMOJI_SELECTED_DIVERSITY
|
||||
} from 'discourse/components/emoji-picker';
|
||||
|
||||
acceptance("EmojiPicker", {
|
||||
loggedIn: true,
|
||||
beforeEach() {
|
||||
keyValueStore.setObject({ key: EMOJI_USAGE, value: {} });
|
||||
keyValueStore.setObject({ key: EMOJI_SCROLL_Y, value: 0 });
|
||||
keyValueStore.setObject({ key: EMOJI_SELECTED_DIVERSITY, value: 1 });
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("emoji picker can be opened/closed", assert => {
|
||||
visit("/t/internationalization-localization/280");
|
||||
click("#topic-footer-buttons .btn.create");
|
||||
|
||||
click("button.emoji.btn");
|
||||
andThen(() => {
|
||||
assert.notEqual(
|
||||
find('.emoji-picker').html().trim(),
|
||||
"",
|
||||
"it opens the picker"
|
||||
);
|
||||
});
|
||||
|
||||
click("button.emoji.btn");
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
find('.emoji-picker').html().trim(),
|
||||
"",
|
||||
"it closes the picker"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("emojis can be hovered to display info", assert => {
|
||||
visit("/t/internationalization-localization/280");
|
||||
click("#topic-footer-buttons .btn.create");
|
||||
|
||||
click("button.emoji.btn");
|
||||
andThen(() => {
|
||||
$(".emoji-picker a[title='grinning']").trigger('mouseover');
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
find('.emoji-picker .info').html().trim(),
|
||||
`<img src=\"/images/emoji/emoji_one/grinning.png?v=${v}\" class=\"emoji\"> <span>:grinning:<span></span></span>`,
|
||||
"it displays emoji info when hovering emoji"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("emoji picker has sections", assert => {
|
||||
visit("/t/internationalization-localization/280");
|
||||
click("#topic-footer-buttons .btn.create");
|
||||
click("button.emoji.btn");
|
||||
|
||||
click(".emoji-picker .categories-column a[title='travel']");
|
||||
andThen(() => {
|
||||
assert.notEqual(
|
||||
find('.emoji-picker .list').scrollTop(),
|
||||
0,
|
||||
"it scrolls to section"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find(".emoji-picker .categories-column a[title='travel']").parent().hasClass('current'),
|
||||
true,
|
||||
"it highlights section icon"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("emoji picker triggers event when picking emoji", assert => {
|
||||
visit("/t/internationalization-localization/280");
|
||||
click("#topic-footer-buttons .btn.create");
|
||||
click("button.emoji.btn");
|
||||
|
||||
click(".emoji-picker a[title='grinning']");
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
find('.d-editor-input').val(),
|
||||
":grinning:",
|
||||
"it adds the emoji code in the editor when selected"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("emoji picker has a list of recently used emojis", assert => {
|
||||
visit("/t/internationalization-localization/280");
|
||||
click("#topic-footer-buttons .btn.create");
|
||||
click("button.emoji.btn");
|
||||
click(".emoji-picker .clear-recent");
|
||||
|
||||
click(".emoji-picker a[title='grinning']");
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
find('.section[data-section="recent"]').css("display"),
|
||||
"block",
|
||||
"it shows recent section"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('.section[data-section="recent"] .section-group img.emoji').length,
|
||||
1,
|
||||
"it adds the emoji code to the recently used emojis list"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("emoji picker can clear recently used emojis", assert => {
|
||||
visit("/t/internationalization-localization/280");
|
||||
click("#topic-footer-buttons .btn.create");
|
||||
click("button.emoji.btn");
|
||||
|
||||
click(".emoji-picker a[title='grinning']");
|
||||
click(".emoji-picker a[title='sunglasses']");
|
||||
click(".emoji-picker a[title='sunglasses']");
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
find('.section[data-section="recent"] .section-group img.emoji').length,
|
||||
2
|
||||
);
|
||||
|
||||
click(".emoji-picker .clear-recent");
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
find('.section[data-section="recent"] .section-group img.emoji').length,
|
||||
0,
|
||||
"it has cleared recent emojis"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('.section[data-section="recent"]').css("display"),
|
||||
"none",
|
||||
"it hides recent section"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('.category-icon a[title="recent"]').parent().css("display"),
|
||||
"none",
|
||||
"it hides recent category icon"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("emoji picker correctly orders recently used emojis", assert => {
|
||||
visit("/t/internationalization-localization/280");
|
||||
click("#topic-footer-buttons .btn.create");
|
||||
click("button.emoji.btn");
|
||||
click(".emoji-picker .clear-recent");
|
||||
|
||||
click(".emoji-picker a[title='grinning']");
|
||||
click(".emoji-picker a[title='sunglasses']");
|
||||
click(".emoji-picker a[title='sunglasses']");
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
find('.section[data-section="recent"] .section-group img.emoji').length,
|
||||
2,
|
||||
"it has multiple recent emojis"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('.section[data-section="recent"] .section-group img.emoji').first().attr('src'),
|
||||
`/images/emoji/emoji_one/sunglasses.png?v=${v}`,
|
||||
"it puts the most used emoji in first"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("emoji picker lazy loads emojis", assert => {
|
||||
visit("/t/internationalization-localization/280");
|
||||
click("#topic-footer-buttons .btn.create");
|
||||
|
||||
click("button.emoji.btn");
|
||||
|
||||
andThen(() => {
|
||||
const $emoji = $('.emoji-picker a[title="massage_woman"] img');
|
||||
assert.equal(
|
||||
$emoji.attr('src'),
|
||||
"",
|
||||
"it doesn't load invisible emojis"
|
||||
);
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
const done = assert.async();
|
||||
setTimeout(() => {
|
||||
$('.emoji-picker .list').scrollTop(2600);
|
||||
setTimeout(() => {
|
||||
const $emoji = $('a[title="massage_woman"] img');
|
||||
assert.equal(
|
||||
$emoji.attr('src'),
|
||||
`/images/emoji/emoji_one/massage_woman.png?v=${v}`,
|
||||
"it loads visible emojis"
|
||||
);
|
||||
done();
|
||||
}, 50);
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("emoji picker supports diversity scale", assert => {
|
||||
visit("/t/internationalization-localization/280");
|
||||
click("#topic-footer-buttons .btn.create");
|
||||
click("button.emoji.btn");
|
||||
|
||||
click('.emoji-picker a.diversity-scale.dark');
|
||||
andThen(() => {
|
||||
const done = assert.async();
|
||||
setTimeout(() => {
|
||||
$('.emoji-picker .list').scrollTop(2900);
|
||||
setTimeout(() => {
|
||||
const $emoji = $('a[title="massage_woman"] img');
|
||||
assert.equal(
|
||||
$emoji.attr('src'),
|
||||
`/images/emoji/emoji_one/massage_woman/6.png?v=${v}`,
|
||||
"it applies diversity scale on emoji"
|
||||
);
|
||||
done();
|
||||
}, 250);
|
||||
}, 250);
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("emoji picker persists state", assert => {
|
||||
visit("/t/internationalization-localization/280");
|
||||
click("#topic-footer-buttons .btn.create");
|
||||
|
||||
click("button.emoji.btn");
|
||||
andThen(() => {
|
||||
$('.emoji-picker .list').scrollTop(2600);
|
||||
click('.emoji-picker a.diversity-scale.medium-dark');
|
||||
});
|
||||
|
||||
click("button.emoji.btn");
|
||||
|
||||
click("button.emoji.btn");
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
find('.emoji-picker .list').scrollTop() > 2500,
|
||||
true,
|
||||
"it stores scroll position"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('.emoji-picker .diversity-scale.medium-dark').hasClass('selected'),
|
||||
true,
|
||||
"it stores diversity scale"
|
||||
);
|
||||
});
|
||||
});
|
|
@ -3,11 +3,3 @@
|
|||
.modal-backdrop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.emoji-modal-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.emoji-modal {
|
||||
position: relative;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue