Add suggest menu for user mentions in text editor
This commit is contained in:
2
assets/css/app.min.css
vendored
2
assets/css/app.min.css
vendored
File diff suppressed because one or more lines are too long
4
assets/js/app.min.js
vendored
4
assets/js/app.min.js
vendored
File diff suppressed because one or more lines are too long
206
assets/js/components/suggest-menu.js
Normal file
206
assets/js/components/suggest-menu.js
Normal 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);
|
||||
});
|
||||
};
|
||||
});
|
||||
@@ -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)
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
28
assets/sass/_suggest_menu.sass
Normal file
28
assets/sass/_suggest_menu.sass
Normal 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
|
||||
@@ -11,6 +11,7 @@
|
||||
@import tooltip
|
||||
@import dropdown
|
||||
@import accordion
|
||||
@import suggest_menu
|
||||
@import dialog_box
|
||||
@import popover
|
||||
@import pagination
|
||||
|
||||
Reference in New Issue
Block a user