Add suggest menu for user mentions in text editor

This commit is contained in:
Frederic Guillot
2016-11-27 15:44:45 -05:00
parent 04ff67e26b
commit d8b0423d15
21 changed files with 454 additions and 30 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,206 @@
KB.component('suggest-menu', function(containerElement, options) {
function onKeyDown(e) {
switch (e.keyCode) {
case 27:
destroy();
break;
case 38:
e.preventDefault();
e.stopImmediatePropagation();
moveUp();
break;
case 40:
e.preventDefault();
e.stopImmediatePropagation();
moveDown();
break;
case 13:
e.preventDefault();
e.stopImmediatePropagation();
insertSelectedItem();
break;
}
}
function onClick() {
insertSelectedItem();
}
function onMouseOver(element) {
if (KB.dom(element).hasClass('suggest-menu-item')) {
KB.find('.suggest-menu-item.active').removeClass('active');
KB.dom(element).addClass('active');
}
}
function insertSelectedItem() {
var element = KB.find('.suggest-menu-item.active');
var value = element.data('value');
var trigger = element.data('trigger');
var position = containerElement.value.lastIndexOf(trigger) + 1;
var content = containerElement.value.substring(0, position);
containerElement.value = content + value;
destroy();
}
function getParentElement() {
var selectors = ['.popover-form', '#popover-content', 'body'];
for (var i = 0; i < selectors.length; i++) {
var element = document.querySelector(selectors[i]);
if (element !== null) {
return element;
}
}
return null;
}
function resetSelection() {
var elements = document.querySelectorAll('.suggest-menu-item');
for (var i = 0; i < elements.length; i++) {
if (KB.dom(elements[i]).hasClass('active')) {
KB.dom(elements[i]).removeClass('active');
break;
}
}
return {items: elements, index: i};
}
function moveUp() {
var result = resetSelection();
if (result.index > 0) {
result.index = result.index - 1;
}
KB.dom(result.items[result.index]).addClass('active');
}
function moveDown() {
var result = resetSelection();
if (result.index < result.items.length - 1) {
result.index++;
}
KB.dom(result.items[result.index]).addClass('active');
}
function destroy() {
var element = KB.find('#suggest-menu');
if (element !== null) {
element.remove();
}
document.removeEventListener('keydown', onKeyDown, false);
}
function search(element) {
var text = element.value.substring(element.value.lastIndexOf(' ') + 1, element.selectionEnd);
var trigger = getTrigger(text, options.triggers);
destroy();
if (trigger !== null) {
fetchItems(trigger, text.substring(trigger.length), options.triggers[trigger]);
}
}
function getTrigger(text, triggers) {
for (var trigger in triggers) {
if (triggers.hasOwnProperty(trigger) && text.indexOf(trigger) === 0) {
return trigger;
}
}
return null;
}
function fetchItems(trigger, text, value) {
if (typeof value === 'string') {
KB.http.get(value).success(function (response) {
onItemFetched(trigger, text, response);
});
} else {
onItemFetched(trigger, text, value);
}
}
function onItemFetched(trigger, text, items) {
items = filterItems(text, items);
if (items.length > 0) {
renderMenu(buildItems(trigger, items));
}
}
function filterItems(text, items) {
var filteredItems = [];
if (text.length === 0) {
return items;
}
for (var i = 0; i < items.length; i++) {
if (items[i].value.toLowerCase().indexOf(text.toLowerCase()) === 0) {
filteredItems.push(items[i]);
}
}
return filteredItems;
}
function buildItems(trigger, items) {
var elements = [];
for (var i = 0; i < items.length; i++) {
var className = 'suggest-menu-item';
if (i === 0) {
className += ' active';
}
elements.push({
class: className,
html: items[i].html,
'data-value': items[i].value,
'data-trigger': trigger
});
}
return elements;
}
function renderMenu(items) {
var parentElement = getParentElement();
var caretPosition = getCaretCoordinates(containerElement, containerElement.selectionEnd);
var left = caretPosition.left + containerElement.offsetLeft;
var top = caretPosition.top + containerElement.offsetTop + 16;
document.addEventListener('keydown', onKeyDown, false);
var menu = KB.dom('ul')
.attr('id', 'suggest-menu')
.click(onClick)
.mouseover(onMouseOver)
.style('left', left + 'px')
.style('top', top + 'px')
.for('li', items)
.build();
parentElement.appendChild(menu);
}
this.render = function () {
containerElement.addEventListener('input', function () {
search(this);
});
};
});

View File

@@ -55,6 +55,10 @@ KB.component('text-editor', function (containerElement, options) {
.attr('placeholder', options.placeholder || null)
.build();
if (options.mentionUrl) {
KB.getComponent('suggest-menu', textarea, {triggers: {'@': options.mentionUrl}}).render();
}
return KB.dom('div')
.attr('class', 'text-editor-write-mode')
.add(toolbarElement)

View File

@@ -61,7 +61,13 @@ KB.render = function () {
for (var i = 0; i < elementList.length; i++) {
if (this.components.hasOwnProperty(name)) {
var component = KB.getComponent(name, elementList[i], JSON.parse(elementList[i].dataset.params));
var options;
if (elementList[i].dataset.params) {
options = JSON.parse(elementList[i].dataset.params);
}
var component = KB.getComponent(name, elementList[i], options);
component.render();
elementList[i].className = elementList[i].className + '-rendered';
}

View File

@@ -10,6 +10,14 @@ KB.dom = function (tag) {
return this;
};
this.data = function (attribute, value) {
if (arguments.length === 1) {
return element.dataset[attribute];
}
element.dataset[attribute] = value;
return this;
};
this.hide = function () {
element.style.display = 'none';
return this;
@@ -30,6 +38,11 @@ KB.dom = function (tag) {
return this;
};
this.style = function(attribute, value) {
element.style[attribute] = value;
return this;
};
this.on = function (eventName, callback) {
element.addEventListener(eventName, function (e) {
e.preventDefault();
@@ -43,6 +56,10 @@ KB.dom = function (tag) {
return this.on('click', callback);
};
this.mouseover = function (callback) {
return this.on('mouseover', callback);
};
this.change = function (callback) {
return this.on('change', callback);
};
@@ -96,6 +113,11 @@ KB.dom = function (tag) {
return this;
};
this.remove = function () {
element.parentNode.removeChild(element);
return this;
};
this.parent = function (selector) {
for (; element && element !== document; element = element.parentNode) {
if (element.matches(selector)) {

View File

@@ -64,8 +64,10 @@ Kanboard.App.prototype.keyboardShortcuts = function() {
// Close popover and dropdown
Mousetrap.bindGlobal("esc", function() {
self.get("Popover").close();
self.get("Dropdown").close();
if (! document.getElementById('suggest-menu')) {
self.get("Popover").close();
self.get("Dropdown").close();
}
});
// Show keyboard shortcut