Commit 2214c0cd by Torkel Ödegaard

More work on adding tags to dashboard, added bootstrap tagsinput

parent 11c5fdc3
......@@ -12,7 +12,7 @@ define([
'angular-strap',
'angular-dragdrop',
'extend-jquery',
'bindonce',
'bindonce'
],
function (angular, $, _, appLevelRequire) {
......
......@@ -45,6 +45,8 @@ require.config({
modernizr: '../vendor/modernizr-2.6.1',
elasticjs: '../vendor/elasticjs/elastic-angular-client',
'bootstrap-tagsinput': '../vendor/tagsinput/bootstrap-tagsinput',
},
shim: {
underscore: {
......@@ -104,6 +106,7 @@ require.config({
elasticjs: ['angular', '../vendor/elasticjs/elastic'],
'bootstrap-tagsinput': ['jquery'],
},
waitSeconds: 60,
});
......@@ -10,5 +10,6 @@ define([
'./confirmClick',
'./configModal',
'./spectrumPicker',
'./grafanaGraph'
'./grafanaGraph',
'./bootstrap-tagsinput'
], function () {});
\ No newline at end of file
define([
'angular',
'jquery',
'bootstrap-tagsinput'
],
function (angular, $) {
'use strict';
angular
.module('kibana.directives')
.directive('bootstrapTagsinput', function() {
function getItemProperty(scope, property) {
if (!property) {
return undefined;
}
if (angular.isFunction(scope.$parent[property])) {
return scope.$parent[property];
}
return function(item) {
return item[property];
};
}
return {
restrict: 'EA',
scope: {
model: '=ngModel'
},
template: '<select multiple></select>',
replace: false,
link: function(scope, element, attrs) {
if (!angular.isArray(scope.model)) {
scope.model = [];
}
var select = $('select', element);
if (attrs.placeholder) {
select.attr('placeholder', attrs.placeholder);
}
select.tagsinput({
typeahead : {
source : angular.isFunction(scope.$parent[attrs.typeaheadSource]) ? scope.$parent[attrs.typeaheadSource] : null
},
itemValue: getItemProperty(scope, attrs.itemvalue),
itemText : getItemProperty(scope, attrs.itemtext),
tagClass : angular.isFunction(scope.$parent[attrs.tagclass]) ?
scope.$parent[attrs.tagclass] : function() { return attrs.tagclass; }
});
for (var i = 0; i < scope.model.length; i++) {
select.tagsinput('add', scope.model[i]);
}
select.on('itemAdded', function(event) {
if (scope.model.indexOf(event.item) === -1) {
scope.model.push(event.item);
}
});
select.on('itemRemoved', function(event) {
var idx = scope.model.indexOf(event.item);
if (idx !== -1) {
scope.model.splice(idx, 1);
}
});
// create a shallow copy of model's current state, needed to determine
// diff when model changes
var prev = scope.model.slice();
scope.$watch("model", function() {
var added = scope.model.filter(function(i) {return prev.indexOf(i) === -1;}),
removed = prev.filter(function(i) {return scope.model.indexOf(i) === -1;}),
i;
prev = scope.model.slice();
// Remove tags no longer in binded model
for (i = 0; i < removed.length; i++) {
select.tagsinput('remove', removed[i]);
}
// Refresh remaining tags
select.tagsinput('refresh');
// Add new items in model as tags
for (i = 0; i < added.length; i++) {
select.tagsinput('add', added[i]);
}
}, true);
}
};
});
});
\ No newline at end of file
......@@ -33,7 +33,10 @@
<div class="editor-row">
<div class="section">
<div class="editor-option">
<label class="small">Tags</label><input type="text" class="input-large" ng-model='dashboard.current.tags'></input>
<label class="small">Tags</label>
<bootstrap-tagsinput ng-model="dashboard.current.tags" tagclass="label label-tag" placeholder="add tags">
</bootstrap-tagsinput>
<tip>Press enter to a add tag</tip>
</div>
</div>
</div>
......
......@@ -61,7 +61,7 @@
<a href="#/dashboard/elasticsearch/{{row._id}}" bo-text="row._id"></a>
</td>
<td style="white-space: nowrap">
<span ng-repeat="tag in row._source.tags" style="margin-right: 5px;" class="label label-info">{{tag}}</span>
<span ng-repeat="tag in row._source.tags" style="margin-right: 5px;" class="label label-tag">{{tag}}</span>
</td>
<td><a><i class="icon-share" ng-click="share = dashboard.share_link(row._id,'elasticsearch',row._id)" bs-modal="'app/partials/dashLoaderShare.html'"></i></a></td>
</tr>
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
@import "submenu.less";
@import "..\..\vendor\tagsinput\bootstrap-tagsinput.less";
.navbar-static-top {
border-bottom: 1px solid black;
......
......@@ -554,25 +554,8 @@ div.flot-text {
// Labels & Badges
// Colors
// Only give background-color difference to links (and to simplify, we don't qualifty with `a` but [href] attribute)
.label,
.badge {
color: @linkColor;
// Important (red)
&-important { background-color: @errorText; }
&-important[href] { background-color: darken(@errorText, 10%); }
// Warnings (orange)
&-warning { background-color: @orange; }
&-warning[href] { background-color: darken(@orange, 10%); }
// Success (green)
&-success { background-color: @successText; }
&-success[href] { background-color: darken(@successText, 10%); }
// Info (turquoise)
&-info { background-color: @purple; }
&-info[href] { background-color: darken(@purple, 10%); }
// Inverse (black)
&-inverse { background-color: @grayDark; }
&-inverse[href] { background-color: darken(@grayDark, 10%); }
.label-tag, .label-tag:hover {
background-color: @purple;
color: @linkColor;
}
\ No newline at end of file
......@@ -159,7 +159,7 @@
// Input placeholder text color
// -------------------------
@placeholderText: @grayLight;
@placeholderText: darken(@textColor, 25%);
// Hr border color
......
(function ($) {
"use strict";
var defaultOptions = {
tagClass: function(item) {
return 'label label-info';
},
itemValue: function(item) {
return item ? item.toString() : item;
},
itemText: function(item) {
return this.itemValue(item);
},
freeInput: true,
maxTags: undefined,
confirmKeys: [13],
onTagExists: function(item, $tag) {
$tag.hide().fadeIn();
}
};
/**
* Constructor function
*/
function TagsInput(element, options) {
this.itemsArray = [];
this.$element = $(element);
this.$element.hide();
this.isSelect = (element.tagName === 'SELECT');
this.multiple = (this.isSelect && element.hasAttribute('multiple'));
this.objectItems = options && options.itemValue;
this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : '';
this.inputSize = Math.max(1, this.placeholderText.length);
this.$container = $('<div class="bootstrap-tagsinput"></div>');
this.$input = $('<input size="' + this.inputSize + '" type="text" placeholder="' + this.placeholderText + '"/>').appendTo(this.$container);
this.$element.after(this.$container);
this.build(options);
}
TagsInput.prototype = {
constructor: TagsInput,
/**
* Adds the given item as a new tag. Pass true to dontPushVal to prevent
* updating the elements val()
*/
add: function(item, dontPushVal) {
var self = this;
if (self.options.maxTags && self.itemsArray.length >= self.options.maxTags)
return;
// Ignore falsey values, except false
if (item !== false && !item)
return;
// Throw an error when trying to add an object while the itemValue option was not set
if (typeof item === "object" && !self.objectItems)
throw("Can't add objects when itemValue option is not set");
// Ignore strings only containg whitespace
if (item.toString().match(/^\s*$/))
return;
// If SELECT but not multiple, remove current tag
if (self.isSelect && !self.multiple && self.itemsArray.length > 0)
self.remove(self.itemsArray[0]);
if (typeof item === "string" && this.$element[0].tagName === 'INPUT') {
var items = item.split(',');
if (items.length > 1) {
for (var i = 0; i < items.length; i++) {
this.add(items[i], true);
}
if (!dontPushVal)
self.pushVal();
return;
}
}
var itemValue = self.options.itemValue(item),
itemText = self.options.itemText(item),
tagClass = self.options.tagClass(item);
// Ignore items allready added
var existing = $.grep(self.itemsArray, function(item) { return self.options.itemValue(item) === itemValue; } )[0];
if (existing) {
// Invoke onTagExists
if (self.options.onTagExists) {
var $existingTag = $(".tag", self.$container).filter(function() { return $(this).data("item") === existing; });
self.options.onTagExists(item, $existingTag);
}
return;
}
// register item in internal array and map
self.itemsArray.push(item);
// add a tag element
var $tag = $('<span class="tag ' + htmlEncode(tagClass) + '">' + htmlEncode(itemText) + '<span data-role="remove"></span></span>');
$tag.data('item', item);
self.findInputWrapper().before($tag);
$tag.after(' ');
// add <option /> if item represents a value not present in one of the <select />'s options
if (self.isSelect && !$('option[value="' + escape(itemValue) + '"]',self.$element)[0]) {
var $option = $('<option selected>' + htmlEncode(itemText) + '</option>');
$option.data('item', item);
$option.attr('value', itemValue);
self.$element.append($option);
}
if (!dontPushVal)
self.pushVal();
// Add class when reached maxTags
if (self.options.maxTags === self.itemsArray.length)
self.$container.addClass('bootstrap-tagsinput-max');
self.$element.trigger($.Event('itemAdded', { item: item }));
},
/**
* Removes the given item. Pass true to dontPushVal to prevent updating the
* elements val()
*/
remove: function(item, dontPushVal) {
var self = this;
if (self.objectItems) {
if (typeof item === "object")
item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == self.options.itemValue(item); } )[0];
else
item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == item; } )[0];
}
if (item) {
$('.tag', self.$container).filter(function() { return $(this).data('item') === item; }).remove();
$('option', self.$element).filter(function() { return $(this).data('item') === item; }).remove();
self.itemsArray.splice($.inArray(item, self.itemsArray), 1);
}
if (!dontPushVal)
self.pushVal();
// Remove class when reached maxTags
if (self.options.maxTags > self.itemsArray.length)
self.$container.removeClass('bootstrap-tagsinput-max');
self.$element.trigger($.Event('itemRemoved', { item: item }));
},
/**
* Removes all items
*/
removeAll: function() {
var self = this;
$('.tag', self.$container).remove();
$('option', self.$element).remove();
while(self.itemsArray.length > 0)
self.itemsArray.pop();
self.pushVal();
if (self.options.maxTags && !this.isEnabled())
this.enable();
},
/**
* Refreshes the tags so they match the text/value of their corresponding
* item.
*/
refresh: function() {
var self = this;
$('.tag', self.$container).each(function() {
var $tag = $(this),
item = $tag.data('item'),
itemValue = self.options.itemValue(item),
itemText = self.options.itemText(item),
tagClass = self.options.tagClass(item);
// Update tag's class and inner text
$tag.attr('class', null);
$tag.addClass('tag ' + htmlEncode(tagClass));
$tag.contents().filter(function() {
return this.nodeType == 3;
})[0].nodeValue = htmlEncode(itemText);
if (self.isSelect) {
var option = $('option', self.$element).filter(function() { return $(this).data('item') === item; });
option.attr('value', itemValue);
}
});
},
/**
* Returns the items added as tags
*/
items: function() {
return this.itemsArray;
},
/**
* Assembly value by retrieving the value of each item, and set it on the
* element.
*/
pushVal: function() {
var self = this,
val = $.map(self.items(), function(item) {
return self.options.itemValue(item).toString();
});
self.$element.val(val, true).trigger('change');
},
/**
* Initializes the tags input behaviour on the element
*/
build: function(options) {
var self = this;
self.options = $.extend({}, defaultOptions, options);
var typeahead = self.options.typeahead || {};
// When itemValue is set, freeInput should always be false
if (self.objectItems)
self.options.freeInput = false;
makeOptionItemFunction(self.options, 'itemValue');
makeOptionItemFunction(self.options, 'itemText');
makeOptionItemFunction(self.options, 'tagClass');
// for backwards compatibility, self.options.source is deprecated
if (self.options.source)
typeahead.source = self.options.source;
if (typeahead.source && $.fn.typeahead) {
makeOptionFunction(typeahead, 'source');
self.$input.typeahead({
source: function (query, process) {
function processItems(items) {
var texts = [];
for (var i = 0; i < items.length; i++) {
var text = self.options.itemText(items[i]);
map[text] = items[i];
texts.push(text);
}
process(texts);
}
this.map = {};
var map = this.map,
data = typeahead.source(query);
if ($.isFunction(data.success)) {
// support for Angular promises
data.success(processItems);
} else {
// support for functions and jquery promises
$.when(data)
.then(processItems);
}
},
updater: function (text) {
self.add(this.map[text]);
},
matcher: function (text) {
return (text.toLowerCase().indexOf(this.query.trim().toLowerCase()) !== -1);
},
sorter: function (texts) {
return texts.sort();
},
highlighter: function (text) {
var regex = new RegExp( '(' + this.query + ')', 'gi' );
return text.replace( regex, "<strong>$1</strong>" );
}
});
}
self.$container.on('click', $.proxy(function(event) {
self.$input.focus();
}, self));
self.$container.on('keydown', 'input', $.proxy(function(event) {
var $input = $(event.target),
$inputWrapper = self.findInputWrapper();
switch (event.which) {
// BACKSPACE
case 8:
if (doGetCaretPosition($input[0]) === 0) {
var prev = $inputWrapper.prev();
if (prev) {
self.remove(prev.data('item'));
}
}
break;
// DELETE
case 46:
if (doGetCaretPosition($input[0]) === 0) {
var next = $inputWrapper.next();
if (next) {
self.remove(next.data('item'));
}
}
break;
// LEFT ARROW
case 37:
// Try to move the input before the previous tag
var $prevTag = $inputWrapper.prev();
if ($input.val().length === 0 && $prevTag[0]) {
$prevTag.before($inputWrapper);
$input.focus();
}
break;
// RIGHT ARROW
case 39:
// Try to move the input after the next tag
var $nextTag = $inputWrapper.next();
if ($input.val().length === 0 && $nextTag[0]) {
$nextTag.after($inputWrapper);
$input.focus();
}
break;
default:
// When key corresponds one of the confirmKeys, add current input
// as a new tag
if (self.options.freeInput && $.inArray(event.which, self.options.confirmKeys) >= 0) {
self.add($input.val());
$input.val('');
event.preventDefault();
}
}
// Reset internal input's size
$input.attr('size', Math.max(this.inputSize, $input.val().length));
}, self));
// Remove icon clicked
self.$container.on('click', '[data-role=remove]', $.proxy(function(event) {
self.remove($(event.target).closest('.tag').data('item'));
}, self));
// Only add existing value as tags when using strings as tags
if (self.options.itemValue === defaultOptions.itemValue) {
if (self.$element[0].tagName === 'INPUT') {
self.add(self.$element.val());
} else {
$('option', self.$element).each(function() {
self.add($(this).attr('value'), true);
});
}
}
},
/**
* Removes all tagsinput behaviour and unregsiter all event handlers
*/
destroy: function() {
var self = this;
// Unbind events
self.$container.off('keypress', 'input');
self.$container.off('click', '[role=remove]');
self.$container.remove();
self.$element.removeData('tagsinput');
self.$element.show();
},
/**
* Sets focus on the tagsinput
*/
focus: function() {
this.$input.focus();
},
/**
* Returns the internal input element
*/
input: function() {
return this.$input;
},
/**
* Returns the element which is wrapped around the internal input. This
* is normally the $container, but typeahead.js moves the $input element.
*/
findInputWrapper: function() {
var elt = this.$input[0],
container = this.$container[0];
while(elt && elt.parentNode !== container)
elt = elt.parentNode;
return $(elt);
}
};
/**
* Register JQuery plugin
*/
$.fn.tagsinput = function(arg1, arg2) {
var results = [];
this.each(function() {
var tagsinput = $(this).data('tagsinput');
// Initialize a new tags input
if (!tagsinput) {
tagsinput = new TagsInput(this, arg1);
$(this).data('tagsinput', tagsinput);
results.push(tagsinput);
if (this.tagName === 'SELECT') {
$('option', $(this)).attr('selected', 'selected');
}
// Init tags from $(this).val()
$(this).val($(this).val());
} else {
// Invoke function on existing tags input
var retVal = tagsinput[arg1](arg2);
if (retVal !== undefined)
results.push(retVal);
}
});
if ( typeof arg1 == 'string') {
// Return the results from the invoked function calls
return results.length > 1 ? results : results[0];
} else {
return results;
}
};
$.fn.tagsinput.Constructor = TagsInput;
/**
* Most options support both a string or number as well as a function as
* option value. This function makes sure that the option with the given
* key in the given options is wrapped in a function
*/
function makeOptionItemFunction(options, key) {
if (typeof options[key] !== 'function') {
var propertyName = options[key];
options[key] = function(item) { return item[propertyName]; };
}
}
function makeOptionFunction(options, key) {
if (typeof options[key] !== 'function') {
var value = options[key];
options[key] = function() { return value; };
}
}
/**
* HtmlEncodes the given value
*/
var htmlEncodeContainer = $('<div />');
function htmlEncode(value) {
if (value) {
return htmlEncodeContainer.text(value).html();
} else {
return '';
}
}
/**
* Returns the position of the caret in the given input field
* http://flightschool.acylt.com/devnotes/caret-position-woes/
*/
function doGetCaretPosition(oField) {
var iCaretPos = 0;
if (document.selection) {
oField.focus ();
var oSel = document.selection.createRange();
oSel.moveStart ('character', -oField.value.length);
iCaretPos = oSel.text.length;
} else if (oField.selectionStart || oField.selectionStart == '0') {
iCaretPos = oField.selectionStart;
}
return (iCaretPos);
}
/**
* Initialize tagsinput behaviour on inputs and selects which have
* data-role=tagsinput
*/
$(function() {
$("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput();
});
})(window.jQuery);
\ No newline at end of file
.bootstrap-tagsinput {
display: inline-block;
padding: 4px 6px;
margin-bottom: 10px;
color: #555;
vertical-align: middle;
border-radius: 4px;
max-width: 100%;
line-height: 22px;
background-color: @inputBackground;
border: 1px solid @inputBorder;
.box-shadow(inset 0 1px 1px rgba(0,0,0,.075));
.transition(~"border linear .2s, box-shadow linear .2s");
input {
border: none;
box-shadow: none;
outline: none;
background-color: transparent;
padding: 0;
padding-left: 5px;
margin: 0;
width: auto !important;
max-width: inherit;
&:focus {
border: none;
box-shadow: none;
}
}
.tag {
margin-right: 2px;
color: white;
[data-role="remove"] {
margin-left:8px;
cursor:pointer;
&:after{
content: "x";
padding:0px 2px;
}
&:hover {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
&:active {
box-shadow: inset 0 3px 5px rgba(0,0,0,0.125);
}
}
}
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment