diff options
Diffstat (limited to 'afb-client/app/Frontend/widgets')
10 files changed, 729 insertions, 0 deletions
diff --git a/afb-client/app/Frontend/widgets/Buttons/SubmitButtons/SubmitButtons.js b/afb-client/app/Frontend/widgets/Buttons/SubmitButtons/SubmitButtons.js new file mode 100644 index 0000000..1b94e25 --- /dev/null +++ b/afb-client/app/Frontend/widgets/Buttons/SubmitButtons/SubmitButtons.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()"> <span>' + + '<i class="{{icon}}"></i>' + + '<span>{{label}}</span>' + + '</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/Buttons/SubmitButtons/SubmitButtons.scss b/afb-client/app/Frontend/widgets/Buttons/SubmitButtons/SubmitButtons.scss new file mode 100644 index 0000000..2150e4d --- /dev/null +++ b/afb-client/app/Frontend/widgets/Buttons/SubmitButtons/SubmitButtons.scss @@ -0,0 +1,22 @@ +/* + * 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 + 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..37519fd --- /dev/null +++ b/afb-client/app/Frontend/widgets/FormInput/FormInput.scss @@ -0,0 +1,69 @@ +/* + 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 { + height: 5rem; + display: inline-block; + float: right; + img { height: inherit;} +} + +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/InputPassword.js b/afb-client/app/Frontend/widgets/FormInput/InputPassword.js new file mode 100644 index 0000000..157009c --- /dev/null +++ b/afb-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/afb-client/app/Frontend/widgets/FormInput/InputText.js b/afb-client/app/Frontend/widgets/FormInput/InputText.js new file mode 100644 index 0000000..2653175 --- /dev/null +++ b/afb-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="?"> </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/afb-client/app/Frontend/widgets/FormInput/UploadFile.js b/afb-client/app/Frontend/widgets/FormInput/UploadFile.js new file mode 100644 index 0000000..9a2f031 --- /dev/null +++ b/afb-client/app/Frontend/widgets/FormInput/UploadFile.js @@ -0,0 +1,113 @@ + +/* + * 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-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 + */ + + +function changeInput() { + console.log ('input imgClicked'); +} + +(function() { +'use strict'; + +// WARNING: Angular ng-change does not work on input/file. Let's hook our callback through standard JS function +var tmpl = '<form target="null" action="/api/afbs/file-upload" method="post" enctype="multipart/form-data" >'+ + '<input type="file" name="{{name}}" onchange="angular.element(this).scope().UpLoadFile(this.files)" accept="{{mime}}/*" style="display" >'+ + '<input type="submit" class="submit" style="display" > ' + + '</form>' + + '<img id="{{name}}-img" src="{{imagepath}}" ng-click="imgClicked()">' ; + +function basename(path) { + return path.split('/').reverse()[0]; +} + +angular.module('UploadFile',['ConfigApp']) + +.directive('uploadFile', function(ConfigApp, $http, JQemu) { + function mymethods(scope, elem, attrs) { + + // get widget image handle from template + scope.imgElem = elem.find('img'); + scope.inputElem = elem.find('input'); + scope.submitElem = JQemu.findByType (elem.children(), "submit"); + + + // Image was ckick let's simulate an input (file) click + scope.imgClicked = function () { + scope.inputElem[0].click(); // Warning Angular TriggerEvent does not work!!! + }; + + // upload file to server + scope.UpLoadFile= function(files) { + + + for (var i = 0; i < files.length; i++) { + var file = files[i]; + console.log ("Selected file=" + file.name + " size="+ file.size/1024); + var mimeType = /image.*/; // build regular expression from Mime + if (!file.type.match(mimeType)) { + continue; + } + + if (file.size > scope.sizemax*1024) { + scope.imagepath = scope.istoobig; // warning is path is wrong nothing happen + scope.$apply('imagepath'); // we short-circuit Angular resync Image + } else { + + scope.basename=basename(file.name); + scope.imgElem[0].file = file; + + var reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = function (upload) { + scope.imagepath = upload.target.result; + scope.$apply('imagepath'); // we short-circuit Angular resync image + scope.submitElem[0].click(); // Warning Angular TriggerEvent does not work!!! + }; + } + } + }; + + // Initiallize default values from attributes values + if (attrs.icon) scope.imagepath= ConfigApp.paths[attrs.category] + attrs.icon; + else scope.imagepath=ConfigApp.paths.avatars + 'tux-bzh.png'; + + if (attrs.istoobig) scope.istoobig= ConfigApp.paths[attrs.category] + attrs.istoobig; + else scope.istoobig=ConfigApp.paths.avatars + 'istoobig.jpg'; + + scope.name= attrs.name || 'avatar'; + scope.mime= attrs.mime || 'image'; + scope.sizemax= attrs.sizemax || 100; // default max size 100KB + + } + + 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..5d42d2a --- /dev/null +++ b/afb-client/app/Frontend/widgets/Notifications/Notifications.scss @@ -0,0 +1,47 @@ +/* + * 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; + } +} |