Skip to content

Commit

Permalink
fix: [plugin-emoji] improvements (#92)
Browse files Browse the repository at this point in the history
* update 'blur' event handler

fix #91

Allow 'on:click' event handler on emoji icon buttons to execute.

* bind loop variable to callback parameter

Testing proves that this isn't actually necessary.
Presumably, {#each} creates a closure and the variable remains in scope.
Never-the-less, (imho) this is more explicit.

* update 'default.css'

fix #90

change from a grid layout to a block element containing inline buttons

* update "up" and "down" arrow key handlers

Since emoji button widths can vary by the count of unicode characters,
advancing either forward or backward by a constant count of emojis doesn't work.

Now, the determination of the emoji that is above or below by one row
is done based on some calculation using position of each emoji within the DOM.

Note: the plugin now takes a new option parameter: "maxResults".
This is the maximum count of emojis to display that match partial user input.
The default is: 120.
---------

Co-authored-by: BearToCode <[email protected]>
  • Loading branch information
warren-bank and BearToCode authored Jul 1, 2024
1 parent ebcc6a0 commit db8d3f0
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 34 deletions.
103 changes: 74 additions & 29 deletions packages/plugin-emoji/src/lib/Emoji.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,36 @@
import * as nodeEmoji from 'node-emoji';
import type { TransitionConfig } from 'svelte/transition';
const cols = 8;
const maxRows = 12;
export let carta: Carta;
export let inTransition: (node: Element) => TransitionConfig;
export let outTransition: (node: Element) => TransitionConfig;
export let maxResults: number;
let visible = false;
let filter = '';
let colonPosition = 0;
let hoveringIndex = 0;
let emojis: nodeEmoji.Emoji[] = [];
let emojisElements: HTMLButtonElement[] = Array(cols * maxRows);
let emojis: { emoji: string; name: string }[] = [];
let emojisElements: HTMLButtonElement[] = Array(maxResults);
onMount(() => {
carta.input?.textarea.addEventListener('keydown', handleKeyDown);
carta.input?.textarea.addEventListener('keyup', handleKeyUp);
carta.input?.textarea.addEventListener('click', hide);
carta.input?.textarea.addEventListener('blur', hide);
carta.input?.textarea.addEventListener('blur', hideAfterDelay);
});
onDestroy(() => {
carta.input?.textarea.removeEventListener('keydown', handleKeyDown);
carta.input?.textarea.removeEventListener('keyup', handleKeyUp);
carta.input?.textarea.removeEventListener('click', hide);
carta.input?.textarea.removeEventListener('blur', hide);
carta.input?.textarea.removeEventListener('blur', hideAfterDelay);
});
function hideAfterDelay() {
setTimeout(hide, 250);
}
function hide() {
visible = false;
}
Expand All @@ -56,12 +58,10 @@
// Check for arrows
if (e.key === 'ArrowUp') {
e.preventDefault();
hoveringIndex =
(emojis.length + hoveringIndex - Math.min(cols, emojis.length)) % emojis.length;
hoveringIndex = getIndexOfEmojiElementInPrevRow();
} else if (e.key === 'ArrowDown') {
e.preventDefault();
hoveringIndex =
(emojis.length + hoveringIndex + Math.min(cols, emojis.length)) % emojis.length;
hoveringIndex = getIndexOfEmojiElementInNextRow();
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
hoveringIndex = (emojis.length + hoveringIndex - 1) % emojis.length;
Expand Down Expand Up @@ -89,12 +89,72 @@
colonPosition + 1,
carta.input.textarea.selectionStart
);
emojis = nodeEmoji.search(filter).slice(0, cols * maxRows);
emojis = nodeEmoji.search(filter).slice(0, maxResults);
hoveringIndex = 0;
}
}
function selectEmoji(emoji: nodeEmoji.Emoji) {
function getIndexOfEmojiElementInPrevRow() {
if (emojisElements.at(hoveringIndex)) {
let index = hoveringIndex;
let el = emojisElements[index];
const startPos = {
top: el.offsetTop,
left: el.offsetLeft,
right: el.offsetLeft + el.offsetWidth
};
let prevIndex, prevPos;
for (;;) {
prevIndex = index - 1;
if (prevIndex < 0 || !emojisElements.at(prevIndex)) return index;
index = prevIndex;
el = emojisElements[index];
prevPos = {
top: el.offsetTop,
left: el.offsetLeft,
right: el.offsetLeft + el.offsetWidth
};
if (prevPos.top === startPos.top) continue;
if (prevPos.left > startPos.right) continue;
return index;
}
}
return hoveringIndex;
}
function getIndexOfEmojiElementInNextRow() {
if (emojisElements.at(hoveringIndex)) {
let index = hoveringIndex;
let el = emojisElements[index];
const startPos = {
top: el.offsetTop,
left: el.offsetLeft,
right: el.offsetLeft + el.offsetWidth
};
let nextIndex, nextPos;
for (;;) {
nextIndex = index + 1;
if (nextIndex >= emojisElements.length || !emojisElements.at(nextIndex)) return index;
index = nextIndex;
el = emojisElements[index];
nextPos = {
top: el.offsetTop,
left: el.offsetLeft,
right: el.offsetLeft + el.offsetWidth
};
if (nextPos.top === startPos.top) continue;
if (nextPos.right < startPos.left) continue;
return index;
}
}
return hoveringIndex;
}
function selectEmoji(emoji: { emoji: string; name: string }) {
if (!carta.input) return;
// Remove slash and filter
carta.input.removeAt(colonPosition, filter.length + 1);
Expand All @@ -118,13 +178,7 @@
</script>

{#if visible && filter.length > 0 && emojis.length > 0}
<div
style="--cols: {cols};"
class="carta-emoji"
in:inTransition
out:outTransition
use:carta.bindToCaret
>
<div class="carta-emoji" in:inTransition out:outTransition use:carta.bindToCaret>
{#each emojis as emoji, i}
<button
class={i === hoveringIndex ? 'carta-active' : ''}
Expand All @@ -137,12 +191,3 @@
{/each}
</div>
{/if}

<style>
.carta-emoji {
position: absolute;
display: grid;
grid-template-columns: repeat(var(--cols), 1fr);
}
</style>
15 changes: 11 additions & 4 deletions packages/plugin-emoji/src/lib/default.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,36 @@
--contrast-background: #f1f1f1;
--hover-background: #e3e1e1;

width: 18rem;
display: grid;
grid-template-columns: repeat(8, minmax(0, 1fr));

max-height: 14rem;
overflow-y: scroll;
overflow-x: auto;
overflow-y: auto;
border-radius: 4px;
font-family: inherit;
background-color: var(--background);
padding: 6px;
word-break: break-word;
scroll-padding: 6px;
gap: 6px;
}

.carta-emoji button {
background: var(--contrast-background);

aspect-ratio: 1;
display: inline-block;
border-radius: 4px;
border: 0;
padding: 0;
margin: 0.175rem;

cursor: pointer;

height: 2rem;
width: 2rem;
font-size: 1.2rem;
line-height: 100%;
white-space: nowrap;
}

.carta-emoji button:hover,
Expand Down
9 changes: 8 additions & 1 deletion packages/plugin-emoji/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export interface EmojiExtensionOptions {
* Custom out transition. See https://svelte.dev/docs#run-time-svelte-transition.
*/
outTransition?: (node: Element) => TransitionConfig;
/**
* Maximum count of emoji icons to display that match partial user input.
*/
maxResults?: number;
/**
* Options for the 'remark-emoji' plugin.
*/
Expand All @@ -25,6 +29,7 @@ export interface EmojiExtensionOptions {
interface ComponentProps {
inTransition: (node: Element) => TransitionConfig;
outTransition: (node: Element) => TransitionConfig;
maxResults: number;
}

/**
Expand All @@ -44,13 +49,15 @@ export const emoji = (options?: EmojiExtensionOptions): Plugin => {
fade(node, {
duration: 100
}));
const maxResults = options?.maxResults ?? 120;

const emojiComponent: ExtensionComponent<ComponentProps> = {
component: Emoji,
parent: 'input',
props: {
inTransition,
outTransition
outTransition,
maxResults
}
};

Expand Down

0 comments on commit db8d3f0

Please sign in to comment.