summaryrefslogtreecommitdiff
path: root/assets/js/components
diff options
context:
space:
mode:
authorFrederic Guillot <fred@kanboard.net>2016-11-27 15:44:45 -0500
committerFrederic Guillot <fred@kanboard.net>2016-11-27 15:44:45 -0500
commitd8b0423d152ca27682b001f2c4d386d9c5dd361e (patch)
tree8b4919d5296b857bcf74e81c8cc06729ddfce5e5 /assets/js/components
parent04ff67e26b880dde8bfb6462f312cf434457cd46 (diff)
Add suggest menu for user mentions in text editor
Diffstat (limited to 'assets/js/components')
-rw-r--r--assets/js/components/suggest-menu.js206
-rw-r--r--assets/js/components/text-editor.js4
2 files changed, 210 insertions, 0 deletions
diff --git a/assets/js/components/suggest-menu.js b/assets/js/components/suggest-menu.js
new file mode 100644
index 00000000..f0e6dfd1
--- /dev/null
+++ b/assets/js/components/suggest-menu.js
@@ -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);
+ });
+ };
+});
diff --git a/assets/js/components/text-editor.js b/assets/js/components/text-editor.js
index 2bf8109e..9b4be55c 100644
--- a/assets/js/components/text-editor.js
+++ b/assets/js/components/text-editor.js
@@ -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)