[IMP] web_editor: add emoji picker in web editor
Purpose: This commit adds Emoji_picker functionality to the web_editor, allowing users to conveniently add emojis to their text and improving the overall user experience. Specifications: Now we can insert emoji in web_editor using `/emoji`. Task id: 3253958 closes odoo/odoo#125137 Related: odoo/enterprise#42772 Signed-off-by: Simon Genin (ges@odoo) <ges@odoo.com>
This commit is contained in:
@@ -98,7 +98,6 @@ Help your customers with this chat, and analyse their feedback.
|
||||
'mail/static/src/core/common/**/*',
|
||||
'mail/static/src/utils/common/**/*',
|
||||
('remove', 'mail/static/src/**/*.dark.scss'),
|
||||
('remove', 'mail/static/src/core/common/emoji_data.js'),
|
||||
|
||||
'im_livechat/static/src/embed/**/*',
|
||||
'im_livechat/static/src/embed/livechat_data.js',
|
||||
@@ -118,6 +117,7 @@ Help your customers with this chat, and analyse their feedback.
|
||||
'web/static/src/env.js',
|
||||
('remove', 'web/static/src/legacy/js/services/ajax_service.js'),
|
||||
('remove', 'web/static/src/legacy/js/common_env.js'),
|
||||
('remove', 'web/static/src/core/emoji_picker/emoji_data.js'),
|
||||
'web/static/src/views/fields/formatters.js',
|
||||
'web/static/src/views/fields/file_handler.*',
|
||||
'bus/static/src/bus_parameters_service.js',
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
.o-mail-EmojiPicker {
|
||||
.o-EmojiPicker {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ QUnit.test("add an emoji after a canned response", async (assert) => {
|
||||
"previous content + canned response substitution + additional whitespace afterwards"
|
||||
);
|
||||
await click("button[aria-label='Emojis']");
|
||||
await click(".o-mail-Emoji:contains(😊)");
|
||||
await click(".o-Emoji:contains(😊)");
|
||||
assert.strictEqual(
|
||||
$(".o-mail-Composer-input").val().replace(/\s/, " "),
|
||||
"Hello! How are you? 😊"
|
||||
|
||||
@@ -132,7 +132,6 @@ For more specific needs, you may also assign custom-defined actions
|
||||
'mail/static/src/**/common/**/*',
|
||||
'mail/static/src/**/web/**/*',
|
||||
('remove', 'mail/static/src/**/*.dark.scss'),
|
||||
('remove', 'mail/static/src/core/common/emoji_data.js'),
|
||||
# discuss (loaded last to fix dependencies)
|
||||
('remove', 'mail/static/src/discuss/**/*'),
|
||||
'mail/static/src/discuss/core/common/**/*',
|
||||
@@ -160,9 +159,7 @@ For more specific needs, you may also assign custom-defined actions
|
||||
('remove', 'mail/static/tests/tours/**/*'),
|
||||
('remove', 'mail/static/tests/helpers/**/*'),
|
||||
],
|
||||
'mail.assets_emoji': [
|
||||
'mail/static/src/core/common/emoji_data.js',
|
||||
],
|
||||
|
||||
'mail.assets_public': [
|
||||
('include', 'web._assets_helpers'),
|
||||
('include', 'web._assets_backend_helpers'),
|
||||
@@ -174,6 +171,7 @@ For more specific needs, you may also assign custom-defined actions
|
||||
'web/static/src/core/**/*',
|
||||
'web/static/src/env.js',
|
||||
('remove', 'web/static/src/legacy/js/services/ajax_service.js'),
|
||||
('remove', 'web/static/src/core/emoji_picker/emoji_data.js'),
|
||||
'web/static/src/views/fields/formatters.js',
|
||||
'web/static/src/views/fields/file_handler.*',
|
||||
|
||||
@@ -189,7 +187,6 @@ For more specific needs, you may also assign custom-defined actions
|
||||
# 'mail/static/src/core/public/**/*',
|
||||
'mail/static/src/**/common/**/*',
|
||||
'mail/static/src/**/public/**/*',
|
||||
('remove', 'mail/static/src/core/common/emoji_data.js'),
|
||||
('remove', 'mail/static/src/**/*.dark.scss'),
|
||||
# discuss (loaded last to fix dependencies)
|
||||
('remove', 'mail/static/src/discuss/**/*'),
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
useMessageHighlight,
|
||||
useMessageToReplyTo,
|
||||
} from "@mail/utils/common/hooks";
|
||||
import { isEventHandled } from "@mail/utils/common/misc";
|
||||
import { isEventHandled } from "@web/core/utils/misc";
|
||||
|
||||
import { Component, useChildSubEnv, useRef, useState } from "@odoo/owl";
|
||||
|
||||
|
||||
@@ -10,11 +10,8 @@ import { NavigableList } from "@mail/core/common/navigable_list";
|
||||
import { useSuggestion } from "@mail/core/common/suggestion_hook";
|
||||
import { escapeAndCompactTextContent } from "@mail/utils/common/format";
|
||||
import { useSelection } from "@mail/utils/common/hooks";
|
||||
import {
|
||||
isDragSourceExternalFile,
|
||||
isEventHandled,
|
||||
markEventHandled,
|
||||
} from "@mail/utils/common/misc";
|
||||
import { isDragSourceExternalFile } from "@mail/utils/common/misc";
|
||||
import { isEventHandled, markEventHandled } from "@web/core/utils/misc";
|
||||
|
||||
import {
|
||||
Component,
|
||||
|
||||
@@ -14,7 +14,7 @@ import { MessageSeenIndicator } from "@mail/core/common/message_seen_indicator";
|
||||
import { useMessaging, useStore } from "@mail/core/common/messaging_hook";
|
||||
import { RelativeTime } from "@mail/core/common/relative_time";
|
||||
import { convertBrToLineBreak, htmlToTextContentInline } from "@mail/utils/common/format";
|
||||
import { isEventHandled, markEventHandled } from "@mail/utils/common/misc";
|
||||
import { isEventHandled, markEventHandled } from "@web/core/utils/misc";
|
||||
|
||||
import {
|
||||
Component,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { ImStatus } from "@mail/core/common/im_status";
|
||||
import { onExternalClick } from "@mail/utils/common/hooks";
|
||||
import { markEventHandled, isEventHandled } from "@mail/utils/common/misc";
|
||||
import { markEventHandled, isEventHandled } from "@web/core/utils/misc";
|
||||
|
||||
import { Component, useEffect, useExternalListener, useRef, useState } from "@odoo/owl";
|
||||
|
||||
|
||||
@@ -928,7 +928,7 @@ export class ThreadService {
|
||||
const prettyContent = await prettifyMessageContent(body, params.validMentions);
|
||||
const { emojis } = await loadEmoji();
|
||||
const recentEmojis = JSON.parse(
|
||||
browser.localStorage.getItem("mail.emoji.frequent") || "{}"
|
||||
browser.localStorage.getItem("web.emoji.frequent") || "{}"
|
||||
);
|
||||
const emojisInContent =
|
||||
prettyContent.match(/\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu) ?? [];
|
||||
@@ -938,7 +938,7 @@ export class ThreadService {
|
||||
recentEmojis[codepoints]++;
|
||||
}
|
||||
}
|
||||
browser.localStorage.setItem("mail.emoji.frequent", JSON.stringify(recentEmojis));
|
||||
browser.localStorage.setItem("web.emoji.frequent", JSON.stringify(recentEmojis));
|
||||
tmpMsg = this.messageService.insert({
|
||||
...tmpData,
|
||||
body: markup(prettyContent),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* @odoo-module */
|
||||
|
||||
import { Message } from "@mail/core/common/message";
|
||||
import { markEventHandled } from "@mail/utils/common/misc";
|
||||
import { markEventHandled } from "@web/core/utils/misc";
|
||||
|
||||
import { getCurrency } from "@web/core/currency";
|
||||
import { deserializeDateTime } from "@web/core/l10n/dates";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ImStatus } from "@mail/core/common/im_status";
|
||||
import { useMessaging, useStore } from "@mail/core/common/messaging_hook";
|
||||
import { ThreadIcon } from "@mail/core/common/thread_icon";
|
||||
import { onExternalClick } from "@mail/utils/common/hooks";
|
||||
import { markEventHandled } from "@mail/utils/common/misc";
|
||||
import { markEventHandled } from "@web/core/utils/misc";
|
||||
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useMessaging, useStore } from "@mail/core/common/messaging_hook";
|
||||
import { CallActionList } from "@mail/discuss/call/common/call_action_list";
|
||||
import { CallParticipantCard } from "@mail/discuss/call/common/call_participant_card";
|
||||
import { useRtc } from "@mail/discuss/call/common/rtc_hook";
|
||||
import { isEventHandled, markEventHandled } from "@mail/utils/common/misc";
|
||||
import { isEventHandled, markEventHandled } from "@web/core/utils/misc";
|
||||
|
||||
import {
|
||||
Component,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { CallContextMenu } from "@mail/discuss/call/common/call_context_menu";
|
||||
import { CallParticipantVideo } from "@mail/discuss/call/common/call_participant_video";
|
||||
import { useRtc } from "@mail/discuss/call/common/rtc_hook";
|
||||
import { useHover } from "@mail/utils/common/hooks";
|
||||
import { isEventHandled, markEventHandled } from "@mail/utils/common/misc";
|
||||
import { isEventHandled, markEventHandled } from "@web/core/utils/misc";
|
||||
|
||||
import { Component, onMounted, onWillUnmount, useRef } from "@odoo/owl";
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useStore } from "@mail/core/common/messaging_hook";
|
||||
import { NavigableList } from "@mail/core/common/navigable_list";
|
||||
import { DEBOUNCE_FETCH_SUGGESTION_TIME } from "@mail/core/common/suggestion_service";
|
||||
import { cleanTerm } from "@mail/utils/common/format";
|
||||
import { createLocalId, isEventHandled, markEventHandled } from "@mail/utils/common/misc";
|
||||
import { createLocalId } from "@mail/utils/common/misc";
|
||||
import { isEventHandled, markEventHandled } from "@web/core/utils/misc";
|
||||
|
||||
import { Component, onMounted, useRef, useState } from "@odoo/owl";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Message } from "@mail/core/common/message";
|
||||
import "@mail/core/web/message_patch"; // dependency ordering
|
||||
import { markEventHandled } from "@mail/utils/common/misc";
|
||||
import { markEventHandled } from "@web/core/utils/misc";
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { GifPicker, useGifPicker } from "@mail/discuss/gif_picker/common/gif_picker";
|
||||
import { Composer } from "@mail/core/common/composer";
|
||||
import { onExternalClick } from "@mail/utils/common/hooks";
|
||||
import { isEventHandled, markEventHandled } from "@mail/utils/common/misc";
|
||||
import { isEventHandled, markEventHandled } from "@web/core/utils/misc";
|
||||
|
||||
import { useState } from "@odoo/owl";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useStore } from "@mail/core/common/messaging_hook";
|
||||
import { removeFromArrayWithPredicate } from "@mail/utils/common/arrays";
|
||||
import { useOnBottomScrolled } from "@mail/utils/common/hooks";
|
||||
import { markEventHandled } from "@mail/utils/common/misc";
|
||||
import { markEventHandled } from "@web/core/utils/misc";;
|
||||
|
||||
import { Component, onWillStart, useRef, useState } from "@odoo/owl";
|
||||
|
||||
|
||||
@@ -39,36 +39,6 @@ export function isDragSourceExternalFile(dataTransfer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const eventHandledWeakMap = new WeakMap();
|
||||
|
||||
/**
|
||||
* Returns whether the given event has been handled with the given markName.
|
||||
*
|
||||
* @param {Event} ev
|
||||
* @param {string} markName
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isEventHandled(ev, markName) {
|
||||
if (!eventHandledWeakMap.get(ev)) {
|
||||
return false;
|
||||
}
|
||||
return eventHandledWeakMap.get(ev).includes(markName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the given event as handled by the given markName. Useful to allow
|
||||
* handlers in the propagation chain to make a decision based on what has
|
||||
* already been done.
|
||||
*
|
||||
* @param {Event} ev
|
||||
* @param {string} markName
|
||||
*/
|
||||
export function markEventHandled(ev, markName) {
|
||||
if (!eventHandledWeakMap.get(ev)) {
|
||||
eventHandledWeakMap.set(ev, []);
|
||||
}
|
||||
eventHandledWeakMap.get(ev).push(markName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} target
|
||||
|
||||
@@ -279,7 +279,7 @@ QUnit.test(
|
||||
await start();
|
||||
await click("button[aria-label='Emojis']");
|
||||
await afterNextRender(() => triggerHotkey("Escape"));
|
||||
assert.containsNone($, ".o-mail-EmojiPicker");
|
||||
assert.containsNone($, ".o-EmojiPicker");
|
||||
assert.containsOnce($, ".o-mail-ChatWindow");
|
||||
}
|
||||
);
|
||||
|
||||
@@ -97,7 +97,7 @@ QUnit.test("add an emoji", async (assert) => {
|
||||
const { openDiscuss } = await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[aria-label='Emojis']");
|
||||
await click(".o-mail-Emoji:contains(😤)");
|
||||
await click(".o-Emoji:contains(😤)");
|
||||
assert.strictEqual($(".o-mail-Composer-input").val(), "😤");
|
||||
});
|
||||
|
||||
@@ -123,7 +123,7 @@ QUnit.test("add an emoji after some text", async (assert) => {
|
||||
assert.strictEqual($(".o-mail-Composer-input").val(), "Blabla");
|
||||
|
||||
await click("button[aria-label='Emojis']");
|
||||
await click(".o-mail-Emoji:contains(🤑)");
|
||||
await click(".o-Emoji:contains(🤑)");
|
||||
assert.strictEqual($(".o-mail-Composer-input").val(), "Blabla🤑");
|
||||
});
|
||||
|
||||
@@ -139,7 +139,7 @@ QUnit.test("add emoji replaces (keyboard) text selection", async (assert) => {
|
||||
// simulate selection of all the content by keyboard
|
||||
textarea.setSelectionRange(0, textarea.value.length);
|
||||
await click("button[aria-label='Emojis']");
|
||||
await click(".o-mail-Emoji:contains(🤠)");
|
||||
await click(".o-Emoji:contains(🤠)");
|
||||
assert.strictEqual($(".o-mail-Composer-input").val(), "🤠");
|
||||
});
|
||||
|
||||
@@ -152,7 +152,7 @@ QUnit.test("Cursor is positioned after emoji after adding it", async (assert) =>
|
||||
await insertText(".o-mail-Composer-input", "Blabla");
|
||||
textarea.setSelectionRange(2, 2);
|
||||
await click("button[aria-label='Emojis']");
|
||||
await click(".o-mail-Emoji:contains(🤠)");
|
||||
await click(".o-Emoji:contains(🤠)");
|
||||
const expectedPos = 2 + "🤠".length;
|
||||
assert.strictEqual(textarea.selectionStart, expectedPos);
|
||||
assert.strictEqual(textarea.selectionEnd, expectedPos);
|
||||
@@ -172,7 +172,7 @@ QUnit.test("selected text is not replaced after cancelling the selection", async
|
||||
$(".o-mail-Discuss-content")[0].click();
|
||||
await nextTick();
|
||||
await click("button[aria-label='Emojis']");
|
||||
await click(".o-mail-Emoji:contains(🤠)");
|
||||
await click(".o-Emoji:contains(🤠)");
|
||||
assert.strictEqual($(".o-mail-Composer-input").val(), "Blabla🤠");
|
||||
});
|
||||
|
||||
@@ -205,9 +205,9 @@ QUnit.test(
|
||||
const { openDiscuss } = await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[aria-label='Emojis']");
|
||||
await click(".o-mail-Emoji:contains(👺)");
|
||||
await click(".o-Emoji:contains(👺)");
|
||||
await click("button[aria-label='Emojis']");
|
||||
assert.containsOnce($, ".o-mail-EmojiPicker");
|
||||
assert.containsOnce($, ".o-EmojiPicker");
|
||||
}
|
||||
);
|
||||
|
||||
@@ -217,10 +217,10 @@ QUnit.test("keep emoji picker scroll value when re-opening it", async (assert) =
|
||||
const { openDiscuss } = await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[aria-label='Emojis']");
|
||||
$(".o-mail-EmojiPicker-content")[0].scrollTop = 150;
|
||||
$(".o-EmojiPicker-content")[0].scrollTop = 150;
|
||||
await click("button[aria-label='Emojis']");
|
||||
await click("button[aria-label='Emojis']");
|
||||
assert.strictEqual($(".o-mail-EmojiPicker-content")[0].scrollTop, 150);
|
||||
assert.strictEqual($(".o-EmojiPicker-content")[0].scrollTop, 150);
|
||||
});
|
||||
|
||||
QUnit.test("reset emoji picker scroll value after an emoji is picked", async (assert) => {
|
||||
@@ -229,10 +229,10 @@ QUnit.test("reset emoji picker scroll value after an emoji is picked", async (as
|
||||
const { openDiscuss } = await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[aria-label='Emojis']");
|
||||
$(".o-mail-EmojiPicker-content")[0].scrollTop = 150;
|
||||
await click(".o-mail-Emoji:contains(😎)");
|
||||
$(".o-EmojiPicker-content")[0].scrollTop = 150;
|
||||
await click(".o-Emoji:contains(😎)");
|
||||
await click("button[aria-label='Emojis']");
|
||||
assert.strictEqual($(".o-mail-EmojiPicker-content")[0].scrollTop, 0);
|
||||
assert.strictEqual($(".o-EmojiPicker-content")[0].scrollTop, 0);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
@@ -253,19 +253,19 @@ QUnit.test(
|
||||
|
||||
await triggerEvent(getFixture(), null, "mousedown");
|
||||
await click("button[aria-label='Emojis']");
|
||||
$(".o-mail-EmojiPicker-content")[0].scrollTop = 150;
|
||||
$(".o-EmojiPicker-content")[0].scrollTop = 150;
|
||||
|
||||
await triggerEvent(getFixture(), null, "mousedown");
|
||||
await click("[title='Add a Reaction']");
|
||||
$(".o-mail-EmojiPicker-content")[0].scrollTop = 200;
|
||||
$(".o-EmojiPicker-content")[0].scrollTop = 200;
|
||||
|
||||
await triggerEvent(getFixture(), null, "mousedown");
|
||||
await click("button[aria-label='Emojis']");
|
||||
assert.strictEqual($(".o-mail-EmojiPicker-content")[0].scrollTop, 150);
|
||||
assert.strictEqual($(".o-EmojiPicker-content")[0].scrollTop, 150);
|
||||
|
||||
await triggerEvent(getFixture(), null, "mousedown");
|
||||
await click("[title='Add a Reaction']");
|
||||
assert.strictEqual($(".o-mail-EmojiPicker-content")[0].scrollTop, 200);
|
||||
assert.strictEqual($(".o-EmojiPicker-content")[0].scrollTop, 200);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -374,7 +374,7 @@ QUnit.test("add an emoji after a partner mention", async (assert) => {
|
||||
assert.strictEqual($(".o-mail-Composer-input").val().replace(/\s/, " "), "@TestPartner ");
|
||||
|
||||
await click("button[aria-label='Emojis']");
|
||||
await click(".o-mail-Emoji:contains(😊)");
|
||||
await click(".o-Emoji:contains(😊)");
|
||||
assert.strictEqual($(".o-mail-Composer-input").val().replace(/\s/, " "), "@TestPartner 😊");
|
||||
});
|
||||
|
||||
@@ -431,7 +431,7 @@ QUnit.test("add an emoji after a channel mention", async (assert) => {
|
||||
|
||||
// select emoji
|
||||
await click("button[aria-label='Emojis']");
|
||||
await click(".o-mail-Emoji:contains(😊)");
|
||||
await click(".o-Emoji:contains(😊)");
|
||||
assert.strictEqual($(".o-mail-Composer-input").val().replace(/\s/, " "), "#General 😊");
|
||||
});
|
||||
|
||||
|
||||
@@ -70,6 +70,6 @@ QUnit.test("add an emoji after a command", async (assert) => {
|
||||
);
|
||||
|
||||
await click("button[aria-label='Emojis']");
|
||||
await click(".o-mail-Emoji:contains(😊)");
|
||||
await click(".o-Emoji:contains(😊)");
|
||||
assert.strictEqual($(".o-mail-Composer-input").val().replace(/\s/, " "), "/who 😊");
|
||||
});
|
||||
|
||||
@@ -72,9 +72,9 @@ QUnit.test("reply: discard on pressing escape", async (assert) => {
|
||||
|
||||
// Escape on emoji picker does not stop replying
|
||||
await click(".o-mail-Composer button[aria-label='Emojis']");
|
||||
assert.containsOnce($, ".o-mail-EmojiPicker");
|
||||
assert.containsOnce($, ".o-EmojiPicker");
|
||||
await afterNextRender(() => triggerHotkey("Escape"));
|
||||
assert.containsNone($, ".o-mail-EmojiPicker");
|
||||
assert.containsNone($, ".o-EmojiPicker");
|
||||
assert.containsOnce($, ".o-mail-Composer");
|
||||
|
||||
// Escape on suggestion prompt does not stop replying
|
||||
|
||||
@@ -20,7 +20,7 @@ QUnit.test("search emoji from keywords", async (assert) => {
|
||||
await openDiscuss(channelId);
|
||||
await click("button[aria-label='Emojis']");
|
||||
await insertText("input[placeholder='Search for an emoji']", "mexican");
|
||||
assert.containsOnce($, ".o-mail-Emoji:contains(🌮)");
|
||||
assert.containsOnce($, ".o-Emoji:contains(🌮)");
|
||||
});
|
||||
|
||||
QUnit.test("search emoji from keywords should be case insensitive", async (assert) => {
|
||||
@@ -30,7 +30,7 @@ QUnit.test("search emoji from keywords should be case insensitive", async (asser
|
||||
await openDiscuss(channelId);
|
||||
await click("button[aria-label='Emojis']");
|
||||
await insertText("input[placeholder='Search for an emoji']", "ok");
|
||||
assert.containsOnce($, ".o-mail-Emoji:contains(🆗)"); // all search terms are uppercase OK
|
||||
assert.containsOnce($, ".o-Emoji:contains(🆗)"); // all search terms are uppercase OK
|
||||
});
|
||||
|
||||
QUnit.test("search emoji from keywords with special regex character", async (assert) => {
|
||||
@@ -40,7 +40,7 @@ QUnit.test("search emoji from keywords with special regex character", async (ass
|
||||
await openDiscuss(channelId);
|
||||
await click("button[aria-label='Emojis']");
|
||||
await insertText("input[placeholder='Search for an emoji']", "(blood");
|
||||
assert.containsOnce($, ".o-mail-Emoji:contains(🆎)");
|
||||
assert.containsOnce($, ".o-Emoji:contains(🆎)");
|
||||
});
|
||||
|
||||
QUnit.test("Press Escape in emoji picker closes the emoji picker", async (assert) => {
|
||||
@@ -50,7 +50,7 @@ QUnit.test("Press Escape in emoji picker closes the emoji picker", async (assert
|
||||
await openDiscuss(channelId);
|
||||
await click("button[aria-label='Emojis']");
|
||||
await afterNextRender(() => triggerHotkey("Escape"));
|
||||
assert.containsNone($, ".o-mail-EmojiPicker");
|
||||
assert.containsNone($, ".o-EmojiPicker");
|
||||
});
|
||||
|
||||
QUnit.test("Basic keyboard navigation", async (assert) => {
|
||||
@@ -59,22 +59,22 @@ QUnit.test("Basic keyboard navigation", async (assert) => {
|
||||
const { openDiscuss } = await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[aria-label='Emojis']");
|
||||
assert.containsOnce($, ".o-mail-EmojiPicker-content .o-mail-Emoji[data-index=0].bg-200"); // bg-200 means active
|
||||
assert.containsOnce($, ".o-EmojiPicker-content .o-Emoji[data-index=0].bg-200"); // bg-200 means active
|
||||
await afterNextRender(() => triggerHotkey("ArrowRight"));
|
||||
assert.containsOnce($, ".o-mail-EmojiPicker-content .o-mail-Emoji[data-index=1].bg-200");
|
||||
assert.containsOnce($, ".o-EmojiPicker-content .o-Emoji[data-index=1].bg-200");
|
||||
await afterNextRender(() => triggerHotkey("ArrowDown"));
|
||||
assert.containsOnce(
|
||||
$,
|
||||
`.o-mail-EmojiPicker-content .o-mail-Emoji[data-index=${EMOJI_PER_ROW + 1}].bg-200`
|
||||
`.o-EmojiPicker-content .o-Emoji[data-index=${EMOJI_PER_ROW + 1}].bg-200`
|
||||
);
|
||||
await afterNextRender(() => triggerHotkey("ArrowLeft"));
|
||||
assert.containsOnce(
|
||||
$,
|
||||
`.o-mail-EmojiPicker-content .o-mail-Emoji[data-index=${EMOJI_PER_ROW}].bg-200`
|
||||
`.o-EmojiPicker-content .o-Emoji[data-index=${EMOJI_PER_ROW}].bg-200`
|
||||
);
|
||||
await afterNextRender(() => triggerHotkey("ArrowUp"));
|
||||
assert.containsOnce($, ".o-mail-EmojiPicker-content .o-mail-Emoji[data-index=0].bg-200");
|
||||
const codepoints = $(".o-mail-EmojiPicker-content .o-mail-Emoji[data-index=0].bg-200").data(
|
||||
assert.containsOnce($, ".o-EmojiPicker-content .o-Emoji[data-index=0].bg-200");
|
||||
const codepoints = $(".o-EmojiPicker-content .o-Emoji[data-index=0].bg-200").data(
|
||||
"codepoints"
|
||||
);
|
||||
await afterNextRender(() => triggerHotkey("Enter"));
|
||||
@@ -87,13 +87,13 @@ QUnit.test("recent category (basic)", async (assert) => {
|
||||
const { openDiscuss } = await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[aria-label='Emojis']");
|
||||
assert.containsNone($, ".o-mail-EmojiPicker-navbar [title='Frequently used']");
|
||||
await click(".o-mail-EmojiPicker-content .o-mail-Emoji:contains(😀)");
|
||||
assert.containsNone($, ".o-EmojiPicker-navbar [title='Frequently used']");
|
||||
await click(".o-EmojiPicker-content .o-Emoji:contains(😀)");
|
||||
await click("button[aria-label='Emojis']");
|
||||
assert.containsOnce($, ".o-mail-EmojiPicker-navbar [title='Frequently used']");
|
||||
assert.containsOnce($, ".o-EmojiPicker-navbar [title='Frequently used']");
|
||||
assert.containsOnce(
|
||||
$,
|
||||
"span:contains(Frequently used) ~ .o-mail-Emoji:contains(😀) ~ span:contains(Smileys & Emotion)"
|
||||
"span:contains(Frequently used) ~ .o-Emoji:contains(😀) ~ span:contains(Smileys & Emotion)"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -103,23 +103,23 @@ QUnit.test("emoji usage amount orders frequent emojis", async (assert) => {
|
||||
const { openDiscuss } = await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[aria-label='Emojis']");
|
||||
await click(".o-mail-EmojiPicker-content .o-mail-Emoji:contains(😀)");
|
||||
await click(".o-EmojiPicker-content .o-Emoji:contains(😀)");
|
||||
await click("button[aria-label='Emojis']");
|
||||
await click(".o-mail-EmojiPicker-content .o-mail-Emoji:contains(👽)");
|
||||
await click(".o-EmojiPicker-content .o-Emoji:contains(👽)");
|
||||
await click("button[aria-label='Emojis']");
|
||||
await click(".o-mail-EmojiPicker-content .o-mail-Emoji:contains(👽)");
|
||||
await click(".o-EmojiPicker-content .o-Emoji:contains(👽)");
|
||||
await click("button[aria-label='Emojis']");
|
||||
assert.containsOnce(
|
||||
$,
|
||||
"span:contains(Frequently used) ~ .o-mail-Emoji:contains(😀) ~ span:contains(Smileys & Emotion)"
|
||||
"span:contains(Frequently used) ~ .o-Emoji:contains(😀) ~ span:contains(Smileys & Emotion)"
|
||||
);
|
||||
assert.containsOnce(
|
||||
$,
|
||||
"span:contains(Frequently used) ~ .o-mail-Emoji:contains(👽) ~ span:contains(Smileys & Emotion)"
|
||||
"span:contains(Frequently used) ~ .o-Emoji:contains(👽) ~ span:contains(Smileys & Emotion)"
|
||||
);
|
||||
assert.containsOnce(
|
||||
$,
|
||||
"span:contains(Frequently used) ~ .o-mail-Emoji:contains(👽) ~ .o-mail-Emoji:contains(😀) ~ span:contains(Smileys & Emotion)"
|
||||
"span:contains(Frequently used) ~ .o-Emoji:contains(👽) ~ .o-Emoji:contains(😀) ~ span:contains(Smileys & Emotion)"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -133,7 +133,7 @@ QUnit.test("posting :wink: in message should impact recent", async (assert) => {
|
||||
await click("button[aria-label='Emojis']");
|
||||
assert.containsOnce(
|
||||
$,
|
||||
"span:contains(Frequently used) ~ .o-mail-Emoji:contains(😉) ~ span:contains(Smileys & Emotion)"
|
||||
"span:contains(Frequently used) ~ .o-Emoji:contains(😉) ~ span:contains(Smileys & Emotion)"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -148,7 +148,7 @@ QUnit.test("posting :snowman: in message should impact recent", async (assert) =
|
||||
await click("button[aria-label='Emojis']");
|
||||
assert.containsOnce(
|
||||
$,
|
||||
"span:contains(Frequently used) ~ .o-mail-Emoji:contains(☃️) ~ span:contains(Smileys & Emotion)"
|
||||
"span:contains(Frequently used) ~ .o-Emoji:contains(☃️) ~ span:contains(Smileys & Emotion)"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -158,7 +158,7 @@ QUnit.test("first category should be highlight by default", async (assert) => {
|
||||
const { openDiscuss } = await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[aria-label='Emojis']");
|
||||
assert.containsOnce($, ".o-mail-EmojiPicker-navbar .o-mail-Emoji:eq(0).bg-300");
|
||||
assert.containsOnce($, ".o-EmojiPicker-navbar .o-Emoji:eq(0).bg-300");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
@@ -170,11 +170,11 @@ QUnit.test(
|
||||
await openDiscuss(channelId);
|
||||
await click("button[aria-label='Emojis']");
|
||||
await afterNextRender(() =>
|
||||
$(".o-mail-EmojiPicker-content .o-mail-Emoji:contains(👺)")[0].dispatchEvent(
|
||||
$(".o-EmojiPicker-content .o-Emoji:contains(👺)")[0].dispatchEvent(
|
||||
new MouseEvent("click", { shiftKey: true })
|
||||
)
|
||||
);
|
||||
assert.containsOnce($, ".o-mail-EmojiPicker");
|
||||
assert.containsOnce($, ".o-EmojiPicker");
|
||||
assert.strictEqual($(".o-mail-Composer-input").val(), "👺");
|
||||
}
|
||||
);
|
||||
|
||||
@@ -172,7 +172,7 @@ QUnit.test("Do not stop edition on click away when clicking on emoji", async (as
|
||||
await click(".o-mail-Message [title='Expand']");
|
||||
await click(".o-mail-Message [title='Edit']");
|
||||
await click(".o-mail-Composer button[aria-label='Emojis']");
|
||||
await click(".o-mail-EmojiPicker-content .o-mail-Emoji");
|
||||
await click(".o-EmojiPicker-content .o-Emoji");
|
||||
assert.containsOnce($, ".o-mail-Message-editable .o-mail-Composer");
|
||||
});
|
||||
|
||||
@@ -442,7 +442,7 @@ QUnit.test("Can open emoji picker after edit mode", async (assert) => {
|
||||
await click(".o-mail-Message [title='Edit']");
|
||||
await triggerEvent(document.body, ".o-mail-DiscussSidebar", "click");
|
||||
await click("[title='Add a Reaction']");
|
||||
assert.containsOnce($, ".o-mail-EmojiPicker");
|
||||
assert.containsOnce($, ".o-EmojiPicker");
|
||||
});
|
||||
|
||||
QUnit.test("Can add a reaction", async (assert) => {
|
||||
@@ -460,7 +460,7 @@ QUnit.test("Can add a reaction", async (assert) => {
|
||||
const { openDiscuss } = await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("[title='Add a Reaction']");
|
||||
await click(".o-mail-Emoji:contains(😅)");
|
||||
await click(".o-Emoji:contains(😅)");
|
||||
assert.containsOnce($, ".o-mail-MessageReaction:contains('😅')");
|
||||
});
|
||||
|
||||
@@ -479,7 +479,7 @@ QUnit.test("Can remove a reaction", async (assert) => {
|
||||
const { openDiscuss } = await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("[title='Add a Reaction']");
|
||||
await click(".o-mail-Emoji:contains(😅)");
|
||||
await click(".o-Emoji:contains(😅)");
|
||||
await click(".o-mail-MessageReaction");
|
||||
assert.containsNone($, ".o-mail-MessageReaction:contains('😅')");
|
||||
});
|
||||
@@ -543,7 +543,7 @@ QUnit.test("Reaction summary", async (assert) => {
|
||||
const partnerId = pyEnv["res.partner"].create({ name });
|
||||
pyEnv.currentPartnerId = partnerId;
|
||||
await click("[title='Add a Reaction']");
|
||||
await click(".o-mail-Emoji:contains(😅)");
|
||||
await click(".o-Emoji:contains(😅)");
|
||||
assert.hasAttrValue($(".o-mail-MessageReaction")[0], "title", expectedSummaries[idx]);
|
||||
}
|
||||
});
|
||||
@@ -563,9 +563,9 @@ QUnit.test("Add the same reaction twice from the emoji picker", async (assert) =
|
||||
const { openDiscuss } = await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("[title='Add a Reaction']");
|
||||
await click(".o-mail-Emoji:contains(😅)");
|
||||
await click(".o-Emoji:contains(😅)");
|
||||
await click("[title='Add a Reaction']");
|
||||
await click(".o-mail-Emoji:contains(😅)");
|
||||
await click(".o-Emoji:contains(😅)");
|
||||
assert.containsOnce($, ".o-mail-MessageReaction:contains('😅')");
|
||||
});
|
||||
|
||||
|
||||
@@ -29,9 +29,9 @@ export async function testEmojiButtonVisible(assert, selector) {
|
||||
export async function testEmojiButton(assert, input, button) {
|
||||
// emoji picker opens
|
||||
await click(button);
|
||||
assert.containsOnce($, ".o-mail-EmojiPicker");
|
||||
assert.containsOnce($, ".o-EmojiPicker");
|
||||
// clicking an emoji adds it to the input field
|
||||
const emoji_1 = $(".o-mail-EmojiPicker-content .o-mail-Emoji")[0];
|
||||
const emoji_1 = $(".o-EmojiPicker-content .o-Emoji")[0];
|
||||
const emojiChar_1 = emoji_1.textContent;
|
||||
await click(emoji_1);
|
||||
assert.ok(input.value.endsWith(emojiChar_1));
|
||||
@@ -40,7 +40,7 @@ export async function testEmojiButton(assert, input, button) {
|
||||
input.setSelectionRange(2, input.value.length - emojiChar_1.length);
|
||||
// pick an emoji while the text is selected
|
||||
await click(button);
|
||||
const emoji_2 = $(".o-mail-EmojiPicker-content .o-mail-Emoji")[0];
|
||||
const emoji_2 = $(".o-EmojiPicker-content .o-Emoji")[0];
|
||||
const emojiChar_2 = emoji_2.textContent;
|
||||
await click(emoji_2);
|
||||
// the selected region is replaced and the rest stays in place
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
'web/static/src/env.js',
|
||||
'web/static/src/core/utils/transitions.scss', # needs to be loaded before other scss files
|
||||
'web/static/src/core/**/*',
|
||||
('remove', 'web/static/src/core/emoji_picker/emoji_data.js'),
|
||||
('remove', 'web/static/src/core/errors/error_handlers.js'), # error handling in PoS is different from the webclient
|
||||
# formatMonetary
|
||||
'web/static/src/views/fields/formatters.js',
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"web/static/src/session.js",
|
||||
"web/static/src/core/utils/transitions.scss",
|
||||
"web/static/src/core/**/*",
|
||||
('remove', 'web/static/src/core/emoji_picker/emoji_data.js'),
|
||||
("remove", "web/static/src/core/browser/router_service.js"),
|
||||
("remove", "web/static/src/core/debug/**/*"),
|
||||
"web/static/lib/owl/owl.js",
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
('remove', 'web/static/src/webclient/navbar/navbar.scss'), # already in assets_common
|
||||
('remove', 'web/static/src/webclient/clickbot/clickbot.js'), # lazy loaded
|
||||
('remove', 'web/static/src/views/form/button_box/*.scss'),
|
||||
('remove', 'web/static/src/core/emoji_picker/emoji_data.js'),
|
||||
|
||||
# remove the report code and whitelist only what's needed
|
||||
('remove', 'web/static/src/webclient/actions/reports/**/*'),
|
||||
|
||||
@@ -163,6 +163,9 @@ This module provides the core of the Odoo Web Client.
|
||||
'web/static/src/legacy/xml/colorpicker.xml',
|
||||
'web/static/src/legacy/translations_loaded.js',
|
||||
],
|
||||
'web.assets_emoji': [
|
||||
'web/static/src/core/emoji_picker/emoji_data.js'
|
||||
],
|
||||
'web.assets_backend': [
|
||||
('include', 'web._assets_helpers'),
|
||||
('include', 'web._assets_backend_helpers'),
|
||||
@@ -184,6 +187,7 @@ This module provides the core of the Odoo Web Client.
|
||||
('remove', 'web/static/src/webclient/clickbot/clickbot.js'), # lazy loaded
|
||||
('remove', 'web/static/src/views/form/button_box/*.scss'),
|
||||
('remove', 'web/static/src/core/file_viewer/file_viewer.dark.scss'),
|
||||
('remove', 'web/static/src/core/emoji_picker/emoji_data.js'),
|
||||
|
||||
# remove the report code and whitelist only what's needed
|
||||
('remove', 'web/static/src/webclient/actions/reports/**/*'),
|
||||
@@ -397,6 +401,7 @@ This module provides the core of the Odoo Web Client.
|
||||
('remove', 'web/static/src/core/commands/**/*'),
|
||||
('remove', 'web/static/src/core/debug/debug_menu.js'),
|
||||
('remove', 'web/static/src/core/file_viewer/file_viewer.dark.scss'),
|
||||
('remove', 'web/static/src/core/emoji_picker/emoji_data.js'),
|
||||
'web/static/src/public/error_notifications.js',
|
||||
|
||||
'web/static/src/legacy/utils.js',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* @odoo-module */
|
||||
|
||||
import { markEventHandled } from "@mail/utils/common/misc";
|
||||
import { markEventHandled } from "@web/core/utils/misc";
|
||||
|
||||
import {
|
||||
Component,
|
||||
@@ -106,7 +106,7 @@ export function useEmojiPicker(ref, props, options = {}) {
|
||||
}
|
||||
|
||||
export const loader = {
|
||||
loadEmoji: memoize(() => getBundle("mail.assets_emoji").then(loadBundle)),
|
||||
loadEmoji: memoize(() => getBundle("web.assets_emoji").then(loadBundle)),
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -126,7 +126,7 @@ export const EMOJI_PER_ROW = 9;
|
||||
|
||||
export class EmojiPicker extends Component {
|
||||
static props = ["onSelect", "className?", "close?", "onClose?", "storeScroll?"];
|
||||
static template = "mail.EmojiPicker";
|
||||
static template = "web.EmojiPicker";
|
||||
|
||||
recent = [];
|
||||
categories = null;
|
||||
@@ -150,7 +150,7 @@ export class EmojiPicker extends Component {
|
||||
this.emojis.map((emoji) => [emoji.codepoints, emoji])
|
||||
);
|
||||
this.state.categoryId = this.categories[0]?.sortId;
|
||||
this.recent = JSON.parse(browser.localStorage.getItem("mail.emoji.frequent") || "{}");
|
||||
this.recent = JSON.parse(browser.localStorage.getItem("web.emoji.frequent") || "{}");
|
||||
this.recentCategory = {
|
||||
name: "Frequently used",
|
||||
displayName: _t("Frequently used"),
|
||||
@@ -176,7 +176,7 @@ export class EmojiPicker extends Component {
|
||||
this.shouldScrollElem = false;
|
||||
const getElement = () =>
|
||||
this.gridRef.el.querySelector(
|
||||
`.o-mail-EmojiPicker-category[data-category="${this.state.categoryId}"`
|
||||
`.o-EmojiPicker-category[data-category="${this.state.categoryId}"`
|
||||
);
|
||||
const elem = getElement();
|
||||
if (elem) {
|
||||
@@ -255,9 +255,10 @@ export class EmojiPicker extends Component {
|
||||
break;
|
||||
}
|
||||
case "Enter":
|
||||
ev.preventDefault();
|
||||
this.gridRef.el
|
||||
.querySelector(
|
||||
`.o-mail-EmojiPicker-content .o-mail-Emoji[data-index="${this.state.activeEmojiIndex}"]`
|
||||
`.o-EmojiPicker-content .o-Emoji[data-index="${this.state.activeEmojiIndex}"]`
|
||||
)
|
||||
.click();
|
||||
break;
|
||||
@@ -290,7 +291,7 @@ export class EmojiPicker extends Component {
|
||||
this.props.onSelect(codepoints);
|
||||
this.recent[codepoints] ??= 0;
|
||||
this.recent[codepoints]++;
|
||||
browser.localStorage.setItem("mail.emoji.frequent", JSON.stringify(this.recent));
|
||||
browser.localStorage.setItem("web.emoji.frequent", JSON.stringify(this.recent));
|
||||
this.gridRef.el.scrollTop = 0;
|
||||
if (!ev.shiftKey) {
|
||||
this.props.close?.();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
.popover .o-mail-EmojiPicker {
|
||||
.popover .o-EmojiPicker {
|
||||
width: 285px;
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
.o-mail-EmojiPicker {
|
||||
.o-EmojiPicker {
|
||||
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
@@ -15,7 +15,7 @@
|
||||
grid-template-rows: 40px 1fr 32px;
|
||||
}
|
||||
|
||||
.o-mail-Emoji {
|
||||
.o-Emoji {
|
||||
width: 30px;
|
||||
font-size: 1rem;
|
||||
&:hover {
|
||||
@@ -23,27 +23,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.o-small .o-mail-Emoji {
|
||||
&.o-small .o-Emoji {
|
||||
width: 40px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.o-mail-EmojiPicker-content {
|
||||
.o-EmojiPicker-content {
|
||||
grid-area: content;
|
||||
}
|
||||
|
||||
.o-mail-EmojiPicker-navbar {
|
||||
.o-EmojiPicker-navbar {
|
||||
grid-area: navbar;
|
||||
.o-mail-Emoji {
|
||||
.o-Emoji {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.o-mail-EmojiPicker-sectionIcon {
|
||||
.o-EmojiPicker-sectionIcon {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
.o-mail-EmojiPicker-search {
|
||||
.o-EmojiPicker-search {
|
||||
grid-area: search;
|
||||
background-color: $input-bg;
|
||||
|
||||
@@ -59,13 +59,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.o-mail-EmojiPicker-empty {
|
||||
.o-EmojiPicker-empty {
|
||||
font-size: 5rem !important;
|
||||
filter: grayscale(0.25);
|
||||
}
|
||||
}
|
||||
|
||||
.o-mail-EmojiPicker-category:before {
|
||||
.o-EmojiPicker-category:before {
|
||||
// invisible character so that category has constant height, regardless of text content.
|
||||
content: "\200b"; /* unicode zero width space character */
|
||||
}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.EmojiPicker" owl="1">
|
||||
<div class="o-mail-EmojiPicker bg-view" t-att-class="{ 'd-flex flex-column align-items-center justify-content-center': emojis.length === 0, 'o-small': ui.isSmall }" t-attf-class="{{ props.className }}" t-on-click="onClick" t-on-keydown="onKeydown">
|
||||
<t t-name="web.EmojiPicker" owl="1">
|
||||
<div class="o-EmojiPicker bg-view" t-att-class="{ 'd-flex flex-column align-items-center justify-content-center': emojis.length === 0, 'o-small': ui.isSmall }" t-attf-class="{{ props.className }}" t-on-click="onClick" t-on-keydown="onKeydown">
|
||||
<t t-if="emojis.length === 0">
|
||||
<span class="o-mail-EmojiPicker-empty">😵💫</span>
|
||||
<span class="o-EmojiPicker-empty">😵💫</span>
|
||||
<span class="fs-5 text-muted">Failed to load emojis...</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="o-mail-EmojiPicker-search d-flex align-items-center mt-2 mx-2 rounded">
|
||||
<div class="o-EmojiPicker-search d-flex align-items-center mt-2 mx-2 rounded">
|
||||
<input type="text" class="form-control border-0 flex-grow-1 rounded-end-0" t-ref="input" placeholder="Search for an emoji" t-model="state.searchStr" t-on-input="() => this.state.activeEmojiIndex = 0"/>
|
||||
<i class="oi oi-search p-2 fs-5 rounded-end" title="Search..." role="img" aria-label="Search..."/>
|
||||
</div>
|
||||
<t t-set="itemIndex" t-value="0"/>
|
||||
<div class="o-mail-EmojiPicker-content overflow-auto d-flex flex-wrap align-items-center user-select-none mt-1" t-att-class="getEmojis().length === 0 ? 'flex-column justify-content-center' : 'align-content-start'" t-ref="emoji-grid" t-on-scroll="highlightActiveCategory">
|
||||
<div class="o-EmojiPicker-content overflow-auto d-flex flex-wrap align-items-center user-select-none mt-1" t-att-class="getEmojis().length === 0 ? 'flex-column justify-content-center' : 'align-content-start'" t-ref="emoji-grid" t-on-scroll="highlightActiveCategory">
|
||||
<t t-if="state.searchStr and getEmojis().length === 0" class="d-flex flex-column">
|
||||
<span class="o-mail-EmojiPicker-empty">😢</span>
|
||||
<span class="o-EmojiPicker-empty">😢</span>
|
||||
<span class="fs-5 text-muted">No emoji match your search</span>
|
||||
</t>
|
||||
<t t-if="recentEmojis.length > 0 and !state.searchStr">
|
||||
<t t-call="mail.EmojiPicker.section">
|
||||
<t t-call="web.EmojiPicker.section">
|
||||
<t t-set="category" t-value="recentCategory"/>
|
||||
</t>
|
||||
<t t-foreach="recentEmojis" t-as="emoji" t-key="'recent_' + emoji_index">
|
||||
<t t-call="mail.EmojiPicker.emoji">
|
||||
<t t-call="web.EmojiPicker.emoji">
|
||||
<t t-set="emoji" t-value="emoji"/>
|
||||
</t>
|
||||
<t t-set="itemIndex" t-value="itemIndex + 1"/>
|
||||
@@ -34,23 +34,23 @@
|
||||
<t t-if="!state.searchStr and current !== emoji.category">
|
||||
<t t-set="current" t-value="emoji.category"/>
|
||||
<t t-set="category" t-value="categories.find(c => c.name === current)"/>
|
||||
<t t-call="mail.EmojiPicker.section">
|
||||
<t t-call="web.EmojiPicker.section">
|
||||
<t t-set="category" t-value="category"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-elif="state.searchStr" t-set="categorySortId" t-value="null"/>
|
||||
<t t-call="mail.EmojiPicker.emoji">
|
||||
<t t-call="web.EmojiPicker.emoji">
|
||||
<t t-set="emoji" t-value="emoji"/>
|
||||
</t>
|
||||
<t t-set="itemIndex" t-value="itemIndex + 1"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="o-mail-EmojiPicker-navbar d-flex align-items-center bg-100 overflow-auto">
|
||||
<t t-if="recentEmojis.length > 0" t-call="mail.EmojiPicker.tab">
|
||||
<div class="o-EmojiPicker-navbar d-flex align-items-center bg-100 overflow-auto">
|
||||
<t t-if="recentEmojis.length > 0" t-call="web.EmojiPicker.tab">
|
||||
<t t-set="category" t-value="recentCategory"/>
|
||||
</t>
|
||||
<t t-foreach="categories" t-as="category" t-key="category.sortId">
|
||||
<t t-call="mail.EmojiPicker.tab">
|
||||
<t t-call="web.EmojiPicker.tab">
|
||||
<t t-set="category" t-value="category"/>
|
||||
</t>
|
||||
</t>
|
||||
@@ -59,19 +59,19 @@
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.EmojiPicker.tab" owl="1">
|
||||
<span class="o-mail-Emoji text-center p-1 cursor-pointer" t-att-class="{'bg-300': category.sortId === state.categoryId}" t-att-title="category.name" t-att-data-id="category.sortId" t-on-click="selectCategory">
|
||||
<t t-name="web.EmojiPicker.tab" owl="1">
|
||||
<span class="o-Emoji text-center p-1 cursor-pointer" t-att-class="{'bg-300': category.sortId === state.categoryId}" t-att-title="category.name" t-att-data-id="category.sortId" t-on-click="selectCategory">
|
||||
<t t-esc="category.title"/>
|
||||
</span>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.EmojiPicker.section" owl="1">
|
||||
<span class="w-100 fs-7 p-2 position-sticky top-0 bg-view" t-att-data-category="category.sortId"><span class="o-mail-EmojiPicker-sectionIcon" t-esc="category.title"/><span class="ms-2" t-esc="category.displayName"/></span>
|
||||
<span class="o-mail-EmojiPicker-category opacity-100 fs-7 py-2" t-att-data-category="category.sortId"/>
|
||||
<t t-name="web.EmojiPicker.section" owl="1">
|
||||
<span class="w-100 fs-7 p-2 position-sticky top-0 bg-view" t-att-data-category="category.sortId"><span class="o-EmojiPicker-sectionIcon" t-esc="category.title"/><span class="ms-2" t-esc="category.displayName"/></span>
|
||||
<span class="o-EmojiPicker-category opacity-100 fs-7 py-2" t-att-data-category="category.sortId"/>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.EmojiPicker.emoji" owl="1">
|
||||
<span class="o-mail-Emoji cursor-pointer d-flex justify-content-center" t-att-class="{ 'bg-200': state.activeEmojiIndex === itemIndex, 'fs-2': !ui.isSmall, 'fs-1': ui.isSmall }" t-att-title="emoji.name" t-att-data-codepoints="emoji.codepoints" t-att-data-index="itemIndex" t-att-data-category="recentCategory.sortId" t-on-click="selectEmoji">
|
||||
<t t-name="web.EmojiPicker.emoji" owl="1">
|
||||
<span class="o-Emoji cursor-pointer d-flex justify-content-center" t-att-class="{ 'bg-200': state.activeEmojiIndex === itemIndex, 'fs-2': !ui.isSmall, 'fs-1': ui.isSmall }" t-att-title="emoji.name" t-att-data-codepoints="emoji.codepoints" t-att-data-index="itemIndex" t-att-data-category="recentCategory.sortId" t-on-click="selectEmoji">
|
||||
<span t-esc="emoji.codepoints"/>
|
||||
</span>
|
||||
</t>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/* @odoo-module */
|
||||
const eventHandledWeakMap = new WeakMap();
|
||||
/**
|
||||
* Returns whether the given event has been handled with the given markName.
|
||||
*
|
||||
* @param {Event} ev
|
||||
* @param {string} markName
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isEventHandled(ev, markName) {
|
||||
if (!eventHandledWeakMap.get(ev)) {
|
||||
return false;
|
||||
}
|
||||
return eventHandledWeakMap.get(ev).includes(markName);
|
||||
}
|
||||
/**
|
||||
* Marks the given event as handled by the given markName. Useful to allow
|
||||
* handlers in the propagation chain to make a decision based on what has
|
||||
* already been done.
|
||||
*
|
||||
* @param {Event} ev
|
||||
* @param {string} markName
|
||||
*/
|
||||
export function markEventHandled(ev, markName) {
|
||||
if (!eventHandledWeakMap.get(ev)) {
|
||||
eventHandledWeakMap.set(ev, []);
|
||||
}
|
||||
eventHandledWeakMap.get(ev).push(markName);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { useWowlService } from '@web/legacy/utils';
|
||||
import { Component, onRendered, xml } from "@odoo/owl";
|
||||
import { EmojiPicker } from '@web/core/emoji_picker/emoji_picker';
|
||||
|
||||
export class EmojiPickerWrapper extends Component {
|
||||
setup() {
|
||||
this.popover = useWowlService('popover');
|
||||
|
||||
onRendered(() => {
|
||||
this.popover.add(this.props.targetEl, EmojiPicker, {
|
||||
onSelect: this.props.onSelect,
|
||||
}, { position: `bottom-${this.props.position}`});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
EmojiPickerWrapper.template = xml``;
|
||||
@@ -28,6 +28,7 @@ import { groupBy } from "@web/core/utils/arrays";
|
||||
import { debounce } from "@web/core/utils/timing";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { FileViewer } from "@web/core/file_viewer/file_viewer";
|
||||
import { EmojiPickerWrapper } from '@web_editor/components/emoji_popover/emoji_popover'
|
||||
|
||||
var _t = core._t;
|
||||
const QWeb = core.qweb;
|
||||
@@ -44,6 +45,8 @@ const setSelection = OdooEditorLib.setSelection;
|
||||
const endPos = OdooEditorLib.endPos;
|
||||
const hasValidSelection = OdooEditorLib.hasValidSelection;
|
||||
const parseHTML = OdooEditorLib.parseHTML;
|
||||
const closestBlock = OdooEditorLib.closestBlock;
|
||||
const getRangePosition = OdooEditorLib.getRangePosition;
|
||||
|
||||
var id = 0;
|
||||
const basicMediaSelector = 'img, .fa, .o_image, .media_iframe_video';
|
||||
@@ -1432,6 +1435,32 @@ const Wysiwyg = Widget.extend({
|
||||
// component is mounted on the global document.
|
||||
return this.mediaDialogWrapper.mount(document.body);
|
||||
},
|
||||
|
||||
showEmojiPicker() {
|
||||
const targetEl = this.odooEditor.document.getSelection();
|
||||
const closest = closestBlock(targetEl.anchorNode);
|
||||
const restoreSelection = preserveCursor(this.odooEditor.document);
|
||||
const blockWidth = parseInt(getComputedStyle(closest).width.slice(0, -2)); // remove 'px'
|
||||
// Calculate one/fouth portion of blockWidth, and then on the basis of our cursor position
|
||||
// check in which quadrant it falls, give the popover its position accordingly
|
||||
const portionWidth = (blockWidth / 4);
|
||||
const cursorAt = getRangePosition(targetEl, this.odooEditor.document).left;
|
||||
let position = 'end';
|
||||
if (cursorAt < portionWidth * 2) {
|
||||
position = 'start';
|
||||
} else if (cursorAt <= portionWidth * 3) {
|
||||
position = 'middle';
|
||||
}
|
||||
const emojiPicker = new ComponentWrapper(this, EmojiPickerWrapper, {
|
||||
targetEl: closest,
|
||||
position,
|
||||
onSelect: (str) => {
|
||||
restoreSelection();
|
||||
this.odooEditor.execCommand('insert', str);
|
||||
}
|
||||
});
|
||||
emojiPicker.mount(closest);
|
||||
},
|
||||
/**
|
||||
* Sets custom CSS Variables on the snippet menu element.
|
||||
* Used for color previews and color palette to get the color
|
||||
@@ -2228,6 +2257,16 @@ const Wysiwyg = Widget.extend({
|
||||
return !row;
|
||||
},
|
||||
},
|
||||
{
|
||||
category: _t('Widget'),
|
||||
name: _t('Emoji'),
|
||||
priority: 70,
|
||||
description: _t('Add an emoji'),
|
||||
fontawesome: 'fa-smile-o',
|
||||
callback: () => {
|
||||
this.showEmojiPicker();
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
if (editorOptions.allowCommandLink) {
|
||||
|
||||
@@ -246,6 +246,7 @@ Featuring
|
||||
'web/static/src/core/**/*.js',
|
||||
'web/static/src/env.js',
|
||||
'web/static/src/legacy/js/services/session.js',
|
||||
('remove', 'web/static/src/core/emoji_picker/emoji_data.js'),
|
||||
|
||||
('include', 'web._assets_helpers'),
|
||||
'web/static/src/scss/pre_variables.scss',
|
||||
|
||||
Reference in New Issue
Block a user