419 lines
10 KiB
JavaScript
419 lines
10 KiB
JavaScript
import Component from "@ember/component";
|
|
import { htmlSafe } from "@ember/template";
|
|
import { action } from "@ember/object";
|
|
import { inject as service } from "@ember/service";
|
|
import { tracked } from "@glimmer/tracking";
|
|
import { emojiUrlFor } from "discourse/lib/text";
|
|
import discourseDebounce from "discourse-common/lib/debounce";
|
|
import { INPUT_DELAY } from "discourse-common/config/environment";
|
|
import { bind } from "discourse-common/utils/decorators";
|
|
import { later, schedule } from "@ember/runloop";
|
|
|
|
export const FITZPATRICK_MODIFIERS = [
|
|
{
|
|
scale: 1,
|
|
modifier: null,
|
|
},
|
|
{
|
|
scale: 2,
|
|
modifier: ":t2",
|
|
},
|
|
{
|
|
scale: 3,
|
|
modifier: ":t3",
|
|
},
|
|
{
|
|
scale: 4,
|
|
modifier: ":t4",
|
|
},
|
|
{
|
|
scale: 5,
|
|
modifier: ":t5",
|
|
},
|
|
{
|
|
scale: 6,
|
|
modifier: ":t6",
|
|
},
|
|
];
|
|
|
|
export default class ChatEmojiPicker extends Component {
|
|
@service chatEmojiPickerManager;
|
|
@service emojiPickerScrollObserver;
|
|
@service chatEmojiReactionStore;
|
|
@tracked filteredEmojis = null;
|
|
@tracked isExpandedFitzpatrickScale = false;
|
|
tagName = "";
|
|
|
|
fitzpatrickModifiers = FITZPATRICK_MODIFIERS;
|
|
|
|
get groups() {
|
|
const emojis = this.chatEmojiPickerManager.emojis;
|
|
const favorites = {
|
|
favorites: this.chatEmojiReactionStore.favorites.map((name) => {
|
|
return {
|
|
name,
|
|
group: "favorites",
|
|
url: emojiUrlFor(name),
|
|
};
|
|
}),
|
|
};
|
|
|
|
return {
|
|
...favorites,
|
|
...emojis,
|
|
};
|
|
}
|
|
|
|
get flatEmojis() {
|
|
// eslint-disable-next-line no-unused-vars
|
|
let { favorites, ...rest } = this.chatEmojiPickerManager.emojis;
|
|
return Object.values(rest).flat();
|
|
}
|
|
|
|
get navIndicatorStyle() {
|
|
const section = this.chatEmojiPickerManager.lastVisibleSection;
|
|
const index = Object.keys(this.groups).indexOf(section);
|
|
|
|
return htmlSafe(
|
|
`width: ${
|
|
100 / Object.keys(this.groups).length
|
|
}%; transform: translateX(${index * 100}%);`
|
|
);
|
|
}
|
|
|
|
get navBtnStyle() {
|
|
return htmlSafe(`width: ${100 / Object.keys(this.groups).length}%;`);
|
|
}
|
|
|
|
@action
|
|
trapKeyDownEvents(event) {
|
|
if (event.key === "Escape") {
|
|
this.chatEmojiPickerManager.close();
|
|
}
|
|
|
|
if (event.key === "ArrowUp") {
|
|
event.stopPropagation();
|
|
}
|
|
|
|
if (
|
|
event.key === "ArrowDown" &&
|
|
event.target.classList.contains("dc-filter-input")
|
|
) {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
|
|
document
|
|
.querySelector(
|
|
`.chat-emoji-picker__scrollable-content .emoji[tabindex="0"]`
|
|
)
|
|
?.focus();
|
|
}
|
|
}
|
|
|
|
@action
|
|
didNavigateFitzpatrickScale(event) {
|
|
if (event.type !== "keyup") {
|
|
return;
|
|
}
|
|
|
|
const scaleNodes =
|
|
event.target
|
|
.closest(".chat-emoji-picker__fitzpatrick-scale")
|
|
?.querySelectorAll(".chat-emoji-picker__fitzpatrick-modifier-btn") ||
|
|
[];
|
|
|
|
const scales = [...scaleNodes];
|
|
|
|
if (event.key === "ArrowRight") {
|
|
event.preventDefault();
|
|
|
|
if (event.target === scales[scales.length - 1]) {
|
|
scales[0].focus();
|
|
} else {
|
|
event.target.nextElementSibling?.focus();
|
|
}
|
|
}
|
|
|
|
if (event.key === "ArrowLeft") {
|
|
event.preventDefault();
|
|
|
|
if (event.target === scales[0]) {
|
|
scales[scales.length - 1].focus();
|
|
} else {
|
|
event.target.previousElementSibling?.focus();
|
|
}
|
|
}
|
|
}
|
|
|
|
@action
|
|
didToggleFitzpatrickScale(event) {
|
|
if (event.type === "keyup") {
|
|
if (event.key === "Escape") {
|
|
event.preventDefault();
|
|
this.isExpandedFitzpatrickScale = false;
|
|
return;
|
|
}
|
|
|
|
if (event.key !== "Enter") {
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.toggleProperty("isExpandedFitzpatrickScale");
|
|
}
|
|
|
|
@action
|
|
didRequestFitzpatrickScale(scale, event) {
|
|
if (event.type === "keyup") {
|
|
if (event.key === "Escape") {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
this.isExpandedFitzpatrickScale = false;
|
|
this._focusCurrentFitzpatrickScale();
|
|
return;
|
|
}
|
|
|
|
if (event.key !== "Enter") {
|
|
return;
|
|
}
|
|
}
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
this.isExpandedFitzpatrickScale = false;
|
|
this.chatEmojiReactionStore.diversity = scale;
|
|
this._focusCurrentFitzpatrickScale();
|
|
}
|
|
|
|
_focusCurrentFitzpatrickScale() {
|
|
schedule("afterRender", () => {
|
|
document
|
|
.querySelector(".chat-emoji-picker__fitzpatrick-modifier-btn.current")
|
|
?.focus();
|
|
});
|
|
}
|
|
|
|
@action
|
|
didInputFilter(value) {
|
|
if (!value?.length) {
|
|
this.filteredEmojis = null;
|
|
return;
|
|
}
|
|
|
|
discourseDebounce(this, this.debouncedDidInputFilter, value, INPUT_DELAY);
|
|
}
|
|
|
|
@action
|
|
focusFilter(target) {
|
|
target.focus();
|
|
}
|
|
|
|
debouncedDidInputFilter(filter = "") {
|
|
filter = filter.toLowerCase();
|
|
|
|
this.filteredEmojis = this.flatEmojis.filter(
|
|
(emoji) =>
|
|
emoji.name.toLowerCase().includes(filter) ||
|
|
emoji.search_aliases?.any((alias) =>
|
|
alias.toLowerCase().includes(filter)
|
|
)
|
|
);
|
|
|
|
schedule("afterRender", () => {
|
|
const scrollableContent = document.querySelector(
|
|
".chat-emoji-picker__scrollable-content"
|
|
);
|
|
|
|
if (scrollableContent) {
|
|
scrollableContent.scrollTop = 0;
|
|
}
|
|
});
|
|
}
|
|
|
|
@action
|
|
onSectionsKeyDown(event) {
|
|
if (event.key === "Enter") {
|
|
this.didSelectEmoji(event);
|
|
} else {
|
|
this.didNavigateSection(event);
|
|
}
|
|
}
|
|
|
|
@action
|
|
didNavigateSection(event) {
|
|
const sectionsEmojis = (section) => [...section.querySelectorAll(".emoji")];
|
|
const focusSectionsLastEmoji = (section) => {
|
|
const emojis = sectionsEmojis(section);
|
|
return emojis[emojis.length - 1].focus();
|
|
};
|
|
const focusSectionsFirstEmoji = (section) => {
|
|
sectionsEmojis(section)[0].focus();
|
|
};
|
|
const currentSection = event.target.closest(".chat-emoji-picker__section");
|
|
const focusFilter = () => {
|
|
document.querySelector(".dc-filter-input")?.focus();
|
|
};
|
|
const allEmojis = () => [
|
|
...document.querySelectorAll(
|
|
".chat-emoji-picker__section:not(.hidden) .emoji"
|
|
),
|
|
];
|
|
|
|
if (event.key === "ArrowRight") {
|
|
event.preventDefault();
|
|
const nextEmoji = event.target.nextElementSibling;
|
|
|
|
if (nextEmoji) {
|
|
nextEmoji.focus();
|
|
} else {
|
|
const nextSection = currentSection.nextElementSibling;
|
|
if (nextSection) {
|
|
focusSectionsFirstEmoji(nextSection);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (event.key === "ArrowLeft") {
|
|
event.preventDefault();
|
|
const prevEmoji = event.target.previousElementSibling;
|
|
|
|
if (prevEmoji) {
|
|
prevEmoji.focus();
|
|
} else {
|
|
const prevSection = currentSection.previousElementSibling;
|
|
if (prevSection) {
|
|
focusSectionsLastEmoji(prevSection);
|
|
} else {
|
|
focusFilter();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (event.key === "ArrowDown") {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
const nextEmoji = allEmojis()
|
|
.filter((c) => c.offsetTop > event.target.offsetTop)
|
|
.findBy("offsetLeft", event.target.offsetLeft);
|
|
|
|
if (nextEmoji) {
|
|
nextEmoji.focus();
|
|
} else {
|
|
// for perf reason all emojis might not be loaded at this point
|
|
// but the first one will always be
|
|
const nextSection = currentSection.nextElementSibling;
|
|
if (nextSection) {
|
|
focusSectionsFirstEmoji(nextSection);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (event.key === "ArrowUp") {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
const prevEmoji = allEmojis()
|
|
.reverse()
|
|
.filter((c) => c.offsetTop < event.target.offsetTop)
|
|
.findBy("offsetLeft", event.target.offsetLeft);
|
|
|
|
if (prevEmoji) {
|
|
prevEmoji.focus();
|
|
} else {
|
|
focusFilter();
|
|
}
|
|
}
|
|
}
|
|
|
|
@action
|
|
didSelectEmoji(event) {
|
|
if (!event.target.classList.contains("emoji")) {
|
|
return;
|
|
}
|
|
|
|
if (event.type === "click" || event.key === "Enter") {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
let emoji = event.target.dataset.emoji;
|
|
const tonable = event.target.dataset.tonable;
|
|
const diversity = this.chatEmojiReactionStore.diversity;
|
|
if (tonable && diversity > 1) {
|
|
emoji = `${emoji}:t${diversity}`;
|
|
}
|
|
|
|
this.chatEmojiPickerManager.didSelectEmoji(emoji);
|
|
this.appEvents.trigger("chat:focus-composer");
|
|
}
|
|
}
|
|
|
|
@action
|
|
didFocusFirstEmoji(event) {
|
|
event.preventDefault();
|
|
const section = event.target.closest(".chat-emoji-picker__section").dataset
|
|
.section;
|
|
this.didRequestSection(section);
|
|
}
|
|
|
|
@action
|
|
didRequestSection(section) {
|
|
const scrollableContent = document.querySelector(
|
|
".chat-emoji-picker__scrollable-content"
|
|
);
|
|
|
|
this.filteredEmojis = null;
|
|
|
|
// we disable scroll listener during requesting section
|
|
// to avoid it from detecting another section during scroll to requested section
|
|
this.emojiPickerScrollObserver.enabled = false;
|
|
this.chatEmojiPickerManager.addVisibleSections([section]);
|
|
this.chatEmojiPickerManager.lastVisibleSection = section;
|
|
|
|
// iOS hack to avoid blank div when requesting section during momentum
|
|
if (scrollableContent && this.capabilities.isIOS) {
|
|
document.querySelector(
|
|
".chat-emoji-picker__scrollable-content"
|
|
).style.overflow = "hidden";
|
|
}
|
|
|
|
schedule("afterRender", () => {
|
|
document
|
|
.querySelector(`.chat-emoji-picker__section[data-section="${section}"]`)
|
|
.scrollIntoView({
|
|
behavior: "auto",
|
|
block: "start",
|
|
inline: "nearest",
|
|
});
|
|
|
|
later(() => {
|
|
// iOS hack to avoid blank div when requesting section during momentum
|
|
if (scrollableContent && this.capabilities.isIOS) {
|
|
document.querySelector(
|
|
".chat-emoji-picker__scrollable-content"
|
|
).style.overflow = "scroll";
|
|
}
|
|
|
|
this.emojiPickerScrollObserver.enabled = true;
|
|
}, 200);
|
|
});
|
|
}
|
|
|
|
@action
|
|
addClickOutsideEventListener() {
|
|
document.addEventListener("click", this.didClickOutside);
|
|
}
|
|
|
|
@action
|
|
removeClickOutsideEventListener() {
|
|
document.removeEventListener("click", this.didClickOutside);
|
|
}
|
|
|
|
@bind
|
|
didClickOutside(event) {
|
|
if (!event.target.closest(".chat-emoji-picker")) {
|
|
this.chatEmojiPickerManager.close();
|
|
}
|
|
}
|
|
}
|