├── .gitignore ├── bower.json ├── dist ├── css │ ├── demo.css │ └── queryBuilder.css └── js │ ├── app.min.js │ └── queryBuilder.min.js ├── gulpfile.js ├── index.html ├── readme.md └── src ├── js ├── app.js └── queryBuilder.js └── scss ├── demo.scss └── queryBuilder.scss /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngQueryBuilder", 3 | "version": "1.0.0", 4 | "homepage": "https://github.com/FauzanKhan/angular-query-builder", 5 | "authors": [ 6 | "FauzanKhan " 7 | ], 8 | "description": "An angular Directive for creating queries/formulae from the UI", 9 | "main": ["dist/js/queryBuilder.min.js", "dist/css/queryBuilder.css" ], 10 | "keywords": [ 11 | "angular", 12 | "directive", 13 | "sql", 14 | "query", 15 | "query", 16 | "builder", 17 | "rules", 18 | "rules", 19 | "builder", 20 | "rules", 21 | "engine", 22 | "query", 23 | "engine", 24 | "formula", 25 | "fomula", 26 | "builder", 27 | "javascript", 28 | "html" 29 | ], 30 | "license": "MIT" 31 | } 32 | -------------------------------------------------------------------------------- /dist/css/demo.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #F5F5F5; 3 | font-family: "Source Sans Pro", arial; 4 | min-height: 100vh; } 5 | body header { 6 | text-align: center; 7 | color: #444; 8 | padding-top: 10px; 9 | font-size: 20px; } 10 | body header h1 { 11 | margin-top: 8px; 12 | line-height: 45px; } 13 | body header small { 14 | color: #999; 15 | font-size: 25px; 16 | font-weight: 300; } 17 | body header small span, body header small a { 18 | text-decoration: none; 19 | color: #BCBCBC; 20 | font-weight: 400; 21 | transition: color 500ms ease; 22 | border-bottom: 1px dotted #BCBCBC; } 23 | body header small span:hover, body header small a:hover { 24 | text-decoration: none; 25 | color: #666; 26 | border-bottom: 1px dotted #666; } 27 | body .demo-container { 28 | margin: 0 auto; 29 | width: 1220px; 30 | text-align: center; } 31 | body .flash-message { 32 | /*background: rgb(0, 113, 181, .8);*/ 33 | padding: 20px; 34 | max-width: 280px; 35 | border: 1px solid #CCC; 36 | border-radius: 3px; 37 | background: #FFF; 38 | position: fixed; 39 | bottom: 50px; 40 | border-radius: 0px; 41 | font-size: 12px; 42 | color: #666; 43 | transition: all 900ms ease; 44 | right: 50px; 45 | font-size: 18px; 46 | word-break: break-all; 47 | overflow: hidden; } 48 | body .flash-message.vis { 49 | opacity: 1; } 50 | body .flash-message.hidden { 51 | opacity: 0; } 52 | body .flash-message h3 { 53 | color: #999; 54 | font-size: 16px; 55 | margin: 0; 56 | margin-bottom: 10px; } 57 | body footer { 58 | text-align: center; 59 | position: absolute; 60 | bottom: 0; 61 | left: 0; 62 | right: 0; 63 | padding-bottom: 40px; } 64 | body footer img { 65 | width: 20px; 66 | vertical-align: -4px; 67 | margin-right: 3px; } 68 | body footer a { 69 | color: #008CCD; 70 | border-radius: 3px; 71 | text-decoration: none; 72 | padding: 14px 18px; 73 | background: #FFF; 74 | border: 1px solid #EAEAEA; 75 | transition: background 200ms ease; } 76 | body footer a:hover { 77 | background: transparent; } 78 | 79 | .button { 80 | color: #008CCD; 81 | border-radius: 3px; 82 | text-decoration: none; 83 | padding: 14px 18px; 84 | background: #FFF; 85 | border: 1px solid #EAEAEA; 86 | transition: background 200ms ease; 87 | cursor: pointer; } 88 | .button:hover { 89 | background: transparent; } 90 | -------------------------------------------------------------------------------- /dist/css/queryBuilder.css: -------------------------------------------------------------------------------- 1 | .query-builder-wrapper { 2 | text-align: left; 3 | position: relative; 4 | display: inline-block; 5 | color: #333; } 6 | .query-builder-wrapper .query-popover { 7 | border: 1px solid #EAEAEA; 8 | background: #FFF; 9 | border-radius: 3px; } 10 | .query-builder-wrapper .operator-wrapper { 11 | position: relative; 12 | display: inline-block; } 13 | .query-builder-wrapper .operator-wrapper .operator { 14 | margin: 0 10px; } 15 | .query-builder-wrapper .operator-wrapper .operator-popover { 16 | display: none; 17 | position: absolute; 18 | top: -55px; 19 | -webkit-transform: translateX(-35%); 20 | moz-transform: translateX(-35%); 21 | o-transform: translateX(-35%); 22 | transform: translateX(-35%); 23 | left: 0; 24 | right: 0; 25 | margin: 0 auto; 26 | padding: 5px; 27 | width: 180px; 28 | text-align: center; } 29 | .query-builder-wrapper .operator-wrapper .operator-popover .popover-body { 30 | position: relative; } 31 | .query-builder-wrapper .operator-wrapper .operator-popover .popover-body .triangle { 32 | width: 15px; 33 | height: 15px; 34 | display: inline-block; 35 | position: absolute; 36 | bottom: -13px; 37 | left: 46%; 38 | -webkit-transform: rotate(45deg); 39 | moz-transform: rotate(45deg); 40 | o-transform: rotate(45deg); 41 | transform: rotate(45deg); 42 | border-right: 1px solid #EAEAEA; 43 | border-bottom: 1px solid #EAEAEA; 44 | background: #FFF; } 45 | .query-builder-wrapper .operator-wrapper .operator-popover .operator-option { 46 | display: inline-block; 47 | border-radius: 2px; 48 | cursor: pointer; 49 | margin: 3px 1px; 50 | padding: 3px; 51 | font-size: 12px; 52 | border: 1px solid #EAEAEA; } 53 | .query-builder-wrapper .operator-wrapper.active .operator-popover { 54 | display: inline-block; 55 | z-index: 9; } 56 | .query-builder-wrapper .operand-selection { 57 | position: relative; 58 | display: inline-block; } 59 | .query-builder-wrapper .operand-selection .button { 60 | margin: 0; 61 | margin-bottom: 10px; } 62 | .query-builder-wrapper .operand-selection .operand-popover { 63 | display: none; 64 | position: absolute; 65 | width: 300px; 66 | top: 50px; 67 | left: 0; 68 | right: 0; 69 | margin: 0 auto; } 70 | .query-builder-wrapper .operand-selection .operand-popover .operand-popover-content { 71 | position: relative; } 72 | .query-builder-wrapper .operand-selection .operand-popover .operand-popover-content .triangle { 73 | position: absolute; 74 | border-top: 1px solid #EAEAEA; 75 | border-right: 1px solid #EAEAEA; 76 | background: #FAFAFA; 77 | height: 10px; 78 | width: 10px; 79 | top: -6px; 80 | left: 20px; 81 | -webkit-transform: rotate(-45deg); 82 | moz-transform: rotate(-45deg); 83 | o-transform: rotate(-45deg); 84 | transform: rotate(-45deg); } 85 | .query-builder-wrapper .operand-selection .operand-popover .popover-body { 86 | padding: 15px 12px; } 87 | .query-builder-wrapper .operand-selection .operand-popover .popover-body .operation-wrapper { 88 | width: 48%; 89 | margin-right: 5px; 90 | display: inline-block; } 91 | .query-builder-wrapper .operand-selection .operand-popover .popover-body .value-wrapper { 92 | width: 48%; 93 | margin-left: 5px; 94 | display: inline-block; } 95 | .query-builder-wrapper .operand-selection .operand-popover .popover-body .done-btn-wrapper { 96 | text-align: right; } 97 | .query-builder-wrapper .operand-selection .operand-popover .popover-body label { 98 | font-size: 12px; 99 | color: #333; } 100 | .query-builder-wrapper .operand-selection .operand-popover .popover-body select { 101 | width: 100%; 102 | height: 35px; 103 | border-radius: 2px; 104 | border: 1px solid #EAEAEA; 105 | box-shadow: none; 106 | padding: 5px; 107 | margin-bottom: 10px; } 108 | .query-builder-wrapper .operand-selection .operand-popover .popover-body input { 109 | width: 100%; 110 | height: 30px; 111 | border: 1px solid #EAEAEA; 112 | border-radius: 3px; 113 | margin-bottom: 15px; } 114 | .query-builder-wrapper .operand-selection .operand-popover .popover-body textarea { 115 | width: 97%; 116 | height: 110px; 117 | border: 1px solid #EAEAEA; 118 | border-radius: 3px; 119 | margin-bottom: 15px; } 120 | .query-builder-wrapper .operand-selection .operand-popover .popover-header { 121 | background: #FAFAFA; 122 | position: relative; 123 | border-bottom: 1px solid #EAEAEA; 124 | border-radius: 3px 3px 0 0; } 125 | .query-builder-wrapper .operand-selection .operand-popover .popover-header .heading { 126 | font-size: 16px; 127 | font-weight: bold; 128 | color: gray; 129 | padding: 15px 10px; 130 | border-right: 1px solid #EAEAEA; 131 | display: inline-block; } 132 | .query-builder-wrapper .operand-selection .operand-popover .popover-header .query-types { 133 | display: inline-block; } 134 | .query-builder-wrapper .operand-selection .operand-popover .popover-header .query-types .query-type { 135 | display: inline-block; 136 | padding: 0 12px; } 137 | .query-builder-wrapper .operand-selection .operand-popover .popover-header .query-types .query-type label { 138 | cursor: pointer; 139 | vertical-align: 2px; 140 | color: gray; } 141 | .query-builder-wrapper .operand-selection .operand-popover .col-name-wraper { 142 | width: 100%; } 143 | .query-builder-wrapper .operand-selection.active .operand-popover { 144 | display: inline-block; 145 | z-index: 9; } 146 | .query-builder-wrapper .bracket { 147 | font-size: 32px; } 148 | .query-builder-wrapper .bracket.left-bracket { 149 | margin-right: 10px; } 150 | .query-builder-wrapper .bracket.right-bracket { 151 | margin-left: 10px; } 152 | .query-builder-wrapper .query-builder-label { 153 | font-size: 12px; 154 | font-weight: 600; 155 | border-radius: 5px; 156 | padding: 2px 8px; 157 | cursor: pointer; } 158 | .query-builder-wrapper .query-builder-label.neutral { 159 | background: #444; 160 | color: #FFF; } 161 | .query-builder-wrapper .query-builder-btn { 162 | color: #333; 163 | border-radius: 3px; 164 | text-decoration: none; 165 | padding: 10px 15px; 166 | background: #FFF; 167 | transition: background 200 ms ease; 168 | cursor: pointer; 169 | border: 1px solid #EAEAEA; 170 | position: relative; 171 | display: inline-block; 172 | margin-bottom: 15px; } 173 | .query-builder-wrapper .query-builder-btn.no-margins { 174 | margin: 0; } 175 | .query-builder-wrapper .query-builder-btn.small { 176 | font-size: 12px; } 177 | .query-builder-wrapper .query-builder-btn.query-builder-btn-dropdown { 178 | padding-right: 35px; } 179 | .query-builder-wrapper .query-builder-btn.query-builder-btn-dropdown::after { 180 | content: ''; 181 | position: absolute; 182 | top: 48%; 183 | right: 0; 184 | margin-right: 18px; 185 | width: 0; 186 | height: 0; 187 | border-right: 4px solid transparent; 188 | border-left: 4px solid; 189 | border-left-color: transparent; 190 | border-top: 4px solid #333; } 191 | 192 | .add-sub-query { 193 | display: block; 194 | margin-bottom: 20px; } 195 | .add-sub-query .new-entry-btn { 196 | cursor: pointer; 197 | height: 20px; 198 | width: 20px; 199 | text-align: center; 200 | border-radius: 100%; 201 | background: #444; 202 | color: #FFF; 203 | font-size: 22px; 204 | font-weight: bold; 205 | display: inline-block; 206 | line-height: 20px; } 207 | -------------------------------------------------------------------------------- /dist/js/app.min.js: -------------------------------------------------------------------------------- 1 | var app=angular.module("app",["ngQueryBuilder"]);app.controller("masterController",["$scope",function(e){e.queryBuilder={},e.columns=["FirstName","LastName","Age","Gender","DOB","Major","School"],e.operations=[">","<","<=",">=","==","=","is"],e.temp={expression:"((0AND1)OR(2AND3))",operands:[{type:"basic",colName:"FirstName",operation:"is",value:"'Tom'",custom:""},{type:"basic",colName:"LastName",operation:"is",value:"'Cruise'",custom:""},{type:"basic",colName:"Age",operation:">",value:"40",custom:""},{type:"basic",colName:"Gender",operation:"is",value:"'Male'",custom:""}],bracketIds:[2,1,1,3,3,2]}}]); -------------------------------------------------------------------------------- /dist/js/queryBuilder.min.js: -------------------------------------------------------------------------------- 1 | !function(e){var r=e.module("ngQueryBuilder",[]);r.directive("queryBuilder",["$compile","templatesFactory",function(r,a){return{restrict:"EA",scope:{data:"=",columns:"=",operations:"="},link:function(n,t,o){var s=n.data||{},p=a.get,c=a.build,i=s.expression?s.expression.match(/[^A-Za-z()]/g):0,l={type:"custom",colName:"",operation:"",value:"",custom:""},d={type:"basic",colName:"",operation:"",value:"",custom:""};n.query=s,n.currIndex=s.expression?i[i.length-1]:0,s.expression=s.expression?s.expression:null,s.operands=s.operands||[e.copy(d)],s.bracketIds=s.bracketIds||[],n.togglePopover=function(r){var a=e.element(r.target).closest(".popover-parent").hasClass("active");e.element(".popover-parent").removeClass("active"),a||e.element(r.target).closest(".popover-parent").addClass("active")},n.newEntry=function(a){n.currIndex=parseInt(n.currIndex),n.currIndex+=1,currIndex=n.currIndex,n.query.operands.push(e.copy(d));var o=p(currIndex);t.find(".query-builder-wrapper").append(r(o.operator)(n)),t.find(".query-builder-wrapper").append(r(o.operandSelection)(n)),n.query.expression=v()},n.changeQueryType=function(r,a){"basic"==a?n.query.operands[r]=e.copy(d):n.query.operands[r]=e.copy(l)},n.changeOperator=function(r,a){var t=e.element(a.target).closest(".operator-wrapper");t.find(".operator").text(r),t.removeClass("active"),n.query.expression=v(),console.log(n.query)},n.addBrackets=function(r){var a=e.element(r.target).closest(".operator-wrapper"),t=a.attr("data-operator-id"),o=a.next(),s=a.prev(),p='(',c=')';if(o.hasClass("bracket opening-bracket"))for(o=o.next(),openingBracketsCount=1;o.length>0;){if(o.hasClass("bracket opening-bracket"))openingBracketsCount+=1;else if(o.hasClass("bracket closing-bracket")&&(openingBracketsCount-=1,0==openingBracketsCount)){e.element(o).after(c);break}o=o.next()}else o.after(c);if(s.hasClass("bracket closing-bracket"))for(s=s.prev(),closingBracketsCount=1;s.length>0;){if(s.hasClass("bracket closing-bracket"))closingBracketsCount+=1;else if(s.hasClass("bracket opening-bracket")&&(closingBracketsCount-=1,0==closingBracketsCount)){e.element(s).before(p);break}s=s.prev()}else s.before(p);a.removeClass("active"),u(),n.query.expression=v()},n.removeBrackets=function(r){var a=e.element(r.target).closest(".operator-wrapper"),t=a.attr("data-operator-id"),o=a.closest(".query-builder-wrapper");o.find('.bracket[data-bracket-id="'+t+'"]').remove(),a.removeClass("active"),u(),n.query.expression=v()};var u=function(){for(var r=t.find(".query-builder-wrapper").find(".bracket"),a=[],o=0;o'),t.append('
'),t.find(".add-sub-query").append(r(p(0).addMoreBtn)(n)),n.query.expression?c(t,n.query.expression,n.query.operands,n.query.bracketIds,n):t.find(".query-builder-wrapper").append(r(p(0).operandSelection)(n)),e.element(".query-builder-wrapper").on("click",".popover-parent",function(e){e.stopPropagation()}),e.element("body").on("click",function(){e.element(".query-builder-wrapper .popover-parent").removeClass("active")})}}}]),r.factory("templatesFactory",["$compile",function(e){var r=function(e){e=parseInt(e);var r={};return r.addMoreBtn='+',r.operator='
AND
+-*/ANDOR(brackets)(brackets)
',r.operandSelection='
{{query.operands['+e+'].colName+" "+query.operands['+e+'].operation+" "+query.operands['+e+'].value+""+query.operands['+e+'].custom}}Enter Query
Query Type
',r},a=function(a,n,t,o,s){var p=0,c=n.split(/((?:\(|\)|[A-Z]+|\d+))/g),i=0;expressionArray=c.filter(function(e){return""!=e});for(var l=0;l(';a.find(".query-builder-wrapper").append(y),p+=1}else if(")"==d){var v=o[p],b=')';a.find(".query-builder-wrapper").append(b),p+=1}else if("AND"==d||"OR"==d){var u=r(parseInt(i));a.find(".query-builder-wrapper").append(e(u.operator)(s))}else if(!isNaN(d)){var u=r(parseInt(d));a.find(".query-builder-wrapper").append(e(u.operandSelection)(s)),i=parseInt(i),i+=1}}};return{get:r,build:a}}])}(angular); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var sass = require('gulp-sass'); 3 | var uglify = require('gulp-uglify'); 4 | var watch = require('gulp-watch'); 5 | var notify = require('gulp-notify'); 6 | var rename = require("gulp-rename"); 7 | 8 | var path = { 9 | src : { 10 | scss: 'src/scss/*.scss', 11 | js: 'src/js/*.js' 12 | }, 13 | dest: { 14 | css: 'dist/css', 15 | js: 'dist/js' 16 | } 17 | } 18 | 19 | gulp.task('sass', function(){ 20 | gulp.src(path.src.scss) 21 | .pipe(sass().on('error', function(){ 22 | notify({ 23 | message: "Error Comiling CSS", 24 | }); 25 | sass.logError(); 26 | })) 27 | .pipe(gulp.dest(path.dest.css)) 28 | .pipe(notify({ 29 | message: "Generated <%= file.relative %>", 30 | })) 31 | }); 32 | 33 | gulp.task('minify-js', function(){ 34 | gulp.src(path.src.js) 35 | .pipe(uglify()) 36 | .pipe(rename({'suffix': '.min'})) 37 | .pipe(gulp.dest(path.dest.js)) 38 | .pipe(notify({ 39 | message: "Generated <%= file.relative %>", 40 | })); 41 | }); 42 | 43 | gulp.task('minify', ['sass', 'minify-js']); 44 | 45 | gulp.task('watch', function(){ 46 | gulp.watch(path.src.scss, ['sass']); 47 | gulp.watch(path.src.js, ['minify-js']); 48 | }) 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ngQueryBuilder | An angular Directive for creating queries/formulae from the UI 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

