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"
|
@click="insertMentionNode"
|
||||||
/>
|
/>
|
||||||
<canned-response
|
<canned-response
|
||||||
v-if="showCannedMenu"
|
v-if="showCannedMenu && !isPrivate"
|
||||||
:search-key="cannedSearchTerm"
|
:search-key="cannedSearchTerm"
|
||||||
@click="insertCannedResponse"
|
@click="insertCannedResponse"
|
||||||
/>
|
/>
|
||||||
|
@ -223,8 +223,8 @@ export default {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const node = this.editorView.state.schema.nodes.mention.create({
|
const node = this.editorView.state.schema.nodes.mention.create({
|
||||||
userId: mentionItem.key,
|
userId: mentionItem.id,
|
||||||
userFullName: mentionItem.label,
|
userFullName: mentionItem.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tr = this.editorView.state.tr.replaceWith(
|
const tr = this.editorView.state.tr.replaceWith(
|
||||||
|
@ -256,6 +256,7 @@ export default {
|
||||||
this.plugins
|
this.plugins
|
||||||
);
|
);
|
||||||
this.editorView.updateState(this.state);
|
this.editorView.updateState(this.state);
|
||||||
|
this.focusEditorInputField();
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,49 +1,160 @@
|
||||||
<template>
|
<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>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import MentionBox from '../mentions/MentionBox.vue';
|
import mentionSelectionKeyboardMixin from '../mentions/mentionSelectionKeyboardMixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { MentionBox },
|
mixins: [mentionSelectionKeyboardMixin],
|
||||||
props: {
|
props: {
|
||||||
searchKey: {
|
searchKey: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return { selectedIndex: 0 };
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({ agents: 'agents/getVerifiedAgents' }),
|
||||||
agents: 'agents/getVerifiedAgents',
|
|
||||||
}),
|
|
||||||
items() {
|
items() {
|
||||||
if (!this.searchKey) {
|
if (!this.searchKey) {
|
||||||
return this.agents.map(agent => ({
|
return this.agents;
|
||||||
label: agent.name,
|
|
||||||
key: agent.id,
|
|
||||||
description: agent.email,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.agents
|
return this.agents.filter(agent =>
|
||||||
.filter(agent =>
|
agent.name
|
||||||
agent.name
|
.toLocaleLowerCase()
|
||||||
.toLocaleLowerCase()
|
.includes(this.searchKey.toLocaleLowerCase())
|
||||||
.includes(this.searchKey.toLocaleLowerCase())
|
);
|
||||||
)
|
|
||||||
.map(agent => ({
|
|
||||||
label: agent.name,
|
|
||||||
key: agent.id,
|
|
||||||
description: agent.email,
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
items(newListOfAgents) {
|
||||||
|
if (newListOfAgents.length < this.selectedIndex + 1) {
|
||||||
|
this.selectedIndex = 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
handleMentionClick(item = {}) {
|
getTopSpacing() {
|
||||||
this.$emit('click', item);
|
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>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import mentionSelectionKeyboardMixin from './mentionSelectionKeyboardMixin';
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [mentionSelectionKeyboardMixin],
|
||||||
props: {
|
props: {
|
||||||
items: {
|
items: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
@ -39,56 +41,25 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
|
||||||
document.addEventListener('keydown', this.keyListener);
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
document.removeEventListener('keydown', this.keyListener);
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
getTopPadding() {
|
getTopPadding() {
|
||||||
if (this.items.length <= 4) {
|
if (this.items.length <= 4) {
|
||||||
return -(this.items.length * 2.8 + 1.7);
|
return -(this.items.length * 2.9 + 1.7);
|
||||||
}
|
}
|
||||||
return -14;
|
return -14;
|
||||||
},
|
},
|
||||||
isUp(e) {
|
handleKeyboardEvent(e) {
|
||||||
return e.keyCode === 38 || (e.ctrlKey && e.keyCode === 80); // UP, Ctrl-P
|
this.processKeyDownEvent(e);
|
||||||
},
|
this.$el.scrollTop = 29 * this.selectedIndex;
|
||||||
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;
|
|
||||||
},
|
},
|
||||||
onHover(index) {
|
onHover(index) {
|
||||||
this.selectedIndex = index;
|
this.selectedIndex = index;
|
||||||
},
|
},
|
||||||
onListItemSelection(index) {
|
onListItemSelection(index) {
|
||||||
this.selectedIndex = index;
|
this.selectedIndex = index;
|
||||||
this.onMentionSelect();
|
this.onSelect();
|
||||||
},
|
},
|
||||||
onMentionSelect() {
|
onSelect() {
|
||||||
this.$emit('mention-select', this.items[this.selectedIndex]);
|
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…
Add table
Add a link
Reference in a new issue