summaryrefslogtreecommitdiffstats
path: root/afb-client/app/Frontend/widgets
diff options
context:
space:
mode:
Diffstat (limited to 'afb-client/app/Frontend/widgets')
-rw-r--r--afb-client/app/Frontend/widgets/ActionButtons/ActionButtons.scss40
-rw-r--r--afb-client/app/Frontend/widgets/ActionButtons/AppliButton.js205
-rw-r--r--afb-client/app/Frontend/widgets/ActionButtons/SubmitButton.js52
-rw-r--r--afb-client/app/Frontend/widgets/FormInput/FormInput.scss91
-rw-r--r--afb-client/app/Frontend/widgets/FormInput/UploadFiles.js310
-rw-r--r--afb-client/app/Frontend/widgets/Navigation/LinkButton.js57
-rw-r--r--afb-client/app/Frontend/widgets/Navigation/Navigation.scss26
-rw-r--r--afb-client/app/Frontend/widgets/Notifications/ModalNotification.js85
-rw-r--r--afb-client/app/Frontend/widgets/Notifications/Notifications.scss63
-rw-r--r--afb-client/app/Frontend/widgets/Notifications/TokenRefreshSvc.js149
-rw-r--r--afb-client/app/Frontend/widgets/RangeSliders/RangeSliderMod.js631
-rw-r--r--afb-client/app/Frontend/widgets/RangeSliders/Rangeslider.scss67
12 files changed, 1776 insertions, 0 deletions
diff --git a/afb-client/app/Frontend/widgets/ActionButtons/ActionButtons.scss b/afb-client/app/Frontend/widgets/ActionButtons/ActionButtons.scss
new file mode 100644
index 0000000..6cb8338
--- /dev/null
+++ b/afb-client/app/Frontend/widgets/ActionButtons/ActionButtons.scss
@@ -0,0 +1,40 @@
+/*
+ * 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;
+ }
+
+
+ .disable>i {
+ text-decoration:none; // really not needed for the Top Bar, just for general technique
+ cursor: auto;
+ color: grey !important;
+ }
+}
+
+.appli-menu-start {
+ .start-start, .stop-stop {
+ i {color: grey;}
+ }
+}
diff --git a/afb-client/app/Frontend/widgets/ActionButtons/AppliButton.js b/afb-client/app/Frontend/widgets/ActionButtons/AppliButton.js
new file mode 100644
index 0000000..269ee81
--- /dev/null
+++ b/afb-client/app/Frontend/widgets/ActionButtons/AppliButton.js
@@ -0,0 +1,205 @@
+/*
+ * 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}}">' +
+ '<span>{{label}}</span>' +
+ '</div>';
+
+ var tmplModal =
+ '<b class="close-button" ng-click="close()">×</b>' +
+ '<img ng-src="{{icon}}">' +
+ '<span class="modal-text">Application <b>{{label}}</b></span>' +
+ '<ul class="vertical icon-left primary menu-bar appli-menu-start">' +
+ '<li class=start-{{runstatus}}><a ng-click=action("start")><i class="fi-check"> Start</i></a></li>' +
+ '<li class=stop-{{runstatus}}><a ng-click=action("stop")><i class="fi-x"> Stop</i></a></li>' +
+ '<li><a ng-click=action("info")><i class="fi-info"> Info</i></a></li>' +
+ '<li class=start-{{runstatus}}><a ng-click=action("uninstall")><i class="fi-x"> Uninstall</i></a></li>' +
+ '</ul>' +
+ '';
+
+ var tmplDetail =
+ '<b class="close-button" ng-click="close()">×</b>' +
+ '<img ng-src="{{icon}}">' +
+ '<span class="modal-text">Application <b>{{label}}</b></span>' +
+ '<ul class="vertical icon-left appli-menu-info">' +
+ '<li><i class="fi-paperclip"> Name : {{detail.name}} </i></li>' +
+ '<li><i class="fi-info"> Description {{detail.description}}</i></li>' +
+ '<li><i class="fi-torso"> Author : {{detail.author}}</i></li>' +
+ '</ul>' +
+ '';
+
+ angular.module('AppliButton', [])
+ .directive('appliButton', function (AppConfig, AppCall, ModalFactory, Notification, $timeout, $window, $location, urlquery) {
+
+ function mymethods(scope, elem, attrs) {
+ scope.runstatus = "stop";
+ scope.runmode = urlquery.runmode || "auto";
+ scope.clicked = function () {
+
+ var notifyError = function(action, response) {
+ Notification.error ({message: "Fail /api/afm-main" + action + "=" + scope.label + " RunID="+ scope.appID, delay: 5000});
+ elem.addClass ("fail");
+ elem.removeClass ("success");
+ scope.callback (scope.appID, action, response);
+ };
+
+ var notifySuccess = function (action, response) {
+ elem.removeClass ("fail");
+ scope.runID = response.data.response.runid;
+ scope.callback (scope.appID, action, response);
+ };
+
+ var closeModApp = function() {
+ scope.modApp.deactivate();
+ $timeout (function() {scope.modApp.destroy();}, 1000);
+ };
+
+ var closeModInfo = function() {
+ scope.modInfo.deactivate();
+ $timeout (function() {scope.modInfo.destroy();}, 1000);
+ };
+
+ var actionModal = function(action) {
+ console.log ("Modal Action=%s", action);
+ switch (action) {
+
+ case "start":
+ if (scope.runstatus !== "stop") return;
+ AppCall.get ("afm-main", "start", {id: scope.appID, mode: scope.runmode}, function(response) {
+ if (response.status !== 200 || response.data.jtype !== "afb-reply") {
+ notifyError ("start", response);
+ return;
+ }
+ scope.runstatus="start";
+ notifySuccess (action, response);
+ if(response.data.response.uri)
+ scope.winapp= $window.open(response.data.response.uri.replace("%h", $location.host()));
+ });
+ break;
+
+ case "stop":
+ if (scope.runstatus !== "start") return;
+
+ AppCall.get ("afm-main", "terminate", {runid: scope.runID}, function(response) {
+ if (response.status !== 200 || response.data.jtype !== "afb-reply") {
+ notifyError ("stop", response);
+ return;
+ }
+ scope.runstatus="stop";
+
+ // if a remote window app was open let's close it
+ if (scope.winapp) {
+ console.log ("Closing Application Window label=%s id=%s", scope.label, scope.appID);
+ scope.winapp.close();
+ scope.winapp=false;
+ }
+ notifySuccess (action, response);
+ });
+ break;
+
+ case "info":
+ AppCall.get ("afm-main", "detail", {id: scope.appID}, function(response) {
+ if (response.status !== 200 || response.data.jtype !== "afb-reply") {
+ notifyError ("detail", response);
+ return;
+ }
+
+ // reference http://foundation.zurb.com/apps/docs/#!/angular-modules
+ var config = {
+ animationIn: 'slideInFromTop',
+ contentScope: {
+ close : closeModInfo,
+ icon : scope.icon,
+ label : scope.appID,
+ detail : response.data.response
+ }, template : tmplDetail
+ };
+ // Popup Modal to render application data
+ scope.modInfo = new ModalFactory(config);
+ scope.modInfo.activate ();
+
+ });
+ break;
+
+ case "uninstall":
+ if (scope.runstatus !== "stop") return;
+ AppCall.get ("afm-main", "uninstall", {id: scope.appID}, function(response) {
+ if (response.status !== 200 || response.data.jtype !== "afb-reply") {
+ notifyError ("uninstall", response);
+ return;
+ }
+
+ notifySuccess (action, response);
+ });
+ break;
+
+ default:
+ console.log ("ActionModal unknown action=[%s]", action);
+ break;
+ }
+
+ closeModApp();
+ };
+
+ // reference http://foundation.zurb.com/apps/docs/#!/angular-modules
+ var config = {
+ animationIn: 'slideInFromTop',
+ contentScope: {
+ action : actionModal,
+ runstatus: scope.runstatus,
+ close : closeModApp,
+ icon : scope.icon,
+ label : scope.label
+ }, template : tmplModal
+ };
+ // Popup Modal to render application data
+ scope.modApp = new ModalFactory(config);
+ scope.modApp.activate ();
+ };
+
+ // extract application information from AppID+Store
+ if (attrs.handle && scope.store [attrs.handle].name) {
+ scope.icon = AppConfig.paths.icons + attrs.handle; //scope.store [attrs.handle].name.toLowerCase() + '-ico.png';
+ scope.label = scope.store [attrs.handle].name;
+ scope.appID= 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/afb-client/app/Frontend/widgets/ActionButtons/SubmitButton.js b/afb-client/app/Frontend/widgets/ActionButtons/SubmitButton.js
new file mode 100644
index 0000000..323cd46
--- /dev/null
+++ b/afb-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/afb-client/app/Frontend/widgets/FormInput/FormInput.scss b/afb-client/app/Frontend/widgets/FormInput/FormInput.scss
new file mode 100644
index 0000000..77aed6e
--- /dev/null
+++ b/afb-client/app/Frontend/widgets/FormInput/FormInput.scss
@@ -0,0 +1,91 @@
+/*
+ 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 {
+ display: inline-block;
+ float: right;
+ height : 5rem;
+ width : 5rem;
+ margin: 0.5rem;
+
+ img { height: inherit;}
+
+ .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/afb-client/app/Frontend/widgets/FormInput/UploadFiles.js b/afb-client/app/Frontend/widgets/FormInput/UploadFiles.js
new file mode 100644
index 0000000..90110c9
--- /dev/null
+++ b/afb-client/app/Frontend/widgets/FormInput/UploadFiles.js
@@ -0,0 +1,310 @@
+
+/*
+ * 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';
+
+// WARNING: Angular ng-change does not work on input/file. Let's hook our callback through standard JS function
+var tmpl = '<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()">' +
+ '<img id="{{name}}-img" src="{{thumbnail}}">' +
+ '<range-slider ng-show="!noslider" id="{{name}}-slider" automatic=true inithook="SliderInitCB"></range-slider>' +
+ '</div>';
+
+
+// Service Create xform insert files in and Post it to url
+function LoadFileSvc (scope, elem, posturl, files, thumbnailCB) {
+ var xmlReq = new XMLHttpRequest();
+ var xform = new FormData();
+
+ var OnLoadCB = function (target) {
+ var status = thumbnailCB (target);
+ //if (status) xform.append(scope.name, file, file.name);
+ };
+ // 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 () {
+ elem.addClass ("success");
+ elem.removeClass ("error");
+ var response ={
+ status : xmlReq.status,
+ headers: xmlReq.getAllResponseHeaders()
+ };
+ scope.callback (response);
+ };
+
+ xmlReq.onerror = function () {
+ elem.addClass ("error");
+ elem.removeClass ("success");
+ var response ={
+ status : xmlReq.status,
+ headers: xmlReq.getAllResponseHeaders()
+ };
+ scope.callback (response);
+ };
+
+ xmlReq.onabort = function () {
+ elem.addClass ("error");
+ elem.removeClass ("success");
+ var response ={
+ status : xmlReq.status,
+ headers: xmlReq.getAllResponseHeaders()
+ };
+ scope.callback (response);
+ };
+
+ for (var i = 0; i < files.length; i++) {
+ var file = files[i];
+ if (!file.type.match(scope.mimetype)) {
+ continue;
+ }
+
+ console.log ("Selected file=" + file.name + " size="+ file.size/1024 + " Type="+ file.type);
+
+ // File to upload is too big
+ if (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(file.size)) {
+ scope.thumbnail = scope.isnotvalid;
+ scope.$apply('thumbnail');
+ return;
+ }
+
+ scope.Basename= file.name.split('/').reverse()[0];
+ scope.imgElem[0].file = file;
+
+ // If File is an image let display it now
+ if (thumbnailCB) {
+ var reader = new FileReader();
+ reader.readAsArrayBuffer(file);
+ reader.onload = OnLoadCB;
+ }
+ // if everything is OK let's add file to xform
+ xform.append(scope.name, file, file.name);
+ }
+
+
+ // everything looks OK let's Post it
+ xmlReq.open("POST", posturl , true);
+ xmlReq.send(xform);
+}
+
+angular.module('UploadFiles',['AppConfig', 'ModalNotification', 'RangeSlider'])
+
+.directive('uploadImage', function(AppConfig, JQemu, Notification) {
+ function mymethods(scope, elem, attrs) {
+
+ // get widget image handle from template
+ scope.imgElem = elem.find('img');
+ scope.inputElem = elem.find('input');
+
+ // 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 readerCB = function (upload) {
+ // scope.thumbnail = upload.target.result;
+ scope.imgElem[0].src = window.URL.createObjectURL(new Blob([upload.target.result], {type: "image"}));
+ return true; // true activates post
+ };
+ var posturl = attrs.posturl + "?token=" + AppConfig.session.token;
+ new LoadFileSvc (scope, elem, posturl, files, readerCB);
+ };
+
+ // Initiallize default values from attributes values
+ scope.name= attrs.name || 'file';
+ scope.category= attrs.category || 'image';
+ scope.mimetype= (attrs.accept || 'image') + '/*';
+ scope.maxsize= attrs.maxsize || 100; // default max size 100KB
+ scope.regexp = new RegExp (attrs.accept+ '.*','i');
+
+ if (attrs.thumbnail) scope.thumbnail= AppConfig.paths[scope.category] + attrs.thumbnail;
+ else scope.thumbnail=AppConfig.paths[scope.category] + 'tux-bzh.png';
+
+ 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: tmpl,
+ link: mymethods,
+ scope: {
+ callback : '='
+ }
+ };
+})
+
+.directive('uploadAudio', function(AppConfig, JQemu, Notification) {
+ function mymethods(scope, elem, attrs) {
+
+ // get widget image handle from template
+ scope.imgElem = elem.find('img');
+ scope.inputElem = elem.find('input');
+
+ // 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 posturl = attrs.posturl + "?token=" + AppConfig.session.token;
+ new LoadFileSvc (scope, elem, posturl, files, false);
+ };
+
+ // Initiallize default values from attributes values
+ scope.name= attrs.name || 'audio';
+ scope.category= attrs.category || 'audio';
+ scope.mimetype= (attrs.accept || 'audio') + '/*';
+ scope.maxsize= attrs.maxsize || 10000; // default max size 10MB
+ scope.regexp = new RegExp (attrs.accept+ '.*','i');
+
+ if (attrs.thumbnail) scope.thumbnail= AppConfig.paths[scope.category] + attrs.thumbnail;
+ else scope.thumbnail=AppConfig.paths[scope.category] + 'upload-music.png';
+
+ 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: tmpl,
+ link: mymethods,
+ scope: {
+ callback : '='
+ }
+ };
+
+})
+
+.directive('uploadAppli', function(AppConfig, JQemu, Notification) {
+ function mymethods(scope, elem, attrs) {
+
+ // get widget image handle from template
+ scope.imgElem = elem.find('img');
+ scope.inputElem = elem.find('input');
+
+ // 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 readerCB = function (upload) {
+ var zipapp = new JSZip(upload.target.result);
+ var thumbnail = zipapp.file("afa-pkg/thumbnail.jpg");
+
+ // 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
+ return false; // do not post zip on binder
+ }
+ scope.imgElem[0].src = window.URL.createObjectURL(new Blob([thumbnail.asArrayBuffer()], {type: "image"}));
+ return true; // true activates post
+ };
+ var posturl = attrs.posturl + "?token=" + AppConfig.session.token;
+ new LoadFileSvc (scope, elem, posturl, files, readerCB);
+ };
+
+ // Initiallize default values from attributes values
+ scope.name= attrs.name || 'appli';
+ scope.category= attrs.category || 'appli';
+ scope.mimetype= (attrs.accept || '.zip');
+ scope.maxsize= attrs.maxsize || 100000; // default max size 100MB
+ scope.regexp = new RegExp (attrs.accept+ '.*','i');
+
+ if (attrs.thumbnail) scope.thumbnail= AppConfig.paths[scope.category] + attrs.thumbnail;
+ else scope.thumbnail=AppConfig.paths[scope.category] + 'upload-appli.png';
+
+ 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: tmpl,
+ link: mymethods,
+ scope: {
+ callback : '='
+ }
+ };
+
+});
+
+console.log ("UploadFile Loaded");
+})();
diff --git a/afb-client/app/Frontend/widgets/Navigation/LinkButton.js b/afb-client/app/Frontend/widgets/Navigation/LinkButton.js
new file mode 100644
index 0000000..3e83425
--- /dev/null
+++ b/afb-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/afb-client/app/Frontend/widgets/Navigation/Navigation.scss b/afb-client/app/Frontend/widgets/Navigation/Navigation.scss
new file mode 100644
index 0000000..2babf24
--- /dev/null
+++ b/afb-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/afb-client/app/Frontend/widgets/Notifications/ModalNotification.js b/afb-client/app/Frontend/widgets/Notifications/ModalNotification.js
new file mode 100644
index 0000000..37ba047
--- /dev/null
+++ b/afb-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/afb-client/app/Frontend/widgets/Notifications/Notifications.scss b/afb-client/app/Frontend/widgets/Notifications/Notifications.scss
new file mode 100644
index 0000000..fb740b7
--- /dev/null
+++ b/afb-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/afb-client/app/Frontend/widgets/Notifications/TokenRefreshSvc.js b/afb-client/app/Frontend/widgets/Notifications/TokenRefreshSvc.js
new file mode 100644
index 0000000..2c7c3da
--- /dev/null
+++ b/afb-client/app/Frontend/widgets/Notifications/TokenRefreshSvc.js
@@ -0,0 +1,149 @@
+/*
+ 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($log, $window, $timeout, $location, Notification, AppConfig, AppCall) {
+
+ function mymethods(scope, elem, attrs) {
+ scope.logged=undefined; // neither thu neither false
+
+ $window.onbeforeunload = function () {
+ AppCall.get (scope.plugin, "logout", {/*query*/}, function () {
+ $log.log("OPA exit");
+ });
+ };
+
+ scope.online = function () {
+ elem.addClass ("online");
+ elem.removeClass ("offline");
+ scope.logged=true;
+ };
+
+ scope.offline = function(){
+ elem.addClass ("offline");
+ elem.removeClass ("online");
+ scope.logged=false;
+ };
+
+ scope.onerror = function() {
+ if (scope.logged !== false) {
+ Notification.warning ({message: "AppFramework Binder Lost", delay: 5000});
+ scope.offline();
+ }
+ scope.status = 0;
+ };
+
+ scope.onsuccess = function(jresp, errcode) {
+
+ if (errcode !== 200 || jresp.request.status !== "success") {
+ Notification.warning ({message: "auto-connect :" + jresp.request.info, delay: 10000});
+ scope.offline();
+ return false;
+ }
+
+ if (scope.logged !== true) {
+ Notification.success ({message: "AppFramework Binder Connected", delay: 3000});
+ scope.online();
+ if (scope.callback) scope.callback(jresp);
+ }
+
+ scope.status = 1;
+ return true;
+ };
+
+ // Check Binder status
+ scope.getping = function() {
+
+ AppCall.get (scope.plugin, "ping", {/*query*/},function(jresp, errcode) {
+ if (errcode !== 200 || jresp.request.status !== "success") {
+ Notification.warning ({message: jresp.request.info, delay: 5000});
+ scope.offline();
+ return;
+ }
+ // restart a new timer for next ping
+ $timeout (scope.getping, AppConfig.session.pingrate*1000);
+ }, scope.onerror);
+ };
+
+ // Check Binder status
+ scope.refresh = function() {
+
+ AppCall.get (scope.plugin, "refresh", {/*query*/}, function(jresp, errcode) {
+
+ scope.onsuccess (jresp, errcode);
+
+ // restart a new timer for next refresh
+ $timeout (scope.refresh, AppConfig.session.timeout *250);
+ }, scope.onerror);
+ };
+
+ // Initial connection
+ scope.loggin = function() {
+ AppCall.get (scope.plugin, "connect", {token: AppConfig.session.initial}, function(jresp, errcode) {
+
+ if (!scope.onsuccess (jresp, errcode)) return;
+
+ // Intial token was accepted let's start ping & refresh
+ $timeout (scope.getping, AppConfig.session.pingrate*1000);
+ $timeout (scope.refresh, AppConfig.session.timeout *250);
+
+ }, scope.onerror);
+ };
+
+
+ // Parse Widget Parameters
+ scope.plugin = attrs.plugin || "auth";
+ scope.icon = attrs.icon || "fi-lightbulb";
+ scope.hostname = $location.host();
+ scope.httpdport = $location.port();
+ scope.autolog = JSON.parse(attrs.autolog || false);
+
+ // autostart log if requested
+ if (scope.autolog) scope.loggin();
+
+ }
+
+ return {
+ template: template,
+ scope: {
+ callback : "="
+ },
+ restrict: 'E',
+ link: mymethods
+ };
+});
+
+})();
+console.log ("Token Refresh Loaded");
diff --git a/afb-client/app/Frontend/widgets/RangeSliders/RangeSliderMod.js b/afb-client/app/Frontend/widgets/RangeSliders/RangeSliderMod.js
new file mode 100644
index 0000000..77f0fce
--- /dev/null
+++ b/afb-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/afb-client/app/Frontend/widgets/RangeSliders/Rangeslider.scss b/afb-client/app/Frontend/widgets/RangeSliders/Rangeslider.scss
new file mode 100644
index 0000000..6717d0e
--- /dev/null
+++ b/afb-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
+ );}
+
+}