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

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

View File

@@ -0,0 +1,28 @@
@import variables
#suggest-menu
position: absolute
display: block
z-index: 1000
min-width: 160px
padding: 5px 0
background: #fff
list-style: none
border: 1px solid #ccc
border-radius: 3px
box-shadow: 0 6px 12px rgba(0, 0, 0, .175)
.suggest-menu-item
white-space: nowrap
padding: 3px 10px
color: color('primary')
font-weight: bold
cursor: pointer
&.active
color: #fff
background: #428bca
small
color: #fff
small
color: color('light')
font-weight: normal

View File

@@ -11,6 +11,7 @@
@import tooltip
@import dropdown
@import accordion
@import suggest_menu
@import dialog_box
@import popover
@import pagination