[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:
Sanjay-Sharma
2023-07-05 11:37:06 +02:00
committed by shsa-odoo
parent 4bc70c585c
commit 4f6a92e684
36 changed files with 213 additions and 151 deletions
+1 -1
View File
@@ -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? 😊"
+2 -5
View File
@@ -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";
+1 -1
View File
@@ -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
+25 -25
View File
@@ -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
+1
View File
@@ -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',
+1
View File
@@ -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",
+1
View File
@@ -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/**/*'),
+5
View File
@@ -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>
+29
View File
@@ -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) {
+1
View File
@@ -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',