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: '