From 3ebdce373e134b70b129154d8033c1c628847a6c Mon Sep 17 00:00:00 2001
From: Fulup Ar Foll <fulup@iot.bzh>
Date: Mon, 25 Jan 2016 14:37:32 +0100
Subject: First version

---
 .../widgets/ActionButtons/ActionButtons.scss       |  27 +
 .../Frontend/widgets/ActionButtons/AppliButton.js  | 130 +++++
 .../Frontend/widgets/ActionButtons/SubmitButton.js |  52 ++
 .../app/Frontend/widgets/FormInput/FormInput.scss  |  90 +++
 .../Frontend/widgets/FormInput/InputPassword.js    |  79 +++
 .../app/Frontend/widgets/FormInput/InputText.js    | 179 ++++++
 .../app/Frontend/widgets/FormInput/UploadAppli.js  | 230 ++++++++
 .../app/Frontend/widgets/Navigation/LinkButton.js  |  57 ++
 .../Frontend/widgets/Navigation/Navigation.scss    |  26 +
 .../widgets/Notifications/ModalNotification.js     |  85 +++
 .../widgets/Notifications/Notifications.scss       |  63 ++
 .../widgets/Notifications/TokenRefreshSvc.js       | 131 +++++
 .../widgets/RangeSliders/RangeSliderMod.js         | 631 +++++++++++++++++++++
 .../Frontend/widgets/RangeSliders/Rangeslider.scss |  67 +++
 14 files changed, 1847 insertions(+)
 create mode 100644 afm-client/app/Frontend/widgets/ActionButtons/ActionButtons.scss
 create mode 100644 afm-client/app/Frontend/widgets/ActionButtons/AppliButton.js
 create mode 100644 afm-client/app/Frontend/widgets/ActionButtons/SubmitButton.js
 create mode 100644 afm-client/app/Frontend/widgets/FormInput/FormInput.scss
 create mode 100644 afm-client/app/Frontend/widgets/FormInput/InputPassword.js
 create mode 100644 afm-client/app/Frontend/widgets/FormInput/InputText.js
 create mode 100644 afm-client/app/Frontend/widgets/FormInput/UploadAppli.js
 create mode 100644 afm-client/app/Frontend/widgets/Navigation/LinkButton.js
 create mode 100644 afm-client/app/Frontend/widgets/Navigation/Navigation.scss
 create mode 100644 afm-client/app/Frontend/widgets/Notifications/ModalNotification.js
 create mode 100644 afm-client/app/Frontend/widgets/Notifications/Notifications.scss
 create mode 100644 afm-client/app/Frontend/widgets/Notifications/TokenRefreshSvc.js
 create mode 100644 afm-client/app/Frontend/widgets/RangeSliders/RangeSliderMod.js
 create mode 100644 afm-client/app/Frontend/widgets/RangeSliders/Rangeslider.scss

(limited to 'afm-client/app/Frontend/widgets')