ngQueryBuilder
An Angular Directive for Formula/Query Builder.

16 |
17 |
18 |
19 | 20 |
21 |
22 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ngQueryBuilder | Angular Query Builder 2 | An elegant, easily customizable SQL Query Builder in Angular.. 3 | 4 | ### Description 5 | * An angular directive for Formula/Query Builder. 6 | * Works well with/without Bootstrap/Foundation. 7 | 8 | ![](http://i.imgur.com/c0cTZPX.png) 9 | ![](http://i.imgur.com/ijCdMnN.png) 10 | ![](http://i.imgur.com/F0veghb.png) 11 | 12 | ### Dependecies 13 | * Angular.js 14 | * jQuery 15 | 16 | ### Installation 17 | 18 | Install using bower 19 | 20 | ``` 21 | bower install ngQueryBuilder 22 | ``` 23 | ###### or 24 | 25 | Get the queryBuilder.min.js & queryBuilder.css files from dist folder. 26 | 27 | ### Usage 28 | * Make sure you include the ```ngQueryBuilder``` module in you angular app: 29 | 30 | ``` 31 | angular.module('myApp', ['ngQueryBuilder']); 32 | ``` 33 | 34 | * once you've added the module in your app. Use the code below to get the query builder up and running: 35 | 36 | ```html 37 | 38 | //Operations which are to be applied on columns (Should be Array of Strings | eg - ['<', '>', '=']) 41 | 42 | 43 | ``` 44 | 45 | ### Output JSON 46 | 47 | ``` 48 | 49 | { 50 | "bracketIds": [3, 2, 1, 1, 2, 3], //Storing Ids for easier repopulation of stored queries 51 | "expression": "(((0OR1)AND2)AND3)", // Expression corresponding to the query created by the user. 52 | "operands": { // Stores the variables referenced in the expression above 53 | "0": { 54 | "colName": "FirstName", 55 | "custom": "", 56 | "operation": "is", 57 | "type": "basic", 58 | "value": "Tom" 59 | }, 60 | "1": { 61 | "colName": "LastName", 62 | "custom": "", 63 | "operation": "==", 64 | "type": "basic", 65 | "value": "Cruise" 66 | }, 67 | "2": { 68 | "colName": "Age", 69 | "custom": "", 70 | "operation": ">", 71 | "type": "basic", 72 | "value": "30" 73 | }, 74 | "3": { 75 | "colName": "", 76 | "custom": "someFunction(convertHeightToCms(height))", 77 | "operation": "", 78 | "type": "custom", 79 | "value": "" 80 | } 81 | } 82 | } 83 | 84 | ``` 85 | 86 | ### Customization 87 | * You can easily customize the look and feel of the query builder by changing the following variables in the queryBuilder.scss file as per your requirement: 88 | 89 | ``` 90 | $border-color: #EAEAEA; // border color for the dropdown & the list inside the dropdown 91 | $label-background: #444; //background of label showing operators (AND, OR, +, - ,etc) 92 | $text-color: #333; // Color of text 93 | $light-gray: #FAFAFA; // Color of popover header 94 | ``` 95 | * Run ```gulp sass``` to get the complied css file from css/multiselectdropdown.css 96 | 97 | ### Demo 98 | visit http://fauzankhan.github.io/angular-query-builder/ to see the Query Builder in action 99 | -------------------------------------------------------------------------------- /src/js/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('app', ['ngQueryBuilder']); 2 | 3 | app.controller('masterController', ['$scope', function($scope){ 4 | $scope.queryBuilder = {}; 5 | $scope.columns = ['FirstName', 'LastName', 'Age', 'Gender', 'DOB', 'Major', 'School']; 6 | $scope.operations = ['>', '<', '<=', '>=', '==', '=', 'is']; 7 | 8 | $scope.temp = { 9 | "expression":"((0AND1)OR(2AND3))", 10 | "operands": 11 | [ 12 | { 13 | "type":"basic", 14 | "colName":"FirstName", 15 | "operation":"is", 16 | "value":"'Tom'", 17 | "custom":"" 18 | }, 19 | { 20 | "type":"basic", 21 | "colName":"LastName", 22 | "operation":"is", 23 | "value":"'Cruise'", 24 | "custom":"" 25 | }, 26 | { 27 | "type":"basic", 28 | "colName":"Age", 29 | "operation":">", 30 | "value":"40", 31 | "custom":"" 32 | }, 33 | { 34 | "type":"basic", 35 | "colName":"Gender", 36 | "operation":"is", 37 | "value":"'Male'", 38 | "custom":"" 39 | } 40 | ], 41 | "bracketIds": [2,1,1,3,3,2] 42 | } 43 | }]); -------------------------------------------------------------------------------- /src/js/queryBuilder.js: -------------------------------------------------------------------------------- 1 | ;(function(angular) { 2 | 3 | var app = angular.module("ngQueryBuilder", []); 4 | 5 | app.directive('queryBuilder', ['$compile', 'templatesFactory', function($compile, templatesFactory) { 6 | return { 7 | restrict: 'EA', 8 | scope: { 9 | 'data': '=', // Object in which the query will be reflected 10 | 'columns': '=', //Columns for building query 11 | 'operations': '=', //Operations which are to be applied on columns 12 | }, 13 | link: function(scope, ele, attr) { 14 | var query = scope.data || {}; 15 | var getTemplates = templatesFactory.get; 16 | var buildTemplate = templatesFactory.build; 17 | var currIndexTemp = query.expression ? query.expression.match(/[^A-Za-z()]/g) : 0; 18 | 19 | //Object template for custom query 20 | var customQuery = { 21 | type: 'custom', 22 | colName: '', 23 | operation: '', 24 | value: '', 25 | custom: '' 26 | } 27 | 28 | //Object template for basic query 29 | var basicQuery = { 30 | type: 'basic', 31 | colName: '', 32 | operation: '', 33 | value: '', 34 | custom: '' 35 | } 36 | 37 | scope.query = query; 38 | scope.currIndex = query.expression ? currIndexTemp[currIndexTemp.length - 1] : 0; // currIndex refers to the index of last sub query 39 | query.expression = query.expression ? query.expression : null; 40 | query.operands = query.operands || [angular.copy(basicQuery)]; // if no operands are present then push a basicQuery to oprands array 41 | query.bracketIds = query.bracketIds || []; //If bracketIds are not present initialize it to an empty array 42 | 43 | //Hides/shows Popover 44 | scope.togglePopover = function(event) { 45 | var isVisible = angular.element(event.target).closest('.popover-parent').hasClass('active'); 46 | angular.element('.popover-parent').removeClass('active'); 47 | if (!isVisible) 48 | angular.element(event.target).closest('.popover-parent').addClass('active'); 49 | } 50 | 51 | //Adds a new Sub Query 52 | scope.newEntry = function(event) { 53 | scope.currIndex = parseInt(scope.currIndex); 54 | scope.currIndex += 1; 55 | currIndex = scope.currIndex; 56 | scope.query.operands.push(angular.copy(basicQuery)); 57 | var templates = getTemplates(currIndex); 58 | ele.find('.query-builder-wrapper').append($compile(templates.operator)(scope)); 59 | ele.find('.query-builder-wrapper').append($compile(templates.operandSelection)(scope)); 60 | scope.query.expression = buildExpression(); 61 | } 62 | 63 | //Changes type of Sub QUery 64 | scope.changeQueryType = function(index, queryType) { 65 | if (queryType == 'basic') 66 | scope.query.operands[index] = angular.copy(basicQuery); 67 | else 68 | scope.query.operands[index] = angular.copy(customQuery); 69 | } 70 | 71 | //Changes operator between two sub queries 72 | scope.changeOperator = function(newOperator, event) { 73 | var operatorWrapper = angular.element(event.target).closest('.operator-wrapper'); 74 | operatorWrapper.find('.operator').text(newOperator); 75 | operatorWrapper.removeClass('active'); 76 | scope.query.expression = buildExpression(); 77 | console.log(scope.query); 78 | } 79 | 80 | //Calculates position of brackets and adds them in the DOM 81 | scope.addBrackets = function(event) { 82 | var parent = angular.element(event.target).closest('.operator-wrapper'); 83 | var operatorId = parent.attr('data-operator-id'); 84 | var next = parent.next(); 85 | var prev = parent.prev(); 86 | var leftBracket = '('; 87 | var rightBracket = ')'; 88 | // if next element is not an opening bracket then add a bracket 89 | if (!next.hasClass('bracket opening-bracket')) { 90 | next.after(rightBracket); 91 | } else { 92 | //if next element is a bracket the recursively iterate to the next of next elements 93 | //counting the no. of opening and closing brackets encountered 94 | //when they become equal add another colsing bracket 95 | next = next.next(); 96 | openingBracketsCount = 1; 97 | while (next.length > 0) {; 98 | if (next.hasClass('bracket opening-bracket')) { 99 | openingBracketsCount += 1; 100 | } else if (next.hasClass('bracket closing-bracket')) { 101 | openingBracketsCount -= 1; 102 | if (openingBracketsCount == 0) { 103 | angular.element(next).after(rightBracket); 104 | break; 105 | } 106 | } 107 | next = next.next(); 108 | } 109 | } 110 | // if prev element is not a closing bracket then add a bracket 111 | if (!prev.hasClass('bracket closing-bracket')) { 112 | prev.before(leftBracket); 113 | } else { 114 | //if prev element is a bracket the recursively iterate to the prev of prev elements 115 | //counting the no. of opening and closing brackets encountered 116 | //when they become equal add another opening bracket 117 | prev = prev.prev(); 118 | closingBracketsCount = 1; 119 | while (prev.length > 0) { 120 | if (prev.hasClass('bracket closing-bracket')) { 121 | closingBracketsCount += 1; 122 | } else if (prev.hasClass('bracket opening-bracket')) { 123 | closingBracketsCount -= 1; 124 | if (closingBracketsCount == 0) { 125 | angular.element(prev).before(leftBracket); 126 | break; 127 | } 128 | } 129 | prev = prev.prev(); 130 | } 131 | } 132 | parent.removeClass('active'); 133 | getBracketIds(); 134 | scope.query.expression = buildExpression(); 135 | } 136 | 137 | //Removes brackets corresponding to a sub query 138 | scope.removeBrackets = function(event) { 139 | var parent = angular.element(event.target).closest('.operator-wrapper'); 140 | var operatorId = parent.attr('data-operator-id'); 141 | var builderWrapper = parent.closest('.query-builder-wrapper'); 142 | builderWrapper.find('.bracket[data-bracket-id="' + operatorId + '"]').remove(); 143 | parent.removeClass('active'); 144 | getBracketIds(); 145 | scope.query.expression = buildExpression(); 146 | } 147 | 148 | //Gets the data-bracket-Ids of current brackets in the DOM 149 | var getBracketIds = function() { 150 | var brackets = ele.find('.query-builder-wrapper').find('.bracket'); 151 | var temp = []; 152 | for (var i = 0; i < brackets.length; i++) { 153 | bracketId = angular.element(brackets[i]).attr('data-bracket-id'); 154 | temp.push(parseInt(bracketId)); 155 | } 156 | scope.query.bracketIds = temp; 157 | } 158 | 159 | //Builds an expression corresponding to current query 160 | var buildExpression = function() { 161 | var expression = []; 162 | var temp = angular.element('.query-builder-wrapper').children(); 163 | for (var i = 0; i < temp.length; i++) {; 164 | var elem = angular.element(temp[i]); 165 | var eleText = elem.text(); 166 | if (elem.hasClass('bracket')) { 167 | expression.push(eleText); 168 | } else if (elem.hasClass('operator-wrapper')) { 169 | eleText = elem.find('.operator').text(); 170 | expression.push(eleText); 171 | } else { 172 | eleText = elem.attr('data-opererand-id'); 173 | expression.push(parseInt(eleText)); 174 | } 175 | } 176 | return expression.join(''); 177 | } 178 | 179 | ele.append('
'); 180 | ele.append('
'); 181 | ele.find('.add-sub-query').append($compile(getTemplates(0).addMoreBtn)(scope)); 182 | if (!scope.query.expression) { 183 | ele.find('.query-builder-wrapper').append($compile(getTemplates(0).operandSelection)(scope)); 184 | } else { 185 | buildTemplate(ele, scope.query.expression, scope.query.operands, scope.query.bracketIds, scope); 186 | } 187 | angular.element('.query-builder-wrapper').on('click', '.popover-parent', function(e) { 188 | e.stopPropagation(); 189 | }); 190 | angular.element('body').on('click', function() { 191 | angular.element('.query-builder-wrapper .popover-parent').removeClass('active'); 192 | }) 193 | } 194 | } 195 | }]); 196 | 197 | app.factory('templatesFactory', ['$compile', function($compile) { 198 | 199 | //Returns templates for sub query popover, operation popover and add more button 200 | var getTemplates = function(currIndex) { 201 | currIndex = parseInt(currIndex); 202 | var templates = {}; 203 | templates.addMoreBtn = '+'; 204 | templates.operator = '
' + 205 | 'AND' + 206 | '
' + 207 | '
' + 208 | '' + 209 | '+' + 210 | '-' + 211 | '*' + 212 | '/' + 213 | 'AND' + 214 | 'OR' + 215 | '(brackets)' + 216 | '(brackets)' + 217 | '
' + 218 | '
' + 219 | '
'; 220 | templates.operandSelection = '
' + 221 | '' + 222 | '' + 223 | '{{query.operands[' + currIndex + '].colName+" "+query.operands[' + currIndex + '].operation+" "+query.operands[' + currIndex + '].value+""+query.operands[' + currIndex + '].custom}}'+ 224 | '' + 225 | 'Enter Query' + 226 | '' + 227 | '
' + 228 | '
' + 229 | '' + 230 | '
' + 231 | '
' + 232 | 'Query Type' + 233 | '
' + 234 | '
' + 235 | '
' + 236 | '' + 237 | '' + 238 | '
' + 239 | '
' + 240 | '' + 241 | '' + 242 | '
' + 243 | '
' + 244 | '
' + 245 | '
' + 246 | '
' + 247 | '
' + 248 | '' + 249 | ''+ 250 | '
' + 251 | '
' + 252 | '' + 253 | '' + 255 | '
' + 256 | '
' + 257 | '' + 258 | '' + 259 | '
' + 260 | '
' + 261 | '
' + 262 | '' + 263 | '
' + 264 | '
' + 265 | '' + 266 | '
' + 267 | '
' + 268 | '
' + 269 | '
' + 270 | '
'; 271 | return templates; 272 | } 273 | 274 | //Builds/Prepopulate query in DOM in case scope.data is present 275 | var buildTemplate = function(ele, expression, operands, bracketIds, scope) { 276 | var bracketCount = 0; 277 | var expArray = expression.split(/((?:\(|\)|[A-Z]+|\d+))/g); 278 | var currOperand = 0; 279 | expressionArray = expArray.filter(function(e) { 280 | return e != ''; 281 | }) 282 | for (var i = 0; i < expressionArray.length; i++) { 283 | var temp = expressionArray[i]; 284 | var template = getTemplates(i); 285 | if (temp == '(') { 286 | var bracketId = bracketIds[bracketCount]; 287 | var leftBracket = '('; 288 | ele.find('.query-builder-wrapper').append(leftBracket); 289 | bracketCount += 1; 290 | } else if (temp == ')') { 291 | var bracketId = bracketIds[bracketCount]; 292 | var rightBracket = ')'; 293 | ele.find('.query-builder-wrapper').append(rightBracket); 294 | bracketCount += 1; 295 | } else if (temp == 'AND' || temp == 'OR') { 296 | var template = getTemplates(parseInt(currOperand)); 297 | ele.find('.query-builder-wrapper').append($compile(template.operator)(scope)); 298 | } else if (!isNaN(temp)) { 299 | var template = getTemplates(parseInt(temp)); 300 | ele.find('.query-builder-wrapper').append($compile(template.operandSelection)(scope)); 301 | currOperand = parseInt(currOperand); 302 | currOperand += 1; 303 | } 304 | } 305 | } 306 | return { 307 | get: getTemplates, 308 | build: buildTemplate 309 | } 310 | }]) 311 | 312 | })(angular); -------------------------------------------------------------------------------- /src/scss/demo.scss: -------------------------------------------------------------------------------- 1 | 2 | $border-color: #EAEAEA; 3 | $light-gray: #FAFAFA; 4 | 5 | body{ 6 | background: #F5F5F5; 7 | font-family: "Source Sans Pro", arial; 8 | //overflow-y: hidden; 9 | min-height: 100vh; 10 | header{ 11 | text-align: center; 12 | color: #444; 13 | padding-top: 10px; 14 | font-size: 20px; 15 | h1{ 16 | margin-top: 8px; 17 | line-height: 45px; 18 | } 19 | small{ 20 | color: #999; 21 | font-size: 25px; 22 | font-weight: 300; 23 | span, a{ 24 | text-decoration: none; 25 | color: #BCBCBC; 26 | font-weight: 400; 27 | transition: color 500ms ease; 28 | border-bottom: 1px dotted #BCBCBC; 29 | &:hover{ 30 | text-decoration: none; 31 | color: #666; 32 | border-bottom: 1px dotted #666; 33 | }; 34 | } 35 | } 36 | } 37 | .demo-container{ 38 | margin: 0 auto; 39 | width: 1220px; 40 | text-align: center; 41 | } 42 | .flash-message{ 43 | /*background: rgb(0, 113, 181, .8);*/ 44 | padding: 20px; 45 | max-width: 280px; 46 | border: 1px solid #CCC; 47 | border-radius: 3px; 48 | background: #FFF; 49 | position: fixed; 50 | bottom: 50px; 51 | border-radius: 0px; 52 | font-size: 12px; 53 | color: #666; 54 | transition: all 900ms ease; 55 | right: 50px; 56 | font-size: 18px; 57 | word-break: break-all; 58 | overflow: hidden; 59 | &.vis{ 60 | opacity: 1; 61 | } 62 | &.hidden{ 63 | opacity: 0; 64 | } 65 | h3{ 66 | color: #999; 67 | font-size: 16px; 68 | margin: 0; 69 | margin-bottom: 10px; 70 | } 71 | } 72 | footer{ 73 | text-align: center; 74 | position: absolute; 75 | bottom: 0; 76 | left: 0; 77 | right: 0; 78 | padding-bottom: 40px; 79 | img{ 80 | width: 20px; 81 | vertical-align: -4px; 82 | margin-right: 3px; 83 | } 84 | a{ 85 | color: #008CCD; 86 | border-radius: 3px; 87 | text-decoration: none; 88 | padding: 14px 18px; 89 | background: #FFF; 90 | border: 1px solid #EAEAEA; 91 | transition: background 200ms ease; 92 | &:hover{ 93 | background: transparent; 94 | } 95 | } 96 | } 97 | } 98 | 99 | .button{ 100 | color: #008CCD; 101 | border-radius: 3px; 102 | text-decoration: none; 103 | padding: 14px 18px; 104 | background: #FFF; 105 | border: 1px solid #EAEAEA; 106 | transition: background 200ms ease; 107 | cursor: pointer; 108 | &:hover{ 109 | background: transparent; 110 | } 111 | } -------------------------------------------------------------------------------- /src/scss/queryBuilder.scss: -------------------------------------------------------------------------------- 1 | $white: #FFF; 2 | $text-color: #333; 3 | $label-background: #444; 4 | $border-color: #EAEAEA; 5 | $light-gray: #FAFAFA; 6 | 7 | @mixin transform($transformation) { 8 | -webkit-transform: $transformation; 9 | moz-transform: $transformation; 10 | o-transform: $transformation; 11 | transform: $transformation; 12 | } 13 | 14 | .query-builder-wrapper { 15 | text-align: left; 16 | position: relative; 17 | display: inline-block; 18 | color: $text-color; 19 | .query-popover { 20 | border: 1px solid $border-color; 21 | background: $white; 22 | border-radius: 3px; 23 | } 24 | .operator-wrapper { 25 | position: relative; 26 | display: inline-block; 27 | .operator { 28 | margin: 0 10px; 29 | } 30 | .operator-popover { 31 | display: none; 32 | position: absolute; 33 | top: -55px; 34 | @include transform(translateX(-35%)); 35 | left: 0; 36 | right: 0; 37 | margin: 0 auto; 38 | padding: 5px; 39 | width: 180px; 40 | text-align: center; 41 | .popover-body { 42 | position: relative; 43 | .triangle { 44 | width: 15px; 45 | height: 15px; 46 | display: inline-block; 47 | position: absolute; 48 | bottom: -13px; 49 | left: 46%; 50 | @include transform(rotate(45deg)); 51 | border-right: 1px solid $border-color; 52 | border-bottom: 1px solid $border-color; 53 | background: $white; 54 | } 55 | } 56 | .operator-option { 57 | display: inline-block; 58 | border-radius: 2px; 59 | cursor: pointer; 60 | margin: 3px 1px; 61 | padding: 3px; 62 | font-size: 12px; 63 | border: 1px solid $border-color; 64 | } 65 | } 66 | &.active { 67 | .operator-popover { 68 | display: inline-block; 69 | z-index: 9; 70 | } 71 | } 72 | } 73 | .operand-selection { 74 | position: relative; 75 | display: inline-block; 76 | .button { 77 | margin: 0; 78 | margin-bottom: 10px; 79 | } 80 | .operand-popover { 81 | display: none; 82 | position: absolute; 83 | width: 300px; 84 | top: 50px; 85 | left: 0; 86 | right: 0; 87 | margin: 0 auto; 88 | .operand-popover-content { 89 | position: relative; 90 | .triangle { 91 | position: absolute; 92 | border-top: 1px solid $border-color; 93 | border-right: 1px solid $border-color; 94 | background: $light-gray; 95 | height: 10px; 96 | width: 10px; 97 | top: -6px; 98 | left: 20px; 99 | @include transform(rotate(-45deg)); 100 | } 101 | } 102 | .popover-body { 103 | padding: 15px 12px; 104 | .operation-wrapper { 105 | width: 48%; 106 | margin-right: 5px; 107 | display: inline-block; 108 | } 109 | .value-wrapper { 110 | width: 48%; 111 | margin-left: 5px; 112 | display: inline-block; 113 | } 114 | .done-btn-wrapper { 115 | text-align: right; 116 | } 117 | label { 118 | font-size: 12px; 119 | color: $text-color; 120 | } 121 | select { 122 | width: 100%; 123 | height: 35px; 124 | border-radius: 2px; 125 | border: 1px solid $border-color; 126 | box-shadow: none; 127 | padding: 5px; 128 | margin-bottom: 10px; 129 | } 130 | input { 131 | width: 100%; 132 | height: 30px; 133 | border: 1px solid $border-color; 134 | border-radius: 3px; 135 | margin-bottom: 15px; 136 | } 137 | textarea { 138 | width: 97%; 139 | height: 110px; 140 | border: 1px solid $border-color; 141 | border-radius: 3px; 142 | margin-bottom: 15px; 143 | } 144 | } 145 | .popover-header { 146 | background: $light-gray; 147 | position: relative; 148 | border-bottom: 1px solid $border-color; 149 | border-radius: 3px 3px 0 0; 150 | .heading { 151 | font-size: 16px; 152 | font-weight: bold; 153 | color: lighten($text-color, 30%); 154 | padding: 15px 10px; 155 | border-right: 1px solid $border-color; 156 | display: inline-block; 157 | } 158 | .query-types { 159 | display: inline-block; 160 | .query-type { 161 | display: inline-block; 162 | padding: 0 12px; 163 | label { 164 | cursor: pointer; 165 | vertical-align: 2px; 166 | color: lighten($text-color, 30%);; 167 | } 168 | } 169 | } 170 | } 171 | .col-name-wraper { 172 | width: 100%; 173 | } 174 | } 175 | &.active { 176 | .operand-popover { 177 | display: inline-block; 178 | z-index: 9; 179 | } 180 | } 181 | } 182 | .bracket { 183 | font-size: 32px; 184 | &.left-bracket { 185 | // margin-left: 20px; 186 | margin-right: 10px; 187 | } 188 | &.right-bracket { 189 | margin-left: 10px; 190 | // margin-right: 20px; 191 | } 192 | } 193 | .query-builder-label { 194 | font-size: 12px; 195 | font-weight: 600; 196 | border-radius: 5px; 197 | padding: 2px 8px; 198 | cursor: pointer; 199 | &.neutral { 200 | background: $label-background; 201 | color: $white; 202 | } 203 | } 204 | .query-builder-btn { 205 | color: $text-color; 206 | border-radius: 3px; 207 | text-decoration: none; 208 | padding: 10px 15px; 209 | background: $white; 210 | transition: background 200 ms ease; 211 | cursor: pointer; 212 | border: 1px solid $border-color; 213 | position: relative; 214 | display: inline-block; 215 | margin-bottom: 15px; 216 | &.no-margins { 217 | margin: 0; 218 | } 219 | &.small { 220 | font-size: 12px; 221 | } 222 | &.query-builder-btn-dropdown { 223 | padding-right: 35px; 224 | &::after { 225 | content: ''; 226 | position: absolute; 227 | top: 48%; 228 | right: 0; 229 | margin-right: 18px; 230 | width: 0; 231 | height: 0; 232 | border-right: 4px solid transparent; 233 | border-left: 4px solid; 234 | border-left-color: transparent; 235 | border-top: 4px solid $text-color; 236 | } 237 | } 238 | } 239 | } 240 | 241 | .add-sub-query { 242 | display: block; 243 | margin-bottom: 20px; 244 | .new-entry-btn { 245 | cursor: pointer; 246 | height: 20px; 247 | width: 20px; 248 | text-align: center; 249 | border-radius: 100%; 250 | background: $label-background; 251 | color: $white; 252 | font-size: 22px; 253 | font-weight: bold; 254 | display: inline-block; 255 | line-height: 20px; 256 | } 257 | } --------------------------------------------------------------------------------