From 3ebdce373e134b70b129154d8033c1c628847a6c Mon Sep 17 00:00:00 2001 From: Fulup Ar Foll Date: Mon, 25 Jan 2016 14:37:32 +0100 Subject: First version --- .../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 +++++++++++++++++++++ 4 files changed, 578 insertions(+) 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 (limited to 'afm-client/app/Frontend/widgets/FormInput') 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 = '' + + '' + + ' '+ + ''; + +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 = '' + + ''+ + '{{errmsg}}'; + +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 = ''+ + '
' + + ' {{label}}' + + '' + + '
'; + +var tmplModal = 'Upload Application {{appname}} ?' + + '
'+ + '' + + ''+ + ' ' + + '
'; + + +// 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"); +})(); -- cgit 1.2.3-korg