feat: Update the design of mentions with thumbnail (#5551)
This commit is contained in:
parent
8b0e95ece8
commit
cd4c1ef27e
5 changed files with 251 additions and 65 deletions
|
@ -6,7 +6,7 @@
|
|||
@click="insertMentionNode"
|
||||
/>
|
||||
<canned-response
|
||||
v-if="showCannedMenu"
|
||||
v-if="showCannedMenu && !isPrivate"
|
||||
:search-key="cannedSearchTerm"
|
||||
@click="insertCannedResponse"
|
||||
/>
|
||||
|
@ -223,8 +223,8 @@ export default {
|
|||
return null;
|
||||
}
|
||||
const node = this.editorView.state.schema.nodes.mention.create({
|
||||
userId: mentionItem.key,
|
||||
userFullName: mentionItem.label,
|
||||
userId: mentionItem.id,
|
||||
userFullName: mentionItem.name,
|
||||
});
|
||||
|
||||
const tr = this.editorView.state.tr.replaceWith(
|
||||
|
@ -256,6 +256,7 @@ export default {
|
|||
this.plugins
|
||||
);
|
||||
this.editorView.updateState(this.state);
|
||||
this.focusEditorInputField();
|
||||
return false;
|
||||
},
|
||||
|
||||
|
|
|
@ -1,49 +1,160 @@
|
|||
<template>
|
||||
<mention-box :items="items" @mention-select="handleMentionClick" />
|
||||
<ul
|
||||
v-if="items.length"
|
||||
class="vertical dropdown menu mention--box"
|
||||
:style="{ top: getTopSpacing() + 'rem' }"
|
||||
:class="{ 'with-bottom-border': items.length <= 4 }"
|
||||
>
|
||||
<li
|
||||
v-for="(agent, index) in items"
|
||||
:id="`mention-item-${index}`"
|
||||
:key="agent.id"
|
||||
:class="{ active: index === selectedIndex }"
|
||||
@click="onAgentSelect(index)"
|
||||
@mouseover="onHover(index)"
|
||||
>
|
||||
<div class="mention--thumbnail">
|
||||
<woot-thumbnail
|
||||
:src="agent.thumbnail"
|
||||
:username="agent.name"
|
||||
size="32px"
|
||||
/>
|
||||
</div>
|
||||
<div class="mention--metadata text-truncate">
|
||||
<h5 class="text-block-title mention--user-name text-truncate">
|
||||
{{ agent.name }}
|
||||
</h5>
|
||||
<div class="text-truncate mention--email text-truncate">
|
||||
{{ agent.email }}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import MentionBox from '../mentions/MentionBox.vue';
|
||||
|
||||
import mentionSelectionKeyboardMixin from '../mentions/mentionSelectionKeyboardMixin';
|
||||
export default {
|
||||
components: { MentionBox },
|
||||
mixins: [mentionSelectionKeyboardMixin],
|
||||
props: {
|
||||
searchKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { selectedIndex: 0 };
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
agents: 'agents/getVerifiedAgents',
|
||||
}),
|
||||
...mapGetters({ agents: 'agents/getVerifiedAgents' }),
|
||||
items() {
|
||||
if (!this.searchKey) {
|
||||
return this.agents.map(agent => ({
|
||||
label: agent.name,
|
||||
key: agent.id,
|
||||
description: agent.email,
|
||||
}));
|
||||
return this.agents;
|
||||
}
|
||||
|
||||
return this.agents
|
||||
.filter(agent =>
|
||||
agent.name
|
||||
.toLocaleLowerCase()
|
||||
.includes(this.searchKey.toLocaleLowerCase())
|
||||
)
|
||||
.map(agent => ({
|
||||
label: agent.name,
|
||||
key: agent.id,
|
||||
description: agent.email,
|
||||
}));
|
||||
return this.agents.filter(agent =>
|
||||
agent.name
|
||||
.toLocaleLowerCase()
|
||||
.includes(this.searchKey.toLocaleLowerCase())
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
items(newListOfAgents) {
|
||||
if (newListOfAgents.length < this.selectedIndex + 1) {
|
||||
this.selectedIndex = 0;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleMentionClick(item = {}) {
|
||||
this.$emit('click', item);
|
||||
getTopSpacing() {
|
||||
if (this.items.length <= 4) {
|
||||
return -(this.items.length * 5 + 1.7);
|
||||
}
|
||||
return -20;
|
||||
},
|
||||
handleKeyboardEvent(e) {
|
||||
this.processKeyDownEvent(e);
|
||||
this.$el.scrollTop = 50 * this.selectedIndex;
|
||||
},
|
||||
onHover(index) {
|
||||
this.selectedIndex = index;
|
||||
},
|
||||
onAgentSelect(index) {
|
||||
this.selectedIndex = index;
|
||||
this.onSelect();
|
||||
},
|
||||
onSelect() {
|
||||
this.$emit('click', this.items[this.selectedIndex]);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mention--box {
|
||||
background: var(--white);
|
||||
border-top: 1px solid var(--color-border);
|
||||
font-size: var(--font-size-small);
|
||||
left: 0;
|
||||
line-height: 1.2;
|
||||
max-height: 20rem;
|
||||
overflow: auto;
|
||||
padding: var(--space-small) var(--space-small) 0 var(--space-small);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
|
||||
&.with-bottom-border {
|
||||
border-bottom: var(--space-small) solid var(--white);
|
||||
|
||||
li {
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
align-items: center;
|
||||
border-radius: var(--border-radius-normal);
|
||||
display: flex;
|
||||
padding: var(--space-small);
|
||||
|
||||
&.active {
|
||||
background: var(--s-50);
|
||||
|
||||
.mention--user-name {
|
||||
color: var(--s-900);
|
||||
}
|
||||
.mention--email {
|
||||
color: var(--s-800);
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: var(--space-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mention--thumbnail {
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
|
||||
.mention--user-name {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mention--email {
|
||||
color: var(--s-700);
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
|
||||
.mention--metadata {
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -20,7 +20,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import mentionSelectionKeyboardMixin from './mentionSelectionKeyboardMixin';
|
||||
export default {
|
||||
mixins: [mentionSelectionKeyboardMixin],
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
|
@ -39,56 +41,25 @@ export default {
|
|||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.keyListener);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.keyListener);
|
||||
},
|
||||
methods: {
|
||||
getTopPadding() {
|
||||
if (this.items.length <= 4) {
|
||||
return -(this.items.length * 2.8 + 1.7);
|
||||
return -(this.items.length * 2.9 + 1.7);
|
||||
}
|
||||
return -14;
|
||||
},
|
||||
isUp(e) {
|
||||
return e.keyCode === 38 || (e.ctrlKey && e.keyCode === 80); // UP, Ctrl-P
|
||||
},
|
||||
isDown(e) {
|
||||
return e.keyCode === 40 || (e.ctrlKey && e.keyCode === 78); // DOWN, Ctrl-N
|
||||
},
|
||||
isEnter(e) {
|
||||
return e.keyCode === 13;
|
||||
},
|
||||
keyListener(e) {
|
||||
if (this.isUp(e)) {
|
||||
if (!this.selectedIndex) {
|
||||
this.selectedIndex = this.items.length - 1;
|
||||
} else {
|
||||
this.selectedIndex -= 1;
|
||||
}
|
||||
}
|
||||
if (this.isDown(e)) {
|
||||
if (this.selectedIndex === this.items.length - 1) {
|
||||
this.selectedIndex = 0;
|
||||
} else {
|
||||
this.selectedIndex += 1;
|
||||
}
|
||||
}
|
||||
if (this.isEnter(e)) {
|
||||
this.onMentionSelect();
|
||||
}
|
||||
this.$el.scrollTop = 28 * this.selectedIndex;
|
||||
handleKeyboardEvent(e) {
|
||||
this.processKeyDownEvent(e);
|
||||
this.$el.scrollTop = 29 * this.selectedIndex;
|
||||
},
|
||||
onHover(index) {
|
||||
this.selectedIndex = index;
|
||||
},
|
||||
onListItemSelection(index) {
|
||||
this.selectedIndex = index;
|
||||
this.onMentionSelect();
|
||||
this.onSelect();
|
||||
},
|
||||
onMentionSelect() {
|
||||
onSelect() {
|
||||
this.$emit('mention-select', this.items[this.selectedIndex]);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
||||
|
||||
export default {
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.handleKeyboardEvent);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.handleKeyboardEvent);
|
||||
},
|
||||
methods: {
|
||||
moveSelectionUp() {
|
||||
if (!this.selectedIndex) {
|
||||
this.selectedIndex = this.items.length - 1;
|
||||
} else {
|
||||
this.selectedIndex -= 1;
|
||||
}
|
||||
},
|
||||
moveSelectionDown() {
|
||||
if (this.selectedIndex === this.items.length - 1) {
|
||||
this.selectedIndex = 0;
|
||||
} else {
|
||||
this.selectedIndex += 1;
|
||||
}
|
||||
},
|
||||
processKeyDownEvent(e) {
|
||||
const keyPattern = buildHotKeys(e);
|
||||
if (['arrowup', 'ctrl+p'].includes(keyPattern)) {
|
||||
this.moveSelectionUp();
|
||||
e.preventDefault();
|
||||
} else if (['arrowdown', 'ctrl+n'].includes(keyPattern)) {
|
||||
this.moveSelectionDown();
|
||||
e.preventDefault();
|
||||
} else if (keyPattern === 'enter') {
|
||||
this.onSelect();
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,64 @@
|
|||
import mentionSelectionKeyboardMixin from '../mentionSelectionKeyboardMixin';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
|
||||
const buildComponent = ({ data = {}, methods = {} }) => ({
|
||||
render() {},
|
||||
data() {
|
||||
return data;
|
||||
},
|
||||
methods,
|
||||
mixins: [mentionSelectionKeyboardMixin],
|
||||
});
|
||||
|
||||
describe('mentionSelectionKeyboardMixin', () => {
|
||||
test('register listeners', () => {
|
||||
jest.spyOn(document, 'addEventListener');
|
||||
const Component = buildComponent({});
|
||||
shallowMount(Component);
|
||||
// undefined expected as the method is not defined in the component
|
||||
expect(document.addEventListener).toHaveBeenCalledWith(
|
||||
'keydown',
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
test('processKeyDownEvent updates index on arrow up', () => {
|
||||
const Component = buildComponent({
|
||||
data: { selectedIndex: 0, items: [1, 2, 3] },
|
||||
});
|
||||
const wrapper = shallowMount(Component);
|
||||
wrapper.vm.processKeyDownEvent({
|
||||
ctrlKey: true,
|
||||
key: 'p',
|
||||
preventDefault: jest.fn(),
|
||||
});
|
||||
expect(wrapper.vm.selectedIndex).toBe(2);
|
||||
});
|
||||
|
||||
test('processKeyDownEvent updates index on arrow down', () => {
|
||||
const Component = buildComponent({
|
||||
data: { selectedIndex: 0, items: [1, 2, 3] },
|
||||
});
|
||||
const wrapper = shallowMount(Component);
|
||||
wrapper.vm.processKeyDownEvent({
|
||||
key: 'ArrowDown',
|
||||
preventDefault: jest.fn(),
|
||||
});
|
||||
expect(wrapper.vm.selectedIndex).toBe(1);
|
||||
});
|
||||
|
||||
test('processKeyDownEvent calls select methods on Enter Key', () => {
|
||||
const onSelectMockFn = jest.fn();
|
||||
const Component = buildComponent({
|
||||
data: { selectedIndex: 0, items: [1, 2, 3] },
|
||||
methods: { onSelect: () => onSelectMockFn('enterKey pressed') },
|
||||
});
|
||||
const wrapper = shallowMount(Component);
|
||||
wrapper.vm.processKeyDownEvent({
|
||||
key: 'Enter',
|
||||
preventDefault: jest.fn(),
|
||||
});
|
||||
expect(onSelectMockFn).toHaveBeenCalledWith('enterKey pressed');
|
||||
wrapper.vm.onSelect();
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue