Add suggest menu for user mentions in text editor
This commit is contained in:
@@ -4,6 +4,7 @@ namespace Kanboard\Controller;
|
|||||||
|
|
||||||
use Kanboard\Filter\UserNameFilter;
|
use Kanboard\Filter\UserNameFilter;
|
||||||
use Kanboard\Formatter\UserAutoCompleteFormatter;
|
use Kanboard\Formatter\UserAutoCompleteFormatter;
|
||||||
|
use Kanboard\Formatter\UserMentionFormatter;
|
||||||
use Kanboard\Model\UserModel;
|
use Kanboard\Model\UserModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,7 +38,12 @@ class UserAjaxController extends BaseController
|
|||||||
$project_id = $this->request->getStringParam('project_id');
|
$project_id = $this->request->getStringParam('project_id');
|
||||||
$query = $this->request->getStringParam('q');
|
$query = $this->request->getStringParam('q');
|
||||||
$users = $this->projectPermissionModel->findUsernames($project_id, $query);
|
$users = $this->projectPermissionModel->findUsernames($project_id, $query);
|
||||||
$this->response->json($users);
|
|
||||||
|
$this->response->json(
|
||||||
|
UserMentionFormatter::getInstance($this->container)
|
||||||
|
->withUsers($users)
|
||||||
|
->format()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use Pimple\Container;
|
|||||||
*
|
*
|
||||||
* @property \Kanboard\Helper\AppHelper $app
|
* @property \Kanboard\Helper\AppHelper $app
|
||||||
* @property \Kanboard\Helper\AssetHelper $asset
|
* @property \Kanboard\Helper\AssetHelper $asset
|
||||||
|
* @property \Kanboard\Helper\AvatarHelper $avatar
|
||||||
* @property \Kanboard\Helper\BoardHelper $board
|
* @property \Kanboard\Helper\BoardHelper $board
|
||||||
* @property \Kanboard\Helper\CalendarHelper $calendar
|
* @property \Kanboard\Helper\CalendarHelper $calendar
|
||||||
* @property \Kanboard\Helper\DateHelper $dt
|
* @property \Kanboard\Helper\DateHelper $dt
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ namespace Kanboard\Formatter;
|
|||||||
|
|
||||||
use Kanboard\Core\Base;
|
use Kanboard\Core\Base;
|
||||||
use PicoDb\Table;
|
use PicoDb\Table;
|
||||||
use Pimple\Container;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class BaseFormatter
|
* Class BaseFormatter
|
||||||
@@ -22,19 +21,6 @@ abstract class BaseFormatter extends Base
|
|||||||
*/
|
*/
|
||||||
protected $query;
|
protected $query;
|
||||||
|
|
||||||
/**
|
|
||||||
* Get object instance
|
|
||||||
*
|
|
||||||
* @static
|
|
||||||
* @access public
|
|
||||||
* @param Container $container
|
|
||||||
* @return static
|
|
||||||
*/
|
|
||||||
public static function getInstance(Container $container)
|
|
||||||
{
|
|
||||||
return new static($container);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set query
|
* Set query
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
namespace Kanboard\Formatter;
|
namespace Kanboard\Formatter;
|
||||||
|
|
||||||
use Kanboard\Core\Filter\FormatterInterface;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common class to handle calendar events
|
* Common class to handle calendar events
|
||||||
*
|
*
|
||||||
@@ -34,7 +32,7 @@ abstract class BaseTaskCalendarFormatter extends BaseFormatter
|
|||||||
* @access public
|
* @access public
|
||||||
* @param string $start_column Column name for the start date
|
* @param string $start_column Column name for the start date
|
||||||
* @param string $end_column Column name for the end date
|
* @param string $end_column Column name for the end date
|
||||||
* @return FormatterInterface
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function setColumns($start_column, $end_column = '')
|
public function setColumns($start_column, $end_column = '')
|
||||||
{
|
{
|
||||||
|
|||||||
60
app/Formatter/UserMentionFormatter.php
Normal file
60
app/Formatter/UserMentionFormatter.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kanboard\Formatter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class UserMentionFormatter
|
||||||
|
*
|
||||||
|
* @package Kanboard\Formatter
|
||||||
|
* @author Frederic Guillot
|
||||||
|
*/
|
||||||
|
class UserMentionFormatter extends BaseFormatter
|
||||||
|
{
|
||||||
|
protected $users = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set users
|
||||||
|
*
|
||||||
|
* @param array $users
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function withUsers(array $users) {
|
||||||
|
$this->users = $users;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply formatter
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function format()
|
||||||
|
{
|
||||||
|
$result = array();
|
||||||
|
|
||||||
|
foreach ($this->users as $user) {
|
||||||
|
$html = $this->helper->avatar->small(
|
||||||
|
$user['id'],
|
||||||
|
$user['username'],
|
||||||
|
$user['name'],
|
||||||
|
$user['email'],
|
||||||
|
$user['avatar_path'],
|
||||||
|
'avatar-inline'
|
||||||
|
);
|
||||||
|
|
||||||
|
$html .= ' '.$this->helper->text->e($user['username']);
|
||||||
|
|
||||||
|
if (! empty($user['name'])) {
|
||||||
|
$html .= ' <small>'.$this->helper->text->e($user['name']).'</small>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[] = array(
|
||||||
|
'value' => $user['username'],
|
||||||
|
'html' => $html,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -204,6 +204,10 @@ class FormHelper extends Base
|
|||||||
'placeholder' => t('Write your text in Markdown'),
|
'placeholder' => t('Write your text in Markdown'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isset($values['project_id'])) {
|
||||||
|
$params['mentionUrl'] = $this->helper->url->to('UserAjaxController', 'mention', array('project_id' => $values['project_id']));
|
||||||
|
}
|
||||||
|
|
||||||
$html = '<div class="js-text-editor" data-params=\''.json_encode($params, JSON_HEX_APOS).'\'></div>';
|
$html = '<div class="js-text-editor" data-params=\''.json_encode($params, JSON_HEX_APOS).'\'></div>';
|
||||||
$html .= $this->errorList($errors, $name);
|
$html .= $this->errorList($errors, $name);
|
||||||
|
|
||||||
|
|||||||
@@ -62,17 +62,33 @@ class ProjectPermissionModel extends Base
|
|||||||
->withFilter(new ProjectUserRoleProjectFilter($project_id))
|
->withFilter(new ProjectUserRoleProjectFilter($project_id))
|
||||||
->withFilter(new ProjectUserRoleUsernameFilter($input))
|
->withFilter(new ProjectUserRoleUsernameFilter($input))
|
||||||
->getQuery()
|
->getQuery()
|
||||||
->findAllByColumn('username');
|
->columns(
|
||||||
|
UserModel::TABLE.'.id',
|
||||||
|
UserModel::TABLE.'.username',
|
||||||
|
UserModel::TABLE.'.name',
|
||||||
|
UserModel::TABLE.'.email',
|
||||||
|
UserModel::TABLE.'.avatar_path'
|
||||||
|
)
|
||||||
|
->findAll();
|
||||||
|
|
||||||
$groupMembers = $this->projectGroupRoleQuery
|
$groupMembers = $this->projectGroupRoleQuery
|
||||||
->withFilter(new ProjectGroupRoleProjectFilter($project_id))
|
->withFilter(new ProjectGroupRoleProjectFilter($project_id))
|
||||||
->withFilter(new ProjectGroupRoleUsernameFilter($input))
|
->withFilter(new ProjectGroupRoleUsernameFilter($input))
|
||||||
->getQuery()
|
->getQuery()
|
||||||
->findAllByColumn('username');
|
->columns(
|
||||||
|
UserModel::TABLE.'.id',
|
||||||
|
UserModel::TABLE.'.username',
|
||||||
|
UserModel::TABLE.'.name',
|
||||||
|
UserModel::TABLE.'.email',
|
||||||
|
UserModel::TABLE.'.avatar_path'
|
||||||
|
)
|
||||||
|
->findAll();
|
||||||
|
|
||||||
$members = array_unique(array_merge($userMembers, $groupMembers));
|
$userMembers = array_column_index_unique($userMembers, 'username');
|
||||||
|
$groupMembers = array_column_index_unique($groupMembers, 'username');
|
||||||
|
$members = array_merge($userMembers, $groupMembers);
|
||||||
|
|
||||||
sort($members);
|
ksort($members);
|
||||||
|
|
||||||
return $members;
|
return $members;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,37 @@ function array_column_index(array &$input, $column)
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create indexed array from a list of dict with unique values
|
||||||
|
*
|
||||||
|
* $input = [
|
||||||
|
* ['k1' => 1, 'k2' => 2], ['k1' => 3, 'k2' => 4], ['k1' => 1, 'k2' => 5]
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* array_column_index_unique($input, 'k1') will returns:
|
||||||
|
*
|
||||||
|
* [
|
||||||
|
* 1 => ['k1' => 1, 'k2' => 2],
|
||||||
|
* 3 => ['k1' => 3, 'k2' => 4],
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* @param array $input
|
||||||
|
* @param string $column
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
function array_column_index_unique(array &$input, $column)
|
||||||
|
{
|
||||||
|
$result = array();
|
||||||
|
|
||||||
|
foreach ($input as &$row) {
|
||||||
|
if (isset($row[$column]) && ! isset($result[$row[$column]])) {
|
||||||
|
$result[$row[$column]] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sum all values from a single column in the input array
|
* Sum all values from a single column in the input array
|
||||||
*
|
*
|
||||||
|
|||||||
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)
|
.attr('placeholder', options.placeholder || null)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
if (options.mentionUrl) {
|
||||||
|
KB.getComponent('suggest-menu', textarea, {triggers: {'@': options.mentionUrl}}).render();
|
||||||
|
}
|
||||||
|
|
||||||
return KB.dom('div')
|
return KB.dom('div')
|
||||||
.attr('class', 'text-editor-write-mode')
|
.attr('class', 'text-editor-write-mode')
|
||||||
.add(toolbarElement)
|
.add(toolbarElement)
|
||||||
|
|||||||
@@ -61,7 +61,13 @@ KB.render = function () {
|
|||||||
|
|
||||||
for (var i = 0; i < elementList.length; i++) {
|
for (var i = 0; i < elementList.length; i++) {
|
||||||
if (this.components.hasOwnProperty(name)) {
|
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();
|
component.render();
|
||||||
elementList[i].className = elementList[i].className + '-rendered';
|
elementList[i].className = elementList[i].className + '-rendered';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ KB.dom = function (tag) {
|
|||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.data = function (attribute, value) {
|
||||||
|
if (arguments.length === 1) {
|
||||||
|
return element.dataset[attribute];
|
||||||
|
}
|
||||||
|
element.dataset[attribute] = value;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
this.hide = function () {
|
this.hide = function () {
|
||||||
element.style.display = 'none';
|
element.style.display = 'none';
|
||||||
return this;
|
return this;
|
||||||
@@ -30,6 +38,11 @@ KB.dom = function (tag) {
|
|||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.style = function(attribute, value) {
|
||||||
|
element.style[attribute] = value;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
this.on = function (eventName, callback) {
|
this.on = function (eventName, callback) {
|
||||||
element.addEventListener(eventName, function (e) {
|
element.addEventListener(eventName, function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -43,6 +56,10 @@ KB.dom = function (tag) {
|
|||||||
return this.on('click', callback);
|
return this.on('click', callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.mouseover = function (callback) {
|
||||||
|
return this.on('mouseover', callback);
|
||||||
|
};
|
||||||
|
|
||||||
this.change = function (callback) {
|
this.change = function (callback) {
|
||||||
return this.on('change', callback);
|
return this.on('change', callback);
|
||||||
};
|
};
|
||||||
@@ -96,6 +113,11 @@ KB.dom = function (tag) {
|
|||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.remove = function () {
|
||||||
|
element.parentNode.removeChild(element);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
this.parent = function (selector) {
|
this.parent = function (selector) {
|
||||||
for (; element && element !== document; element = element.parentNode) {
|
for (; element && element !== document; element = element.parentNode) {
|
||||||
if (element.matches(selector)) {
|
if (element.matches(selector)) {
|
||||||
|
|||||||
@@ -64,8 +64,10 @@ Kanboard.App.prototype.keyboardShortcuts = function() {
|
|||||||
|
|
||||||
// Close popover and dropdown
|
// Close popover and dropdown
|
||||||
Mousetrap.bindGlobal("esc", function() {
|
Mousetrap.bindGlobal("esc", function() {
|
||||||
self.get("Popover").close();
|
if (! document.getElementById('suggest-menu')) {
|
||||||
self.get("Dropdown").close();
|
self.get("Popover").close();
|
||||||
|
self.get("Dropdown").close();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show keyboard shortcut
|
// 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 tooltip
|
||||||
@import dropdown
|
@import dropdown
|
||||||
@import accordion
|
@import accordion
|
||||||
|
@import suggest_menu
|
||||||
@import dialog_box
|
@import dialog_box
|
||||||
@import popover
|
@import popover
|
||||||
@import pagination
|
@import pagination
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ var strip = require('gulp-strip-comments');
|
|||||||
|
|
||||||
var src = {
|
var src = {
|
||||||
js: [
|
js: [
|
||||||
|
'node_modules/textarea-caret/index.js',
|
||||||
'assets/js/polyfills/*.js',
|
'assets/js/polyfills/*.js',
|
||||||
'assets/js/core/base.js',
|
'assets/js/core/base.js',
|
||||||
'assets/js/core/!(base|bootstrap)*.js',
|
'assets/js/core/!(base|bootstrap)*.js',
|
||||||
|
|||||||
@@ -10,5 +10,8 @@
|
|||||||
"gulp-strip-comments": "^2.4.3",
|
"gulp-strip-comments": "^2.4.3",
|
||||||
"gulp-uglify": "^1.5.3",
|
"gulp-uglify": "^1.5.3",
|
||||||
"jshint": "^2.9.4"
|
"jshint": "^2.9.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"textarea-caret": "^3.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,38 @@ class FunctionTest extends Base
|
|||||||
$this->assertSame($expected, array_column_index($input, 'k1'));
|
$this->assertSame($expected, array_column_index($input, 'k1'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testArrayColumnIndexUnique()
|
||||||
|
{
|
||||||
|
$input = array(
|
||||||
|
array(
|
||||||
|
'k1' => 11,
|
||||||
|
'k2' => 22,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'k1' => 11,
|
||||||
|
'k2' => 55,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'k1' => 33,
|
||||||
|
'k2' => 44,
|
||||||
|
),
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
|
||||||
|
$expected = array(
|
||||||
|
11 => array(
|
||||||
|
'k1' => 11,
|
||||||
|
'k2' => 22,
|
||||||
|
),
|
||||||
|
33 => array(
|
||||||
|
'k1' => 33,
|
||||||
|
'k2' => 44,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertSame($expected, array_column_index_unique($input, 'k1'));
|
||||||
|
}
|
||||||
|
|
||||||
public function testArrayMergeRelation()
|
public function testArrayMergeRelation()
|
||||||
{
|
{
|
||||||
$relations = array(
|
$relations = array(
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class ProjectPermissionModelTest extends Base
|
|||||||
$this->assertEquals(1, $projectModel->create(array('name' => 'Project 1')));
|
$this->assertEquals(1, $projectModel->create(array('name' => 'Project 1')));
|
||||||
|
|
||||||
$this->assertEquals(2, $userModel->create(array('username' => 'user1')));
|
$this->assertEquals(2, $userModel->create(array('username' => 'user1')));
|
||||||
$this->assertEquals(3, $userModel->create(array('username' => 'user2')));
|
$this->assertEquals(3, $userModel->create(array('username' => 'user2', 'name' => 'User 2', 'email' => 'test@here', 'avatar_path' => 'test')));
|
||||||
$this->assertEquals(4, $userModel->create(array('username' => 'user3')));
|
$this->assertEquals(4, $userModel->create(array('username' => 'user3')));
|
||||||
|
|
||||||
$this->assertEquals(1, $groupModel->create('Group A'));
|
$this->assertEquals(1, $groupModel->create('Group A'));
|
||||||
@@ -35,7 +35,24 @@ class ProjectPermissionModelTest extends Base
|
|||||||
$this->assertTrue($groupRoleModel->addGroup(1, 1, Role::PROJECT_MEMBER));
|
$this->assertTrue($groupRoleModel->addGroup(1, 1, Role::PROJECT_MEMBER));
|
||||||
$this->assertTrue($userRoleModel->addUser(1, 3, Role::PROJECT_MANAGER));
|
$this->assertTrue($userRoleModel->addUser(1, 3, Role::PROJECT_MANAGER));
|
||||||
|
|
||||||
$this->assertEquals(array('user1', 'user2'), $projectPermissionModel->findUsernames(1, 'us'));
|
$expected = array(
|
||||||
|
'user1' => array(
|
||||||
|
'username' => 'user1',
|
||||||
|
'name' => null,
|
||||||
|
'email' => null,
|
||||||
|
'avatar_path' => null,
|
||||||
|
'id' => '2',
|
||||||
|
),
|
||||||
|
'user2' => array(
|
||||||
|
'username' => 'user2',
|
||||||
|
'name' => 'User 2',
|
||||||
|
'email' => 'test@here',
|
||||||
|
'avatar_path' => 'test',
|
||||||
|
'id' => '3',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals($expected, $projectPermissionModel->findUsernames(1, 'us'));
|
||||||
$this->assertEmpty($projectPermissionModel->findUsernames(1, 'a'));
|
$this->assertEmpty($projectPermissionModel->findUsernames(1, 'a'));
|
||||||
$this->assertEmpty($projectPermissionModel->findUsernames(2, 'user'));
|
$this->assertEmpty($projectPermissionModel->findUsernames(2, 'user'));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user