mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
REFACTOR: rewrite the emoji-picker (#10464)
The emoji-picker is a specific piece of code as it has very strong performance requirements which are almost not found anywhere else in the app, as a result it was using various hacks to make it work decently even on old browsers. Following our drop of Internet Explorer, and various new features in Ember and recent browsers we can now take advantage of this to reduce the amount of code needed, this rewrite most importantly does the following: - use loading="lazy" preventing the full list of emojis to be loaded on opening - uses InterserctionObserver to find the active section - limits the use of native event listentes only for hover/click emojis (for performance reason we track click on the whole emoji area and delegate events), everything else is using ember events - uses popper to position the emoji picker - no jquery code
This commit is contained in:
parent
9debfed060
commit
226be994da
@ -20,3 +20,10 @@ define("message-bus-client", ["exports"], function(__exports__) {
|
|||||||
define("ember-buffered-proxy/proxy", ["exports"], function(__exports__) {
|
define("ember-buffered-proxy/proxy", ["exports"], function(__exports__) {
|
||||||
__exports__.default = window.BufferedProxy;
|
__exports__.default = window.BufferedProxy;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
define("@popperjs/core", ["exports"], function(__exports__) {
|
||||||
|
__exports__.default = window.Popper;
|
||||||
|
__exports__.createPopper = window.Popper.createPopper;
|
||||||
|
__exports__.defaultModifiers = window.Popper.defaultModifiers;
|
||||||
|
__exports__.popperGenerator = window.Popper.popperGenerator;
|
||||||
|
});
|
||||||
|
@ -459,9 +459,7 @@ export default Component.extend({
|
|||||||
return `${v.code}:`;
|
return `${v.code}:`;
|
||||||
} else {
|
} else {
|
||||||
$editorInput.autocomplete({ cancel: true });
|
$editorInput.autocomplete({ cancel: true });
|
||||||
this.setProperties({
|
this.set("emojiPickerIsActive", true);
|
||||||
emojiPickerIsActive: true
|
|
||||||
});
|
|
||||||
|
|
||||||
schedule("afterRender", () => {
|
schedule("afterRender", () => {
|
||||||
const filterInput = document.querySelector(
|
const filterInput = document.querySelector(
|
||||||
|
@ -1,18 +1,21 @@
|
|||||||
|
import { observes } from "discourse-common/utils/decorators";
|
||||||
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
import { htmlSafe } from "@ember/template";
|
||||||
|
import { emojiUnescape } from "discourse/lib/text";
|
||||||
|
import { escapeExpression } from "discourse/lib/utilities";
|
||||||
|
import { action, computed } from "@ember/object";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { throttle, debounce, schedule, later } from "@ember/runloop";
|
import { schedule, later } from "@ember/runloop";
|
||||||
import Component from "@ember/component";
|
import Component from "@ember/component";
|
||||||
import { on, observes } from "discourse-common/utils/decorators";
|
|
||||||
import { findRawTemplate } from "discourse-common/lib/raw-templates";
|
|
||||||
import { emojiUrlFor } from "discourse/lib/text";
|
import { emojiUrlFor } from "discourse/lib/text";
|
||||||
|
import { createPopper } from "@popperjs/core";
|
||||||
import {
|
import {
|
||||||
extendedEmojiList,
|
extendedEmojiList,
|
||||||
isSkinTonableEmoji,
|
isSkinTonableEmoji,
|
||||||
emojiSearch
|
emojiSearch
|
||||||
} from "pretty-text/emoji";
|
} from "pretty-text/emoji";
|
||||||
import { safariHacksDisabled } from "discourse/lib/utilities";
|
import { safariHacksDisabled } from "discourse/lib/utilities";
|
||||||
import { isTesting, INPUT_DELAY } from "discourse-common/config/environment";
|
|
||||||
|
|
||||||
const PER_ROW = 11;
|
|
||||||
function customEmojis() {
|
function customEmojis() {
|
||||||
const list = extendedEmojiList();
|
const list = extendedEmojiList();
|
||||||
const groups = [];
|
const groups = [];
|
||||||
@ -28,626 +31,261 @@ function customEmojis() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
automaticPositioning: true,
|
|
||||||
emojiStore: service("emoji-store"),
|
emojiStore: service("emoji-store"),
|
||||||
|
tagName: "",
|
||||||
|
customEmojis: null,
|
||||||
|
selectedDiversity: null,
|
||||||
|
recentEmojis: null,
|
||||||
|
hoveredEmoji: null,
|
||||||
|
isActive: false,
|
||||||
|
isLoading: true,
|
||||||
|
|
||||||
close() {
|
init() {
|
||||||
this._unbindEvents();
|
this._super(...arguments);
|
||||||
|
|
||||||
this.$picker &&
|
this.set("customEmojis", customEmojis());
|
||||||
this.$picker.css({ width: "", left: "", bottom: "", display: "none" });
|
this.set("recentEmojis", this.emojiStore.favorites);
|
||||||
|
this.set("selectedDiversity", this.emojiStore.diversity);
|
||||||
|
|
||||||
this.$modal.removeClass("fadeIn");
|
this._sectionObserver = this._setupSectionObserver();
|
||||||
},
|
},
|
||||||
|
|
||||||
show() {
|
didInsertElement() {
|
||||||
this.$filter = this.$picker.find(".filter");
|
this._super(...arguments);
|
||||||
this.$results = this.$picker.find(".results");
|
|
||||||
this.$list = this.$picker.find(".list");
|
|
||||||
|
|
||||||
this.setProperties({
|
this.appEvents.on("emoji-picker:close", this, "onClose");
|
||||||
selectedDiversity: this.emojiStore.diversity,
|
},
|
||||||
recentEmojis: this.emojiStore.favorites
|
|
||||||
});
|
|
||||||
|
|
||||||
schedule("afterRender", this, function() {
|
// didReceiveAttrs would be a better choice here, but this is sadly causing
|
||||||
this._bindEvents();
|
// too many unexpected reloads as it's triggered for other reasons than a mutation
|
||||||
this._loadCategoriesEmojis();
|
// of isActive
|
||||||
this._positionPicker();
|
@observes("isActive")
|
||||||
this._scrollTo();
|
_setup() {
|
||||||
this._updateSelectedDiversity();
|
if (this.isActive) {
|
||||||
this._checkVisibleSection(true);
|
this.onShow();
|
||||||
|
} else {
|
||||||
|
this.onClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
willDestroyElement() {
|
||||||
|
this._super(...arguments);
|
||||||
|
|
||||||
|
this._sectionObserver && this._sectionObserver.disconnect();
|
||||||
|
|
||||||
|
this.appEvents.off("emoji-picker:close", this, "onClose");
|
||||||
|
},
|
||||||
|
|
||||||
|
@action
|
||||||
|
onShow() {
|
||||||
|
this.set("isLoading", true);
|
||||||
|
|
||||||
|
schedule("afterRender", () => {
|
||||||
|
document.addEventListener("click", this.handleOutsideClick);
|
||||||
|
|
||||||
|
const emojiPicker = document.querySelector(".emoji-picker");
|
||||||
|
if (!emojiPicker) return;
|
||||||
|
|
||||||
|
if (!this.site.isMobileDevice) {
|
||||||
|
this._popper = createPopper(
|
||||||
|
document.querySelector(".d-editor-textarea-wrapper"),
|
||||||
|
emojiPicker,
|
||||||
|
{
|
||||||
|
placement: "auto",
|
||||||
|
modifiers: [
|
||||||
|
{
|
||||||
|
name: "preventOverflow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "offset",
|
||||||
|
options: {
|
||||||
|
offset: [5, 5]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
emojiPicker
|
||||||
|
.querySelectorAll(".emojis-container .section .section-header")
|
||||||
|
.forEach(p => this._sectionObserver.observe(p));
|
||||||
|
|
||||||
|
// this is a low-tech trick to prevent appending hundreds of emojis
|
||||||
|
// of blocking the rendering of the picker
|
||||||
|
later(() => {
|
||||||
|
this.set("isLoading", false);
|
||||||
|
|
||||||
|
schedule("afterRender", () => {
|
||||||
if (
|
if (
|
||||||
(!this.site.isMobileDevice || this.isEditorFocused) &&
|
(!this.site.isMobileDevice || this.isEditorFocused) &&
|
||||||
!safariHacksDisabled()
|
!safariHacksDisabled()
|
||||||
)
|
|
||||||
this.$filter.find("input[name='filter']").focus();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
@on("init")
|
|
||||||
_setInitialValues() {
|
|
||||||
this.set("customEmojis", customEmojis());
|
|
||||||
this.scrollPosition = 0;
|
|
||||||
this.$visibleSections = [];
|
|
||||||
},
|
|
||||||
|
|
||||||
@on("willDestroyElement")
|
|
||||||
_unbindGlobalEvents() {
|
|
||||||
this.appEvents.off("emoji-picker:close", this, "_closeEmojiPicker");
|
|
||||||
},
|
|
||||||
|
|
||||||
_closeEmojiPicker() {
|
|
||||||
this.set("active", false);
|
|
||||||
},
|
|
||||||
|
|
||||||
@on("didInsertElement")
|
|
||||||
_setup() {
|
|
||||||
this.appEvents.on("emoji-picker:close", this, "_closeEmojiPicker");
|
|
||||||
},
|
|
||||||
|
|
||||||
@on("didUpdateAttrs")
|
|
||||||
_setState() {
|
|
||||||
schedule("afterRender", () => {
|
|
||||||
if (!this.element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$picker = $(this.element.querySelector(".emoji-picker"));
|
|
||||||
this.$modal = $(this.element.querySelector(".emoji-picker-modal"));
|
|
||||||
|
|
||||||
this.active ? this.show() : this.close();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
@observes("filter")
|
|
||||||
filterChanged() {
|
|
||||||
this.$filter.find(".clear-filter").toggle(!_.isEmpty(this.filter));
|
|
||||||
const filterDelay = this.site.isMobileDevice ? 400 : INPUT_DELAY;
|
|
||||||
debounce(this, this._filterEmojisList, filterDelay);
|
|
||||||
},
|
|
||||||
|
|
||||||
@observes("selectedDiversity")
|
|
||||||
selectedDiversityChanged() {
|
|
||||||
this.emojiStore.diversity = this.selectedDiversity;
|
|
||||||
|
|
||||||
$.each(
|
|
||||||
this.$list.find(".emoji[data-loaded='1'].diversity"),
|
|
||||||
(_, button) => {
|
|
||||||
$(button)
|
|
||||||
.css("background-image", "")
|
|
||||||
.removeAttr("data-loaded");
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.filter !== "") {
|
|
||||||
$.each(this.$results.find(".emoji.diversity"), (_, button) =>
|
|
||||||
this._setButtonBackground(button, true)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._updateSelectedDiversity();
|
|
||||||
this._checkVisibleSection(true);
|
|
||||||
},
|
|
||||||
|
|
||||||
@observes("recentEmojis")
|
|
||||||
_recentEmojisChanged() {
|
|
||||||
const previousScrollTop = this.scrollPosition;
|
|
||||||
const $recentSection = this.$list.find(".section[data-section='recent']");
|
|
||||||
const $recentSectionGroup = $recentSection.find(".section-group");
|
|
||||||
const $recentCategory = this.$picker
|
|
||||||
.find(".category-icon button[data-section='recent']")
|
|
||||||
.parent();
|
|
||||||
let persistScrollPosition = !$recentCategory.is(":visible") ? true : false;
|
|
||||||
|
|
||||||
// we set height to 0 to avoid it being taken into account for scroll position
|
|
||||||
if (_.isEmpty(this.recentEmojis)) {
|
|
||||||
$recentCategory.hide();
|
|
||||||
$recentSection.css("height", 0).hide();
|
|
||||||
} else {
|
|
||||||
$recentCategory.show();
|
|
||||||
$recentSection.css("height", "auto").show();
|
|
||||||
}
|
|
||||||
|
|
||||||
const recentEmojis = this.recentEmojis.map(code => {
|
|
||||||
return { code, src: emojiUrlFor(code) };
|
|
||||||
});
|
|
||||||
const template = findRawTemplate("emoji-picker-recent")({ recentEmojis });
|
|
||||||
$recentSectionGroup.html(template);
|
|
||||||
|
|
||||||
if (persistScrollPosition) {
|
|
||||||
this.$list.scrollTop(previousScrollTop + $recentSection.outerHeight());
|
|
||||||
}
|
|
||||||
|
|
||||||
this._bindHover($recentSectionGroup);
|
|
||||||
},
|
|
||||||
|
|
||||||
_updateSelectedDiversity() {
|
|
||||||
const $diversityPicker = this.$picker.find(".diversity-picker");
|
|
||||||
|
|
||||||
$diversityPicker.find(".diversity-scale").removeClass("selected");
|
|
||||||
$diversityPicker
|
|
||||||
.find(`.diversity-scale[data-level="${this.selectedDiversity}"]`)
|
|
||||||
.addClass("selected");
|
|
||||||
},
|
|
||||||
|
|
||||||
_loadCategoriesEmojis() {
|
|
||||||
$.each(
|
|
||||||
this.$picker.find(".categories-column button.emoji"),
|
|
||||||
(_, button) => {
|
|
||||||
const $button = $(button);
|
|
||||||
const code = this._codeWithDiversity($button.data("tabicon"), false);
|
|
||||||
$button.css("background-image", `url("${emojiUrlFor(code)}")`);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
_bindEvents() {
|
|
||||||
this._bindDiversityClick();
|
|
||||||
this._bindSectionsScroll();
|
|
||||||
this._bindEmojiClick(this.$list.find(".section-group"));
|
|
||||||
this._bindClearRecentEmojisGroup();
|
|
||||||
this._bindResizing();
|
|
||||||
this._bindCategoryClick();
|
|
||||||
this._bindModalClick();
|
|
||||||
this._bindFilterInput();
|
|
||||||
|
|
||||||
if (!this.site.isMobileDevice) {
|
|
||||||
this._bindHover();
|
|
||||||
}
|
|
||||||
|
|
||||||
later(this, this._onScroll, 100);
|
|
||||||
},
|
|
||||||
|
|
||||||
_bindModalClick() {
|
|
||||||
this.$modal.on("click", () => this.set("active", false));
|
|
||||||
|
|
||||||
$("html").on("mouseup.emoji-picker", event => {
|
|
||||||
let $target = $(event.target);
|
|
||||||
if (
|
|
||||||
$target.closest(".emoji-picker").length ||
|
|
||||||
$target.closest(".emoji.btn").length ||
|
|
||||||
$target.hasClass("grippie")
|
|
||||||
) {
|
) {
|
||||||
return;
|
const filter = emojiPicker.querySelector("input.filter");
|
||||||
|
filter && filter.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the popup if clicked outside
|
if (this.selectedDiversity !== 0) {
|
||||||
this.set("active", false);
|
this._applyDiversity(this.selectedDiversity);
|
||||||
return false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
@on("willDestroyElement")
|
|
||||||
_unbindEvents() {
|
|
||||||
$(this.element).off();
|
|
||||||
$(window).off("resize");
|
|
||||||
$("#reply-control").off("div-resizing");
|
|
||||||
$("html").off("mouseup.emoji-picker");
|
|
||||||
},
|
|
||||||
|
|
||||||
_filterEmojisList() {
|
|
||||||
if (this.filter === "") {
|
|
||||||
this.$filter.find("input[name='filter']").val("");
|
|
||||||
this.$results.empty().hide();
|
|
||||||
this.$list.css("visibility", "visible");
|
|
||||||
} else {
|
|
||||||
const lowerCaseFilter = this.filter.toLowerCase();
|
|
||||||
const filteredCodes = emojiSearch(lowerCaseFilter, { maxResults: 30 });
|
|
||||||
this.$results
|
|
||||||
.empty()
|
|
||||||
.html(
|
|
||||||
filteredCodes.map(code => {
|
|
||||||
const hasDiversity = isSkinTonableEmoji(code);
|
|
||||||
const diversity = hasDiversity ? "diversity" : "";
|
|
||||||
const scaledCode = this._codeWithDiversity(code, hasDiversity);
|
|
||||||
return `<button style="background-image: url('${emojiUrlFor(
|
|
||||||
scaledCode
|
|
||||||
)}')" type="button" class="emoji ${diversity}" tabindex="-1" title="${code}"></button>`;
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.show();
|
|
||||||
this._bindHover(this.$results);
|
|
||||||
this._bindEmojiClick(this.$results);
|
|
||||||
this.$list.css("visibility", "hidden");
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
_bindFilterInput() {
|
|
||||||
const $input = this.$filter.find("input");
|
|
||||||
|
|
||||||
$input.on("input", event => {
|
|
||||||
this.set("filter", event.currentTarget.value);
|
|
||||||
});
|
});
|
||||||
|
}, 50);
|
||||||
this.$filter.find(".clear-filter").on("click", () => {
|
|
||||||
$input.val("").focus();
|
|
||||||
this.set("filter", "");
|
|
||||||
return false;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_bindCategoryClick() {
|
@action
|
||||||
this.$picker.find(".category-icon").on("click", "button.emoji", event => {
|
onClose() {
|
||||||
this.set("filter", "");
|
document.removeEventListener("click", this.handleOutsideClick);
|
||||||
this.$results.empty();
|
this.onEmojiPickerClose && this.onEmojiPickerClose();
|
||||||
this.$list.css("visibility", "visible");
|
},
|
||||||
|
|
||||||
const section = $(event.currentTarget).data("section");
|
diversityScales: computed("selectedDiversity", function() {
|
||||||
const $section = this.$list.find(`.section[data-section="${section}"]`);
|
return [
|
||||||
const scrollTop =
|
"default",
|
||||||
this.$list.scrollTop() +
|
"light",
|
||||||
($section.offset().top - this.$list.offset().top);
|
"medium-light",
|
||||||
this._scrollTo(scrollTop);
|
"medium",
|
||||||
return false;
|
"medium-dark",
|
||||||
|
"dark"
|
||||||
|
].map((name, index) => {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
icon: index === this.selectedDiversity ? "check" : ""
|
||||||
|
};
|
||||||
});
|
});
|
||||||
},
|
}),
|
||||||
|
|
||||||
_bindHover($hoverables) {
|
@action
|
||||||
const replaceInfoContent = html =>
|
onClearRecents() {
|
||||||
this.$picker.find(".footer .info").html(html || "");
|
|
||||||
|
|
||||||
($hoverables || this.$list.find(".section-group")).on(
|
|
||||||
{
|
|
||||||
mouseover: event => {
|
|
||||||
const code = this._codeForEmojiButton($(event.currentTarget));
|
|
||||||
const html = `<img src="${emojiUrlFor(
|
|
||||||
code
|
|
||||||
)}" class="emoji"> <span>:${code}:<span>`;
|
|
||||||
replaceInfoContent(html);
|
|
||||||
},
|
|
||||||
mouseleave: () => replaceInfoContent()
|
|
||||||
},
|
|
||||||
"button.emoji"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
_bindResizing() {
|
|
||||||
$(window).on("resize", () => {
|
|
||||||
throttle(this, this._positionPicker, 16);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#reply-control").on("div-resizing", () => {
|
|
||||||
throttle(this, this._positionPicker, 16);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_bindClearRecentEmojisGroup() {
|
|
||||||
const $recent = this.$picker.find(
|
|
||||||
".section[data-section='recent'] .clear-recent"
|
|
||||||
);
|
|
||||||
$recent.on("click", () => {
|
|
||||||
this.emojiStore.favorites = [];
|
this.emojiStore.favorites = [];
|
||||||
this.set("recentEmojis", []);
|
this.set("recentEmojis", []);
|
||||||
this._scrollTo(0);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_bindEmojiClick($emojisContainer) {
|
@action
|
||||||
const handler = event => {
|
onDiversitySelection(scale) {
|
||||||
const code = this._codeForEmojiButton($(event.currentTarget));
|
this.emojiStore.diversity = scale;
|
||||||
|
this.set("selectedDiversity", scale);
|
||||||
|
|
||||||
if (
|
this._applyDiversity(scale);
|
||||||
$(event.currentTarget).parents(".section[data-section='recent']")
|
},
|
||||||
.length === 0
|
|
||||||
) {
|
@action
|
||||||
this._trackEmojiUsage(code);
|
onEmojiHover(event) {
|
||||||
|
const img = event.target;
|
||||||
|
if (!img.classList.contains("emoji") || img.tagName !== "IMG") {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.set("hoveredEmoji", event.target.title);
|
||||||
|
},
|
||||||
|
|
||||||
|
@action
|
||||||
|
onEmojiSelection(event) {
|
||||||
|
const img = event.target;
|
||||||
|
|
||||||
|
if (!img.classList.contains("emoji") || img.tagName !== "IMG") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = event.target.title;
|
||||||
|
code = this._codeWithDiversity(code, this.selectedDiversity);
|
||||||
|
|
||||||
this.emojiSelected(code);
|
this.emojiSelected(code);
|
||||||
|
|
||||||
if (this.$modal.hasClass("fadeIn")) {
|
if (!img.parentNode.parentNode.classList.contains("recent")) {
|
||||||
this.set("active", false);
|
this._trackEmojiUsage(code);
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.site.isMobileDevice) {
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
$emojisContainer
|
|
||||||
.off("touchstart")
|
|
||||||
.on("touchstart", "button.emoji", touchStartEvent => {
|
|
||||||
const $this = $(touchStartEvent.currentTarget);
|
|
||||||
|
|
||||||
$this.on("touchend", touchEndEvent => {
|
|
||||||
touchEndEvent.preventDefault();
|
|
||||||
touchEndEvent.stopPropagation();
|
|
||||||
|
|
||||||
handler.bind(self)(touchEndEvent);
|
|
||||||
$this.off("touchend");
|
|
||||||
});
|
|
||||||
|
|
||||||
$this.on("touchmove", () => $this.off("touchend"));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$emojisContainer
|
|
||||||
.off("click")
|
|
||||||
.on("click", "button.emoji", e => handler.bind(this)(e));
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_bindSectionsScroll() {
|
@action
|
||||||
this.$list.on("scroll", this._onScroll.bind(this));
|
onCategorySelection(sectionName) {
|
||||||
},
|
const section = document.querySelector(
|
||||||
|
`.emoji-picker-emoji-area .section[data-section="${sectionName}"]`
|
||||||
_onScroll() {
|
|
||||||
debounce(this, this._checkVisibleSection, 50);
|
|
||||||
},
|
|
||||||
|
|
||||||
_checkVisibleSection(force) {
|
|
||||||
// make sure we stop loading if picker has been removed
|
|
||||||
if (!this.$picker) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPosition = this.$list.scrollTop();
|
|
||||||
if (newPosition === this.scrollPosition && !force) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.scrollPosition = newPosition;
|
|
||||||
|
|
||||||
const $sections = this.$list.find(".section");
|
|
||||||
const listHeight = this.$list.innerHeight();
|
|
||||||
let $selectedSection;
|
|
||||||
|
|
||||||
this.$visibleSections = _.filter($sections, section => {
|
|
||||||
const $section = $(section);
|
|
||||||
const sectionTop = $section.position().top;
|
|
||||||
return sectionTop + $section.height() > 0 && sectionTop < listHeight;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!_.isEmpty(this.recentEmojis) && this.scrollPosition === 0) {
|
|
||||||
$selectedSection = $(_.first(this.$visibleSections));
|
|
||||||
} else {
|
|
||||||
$selectedSection = $(_.last(this.$visibleSections));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($selectedSection) {
|
|
||||||
this.$picker.find(".category-icon").removeClass("current");
|
|
||||||
this.$picker
|
|
||||||
.find(
|
|
||||||
`.category-icon button[data-section='${$selectedSection.data(
|
|
||||||
"section"
|
|
||||||
)}']`
|
|
||||||
)
|
|
||||||
.parent()
|
|
||||||
.addClass("current");
|
|
||||||
|
|
||||||
this._loadVisibleSections();
|
|
||||||
}
|
|
||||||
|
|
||||||
later(this, this._checkVisibleSection, 100);
|
|
||||||
},
|
|
||||||
|
|
||||||
_loadVisibleSections() {
|
|
||||||
if (!this.$visibleSections) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const listHeight = this.$list.innerHeight();
|
|
||||||
|
|
||||||
this.$visibleSections.forEach(visibleSection => {
|
|
||||||
const $unloadedEmojis = $(visibleSection).find(
|
|
||||||
"button.emoji:not(.custom)[data-loaded!='1']"
|
|
||||||
);
|
);
|
||||||
$.each($unloadedEmojis, (_, button) => {
|
section && section.scrollIntoView();
|
||||||
let offsetTop = button.offsetTop;
|
|
||||||
|
|
||||||
if (offsetTop < this.scrollPosition + listHeight + 200) {
|
|
||||||
if (offsetTop + 200 > this.scrollPosition) {
|
|
||||||
const $button = $(button);
|
|
||||||
this._setButtonBackground($button);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_bindDiversityClick() {
|
@action
|
||||||
const $diversityScales = this.$picker.find(
|
onFilter(event) {
|
||||||
".diversity-picker .diversity-scale"
|
const emojiPickerArea = document.querySelector(".emoji-picker-emoji-area");
|
||||||
);
|
const emojisContainer = emojiPickerArea.querySelector(".emojis-container");
|
||||||
$diversityScales.on("click", event => {
|
const results = emojiPickerArea.querySelector(".results");
|
||||||
const $selectedDiversity = $(event.currentTarget);
|
results.innerHTML = "";
|
||||||
this.set(
|
|
||||||
"selectedDiversity",
|
|
||||||
parseInt($selectedDiversity.data("level"), 10)
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_isReplyControlExpanded() {
|
if (event.target.value) {
|
||||||
const verticalSpace =
|
results.innerHTML = emojiSearch(event.target.value, { maxResults: 10 })
|
||||||
$(window).height() -
|
.map(this._replaceEmoji)
|
||||||
$(".d-header").height() -
|
.join("");
|
||||||
$("#reply-control").height();
|
|
||||||
|
|
||||||
return verticalSpace < this.$picker.height() - 48;
|
emojisContainer.style.visibility = "hidden";
|
||||||
},
|
results.scrollIntoView();
|
||||||
|
|
||||||
_positionPicker() {
|
|
||||||
if (!this.active) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let windowWidth = $(window).width();
|
|
||||||
|
|
||||||
const desktopModalePositioning = options => {
|
|
||||||
let attributes = {
|
|
||||||
width: Math.min(windowWidth, 400) - 12,
|
|
||||||
marginLeft: -(Math.min(windowWidth, 400) / 2) + 6,
|
|
||||||
marginTop: -130,
|
|
||||||
left: "50%",
|
|
||||||
bottom: "",
|
|
||||||
top: "50%",
|
|
||||||
display: "flex"
|
|
||||||
};
|
|
||||||
|
|
||||||
this.$modal.addClass("fadeIn");
|
|
||||||
this.$picker.css(_.merge(attributes, options));
|
|
||||||
};
|
|
||||||
|
|
||||||
const mobilePositioning = options => {
|
|
||||||
let attributes = {
|
|
||||||
width: windowWidth,
|
|
||||||
marginLeft: 0,
|
|
||||||
marginTop: "auto",
|
|
||||||
left: 0,
|
|
||||||
bottom: "",
|
|
||||||
top: 0,
|
|
||||||
display: "flex"
|
|
||||||
};
|
|
||||||
|
|
||||||
this.$modal.addClass("fadeIn");
|
|
||||||
this.$picker.css(_.merge(attributes, options));
|
|
||||||
};
|
|
||||||
|
|
||||||
const desktopPositioning = options => {
|
|
||||||
let attributes = {
|
|
||||||
position: "fixed",
|
|
||||||
width: windowWidth < 485 ? windowWidth - 12 : 400,
|
|
||||||
marginLeft: "",
|
|
||||||
marginTop: "",
|
|
||||||
right: "",
|
|
||||||
left: "",
|
|
||||||
bottom: 32,
|
|
||||||
top: "",
|
|
||||||
display: "flex"
|
|
||||||
};
|
|
||||||
|
|
||||||
this.$modal.removeClass("fadeIn");
|
|
||||||
this.$picker.css(_.merge(attributes, options));
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isTesting() || !this.automaticPositioning) {
|
|
||||||
desktopPositioning();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.site.isMobileDevice) {
|
|
||||||
mobilePositioning();
|
|
||||||
} else {
|
} else {
|
||||||
if (this._isReplyControlExpanded()) {
|
emojisContainer.style.visibility = "visible";
|
||||||
let $editorWrapper = $(".d-editor-preview-wrapper");
|
|
||||||
if (
|
|
||||||
($editorWrapper.is(":visible") && $editorWrapper.width() < 400) ||
|
|
||||||
windowWidth < 485
|
|
||||||
) {
|
|
||||||
desktopModalePositioning();
|
|
||||||
} else {
|
|
||||||
if ($editorWrapper.is(":visible")) {
|
|
||||||
let previewOffset = $(".d-editor-preview-wrapper").offset();
|
|
||||||
let replyControlOffset = $("#reply-control").offset();
|
|
||||||
let left = previewOffset.left - replyControlOffset.left;
|
|
||||||
desktopPositioning({ left });
|
|
||||||
} else {
|
|
||||||
desktopPositioning({
|
|
||||||
right:
|
|
||||||
($("#reply-control").width() -
|
|
||||||
$(".d-editor-container").width()) /
|
|
||||||
2
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (windowWidth < 485) {
|
|
||||||
desktopModalePositioning();
|
|
||||||
} else {
|
|
||||||
const previewInputOffset = $(".d-editor-input").offset();
|
|
||||||
|
|
||||||
const pickerHeight = $(".d-editor .emoji-picker").height();
|
|
||||||
const editorHeight = $(".d-editor-input").height();
|
|
||||||
const windowBottom = $(window).scrollTop() + $(window).height();
|
|
||||||
|
|
||||||
if (
|
|
||||||
previewInputOffset.top + editorHeight + pickerHeight <
|
|
||||||
windowBottom
|
|
||||||
) {
|
|
||||||
// position it below editor if there is enough space
|
|
||||||
desktopPositioning({
|
|
||||||
position: "absolute",
|
|
||||||
top: previewInputOffset.top + editorHeight,
|
|
||||||
left: previewInputOffset.left
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// try positioning it above
|
|
||||||
desktopPositioning({
|
|
||||||
position: "absolute",
|
|
||||||
top: -pickerHeight,
|
|
||||||
left: previewInputOffset.left
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const infoMaxWidth =
|
|
||||||
this.$picker.width() -
|
|
||||||
this.$picker.find(".categories-column").width() -
|
|
||||||
this.$picker.find(".diversity-picker").width() -
|
|
||||||
60;
|
|
||||||
this.$picker.find(".info").css("max-width", infoMaxWidth);
|
|
||||||
},
|
|
||||||
|
|
||||||
_codeWithDiversity(code, diversity) {
|
|
||||||
if (diversity && this.selectedDiversity !== 1) {
|
|
||||||
return `${code}:t${this.selectedDiversity}`;
|
|
||||||
} else {
|
|
||||||
return code;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_trackEmojiUsage(code) {
|
_trackEmojiUsage(code) {
|
||||||
this.emojiStore.track(code);
|
this.emojiStore.track(code);
|
||||||
this.set("recentEmojis", this.emojiStore.favorites.slice(0, PER_ROW));
|
this.set("recentEmojis", this.emojiStore.favorites.slice(0, 10));
|
||||||
},
|
},
|
||||||
|
|
||||||
_scrollTo(y) {
|
_replaceEmoji(code) {
|
||||||
const yPosition = typeof y === "undefined" ? this.scrollPosition : y;
|
const escaped = emojiUnescape(`:${escapeExpression(code)}:`, {
|
||||||
|
lazy: true
|
||||||
|
});
|
||||||
|
return htmlSafe(`<span>${escaped}</span>`);
|
||||||
|
},
|
||||||
|
|
||||||
this.$list.scrollTop(yPosition);
|
_codeWithDiversity(code, selectedDiversity) {
|
||||||
|
if (selectedDiversity !== 0 && isSkinTonableEmoji(code)) {
|
||||||
// if we don’t actually scroll we need to force it
|
return `${code}:t${selectedDiversity + 1}`;
|
||||||
if (yPosition === 0) {
|
} else {
|
||||||
this.$list.scroll();
|
return code;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_codeForEmojiButton($button) {
|
_applyDiversity(diversity) {
|
||||||
const title = $button.attr("title");
|
const emojiPickerArea = document.querySelector(".emoji-picker-emoji-area");
|
||||||
return this._codeWithDiversity(title, $button.hasClass("diversity"));
|
|
||||||
|
emojiPickerArea &&
|
||||||
|
emojiPickerArea.querySelectorAll(".emoji.diversity").forEach(img => {
|
||||||
|
const code = this._codeWithDiversity(img.title, diversity);
|
||||||
|
img.src = emojiUrlFor(code);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_setButtonBackground(button, diversity) {
|
_setupSectionObserver() {
|
||||||
if (!button) {
|
return new IntersectionObserver(
|
||||||
return;
|
entries => {
|
||||||
}
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
const $button = $(button);
|
const sectionName = entry.target.parentNode.dataset.section;
|
||||||
button = $button[0];
|
const categoryButtons = document.querySelector(
|
||||||
|
".emoji-picker .emoji-picker-category-buttons"
|
||||||
// changing style can force layout events
|
|
||||||
// this could slow down timers and lead to
|
|
||||||
// chrome delaying the request
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
const code = this._codeWithDiversity(
|
|
||||||
$button.attr("title"),
|
|
||||||
diversity || $button.hasClass("diversity")
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// // force visual reloading if needed
|
if (!categoryButtons) return;
|
||||||
if (button.style.backgroundImage !== "none") {
|
|
||||||
button.style.backgroundImage = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
button.style.backgroundImage = `url("${emojiUrlFor(code)}")`;
|
const button = categoryButtons.querySelector(
|
||||||
$button.attr("data-loaded", 1);
|
`.category-button[data-section="${sectionName}"]`
|
||||||
|
);
|
||||||
|
|
||||||
|
categoryButtons
|
||||||
|
.querySelectorAll(".category-button")
|
||||||
|
.forEach(b => b.classList.remove("current"));
|
||||||
|
button && button.classList.add("current");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
{ threshold: 1 }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
@bind
|
||||||
|
handleOutsideClick(event) {
|
||||||
|
const emojiPicker = document.querySelector(".emoji-picker");
|
||||||
|
if (emojiPicker && !emojiPicker.contains(event.target)) {
|
||||||
|
this.onClose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,6 @@ import { registerUnbound } from "discourse-common/lib/helpers";
|
|||||||
import { emojiUnescape } from "discourse/lib/text";
|
import { emojiUnescape } from "discourse/lib/text";
|
||||||
import { htmlSafe } from "@ember/template";
|
import { htmlSafe } from "@ember/template";
|
||||||
|
|
||||||
registerUnbound("replace-emoji", text => {
|
registerUnbound("replace-emoji", (text, options) => {
|
||||||
return htmlSafe(emojiUnescape(text));
|
return htmlSafe(emojiUnescape(text, options));
|
||||||
});
|
});
|
||||||
|
@ -18,7 +18,8 @@ export default {
|
|||||||
group: "extras",
|
group: "extras",
|
||||||
icon: "far-smile",
|
icon: "far-smile",
|
||||||
action: () => toolbar.context.send("emoji"),
|
action: () => toolbar.context.send("emoji"),
|
||||||
title: "composer.emoji"
|
title: "composer.emoji",
|
||||||
|
className: "emoji insert-emoji"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -54,4 +54,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{emoji-picker active=emojiPickerIsActive isEditorFocused=isEditorFocused emojiSelected=(action "emojiSelected")}}
|
{{emoji-picker
|
||||||
|
isActive=emojiPickerIsActive
|
||||||
|
isEditorFocused=isEditorFocused
|
||||||
|
emojiSelected=(action "emojiSelected")
|
||||||
|
onEmojiPickerClose=(action (mut emojiPickerIsActive) false)
|
||||||
|
}}
|
||||||
|
@ -1,42 +1,56 @@
|
|||||||
<div class="emoji-picker">
|
{{#if isActive}}
|
||||||
{{#if active}}
|
<div class="emoji-picker {{if @isActive 'opened'}}">
|
||||||
<div class='categories-column'>
|
<div class="emoji-picker-category-buttons">
|
||||||
<div class='category-icon'>
|
{{#if recentEmojis.length}}
|
||||||
<button type="button" class="emoji" tabindex="-1" title="{{i18n 'emoji_picker.recent'}}" data-section="recent" data-tabicon="star"></button>
|
<button data-section="recent" {{action "onCategorySelection" "recent"}} class="btn btn-default category-button emoji">
|
||||||
</div>
|
{{replace-emoji ":star:"}}
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
<% JSON.parse(File.read("lib/emoji/groups.json")).each.with_index do |group, group_index| %>
|
<% JSON.parse(File.read("lib/emoji/groups.json")).each.with_index do |group, group_index| %>
|
||||||
<div class='category-icon'>
|
<button data-section="<%= group["name"] %>" {{action "onCategorySelection" "<%= group["name"] %>"}} class="btn btn-default category-button emoji">
|
||||||
<button type="button" class="emoji" tabindex="-1" data-tabicon="<%= group["tabicon"] %>" data-section="<%= group["name"] %>" title="{{i18n '<%= "emoji_picker.#{group["name"]}" %>'}}"></button>
|
{{replace-emoji ":<%= group["tabicon"] %>:"}}
|
||||||
</div>
|
</button>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
{{#each-in customEmojis as |group emojis|}}
|
{{#each-in customEmojis as |group emojis|}}
|
||||||
<div class='category-icon'>
|
<button data-section={{concat "custom-" group}} {{action "onCategorySelection" (concat "custom-" group)}} class="btn btn-default category-button emoji">
|
||||||
<button data-tabicon={{emojis.firstObject.code}} type="button" class="emoji" tabindex="-1" data-section="custom-{{group}}" title="{{i18n (concat 'emoji_picker.' group)}}"></button>
|
{{replace-emoji (concat ":" emojis.firstObject.code ":")}}
|
||||||
</div>
|
</button>
|
||||||
{{/each-in}}
|
{{/each-in}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='main-column'>
|
<div class="emoji-picker-content">
|
||||||
<div class='filter'>
|
<div class="emoji-picker-search-container">
|
||||||
{{d-icon 'search'}}
|
{{input
|
||||||
<input type='text' name="filter" placeholder="{{i18n 'emoji_picker.filter_placeholder'}}" autocomplete="discourse"/>
|
class="filter"
|
||||||
<button class='clear-filter'>
|
name="filter"
|
||||||
{{d-icon 'times'}}
|
placeholder=(i18n "emoji_picker.filter_placeholder")
|
||||||
</button>
|
autocomplete="discourse"
|
||||||
|
input=(action "onFilter")
|
||||||
|
}}
|
||||||
|
|
||||||
|
{{d-icon "search"}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="emoji-picker-emoji-area" {{on "click" this.onEmojiSelection}} {{on "mouseover" this.onEmojiHover}}>
|
||||||
<div class='results'></div>
|
<div class='results'></div>
|
||||||
|
|
||||||
<div class='list'>
|
{{#conditional-loading-spinner condition=isLoading}}
|
||||||
<div class='section' data-section='recent'>
|
<div class="emojis-container">
|
||||||
|
{{#if recentEmojis.length}}
|
||||||
|
<div class='section recent' data-section='recent'>
|
||||||
<div class='section-header'>
|
<div class='section-header'>
|
||||||
<span class="title">{{i18n 'emoji_picker.recent'}}</span>
|
<span class="title">{{i18n 'emoji_picker.recent'}}</span>
|
||||||
<a href='#' class='clear-recent'>{{d-icon "trash-alt"}}</a>
|
{{d-button icon="trash-alt" action=(action "onClearRecents") class="trash-recent"}}
|
||||||
</div>
|
</div>
|
||||||
<div class='section-group'></div>
|
<div class='section-group'>
|
||||||
|
{{#each recentEmojis as |emoji|}}
|
||||||
|
{{replace-emoji (concat ":" emoji ":") (hash lazy=true)}}
|
||||||
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
<% JSON.parse(File.read("lib/emoji/groups.json")).each.with_index do |group, group_index| %>
|
<% 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' data-section='<%= group["name"] %>'>
|
||||||
@ -45,7 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class='section-group'>
|
<div class='section-group'>
|
||||||
<% group["icons"].each do |icon| %>
|
<% group["icons"].each do |icon| %>
|
||||||
<button type="button" class="emoji <%= "diversity" if icon["diversity"] %>" tabindex="-1" title="<%= icon['name']%>"></button>
|
{{replace-emoji ":<%= icon['name'] %>:" (hash lazy=true class="<%= "diversity" if icon["diversity"] %>")}}
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -61,28 +75,38 @@
|
|||||||
{{#if emojis.length}}
|
{{#if emojis.length}}
|
||||||
<div class='section-group'>
|
<div class='section-group'>
|
||||||
{{#each emojis as |emoji|}}
|
{{#each emojis as |emoji|}}
|
||||||
<button type="button" class="custom emoji" tabindex="-1" title="{{emoji.code}}">
|
<img title="{{emoji.code}}" width=20 height=20 loading="lazy" class="emoji" src="{{emoji.src}}">
|
||||||
<img width=20 height=20 loading="lazy" class="emoji" src="{{emoji.src}}">
|
|
||||||
</button>
|
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
{{/each-in}}
|
{{/each-in}}
|
||||||
</div>
|
</div>
|
||||||
|
{{/conditional-loading-spinner}}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class='footer'>
|
<div class="emoji-picker-footer">
|
||||||
<div class='info'></div>
|
<div class="emoji-picker-emoji-info">
|
||||||
<div class='diversity-picker'>
|
{{#if hoveredEmoji}}
|
||||||
<% ['default', 'light', 'medium-light', 'medium', 'medium-dark', 'dark'].each.with_index do |diversity, index| %>
|
{{replace-emoji (concat ":" hoveredEmoji ":")}}
|
||||||
<a href='#' title="{{i18n 'emoji_picker.<%= diversity.gsub('-', '_') %>_tone'}}" class='diversity-scale <%= diversity %>' data-level="<%= index + 1 %>">
|
|
||||||
{{d-icon "check"}}
|
|
||||||
</a>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="emoji-picker-modal"></div>
|
<div class="emoji-picker-diversity-picker">
|
||||||
|
{{#each diversityScales as |diversityScale index|}}
|
||||||
|
{{d-button
|
||||||
|
icon=diversityScale.icon
|
||||||
|
class=(concat "diversity-scale " diversityScale.name)
|
||||||
|
title=(concat "emoji_picker." diversityScale.name "_tone")
|
||||||
|
action=(action "onDiversitySelection" index)
|
||||||
|
}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if site.mobileView}}
|
||||||
|
<div class="emoji-picker-modal-overlay" {{on "click" this.onClose}}></div>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
{{#each recentEmojis as |emoji|}}
|
|
||||||
<button style="background-image: url('{{emoji.src}}')" type="button" class="emoji" tabindex="-1" title="{{emoji.code}}"></button>
|
|
||||||
{{/each}}
|
|
@ -117,18 +117,22 @@ export function performEmojiUnescape(string, opts) {
|
|||||||
}
|
}
|
||||||
const hasEndingColon = m.lastIndexOf(":") === m.length - 1;
|
const hasEndingColon = m.lastIndexOf(":") === m.length - 1;
|
||||||
const url = buildEmojiUrl(emojiVal, opts);
|
const url = buildEmojiUrl(emojiVal, opts);
|
||||||
const classes = isCustomEmoji(emojiVal, opts)
|
let classes = isCustomEmoji(emojiVal, opts)
|
||||||
? "emoji emoji-custom"
|
? "emoji emoji-custom"
|
||||||
: "emoji";
|
: "emoji";
|
||||||
|
|
||||||
|
if (opts.class) {
|
||||||
|
classes = `${classes} ${opts.class}`;
|
||||||
|
}
|
||||||
|
|
||||||
const isReplacable =
|
const isReplacable =
|
||||||
(isEmoticon || hasEndingColon || isUnicodeEmoticon) &&
|
(isEmoticon || hasEndingColon || isUnicodeEmoticon) &&
|
||||||
isReplacableInlineEmoji(string, index, inlineEmoji);
|
isReplacableInlineEmoji(string, index, inlineEmoji);
|
||||||
|
|
||||||
return url && isReplacable
|
return url && isReplacable
|
||||||
? `<img src='${url}' ${
|
? `<img src='${url}' ${opts.skipTitle ? "" : `title='${emojiVal}'`} ${
|
||||||
opts.skipTitle ? "" : `title='${emojiVal}'`
|
opts.lazy ? "loading='lazy' " : ""
|
||||||
} alt='${emojiVal}' class='${classes}'>`
|
}alt='${emojiVal}' class='${classes}'>`
|
||||||
: m;
|
: m;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -28,15 +28,18 @@ sup img.emoji {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.emoji-picker {
|
.emoji-picker {
|
||||||
background-clip: padding-box;
|
width: 100%;
|
||||||
z-index: z("modal", "content");
|
|
||||||
position: fixed;
|
|
||||||
display: none;
|
|
||||||
flex-direction: row;
|
|
||||||
height: 320px;
|
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
background-color: var(--secondary);
|
background-color: var(--secondary);
|
||||||
border: 1px solid var(--primary-low);
|
border: 1px solid var(--primary-low);
|
||||||
|
display: flex;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-clip: padding-box;
|
||||||
|
z-index: z("modal", "content");
|
||||||
|
flex-direction: row;
|
||||||
|
height: 320px;
|
||||||
|
max-height: 50vh;
|
||||||
|
max-width: 420px;
|
||||||
|
|
||||||
img.emoji {
|
img.emoji {
|
||||||
// custom emojis might import images of various sizes
|
// custom emojis might import images of various sizes
|
||||||
@ -44,175 +47,176 @@ sup img.emoji {
|
|||||||
width: 20px !important;
|
width: 20px !important;
|
||||||
height: 20px !important;
|
height: 20px !important;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-picker .categories-column {
|
.emoji-picker-content {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1 0 0px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
border-right: 1px solid var(--primary-low);
|
|
||||||
min-width: 36px;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-picker .category-icon {
|
|
||||||
display: block;
|
|
||||||
margin: 4px auto;
|
|
||||||
-webkit-filter: grayscale(100%);
|
|
||||||
filter: grayscale(100%);
|
|
||||||
|
|
||||||
button.emoji {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-picker .category-icon.current,
|
|
||||||
.emoji-picker .category-icon:hover {
|
|
||||||
-webkit-filter: grayscale(0%);
|
|
||||||
filter: grayscale(0%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-picker .main-column {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 20;
|
flex: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-picker .list {
|
.emoji-picker-emoji-area {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
padding: 0;
|
width: 100%;
|
||||||
flex: 1 0 0px;
|
box-sizing: border-box;
|
||||||
flex-direction: column;
|
padding: 0.25em;
|
||||||
}
|
height: 100%;
|
||||||
|
background: $secondary;
|
||||||
|
|
||||||
.emoji-picker .section-header {
|
.section {
|
||||||
padding: 8px;
|
margin-bottom: 1em;
|
||||||
margin-top: 2px;
|
content-visibility: auto;
|
||||||
margin-bottom: 0px;
|
|
||||||
padding-bottom: 0px;
|
.trash-recent {
|
||||||
justify-content: space-between;
|
background: none;
|
||||||
|
font-size: $font-down-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
font-weight: 900;
|
||||||
|
padding: 0.25em 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-weight: bold;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-picker .section-header .title {
|
.section-group,
|
||||||
color: var(--primary);
|
.results {
|
||||||
}
|
img.emoji {
|
||||||
|
padding: 0.25em;
|
||||||
.emoji-picker .section-header .clear-recent .fa {
|
cursor: pointer;
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
color: var(--primary-medium);
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--primary-high);
|
background: $tertiary-low;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
padding: 0.25em 0;
|
||||||
|
img.emoji {
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-picker-category-buttons {
|
||||||
|
overflow-y: scroll;
|
||||||
|
width: 60px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
border-right: 1px solid $primary-low;
|
||||||
|
|
||||||
|
.category-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5em;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
.emoji {
|
||||||
|
pointer-events: none;
|
||||||
|
filter: grayscale(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .emoji,
|
||||||
|
&.current .emoji {
|
||||||
|
filter: grayscale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-picker .section-group {
|
.emoji-picker-search-container {
|
||||||
flex-wrap: wrap;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
padding: 0.25em;
|
||||||
|
border-bottom: 1px solid $primary-low;
|
||||||
|
box-sizing: border-box;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
|
||||||
padding: 4px;
|
.filter {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-icon {
|
||||||
|
color: $primary-medium;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-picker .footer {
|
.emoji-picker-footer {
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border-top: 1px solid var(--primary-low);
|
align-items: center;
|
||||||
|
border-top: 1px solid $primary-low;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-picker .info {
|
.emoji-picker-emoji-info {
|
||||||
@include ellipsis;
|
|
||||||
padding-left: 8px;
|
|
||||||
font-weight: 700;
|
|
||||||
max-width: 125px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-picker .diversity-picker {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
align-items: center;
|
||||||
padding: 8px;
|
padding-left: 0.5em;
|
||||||
|
|
||||||
|
img.emoji {
|
||||||
|
height: 32px !important;
|
||||||
|
width: 32px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-picker .diversity-picker .diversity-scale {
|
.emoji-picker-diversity-picker {
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
margin-left: 5px;
|
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 3px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
padding: 0.5em;
|
||||||
.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-picker .diversity-picker .diversity-scale.selected .d-icon {
|
.diversity-scale {
|
||||||
display: block;
|
display: flex;
|
||||||
}
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: auto;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin: 0.15em;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
|
||||||
.emoji-picker .diversity-picker .d-icon {
|
.d-icon {
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-picker .diversity-picker .d-icon {
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: $font-0;
|
|
||||||
filter: drop-shadow(0.5px 1.5px 0 rgba(0, 0, 0, 0.3));
|
filter: drop-shadow(0.5px 1.5px 0 rgba(0, 0, 0, 0.3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.diversity-scale.default {
|
||||||
|
background: #ffcc4d;
|
||||||
|
}
|
||||||
|
.diversity-scale.light {
|
||||||
|
background: #f7dece;
|
||||||
|
}
|
||||||
|
.diversity-scale.medium-light {
|
||||||
|
background: #f3d2a2;
|
||||||
|
}
|
||||||
|
.diversity-scale.medium {
|
||||||
|
background: #d5ab88;
|
||||||
|
}
|
||||||
|
.diversity-scale.medium-dark {
|
||||||
|
background: #af7e57;
|
||||||
|
}
|
||||||
|
.diversity-scale.dark {
|
||||||
|
background: #7c533e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-picker button.emoji {
|
.emoji-picker-modal-overlay {
|
||||||
background: transparent;
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
border-radius: 0;
|
|
||||||
background-size: 20px 20px;
|
|
||||||
-moz-box-sizing: content-box;
|
|
||||||
box-sizing: content-box;
|
|
||||||
height: 20px;
|
|
||||||
border: 0;
|
|
||||||
vertical-align: top;
|
|
||||||
width: 20px;
|
|
||||||
outline: none;
|
|
||||||
padding: 3px;
|
|
||||||
margin: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-picker .section-group button.emoji:hover,
|
|
||||||
.emoji-picker .results button.emoji:hover {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: top;
|
|
||||||
border-radius: 2px;
|
|
||||||
background-color: var(--tertiary-low);
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-picker-modal.fadeIn {
|
|
||||||
z-index: z("modal", "overlay");
|
z-index: z("modal", "overlay");
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -220,70 +224,5 @@ sup img.emoji {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
background-color: black;
|
background-color: $primary;
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-picker .filter {
|
|
||||||
background-color: none;
|
|
||||||
border-bottom: 1px solid var(--primary-low);
|
|
||||||
padding: 5px;
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
input[type="text"] {
|
|
||||||
width: auto !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-picker .filter .d-icon-search {
|
|
||||||
color: var(--primary-medium);
|
|
||||||
font-size: $font-up-1;
|
|
||||||
margin-left: 5px;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-picker .filter input {
|
|
||||||
height: 24px;
|
|
||||||
margin: 0;
|
|
||||||
flex: 1 0 0px;
|
|
||||||
border: none;
|
|
||||||
box-shadow: none;
|
|
||||||
padding-right: 24px;
|
|
||||||
outline: none;
|
|
||||||
color: var(--primary);
|
|
||||||
background: var(--secondary);
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-picker .filter input::-ms-clear {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-picker .results {
|
|
||||||
display: none;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
padding: 4px;
|
|
||||||
flex: 1 0 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-picker .filter .clear-filter {
|
|
||||||
position: absolute;
|
|
||||||
right: 5px;
|
|
||||||
top: 12px;
|
|
||||||
border: 0;
|
|
||||||
background: none;
|
|
||||||
color: var(--primary-high);
|
|
||||||
outline: none;
|
|
||||||
display: none;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--primary);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
.emoji-picker {
|
.emoji-picker {
|
||||||
max-height: 280px;
|
|
||||||
border: none;
|
border: none;
|
||||||
}
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
.emoji-picker .category-icon {
|
max-width: 100vh;
|
||||||
margin: 2px;
|
top: 0;
|
||||||
}
|
left: 0;
|
||||||
|
|
||||||
.emoji-picker .categories-column {
|
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { acceptance } from "helpers/qunit-helpers";
|
import { acceptance } from "helpers/qunit-helpers";
|
||||||
import { IMAGE_VERSION as v } from "pretty-text/emoji/version";
|
|
||||||
|
|
||||||
acceptance("EmojiPicker", {
|
acceptance("EmojiPicker", {
|
||||||
loggedIn: true,
|
loggedIn: true,
|
||||||
@ -17,45 +16,18 @@ QUnit.test("emoji picker can be opened/closed", async assert => {
|
|||||||
await click("#topic-footer-buttons .btn.create");
|
await click("#topic-footer-buttons .btn.create");
|
||||||
|
|
||||||
await click("button.emoji.btn");
|
await click("button.emoji.btn");
|
||||||
assert.notEqual(
|
assert.ok(exists(".emoji-picker.opened"), "it opens the picker");
|
||||||
find(".emoji-picker")
|
|
||||||
.html()
|
|
||||||
.trim(),
|
|
||||||
"<!---->",
|
|
||||||
"it opens the picker"
|
|
||||||
);
|
|
||||||
|
|
||||||
await click("button.emoji.btn");
|
await click("button.emoji.btn");
|
||||||
assert.equal(
|
assert.notOk(exists(".emoji-picker.opened"), "it closes the picker");
|
||||||
find(".emoji-picker")
|
|
||||||
.html()
|
|
||||||
.trim(),
|
|
||||||
"<!---->",
|
|
||||||
"it closes the picker"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
QUnit.test("emojis can be hovered to display info", async assert => {
|
|
||||||
await visit("/t/internationalization-localization/280");
|
|
||||||
await click("#topic-footer-buttons .btn.create");
|
|
||||||
|
|
||||||
await click("button.emoji.btn");
|
|
||||||
$(".emoji-picker button[title='grinning']").trigger("mouseover");
|
|
||||||
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 triggers event when picking emoji", async assert => {
|
QUnit.test("emoji picker triggers event when picking emoji", async assert => {
|
||||||
await visit("/t/internationalization-localization/280");
|
await visit("/t/internationalization-localization/280");
|
||||||
await click("#topic-footer-buttons .btn.create");
|
await click("#topic-footer-buttons .btn.create");
|
||||||
await click("button.emoji.btn");
|
await click("button.emoji.btn");
|
||||||
|
await click(".emoji-picker-emoji-area img.emoji[title='grinning']");
|
||||||
|
|
||||||
await click(".emoji-picker button[title='grinning']");
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
find(".d-editor-input").val(),
|
find(".d-editor-input").val(),
|
||||||
":grinning:",
|
":grinning:",
|
||||||
@ -72,24 +44,22 @@ QUnit.test(
|
|||||||
// Whitespace should be added on text
|
// Whitespace should be added on text
|
||||||
await fillIn(".d-editor-input", "This is a test input");
|
await fillIn(".d-editor-input", "This is a test input");
|
||||||
await click("button.emoji.btn");
|
await click("button.emoji.btn");
|
||||||
await click(".emoji-picker button[title='grinning']");
|
await click(".emoji-picker-emoji-area img.emoji[title='grinning']");
|
||||||
assert.equal(
|
assert.equal(
|
||||||
find(".d-editor-input").val(),
|
find(".d-editor-input").val(),
|
||||||
"This is a test input :grinning:",
|
"This is a test input :grinning:",
|
||||||
"it adds the emoji code and a leading whitespace when there is text"
|
"it adds the emoji code and a leading whitespace when there is text"
|
||||||
);
|
);
|
||||||
await click("button.emoji.btn");
|
|
||||||
|
|
||||||
// Whitespace should not be added on whitespace
|
// Whitespace should not be added on whitespace
|
||||||
await fillIn(".d-editor-input", "This is a test input ");
|
await fillIn(".d-editor-input", "This is a test input ");
|
||||||
await click("button.emoji.btn");
|
await click(".emoji-picker-emoji-area img.emoji[title='grinning']");
|
||||||
await click(".emoji-picker button[title='grinning']");
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
find(".d-editor-input").val(),
|
find(".d-editor-input").val(),
|
||||||
"This is a test input :grinning:",
|
"This is a test input :grinning:",
|
||||||
"it adds the emoji code and no leading whitespace when user already entered whitespace"
|
"it adds the emoji code and no leading whitespace when user already entered whitespace"
|
||||||
);
|
);
|
||||||
await click("button.emoji.btn");
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -97,44 +67,36 @@ QUnit.test("emoji picker has a list of recently used emojis", async assert => {
|
|||||||
await visit("/t/internationalization-localization/280");
|
await visit("/t/internationalization-localization/280");
|
||||||
await click("#topic-footer-buttons .btn.create");
|
await click("#topic-footer-buttons .btn.create");
|
||||||
await click("button.emoji.btn");
|
await click("button.emoji.btn");
|
||||||
|
await click(".emoji-picker-emoji-area img.emoji[title='grinning']");
|
||||||
|
|
||||||
await click(
|
assert.ok(
|
||||||
".emoji-picker .section[data-section='smileys_&_emotion'] button.emoji[title='grinning']"
|
exists(
|
||||||
);
|
".emoji-picker .section.recent .section-group img.emoji[title='grinning']"
|
||||||
assert.equal(
|
),
|
||||||
find('.emoji-picker .section[data-section="recent"]').css("display"),
|
"it shows recent selected emoji"
|
||||||
"block",
|
|
||||||
"it shows recent section"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(
|
assert.ok(
|
||||||
find(
|
exists('.emoji-picker .category-button[data-section="recent"]'),
|
||||||
'.emoji-picker .section[data-section="recent"] .section-group button.emoji'
|
"it shows recent category icon"
|
||||||
).length,
|
|
||||||
1,
|
|
||||||
"it adds the emoji code to the recently used emojis list"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await click(".emoji-picker .clear-recent");
|
await click(".emoji-picker .trash-recent");
|
||||||
assert.equal(
|
|
||||||
find(
|
assert.notOk(
|
||||||
'.emoji-picker .section[data-section="recent"] .section-group button.emoji'
|
exists(
|
||||||
).length,
|
".emoji-picker .section.recent .section-group img.emoji[title='grinning']"
|
||||||
0,
|
),
|
||||||
"it has cleared recent emojis"
|
"it has cleared recent emojis"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(
|
assert.notOk(
|
||||||
find('.emoji-picker .section[data-section="recent"]').css("display"),
|
exists('.emoji-picker .section[data-section="recent"]'),
|
||||||
"none",
|
|
||||||
"it hides recent section"
|
"it hides recent section"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(
|
assert.notOk(
|
||||||
find('.emoji-picker .category-icon button.emoji[data-section="recent"]')
|
exists('.emoji-picker .category-button[data-section="recent"]'),
|
||||||
.parent()
|
|
||||||
.css("display"),
|
|
||||||
"none",
|
|
||||||
"it hides recent category icon"
|
"it hides recent category icon"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -144,22 +106,21 @@ QUnit.test(
|
|||||||
async assert => {
|
async assert => {
|
||||||
await visit("/t/internationalization-localization/280");
|
await visit("/t/internationalization-localization/280");
|
||||||
await click("#topic-footer-buttons .btn.create");
|
await click("#topic-footer-buttons .btn.create");
|
||||||
|
|
||||||
await click("button.emoji.btn");
|
await click("button.emoji.btn");
|
||||||
await click(".emoji-picker button[title='sunglasses']");
|
await click(".emoji-picker-emoji-area img.emoji[title='sunglasses']");
|
||||||
await click(".emoji-picker button[title='grinning']");
|
await click(".emoji-picker-emoji-area img.emoji[title='grinning']");
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
find('.section[data-section="recent"] .section-group button.emoji')
|
find('.section[data-section="recent"] .section-group img.emoji').length,
|
||||||
.length,
|
|
||||||
2,
|
2,
|
||||||
"it has multiple recent emojis"
|
"it has multiple recent emojis"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
/grinning/.test(
|
/grinning/.test(
|
||||||
find('.section[data-section="recent"] .section-group button.emoji')
|
find(".section.recent .section-group img.emoji")
|
||||||
.first()
|
.first()
|
||||||
.css("background-image")
|
.attr("src")
|
||||||
),
|
),
|
||||||
true,
|
true,
|
||||||
"it puts the last used emoji in first"
|
"it puts the last used emoji in first"
|
||||||
@ -170,14 +131,13 @@ QUnit.test(
|
|||||||
QUnit.test("emoji picker persists state", async assert => {
|
QUnit.test("emoji picker persists state", async assert => {
|
||||||
await visit("/t/internationalization-localization/280");
|
await visit("/t/internationalization-localization/280");
|
||||||
await click("#topic-footer-buttons .btn.create");
|
await click("#topic-footer-buttons .btn.create");
|
||||||
|
|
||||||
await click("button.emoji.btn");
|
await click("button.emoji.btn");
|
||||||
await click(".emoji-picker a.diversity-scale.medium-dark");
|
await click(".emoji-picker button.diversity-scale.medium-dark");
|
||||||
|
await click("button.emoji.btn");
|
||||||
await click("button.emoji.btn");
|
await click("button.emoji.btn");
|
||||||
|
|
||||||
await click("button.emoji.btn");
|
assert.ok(
|
||||||
assert.equal(
|
exists(".emoji-picker button.diversity-scale.medium-dark .d-icon"),
|
||||||
find(".emoji-picker .diversity-scale.medium-dark").hasClass("selected"),
|
|
||||||
true,
|
true,
|
||||||
"it stores diversity scale"
|
"it stores diversity scale"
|
||||||
);
|
);
|
||||||
|
@ -658,7 +658,7 @@ componentTest("emoji", {
|
|||||||
await click("button.emoji");
|
await click("button.emoji");
|
||||||
|
|
||||||
await click(
|
await click(
|
||||||
'.emoji-picker .section[data-section="smileys_&_emotion"] button.emoji[title="grinning"]'
|
'.emoji-picker .section[data-section="smileys_&_emotion"] img.emoji[title="grinning"]'
|
||||||
);
|
);
|
||||||
assert.equal(this.value, "hello world. :grinning:");
|
assert.equal(this.value, "hello world. :grinning:");
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user