Add arrow navigation to the emoji picker
This commit is contained in:
parent
2b213d0561
commit
995f2a9997
@ -75,9 +75,13 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
&.insert-emoji {
|
&.insert-emoji {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
height: 32px;
|
padding: 3px;
|
||||||
|
height: 30px;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: var(--highlight-color);
|
||||||
|
}
|
||||||
&.picked {
|
&.picked {
|
||||||
background-color: var(--highlight-color);
|
background-color: var(--highlight-color);
|
||||||
}
|
}
|
||||||
@ -112,6 +116,9 @@
|
|||||||
border: 1px var(--chat-head-color) solid;
|
border: 1px var(--chat-head-color) solid;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
&.selected {
|
||||||
|
background-color: var(--highlight-color);
|
||||||
|
}
|
||||||
padding: 0.25em;
|
padding: 0.25em;
|
||||||
font-size: var(--font-size-huge);
|
font-size: var(--font-size-huge);
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -62,7 +62,7 @@ $mobile_portrait_length: 480px !default;
|
|||||||
--chat-topic-display: block;
|
--chat-topic-display: block;
|
||||||
--chat-info-display: block;
|
--chat-info-display: block;
|
||||||
|
|
||||||
--highlight-color: #DCF9F6;
|
--highlight-color: #B0E8E2;
|
||||||
|
|
||||||
--primary-color: var(--light-blue);
|
--primary-color: var(--light-blue);
|
||||||
--primary-color-dark: #397491;
|
--primary-color-dark: #397491;
|
||||||
|
@ -74,7 +74,7 @@
|
|||||||
|
|
||||||
// Check that ENTER now inserts the match
|
// Check that ENTER now inserts the match
|
||||||
const enter_event = Object.assign({}, tab_event, {'keyCode': 13, 'key': 'Enter', 'target': input});
|
const enter_event = Object.assign({}, tab_event, {'keyCode': 13, 'key': 'Enter', 'target': input});
|
||||||
view.emoji_picker_view.onKeyDown(enter_event);
|
view.emoji_picker_view._onGlobalKeyDown(enter_event);
|
||||||
expect(input.value).toBe('');
|
expect(input.value).toBe('');
|
||||||
expect(textarea.value).toBe(':grimacing: ');
|
expect(textarea.value).toBe(':grimacing: ');
|
||||||
|
|
||||||
@ -136,7 +136,7 @@
|
|||||||
|
|
||||||
// Check that pressing enter without an unambiguous match does nothing
|
// Check that pressing enter without an unambiguous match does nothing
|
||||||
const enter_event = Object.assign({}, event, {'keyCode': 13});
|
const enter_event = Object.assign({}, event, {'keyCode': 13});
|
||||||
view.emoji_picker_view.onKeyDown(enter_event);
|
view.emoji_picker_view._onGlobalKeyDown(enter_event);
|
||||||
expect(input.value).toBe('smiley');
|
expect(input.value).toBe('smiley');
|
||||||
|
|
||||||
// Test that TAB autocompletes the to first match
|
// Test that TAB autocompletes the to first match
|
||||||
@ -148,7 +148,7 @@
|
|||||||
expect(visible_emojis[0].getAttribute('data-emoji')).toBe(':smiley:');
|
expect(visible_emojis[0].getAttribute('data-emoji')).toBe(':smiley:');
|
||||||
|
|
||||||
// Check that ENTER now inserts the match
|
// Check that ENTER now inserts the match
|
||||||
view.emoji_picker_view.onKeyDown(enter_event);
|
view.emoji_picker_view._onGlobalKeyDown(enter_event);
|
||||||
expect(input.value).toBe('');
|
expect(input.value).toBe('');
|
||||||
expect(view.el.querySelector('textarea.chat-textarea').value).toBe(':smiley: ');
|
expect(view.el.querySelector('textarea.chat-textarea').value).toBe(':smiley: ');
|
||||||
done();
|
done();
|
||||||
|
@ -282,28 +282,28 @@ converse.plugins.add("converse-autocomplete", {
|
|||||||
|
|
||||||
onKeyDown (ev) {
|
onKeyDown (ev) {
|
||||||
if (this.opened) {
|
if (this.opened) {
|
||||||
if (_.includes([_converse.keycodes.ENTER, _converse.keycodes.TAB], ev.keyCode) && this.selected) {
|
if (_.includes([converse.keycodes.ENTER, converse.keycodes.TAB], ev.keyCode) && this.selected) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this.select();
|
this.select();
|
||||||
return true;
|
return true;
|
||||||
} else if (ev.keyCode === _converse.keycodes.ESCAPE) {
|
} else if (ev.keyCode === converse.keycodes.ESCAPE) {
|
||||||
this.close({'reason': 'esc'});
|
this.close({'reason': 'esc'});
|
||||||
return true;
|
return true;
|
||||||
} else if (_.includes([_converse.keycodes.UP_ARROW, _converse.keycodes.DOWN_ARROW], ev.keyCode)) {
|
} else if (_.includes([converse.keycodes.UP_ARROW, converse.keycodes.DOWN_ARROW], ev.keyCode)) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this[ev.keyCode === _converse.keycodes.UP_ARROW ? "previous" : "next"]();
|
this[ev.keyCode === converse.keycodes.UP_ARROW ? "previous" : "next"]();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_.includes([
|
if (_.includes([
|
||||||
_converse.keycodes.SHIFT,
|
converse.keycodes.SHIFT,
|
||||||
_converse.keycodes.META,
|
converse.keycodes.META,
|
||||||
_converse.keycodes.META_RIGHT,
|
converse.keycodes.META_RIGHT,
|
||||||
_converse.keycodes.ESCAPE,
|
converse.keycodes.ESCAPE,
|
||||||
_converse.keycodes.ALT]
|
converse.keycodes.ALT]
|
||||||
, ev.keyCode)) {
|
, ev.keyCode)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -323,8 +323,8 @@ converse.plugins.add("converse-autocomplete", {
|
|||||||
|
|
||||||
evaluate (ev) {
|
evaluate (ev) {
|
||||||
const selecting = this.selected && ev && (
|
const selecting = this.selected && ev && (
|
||||||
ev.keyCode === _converse.keycodes.UP_ARROW ||
|
ev.keyCode === converse.keycodes.UP_ARROW ||
|
||||||
ev.keyCode === _converse.keycodes.DOWN_ARROW
|
ev.keyCode === converse.keycodes.DOWN_ARROW
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!this.auto_evaluate && !this.auto_completing || selecting) {
|
if (!this.auto_evaluate && !this.auto_completing || selecting) {
|
||||||
|
@ -869,29 +869,29 @@ converse.plugins.add('converse-chatview', {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
if (!ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||||
if (ev.keyCode === _converse.keycodes.FORWARD_SLASH) {
|
if (ev.keyCode === converse.keycodes.FORWARD_SLASH) {
|
||||||
// Forward slash is used to run commands. Nothing to do here.
|
// Forward slash is used to run commands. Nothing to do here.
|
||||||
return;
|
return;
|
||||||
} else if (ev.keyCode === _converse.keycodes.ESCAPE) {
|
} else if (ev.keyCode === converse.keycodes.ESCAPE) {
|
||||||
return this.onEscapePressed(ev);
|
return this.onEscapePressed(ev);
|
||||||
} else if (ev.keyCode === _converse.keycodes.ENTER) {
|
} else if (ev.keyCode === converse.keycodes.ENTER) {
|
||||||
return this.onEnterPressed(ev);
|
return this.onEnterPressed(ev);
|
||||||
} else if (ev.keyCode === _converse.keycodes.UP_ARROW && !ev.target.selectionEnd) {
|
} else if (ev.keyCode === converse.keycodes.UP_ARROW && !ev.target.selectionEnd) {
|
||||||
const textarea = this.el.querySelector('.chat-textarea');
|
const textarea = this.el.querySelector('.chat-textarea');
|
||||||
if (!textarea.value || u.hasClass('correcting', textarea)) {
|
if (!textarea.value || u.hasClass('correcting', textarea)) {
|
||||||
return this.editEarlierMessage();
|
return this.editEarlierMessage();
|
||||||
}
|
}
|
||||||
} else if (ev.keyCode === _converse.keycodes.DOWN_ARROW &&
|
} else if (ev.keyCode === converse.keycodes.DOWN_ARROW &&
|
||||||
ev.target.selectionEnd === ev.target.value.length &&
|
ev.target.selectionEnd === ev.target.value.length &&
|
||||||
u.hasClass('correcting', this.el.querySelector('.chat-textarea'))) {
|
u.hasClass('correcting', this.el.querySelector('.chat-textarea'))) {
|
||||||
return this.editLaterMessage();
|
return this.editLaterMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ([_converse.keycodes.SHIFT,
|
if ([converse.keycodes.SHIFT,
|
||||||
_converse.keycodes.META,
|
converse.keycodes.META,
|
||||||
_converse.keycodes.META_RIGHT,
|
converse.keycodes.META_RIGHT,
|
||||||
_converse.keycodes.ESCAPE,
|
converse.keycodes.ESCAPE,
|
||||||
_converse.keycodes.ALT].includes(ev.keyCode)) {
|
converse.keycodes.ALT].includes(ev.keyCode)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.model.get('chat_state') !== _converse.COMPOSING) {
|
if (this.model.get('chat_state') !== _converse.COMPOSING) {
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
// Converse.js
|
|
||||||
// https://conversejs.org
|
|
||||||
//
|
|
||||||
// Copyright (c) 2013-2019, the Converse.js developers
|
|
||||||
// Licensed under the Mozilla Public License (MPLv2)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module converse-emoji-views
|
* @module converse-emoji-views
|
||||||
|
* @copyright 2013-2019, the Converse.js developers
|
||||||
|
* @license Mozilla Public License (MPLv2)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "@converse/headless/converse-emoji";
|
import "@converse/headless/converse-emoji";
|
||||||
import { debounce, find } from "lodash";
|
import { debounce, find, get } from "lodash";
|
||||||
|
import DOMNavigator from "./dom-navigator";
|
||||||
import bootstrap from "bootstrap.native";
|
import bootstrap from "bootstrap.native";
|
||||||
import tpl_emoji_button from "templates/emoji_button.html";
|
import tpl_emoji_button from "templates/emoji_button.html";
|
||||||
import tpl_emojis from "templates/emojis.html";
|
import tpl_emojis from "templates/emojis.html";
|
||||||
@ -46,8 +43,7 @@ converse.plugins.add('converse-emoji-views', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onKeyDown (ev) {
|
onKeyDown (ev) {
|
||||||
const { _converse } = this.__super__;
|
if (ev.keyCode === converse.keycodes.TAB) {
|
||||||
if (ev.keyCode === _converse.keycodes.TAB) {
|
|
||||||
const value = u.getCurrentWord(ev.target, null, /(:.*?:)/g);
|
const value = u.getCurrentWord(ev.target, null, /(:.*?:)/g);
|
||||||
if (value.startsWith(':')) {
|
if (value.startsWith(':')) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
@ -95,6 +91,10 @@ converse.plugins.add('converse-emoji-views', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
createEmojiPicker () {
|
createEmojiPicker () {
|
||||||
|
if (this.emoji_picker_view) {
|
||||||
|
this.insertEmojiPicker();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!_converse.emojipicker) {
|
if (!_converse.emojipicker) {
|
||||||
const id = `converse.emoji-${_converse.bare_jid}`;
|
const id = `converse.emoji-${_converse.bare_jid}`;
|
||||||
_converse.emojipicker = new _converse.EmojiPicker({'id': id});
|
_converse.emojipicker = new _converse.EmojiPicker({'id': id});
|
||||||
@ -134,18 +134,28 @@ converse.plugins.add('converse-emoji-views', {
|
|||||||
_converse.EmojiPickerView = Backbone.VDOMView.extend({
|
_converse.EmojiPickerView = Backbone.VDOMView.extend({
|
||||||
className: 'emoji-picker',
|
className: 'emoji-picker',
|
||||||
events: {
|
events: {
|
||||||
'click .emoji-picker__header li.emoji-category': 'chooseCategory',
|
'click .emoji-picker__header li.emoji-category .pick-category': 'chooseCategory',
|
||||||
'click .emoji-skintone-picker li.emoji-skintone': 'chooseSkinTone',
|
'click .emoji-skintone-picker li.emoji-skintone': 'chooseSkinTone',
|
||||||
'click .insert-emoji': 'insertEmoji',
|
'click .insert-emoji': 'insertEmoji',
|
||||||
|
'focus .emoji-search': 'disableArrowNavigation',
|
||||||
'keydown .emoji-search': 'onKeyDown'
|
'keydown .emoji-search': 'onKeyDown'
|
||||||
},
|
},
|
||||||
|
|
||||||
async initialize () {
|
async initialize () {
|
||||||
|
this.onGlobalKeyDown = ev => this._onGlobalKeyDown(ev);
|
||||||
|
document.addEventListener('keydown', this.onGlobalKeyDown);
|
||||||
|
|
||||||
this.search_results = [];
|
this.search_results = [];
|
||||||
this.debouncedFilter = debounce(input => this.filter(input.value), 150);
|
this.debouncedFilter = debounce(input => this.filter(input.value), 150);
|
||||||
this.listenTo(this.model, 'change:query', this.render)
|
this.listenTo(this.model, 'change:query', this.render)
|
||||||
this.listenTo(this.model, 'change:current_skintone', this.render)
|
this.listenTo(this.model, 'change:current_skintone', this.render)
|
||||||
this.listenTo(this.model, 'change:current_category', this.render)
|
this.listenTo(this.model, 'change:current_category', () => {
|
||||||
|
this.render();
|
||||||
|
const category = this.model.get('current_category');
|
||||||
|
const el = this.el.querySelector(`.emoji-category[data-category="${category}"]`);
|
||||||
|
this.navigator.select(el);
|
||||||
|
!this.navigator.enabled && this.navigator.enable();
|
||||||
|
});
|
||||||
await _converse.api.waitUntil('emojisInitialized');
|
await _converse.api.waitUntil('emojisInitialized');
|
||||||
this.render();
|
this.render();
|
||||||
},
|
},
|
||||||
@ -169,11 +179,45 @@ converse.plugins.add('converse-emoji-views', {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
remove () {
|
||||||
|
document.removeEventListener('keydown', this.onGlobalKeyDown);
|
||||||
|
Backbone.VDOMView.prototype.remove.call(this);
|
||||||
|
},
|
||||||
|
|
||||||
afterRender () {
|
afterRender () {
|
||||||
this.initIntersectionObserver();
|
this.initIntersectionObserver();
|
||||||
const textarea = this.el.querySelector('.emoji-search');
|
const textarea = this.el.querySelector('.emoji-search');
|
||||||
textarea.addEventListener('focus', ev => this.chatview.emitFocused(ev));
|
textarea.addEventListener('focus', ev => this.chatview.emitFocused(ev));
|
||||||
textarea.addEventListener('blur', ev => this.chatview.emitBlurred(ev));
|
textarea.addEventListener('blur', ev => this.chatview.emitBlurred(ev));
|
||||||
|
this.initArrowNavigation();
|
||||||
|
},
|
||||||
|
|
||||||
|
initArrowNavigation () {
|
||||||
|
if (!this.navigator) {
|
||||||
|
const default_selector = 'li:not(.hidden):not(.emoji-skintone), .emoji-search';
|
||||||
|
const options = {
|
||||||
|
'jump_to_picked': '.emoji-category',
|
||||||
|
'jump_to_picked_selector': '.emoji-category.picked',
|
||||||
|
'jump_to_picked_direction': DOMNavigator.DIRECTION.down,
|
||||||
|
'picked_selector': '.picked',
|
||||||
|
'scroll_container': this.el.querySelector('.emoji-picker__lists'),
|
||||||
|
'getSelector': direction => {
|
||||||
|
if (direction === DOMNavigator.DIRECTION.down) {
|
||||||
|
const c = this.navigator.selected && this.navigator.selected.getAttribute('data-category');
|
||||||
|
return c ? `ul[data-category="${c}"] li:not(.hidden):not(.emoji-skintone), .emoji-search` : default_selector;
|
||||||
|
} else {
|
||||||
|
return default_selector;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'onSelected': el => el.matches('.insert-emoji') && this.setCategoryForElement(el.parentElement)
|
||||||
|
};
|
||||||
|
this.navigator = new DOMNavigator(this.el, options);
|
||||||
|
this.listenTo(this.chatview.model, 'destroy', () => this.navigator.destroy());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
disableArrowNavigation () {
|
||||||
|
this.navigator.disable();
|
||||||
},
|
},
|
||||||
|
|
||||||
filter (value, set_property) {
|
filter (value, set_property) {
|
||||||
@ -196,26 +240,36 @@ converse.plugins.add('converse-emoji-views', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setCategoryOnVisibilityChange (ev) {
|
setCategoryForElement (el) {
|
||||||
const current = ev.filter(e => e.isIntersecting).pop();
|
const category = el.getAttribute('data-category');
|
||||||
if (current) {
|
const old_category = this.model.get('current_category');
|
||||||
const category = current.target.getAttribute('data-category');
|
if (old_category !== category) {
|
||||||
const old_category = this.model.get('current_category');
|
// XXX: Manually set the classes, it's quicker than using the VDOM
|
||||||
if (old_category !== category) {
|
this.model.set(
|
||||||
// XXX: Manually set the classes, it's quicker than using the VDOM
|
{'current_category': category},
|
||||||
this.model.set(
|
{'silent': true}
|
||||||
{'current_category': category},
|
);
|
||||||
{'silent': true}
|
const category_els = sizzle('.emoji-picker__header .emoji-category', this.el);
|
||||||
);
|
category_els.forEach(el => u.removeClass('picked', el));
|
||||||
const categories = sizzle('.emoji-picker__header .emoji-category', this.el);
|
const new_el = category_els.filter(el => el.getAttribute('data-category') === category).pop();
|
||||||
const new_el = categories.filter(el => el.getAttribute('data-category') === category).pop();
|
new_el && u.addClass('picked', new_el);
|
||||||
const old_el = categories.filter(el => el.getAttribute('data-category') === old_category).pop();
|
|
||||||
new_el && u.addClass('picked', new_el);
|
|
||||||
old_el && u.removeClass('picked', old_el);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setCategoryOnVisibilityChange (ev) {
|
||||||
|
const selected = this.navigator.selected;
|
||||||
|
const intersection_with_selected = ev.filter(i => i.target.contains(selected)).pop();
|
||||||
|
let current;
|
||||||
|
// Choose the intersection that contains the currently selected
|
||||||
|
// element, or otherwise the one with the largest ratio.
|
||||||
|
if (intersection_with_selected) {
|
||||||
|
current = intersection_with_selected;
|
||||||
|
} else {
|
||||||
|
current = ev.reduce((p, c) => c.intersectionRatio >= get(p, 'intersectionRatio', 0) ? c : p, null);
|
||||||
|
}
|
||||||
|
current && current.isIntersecting && this.setCategoryForElement(current.target);
|
||||||
|
},
|
||||||
|
|
||||||
initIntersectionObserver () {
|
initIntersectionObserver () {
|
||||||
if (!window.IntersectionObserver) {
|
if (!window.IntersectionObserver) {
|
||||||
return;
|
return;
|
||||||
@ -225,10 +279,9 @@ converse.plugins.add('converse-emoji-views', {
|
|||||||
} else {
|
} else {
|
||||||
const options = {
|
const options = {
|
||||||
root: this.el.querySelector('.emoji-picker__lists'),
|
root: this.el.querySelector('.emoji-picker__lists'),
|
||||||
rootMargin: '0px',
|
threshold: [0.1]
|
||||||
threshold: [0.1, 0.2, 0.3, 0.4, 0.5]
|
|
||||||
}
|
}
|
||||||
const handler = debounce((ev) => this.setCategoryOnVisibilityChange(ev), 200);
|
const handler = ev => this.setCategoryOnVisibilityChange(ev);
|
||||||
this.observer = new IntersectionObserver(handler, options);
|
this.observer = new IntersectionObserver(handler, options);
|
||||||
}
|
}
|
||||||
sizzle('.emoji-picker', this.el).forEach(a => this.observer.observe(a));
|
sizzle('.emoji-picker', this.el).forEach(a => this.observer.observe(a));
|
||||||
@ -243,24 +296,62 @@ converse.plugins.add('converse-emoji-views', {
|
|||||||
this.chatview.emoji_dropdown.toggle();
|
this.chatview.emoji_dropdown.toggle();
|
||||||
}
|
}
|
||||||
this.filter('', true);
|
this.filter('', true);
|
||||||
|
this.disableArrowNavigation();
|
||||||
|
},
|
||||||
|
|
||||||
|
_onGlobalKeyDown (ev) {
|
||||||
|
if (ev.keyCode === converse.keycodes.ENTER) {
|
||||||
|
if (ev.target.matches('.emoji-search') || (
|
||||||
|
ev.target.matches('body') &&
|
||||||
|
u.isVisible(this.el) &&
|
||||||
|
this.navigator.selected
|
||||||
|
)) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
if (_converse.emoji_shortnames.includes(ev.target.value)) {
|
||||||
|
this.insertIntoTextArea(ev.target.value);
|
||||||
|
} else if (this.search_results.length === 1) {
|
||||||
|
this.insertIntoTextArea(this.search_results[0].sn);
|
||||||
|
} else if (this.navigator.selected && this.navigator.selected.matches('.insert-emoji')) {
|
||||||
|
this.insertIntoTextArea(this.navigator.selected.getAttribute('data-emoji'));
|
||||||
|
} else if (this.navigator.selected && this.navigator.selected.matches('.emoji-category')) {
|
||||||
|
this.chooseCategory({'target': this.navigator.selected});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (ev.keyCode === converse.keycodes.DOWN_ARROW) {
|
||||||
|
if (ev.target.matches('.emoji-search') || (
|
||||||
|
!this.navigator.enabled &&
|
||||||
|
(ev.target.matches('.pick-category') || ev.target.matches('body')) &&
|
||||||
|
u.isVisible(this.el)
|
||||||
|
)) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.target.blur();
|
||||||
|
const category = this.model.get('current_category');
|
||||||
|
// If there's no category, we're viewing search results.
|
||||||
|
const selector = category ? `ul[data-category="${category}"]` : 'ul';
|
||||||
|
this.disableArrowNavigation();
|
||||||
|
this.navigator.enable();
|
||||||
|
this.navigator.handleKeydown(ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeyDown (ev) {
|
onKeyDown (ev) {
|
||||||
if (ev.keyCode === _converse.keycodes.TAB) {
|
if (ev.keyCode === converse.keycodes.RIGHT_ARROW) {
|
||||||
ev.preventDefault();
|
|
||||||
const match = find(_converse.emoji_shortnames, sn => _converse.FILTER_CONTAINS(sn, ev.target.value));
|
|
||||||
if (match) {
|
|
||||||
this.filter(match, true);
|
|
||||||
}
|
|
||||||
} else if (ev.keyCode === _converse.keycodes.ENTER) {
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
if (_converse.emoji_shortnames.includes(ev.target.value)) {
|
ev.target.blur();
|
||||||
this.insertIntoTextArea(ev.target.value);
|
const first_el = this.el.querySelector('.pick-category');
|
||||||
} else if (this.search_results.length === 1) {
|
this.navigator.select(first_el, 'right');
|
||||||
this.insertIntoTextArea(this.search_results[0].sn);
|
} else if (ev.keyCode === converse.keycodes.TAB) {
|
||||||
}
|
ev.preventDefault();
|
||||||
} else {
|
const match = find(_converse.emoji_shortnames, sn => _converse.FILTER_CONTAINS(sn, ev.target.value));
|
||||||
|
match && this.filter(match, true);
|
||||||
|
} else if (
|
||||||
|
ev.keyCode !== converse.keycodes.ENTER &&
|
||||||
|
ev.keyCode !== converse.keycodes.DOWN_ARROW
|
||||||
|
) {
|
||||||
this.debouncedFilter(ev.target);
|
this.debouncedFilter(ev.target);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -307,14 +398,13 @@ converse.plugins.add('converse-emoji-views', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
chooseCategory (ev) {
|
chooseCategory (ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault && ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation && ev.stopPropagation();
|
||||||
const target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target;
|
|
||||||
const category = target.getAttribute("data-category").trim();
|
|
||||||
// XXX: See above
|
|
||||||
const input = this.el.querySelector('.emoji-search');
|
const input = this.el.querySelector('.emoji-search');
|
||||||
input.value = '';
|
input.value = '';
|
||||||
this.model.save({'current_category': category, 'query': undefined});
|
const target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target;
|
||||||
|
this.setCategoryForElement(target);
|
||||||
|
this.navigator.select(target);
|
||||||
this.setScrollPosition();
|
this.setScrollPosition();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -322,7 +412,9 @@ converse.plugins.add('converse-emoji-views', {
|
|||||||
const category = this.model.get('current_category');
|
const category = this.model.get('current_category');
|
||||||
const el = this.el.querySelector('.emoji-picker__lists');
|
const el = this.el.querySelector('.emoji-picker__lists');
|
||||||
const heading = this.el.querySelector(`#emoji-picker-${category}`);
|
const heading = this.el.querySelector(`#emoji-picker-${category}`);
|
||||||
el.scrollTop = heading.offsetTop - heading.offsetHeight*3;
|
if (heading) {
|
||||||
|
el.scrollTop = heading.offsetTop - heading.offsetHeight*3;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
insertEmoji (ev) {
|
insertEmoji (ev) {
|
||||||
@ -340,6 +432,9 @@ converse.plugins.add('converse-emoji-views', {
|
|||||||
|
|
||||||
|
|
||||||
/************************ BEGIN Event Handlers ************************/
|
/************************ BEGIN Event Handlers ************************/
|
||||||
|
|
||||||
|
_converse.api.listen.on('chatBoxClosed', view => view.emoji_picker_view && view.emoji_picker_view.remove());
|
||||||
|
|
||||||
_converse.api.listen.on('renderToolbar', view => {
|
_converse.api.listen.on('renderToolbar', view => {
|
||||||
if (_converse.visible_toolbar_buttons.emoji) {
|
if (_converse.visible_toolbar_buttons.emoji) {
|
||||||
const html = tpl_emoji_button({'tooltip_insert_smiley': __('Insert emojis')});
|
const html = tpl_emoji_button({'tooltip_insert_smiley': __('Insert emojis')});
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
// Converse.js
|
|
||||||
// https://conversejs.org
|
|
||||||
//
|
|
||||||
// Copyright (c) 2013-2019, the Converse.js developers
|
|
||||||
// Licensed under the Mozilla Public License (MPLv2)
|
|
||||||
//
|
|
||||||
/**
|
/**
|
||||||
* @module converse-muc-views
|
* @module converse-muc-views
|
||||||
* @description
|
* @copyright 2013-2019, the Converse.js developers
|
||||||
* XEP-0045 Multi-User Chat Views
|
* @description XEP-0045 Multi-User Chat Views
|
||||||
|
* @license Mozilla Public License (MPLv2)
|
||||||
*/
|
*/
|
||||||
import "converse-modal";
|
import "converse-modal";
|
||||||
import "backbone.vdomview";
|
import "backbone.vdomview";
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
// Converse.js
|
|
||||||
// https://conversejs.org
|
|
||||||
//
|
|
||||||
// Copyright (c) 2013-2019, the Converse.js developers
|
|
||||||
// Licensed under the Mozilla Public License (MPLv2)
|
|
||||||
/**
|
/**
|
||||||
* @module converse-singleton
|
* @module converse-singleton
|
||||||
* @description
|
* @copyright JC Brand
|
||||||
* A plugin which restricts Converse to only one chat.
|
* @license Mozilla Public License (MPLv2)
|
||||||
|
* @description A plugin which restricts Converse to only one chat.
|
||||||
*/
|
*/
|
||||||
import converse from "@converse/headless/converse-core";
|
import converse from "@converse/headless/converse-core";
|
||||||
|
|
||||||
|
420
src/dom-navigator.js
Normal file
420
src/dom-navigator.js
Normal file
@ -0,0 +1,420 @@
|
|||||||
|
/**
|
||||||
|
* @module dom-navigator
|
||||||
|
* @description A class for navigating the DOM with the keyboard
|
||||||
|
* This module started as a fork of Rubens Mariuzzo's dom-navigator.
|
||||||
|
* @copyright Rubens Mariuzzo, JC Brand
|
||||||
|
*/
|
||||||
|
import log from "@converse/headless/log";
|
||||||
|
import u from './utils/html';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if a given element is fully visible in the viewport.
|
||||||
|
* @param { Element } el The element to check.
|
||||||
|
* @return { Boolean } True if the given element is fully visible in the viewport, otherwise false.
|
||||||
|
*/
|
||||||
|
function inViewport(el) {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
return (
|
||||||
|
rect.top >= 0 &&
|
||||||
|
rect.left >= 0 &&
|
||||||
|
rect.bottom <= window.innerHeight &&
|
||||||
|
rect.right <= window.innerWidth
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the absolute offset top of an element.
|
||||||
|
* @param el { Element } The element.
|
||||||
|
* @return { Number } The offset top.
|
||||||
|
*/
|
||||||
|
function absoluteOffsetTop(el) {
|
||||||
|
let offsetTop = 0;
|
||||||
|
do {
|
||||||
|
if (!isNaN(el.offsetTop)) {
|
||||||
|
offsetTop += el.offsetTop;
|
||||||
|
}
|
||||||
|
} while ((el = el.offsetParent));
|
||||||
|
return offsetTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the absolute offset left of an element.
|
||||||
|
* @param el { Element } The element.
|
||||||
|
* @return { Number } The offset left.
|
||||||
|
*/
|
||||||
|
function absoluteOffsetLeft(el) {
|
||||||
|
let offsetLeft = 0;
|
||||||
|
do {
|
||||||
|
if (!isNaN(el.offsetLeft)) {
|
||||||
|
offsetLeft += el.offsetLeft;
|
||||||
|
}
|
||||||
|
} while ((el = el.offsetParent));
|
||||||
|
return offsetLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the ability to navigate the DOM with the arrow keys
|
||||||
|
* @class
|
||||||
|
* @namespace DOMNavigator
|
||||||
|
*/
|
||||||
|
class DOMNavigator {
|
||||||
|
/**
|
||||||
|
* Directions.
|
||||||
|
* @returns {{left: string, up: string, right: string, down: string}}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
static get DIRECTION () {
|
||||||
|
return {
|
||||||
|
left: 'left',
|
||||||
|
up: 'up',
|
||||||
|
right: 'right',
|
||||||
|
down: 'down'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default options for the DOM navigator.
|
||||||
|
* @returns {{
|
||||||
|
* down: number
|
||||||
|
* getSelector: null,
|
||||||
|
* jump_to_picked: null,
|
||||||
|
* jump_to_picked_direction: null,
|
||||||
|
* jump_to_picked_selector: string,
|
||||||
|
* left: number,
|
||||||
|
* onSelected: null,
|
||||||
|
* right: number,
|
||||||
|
* selected: string,
|
||||||
|
* up: number,
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
static get DEFAULTS () {
|
||||||
|
return {
|
||||||
|
down: 40,
|
||||||
|
getSelector: null,
|
||||||
|
jump_to_picked: null,
|
||||||
|
jump_to_picked_direction: null,
|
||||||
|
jump_to_picked_selector: 'picked',
|
||||||
|
left: 37,
|
||||||
|
onSelected: null,
|
||||||
|
right: 39,
|
||||||
|
selected: 'selected',
|
||||||
|
selector: 'li',
|
||||||
|
up: 38,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new DOM Navigator.
|
||||||
|
* @param { Element } container The container of the element to navigate.
|
||||||
|
* @param { Object } options The options to configure the DOM navigator.
|
||||||
|
* @param { Function } options.getSelector
|
||||||
|
* @param { Number } [options.down] - The keycode for navigating down
|
||||||
|
* @param { Number } [options.left] - The keycode for navigating left
|
||||||
|
* @param { Number } [options.right] - The keycode for navigating right
|
||||||
|
* @param { Number } [options.up] - The keycode for navigating up
|
||||||
|
* @param { String } [options.selected] - The class that should be added to the currently selected DOM element.
|
||||||
|
* @param { String } [options.jump_to_picked] - A selector, which if
|
||||||
|
* matched by the next element being navigated to, based on the direction
|
||||||
|
* given by `jump_to_picked_direction`, will cause navigation
|
||||||
|
* to jump to the element that matches the `jump_to_picked_selector`.
|
||||||
|
* For example, this is useful when navigating to tabs. You want to
|
||||||
|
* immediately navigate to the currently active tab instead of just
|
||||||
|
* navigating to the first tab.
|
||||||
|
* @param { String } [options.jump_to_picked_selector=picked] - The selector
|
||||||
|
* indicating the currently picked element to jump to.
|
||||||
|
* @param { String } [options.jump_to_picked_direction] - The direction for
|
||||||
|
* which jumping to the picked element should be enabled.
|
||||||
|
* @param { Function } [options.onSelected] - The callback function which
|
||||||
|
* should be called when en element gets selected.
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
constructor (container, options) {
|
||||||
|
this.doc = window.document;
|
||||||
|
this.container = container;
|
||||||
|
this.scroll_container = options.scroll_container || container;
|
||||||
|
this.options = Object.assign({}, DOMNavigator.DEFAULTS, options);
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the navigator.
|
||||||
|
* @method DOMNavigator#init
|
||||||
|
*/
|
||||||
|
init () {
|
||||||
|
this.selected = null;
|
||||||
|
this.keydownHandler = null;
|
||||||
|
this.elements = {};
|
||||||
|
// Create hotkeys map.
|
||||||
|
this.keys = {};
|
||||||
|
this.keys[this.options.left] = DOMNavigator.DIRECTION.left;
|
||||||
|
this.keys[this.options.up] = DOMNavigator.DIRECTION.up;
|
||||||
|
this.keys[this.options.right] = DOMNavigator.DIRECTION.right;
|
||||||
|
this.keys[this.options.down] = DOMNavigator.DIRECTION.down;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable this navigator.
|
||||||
|
* @method DOMNavigator#enable
|
||||||
|
*/
|
||||||
|
enable () {
|
||||||
|
this.getElements();
|
||||||
|
this.keydownHandler = event => this.handleKeydown(event);
|
||||||
|
this.doc.addEventListener('keydown', this.keydownHandler);
|
||||||
|
this.enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable this navigator.
|
||||||
|
* @method DOMNavigator#disable
|
||||||
|
*/
|
||||||
|
disable () {
|
||||||
|
if (this.keydownHandler) {
|
||||||
|
this.doc.removeEventListener('keydown', this.keydownHandler);
|
||||||
|
}
|
||||||
|
this.unselect();
|
||||||
|
this.elements = {};
|
||||||
|
this.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy this navigator removing any event registered and any other data.
|
||||||
|
* @method DOMNavigator#destroy
|
||||||
|
*/
|
||||||
|
destroy () {
|
||||||
|
this.disable();
|
||||||
|
if (this.container.domNavigator) {
|
||||||
|
delete this.container.domNavigator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getClosestElement (els, getDistance) {
|
||||||
|
const next = els.reduce((prev, curr) => {
|
||||||
|
const current_distance = getDistance(curr);
|
||||||
|
if (current_distance < prev.distance) {
|
||||||
|
return {
|
||||||
|
distance: current_distance,
|
||||||
|
element: curr
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
}, {
|
||||||
|
distance: Infinity
|
||||||
|
});
|
||||||
|
return next.element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @method DOMNavigator#getNextElement
|
||||||
|
* @param {'down'|'right'|'left'|'up'} direction
|
||||||
|
* @returns { HTMLElement }
|
||||||
|
*/
|
||||||
|
getNextElement (direction) {
|
||||||
|
let el;
|
||||||
|
if (this.selected) {
|
||||||
|
if (direction === DOMNavigator.DIRECTION.right) {
|
||||||
|
const els = this.getElements(direction);
|
||||||
|
el = els.slice(els.indexOf(this.selected))[1];
|
||||||
|
} else if (direction == DOMNavigator.DIRECTION.left) {
|
||||||
|
const els = this.getElements(direction);
|
||||||
|
el = els.slice(0, els.indexOf(this.selected)).pop() || this.selected;
|
||||||
|
} else if (direction == DOMNavigator.DIRECTION.down) {
|
||||||
|
const left = this.selected.offsetLeft;
|
||||||
|
const top = this.selected.offsetTop + this.selected.offsetHeight;
|
||||||
|
const els = this.elementsAfter(0, top);
|
||||||
|
const getDistance = el => Math.abs(el.offsetLeft - left) + Math.abs(el.offsetTop - top);
|
||||||
|
el = this.getClosestElement(els, getDistance);
|
||||||
|
} else if (direction == DOMNavigator.DIRECTION.up) {
|
||||||
|
const left = this.selected.offsetLeft;
|
||||||
|
const top = this.selected.offsetTop - 1;
|
||||||
|
const els = this.elementsBefore(Infinity, top);
|
||||||
|
const getDistance = el => Math.abs(left - el.offsetLeft) + Math.abs(top - el.offsetTop);
|
||||||
|
el = this.getClosestElement(els, getDistance);
|
||||||
|
} else {
|
||||||
|
throw new Error("getNextElement: invalid direction value");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (direction === DOMNavigator.DIRECTION.right || direction === DOMNavigator.DIRECTION.down) {
|
||||||
|
// If nothing is selected, we pretend that the first element is
|
||||||
|
// selected, so we return the next.
|
||||||
|
el = this.getElements(direction)[1];
|
||||||
|
} else {
|
||||||
|
el = this.getElements(direction)[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.jump_to_picked && el && el.matches(this.options.jump_to_picked) &&
|
||||||
|
direction === this.options.jump_to_picked_direction
|
||||||
|
) {
|
||||||
|
el = this.container.querySelector(this.options.jump_to_picked_selector) || el;
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select the given element.
|
||||||
|
* @method DOMNavigator#select
|
||||||
|
* @param { Element } el The DOM element to select.
|
||||||
|
* @param { string } [direction] The direction.
|
||||||
|
*/
|
||||||
|
select (el, direction) {
|
||||||
|
if (!el || el === this.selected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.unselect();
|
||||||
|
direction && this.scrollTo(el, direction);
|
||||||
|
if (el.matches('input')) {
|
||||||
|
el.focus();
|
||||||
|
} else {
|
||||||
|
u.addClass(this.options.selected, el);
|
||||||
|
}
|
||||||
|
this.selected = el;
|
||||||
|
this.options.onSelected && this.options.onSelected(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the current selection
|
||||||
|
* @method DOMNavigator#unselect
|
||||||
|
*/
|
||||||
|
unselect () {
|
||||||
|
if (this.selected) {
|
||||||
|
u.removeClass(this.options.selected, this.selected);
|
||||||
|
delete this.selected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll the container to an element.
|
||||||
|
* @method DOMNavigator#scrollTo
|
||||||
|
* @param { HTMLElement } el The destination element.
|
||||||
|
* @param { String } direction The direction of the current navigation.
|
||||||
|
* @return void.
|
||||||
|
*/
|
||||||
|
scrollTo (el, direction) {
|
||||||
|
if (!this.inScrollContainerViewport(el)) {
|
||||||
|
const container = this.scroll_container;
|
||||||
|
if (!container.contains(el)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (direction) {
|
||||||
|
case DOMNavigator.DIRECTION.left:
|
||||||
|
container.scrollLeft = el.offsetLeft - container.offsetLeft;
|
||||||
|
container.scrollTop = el.offsetTop - container.offsetTop;
|
||||||
|
break;
|
||||||
|
case DOMNavigator.DIRECTION.up:
|
||||||
|
container.scrollTop = el.offsetTop - container.offsetTop;
|
||||||
|
break;
|
||||||
|
case DOMNavigator.DIRECTION.right:
|
||||||
|
container.scrollLeft = el.offsetLeft - container.offsetLeft - (container.offsetWidth - el.offsetWidth);
|
||||||
|
container.scrollTop = el.offsetTop - container.offsetTop - (container.offsetHeight - el.offsetHeight);
|
||||||
|
break;
|
||||||
|
case DOMNavigator.DIRECTION.down:
|
||||||
|
container.scrollTop = el.offsetTop - container.offsetTop - (container.offsetHeight - el.offsetHeight);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (!inViewport(el)) {
|
||||||
|
switch (direction) {
|
||||||
|
case DOMNavigator.DIRECTION.left:
|
||||||
|
document.body.scrollLeft = absoluteOffsetLeft(el) - document.body.offsetLeft;
|
||||||
|
break;
|
||||||
|
case DOMNavigator.DIRECTION.up:
|
||||||
|
document.body.scrollTop = absoluteOffsetTop(el) - document.body.offsetTop;
|
||||||
|
break;
|
||||||
|
case DOMNavigator.DIRECTION.right:
|
||||||
|
document.body.scrollLeft = absoluteOffsetLeft(el) - document.body.offsetLeft - (document.documentElement.clientWidth - el.offsetWidth);
|
||||||
|
break;
|
||||||
|
case DOMNavigator.DIRECTION.down:
|
||||||
|
document.body.scrollTop = absoluteOffsetTop(el) - document.body.offsetTop - (document.documentElement.clientHeight - el.offsetHeight);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate if an element is in the container viewport.
|
||||||
|
* @method DOMNavigator#inScrollContainerViewport
|
||||||
|
* @param { HTMLElement } el The element to check.
|
||||||
|
* @return { Boolean } true if the given element is in the container viewport, otherwise false.
|
||||||
|
*/
|
||||||
|
inScrollContainerViewport(el) {
|
||||||
|
const container = this.scroll_container;
|
||||||
|
// Check on left side.
|
||||||
|
if (el.offsetLeft - container.scrollLeft < container.offsetLeft) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Check on top side.
|
||||||
|
if (el.offsetTop - container.scrollTop < container.offsetTop) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Check on right side.
|
||||||
|
if ((el.offsetLeft + el.offsetWidth - container.scrollLeft) > (container.offsetLeft + container.offsetWidth)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Check on down side.
|
||||||
|
if ((el.offsetTop + el.offsetHeight - container.scrollTop) > (container.offsetTop + container.offsetHeight)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find and store the navigable elements
|
||||||
|
* @method DOMNavigator#getElements
|
||||||
|
*/
|
||||||
|
getElements (direction) {
|
||||||
|
const selector = this.options.getSelector ? this.options.getSelector(direction) : this.options.selector;
|
||||||
|
if (!this.elements[selector]) {
|
||||||
|
this.elements[selector] = Array.from(this.container.querySelectorAll(selector));
|
||||||
|
}
|
||||||
|
return this.elements[selector];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of navigable elements after an offset.
|
||||||
|
* @method DOMNavigator#elementsAfter
|
||||||
|
* @param { number } left The left offset.
|
||||||
|
* @param { number } top The top offset.
|
||||||
|
* @return { Array } An array of elements.
|
||||||
|
*/
|
||||||
|
elementsAfter (left, top) {
|
||||||
|
return this.getElements(DOMNavigator.DIRECTION.down).filter(el => el.offsetLeft >= left && el.offsetTop >= top);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of navigable elements before an offset.
|
||||||
|
* @method DOMNavigator#elementsBefore
|
||||||
|
* @param { number } left The left offset.
|
||||||
|
* @param { number } top The top offset.
|
||||||
|
* @return { Array } An array of elements.
|
||||||
|
*/
|
||||||
|
elementsBefore (left, top) {
|
||||||
|
return this.getElements(DOMNavigator.DIRECTION.up).filter(el => el.offsetLeft <= left && el.offsetTop <= top);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the key down event.
|
||||||
|
* @method DOMNavigator#handleKeydown
|
||||||
|
* @param { Event } event The event object.
|
||||||
|
*/
|
||||||
|
handleKeydown (event) {
|
||||||
|
const direction = this.keys[event.which];
|
||||||
|
if (direction) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
let next;
|
||||||
|
if (event.shiftKey && direction === DOMNavigator.DIRECTION.up) {
|
||||||
|
// shift-up goes to the first element
|
||||||
|
next = this.getElements(direction)[0];
|
||||||
|
} else if (event.shiftKey && direction === DOMNavigator.DIRECTION.down) {
|
||||||
|
// shift-down goes to the last element
|
||||||
|
next = Array.from(this.getElements(direction)).pop();
|
||||||
|
} else {
|
||||||
|
next = this.getNextElement(direction, event);
|
||||||
|
}
|
||||||
|
this.select(next, direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DOMNavigator;
|
@ -149,21 +149,6 @@ _converse.IllegalMessage = IllegalMessage;
|
|||||||
// Make converse pluggable
|
// Make converse pluggable
|
||||||
pluggable.enable(_converse, '_converse', 'pluggable');
|
pluggable.enable(_converse, '_converse', 'pluggable');
|
||||||
|
|
||||||
_converse.keycodes = {
|
|
||||||
TAB: 9,
|
|
||||||
ENTER: 13,
|
|
||||||
SHIFT: 16,
|
|
||||||
CTRL: 17,
|
|
||||||
ALT: 18,
|
|
||||||
ESCAPE: 27,
|
|
||||||
UP_ARROW: 38,
|
|
||||||
DOWN_ARROW: 40,
|
|
||||||
FORWARD_SLASH: 47,
|
|
||||||
AT: 50,
|
|
||||||
META: 91,
|
|
||||||
META_RIGHT: 93
|
|
||||||
};
|
|
||||||
|
|
||||||
// Module-level constants
|
// Module-level constants
|
||||||
_converse.STATUS_WEIGHTS = {
|
_converse.STATUS_WEIGHTS = {
|
||||||
'offline': 6,
|
'offline': 6,
|
||||||
@ -1732,6 +1717,23 @@ window.converse = window.converse || {};
|
|||||||
* @namespace converse
|
* @namespace converse
|
||||||
*/
|
*/
|
||||||
Object.assign(window.converse, {
|
Object.assign(window.converse, {
|
||||||
|
keycodes: {
|
||||||
|
TAB: 9,
|
||||||
|
ENTER: 13,
|
||||||
|
SHIFT: 16,
|
||||||
|
CTRL: 17,
|
||||||
|
ALT: 18,
|
||||||
|
ESCAPE: 27,
|
||||||
|
LEFT_ARROW: 37,
|
||||||
|
UP_ARROW: 38,
|
||||||
|
RIGHT_ARROW: 39,
|
||||||
|
DOWN_ARROW: 40,
|
||||||
|
FORWARD_SLASH: 47,
|
||||||
|
AT: 50,
|
||||||
|
META: 91,
|
||||||
|
META_RIGHT: 93
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public API method which initializes Converse.
|
* Public API method which initializes Converse.
|
||||||
* This method must always be called when using Converse.
|
* This method must always be called when using Converse.
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
// Converse.js
|
|
||||||
// https://conversejs.org
|
|
||||||
//
|
|
||||||
// Copyright (c) 2013-2019, the Converse.js developers
|
|
||||||
// Licensed under the Mozilla Public License (MPLv2)
|
|
||||||
//
|
|
||||||
/**
|
/**
|
||||||
* @module converse-muc
|
* @module converse-muc
|
||||||
* @description
|
* @copyright The Converse.js developers
|
||||||
* Implements the non-view logic for XEP-0045 Multi-User Chat
|
* @license Mozilla Public License (MPLv2)
|
||||||
|
* @description Implements the non-view logic for XEP-0045 Multi-User Chat
|
||||||
*/
|
*/
|
||||||
import "./converse-chat";
|
import "./converse-chat";
|
||||||
import "./converse-disco";
|
import "./converse-disco";
|
||||||
|
@ -279,11 +279,23 @@ u.hasClass = function (className, el) {
|
|||||||
return (el instanceof Element) && el.classList.contains(className);
|
return (el instanceof Element) && el.classList.contains(className);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a class to an element.
|
||||||
|
* @method u#addClass
|
||||||
|
* @param {string} className
|
||||||
|
* @param {Element} el
|
||||||
|
*/
|
||||||
u.addClass = function (className, el) {
|
u.addClass = function (className, el) {
|
||||||
(el instanceof Element) && el.classList.add(className);
|
(el instanceof Element) && el.classList.add(className);
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a class from an element.
|
||||||
|
* @method u#removeClass
|
||||||
|
* @param {string} className
|
||||||
|
* @param {Element} el
|
||||||
|
*/
|
||||||
u.removeClass = function (className, el) {
|
u.removeClass = function (className, el) {
|
||||||
(el instanceof Element) && el.classList.remove(className);
|
(el instanceof Element) && el.classList.remove(className);
|
||||||
return el;
|
return el;
|
||||||
|
Loading…
Reference in New Issue
Block a user