diff --git a/docs-core/src/main/resources/db/update/dbupdate-002-0.sql b/docs-core/src/main/resources/db/update/dbupdate-002-0.sql
index 79ff42c0..6dc22cba 100644
--- a/docs-core/src/main/resources/db/update/dbupdate-002-0.sql
+++ b/docs-core/src/main/resources/db/update/dbupdate-002-0.sql
@@ -1,3 +1,3 @@
-alter table T_TAG add column TAG_COLOR_C varchar(6) not null;
-update T_TAG set TAG_COLOR_C = '3a87ad';
+alter table T_TAG add column TAG_COLOR_C varchar(7) not null;
+update T_TAG set TAG_COLOR_C = '#3a87ad';
update T_CONFIG set CFG_VALUE_C='2' where CFG_ID_C='DB_VERSION';
diff --git a/docs-parent/TODO b/docs-parent/TODO
index 38913018..8dbc8e35 100644
--- a/docs-parent/TODO
+++ b/docs-parent/TODO
@@ -1,2 +1 @@
- Users administration (client)
-- Tag color (client)
diff --git a/docs-web-common/src/main/java/com/sismics/rest/util/ValidationUtil.java b/docs-web-common/src/main/java/com/sismics/rest/util/ValidationUtil.java
index e46416ba..fb7a6b10 100644
--- a/docs-web-common/src/main/java/com/sismics/rest/util/ValidationUtil.java
+++ b/docs-web-common/src/main/java/com/sismics/rest/util/ValidationUtil.java
@@ -63,7 +63,7 @@ public class ValidationUtil {
throw new ClientException("ValidationError", MessageFormat.format("{0} must be more than {1} characters", name, lengthMin));
}
if (lengthMax != null && s.length() > lengthMax) {
- throw new ClientException("ValidationError", MessageFormat.format("{0} must be more than {1} characters", name, lengthMax));
+ throw new ClientException("ValidationError", MessageFormat.format("{0} must be less than {1} characters", name, lengthMax));
}
return s;
}
@@ -94,6 +94,19 @@ public class ValidationUtil {
return validateLength(s, name, 1, null, false);
}
+ /**
+ * Checks if the string is a hexadecimal color.
+ *
+ * @param s String to validate
+ * @param name Name of the parameter
+ * @param nullable True if the string can be empty or null
+ * @throws JSONException
+ */
+ public static void validateHexColor(String s, String name, boolean nullable) throws JSONException {
+ // TODO Do a real check
+ ValidationUtil.validateLength(s, "name", 7, 7, nullable);
+ }
+
/**
* Checks if the string is an email.
*
diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/TagResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/TagResource.java
index 67593266..1270e618 100644
--- a/docs-web/src/main/java/com/sismics/docs/rest/resource/TagResource.java
+++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/TagResource.java
@@ -110,7 +110,7 @@ public class TagResource extends BaseResource {
// Validate input data
name = ValidationUtil.validateLength(name, "name", 1, 36, false);
- color = ValidationUtil.validateLength(color, "color", 6, 6, false);
+ ValidationUtil.validateHexColor(color, "color", true);
// Get the tag
TagDao tagDao = new TagDao();
@@ -151,7 +151,7 @@ public class TagResource extends BaseResource {
// Validate input data
name = ValidationUtil.validateLength(name, "name", 1, 36, true);
- color = ValidationUtil.validateLength(color, "color", 6, 6, true);
+ ValidationUtil.validateHexColor(color, "color", true);
// Get the tag
TagDao tagDao = new TagDao();
diff --git a/docs-web/src/main/webapp/img/alpha.png b/docs-web/src/main/webapp/img/alpha.png
new file mode 100644
index 00000000..eaddb40b
Binary files /dev/null and b/docs-web/src/main/webapp/img/alpha.png differ
diff --git a/docs-web/src/main/webapp/img/hue.png b/docs-web/src/main/webapp/img/hue.png
new file mode 100644
index 00000000..ad35ccb5
Binary files /dev/null and b/docs-web/src/main/webapp/img/hue.png differ
diff --git a/docs-web/src/main/webapp/img/saturation.png b/docs-web/src/main/webapp/img/saturation.png
new file mode 100644
index 00000000..82a4e3d1
Binary files /dev/null and b/docs-web/src/main/webapp/img/saturation.png differ
diff --git a/docs-web/src/main/webapp/index.html b/docs-web/src/main/webapp/index.html
index 80fbc1bb..80036a4c 100644
--- a/docs-web/src/main/webapp/index.html
+++ b/docs-web/src/main/webapp/index.html
@@ -6,6 +6,7 @@
+
+
@@ -27,6 +29,7 @@
+
diff --git a/docs-web/src/main/webapp/js/app.js b/docs-web/src/main/webapp/js/app.js
index 4c322ef8..e8414734 100644
--- a/docs-web/src/main/webapp/js/app.js
+++ b/docs-web/src/main/webapp/js/app.js
@@ -3,7 +3,7 @@
/**
* Trackino application.
*/
-var App = angular.module('docs', ['ui.state', 'ui.bootstrap', 'ui.route', 'ui.keypress', 'ui.validate', 'ui.sortable', 'restangular', 'ngSanitize'])
+var App = angular.module('docs', ['ui.state', 'ui.bootstrap', 'ui.route', 'ui.keypress', 'ui.validate', 'ui.sortable', 'restangular', 'ngSanitize', 'colorpicker.module'])
/**
* Configuring modules.
diff --git a/docs-web/src/main/webapp/js/controller/Tag.js b/docs-web/src/main/webapp/js/controller/Tag.js
index 052b0801..7d9d88f4 100644
--- a/docs-web/src/main/webapp/js/controller/Tag.js
+++ b/docs-web/src/main/webapp/js/controller/Tag.js
@@ -4,6 +4,8 @@
* Tag controller.
*/
App.controller('Tag', function($scope, $dialog, $state, Tag, Restangular) {
+ $scope.tag = { name: '', color: '#3a87ad' };
+
// Retrieve tags
Tag.tags().then(function(data) {
$scope.tags = data.tags;
@@ -27,11 +29,10 @@ App.controller('Tag', function($scope, $dialog, $state, Tag, Restangular) {
* Add a tag.
*/
$scope.addTag = function() {
- var name = $scope.tag.name;
- $scope.tag.name = '';
// TODO Check if the tag don't already exists
- Restangular.one('tag').put({ name: name }).then(function(data) {
- $scope.tags.push({ id: data.id, name: name });
+ Restangular.one('tag').put($scope.tag).then(function(data) {
+ $scope.tags.push({ id: data.id, name: $scope.tag.name, color: $scope.tag.color });
+ $scope.tag = { name: '', color: '#3a87ad' };
});
};
@@ -57,7 +58,7 @@ App.controller('Tag', function($scope, $dialog, $state, Tag, Restangular) {
};
/**
- * Update a tag name.
+ * Update a tag.
*/
$scope.updateTag = function(tag) {
Restangular.one('tag', tag.id).post('', tag);
diff --git a/docs-web/src/main/webapp/lib/angular.colorpicker.js b/docs-web/src/main/webapp/lib/angular.colorpicker.js
new file mode 100644
index 00000000..d483cab9
--- /dev/null
+++ b/docs-web/src/main/webapp/lib/angular.colorpicker.js
@@ -0,0 +1,97 @@
+'use strict';
+
+angular.module('colorpicker.module', [])
+ .factory('helper', function () {
+ return {
+ prepareValues: function(format) {
+ var thisFormat = 'hex';
+ if (format) {
+ thisFormat = format;
+ }
+ return {
+ name: thisFormat,
+ transform: 'to' + (thisFormat === 'hex' ? thisFormat.charAt(0).toUpperCase() + thisFormat.slice(1) : thisFormat.length > 3 ? thisFormat.toUpperCase().slice(0, -1) : thisFormat.toUpperCase())
+ };
+ },
+ updateView: function(element, value) {
+ if (!value) {
+ value = '';
+ }
+ element.val(value);
+ element.data('color', value);
+ element.data('colorpicker').update();
+ }
+ }
+ })
+ .directive('colorpicker', ['helper', function(helper) {
+ return {
+ require: '?ngModel',
+ restrict: 'A',
+ link: function(scope, element, attrs, ngModel) {
+
+ var thisFormat = helper.prepareValues(attrs.colorpicker);
+
+ element.colorpicker({format: thisFormat.name});
+
+ element.on('$destroy', function() {
+ element.data('colorpicker').picker.remove();
+ });
+
+ if(!ngModel) return;
+
+ element.colorpicker().on('changeColor', function(event) {
+ element.val(element.data('colorpicker').format(event.color[thisFormat.transform]()));
+ scope.$apply(ngModel.$setViewValue(element.data('colorpicker').format(event.color[thisFormat.transform]())));
+ });
+
+ element.colorpicker().on('hide', function(){
+ scope.$apply(attrs.onHide);
+ });
+
+ element.colorpicker().on('show', function(){
+ scope.$apply(attrs.onShow);
+ });
+
+ ngModel.$render = function() {
+ helper.updateView(element, ngModel.$viewValue) ;
+ }
+ }
+ };
+ }])
+ .directive('colorpicker', ['helper', function(helper) {
+ return {
+ require: '?ngModel',
+ restrict: 'E',
+ replace: true,
+ transclude: false,
+ scope: {
+ componentPicker: '=ngModel',
+ inputName: '@inputName',
+ inputClass: '@inputClass',
+ colorFormat: '@colorFormat'
+ },
+ template: '
' +
+ '' +
+ '' +
+ '
',
+
+ link: function(scope, element, attrs, ngModel) {
+
+ var thisFormat = helper.prepareValues(attrs.colorFormat);
+
+ element.colorpicker();
+ if(!ngModel) return;
+
+ var elementInput = angular.element(element.children()[0]);
+
+ element.colorpicker().on('changeColor', function(event) {
+ elementInput.val(element.data('colorpicker').format(event.color[thisFormat.transform]()));
+ scope.$parent.$apply(ngModel.$setViewValue(element.data('colorpicker').format(event.color[thisFormat.transform]())));
+ });
+
+ ngModel.$render = function() {
+ helper.updateView(element, ngModel.$viewValue) ;
+ }
+ }
+ };
+ }]);
diff --git a/docs-web/src/main/webapp/lib/colorpicker.js b/docs-web/src/main/webapp/lib/colorpicker.js
new file mode 100644
index 00000000..1f512d59
--- /dev/null
+++ b/docs-web/src/main/webapp/lib/colorpicker.js
@@ -0,0 +1,543 @@
+/* =========================================================
+ * bootstrap-colorpicker.js
+ * http://www.eyecon.ro/bootstrap-colorpicker
+ * =========================================================
+ * Copyright 2012 Stefan Petre
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================================================= */
+
+!function( $ ) {
+
+ // Color object
+
+ var Color = function(val) {
+ this.value = {
+ h: 1,
+ s: 1,
+ b: 1,
+ a: 1
+ };
+ this.setColor(val);
+ };
+
+ Color.prototype = {
+ constructor: Color,
+
+ //parse a string to HSB
+ setColor: function(val){
+ val = val.toLowerCase();
+ var that = this;
+ $.each( CPGlobal.stringParsers, function( i, parser ) {
+ var match = parser.re.exec( val ),
+ values = match && parser.parse( match ),
+ space = parser.space||'rgba';
+ if ( values ) {
+ if (space === 'hsla') {
+ that.value = CPGlobal.RGBtoHSB.apply(null, CPGlobal.HSLtoRGB.apply(null, values));
+ } else {
+ that.value = CPGlobal.RGBtoHSB.apply(null, values);
+ }
+ return false;
+ }
+ });
+ },
+
+ setHue: function(h) {
+ this.value.h = 1- h;
+ },
+
+ setSaturation: function(s) {
+ this.value.s = s;
+ },
+
+ setLightness: function(b) {
+ this.value.b = 1- b;
+ },
+
+ setAlpha: function(a) {
+ this.value.a = parseInt((1 - a)*100, 10)/100;
+ },
+
+ // HSBtoRGB from RaphaelJS
+ // https://github.com/DmitryBaranovskiy/raphael/
+ toRGB: function(h, s, b, a) {
+ if (!h) {
+ h = this.value.h;
+ s = this.value.s;
+ b = this.value.b;
+ }
+ h *= 360;
+ var R, G, B, X, C;
+ h = (h % 360) / 60;
+ C = b * s;
+ X = C * (1 - Math.abs(h % 2 - 1));
+ R = G = B = b - C;
+
+ h = ~~h;
+ R += [C, X, 0, 0, X, C][h];
+ G += [X, C, C, X, 0, 0][h];
+ B += [0, 0, X, C, C, X][h];
+ return {
+ r: Math.round(R*255),
+ g: Math.round(G*255),
+ b: Math.round(B*255),
+ a: a||this.value.a
+ };
+ },
+
+ toHex: function(h, s, b, a){
+ var rgb = this.toRGB(h, s, b, a);
+ return '#'+((1 << 24) | (parseInt(rgb.r) << 16) | (parseInt(rgb.g) << 8) | parseInt(rgb.b)).toString(16).substr(1);
+ },
+
+ toHSL: function(h, s, b, a){
+ if (!h) {
+ h = this.value.h;
+ s = this.value.s;
+ b = this.value.b;
+ }
+ var H = h,
+ L = (2 - s) * b,
+ S = s * b;
+ if (L > 0 && L <= 1) {
+ S /= L;
+ } else {
+ S /= 2 - L;
+ }
+ L /= 2;
+ if (S > 1) {
+ S = 1;
+ }
+ return {
+ h: H,
+ s: S,
+ l: L,
+ a: a||this.value.a
+ };
+ }
+ };
+
+ // Picker object
+
+ var Colorpicker = function(element, options){
+ this.element = $(element);
+ var format = options.format||this.element.data('color-format')||'hex';
+ this.format = CPGlobal.translateFormats[format];
+ this.isInput = this.element.is('input');
+ this.component = this.element.is('.color') ? this.element.find('.add-on') : false;
+
+ this.picker = $(CPGlobal.template)
+ .appendTo('body')
+ .on('mousedown', $.proxy(this.mousedown, this));
+
+ if (this.isInput) {
+ this.element.on({
+ 'focus': $.proxy(this.show, this),
+ 'keyup': $.proxy(this.update, this)
+ });
+ } else if (this.component){
+ this.component.on({
+ 'click': $.proxy(this.show, this)
+ });
+ } else {
+ this.element.on({
+ 'click': $.proxy(this.show, this)
+ });
+ }
+ if (format === 'rgba' || format === 'hsla') {
+ this.picker.addClass('alpha');
+ this.alpha = this.picker.find('.colorpicker-alpha')[0].style;
+ }
+
+ if (this.component){
+ this.picker.find('.colorpicker-color').hide();
+ this.preview = this.element.find('i')[0].style;
+ } else {
+ this.preview = this.picker.find('div:last')[0].style;
+ }
+
+ this.base = this.picker.find('div:first')[0].style;
+ this.update();
+ };
+
+ Colorpicker.prototype = {
+ constructor: Colorpicker,
+
+ show: function(e) {
+ this.update();
+ this.picker.show();
+ this.height = this.component ? this.component.outerHeight() : this.element.outerHeight();
+ this.place();
+ $(window).on('resize', $.proxy(this.place, this));
+ if (!this.isInput) {
+ if (e) {
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+ $(document).on({
+ 'mousedown': $.proxy(this.hide, this)
+ });
+
+ this.element.trigger({
+ type: 'show',
+ color: this.color
+ });
+ },
+
+ update: function(){
+ this.color = new Color(this.isInput ? this.element.prop('value') : this.element.data('color'));
+ this.picker.find('i')
+ .eq(0).css({left: this.color.value.s*100, top: 100 - this.color.value.b*100}).end()
+ .eq(1).css('top', 100 * (1 - this.color.value.h)).end()
+ .eq(2).css('top', 100 * (1 - this.color.value.a));
+ this.previewColor();
+ },
+
+ setValue: function(newColor) {
+ this.color = new Color(newColor);
+ this.picker.find('i')
+ .eq(0).css({left: this.color.value.s*100, top: 100 - this.color.value.b*100}).end()
+ .eq(1).css('top', 100 * (1 - this.color.value.h)).end()
+ .eq(2).css('top', 100 * (1 - this.color.value.a));
+ this.previewColor();
+ this.element.trigger({
+ type: 'changeColor',
+ color: this.color
+ });
+ },
+
+ hide: function(){
+ this.picker.hide();
+ $(window).off('resize', this.place);
+ if (!this.isInput) {
+ $(document).off({
+ 'mousedown': this.hide
+ });
+ if (this.component){
+ this.element.find('input').prop('value', this.format.call(this));
+ }
+ this.element.data('color', this.format.call(this));
+ } else {
+ this.element.prop('value', this.format.call(this));
+ }
+ this.element.trigger({
+ type: 'hide',
+ color: this.color
+ });
+ },
+
+ place: function(){
+ var offset = this.component ? this.component.offset() : this.element.offset();
+ this.picker.css({
+ top: offset.top + this.height,
+ left: offset.left
+ });
+ },
+
+ //preview color change
+ previewColor: function(){
+ try {
+ this.preview.backgroundColor = this.format.call(this);
+ } catch(e) {
+ this.preview.backgroundColor = this.color.toHex();
+ }
+ //set the color for brightness/saturation slider
+ this.base.backgroundColor = this.color.toHex(this.color.value.h, 1, 1, 1);
+ //set te color for alpha slider
+ if (this.alpha) {
+ this.alpha.backgroundColor = this.color.toHex();
+ }
+ },
+
+ pointer: null,
+
+ slider: null,
+
+ mousedown: function(e){
+ e.stopPropagation();
+ e.preventDefault();
+
+ var target = $(e.target);
+
+ //detect the slider and set the limits and callbacks
+ var zone = target.closest('div');
+ if (!zone.is('.colorpicker')) {
+ if (zone.is('.colorpicker-saturation')) {
+ this.slider = $.extend({}, CPGlobal.sliders.saturation);
+ }
+ else if (zone.is('.colorpicker-hue')) {
+ this.slider = $.extend({}, CPGlobal.sliders.hue);
+ }
+ else if (zone.is('.colorpicker-alpha')) {
+ this.slider = $.extend({}, CPGlobal.sliders.alpha);
+ } else {
+ return false;
+ }
+ var offset = zone.offset();
+ //reference to knob's style
+ this.slider.knob = zone.find('i')[0].style;
+ this.slider.left = e.pageX - offset.left;
+ this.slider.top = e.pageY - offset.top;
+ this.pointer = {
+ left: e.pageX,
+ top: e.pageY
+ };
+ //trigger mousemove to move the knob to the current position
+ $(document).on({
+ mousemove: $.proxy(this.mousemove, this),
+ mouseup: $.proxy(this.mouseup, this)
+ }).trigger('mousemove');
+ }
+ return false;
+ },
+
+ mousemove: function(e){
+ e.stopPropagation();
+ e.preventDefault();
+ var left = Math.max(
+ 0,
+ Math.min(
+ this.slider.maxLeft,
+ this.slider.left + ((e.pageX||this.pointer.left) - this.pointer.left)
+ )
+ );
+ var top = Math.max(
+ 0,
+ Math.min(
+ this.slider.maxTop,
+ this.slider.top + ((e.pageY||this.pointer.top) - this.pointer.top)
+ )
+ );
+ this.slider.knob.left = left + 'px';
+ this.slider.knob.top = top + 'px';
+ if (this.slider.callLeft) {
+ this.color[this.slider.callLeft].call(this.color, left/100);
+ }
+ if (this.slider.callTop) {
+ this.color[this.slider.callTop].call(this.color, top/100);
+ }
+ this.previewColor();
+ this.element.trigger({
+ type: 'changeColor',
+ color: this.color
+ });
+ return false;
+ },
+
+ mouseup: function(e){
+ e.stopPropagation();
+ e.preventDefault();
+ $(document).off({
+ mousemove: this.mousemove,
+ mouseup: this.mouseup
+ });
+ return false;
+ }
+ }
+
+ $.fn.colorpicker = function ( option, val ) {
+ return this.each(function () {
+ var $this = $(this),
+ data = $this.data('colorpicker'),
+ options = typeof option === 'object' && option;
+ if (!data) {
+ $this.data('colorpicker', (data = new Colorpicker(this, $.extend({}, $.fn.colorpicker.defaults,options))));
+ }
+
+ if (typeof option === 'string') data[option](val);
+ });
+ };
+
+ $.fn.colorpicker.defaults = {
+ };
+
+ $.fn.colorpicker.Constructor = Colorpicker;
+
+ var CPGlobal = {
+
+ // translate a format from Color object to a string
+ translateFormats: {
+ 'rgb': function(){
+ var rgb = this.color.toRGB();
+ return 'rgb('+rgb.r+','+rgb.g+','+rgb.b+')';
+ },
+
+ 'rgba': function(){
+ var rgb = this.color.toRGB();
+ return 'rgba('+rgb.r+','+rgb.g+','+rgb.b+','+rgb.a+')';
+ },
+
+ 'hsl': function(){
+ var hsl = this.color.toHSL();
+ return 'hsl('+Math.round(hsl.h*360)+','+Math.round(hsl.s*100)+'%,'+Math.round(hsl.l*100)+'%)';
+ },
+
+ 'hsla': function(){
+ var hsl = this.color.toHSL();
+ return 'hsla('+Math.round(hsl.h*360)+','+Math.round(hsl.s*100)+'%,'+Math.round(hsl.l*100)+'%,'+hsl.a+')';
+ },
+
+ 'hex': function(){
+ return this.color.toHex();
+ }
+ },
+
+ sliders: {
+ saturation: {
+ maxLeft: 100,
+ maxTop: 100,
+ callLeft: 'setSaturation',
+ callTop: 'setLightness'
+ },
+
+ hue: {
+ maxLeft: 0,
+ maxTop: 100,
+ callLeft: false,
+ callTop: 'setHue'
+ },
+
+ alpha: {
+ maxLeft: 0,
+ maxTop: 100,
+ callLeft: false,
+ callTop: 'setAlpha'
+ }
+ },
+
+ // HSBtoRGB from RaphaelJS
+ // https://github.com/DmitryBaranovskiy/raphael/
+ RGBtoHSB: function (r, g, b, a){
+ r /= 255;
+ g /= 255;
+ b /= 255;
+
+ var H, S, V, C;
+ V = Math.max(r, g, b);
+ C = V - Math.min(r, g, b);
+ H = (C === 0 ? null :
+ V == r ? (g - b) / C :
+ V == g ? (b - r) / C + 2 :
+ (r - g) / C + 4
+ );
+ H = ((H + 360) % 6) * 60 / 360;
+ S = C === 0 ? 0 : C / V;
+ return {h: H||1, s: S, b: V, a: a||1};
+ },
+
+ HueToRGB: function (p, q, h) {
+ if (h < 0)
+ h += 1;
+ else if (h > 1)
+ h -= 1;
+
+ if ((h * 6) < 1)
+ return p + (q - p) * h * 6;
+ else if ((h * 2) < 1)
+ return q;
+ else if ((h * 3) < 2)
+ return p + (q - p) * ((2 / 3) - h) * 6;
+ else
+ return p;
+ },
+
+ HSLtoRGB: function (h, s, l, a)
+ {
+ if (s < 0) {
+ s = 0;
+ }
+ var q;
+ if (l <= 0.5) {
+ q = l * (1 + s);
+ } else {
+ q = l + s - (l * s);
+ }
+
+ var p = 2 * l - q;
+
+ var tr = h + (1 / 3);
+ var tg = h;
+ var tb = h - (1 / 3);
+
+ var r = Math.round(CPGlobal.HueToRGB(p, q, tr) * 255);
+ var g = Math.round(CPGlobal.HueToRGB(p, q, tg) * 255);
+ var b = Math.round(CPGlobal.HueToRGB(p, q, tb) * 255);
+ return [r, g, b, a||1];
+ },
+
+ // a set of RE's that can match strings and generate color tuples.
+ // from John Resig color plugin
+ // https://github.com/jquery/jquery-color/
+ stringParsers: [
+ {
+ re: /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,
+ parse: function( execResult ) {
+ return [
+ execResult[ 1 ],
+ execResult[ 2 ],
+ execResult[ 3 ],
+ execResult[ 4 ]
+ ];
+ }
+ }, {
+ re: /rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,
+ parse: function( execResult ) {
+ return [
+ 2.55 * execResult[1],
+ 2.55 * execResult[2],
+ 2.55 * execResult[3],
+ execResult[ 4 ]
+ ];
+ }
+ }, {
+ re: /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/,
+ parse: function( execResult ) {
+ return [
+ parseInt( execResult[ 1 ], 16 ),
+ parseInt( execResult[ 2 ], 16 ),
+ parseInt( execResult[ 3 ], 16 )
+ ];
+ }
+ }, {
+ re: /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/,
+ parse: function( execResult ) {
+ return [
+ parseInt( execResult[ 1 ] + execResult[ 1 ], 16 ),
+ parseInt( execResult[ 2 ] + execResult[ 2 ], 16 ),
+ parseInt( execResult[ 3 ] + execResult[ 3 ], 16 )
+ ];
+ }
+ }, {
+ re: /hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,
+ space: 'hsla',
+ parse: function( execResult ) {
+ return [
+ execResult[1]/360,
+ execResult[2] / 100,
+ execResult[3] / 100,
+ execResult[4]
+ ];
+ }
+ }
+ ],
+ template: ''
+ };
+
+}( window.jQuery )
\ No newline at end of file
diff --git a/docs-web/src/main/webapp/partial/directive.selecttag.html b/docs-web/src/main/webapp/partial/directive.selecttag.html
index c18d98a6..3a4dc26c 100644
--- a/docs-web/src/main/webapp/partial/directive.selecttag.html
+++ b/docs-web/src/main/webapp/partial/directive.selecttag.html
@@ -1,6 +1,6 @@
- - {{ tag.name }}
+ - {{ tag.name }}
diff --git a/docs-web/src/main/webapp/partial/document.html b/docs-web/src/main/webapp/partial/document.html
index 8eb6572b..a68384a4 100644
--- a/docs-web/src/main/webapp/partial/document.html
+++ b/docs-web/src/main/webapp/partial/document.html
@@ -50,7 +50,7 @@
{{ document.create_date | date: 'yyyy-MM-dd' }} |
- {{ tag.name | shorten }}{{ tag.name }}
+ {{ tag.name | shorten }}{{ tag.name }}
|
diff --git a/docs-web/src/main/webapp/partial/document.view.html b/docs-web/src/main/webapp/partial/document.view.html
index dffa0fed..70620642 100644
--- a/docs-web/src/main/webapp/partial/document.view.html
+++ b/docs-web/src/main/webapp/partial/document.view.html
@@ -8,7 +8,7 @@
diff --git a/docs-web/src/main/webapp/partial/tag.html b/docs-web/src/main/webapp/partial/tag.html
index 9c589946..422c134f 100644
--- a/docs-web/src/main/webapp/partial/tag.html
+++ b/docs-web/src/main/webapp/partial/tag.html
@@ -1,9 +1,8 @@
-
-
+
@@ -17,7 +16,8 @@
|
- |
+ |
+ |
@@ -27,7 +27,7 @@
{{ tags.length }} tag{{ tags.length > 1 ? 's' : '' }}
- - {{ stat.name }} {{ stat.count }}
+ - {{ stat.name }} {{ stat.count }}
diff --git a/docs-web/src/main/webapp/style/colorpicker.css b/docs-web/src/main/webapp/style/colorpicker.css
new file mode 100644
index 00000000..1a34503a
--- /dev/null
+++ b/docs-web/src/main/webapp/style/colorpicker.css
@@ -0,0 +1,112 @@
+.colorpicker-saturation {
+ width: 100px;
+ height: 100px;
+ background-image: url('data:image/png;base64,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');
+ cursor: crosshair;
+ float: left;
+}
+.colorpicker-saturation i {
+ display: block;
+ height: 5px;
+ width: 5px;
+ border: 1px solid #000;
+ border-radius: 5px;
+ position: absolute;
+ top: 0;
+ left: 0;
+ margin: -4px 0 0 -4px;
+}
+.colorpicker-saturation i b {
+ display: block;
+ height: 5px;
+ width: 5px;
+ border: 1px solid #fff;
+ border-radius: 5px;
+}
+.colorpicker-hue,
+.colorpicker-alpha {
+ width: 15px;
+ height: 100px;
+ float: left;
+ cursor: row-resize;
+ margin-left: 4px;
+ margin-bottom: 4px;
+}
+.colorpicker-hue i,
+.colorpicker-alpha i {
+ display: block;
+ height: 1px;
+ background: #000;
+ border-top: 1px solid #fff;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ margin-top: -1px;
+}
+.colorpicker-hue {
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAABkCAMAAABw8qpSAAABLFBMVEUA/z8AuP//JAAA/33/3AAA/1ABAv8A/7r/AH7/jgD2AP8A//j/AEHmAP/XAP/HAP+4AP//ALyoAP+aAP+JAP97AP9rAP9cAP9MAP8+AP8tAP8fAP8PAP8ATv//AG7/cAD/vgD/APoAmv//ADH/AKwB/wMA5//4Eg4AL///AOr/UQD/nwAA/27/7AAA/+kAe/8Ayf8A/5sA/zEA/6z/ABEAEP8A/17/MgAA/9n/ACL/gAD/AJ0AXP8Aqv//AMoA/yHqFBb/zAD/AGD/ANsA9//1/wDk/wDV/wDF/wC3/wD/AI2m/wD/FACY/wCI/wB5/wBp/wD/YgBb/wBK/wA8/wAs/wAd/wAN/wAAPv8A/xH/AFAAi///rQAA/8r/+gAA1///QwAAH/8Abf8A/43c/JNGAAAAiUlEQVR4AQXBg2EDAAAAsMy2bds2ttp2+/8PTby79mDLsKJPq/oFPdk24dWXAxsGjRg1ZtykKdNmzJozb8GiJct63WjYl7fiWdOZkk0vOpyr2fVtyKl7FX2uXGjpcuxWDy69KdiRk5WRlpIUFxMVERLw78+vH1Unun1YV3ZkwKM1CYfq7nQK22sD03ITV2Aqp0IAAAAASUVORK5CYII=');
+}
+.colorpicker-alpha {
+ display: none;
+}
+.colorpicker-alpha,
+.colorpicker-color {
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAABkCAMAAACIElGlAAADAFBMVEUAAADT09PT09P////T09P////e3t7q6urT09Px8fHT09P////////T09PT09P////////T09PT09P////////////T09PT09P////////////T09P////T09PT09PT09P////T09PT09P////////////////T09P////T09PT09P////T09PT09PT09PT09PT09PT09P////T09P////T09PT09P////////////T09P////T09P////////////T09PT09P////T09P////////////////////T09P////////T09PT09P////////////////////////T09PT09P////////////////////////T09PT09P////T09PT09P////////T09P////////////T09P////////T09P////T09P////T09P////T09PT09PT09PT09P////T09PT09PT09PT09PT09PT09P////T09P////T09PT09P////////////T09PT09PT09P////T09PT09PT09PT09PT09PT09PT09P////////////////T09PT09P////////////T09P////////T09P////T09PT09PT09P////////T09P////////T09P////T09PT09P////////////////T09PT09PT09PT09P////T09PT09PT09PT09PT09PT09PT09P////T09P////T09PT09PT09PT09PT09P////////////////////////////////////T09P////T09P////T09P////T09PT09P////////////T09P////T09P////T09P////////////T09PT09P////////////////T09PT09P////T09P////T09PT09P////T09P////T09PT09P////T09PT09PT09PT09P////T09PT09P////////////T09PT09P////T09P////////T09MQsm1FAAABAHRSTlMAgJN8/vcDAfcCnJyGaZmZlomGk4yJOmM/eTxs8wY0YDFC7HNdLx18n5/7aUvzCcW9+qKiK8P0ZiltRwfdw/n8Px3WduJjItj78ss5PDHUNELbwP5wplA2FglEVwvkqNarCs4Z7b2sDLgQ0xNdyLrr0eLLUeW1Vs5TWQLwjPI3ZvQGdvxFyFrAeevaLCLvGd0kpRskGyf4qK605xKvFrGyDRHnBYMEkJaDkIBvB/gpH99O6CrbIC4nH3Lg2SXp4A7Qul/GDEgPSlMQ6LjqFU0SjyCCj5V/gnBN7xglL3O70WBU7gjFFEfft0sPTo1ndRipwXr2yRemWRVENq+ytbGuxGaWGQAAAnpJREFUeNpNxmdcDHAAxvEHOWXvmXX23ntv2XvvMg/Z44x0KaRBSnfcKSqlnYZKp1QqGpT20KZh783/eePj+3vzw3/q/AM9iURPTy6XS+RwcXNzcZMQTE1dTE3bE7y9S0u9xxD600rCGtpI2ErOsbGxzoiMrIqscnaOi4vDdnp4XYSrguwxQSb7KpMllycnl2MFaTSaTxrY5NrY5BoKb2BnZ2doZ29vb2OPSdSd4Bcc7OcXGhqamgqrYCsrq+mEiEtC3gUBDg55Dg53IkQYSYG+gb6+GEqB5wV0o5D8/JAQdKK5hA40n3BS+L6c0JJmE+ZQW8JEak1YQJ0Jq+jbaQEPnotGE+IrK+Pj+xKqU6pTUrIDAgKysZ+WEY5QQkJxcQIO0WrCUbL4YmFhgZ3UlVAUHR1d1JEwjVoRRpDJKxMTE/ShBoRR1I6whLyyvLyyUFAQFhbWhTCFFArFawXOCu/bEHpQLYKnZ0aGZw2CR1qah0dmpqurK+pTU0ILakJoTi+PC7j5RHTrqQg1qRkh3T3d3b0xoSE1ItSleoSe1IswmIYQPl8UlhLWkaOj41tHbKENBLX6j1qt/KFUKnFN+Onk9NvJCQdJpfqlUqGiIjExcRthN91/JsJmGkhYRO/OCBhHwwgxMWUxZQMIu8j8o7m5OfbRLMJh2kTYQ1FRhYVR2EHzCOtpMmEG3SUsJEvLoKAg2Prb2tqOJYSH+/uH33shwlTaS1hLBwhX6AYX2tuiRwStVntOa5yUZJwEqbREKjUi5Bjl5BhZW1uXWOMYGRBOnNDX1zfoxz1FwwkfSJfgo6vr41P7soBBNJ7Qm2YSzHTMzMwmEHRoMf0Fm5mYOUrzNBYAAAAASUVORK5CYII=');
+}
+.colorpicker {
+ *zoom: 1;
+ top: 0;
+ left: 0;
+ padding: 4px;
+ min-width: 120px;
+ margin-top: 1px;
+ border-radius: 4px;
+}
+.colorpicker:after {
+ clear: both;
+ content: '';
+ display: inline-block;
+ border-left: 6px solid transparent;
+ border-right: 6px solid transparent;
+ border-bottom: 6px solid #ffffff;
+ position: absolute;
+ top: -6px;
+ left: 7px;
+}
+.colorpicker:before {
+ content: '';
+ display: inline-block;
+ border-left: 7px solid transparent;
+ border-right: 7px solid transparent;
+ border-bottom: 7px solid #ccc;
+ border-bottom-color: rgba(0, 0, 0, 0.2);
+ position: absolute;
+ top: -7px;
+ left: 6px;
+}
+.colorpicker div {
+ position: relative;
+}
+.colorpicker.alpha {
+ min-width: 140px;
+}
+.colorpicker.alpha .colorpicker-alpha {
+ display: block;
+}
+.colorpicker-color {
+ height: 10px;
+ margin-top: 5px;
+ clear: both;
+ background-position: 0 100%;
+}
+.colorpicker-color div {
+ height: 10px;
+}
+.input-append.color .add-on i,
+.input-prepend.color .add-on i {
+ display: block;
+ cursor: pointer;
+ width: 16px;
+ height: 16px;
+}
diff --git a/docs-web/src/main/webapp/style/main.less b/docs-web/src/main/webapp/style/main.less
index 17dabe1e..d7b17afe 100644
--- a/docs-web/src/main/webapp/style/main.less
+++ b/docs-web/src/main/webapp/style/main.less
@@ -43,6 +43,13 @@
}
}
+// Tags list
+.table-tags {
+ tbody td {
+ vertical-align: middle;
+ }
+}
+
// Logs list
.table-logs {
tbody tr td {
diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java
index f4f81a63..48d06e69 100644
--- a/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java
+++ b/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java
@@ -38,7 +38,7 @@ public class TestDocumentResource extends BaseJerseyTest {
tagResource.addFilter(new CookieAuthenticationFilter(document1Token));
MultivaluedMapImpl postParams = new MultivaluedMapImpl();
postParams.add("name", "Super tag");
- postParams.add("color", "ffff00");
+ postParams.add("color", "#ffff00");
ClientResponse response = tagResource.put(ClientResponse.class, postParams);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
JSONObject json = response.getEntity(JSONObject.class);
@@ -76,7 +76,7 @@ public class TestDocumentResource extends BaseJerseyTest {
Assert.assertEquals(1, tags.length());
Assert.assertEquals(tag1Id, tags.getJSONObject(0).getString("id"));
Assert.assertEquals("Super tag", tags.getJSONObject(0).getString("name"));
- Assert.assertEquals("ffff00", tags.getJSONObject(0).getString("color"));
+ Assert.assertEquals("#ffff00", tags.getJSONObject(0).getString("color"));
// Search documents by query
documentResource = resource().path("/document/list");
@@ -143,7 +143,7 @@ public class TestDocumentResource extends BaseJerseyTest {
tagResource.addFilter(new CookieAuthenticationFilter(document1Token));
postParams = new MultivaluedMapImpl();
postParams.add("name", "Super tag 2");
- postParams.add("color", "00ffff");
+ postParams.add("color", "#00ffff");
response = tagResource.put(ClientResponse.class, postParams);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
json = response.getEntity(JSONObject.class);
diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java
index ca041176..64923683 100644
--- a/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java
+++ b/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java
@@ -35,7 +35,7 @@ public class TestTagResource extends BaseJerseyTest {
tagResource.addFilter(new CookieAuthenticationFilter(tag1Token));
MultivaluedMapImpl postParams = new MultivaluedMapImpl();
postParams.add("name", "Tag 3");
- postParams.add("color", "ff0000");
+ postParams.add("color", "#ff0000");
ClientResponse response = tagResource.put(ClientResponse.class, postParams);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
JSONObject json = response.getEntity(JSONObject.class);
@@ -47,7 +47,7 @@ public class TestTagResource extends BaseJerseyTest {
tagResource.addFilter(new CookieAuthenticationFilter(tag1Token));
postParams = new MultivaluedMapImpl();
postParams.add("name", "Tag 4");
- postParams.add("color", "00ff00");
+ postParams.add("color", "#00ff00");
response = tagResource.put(ClientResponse.class, postParams);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
json = response.getEntity(JSONObject.class);
@@ -94,14 +94,14 @@ public class TestTagResource extends BaseJerseyTest {
JSONArray tags = json.getJSONArray("tags");
Assert.assertTrue(tags.length() > 0);
Assert.assertEquals("Tag 4", tags.getJSONObject(1).getString("name"));
- Assert.assertEquals("00ff00", tags.getJSONObject(1).getString("color"));
+ Assert.assertEquals("#00ff00", tags.getJSONObject(1).getString("color"));
// Update a tag
tagResource = resource().path("/tag/" + tag4Id);
tagResource.addFilter(new CookieAuthenticationFilter(tag1Token));
postParams = new MultivaluedMapImpl();
postParams.add("name", "Updated name");
- postParams.add("color", "0000ff");
+ postParams.add("color", "#0000ff");
response = tagResource.post(ClientResponse.class, postParams);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
json = response.getEntity(JSONObject.class);
@@ -116,7 +116,7 @@ public class TestTagResource extends BaseJerseyTest {
tags = json.getJSONArray("tags");
Assert.assertTrue(tags.length() > 0);
Assert.assertEquals("Updated name", tags.getJSONObject(1).getString("name"));
- Assert.assertEquals("0000ff", tags.getJSONObject(1).getString("color"));
+ Assert.assertEquals("#0000ff", tags.getJSONObject(1).getString("color"));
// Deletes a tag
tagResource = resource().path("/tag/" + tag4Id);