diff --git a/afm-client/app/Frontend/widgets/ActionButtons/ActionButtons.scss b/afm-client/app/Frontend/widgets/ActionButtons/ActionButtons.scss
new file mode 100644
index 0000000..16f7bb5
--- /dev/null
+++ b/afm-client/app/Frontend/widgets/ActionButtons/ActionButtons.scss
@@ -0,0 +1,27 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+@import "app/ibz-mixins";
+
+// place here your submit buttons customization
+appli-button {
+    @include ibz-button(grey,1rem)
+    img {
+        height: 3rem;
+    }
+}
diff --git a/afm-client/app/Frontend/widgets/ActionButtons/AppliButton.js b/afm-client/app/Frontend/widgets/ActionButtons/AppliButton.js
new file mode 100644
index 0000000..387212e
--- /dev/null
+++ b/afm-client/app/Frontend/widgets/ActionButtons/AppliButton.js
@@ -0,0 +1,130 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * Bugs: Input with Callback SHOULD BE get 'required' class
+ */
+
+(function () {
+    'use strict';
+
+    var tmplAppli = '<div  ng-click="clicked()">' +
+            '<img ng-src={{icon}}-ico.png>' +
+            '<span>{{label}}</span>' +
+            '</div>';
+    
+    var tmplModal = 
+            '<b class="close-button" ng-click="close()">×</b>' +
+            '<img ng-src="{{appicon}}-ico.png">' +
+            '<span class="modal-text">Application <b>{{appname}}</b></span>' +
+            '<ul class="vertical icon-left primary menu-bar">' +
+            '<li><a ng-click=action("start")><i class="fi-check"> Start</i></a></li>' +
+            '<li><a href="#"><i class="fi-x"> Stop</i></a></li>' +
+            '<li><a href="#"><i class="fi-info"> Info</i></a></li>' +
+            '</ul>' +
+            '';
+
+    angular.module('AppliButton', [])
+            .directive('appliButton', function (AppConfig, AppCall, ModalFactory, Notification, $timeout) {
+
+                function mymethods(scope, elem, attrs) {
+                    scope.clicked = function () {
+                        
+                        var closeModal = function() {
+                            console.log ("Modal Closing");
+                            scope.modal.deactivate();
+                            $timeout (function() {scope.modal.destroy();}, 1000);
+                        };
+                        
+                        var actionModal = function(action) {
+                            console.log ("Modal Action=%s", action);
+                            switch (action) {
+                                
+                                case "start":
+                                    AppCall.get ("afm-main", "start", {id: scope.appliID}, function(response) {
+                                        if (response.status !== 200) {
+                                            Notification.error ({message: "Fail to start application=" + scope.label +" ID="+ scope.appliID, delay: 5000});
+                                            elem.addClass ("fail");
+                                            elem.removeClass ("success");
+                                            scope.callback (scope.appliID, "/api/afm-main/start", response);
+                                            return;
+                                        }
+
+                                        // Check this is a valid response from Binder
+                                        if (response.data.request.jtype !== "AJB_reply" && response.data.request.api !== "start") {
+                                            Notification.error ({message: "Invalid Respond to /opa/afm-main/start response.data="+response.data, delay: 5000}); 
+                                            elem.addClass ("fail");
+                                            elem.removeClass ("success");
+                                            scope.callback (scope.appliID, "/api/afm-main/start", response);
+                                            return;
+                                        }
+                                        
+                                        // Application was stated
+                                        scope.callback (scope.appliID, "/api/afm-main/start", response);
+                                    });
+                                    break;
+                                    
+                                case "stop":
+                                    break;
+                                    
+                                default:
+                                    console.log ("ActionModal unknown action=[%s]", action);
+                                    break;
+                            }
+                            
+                            closeModal();
+                        };
+            
+                        // reference http://foundation.zurb.com/apps/docs/#!/angular-modules
+                        var config = {
+                            animationIn: 'slideInFromTop',
+                            contentScope: {
+                                action  : actionModal,
+                                close   : closeModal,
+                                appicon : scope.icon,
+                                appname : scope.label,
+                            }, template : tmplModal
+                        }; 
+                        // Popup Modal to render application data
+                        scope.modal = new ModalFactory(config);
+                        scope.modal.activate ();
+                    };
+
+                    // extract application information from AppID+Store
+                    if (attrs.handle && scope.store [attrs.handle].name) {
+                        scope.icon  = AppConfig.paths.icons + scope.store [attrs.handle].name.toLowerCase();
+                        scope.label = scope.store [attrs.handle].name;
+                        scope.appliID= attrs.handle;
+                    } else {
+                         scope.icon  = AppConfig.paths.icons + 'w3c-ico.png';
+                         scope.label = attrs.handle;
+                    }
+                                
+                    // add label as class
+                    elem.addClass (scope.label.toLowerCase());
+                    
+                    // note: clicked in imported and when template is clicked
+                    // it will call clicked method passed in param.
+                }
+                
+                return {
+                    restrict: 'E',
+                    template: tmplAppli,
+                    link: mymethods,
+                    scope: {callback: '=', store: '='}
+                };
+            });
+})();
diff --git a/afm-client/app/Frontend/widgets/ActionButtons/SubmitButton.js b/afm-client/app/Frontend/widgets/ActionButtons/SubmitButton.js
new file mode 100644
index 0000000..323cd46
--- /dev/null
+++ b/afm-client/app/Frontend/widgets/ActionButtons/SubmitButton.js
@@ -0,0 +1,52 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * Bugs: Input with Callback SHOULD BE get 'required' class
+ */
+
+(function () {
+    'use strict';
+
+    var tmpl = '<div  ng-click="clicked()">' +
+            '<i class="{{icon}}"></i>' +
+            '<span>{{label}}</span>' +
+            '</div>';
+
+    angular.module('SubmitButton', [])
+            .directive('submitButton', function () {
+
+                function mymethods(scope, elem, attrs) {
+
+                    // ajust icon or use default
+                    scope.icon = attrs.icon || 'fi-foot';
+                    scope.label = attrs.label || 'Next';
+                                
+                    // add label as class
+                    elem.addClass (scope.label.toLowerCase());
+                    
+                    // note: clicked in imported and when template is clicked
+                    // it will call clicked method passed in param.
+                }
+                
+                return {
+                    restrict: 'E',
+                    template: tmpl,
+                    link: mymethods,
+                    scope: {clicked : '='}
+                };
+            });
+})();
diff --git a/afm-client/app/Frontend/widgets/FormInput/FormInput.scss b/afm-client/app/Frontend/widgets/FormInput/FormInput.scss
new file mode 100644
index 0000000..528ddfd
--- /dev/null
+++ b/afm-client/app/Frontend/widgets/FormInput/FormInput.scss
@@ -0,0 +1,90 @@
+/*
+    Sample of style overload for a specific widget
+
+    Note: this SCSS is injected with main HTML page, it scope should be reduce
+    to a specific widget or it value will be propagated at a global level.
+*/
+
+@import "app/ibz-mixins";
+
+.upload-file {
+    @include ibz-button(grey,1rem)
+    float: right;
+    height  : 3rem;
+    margin: 0.5rem;
+
+    i { font-size: 2rem}
+    
+    .ibz-range-slider {
+        height: 10% !important;
+        border-radius: 5px;
+        background-color: lightgrey !important;
+    
+        .range-slider-handle {
+            width: 10% !important;
+            height: 100% !important;
+            margin-top: .2rem;
+            background-color: purple !important;
+        }
+
+        .range-slider-active-segment {
+            height: 80% !important;
+            background-color: lightgreen;
+        }
+    }
+
+}
+
+input-text {
+  
+    alert {@include ibz-input-alert(darkblue, rgba(200, 200, 200, 0.6))};
+
+    input {
+        margin-bottom: .5rem !important;
+    }
+
+    label {
+        margin-top: 1rem !important;
+    }
+    
+    .required {
+        color: blue;
+        float: right;
+        color: lightskyblue;
+    }
+    .required.valid {
+        color: green;
+    }
+    
+    .required.invalid {
+        color: plum;
+    }  
+
+    .status-untouch {
+        border-color: rgba(200, 200, 200, 0.6) !important;
+        box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 8px rgba(200, 200, 200, 0.6) !important;
+        color: #696969 !important;
+    }
+
+    input:focus {
+        border-color: rgba(82,168,236,0.8) ;
+        box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 8px rgba(82,168,236,0.8) !important;
+        transition: border 0.2s linear 0s, box-shadow 0.2s linear 0s;
+        color: darkslateblue !important;
+        @extend shadow-transition;
+    }
+
+    .status-valid {
+        border-color: rgba(154, 205, 50, 0.6)!important;
+        box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 8px rgba(154, 205, 50, 0.6)!important;
+        @extend shadow-transition;
+    }
+
+    .status-invalid {
+        border-color: rgba(154, 17, 69, 0.6);
+        box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 8px rgba(154, 17, 69, 0.6)!important;
+        color: rgb(154, 17, 69);
+        @extend shadow-transition;
+    }
+
+}
diff --git a/afm-client/app/Frontend/widgets/FormInput/InputPassword.js b/afm-client/app/Frontend/widgets/FormInput/InputPassword.js
new file mode 100644
index 0000000..157009c
--- /dev/null
+++ b/afm-client/app/Frontend/widgets/FormInput/InputPassword.js
@@ -0,0 +1,79 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+(function() {
+'use strict';
+
+var tmpl = '<input-text  class="password" tip="{{tip1}}"  placeholder="{{place1}}"' +
+           'label="{{label1}}" callback="valid1" name="{{name}}-1" value="pass1" required minlen="{{minlen}}" type="password" >' +
+           '</input-text>' + 
+           '<input-text  class="password" tip="tip2"  placeholder="{{place2}}"' +
+           'label="{{label2}}" callback="valid2" name="{{name}}-2" value="pass2" required minlen="{{minlen}}" type="password" > '+
+           '</input-text>';
+
+angular.module('InputPassword',[])
+
+.directive('inputPassword', function() {
+    function mymethods(scope, elem, attrs) {
+    
+    scope.valid1 = function (name, value) {
+        console.log ("Clicked InputPassword1 name=%s value=%s", name, value);        
+        scope.firstpwd = value;
+    };
+    
+    scope.valid2 = function (name, value, done) {        
+        console.log ("Clicked InputPassword2 name=%s value=%s", name, value);        
+        
+        // if both passwd equal then call form CB
+        if (scope.firstpwd !== value) {
+          done({valid: false, status: 'invalid', errmsg: "both password should match"});  
+        } else {  
+          scope.callback (attrs.name, value);
+        }
+                  
+     };
+     
+     // this method can be called from controller to update widget status
+     scope.done=function (data) {
+       console.log ("Text-Input Callback ID="+ attrs.name + " data=", data);
+       for (var i in data) scope[i] = data[i];         
+     };
+     
+     // Export some attributes within directive scope for template
+     scope.name   = attrs.name;
+     scope.label1 = attrs.label || 'Password';
+     scope.label2 = attrs.label || 'Password Verification';
+     scope.place1 = attrs.placeholder1 || 'User Password';
+     scope.tip1   = attrs.tip || 'Choose a Password';
+     scope.place2 = attrs.placeholder1 || 'Password Verification';
+     scope.tip2   = attrs.tip    || 'Confirme your Password';
+     scope.minlen = attrs.minlen || 10;
+     
+     if ("required" in attrs) scope.required = 'required';
+         
+    }
+    
+    return {
+        restrict: 'E',
+        template: tmpl,
+        link: mymethods,
+        scope: {
+            callback : '=',
+        }
+    };
+});
+
+console.log ("InputPassword Loaded");
+})();
diff --git a/afm-client/app/Frontend/widgets/FormInput/InputText.js b/afm-client/app/Frontend/widgets/FormInput/InputText.js
new file mode 100644
index 0000000..2653175
--- /dev/null
+++ b/afm-client/app/Frontend/widgets/FormInput/InputText.js
@@ -0,0 +1,179 @@
+
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details..
+ */
+
+
+
+(function() {
+'use strict';
+
+var tmpl = '<tip-modal tip="tip"></tip-modal>' +
+           '<label for="{{name}}-intext">{{label}} <i ng-show="required" ng-click="ToBeDefined" ' +
+           'class="required {{status}} fi-checkbox" title="Free Value But Mandatory Argument" alt="?"> &nbsp; </i></label>'+          
+           '<input '+
+           ' type="{{type}}" id="{{name}}-intext" placeholder="{{placeholder}}"  class="status-{{status}}"'+
+           ' ng-model="value" ng-blur="validate()" ng-focus="selected()" '+
+           ' ng-model-options="{ updateOn: \'default blur\', debounce: {default: 500, blur: 0} }"' +
+           '><alert data-ng-show="!valid&&errmsg">{{errmsg}}</alert>';
+
+var emailpatern = /^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
+
+angular.module('InputText',['JQueryEmu'])
+
+.directive('inputText', function(JQemu) {
+    function mymethods(scope, elem, attrs) {
+    
+    // default value at 1st rendering
+    scope.error  = false;
+    scope.valid  = false;
+    scope.status = 'untouch';
+   
+    scope.input = elem.find ("input");
+    scope.required = 0;
+    
+    // requirer is use to increment requested counter
+    if ("required" in attrs) {
+        scope.required = 1;
+        elem.addClass ("required");
+    }
+       
+     // user enter input reset error status
+     scope.selected = function () {
+        scope.error=false; 
+        scope.errmsg=false; 
+        scope.status = 'touch';
+     };   
+            
+     scope.validate = function () {
+         
+         // get value from input field bypassing Angular ng-model
+         console.log ("Clicked InputText name=%s value=%s valid=%s", scope.name, scope.value, scope.valid);        
+
+         // form is not untouched anymore
+         scope.parent.removeClass ("ng-pristine");
+
+         // if value not null clean up string
+         if (scope.value) {
+             scope.error=false; 
+            // remove leading and trailling space
+            scope.value = scope.value.trim();
+         
+            // remove any space is not allowed
+            if ('nospace' in attrs) {
+               scope.value=scope.value.replace(/\s/g, '');    
+            }
+         
+            if ('lowercase' in attrs) {
+               scope.value = scope.value.toLowerCase();
+            }
+         
+            // check minimum lenght
+            if ("minlen" in attrs) {
+              if (scope.value.length < attrs.minlen) {
+                 scope.status='invalid';
+                 scope.errmsg=scope.name + ': Mininum Lengh= ' + attrs.minlen + ' Characters';
+                 scope.error=true;
+              }
+            }
+            
+            if ('email' in attrs) {
+            if (!emailpatern.test (scope.value)) {
+                scope.status='invalid';
+                scope.errmsg='invalid email address';
+                scope.error=true;
+            }
+         }
+         
+        } else {
+            if (scope.required) {
+                 scope.status='invalid';
+                 scope.errmsg=scope.name + ': Required Attribute';
+                 scope.error=true; 
+            }
+        }
+                           
+         // If local control fail let's refuse input
+         if (scope.error) {
+             if (scope.required && scope.valid) {
+                 scope.valid = false;
+                 if (scope.l4acounter.validated > 0) scope.l4acounter.validated --;
+             } 
+             // use call to update form scope on form completeness
+             scope.callback (attrs.name, null, scope.done);
+         } else { 
+             // localcheck is OK backup may nevertheless change status to false
+            if (scope.required  && !scope.valid) scope.l4acounter.validated ++;
+            scope.status='valid';
+            scope.valid=true;
+            scope.callback (attrs.name, scope.value, scope.done);
+         }
+          
+     };
+     
+     // this method can be called from controller to update widget status
+     scope.done=function (data) {
+       console.log ("Text-Input Callback ID="+ attrs.name + " data=", data);
+       for (var i in data) scope[i] = data[i];         
+     };
+     
+     // Export some attributes within directive scope for template
+     scope.label       = attrs.label;
+     scope.name        = attrs.name;
+     scope.placeholder = attrs.placeholder;
+     scope.type        = attrs.type || "text";
+     scope.tip         = attrs.tip;
+
+     // search for form within parent elemnts
+     scope.parent = JQemu.parent (elem, "FORM");
+
+     // email enforce lowercase and nospace   
+     if ("email" in attrs) {
+        attrs.lowercase=true; 
+        attrs.nospace=true; 
+        attrs.minlen=6; 
+     }
+
+     if (scope.required) {
+         scope.l4acounter = scope.parent.data ("l4acounter");
+         if (!scope.l4acounter) { 
+            scope.l4acounter =  {required:1, validated:0};
+            console.log("Field "+scope.name+" is required (1st)");
+            scope.parent.data ("l4acounter", scope.l4acounter); 
+         } else {
+             console.log("Field "+scope.name+" is required");
+             scope.l4acounter.required ++;
+         }
+     }
+         
+     // refresh validation each time controler update value
+     scope.$watch ('value', function(){
+         if(scope.value) scope.validate(); }
+     );
+    
+    }
+    
+    return {
+        restrict: 'E',
+        template: tmpl,
+        link: mymethods,
+        scope: {
+            callback : '=',
+            value: '='
+        }
+    };
+});
+
+console.log ("InputText Loaded");
+})();
diff --git a/afm-client/app/Frontend/widgets/FormInput/UploadAppli.js b/afm-client/app/Frontend/widgets/FormInput/UploadAppli.js
new file mode 100644
index 0000000..d18f620
--- /dev/null
+++ b/afm-client/app/Frontend/widgets/FormInput/UploadAppli.js
@@ -0,0 +1,230 @@
+
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details..
+ * 
+ * Reference:
+ *   https://developer.mozilla.org/en/docs/Web/API/FileReader 
+ *   https://developer.mozilla.org/en-US/docs/Using_files_from_web_applications#Using_hidden_file_input_elements_using_the_click%28%29_method
+ *   https://uncorkedstudios.com/blog/multipartformdata-file-upload-with-angularjs
+ *   https://www.terlici.com/2015/05/16/uploading-files-locally.html
+ *   https://github.com/nervgh/angular-file-upload/blob/master/src/services/FileUploader.js
+ *   https://stuk.github.io/jszip/documentation/howto/read_zip.html
+ *   http://onehungrymind.com/zip-parsing-jszip-angular/
+ *   http://stackoverflow.com/questions/15341912/how-to-go-from-blob-to-arraybuffer
+ *   
+ *   Bugs: zip file sent even when flag as invalid 
+ */
+
+ 
+
+(function() {
+'use strict';
+
+var tmplAppli = '<input type="file" name="{{name}}-input" onchange="angular.element(this).scope().UpLoadFile(this.files)" accept="{{mimetype}}" style="display:none">'+
+            '<div class="upload-file" ng-click="imgClicked()">' +
+            '<i class="{{icon}}"></i> <span>{{label}}</span>' +
+            '<range-slider ng-show="!noslider" id="{{name}}-slider" automatic=true inithook="SliderInitCB"></range-slider>' +
+            '</div>';
+    
+var tmplModal = '<span class="modal-text">Upload Application <b>{{appname}}</b> ?</span>' +
+            '<div>'+
+            '<img ng-src="{{appicon}}">' +
+            '<submit-button icon="fi-x" label="Cancel" clicked="refused"></submit-button>'+
+            '<submit-button icon="fi-like" label="Install" clicked="accepted"></submit-button> ' +
+            '</div>';
+    
+
+// Service Create xform insert files in and Post it to url
+function LoadFileSvc (scope, files, fileCB) {
+    var xmlReq = new XMLHttpRequest();
+    var xform  = new FormData();
+    
+    // Update slider during Upload
+    xmlReq.upload.onprogress = function (event) {
+        var progress = Math.round(event.lengthComputable ? event.loaded * 100 / event.total : 0);
+        if (scope.slider) scope.slider.setValue (progress);
+    };
+
+    // Upload is finish let's notify controler callback
+    xmlReq.onload = function () {
+        scope.divElem.addClass ("success");
+        scope.divElem.removeClass ("error");
+        var response ={
+            status : xmlReq.status,
+            headers: xmlReq.getAllResponseHeaders() 
+        };
+        scope.callback (response);
+    };
+
+    xmlReq.onerror = function () {
+        scope.divElem.addClass ("error");
+        scope.divElem.removeClass ("success");
+    };
+
+    xmlReq.onabort = function () {
+        scope.divElem.addClass ("error");
+        scope.divElem.removeClass ("success");
+        var response ={
+            status : xmlReq.status,
+            headers: xmlReq.getAllResponseHeaders() 
+        };
+        scope.callback (response);
+    };
+    
+    this.postfile = function(posturl) { 
+        // everything looks OK let's Post it
+        xmlReq.open("POST", posturl , true);
+        xmlReq.send(xform);
+    };
+
+    for (var i = 0; i < files.length; i++) {
+        this.file = files[i];
+        console.log ("filetype=%s",this.file.type );
+        // Unknow Type !!! if (!this.file.type.match(scope.mimetype)) continue;
+
+        console.log ("Selected file=" + this.file.name + " size="+ this.file.size/1024 + " Type="+ this.file.type);
+
+        // File to upload is too big
+        if (this.file.size > scope.maxsize*1024) {
+            scope.thumbnail = scope.istoobig; // warning if image path is wrong nothing happen
+            scope.$apply('thumbnail'); // we short-circuit Angular resync Image
+            return;
+        }
+
+        // This is not an uploadable file
+        if(isNaN(this.file.size)) {
+            scope.thumbnail = scope.isnotvalid; 
+            scope.$apply('thumbnail');
+            return;
+        }
+
+        this.basename= this.file.name.split('/').reverse()[0];
+        //scope.imgElem[0].file = this.file;
+
+        // If File is an image let display it now
+        if (fileCB) {
+            var reader = new FileReader();
+            reader.readAsArrayBuffer(this.file);
+            reader.onload = fileCB;
+        } 
+        // if everything is OK let's add file to xform
+        xform.append(scope.name, this.file, this.file.name);
+    }
+
+}
+
+angular.module('UploadFiles',['AppConfig', 'ModalNotification', 'RangeSlider'])
+
+.directive('uploadAppli', function(AppConfig,  JQemu, Notification, ModalFactory, $timeout) {
+    function mymethods(scope, elem, attrs) {
+        
+        // get widget image handle from template
+        scope.inputElem  = elem.find('input');
+        scope.divElem    = elem.find('div');
+        
+        // Image was ckick let's simulate an input (file) click
+        scope.imgClicked = function () {
+            scope.inputElem[0].click(); // Warning Angular TriggerEvent does not work!!!
+        };
+        
+        // Slider control handle registration after creation
+        scope.SliderInitCB=function (slider) {
+           scope.slider= slider; 
+        };
+        
+        // Upload is delegated to a shared function
+        scope.UpLoadFile=function (files) {
+            var handle; 
+            var appicon;
+            
+            var accepted = function() {
+                console.log ("Modal Accepted");
+                // This Looks OK let's Post Xform/File
+                handle.postfile(attrs.posturl + "?token=" + AppConfig.session.token);
+
+                scope.modal.deactivate();
+                $timeout (function() {scope.modal.destroy();}, 1000);
+            };
+            
+            var refused = function() {
+                console.log ("Modal Refused");
+                scope.modal.deactivate();
+                $timeout (function() {scope.modal.destroy();}, 1000);
+            };
+                       
+            var readerCB = function (upload) {
+
+                var zipapp = new JSZip (upload.target.result);
+                var thumbnail = zipapp.file("icon_128.png");
+                
+                // Check is we have a thumbnail within loaded Zipfile
+                if (!thumbnail) {
+                    console.log ("This is not a valid Application Framework APP");
+                    scope.thumbnail=AppConfig.paths[scope.category] + 'isnotvalid.png';
+                    scope.$apply('thumbnail'); // we short-circuit Angular resync Image
+                } else {
+                    //scope.imgElem[0].src = window.URL.createObjectURL(new Blob([thumbnail.asArrayBuffer()], {type: "image"}));
+                    appicon = window.URL.createObjectURL(new Blob([thumbnail.asArrayBuffer()], {type: "image"}));
+                    
+                    // reference http://foundation.zurb.com/apps/docs/#!/angular-modules
+                    var config = {
+                        animationIn: 'slideInFromTop',
+                        contentScope: {
+                            accepted: accepted,
+                            refused:  refused,
+                            appicon:  appicon,
+                            appname:  handle.basename
+                        }, template:  tmplModal
+                    }; 
+                    // Popup Modal to render application data
+                    scope.modal = new ModalFactory(config);
+                    scope.modal.activate ();
+                }
+            };
+            
+            // Load file within browser and if OK call readerCB
+            handle = new LoadFileSvc (scope, files, readerCB);
+        };
+
+        // Initiallize default values from attributes values
+        scope.name= attrs.name || 'appli';
+        scope.category= attrs.category  || 'appli';
+        scope.mimetype= (attrs.accept || '.wgt');
+        scope.maxsize = attrs.maxsize || 100000; // default max size 100MB
+        scope.regexp  = new RegExp (attrs.accept+ '.*','i');
+        scope.icon    = attrs.icon || 'fi-upload';
+        scope.label   = attrs.label || 'Upload';
+        
+        if (attrs.thumbnail) scope.isnotvalid= AppConfig.paths[scope.category] +  attrs.isnotvalid;
+        else  scope.isnotvalid=AppConfig.paths[scope.category] + 'isnotvalid.png';
+
+        if (attrs.istoobig) scope.istoobig= AppConfig.paths[scope.category] +  attrs.istoobig;
+        else  scope.istoobig=AppConfig.paths[scope.category] + 'istoobig.png';
+        scope.noslider = attrs.noslider || false;
+
+        if (!attrs.posturl) throw new TypeError('file-upload %s posturl=/api/xxxx/xxxx required', scope.attrs);            
+    }
+    return {
+        restrict: 'E',
+        template: tmplAppli,
+        link: mymethods,
+        scope: {
+            callback : '='
+        }
+    };
+    
+});
+
+console.log ("UploadFile Loaded");
+})();
diff --git a/afm-client/app/Frontend/widgets/Navigation/LinkButton.js b/afm-client/app/Frontend/widgets/Navigation/LinkButton.js
new file mode 100644
index 0000000..3e83425
--- /dev/null
+++ b/afm-client/app/Frontend/widgets/Navigation/LinkButton.js
@@ -0,0 +1,57 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * Bugs: Input with Callback SHOULD BE get 'required' class
+ */
+
+(function () {
+    'use strict';
+
+    var tmpl = '<span title="Goto: {{href}}" ng-click="clicked()">' +
+            '<i class="{{icon}}"></i>' +
+            '<span>{{label}}</span>' +
+            '</span>';
+
+
+    angular.module('LinkButton', [])
+            .directive('linkButton', function ($location) {
+                
+                function mymethods(scope, elem, attrs) {
+
+                    scope.clicked = function () {
+                        
+                        if (!attrs.query) $location.path(attrs.href);
+                        else $location.path(attrs.href).search(attrs.query);
+                    };
+
+                    // ajust icon or use default
+                    scope.icon = attrs.icon   || 'fi-link';
+                    scope.label = attrs.label || 'Jump';
+                    scope.href  = attrs.href  || '/home';
+                    
+                    // add label as class
+                    elem.addClass (scope.label.toLowerCase());
+                }
+
+                return {
+                    restrict: 'E',
+                    template: tmpl,
+                    link: mymethods,
+                    scope: {}
+                };
+            });
+})();
diff --git a/afm-client/app/Frontend/widgets/Navigation/Navigation.scss b/afm-client/app/Frontend/widgets/Navigation/Navigation.scss
new file mode 100644
index 0000000..2babf24
--- /dev/null
+++ b/afm-client/app/Frontend/widgets/Navigation/Navigation.scss
@@ -0,0 +1,26 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+@import "app/ibz-mixins";
+
+
+link-button {@include ibz-button(#3366ff,1rem)};
+
+//pale blue for secondaty link
+link-button.secondary {@include ibz-button(#99b3ff,1rem)};
+
diff --git a/afm-client/app/Frontend/widgets/Notifications/ModalNotification.js b/afm-client/app/Frontend/widgets/Notifications/ModalNotification.js
new file mode 100644
index 0000000..37ba047
--- /dev/null
+++ b/afm-client/app/Frontend/widgets/Notifications/ModalNotification.js
@@ -0,0 +1,85 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * Bugs: Input with Callback SHOULD BE get 'required' class
+ * 
+ * ref: https://developer.mozilla.org/en-US/docs/Web/Events/mouseover
+ * 
+ * usage: 
+ * 
+ * tipModal: listen event from elem.parent() to display tip-modal
+ *      <div class="xxxx">
+ *          <tip-modal tip=xxxx></tip-modal>
+ *          <input-text ....></input-text>
+ *      </div>
+ *      
+ * Note: use CSS.visibility to avoid display flickering at initial display.
+ */
+
+(function () {
+    'use strict';
+
+    var tmpl = '<div class="tip-modal-popup">' +
+            '<i class="{{icon}}"></i>' +
+            '<span>{{tip}}</span>' +
+            '</span></div>' ;
+
+    angular.module('ModalNotification', [])
+            .directive('tipModal', function ($timeout) {
+
+                function mymethods(scope, elem, attrs) {
+                    scope.parent = elem.parent();
+                    scope.modal    = elem.find("div");
+                    
+    
+                    // delay tip display to avoid blinking when moving mouse fast
+                    function display () {
+                        function action() {
+                             if (scope.show) scope.modal.css({opacity: 1, visibility:'visible'});  
+                        }
+                        scope.show = true;
+                        scope.timeout = $timeout(action, scope.delay);
+                    }
+                    
+                    function close () {
+                      scope.show = false;                     
+                      scope.modal.css({opacity: 0, visibility:'hidden'});
+                    }
+                    
+
+                    // ajust icon or use default
+                    scope.icon  = attrs.icon || 'fi-lightbulb';
+                    
+                    // Update Parent element to get mouse event
+                    scope.parent.addClass ('as-modal-tip');
+                    scope.parent.bind('click', close);
+                    scope.parent.bind('focus', display);
+                    scope.parent.bind('mouseover', display);
+                    scope.parent.bind('mouseleave', close);
+                    scope.parent.bind('blur', close);
+                    
+                    scope.delay = attrs.delay || 1000; // wait 1s before displaying tip
+                }
+
+                return {
+                    restrict: 'E',
+                    template: tmpl,
+                    link: mymethods,
+                    scope: {tip: "="} // tip may not be defined when widget is display
+                };
+            });
+})();
diff --git a/afm-client/app/Frontend/widgets/Notifications/Notifications.scss b/afm-client/app/Frontend/widgets/Notifications/Notifications.scss
new file mode 100644
index 0000000..fb740b7
--- /dev/null
+++ b/afm-client/app/Frontend/widgets/Notifications/Notifications.scss
@@ -0,0 +1,63 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * Reference: http://www.greywyvern.com/?post=337
+ */
+
+@import "app/ibz-mixins";
+
+
+link-button {@include ibz-button(#3366ff,1rem)};
+
+// Modal should be relative and tip-modal-popup absolute
+tip-modal {
+    position:relative;
+}
+
+.tip-modal-popup {
+    //visibility: hidden;
+    width: 20rem;
+    position:absolute;
+    top:1em;
+    padding: 0.2em 0.6em;
+    border:1px solid #996633;
+    background-color:#e5ffff;
+    color:#000;
+    opacity:0;
+    transition:visibility .5s linear 1s,opacity 1s linear;
+    border-radius: 5px;
+    i {
+        margin: 0 .3rem 0 0;
+        display: inline;
+    }
+}
+
+token-refresh {
+    @include ibz-button(grey,1rem)
+    i {margin-left: .5rem;}
+    margin-right: 1rem;
+}
+
+token-refresh.online {
+    color: #0066cc;
+    i {color: lime;}    
+}
+
+token-refresh.offline {
+    color: #ff00ff;
+    i {color: red;}    
+}
diff --git a/afm-client/app/Frontend/widgets/Notifications/TokenRefreshSvc.js b/afm-client/app/Frontend/widgets/Notifications/TokenRefreshSvc.js
new file mode 100644
index 0000000..5c5b5ae
--- /dev/null
+++ b/afm-client/app/Frontend/widgets/Notifications/TokenRefreshSvc.js
@@ -0,0 +1,131 @@
+/*
+ alsa-gateway -- provide a REST/HTTP interface to ALSA-Mixer
+
+ Copyright (C) 2015, Fulup Ar Foll
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with scope program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+ References:
+
+ */
+
+(function () {
+    'use strict';
+
+    var template =
+          '<div class="afb-monitor" ng-click="getping()">' +
+         '<span class="afb-refresh-token"  >afb://{{hostname}}:{{httpdport}}</span>' +
+         '<i class="{{icon}}"></i>' +
+         '</div>';
+
+
+// scope module is load statically before any route is cativated
+angular.module('TokenRefresh', ['AppConfig', 'ModalNotification'])
+
+    .directive ('tokenRefresh', function($timeout, $http, $location, Notification, AppConfig) {
+
+    function mymethods(scope, elem, attrs) {
+        scope.status=undefined; // neither thu neither false
+        
+    
+        scope.online = function () {
+            elem.addClass    ("online");
+            elem.removeClass ("offline");
+        };
+
+        scope.offline = function(){
+            elem.addClass    ("offline");
+            elem.removeClass ("online");
+        };
+        
+        scope.onerror = function(data, errcode, headers) {
+            if (scope.status !== false)  {
+                Notification.warning ({message: "AppFramework Binder Lost", delay: 5000});
+                scope.offline();
+            }
+            scope.status = 0;
+        };
+        
+        scope.onsuccess = function(data, errcode, headers, config) {
+            if (scope.status !== true)  {
+                if (data.request.token) AppConfig.session.token = data.request.token;
+                if (data.request.uuid)  AppConfig.session.uuid  = data.request.uuid;
+                if (data.request.timeout)  AppConfig.session.timeout  = data.request.timeout;
+
+                Notification.success ({message: "AppFramework Binder Back to Live", delay: 3000});
+                scope.online();
+                if (scope.callback) scope.callback();
+            }
+            scope.status = 1;
+        };
+
+        // Check Binder status
+        scope.getping = function() {
+
+            var handler = $http.get(AppConfig.session.ping+'?token='+ AppConfig.session.token);
+            
+            // process success and error
+            handler.success(scope.onsuccess);
+            handler.error(scope.onerror);
+
+            // restart a new timer for next ping
+            $timeout (scope.getping, AppConfig.session.pingrate*1000);
+        };
+        
+        // Check Binder status
+        scope.refresh = function() {
+            var handler = $http.get(AppConfig.session.refresh+'?token='+ AppConfig.session.token);
+            
+            // process success and error
+            handler.success(scope.onsuccess);
+            handler.error(scope.onerror);
+            // restart a new timer for next refresh to 1/4 of timeout session
+            $timeout (scope.refresh, AppConfig.session.timeout *250);
+        };
+        
+        // Initial connection
+        scope.tkcreate = function() {
+            var handler = $http.get(AppConfig.session.create+'?token='+ AppConfig.session.initial);
+            
+            // process success and error
+            handler.success(scope.onsuccess);
+            handler.error(scope.onerror);
+        };
+ 
+        scope.icon      = attrs.icon   || "fi-lightbulb";
+        scope.hostname  = $location.host();
+        scope.httpdport = $location.port();
+        scope.autolog   = JSON.parse(attrs.autolog || false);
+        
+        if (scope.autolog) scope.tkcreate();
+
+        // Init ping and refresh process
+        $timeout (scope.getping, AppConfig.session.pingrate*1000);
+        $timeout (scope.refresh, AppConfig.session.timeout *250);
+    }
+
+    return {
+        template: template,
+        scope: {
+            callback : "="
+        },
+        restrict: 'E',
+        link: mymethods
+    };
+});
+
+})();
+console.log ("Token Refresh Loaded");
+
diff --git a/afm-client/app/Frontend/widgets/RangeSliders/RangeSliderMod.js b/afm-client/app/Frontend/widgets/RangeSliders/RangeSliderMod.js
new file mode 100644
index 0000000..77f0fce
--- /dev/null
+++ b/afm-client/app/Frontend/widgets/RangeSliders/RangeSliderMod.js
@@ -0,0 +1,631 @@
+/* 
+ * Copyright (C) 2015 "IoT.bzh"
+ * Author "Fulup Ar Foll"
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * Bugs: Input with Callback SHOULD BE get 'required' class
+ * 
+ * ref: https://developer.mozilla.org/en-US/docs/Web/Events/mouseover
+ * 
+ * usage: 
+Usage  <range-slider>
+---------------------
+   <range-slider
+      id="my-slider-name"                     // only use as an argument to callback
+      class="my-custom-class"                 // default class is ibz-range-slider
+      placeholder="Track Date Selection"      // place holder for date readonly input zone
+
+      <!-- Foundation classes -->
+      class="radius"                          // check Zurb foundation doc for further info.
+      class="ibz-handle-display"              // increase handle width to hold slider current value
+
+      <!-- Angular Scope Variables -->
+      callback="myCallBack"                    // $scope.myCallBack(sliderhandle) is called when ever slider handle blur
+      formatter="SliderFormatCB"               // $scope.myFormatter(value, sliderid) when exist is call when ever slider handle moves. Should return external form of slider value.
+      ng-model="xxxxxx"                        // xxx Must be defined, script will store a new RangerObject within provided ng-model variable.
+      start-at="ScopeVar"                      // Dynamic limitation when slider is constrains by an external componant [ex: check in/out]
+      stop-at="ScopeVar"                       // Idem but for end.
+
+      <!-- Angular Directive Attributes -->
+      not-less="integer"                       // Fixed starting value for slider [default 0]
+      not-more="integer"                       // Fixed end value for sliders [default 100]
+      by-step="+-integer"                      // If by-step is >0 then slider use it as step-value, when negative use it for decimal precision
+      display-target="handle"                  // display slider external formated value in the handle [requirer calss="ibz-handle-display"]
+      dual-handles='true'                      // add a second handle to slider for min/max range
+      initial='value|[start/stop]'             // slider initial value [dual-handles] may have initial values
+   /></range-slider>
+ */
+
+(function () {
+    'use strict';
+
+var RangeSlider = angular.module('RangeSlider',[]);
+
+function RangeSliderHandle (scope) {
+    var internals = [];
+    var externals = [];
+
+    this.getId = function() {
+        return scope.sliderid;
+    };
+
+    this.getCbHandle = function() {
+        return scope.cbhandle;
+    };
+
+    this.getView= function (handle) {
+        if (!handle) handle = 0;
+
+        // if value did not change return current external representation
+        if (scope.value[handle] === internals[handle]) return externals[handle];
+
+        // build external representation and save it for further requests
+        internals[handle] = scope.value[handle];
+        if (scope.formatter) externals[handle] = scope.formatter(scope.value[handle], scope.ctrlhandle);
+        else  externals[handle] = scope.value[handle];
+
+        return externals[handle];
+    };
+
+    this.updateClass = function (classe, status) {
+       scope.updateClass (classe, status);
+    };
+
+    this.forceRefresh = function (timer) {
+       scope.forceRefresh(timer);
+    };
+
+    this.getValue= function (handle) {
+        if (!handle) handle = 0;
+        return scope.value[handle];
+    };
+
+    this.getRelative= function (handle) {
+        if (!handle) handle = 0;
+        return scope.relative[handle];
+    };
+
+    this.setValue= function (value, handle) {
+        if (!handle) handle = 0;
+        scope.setValue (value, handle);
+    };
+
+    this.setDisable= function (flag) {
+        scope.setDisable(flag);
+    };
+}
+
+RangeSlider.directive('rangeSlider', function ($log, $document, $timeout) {
+
+    var template= '<div class="ibz-range-slider range-slider" title="{{title}}"data-slider>'+
+                  '<span class="range-slider-handle handle-min" ng-mousedown="handleCB($event,0)" ng-focus="focusCB(true)" ng-blur="focusCB(false)" role="slider" tabindex="0"></span>'+
+                  '<span class="handle-max" ng-mousedown="handleCB($event,1)" ng-focus="focusCB(true)" ng-blur="focusCB(false)" role="slider" tabindex="0"></span>'+
+                  '<span class="range-slider-active-segment"></span>'+
+                  '<span class="ibz-range-slider-start" ></span> '+
+                  '<span class="ibz-range-slider-stop"></span> '+
+                  '<input id={{sliderid}} type="hidden">'+
+                  '</div>';
+
+
+    function link (scope, element, attrs, model) {
+        // full initialisation of slider from a single object
+        scope.initWidget = function (initvalues) {
+
+            if (initvalues.byStep)  scope.byStep  = parseInt(initvalues.byStep);
+            if (initvalues.notMore) scope.notMore = parseInt(initvalues.notMore);
+            if (initvalues.notLess) scope.notLess = parseInt(initvalues.notLess);
+            if (initvalues.id)      scope.sliderid= initvalues.id;
+
+            // hugely but in some case DOM is not finish when we try to set values !!!
+            if (initvalues.value !== undefined)   {
+                scope.value = initvalues.value;
+                scope.forceRefresh (50); // wait 50ms for DOM to be ready
+            }
+        };
+
+        // this function recompute slide positioning
+        scope.forceRefresh = function (timer) {
+           var value = scope.value;
+           scope.value = [undefined,undefined];
+           $timeout (function() {
+               scope.setValue(value[0],0);
+               if (scope.dual)  scope.setValue(value[1],1);
+           }, timer);
+        };
+
+        // handler to change class from slider handle
+        scope.updateClass = function (classe, status) {
+
+            if (status) element.addClass (classe);
+            else  element.removeClass (classe);
+        };
+
+        scope.setDisable = function (disabled) {
+
+            if (disabled) {
+                element.addClass ("disable");
+                scope.handles[0].css ('visibility','hidden');
+                if (scope.dual) {
+                    scope.handles[1].css ('visibility','hidden');
+                }
+            } else {
+                element.removeClass ("disable");
+                scope.handles[0].css ('visibility','visible');
+                if (scope.dual) scope.handles[1].css ('visibility','visible');
+            }
+
+        };
+
+        scope.normalize = function (value) {
+            var result;
+            var range = scope.notMore - scope.notLess;
+            var point = value * range;
+
+            // if step is positive let's round step by step
+            if (scope.byStep >  0) {
+                var mod = (point - (point % scope.byStep)) / scope.byStep;
+                var rem = point % scope.byStep;
+
+                var round = (rem >= scope.byStep * 0.5 ? scope.byStep : 0);
+                result= (mod * scope.byStep + round) + scope.notLess;
+                //console.log ("range=%d value=%d point=%d mod=%d rem=%d round=%d result=%d", range, value, point, mod, rem, round, result)
+                return result;
+            }
+
+            // if step is negative return round to asked decimal
+            if (scope.byStep <  0) {
+                var power  =  Math.pow (10,(scope.byStep * -1));
+                result = scope.notLess + parseInt (point * power) / power;
+                return (result);
+            }
+
+            // if step is null return full value
+            return point;
+       };
+
+        // return current value
+        scope.getValue = function (offset, handle) {
+            if (scope.vertical) {
+                scope.relative[handle] = (offset - scope.bounds.handles[handle].getBoundingClientRect().height) / (scope.bounds.bar.getBoundingClientRect().height - scope.bounds.handles[handle].getBoundingClientRect().height);
+            } else {
+                scope.relative[handle] = offset /  (scope.bounds.bar.getBoundingClientRect().width - scope.bounds.handles[handle].getBoundingClientRect().width);
+            }
+
+            var newvalue = scope.normalize (scope.relative[handle]);
+
+
+            // if internal value change update or model
+            if (newvalue !== scope.value[handle]) {
+                if (newvalue < scope.startValue) newvalue=scope.startValue;
+                if (newvalue > scope.stopValue)  newvalue=scope.stopValue;
+
+
+                if (scope.formatter) {
+                    scope.viewValue = scope.formatter (newvalue, scope.ctrlhandle);
+                } else {
+                    scope.viewValue = newvalue;
+                }
+                if (scope.displays[handle]) {
+                    scope.displays[handle].html (scope.viewValue);
+                }
+
+                // update external representation of the model
+                scope.value[handle] = newvalue;
+                if (model) model.$setViewValue (scope.viewValue);
+                scope.$apply();
+                if (newvalue > scope.startValue && newvalue < scope.stopValue) scope.translate(offset, handle);
+            }
+        };
+
+
+        scope.setStart = function (value) {
+            var offset;
+            
+            if (value > scope.value[0]) {
+                if (!scope.dual) scope.setValue (value,0);
+                else scope.setValue (value,1);
+            }
+
+            if (scope.vertical) {
+                offset = scope.bounds.bar.getBoundingClientRect().height * (value - scope.notLess) / (scope.notMore - scope.notLess);
+                scope.start.css('height',offset + 'px');
+            } else {
+                offset = scope.bounds.bar.getBoundingClientRect().width * (value - scope.notLess) / (scope.notMore - scope.notLess);
+                scope.start.css('width',offset + 'px');
+            }
+
+            scope.startValue= value;
+        };
+
+        scope.setStop = function (value) {
+            var offset;
+            
+            if (value < scope.value[0]) {
+                if (!scope.dual) scope.setValue (value,0);
+                else scope.setValue (value,1);
+            }
+
+            if (scope.vertical) {
+                offset = scope.bounds.bar.getBoundingClientRect().height * (value - scope.notLess) / (scope.notMore - scope.notLess);
+                scope.start.css('height',offset + 'px');
+            } else {
+                offset = scope.bounds.bar.getBoundingClientRect().width * (value - scope.notLess) / (scope.notMore - scope.notLess);
+                scope.stop.css({'right': 0, 'width': (scope.bounds.bar.getBoundingClientRect().width  - offset) + 'px'});
+            }
+
+            scope.stopValue= value;
+        };
+
+        scope.translate = function (offset, handle) {
+            var start;
+            
+            if (scope.vertical) {
+                // take handle size in account to compute middle
+                var voffset = scope.bounds.bar.getBoundingClientRect().height - offset;
+
+                scope.handles[handle].css({
+                    '-webkit-transform': 'translateY(' + voffset + 'px)',
+                    '-moz-transform': 'translateY(' + voffset + 'px)',
+                    '-ms-transform': 'translateY(' + voffset + 'px)',
+                    '-o-transform': 'translateY(' + voffset + 'px)',
+                    'transform': 'translateY(' + voffset + 'px)'
+               });
+               if (!scope.dual) scope.slider.css('height', offset + 'px');
+               else if (scope.relative[1] && scope.relative[0]) {
+                   var height = (scope.relative[1] - scope.relative[0]) *  scope.bounds.bar.getBoundingClientRect().height;
+                   start  = (scope.relative[0] *  scope.bounds.bar.getBoundingClientRect().height);
+                   scope.slider.css ({'bottom': start+'px','height': height + 'px'});
+               }
+            } else {
+
+                scope.handles[handle].css({
+                    '-webkit-transform': 'translateX(' + offset + 'px)',
+                    '-moz-transform': 'translateX(' + offset + 'px)',
+                    '-ms-transform': 'translateX(' + offset + 'px)',
+                    '-o-transform': 'translateX(' + offset + 'px)',
+                    'transform': 'translateX(' + offset + 'px)'
+                });
+                if (!scope.dual) scope.slider.css('width',offset + 'px');
+                else if (scope.relative[1] && scope.relative[0]) {
+                    var width = (scope.relative[1] - scope.relative[0]) *  scope.bounds.bar.getBoundingClientRect().width;
+                    start = (scope.relative[0] *  scope.bounds.bar.getBoundingClientRect().width);
+                    scope.slider.css ({'left': start+'px','width': width + 'px'});
+                }
+            }
+        };
+
+        // position handle on the bar depending a given value
+        scope.setValue = function (value , handle) {
+            var offset;
+
+            // if value did not change ignore
+            if (value === scope.value[handle]) return;
+            if (value === undefined)   value=0;
+            if (value > scope.notMore) value=scope.notMore;
+            if (value < scope.notLess) value=scope.notLess;
+
+            if (scope.vertical) {
+                scope.relative[handle] = (value - scope.notLess) / (scope.notMore - scope.notLess);
+                if (handle === 0) offset = (scope.relative[handle] * scope.bounds.bar.getBoundingClientRect().height) + scope.bounds.handles[handle].getBoundingClientRect().height/2;
+                if (handle === 1) offset = scope.relative[handle] * scope.bounds.bar.getBoundingClientRect().height;
+
+            } else {
+                scope.relative[handle] = (value - scope.notLess) / (scope.notMore - scope.notLess);
+                offset = scope.relative[handle] *  (scope.bounds.bar.getBoundingClientRect().width - scope.bounds.handles[handle].getBoundingClientRect().width);
+            }
+
+            scope.translate (offset,handle);
+            scope.value[handle] = value;
+
+            if (scope.formatter) {
+                // when call through setValue we do not pass cbHandle
+                scope.viewValue = scope.formatter (value, undefined);
+            } else {
+                scope.viewValue = value;
+            }
+
+            if (model) model.$setViewValue( scope.viewValue);
+
+            if (scope.displays[handle]) {
+                scope.displays[handle].html (scope.viewValue);
+            }
+        };
+
+
+        // Minimal keystroke handling to close picker with ESC [scope.actif is current handle index]
+        scope.keydown=  function(e){
+
+            switch(e.keyCode){
+                case 39: // Right
+                case 38: // up
+                     if (scope.byStep > 0) scope.$apply(scope.setValue ((scope.value[scope.actif]+scope.byStep), scope.actif));
+                     if (scope.byStep < 0) scope.$apply(scope.setValue ((scope.value[scope.actif]+(1 / Math.pow(10, scope.byStep*-1))),scope.actif));
+                     if (scope.callback)  scope.callback (scope.value[scope.actif], scope.ctrlhandle);
+                     break;
+                case 37: // left
+                case 40: // down
+                    if (scope.byStep > 0) scope.$apply(scope.setValue ((scope.value[scope.actif] - scope.byStep), scope.actif));
+                    if (scope.byStep < 0) scope.$apply(scope.setValue ((scope.value[scope.actif] - (1 / Math.pow(10, scope.byStep*-1))),scope.actif));
+                    if (scope.callback)  scope.callback (scope.value[scope.actif], scope.ctrlhandle);
+                    break;
+                case 27: // esc
+                    scope.handles[scope.actif][0].blur();
+            }
+        };
+
+        scope.moveHandle = function (handle, clientX, clientY) {
+            var offset;
+            if (scope.vertical) {
+                offset = scope.bounds.bar.getBoundingClientRect().bottom - clientY;
+                if (offset > scope.bounds.bar.getBoundingClientRect().height) offset = scope.bounds.bar.getBoundingClientRect().height;
+                if (offset < scope.bounds.handles[handle].getBoundingClientRect().height) offset = scope.bounds.handles[handle].getBoundingClientRect().height;
+            } else {
+                offset = clientX - scope.bounds.bar.getBoundingClientRect().left;
+
+                if (offset < 0) offset = 0;
+                if ((clientX + scope.bounds.handles[handle].getBoundingClientRect().width) > scope.bounds.bar.getBoundingClientRect().right) {
+                    offset = scope.bounds.bar.getBoundingClientRect().width - scope.bounds.handles[handle].getBoundingClientRect().width;
+                }
+            }
+
+            scope.getValue  (offset, handle);
+
+            // prevent dual handle to cross
+            if (scope.dual && scope.value [0] > scope.value[1]) {
+                if (handle === 0) scope.setValue (scope.value[0] , 1);
+                else scope.setValue(scope.value[1],0);
+            }
+        };
+
+
+        scope.focusCB = function (inside) {
+            if (inside) {
+                $document.on('keydown',scope.keydown);
+            } else {
+                $document.unbind('keydown',scope.keydown);
+            }
+        };
+
+        // bar was touch let move handle to this point
+        scope.touchBarCB = function (event) {
+            var handle=0;
+            var relative;
+            var touches = event.changedTouches;
+            var oldvalue = scope.value[handle];
+
+            event.preventDefault();
+
+            // if we have two handles select closest one from touch point
+            if (scope.dual) {
+                if (scope.vertical) relative = (touches[0].pageY - scope.bounds.bar.getBoundingClientRect().bottom) / scope.bounds.bar.getBoundingClientRect().height;
+                else relative= (touches[0].pageX - scope.bounds.bar.getBoundingClientRect().left) / scope.bounds.bar.getBoundingClientRect().width;
+
+                var distance0 = Math.abs(relative - scope.relative[0]);
+                var distance1 = Math.abs(relative - scope.relative[1]);
+                if (distance1 < distance0) handle=1;
+            }
+
+            // move handle to new place
+            scope.moveHandle (handle,touches[0].pageX, touches[0].pageY);
+            if (scope.callback && oldvalue !== scope.value[handle]) scope.callback (scope.value[handle], scope.ctrlhandle);
+        };
+
+        // handle was touch and drag
+        scope.touchHandleCB = function (touchevt, handle) {
+            var oldvalue = scope.value[handle];
+
+            touchevt.preventDefault();
+            $document.on('touchmove',touchmove);
+            $document.on('touchend' ,touchend);
+            element.unbind('touchstart', scope.touchBarCB);
+
+            function touchmove(event) {
+                event.preventDefault();
+                var touches = event.changedTouches;
+                for (var idx = 0; idx < touches.length; idx++) {
+                    scope.moveHandle (handle,touches[idx].pageX, touches[idx].pageY);
+                }
+            }
+
+            function touchend(event) {
+               $document.unbind('touchmove',touchmove);
+               $document.unbind('touchend' ,touchend);
+               element.on('touchstart', scope.touchBarCB);
+
+                // if value change notify application callback
+                if (scope.callback && oldvalue !== scope.value[handle]) scope.callback (scope.value[handle], scope.ctrlhandle);
+            }
+        };
+
+        scope.handleCB = function (clickevent, handle) {
+
+            if (attrs.automatic) return;
+            
+            var oldvalue = scope.value[handle];
+            // register mouse event to track handle
+            clickevent.preventDefault();
+
+            $document.on('mousemove',mousemove);
+            $document.on('mouseup', mouseup);
+            scope.handles[handle][0].focus();
+            scope.actif=handle;
+
+            // slider handle is moving
+            function mousemove(event) {
+                scope.moveHandle (handle, event.clientX, event.clientY);
+            }
+
+            // mouse is up dans leave slider send resize events
+            function mouseup() {
+                $document.unbind('mousemove', mousemove);
+                $document.unbind('mouseup', mouseup);
+
+                // if value change notify application callback
+                if (scope.callback && oldvalue !== scope.value[handle]) scope.callback (scope.value[handle], scope.ctrlhandle);
+            }
+        };
+
+        // simulate jquery find by classes capabilities [warning only return 1st elements]
+        scope.find = function (select, elem) {
+            var domelem;
+
+            if (elem) domelem = elem[0].querySelector(select);
+            else domelem = element[0].querySelector(select);
+
+            var angelem = angular.element(domelem);
+            return (angelem);
+        };
+
+
+
+        scope.initialSettings = function (initial) {
+            var decimal_places_match_result;
+            scope.value=[];  // store low/height value when two handles
+            scope.relative=[];
+
+            if (scope.precision === null) {
+                decimal_places_match_result = ('' + scope.byStep).match(/\.([\d]*)/);
+                scope.precision = decimal_places_match_result && decimal_places_match_result[1] ? decimal_places_match_result[1].length : 0;
+            }
+
+            // position handle to initial value(s)
+            element.on('touchstart', scope.touchBarCB);
+            scope.handles[0].on('touchstart', function(evt){scope.touchHandleCB(evt,0);});
+
+            // this slider has two handles low/hight
+            if (scope.dual) {
+                scope.handles[1].addClass('range-slider-handle');
+                scope.handles[1].on('touchstart', function(evt){scope.touchHandleCB(evt,1);});
+                if (!scope.initvalues) scope.setValue (initial[1],1);
+            }
+
+            // if we have an initstate object apply it
+            if (scope.initvalues) scope.initWidget (scope.initvalues);
+            else   scope.setValue (initial[0],0);
+        };
+
+        scope.init = function () {
+            scope.sliderid   = attrs.id || "slider-" + parseInt (Math.random() * 1000);
+            scope.startValue = -Infinity;
+            scope.stopValue  = Infinity;
+            scope.byStep   = parseInt(attrs.byStep) || 1;
+            scope.vertical = attrs.vertical   || false;
+            scope.dual     = attrs.dualHandles|| false;
+            scope.trigger_input_change= false;
+            scope.notMore  = parseInt(attrs.notMore)   || 100;
+            scope.notLess  = parseInt(attrs.notLess)   || 0;
+
+            if (scope.vertical) element.addClass("vertical-range");
+
+            scope.handles= [scope.find('.handle-min'), scope.find('.handle-max')];
+            scope.bar    = element;
+            scope.slider = scope.find('.range-slider-active-segment');
+            scope.start  = scope.find('.ibz-range-slider-start');
+            scope.stop   = scope.find('.ibz-range-slider-stop');
+            scope.disable= attrs.disable || false;
+
+            scope.ctrlhandle = new RangeSliderHandle (scope);
+
+            // prepare DOM object pointer to compute size dynamically
+            scope.bounds = {
+                bar    : element[0],
+                handles: [scope.handles[0][0], scope.handles[1][0]]
+            };
+
+            if (attrs.disable === 'true') scope.setDisable(true);
+
+            if (attrs.displayTarget) {
+                switch (attrs.displayTarget) {
+                    case true :
+                    case 'handle' :
+                        scope.displays = scope.handles;
+                        scope.handles[0].addClass('ibz-range-slider-display');
+                        if (scope.dual) scope.handles[1].addClass('ibz-range-slider-display');
+                        break;
+                    default:
+                        scope.displays =  [$document.getElementById (attrs.displayTarget)];
+                }
+            } else scope.displays=[];
+
+            // extract initial values from attrs and parse into int
+            if (!attrs.initial) {
+                scope.initial  = [scope.ngModel, scope.ngModel]; // initialize to model values
+            } else {
+                var initial  = attrs.initial.split(',');
+                scope.initial = [
+                    initial[0] !== undefined ? parseInt (initial[0]) : scope.notLess,
+                    initial[1] !== undefined ? parseInt (initial[1]) : scope.notMore
+                ];
+            }
+
+            // Monitor any changes on start/stop dates.
+            scope.$watch('startAt', function() {
+                if (scope.value < scope.startAt ) {
+                    //scope.setValue (scope.startAt);
+                }
+                if (scope.startAt) scope.setStart (scope.startAt);
+            });
+
+            scope.$watch('stopAt' , function() {
+                if (scope.value > scope.stopAt) {
+                    //scope.setValue (scope.stopAt);
+                }
+                if (scope.stopAt) scope.setStop (scope.stopAt);
+            });
+
+            // finish widget initialisation
+            scope.initialSettings (scope.initial);
+
+        };
+
+        scope.init();
+        
+         // slider is ready provide control handle to application controller
+        scope.$watch ('inithook', function () {         // init Values may arrive late
+            if (scope.inithook) scope.inithook (scope.ctrlhandle);
+        });
+
+        scope.$watch ('initvalues', function () { 	// init Values may arrive late
+            if (scope.initvalues) scope.initWidget(scope.initvalues);
+        });
+
+        // two-way binding if model value changes
+        scope.$watch ('ngModel', function (newValue) {
+          scope.setValue(newValue, 0);
+        });
+    }
+
+return {
+    restrict: "E",    // restrict to <range-slider> HTML element name
+    scope: {
+        startAt  :'=',  // First acceptable date
+        stopAt   :'=',  // Last acceptable date
+        callback :'=',  // Callback to actif when a date is selected
+        formatter:'=',  // Callback for drag event call each time internal value changes
+        inithook :'=',  // Hook point to control slider from API
+        cbhandle :'=',  // Argument added to every callback
+        initvalues:'=',   // Initial values as a single object
+        ngModel: '='    // the model value
+    },
+    require: '?ngModel',
+    template: template, // html template is build from JS
+    replace: true,      // replace current directive with template while inheriting of class
+    link: link          // pickadate object's methods
+};
+});
+
+console.log ("RangeSlider Loaded");
+
+})();
\ No newline at end of file
diff --git a/afm-client/app/Frontend/widgets/RangeSliders/Rangeslider.scss b/afm-client/app/Frontend/widgets/RangeSliders/Rangeslider.scss
new file mode 100644
index 0000000..6717d0e
--- /dev/null
+++ b/afm-client/app/Frontend/widgets/RangeSliders/Rangeslider.scss
@@ -0,0 +1,67 @@
+/*
+ * ibz-Datepicker for Foundation
+ *
+ * Author: Fulup Ar Foll
+ * Date  : March-2015
+ * Object: SASS stylesheet, customized to Foundation
+ * References: https://css-tricks.com/stripes-css/
+ *
+ */
+@import "app/ibz-mixins";
+
+.range-slider-handle {
+  display: inline-block;
+  position: absolute;
+  z-index: 1;
+  top: -0.2rem;
+  width: 2rem;
+  height: 1.375rem;
+  border: 1px solid none;
+  cursor: pointer;
+  background: #008cba;
+}
+
+.range-slider.radius, .range-slider-handle {
+  background: #008cba;
+  -webkit-border-radius: 3px;
+  border-radius: 3px;
+}
+
+.range-slider-active-segment {
+  display: inline-block;
+  top: 0.07rem;
+  position: absolute;
+  height: 0.80rem;
+  background: #e5e5e5;
+}
+
+.ibz-range-slider {
+   background-color: rgba(154, 205, 50, 0.6) !important;
+   height: 1rem;
+   position: relative;
+   
+  .range-slider-active-segment {
+    background-color: rgba(82, 168, 200, 0.6);
+  }
+  
+  &-display {
+    background-color: rgba(82, 168, 200, 0.6) !important;
+    width : 4rem !important;
+    padding: .25rem;
+    text-align:center
+   }
+
+   &-start,&-stop {
+   display: inline-block;
+   position: absolute;
+   padding-top: 2px;
+   height: 95%;
+   background: repeating-linear-gradient(
+    45deg,
+    #606dbc,
+    #606dbc 10px,
+    #465298 10px,
+    #465298 20px
+  );}
+
+}
-- 
cgit