diff options
author | Frederic Guillot <fred@kanboard.net> | 2016-11-27 15:44:45 -0500 |
---|---|---|
committer | Frederic Guillot <fred@kanboard.net> | 2016-11-27 15:44:45 -0500 |
commit | d8b0423d152ca27682b001f2c4d386d9c5dd361e (patch) | |
tree | 8b4919d5296b857bcf74e81c8cc06729ddfce5e5 /assets/js/components | |
parent | 04ff67e26b880dde8bfb6462f312cf434457cd46 (diff) |
Add suggest menu for user mentions in text editor
Diffstat (limited to 'assets/js/components')
-rw-r--r-- | assets/js/components/suggest-menu.js | 206 | ||||
-rw-r--r-- | assets/js/components/text-editor.js | 4 |
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) |