\
663 | "
664 | },
665 |
666 | // *********************************************************************************
667 | // *********************************************************************************
668 | // ** PUBLIC METHODS
669 | // *********************************************************************************
670 | // *********************************************************************************
671 |
672 | // IDEA - ONLY EVER KEEP 30 ROWS ON THE DOM, REMOVE TOP AND BOTTOM ROWS AND STORE IN MEMORY
673 | // ONLY CREATE 30 ROWS AT A TIME, NEVER MORE. FILTERING IS ALREADY DONE ON MEMORY, BUT WOULD NEED
674 | // TO ADD BACK ROWS THAT ARE IN MEMORY AND NOT IN THE DOM, SHOULD BE EASY.
675 |
676 | // public methods
677 | load : function(opts) {
678 | var self = this, packet, promise, rowHtml = "", colHtml = "",
679 | col = 0, key = 0, pKey, rowCol = 0, cellValue, checked = 0,
680 | cellClass = "", type;
681 |
682 | // if we are reloading with options pass them in
683 | // if(opts) this.grid(opts);
684 | if(opts) this.opts = this.extend(this.opts,opts);
685 |
686 | // register loadStart callback
687 | $(this.el).trigger("loadStart");
688 |
689 | // we have some more data than in this.opts that we wanna send to ajax
690 | packet = $.extend({
691 | cols : this.cols
692 | },this.opts);
693 |
694 | // cache the el because self.el changes some where?
695 | var el = self.el
696 | var cellTypes = self.cellTypes;
697 |
698 | // show loading box
699 | this.loadingDialog = this.notify("Loading");
700 |
701 | /////////////////////////
702 | // LOAD SELECT BOXES
703 | ////////////////////////
704 | var selCol, colName, selectCols = [], selectPromise = $.Deferred();
705 | for(colName in this.columns) {
706 | selCol = this.columns[colName];
707 | if(typeof selCol.type != "undefined" && selCol.type == "select") {
708 | selectCols.push(colName);
709 | }
710 | }
711 | // get all the drop downs, store the promise in case we wanna check this
712 | if(selectCols.length && !self.selects) {
713 | selectPromise = $.post(this.opts.action,{select : true, cols : selectCols},function(data) {
714 | // by saving the data, we dont ever have to do this ajax call again til page reload
715 | self.selects = data;
716 | return true;
717 | });
718 | } else {
719 | selectPromise.resolve();
720 | }
721 |
722 |
723 | promise = $.post(this.opts.action,packet,function(data) {
724 | self.el = el; // fixes some problem i dont know :(
725 | self.cellTypes = cellTypes;
726 | var $grid = $(self.el),
727 | $columns = $grid.find(".columns");
728 |
729 | // store some data we got back
730 | self.totalRows = data.nRows;
731 | self.start = data.start;
732 | self.end = Math.min(data.end,data.nRows);
733 | self.saveable = data.saveable;
734 |
735 | self.opts.orderBy = data.order_by;
736 | self.opts.sort = data.sort;
737 |
738 | // were gonna build the table in a string, then append it
739 | // this is 1000000x times faster than dom manipulation
740 | self.rows = data.rows;
741 |
742 | // when our ajax is done, move on.
743 | selectPromise.done(function() {
744 |
745 | // it will be an object if it has data
746 | if(!Array.isArray(data.rows)) {
747 |
748 | // build the table in column form, instead of row form
749 | for(col in self.columns) {
750 |
751 | // options on the column
752 | colOpts = self.columns[col];
753 |
754 | // opening col div
755 | colHtml += "
";
756 |
757 | // blank cells mess things up
758 | if(colOpts.header == "") colOpts.header = " "
759 |
760 | // add header cell with resizer, sortable bar and blank cell
761 | // this is only the header and not the whole column because we want the ability
762 | // to keep adding strings to the return for speed
763 | colHtml += self._render("columnHeader")(colOpts);
764 |
765 | for(key in data.rows) {
766 | pkey = key.substr(1);
767 | row = data.rows[key];
768 | for(rowCol in row) {
769 | if(rowCol === col) {
770 |
771 | // main value
772 | cellValue = row[col],
773 | cellClass = "";
774 |
775 | // setup some types
776 | if(typeof self.cellTypes[colOpts.type] == "function") {
777 |
778 | typeOpts = self.cellTypes[colOpts.type](cellValue,colOpts,self);
779 |
780 | // protect a no return
781 | if(typeof typeOpts == "undefined") typeOpts = {cellValue : cellValue,cellClass: ""};
782 |
783 | cellValue = typeOpts.cellValue;
784 | cellClass = typeOpts.cellClass;
785 | }
786 |
787 |
788 | // empty cells kinda mess with things
789 | if(cellValue == "") cellValue = " ";
790 |
791 | // add linking
792 | // this is not a type because you can link anything by adding href
793 | if(colOpts.href) {
794 | // make some tokens for use in the href
795 | var linkTokens = {value : cellValue}
796 | // add all the column values, column.Title i.e.
797 | for(var aCol in row) linkTokens["columns."+aCol] = row[aCol];
798 | // render the href with the tokens
799 | var href = self._render(colOpts.href)(linkTokens);
800 | // wrap the cell value in an a tag with the rendered href
801 | cellValue = ""+cellValue+"";
802 | }
803 |
804 | // create the cell from template
805 | colHtml += self._render("cell")({
806 | cl : cellClass,
807 | id : pkey,
808 | col : col,
809 | val : cellValue
810 | });
811 | }
812 | }
813 | }
814 |
815 | colHtml += "
";
816 | }
817 | } else {
818 | colHtml = "No Rows";
819 | }
820 |
821 | // hide our loading
822 | $grid.find(".gridLoading").hide();
823 |
824 | // place all the content
825 | $columns.html(colHtml);
826 |
827 | // do things after ajax
828 | self._afterLoad();
829 |
830 | // register loadComplate Callback
831 | $(self.el).trigger("loadComplete",self);
832 |
833 | });
834 |
835 | },"json");
836 |
837 | return promise;
838 | },
839 |
840 | // [none | success | warning | important | info | inverse]
841 | // helper dialog alert function
842 | alert : function(type, title, msg) {
843 | return Dialog.inherit({
844 | tmpl : "alert",
845 | type: type,
846 | title: title,
847 | msg : msg,
848 | grid: this
849 | }).show();
850 | },
851 |
852 | // helper dialog notify method
853 | notify : function(msg, ms) {
854 | var self = this;
855 | // our opts
856 | var opts = {msg:msg, grid:this};
857 | // if we wanted a timer
858 | if(ms) opts.autoFadeTimer = ms;
859 | // create and show
860 | return Dialog.inherit(opts).show();
861 | },
862 |
863 |
864 | // shortcut error function
865 | error : function(msg) {
866 | return this.alert("important", "Error!", msg);
867 | },
868 |
869 | // a confrim dialog box
870 | confirm : function(msg, callback) {
871 | var $grid = $(this.el);
872 | var dialog = Dialog.inherit({
873 | tmpl : "confirm",
874 | msg : msg,
875 | grid : this
876 | }).show();
877 |
878 | // add our confirm ok
879 | dialog.$dialog.one("click",".confirmOk",callback);
880 |
881 | return dialog;
882 | },
883 |
884 | // debouncing the typing
885 | _filter : function(e,el) {
886 | var self = this,
887 | $el = $(el);
888 | // store on pager
889 | this.pager.query = $el.val();
890 | // start typing timer
891 | clearTimeout(this.debounce);
892 | this.debounce = setTimeout(function() {
893 | self.filter( $el.val() )
894 | },150);
895 | },
896 |
897 | // finds matches in the dom as fast as i know how
898 | // do intelligent searches with column:
899 | // right click a column header and choose "search on" which would fill out the search filter
900 | filter : function(val) {
901 | var $grid = $(this.el),
902 | $all = $grid.find("[data-row]"),
903 | $cols = $grid.find(".col");
904 |
905 | if(val) {
906 | var matches = [],
907 | val = val.toLowerCase();
908 | for(id in this.rows) {
909 | var row = this.rows[id],
910 | id = id.substring(1);
911 | for(key in row) {
912 | var string = row[key].toLowerCase();
913 | if(~string.indexOf(val) && !~matches.indexOf(id)) matches.push(id);
914 | }
915 | }
916 |
917 | $all.hide();
918 | $all.removeClass("topMargin");
919 |
920 | if(matches.length) {
921 | $(".cell.temp").remove();
922 | for(i=0;i ");
932 | }
933 | } else {
934 | $all.show();
935 | $all.removeClass("topMargin");
936 | }
937 | // we need to recache the scroll height account for scrollbars
938 |
939 | //this.scrollHeight = $grid.find(".columns")[0].scrollHeight;
940 | this.aColumnHeight = $grid.children(".columns").children(".col:first").height();
941 | this._equalize();
942 | },
943 |
944 | // adds a column with options to the grid
945 | // runs a function on the value so you can pass in as it builds
946 | // opts : {width, insertAt, cellClass}
947 | addColumn : function(col, opts, fn) {
948 |
949 | // if it already exists delete it
950 | if(this.colExists(col)) {
951 | $(this.el).find(".col[col='"+col+"']").remove();
952 | }
953 |
954 | // create the new column from template
955 | var newCol = "
",
956 | $newCol,pkey;
957 |
958 | // column header stuff
959 | var header = opts.header || col;
960 | newCol += this._render("columnHeader")({col : col, header : header});
961 |
962 | // if the value fn wasn't passed, use blank
963 | if(typeof fn != "function") fn = function(i) { return " " }
964 |
965 | // add in rows;
966 | var i = 0;
967 | for(key in this.rows) {
968 | pkey = key.substr(1);
969 | newCol += this._render("cell")({
970 | cl : opts.cellClass || "",
971 | id : pkey,
972 | col : col,
973 | val : fn(i, this.rows[key])
974 | });
975 | i++;
976 |
977 | }
978 | // cap off our col
979 | newCol += "
";
980 |
981 | // DOMit
982 | $newCol = $(newCol);
983 |
984 | // add to the DOM
985 | this._insertCol($newCol,opts.insertAt);
986 |
987 | // note that this is dynamically added
988 | opts.dynamic = true;
989 |
990 | // if we passed in options, add those to the columns object
991 | this.columns[col] = opts;
992 |
993 | // resize with our new column
994 | this._cacheSize();
995 | this._equalize();
996 |
997 | // return new col
998 | return $newCol;
999 |
1000 | },
1001 |
1002 | /*
1003 | addRow : function() {
1004 | var $grid = $(this.el),
1005 | $cols = $grid.find(".col"),
1006 | i = 0,
1007 | col = null,
1008 | $col = null,
1009 | $cell = null;
1010 |
1011 | for(i=0; i<$cols.length; i++) {
1012 | col = $cols[i],
1013 | colName = col.getAttribute("col"),
1014 | $col = $(col),
1015 | $cell = $col.find(".cell:eq(2)");
1016 |
1017 | //var $new = $(this._render("cell")({
1018 | // cl : "level2",
1019 | // id : 0,
1020 | // col : colName,
1021 | // val : " "
1022 | //}));
1023 |
1024 |
1025 | //$new.insertAfter($cell);
1026 |
1027 | }
1028 |
1029 |
1030 |
1031 | // insert new div in first col
1032 | //var $cell = $grid.find(".col:first").find(".cell.level2");
1033 | //$cell
1034 | //.css("overflow","none")
1035 | //.html(" .col:nth-child(" + ++i + ")").before($col);
1050 | } else {
1051 | if(typeof this.columns[i] != "undefined") {
1052 | var k = 0, j;
1053 | // find the index of this column
1054 | for(j in this.columns) { k++; if(j === i) break };
1055 | // insert at that column
1056 | this._insertCol($col,k);
1057 | } else {
1058 | console.log("Trying to inserter after column ["+i+"], not found, inserting at end");
1059 | this._insertCol($col);
1060 | }
1061 | }
1062 | },
1063 |
1064 | // bool if a column exists or not
1065 | colExists : function(col) {
1066 | return !(typeof this.columns[col] == "undefined");
1067 | },
1068 |
1069 | /*
1070 | cellClick : function($cell) {
1071 | var $tr = $cell.closest("tr"),
1072 | tr = $tr[0],
1073 | id = tr.getAttribute("data-row"),
1074 | rowData= this.rows["_"+id];
1075 | $(this.el).trigger("cellClick", $cell);
1076 | },
1077 |
1078 | rowClick : function($cells) {
1079 | $(this.el).trigger("rowClick", $cells);
1080 | },
1081 | */
1082 |
1083 | // returns a jQuery object of cells from the passed column
1084 | getCells : function(col) {
1085 | if(typeof col == "string") {
1086 | return $(this.el).find("[col='"+col+"'].cell:not(.headerCell)");
1087 | } else {
1088 | return col.find(".cell:not(.headerCell)");
1089 | }
1090 | },
1091 |
1092 | // gets all the cells from a given row id
1093 | getRow : function(id) {
1094 | return $(this.el).find(".grid-row-"+id);
1095 | },
1096 |
1097 | getRowData: function(id) {
1098 | return this.rows["_"+id];
1099 | },
1100 |
1101 | // when you hover a row
1102 | rowHover : function(e,el) {
1103 | var id = el.getAttribute("data-row");
1104 | $(this.getRow(id)).addClass("row-hover");
1105 | },
1106 |
1107 | // row mouse out
1108 | rowHoverOut : function(e,el) {
1109 | var id = el.getAttribute("data-row");
1110 | $(this.getRow(id)).removeClass("row-hover");
1111 | },
1112 |
1113 | deleteRow : function(e,el) {
1114 | e.preventDefault();
1115 | var self = this,
1116 | $cell = $(el).closest(".cell"),
1117 | id = $cell[0].getAttribute("data-row");
1118 |
1119 | this.confirm("Are you sure you want to delete?", function() {
1120 | $.post(self.opts.action, {delete:true,id:id}, function(success) {
1121 | if(success) {
1122 | // function for timeout
1123 | var fadeRow = function() {
1124 | $(self.el).find(".grid-row-"+id).remove();
1125 |
1126 | // don't pop this up unless they pass it in
1127 | if(self.opts.deleteConfirm) {
1128 | self.alert("info", "Deleted!", "Row "+id+" has been deleted");
1129 | }
1130 | }
1131 | // fade this row out
1132 | $(self.el).find(".grid-row-"+id).fadeOut(500);
1133 | // after the fade, remove the row, dont do this in the callback, it will call many times
1134 | setTimeout(fadeRow,500);
1135 |
1136 | var rowData = self.rows["_"+id];
1137 | $(self.el).trigger("rowDelete",[self.getRow(id), rowData]);
1138 |
1139 | } else {
1140 | self.error("Failed to delete");
1141 | }
1142 | });
1143 | });
1144 | },
1145 |
1146 | // save
1147 | saveRow : function(e,el) {
1148 | e.preventDefault();
1149 | var self = this, i, rows = {};
1150 |
1151 | // get the rows we need from the rows object
1152 | var pkeys = [];
1153 | for(i=0; i< this.toSave.length; i++) {
1154 | var pkey = this.toSave[i]
1155 | rows[pkey] = this.rows["_"+pkey];
1156 | pkeys.push(pkey);
1157 | }
1158 |
1159 | // post save
1160 | $.post(this.opts.action,{
1161 | save : true,
1162 | json : rows,
1163 | saveable : self.saveable
1164 | }, function(res) {
1165 | if(res == 1) {
1166 | self.alert("info","Saved!",i + " Row(s) saved");
1167 | $(self.el).trigger("save",rows[pkey],res);
1168 | } else {
1169 | self.error(res);
1170 | $(self.el).trigger("saveFail",rows[pkey],res);
1171 | }
1172 |
1173 | });
1174 | },
1175 |
1176 | sort : function(e,el) {
1177 |
1178 | // hide all sortbars
1179 | $(this.el).find(".sortbar").hide();
1180 |
1181 | // toggle sort and store value
1182 | var $sortbar = $(el).find(".sortbar"),
1183 | col = el.getAttribute("col"),
1184 | sort = $sortbar.show().toggleClass("desc").hasClass("desc") ? "desc" : "asc";
1185 |
1186 | // get possible columns to sort on
1187 | var sortable = this.cols.split(",");
1188 | // dont sort on columns that are dynamic
1189 | if($.inArray(col,sortable) != -1) {
1190 |
1191 | // load the grid with new sorting
1192 | this.load({ sort : sort, orderBy : col });
1193 | };
1194 | },
1195 |
1196 | // updates the row (should be called as you type)
1197 | markForSaving : function(e,el) {
1198 |
1199 | // make sure the save is visible
1200 | $(this.el).find(".gridSave").removeClass("disabled");
1201 |
1202 | // get our row col and val
1203 | var div = $(el).closest(".cell")[0],
1204 | col = div.getAttribute("data-col"),
1205 | row = div.getAttribute("data-row")
1206 | val = el.value;
1207 |
1208 | // checkboxes dont need value, they need checked
1209 | if($(el).is(":checkbox")) val = ~~el.checked;
1210 |
1211 | // set the value on the object
1212 | this.rows["_"+row][col] = val;
1213 |
1214 | // add the row if its not there
1215 | if(!~this.toSave.indexOf(row)) this.toSave.push(row);
1216 | }
1217 | });
1218 |
1219 | var Dialog = Root.inherit({
1220 | type : "notify",
1221 | tmpl : "notify",
1222 | title : "",
1223 | msg : "",
1224 | grid : null,
1225 | autoFadeTimer : false,
1226 | blur : true,
1227 | $dialog : null,
1228 | _timer : 200, // match the css fade
1229 | _construct : function() {
1230 | // our grid el
1231 | var $grid = $(this.grid.el), self = this;
1232 | // render the type to our templates
1233 | this.$dialog = $(this.grid._render(this.tmpl)(this));
1234 | // if our dialog has a button, add an event
1235 | this.$dialog.find(".cancel,.confirmOk").one("click",function(e) {
1236 | e.preventDefault();
1237 | self.close();
1238 | });
1239 | // add to our grid
1240 | $grid.append(this.$dialog);
1241 | },
1242 | show : function() {
1243 | // our grid el
1244 | var $grid = $(this.grid.el), self = this;
1245 | // blur the bg if needed
1246 | if(this.blur) $grid.find(".columns").addClass("blur");
1247 | // show this guy
1248 | setTimeout(function() {
1249 | self.$dialog.addClass("show");
1250 | },50);
1251 | // setup for auto fade timer
1252 | if(this.autoFadeTimer) {
1253 | setTimeout(function() {
1254 | self.close();
1255 | },this.autoFadeTimer);
1256 | }
1257 | // return
1258 | return this;
1259 | },
1260 | close : function() {
1261 | var self = this, $grid = $(this.grid.el);
1262 | // fade out
1263 | this.$dialog.removeClass("show");
1264 | // blur out
1265 | if(this.blur) $grid.find(".columns").removeClass("blur");
1266 | // kill the element after x time
1267 | setTimeout(function() {
1268 | // remove the element
1269 | self.$dialog.remove();
1270 | },this._timer);
1271 | // return
1272 | return this;
1273 | }
1274 | });
1275 |
1276 | var Slider = Root.inherit({
1277 | thumb : null,
1278 | pager : null,
1279 | min : 0,
1280 | max : 100,
1281 | val : 0,
1282 | startX : 0,
1283 | _construct : function() {
1284 | this.onMove = $(this.pager.grid.el).hasClass("touch") ? "touchmove" : "mousemove";
1285 | this.onStart = $(this.pager.grid.el).hasClass("touch") ? "touchstart" : "mousedown";
1286 | this.onEnd = $(this.pager.grid.el).hasClass("touch") ? "touchend" : "mouseup";
1287 | // mouse down start
1288 | $(this.pager.el)._on(this.onStart, ".sliderThumb",this.start, this);
1289 | },
1290 | // since the pager is reconstructed each time, we need to update the DOM elements for slider
1291 | update : function() {
1292 | this.thumb = $(this.pager.el).find(".sliderThumb");
1293 | this.setVal(this.pager.currentPage);
1294 | this.max = this.pager.totalPages;
1295 | },
1296 | start : function(e,el) {
1297 |
1298 | // setup start
1299 | this.startX = e.clientX || e.originalEvent.touches[0].clientX;
1300 |
1301 | // mouse move to slide
1302 | $(this.pager.el)._on(this.onMove+".slider",this.slide,this);
1303 | $(document)._on(this.onEnd+".slider",this.stop,this);
1304 | },
1305 | stop : function() {
1306 | // remove the mousemove
1307 | $(this.pager.el).off(this.onMove+".slider");
1308 | // remove the mousup
1309 | $(document).off(this.onEnd+".slider");
1310 | // go to page when stopped
1311 | this.pager.goto(this.val);
1312 | },
1313 | setVal : function(val) {
1314 | var $thumb = $(this.thumb),
1315 | $track = $thumb.prev(),
1316 | trackLength = $track.width() - $thumb.width();
1317 |
1318 | // intify val
1319 | this.val = parseInt(val);
1320 | // calculate pos
1321 | var pos = (this.val * trackLength) / this.max;
1322 | // dont let it go below 0
1323 | if(pos < 0) pos = 0;
1324 | // set the thumb
1325 | $thumb.css("margin-left",pos);
1326 | },
1327 | slide : function (e,el) {
1328 | var self = this;
1329 | if(~["slider","sliderThumb","sliderSpan","sliderTrack"].indexOf(e.target.className)) {
1330 |
1331 | // touch fix
1332 | e.clientX = e.clientX || e.originalEvent.touches[0].clientX;
1333 |
1334 | // current left and new left
1335 | var $thumb = $(self.thumb),
1336 | mleft = parseFloat($thumb.css("margin-left")),
1337 | end = $thumb.prev().width() - $thumb.width();
1338 | pos = mleft + (e.clientX - this.startX);
1339 |
1340 | // protect upper edge
1341 | if(pos >= end) {
1342 | pos = end;
1343 | self.val = this.max
1344 |
1345 | // protect the lower edge
1346 | } else if(pos <= 0) {
1347 | pos = 1;
1348 | self.val = this.min;
1349 |
1350 | // all other cases
1351 | } else {
1352 | val = ~~((pos / end) * self.max);
1353 | self.val = val || 1; // can't go to 0
1354 | }
1355 |
1356 |
1357 |
1358 | // set the thumb
1359 | $(self.thumb).css("margin-left",pos);
1360 |
1361 | // reset start x
1362 | this.startX = e.clientX;
1363 |
1364 | // input the val
1365 | $(this.pager.el).find(".currentPage input").val( self.val );
1366 | }
1367 | }
1368 | })
1369 |
1370 | // the pager object
1371 | var Pager = Root.inherit({
1372 | el : null,
1373 | grid : null,
1374 | currentPage : 1,
1375 | totalPages : 1,
1376 | slider : null,
1377 | query : "",
1378 | _construct : function() {
1379 |
1380 | // call initial
1381 | this.update();
1382 |
1383 | },
1384 |
1385 | update : function() {
1386 | var self = this,
1387 | grid = this.grid,
1388 | $grid = $(grid.el),
1389 | nRows = grid.totalRows,
1390 | showing = grid.opts.nRowsShowing,
1391 | nPages = Math.ceil(nRows / showing),
1392 | page = parseInt(grid.opts.page),
1393 | $pager = null;
1394 |
1395 | // store some vars
1396 | this.currentPage = page;
1397 | this.totalPages = nPages;
1398 |
1399 | // setup
1400 | // for now, we do have to render the pager, just not show it
1401 | var pagerHtml = grid._render("pager")({
1402 | start : grid.start,
1403 | end : Math.min(grid.end,nRows),
1404 | nRows : nRows,
1405 | nPages : nPages,
1406 | page : page,
1407 | nextPage : page + 1,
1408 | secondToLastPage : nPages - 1,
1409 | lastPage : nPages,
1410 | search : this.query
1411 | });
1412 |
1413 | // make it the first time
1414 | if(!this.el) {
1415 | // create the element
1416 | $pager = $("
"+pagerHtml+"
");
1417 | $grid.append($pager);
1418 |
1419 | // set the element
1420 | this.el = $pager[0];
1421 |
1422 | // setup slider
1423 | var nRows = this.grid.totalRows,
1424 | showing = this.grid.opts.nRowsShowing,
1425 | nPages = Math.ceil(nRows / showing);
1426 |
1427 | this.slider = Slider.inherit({
1428 | pager : this,
1429 | thumb : $(this.el).find(".sliderThumb"),
1430 | min : 1,
1431 | max : nPages
1432 | });
1433 |
1434 | // page events
1435 | $pager._on('click','.gridNext', self.next, self);
1436 | $pager._on('click','.gridPrev', self.prev, self);
1437 | $pager._on('keyup','.currentPage input', self.pageEnter, self);
1438 | $pager.on('click','.goto', function(e) {
1439 | e.preventDefault();
1440 | var page = $(this).attr("href").substr(1);
1441 | self.goto.call(self,page);
1442 | });
1443 |
1444 | // handle search icon thing
1445 | $pager.on('blur','.search :input', function() {
1446 | if($(this).val()) $(this).parent().removeClass("icon");
1447 | })
1448 | $pager.on('focus','.search :input', function() {
1449 | $(this).parent().addClass("icon");
1450 | });
1451 | $pager.on('click','.search', function() {
1452 | $(this).find(":input").focus();
1453 | });
1454 | // search back to the db
1455 | $pager._on('keyup','.search :input',self.search, self);
1456 |
1457 | // live typing action is on grid not pager
1458 | $pager._on('keyup','.search :input',self.grid._filter,self.grid);
1459 |
1460 | } else {
1461 | // common var
1462 | $pager = $(this.el);
1463 |
1464 | // replace our templated HTML
1465 | $pager.html(pagerHtml);
1466 |
1467 | // check the search icon, if there is text we need it not be there
1468 | if(this.query) {
1469 | $pager.find(".search.icon").removeClass("icon");
1470 | }
1471 | }
1472 |
1473 | // start fresh
1474 | $pager.find(".gridPrev, .gridNext").removeClass("disabled");
1475 |
1476 | // if the previous page is gonna be 1, disabled the button
1477 | if(this.currentPage - 1 <= 0) {
1478 | $pager.find(".gridPrev").addClass("disabled");
1479 | }
1480 |
1481 | // if the previous page is gonna be 1, disabled the button
1482 | if(this.currentPage + 1 > nPages) {
1483 | $pager.find(".gridNext").addClass("disabled");
1484 | }
1485 |
1486 | // update slider
1487 | var self = this;
1488 | this.slider.update();
1489 |
1490 | // do we need the pager part of the pager
1491 | if(!grid.opts.editing) {
1492 | $grid.find(".gridSave").hide();
1493 | }
1494 |
1495 | // do we need the pager part of the pager
1496 | if(!grid.opts.showPager) {
1497 | $pager.find("div.pagination.left").hide();
1498 | }
1499 |
1500 | },
1501 |
1502 | search : function(e,el) {
1503 | if(e.keyCode == 13) {
1504 | // search grid
1505 | this.grid.load({
1506 | search : $(el).val(),
1507 | page : 1
1508 | });
1509 | // remove the glass
1510 | $(el).parent().removeClass("icon");
1511 | // false
1512 | return false;
1513 | }
1514 | },
1515 |
1516 | // hitting enter to go to a page
1517 | pageEnter : function(e, el) {
1518 | e.preventDefault();
1519 | if(e.keyCode === 13) {
1520 | this.goto( $(el).val() )
1521 | }
1522 | },
1523 |
1524 | // pager go next
1525 | next : function(e, el) {
1526 | // because we want to chain, we prevenDefault instead of return false;
1527 | e.preventDefault();
1528 | if(!$(el).hasClass("disabled")) {
1529 | // load the grid with one page forward
1530 | this.grid.load({ page : ++this.currentPage });
1531 | }
1532 | // chain
1533 | return this;
1534 | },
1535 |
1536 | // pager go prev
1537 | prev : function(e,el) {
1538 | // because we want to chain, we prevenDefault instead of return false;
1539 | e.preventDefault();
1540 | if(!$(el).hasClass("disabled")) {
1541 | // load the grid one page back
1542 | this.grid.load({ page : --this.currentPage });
1543 | }
1544 | // chain
1545 | return this;
1546 | },
1547 |
1548 | // pager go
1549 | goto : function(page) {
1550 | // don't allow a page to be higher than total pages
1551 | if(page > this.totalPages) page = this.totalPages;
1552 | // if page is <1 make it 1
1553 | if(page < 1) page = 1;
1554 | // do load
1555 | this.grid.load({ page : page });
1556 | // set the sider
1557 | this.slider.setVal(page);
1558 | // chain
1559 | return this;
1560 | }
1561 | });
1562 |
1563 | })(jQuery);
1564 |
1565 |
1566 | String.prototype.has = function(search) {
1567 | return (this.indexOf(search) !== -1);
1568 | }
1569 |
--------------------------------------------------------------------------------
/grid.php:
--------------------------------------------------------------------------------
1 | table = $table;
29 |
30 | // save
31 | if( isset($options['save']) && isset($_POST['save']) && $options['save'] == "true") {
32 | echo $this->save();
33 |
34 | // delete
35 | } else if( isset($options['delete']) && isset($_POST['delete']) && $options['delete'] == "true") {
36 | echo $this->delete();
37 |
38 | // delete
39 | } else if( isset($options['select']) && isset($_POST['select']) && $options['select'] == "true") {
40 | $this->select = true;
41 |
42 | // select boxes
43 | } else if(isset($options['select']) && isset($_POST['select'])) {
44 |
45 | $this->joins = array();
46 | $this->where = "";
47 | $this->fields = array();
48 |
49 | $call = $options['select'];
50 | if(is_array($call)) {
51 | call_user_func($call,$this);
52 | } else {
53 | $call($this);
54 | }
55 |
56 |
57 | // load
58 | } else {
59 |
60 | if(isset($options['where'])) $this->where = $options['where'];
61 | if(isset($options['fields'])) $this->fields = $options['fields'];
62 | if(isset($options['joins'])) $this->joins = $options['joins'];
63 | if(isset($options['groupBy'])) $this->groupBy = $options['groupBy'];
64 | if(isset($options['having'])) $this->having = $options['having'];
65 |
66 | $this->load()->render();
67 | }
68 |
69 | }
70 |
71 | function save() {
72 | $saveArray = $this->getSaveArray();
73 |
74 | // we need a primary key for editing
75 | $primaryKey = $this->getPrimaryKey();
76 |
77 | // die here if a primary is not found
78 | if(empty($primaryKey)) die("Primary Key for table {$this->table} Not set! For inline editing you must have a primary key on your table.");
79 |
80 | // go through each row and perform an update
81 | foreach($saveArray as $rowId=>$row) {
82 | $setArray = array();
83 | foreach($row as $key=>$value) {
84 | // don't update this row if you have security set
85 | // idea from youtube user jfuruskog
86 | if(!is_array($this->security) || in_array($key,$this->security)) {
87 | // dont save fields that weren't saveable. i.e. joined fields
88 | if(in_array($key,$_POST['saveable'])) {
89 | $key = mysql_real_escape_string($key);
90 | $value = mysql_real_escape_string($value);
91 | $setArray[] = "`$key`='$value'";
92 | }
93 | }
94 | }
95 |
96 | $sql = "UPDATE {$this->table} SET ".implode(",",$setArray)." WHERE `$primaryKey` = '$rowId'";
97 |
98 | $res = mysql_query($sql);
99 |
100 | // die with messages if fail
101 | $this->dieOnError($sql);
102 | }
103 | return (bool) $res;
104 | }
105 |
106 | // use this to write your own custom save function for the data
107 | function getSaveArray() {
108 | return $_POST['json'];
109 | }
110 |
111 | // adds a new row based on the editable fields
112 | function add() {
113 |
114 | // if didn't pass a set param, just add a new row
115 | if(empty($this->set)) {
116 | mysql_query("INSERT INTO {$this->table} VALUES ()");
117 |
118 | // if you passed a set param then use that in the insert
119 | } else {
120 | mysql_query("INSERT INTO {$this->table} SET {$this->set}");
121 | }
122 |
123 | // we return the primary key so that we can order by it in the jS
124 | echo $this->getPrimaryKey();
125 | }
126 |
127 | function delete() {
128 | $post = $this->_safeMysql();
129 | $primaryKey = $this->getPrimaryKey();
130 | return mysql_query("DELETE FROM {$this->table} WHERE `$primaryKey` = '$post[id]'");
131 | }
132 |
133 | function select($selects) {
134 | foreach($selects as $s) {
135 | echo function_exists($s);
136 | }
137 |
138 | }
139 |
140 | // will build an id, value array to be used to make a select box
141 | function makeSelect($value,$display) {
142 | // build sql if they are there
143 | $where = $this->where ? "WHERE {$this->where}":"";
144 | $order_by = $this->order_by ? "ORDER BY {$this->order_by}":"";
145 | $sort = $this->sort ? "{$this->sort}":"";
146 | $limit = $this->limit ? "LIMIT {$this->limit}":"";
147 | $table = $this->table;
148 |
149 | // bring all the joins togther if sent
150 | if(is_array($this->joins)) {
151 | $joins = implode(" ",$this->joins);
152 | } else {
153 | $joins = "";
154 | }
155 |
156 | // we only are selecting 2 columns, the one to use as the ID and the one for the display
157 | $colsArray = array($value,$display);
158 | $newColsArray = array();
159 | $usedCols = array();
160 |
161 | // loop through each complex field
162 | if($this->fields && is_array($this->fields)) {
163 | foreach($this->fields as $as=>$field) {
164 | // find which column this is to replace (replace in terms of the column for its complex counterpart)
165 | foreach($colsArray as $col) {
166 | // replace your alias with the complex field
167 | if($col == $as) {
168 | // field from OTHER table
169 | $newColsArray[] = "$field as `$as`";
170 | // mark as used
171 | $usedCols[] = $col;
172 | } else {
173 | // field from THIS table that aren't in the fields array
174 | if(!isset($this->fields[$col]) && !in_array($col,$usedCols)) {
175 | $newColsArray[] = "`$table`.`$col`";
176 | $usedCols[] = $col;
177 | }
178 | }
179 | }
180 | }
181 | } else {
182 | // add safe tics
183 | foreach($colsArray as $key=>$col) {
184 | $newColsArray[] = "`$table`.`$col`";
185 | }
186 | }
187 |
188 | // put it back
189 | $colsArray = $newColsArray;
190 |
191 | // get group and having
192 | $groupBy = $this->groupBy ? "GROUP BY ".$this->groupBy : "";
193 | $having = $this->having ? "HAVING ".$this->having : "";
194 |
195 | // bring it all together again
196 | $cols = implode(",",$colsArray);
197 |
198 | // setup the sql - bring it all together
199 | $sql = "
200 | SELECT $cols
201 | FROM `$table`
202 | $joins
203 | $where
204 | $groupBy
205 | $having
206 | $order_by $sort
207 | $limit
208 | ";
209 |
210 | // run sql, build id/value json
211 | $rows = $this->_queryMulti($sql);
212 | $this->dieOnError($sql);
213 |
214 | // setup rows to feed back to JS
215 | foreach($rows as $row) {
216 | $data[$row[$value]] = $row[$display];
217 | }
218 |
219 | // set our data so we can get it later
220 | $this->data = $data;
221 |
222 | return $data;
223 |
224 | }
225 |
226 | // loads data into the grid
227 | function load() {
228 | $post = $this->_safeMysql();
229 |
230 | // setup variables from properties
231 | $joins = $this->joins;
232 | $fields = $this->fields;
233 | $where = $this->where;
234 | $table = $this->table;
235 |
236 | // we need to break this up for use
237 | $colsArray = explode(",",$post['cols']);
238 |
239 | // get an array of saveable fields
240 | $saveable = $colsArray;
241 | // bug #1# @eric.tuvesson@gmail.com
242 | if(is_array($fields)) {
243 | foreach($fields as $field=>$detail) {
244 | foreach($saveable as $k=>$f) {
245 | if( $f == $field ) {
246 | unset($saveable[$k]);
247 | }
248 | }
249 | }
250 | }
251 |
252 |
253 | // were gonna use this one because this allows us to order by a column that we didnt' pass
254 | $order_by = isset($post['orderBy']) ? $post['orderBy'] : $colsArray[0];
255 |
256 | // save variables for easier use throughout the code
257 | $sort = isset($post['sort']) ? $post['sort'] : "asc";
258 | $nRowsShowing = isset($post['nRowsShowing']) ? $post['nRowsShowing'] : 10;
259 | $page = isset($post['page']) ? $post['page'] : 1;
260 |
261 |
262 | $startRow = ($page - 1) * $nRowsShowing;
263 |
264 | // bring all the joins togther if sent
265 | if((bool)$joins && is_array($joins)) {
266 | $joins = implode(" ",$joins);
267 | } else {
268 | $joins = "";
269 | }
270 |
271 | // if there are specific fields to add
272 | // replace the specefied alias with its complex field
273 | $colsArrayForWhere = array();
274 | $newColsArray = array();
275 | $usedCols = array();
276 |
277 | $groupFunctions = array(
278 | "AVG",
279 | "BIT_AND",
280 | "BIT_OR",
281 | "BIT_XOR",
282 | "COUNT",
283 | "GROUP_CONCAT",
284 | "ROUND",
285 | "MAX",
286 | "MIN",
287 | "STD",
288 | "STDDEV_POP",
289 | "STDDEV_SAMP",
290 | "STDDEV",
291 | "SUM",
292 | "VAR_POP",
293 | "VAR_SAMP",
294 | "VARIANCE"
295 | );
296 |
297 | if($fields && is_array($fields)) {
298 | foreach($fields as $as=>$field) {
299 | // find which column this is to replace
300 | foreach($colsArray as $col) {
301 | // replace your alias with the complex field
302 | if($col == $as && !in_array($col,$usedCols)) {
303 | // field from OTHER table
304 | $newColsArray[] = "$field as `$as`";
305 | // we can't search by group functions
306 | preg_match('/^\w+/i',$field,$needle);
307 | if(!in_array(strtoupper($needle[0]),$groupFunctions)) {
308 | $colsArrayForWhere[] = $field;
309 | }
310 | // mark as used
311 | $usedCols[] = $col;
312 | } else {
313 | // field from THIS non joined table that aren't in the fields array
314 | if(!isset($fields[$col]) && !in_array($col,$usedCols)) {
315 | $newColsArray[] = "`$table`.`$col`";
316 | $colsArrayForWhere[] = "`$table`.`$col`";
317 | $usedCols[] = $col;
318 |
319 | // add fields that aren't in the
but you want passed anyway
320 | } else if(!in_array($as,$usedCols)){
321 | // were just using field & as because you should have back ticked and chosen your table in your call
322 | $newColsArray[] = "$field as `$as`";
323 | // we can't search by group functions
324 | preg_match('/^\w+/i',$field,$needle);
325 | if(isset($needle[0])) {
326 | if(!in_array(strtoupper($needle[0]),$groupFunctions)) {
327 | $colsArrayForWhere[] = $field;
328 | }
329 | }
330 | $usedCols[] = $as;
331 | }
332 | }
333 | }
334 | }
335 | } else {
336 | // add safe tics
337 | foreach($colsArray as $key=>$col) {
338 | $newColsArray[] = "`$table`.`$col`";
339 | $colsArrayForWhere[] = "`$table`.`$col`";
340 | }
341 | }
342 |
343 | // put it back
344 | $colsArray = $newColsArray;
345 |
346 | // get primary key
347 | $primaryKey = $this->getPrimaryKey();
348 |
349 | // if primary key isn't in the list. add it.
350 | if($primaryKey && !in_array($primaryKey,$usedCols)) {
351 | $colsArray[] = $table.".".$primaryKey;
352 | }
353 |
354 | // with the cols array, if requested
355 | $colData = array();
356 | if(isset($post['maxLength']) && $post['maxLength'] == "true") {
357 | foreach($colsArray as $col) {
358 | // if there is no as (we can't determine length on aliased fields)
359 | if(stripos($col," as ") === false) {
360 | $col = str_replace("`","",$col);
361 | list($aTable,$field) = explode(".",$col);
362 | if(!$aTable) $aTable = $this->table;
363 | $colDataSql = mysql_query("SHOW columns FROM $aTable WHERE Field = '$field'");
364 | while($row = mysql_fetch_assoc($colDataSql)) {
365 | $type = $row['Type'];
366 | }
367 | preg_match('/\(([^\)]+)/',$type,$matches);
368 | $colData[$field] = array("maxLength"=>$matches[1]);
369 | }
370 | }
371 | }
372 |
373 | // shrink to comma list
374 | $post['cols'] = implode(",",$colsArray);
375 |
376 |
377 | // add dateRange to where
378 | if(!empty($post['dateRangeFrom']) || !empty($post['dateRangeTo'])) {
379 |
380 | // if one or the other is empty - use today otherwise parse into mysql date the date that was passed
381 | $dateFrom = empty($post['dateRangeFrom']) ? date('Y-m-d H:i:s') : date('Y-m-d H:i:s',strtotime($post['dateRangeFrom']));
382 | $dateTo = empty($post['dateRangeTo']) ? date('Y-m-d H:i:s') : date('Y-m-d H:i:s',strtotime($post['dateRangeTo']));
383 |
384 | // if they are = we want just this day (otherwise it would be blank)
385 | if($dateFrom == $dateTo) {
386 | $dateWhere = "DATE($table.$post[dateRange]) = DATE('$dateFrom')";
387 | // we actually want a range
388 | } else {
389 | $dateWhere = "`$table`.`$post[dateRange]` BETWEEN '$dateFrom' AND '$dateTo'";
390 | }
391 |
392 | // add this to the global where statement
393 | if(empty($where)) {
394 | $where = $dateWhere;
395 | } else {
396 | $where = "($dateWhere) && $where";
397 | }
398 |
399 | }
400 |
401 |
402 | // specific where setup for searching
403 | if(isset($post['search']) && $post['search']) {
404 | // if there is a search term, add the custom where first, then the search
405 | $where = !$where ? " WHERE " : " WHERE ($where) && ";
406 | // if you are searching, at a like to all the columns
407 | $where .= "(".implode(" LIKE '%$post[search]%' || ",$colsArrayForWhere) . " LIKE '%$post[search]%')";
408 | } else {
409 | // add the where keyword if there is no search term
410 | if($where) {
411 | $where = "WHERE $where";
412 | }
413 | }
414 |
415 | // get group and having
416 | $groupBy = $this->groupBy ? "GROUP BY ".$this->groupBy : "";
417 | $having = $this->having ? "HAVING ".$this->having : "";
418 |
419 | if($startRow < 0) $startRow = 1;
420 |
421 | // we need this seperate so we can not have a limit at all
422 | $limit = "LIMIT $startRow,$nRowsShowing";
423 |
424 | // if were searching, see if we want all results or not
425 | if(isset($_POST['pager']) && $_POST['pager'] == "false" || (!empty($_POST['search']) && isset($_POST['pageSearchResults']))) {
426 | $limit = "";
427 | }
428 |
429 |
430 |
431 | // setup the sql - bring it all together
432 | $order = strpos($order_by,".") === false ? "`$order_by`" : $order_by;
433 | $sql = "
434 | SELECT $post[cols]
435 | FROM `$table`
436 | $joins
437 | $where
438 | $groupBy
439 | $having
440 | ORDER BY $order $sort
441 | $limit
442 | ";
443 |
444 | $this->sql = $sql;
445 |
446 | // execute the sql, get back a multi dimensial array
447 | $rows = $this->_queryMulti($sql);
448 |
449 | // die with messages if fail
450 | $this->dieOnError($sql);
451 |
452 |
453 | // form an array of the data to send back
454 | $data = array();
455 | $data['rows'] = array();
456 | foreach($rows as $i=>$row) {
457 | foreach($row as $col=>$cell) {
458 | // use primary key if possible, other wise use index
459 | $key = $primaryKey ? $row[$primaryKey] : $i;
460 |
461 | // primary key has an _ infront becuase of google chrome re ordering JSON objects
462 | //http://code.google.com/p/v8/issues/detail?id=164
463 | $data['rows']["_".$key][$col] = utf8_encode($cell);
464 | }
465 | }
466 |
467 | // if were searching and we dont want all the results - dont run a 2nd query
468 | if(isset($_POST['pager']) && $_POST['pager'] == "false" || (!empty($_POST['search']) && isset($_POST['pageSearchResults']))) {
469 | $data['nRows'] = count($rows);
470 | $startRow = 0;
471 | $nRowsShowing = $data['nRows'];
472 | } else {
473 | if(!$this->limit) {
474 | // use the same query for counting less the limit
475 | $sql2 = preg_replace('/LIMIT[\s\d,]+$/','',$sql);
476 |
477 | // find the total results to send back
478 | $res = mysql_query($sql2);
479 | $data['nRows'] = mysql_num_rows($res);
480 | } else {
481 | $data['nRows'] = $this->limit;
482 | }
483 | }
484 |
485 | $data['order_by'] = $order_by;
486 | $data['sort'] = $sort;
487 | $data['page'] = $page;
488 | $data['start'] = $startRow + 1;
489 | $data['end'] = $startRow + $nRowsShowing;
490 | $data['colData'] = $colData;
491 | $data['saveable'] = $saveable;
492 | $this->data = $data;
493 |
494 | return $this;
495 | }
496 |
497 | // renders the json data out
498 | function render($data=NULL) {
499 | header('Cache-Control: no-cache, must-revalidate');
500 | header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
501 | header('Content-type: application/json');
502 | if($data) $this->data = $data;
503 | echo json_encode($this->data);
504 | }
505 |
506 | // incomplete
507 | // will allow this whole thing to run off an entire custom query
508 | // as in not just setting props
509 | function loadWithSql($sql) {
510 | $sql = preg_replace('/[ORDER BY|order by]+[\s]+[^\n\r]+/','',$sql);
511 | $sql = preg_replace('/[LIMIT|limit]+[\s\d,]+$/','',$sql);
512 | echo $sql;
513 |
514 | }
515 |
516 | // using the current table will get the primary key column name
517 | // does not work for combined primary keys
518 | function getPrimaryKey($table=NULL) {
519 | if(!$table) $table = $this->table;
520 | $primaryKey = mysql_query("SHOW KEYS FROM `$table` WHERE Key_name = 'PRIMARY'");
521 | $primaryKey = mysql_fetch_assoc($primaryKey);
522 | return $primaryKey['Column_name'];
523 | }
524 |
525 | // if there is a mysql error it will die with that error
526 | function dieOnError($sql) {
527 | if($e=mysql_error()) {
528 | //var_dump($sql);
529 | die($e);
530 | }
531 | }
532 |
533 | // runs a query, always returns a multi dimensional array of results
534 | function _queryMulti($sql) {
535 | $array = array();
536 | $res = mysql_query($sql);
537 | if((bool)$res) {
538 | // if there is only 1 field, just return and array with that field as each value
539 | if(mysql_num_fields($res) > 1) {
540 | while($row = mysql_fetch_assoc($res)) $array[] = $row;
541 | } else if(mysql_num_fields($res) == 1) {
542 | while($row = mysql_fetch_assoc($res)) {
543 | foreach($row as $item) $array[] = $item;
544 | }
545 | }
546 | $error = mysql_error();
547 | if($error) echo $error;
548 | }
549 | return $array;
550 | }
551 |
552 | // safeify post
553 | function _safeMysql($post=NULL) {
554 | if(!isset($post)) $post = $_POST;
555 | $postReturn = array();
556 | foreach($post as $key=>$value) {
557 | if(!is_array($value)) {
558 | $postReturn[$key] = mysql_real_escape_string(urldecode($value));
559 | } else if(is_array($value)) {
560 | $postReturn[$key] = $value;
561 | }
562 | }
563 | return $postReturn;
564 | }
565 | }
566 |
567 |
568 | ?>
--------------------------------------------------------------------------------
/grid.styl:
--------------------------------------------------------------------------------
1 | @import 'lib/nib'
2 |
3 | body
4 | padding:80px
5 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif
6 | font-size: 13px
7 | line-height: 18px
8 | color: #333
9 |
10 | .left {float:left}
11 | .right {float:right}
12 | .fc {overflow:hidden}
13 |
14 | div.gridWrapper
15 | width:800px
16 | min-width:515px
17 | border: 1px solid #DDD
18 | border-radius: 4px
19 | border-radius: 4px
20 | height:280px
21 | position:relative
22 | background:white;
23 | margin-bottom:70px;
24 |
25 | &.resizing, &.resizing input
26 | user-select: none
27 | //overflow:hidden;
28 | &.moving
29 | user-select: none
30 |
31 | .dialog
32 | position absolute
33 | top: 50px
34 | left: 50%
35 | padding:20px
36 | border-radius: 7px
37 | color:#333
38 | font-size:11px
39 | width:200px
40 | height:80px
41 | margin-left:-100px
42 | background:white
43 | //border: 1px solid rgba(0,0,0,.2)
44 | box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.4);
45 | opacity 0
46 | transition opacity 200ms ease-out
47 |
48 | &.show
49 | opacity 1
50 |
51 | span.body
52 | display:block
53 | margin-bottom:5px
54 |
55 | span.label
56 | top: -10px;
57 | position: relative;
58 | padding-top: 2px;
59 |
60 | .buttons
61 | bottom: 12px;
62 | position: absolute;
63 | right: 15px;
64 |
65 | button
66 | outline:none
67 |
68 | &.gridNotify
69 | width: 100px;
70 | height: 30px;
71 | text-align: center;
72 | margin-left: -50px;
73 |
74 | .gridLoading
75 | text-align:center
76 | display:block
77 | width:100%
78 | font-size:11px
79 | height:100%
80 |
81 | .gridResizer
82 | position:absolute
83 | right:0px
84 | top:0px
85 | bottom:0px
86 | width:10px
87 | cursor:e-resize
88 |
89 | .columns
90 | height:inherit
91 | overflow-x:hidden
92 | -webkit-overflow-scrolling: touch
93 | opacity 1
94 |
95 | &.blur
96 | //-webkit-filter: blur(20px);
97 | opacity .2
98 |
99 |
100 | .cell
101 | padding:5px
102 | border-left:1px solid #DDD
103 | white-space:nowrap
104 | height:24px
105 | position:relative;
106 | overflow:hidden;
107 | //background-color #e5e5e5
108 |
109 | &:nth-child(2), &.topMargin
110 | margin-top:40px
111 |
112 | &:nth-child(2n+1)
113 | //background-color #eee
114 |
115 | &.level2
116 | height:160px;
117 | overflow:visible;
118 |
119 | .level2Grid
120 | background: blue;
121 | height: 147px;
122 | width: 793px;
123 | z-index: 5;
124 | position: absolute;
125 |
126 | img
127 | max-width: 100%;
128 | max-height: 100%;
129 |
130 | &.row-hover
131 | background:rgba(0,0,0,.05)
132 |
133 | &.input
134 | padding-right:15px
135 | &.select
136 | padding-right 5px
137 |
138 | &.center
139 | text-align:center
140 |
141 | &.editable input[type="text"]
142 | width: 100%
143 | box-shadow: inset 0px 1px 3px #ccc
144 | border:0px
145 | margin:0px
146 |
147 | &.blankCell
148 | zoom:1;
149 | //padding-bottom: 15px
150 |
151 | &.headerCell
152 | position:absolute
153 | width:inherit
154 | height:inherit
155 | overflow:hidden
156 | border-bottom:1px solid #DDD
157 | padding:0px
158 | cursor:pointer
159 | user-select : none
160 | background: linear-gradient(top, #ffffff 0%,rgb(228,228,228) 100%)
161 | z-index 2
162 | min-width 50px
163 |
164 | span
165 | font-weight:bold
166 | padding:9px
167 | display:block
168 |
169 | .sortbar
170 | position:absolute
171 | height:10px
172 | width:100%
173 | bottom:0px
174 | left:0px
175 | font-size:13px
176 | line-height: 9px
177 | text-align: center
178 | display:none
179 | transform: rotate(0deg)
180 |
181 | &.desc
182 | transform: rotate(180deg)
183 |
184 | .resizer
185 | position:absolute
186 | top:0px
187 | right:0px
188 | height:100%
189 | width:10px
190 | background:transparent
191 | cursor:move
192 |
193 | .col
194 | float:left
195 | min-width 50px
196 | //max-width 150px
197 | //overflow:hidden
198 | //display inline-block
199 |
200 | &.dynamic
201 | min-width 20px
202 | .headerCell
203 | min-width 20px
204 |
205 | &._checkbox .headerCell
206 | text-align:center
207 |
208 | &:first-child .cell
209 | border-left:0px
210 |
211 |
212 | &.small
213 | .cell.headerCell
214 | text-align:center
215 |
216 | span
217 | padding 4.5px // half of the original span padding
218 | .gridPager
219 | .slider span
220 | width:70px;
221 | .search input:focus
222 | width:120px;
223 |
224 | &.small, &.small input
225 | font-size:10px
226 |
227 | .gridPager
228 | margin-top:15px
229 | user-select: none
230 | .slider
231 |
232 | span
233 | border: 1px solid #DDD;
234 | float:left;
235 | height:34px;
236 | border-left:none;
237 | width:150px;
238 | .slider
239 | width:88%
240 | padding-left:6%;
241 | padding-right:6%;
242 |
243 | .sliderTrack
244 | width: auto
245 | height: 4px
246 | box-shadow: inset 0px 2px 5px #333
247 | margin-top: 15px
248 | background: #CCC
249 | border-radius: 5px
250 | .sliderThumb
251 | width:13px
252 | height:13px
253 | margin-top: -7px;
254 |
255 | border-radius:6px
256 | box-shadow: 0px 0px 4px #333
257 | background: linear-gradient(top, #ffffff 0%,#f1f1f1 50%,#e1e1e1 51%,#f6f6f6 100%)
258 | cursor:pointer
259 |
260 | .currentPage, .search
261 | input
262 | float: left;
263 | height: 26px;
264 | width: 30px;
265 | margin-left: 2px;
266 | text-align: center;
267 | .pagination
268 | margin:0
269 | li a, li span
270 | background:white;
271 | &.left li
272 | white-space:no-wrap
273 | ul,ol
274 | margin:0
275 | .btn
276 | padding: 8px 10px;
277 | margin-left:5px;
278 | .pager_showing
279 | min-width 108px
280 | text-align center
281 | .search
282 | position:relative;
283 | input
284 | transition:width, 200ms ease-out
285 | text-align:left;
286 |
287 | &:focus
288 | width:150px
289 | .search.icon:after, .search input:focus:after
290 | content : ''
291 | width: 14px;
292 | height:14px;
293 | display:block;
294 | position:absolute;
295 | top 10px;
296 | left -27px;
297 | background-image: url('http://twitter.github.com/bootstrap/assets/img/glyphicons-halflings.png');
298 | background-position: -48px 0px;
299 |
300 | select
301 | width 100%
302 |
303 | &.touch
304 | .gridPager .slider .sliderThumb
305 | width:23px
306 | height:23px
307 | margin-top: -13px;
308 |
309 | .ui-datepicker
310 | z-index 2
311 |
312 | // bootstrap button fix
313 | .btn:hover
314 | transition: none
315 |
316 | @-moz-document url-prefix() {
317 | div.gridWrapper .gridPager .search.icon:after, .search input:focus:after {
318 | top:25px
319 | }
320 | }
321 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
48 |
49 |
50 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Open JS Grid
2 |
3 | 
4 |
5 | ## Overview
6 |
7 | **Open JS Grid**, The easiest data table / grid available for js and php developers. I used all of my knowledge of OOP Javascript, jQuery, mySQL and PHP to do all the work for you, as well as comment the hell out of everything.
8 |
9 | **For all the documentation and examples, see **
10 |
11 |
12 | ##### Current Version 2.1
13 | - Column resizing is fixed
14 | - touch enabled partially working
15 | - slider is bigger when using touch
16 | - class touch is givent to the grid when its touch enabled
17 | - the 'dynamic' property is given to columns that are dynamically created
18 | - new minWidthForDynamicCols added for dynamic columns
19 | - minWidth is now utilized in css
20 |
21 | ### Getting Started
22 |
23 | #### HTML Setup
24 |
25 |
26 |
27 |
Username
28 |
First Name
29 |
Last Name
30 |
Email
31 |
32 |
33 |
34 | #### JS Setup
35 |
36 |
41 |
42 |
43 | #### PHP Setup
44 |
45 | true,
49 | "delete"=>true
50 | ));
51 | ?>
52 |
53 | #### The philosophy
54 |
55 | Basically, the whole idea here is that in backend coding, you have to make a ton of table displays for customers or clients. OpenJS Grid provides a super easy / powerful way to give customers access to data. I do all the database work for you, so you don't have to figure out searching and sorting, as well as give you cool stuff like events and cell types so you can customize to fit your need. And now with Stylus and Bootstrap, you can easily restyle the grid to your liking.
56 |
57 | Once more, I believe data grids should still be written as HTML tables, with very little javascript config. So that's why the setup is an HTML table. You can also specificy column options as attributes on the `
` elements. That's so you can have dynamic grids, and less javascript config.
58 |
59 | #### The Features 2.1
60 |
61 | - Sorting
62 | - Live Filtering
63 | - Searching (database)
64 | - Paging
65 | - Editing
66 | - textbox, dropdown, checkbox
67 | - Deleting
68 | - Row numbering
69 | - Row checkboxes
70 | - Hyperlinking
71 | - use data from that cell, or any cell, even if its not on the grid
72 | - Events
73 | - loadComplete, cellClick, rowClick, rowCheck
74 | - CSS written in Stylus
75 | - Completely OOP
76 | - Grid, Pager, Slider, Dialog
77 | - Custom Dialogs for each grid
78 | - notify, alert, error, confirm
79 | - Custom cell types
80 | - input, money, drop down, checkbox, image, even your own
81 | - Dynamically add your own columns
82 | - Column resizing and full grid resizing
83 | - Allow for complex mysql joins, having, groups and limits
84 | - I think that's all?
85 |
86 | #### The Future 3.0?
87 |
88 | - Per row editing
89 | - textarea type
90 | - multi level grids
91 | - millions of rows support
92 | - row adding
93 | - row highlights (its too slow right now with it)
94 |
95 | #### Who am I?
96 |
97 | I'm Sean Clark. I run a training [youtube](http://youtube.com/optikalefxx) channel for advanced coding. That coincides with [Square Bracket](http://square-bracket.com) which is where I make demos and other training videos.
98 |
99 | I have a [blog](http://sean-optikalefx.blogspot.com/>) about developer things.
--------------------------------------------------------------------------------
/root.js:
--------------------------------------------------------------------------------
1 | // Root Version SC.05.27.12
2 | ///////////////////////////////////////////////////////////////////////////////
3 | // RootJS is basically a way to write much easier OOP code
4 | // Root.DOM is a helper object to inherit from, so you get a bunch of really cool
5 | // dom stuff, like events, objects tied to elements, and a jQuery plugin arch
6 | ///////////////////////////////////////////////////////////////////////////////
7 | (function($, window, document, undefined) {
8 | "use strict";
9 |
10 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////
11 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////
12 | ////// Root
13 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////
14 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////
15 |
16 | window.Root = {
17 | _construct : function() {}, // construct method
18 | _init : function() {}, // method called on every instantiation
19 | proto : function () { // shortcut to parent object
20 | return Object.getPrototypeOf(this);
21 | },
22 |
23 | // borrowed and modifed from jQuery source. They do it the best.
24 | // we use our own check methods though
25 | extend : function() {
26 | var options, name, src, copy, clone,
27 | target = arguments[0] || {},
28 | i = 1,
29 | length = arguments.length,
30 | deep = true;
31 |
32 | // Handle case when target is a string or something (possible in deep copy)
33 | if ( typeof target !== "object" && typeof target != "function" ) {
34 | target = {};
35 | }
36 |
37 | for ( ; i < length; i++ ) {
38 | // Only deal with non-null/undefined values
39 | if ( (options = arguments[ i ]) != null ) {
40 | // Extend the base object
41 | for ( name in options ) {
42 | src = target[ name ];
43 | copy = options[ name ];
44 |
45 | // Prevent never-ending loop
46 | if ( target === copy ) {
47 | continue;
48 | }
49 |
50 | // Recurse if we're merging plain objects or arrays
51 | if ( deep && copy && ( this.isPlainObject(copy) )) {
52 | if(src && Array.isArray(src)) {
53 | clone = src && Array.isArray(src) ? src : [];
54 | } else {
55 | clone = src && this.isPlainObject(src) ? src : {};
56 | }
57 |
58 | // Never move original objects, clone them
59 | target[ name ] = this.extend( deep, clone, copy );
60 |
61 | // Don't bring in undefined values
62 | } else if ( copy !== undefined ) {
63 | target[ name ] = copy;
64 | }
65 | }
66 | }
67 | }
68 |
69 | // Return the modified object
70 | return target;
71 | },
72 | // tests if object is a plain object
73 | isPlainObject : function(obj) {
74 | // typeof null == 'object' haha
75 | if(obj !== null && typeof obj === "object") {
76 | // yea!
77 | return this.proto.call(obj).hasOwnProperty("hasOwnProperty");
78 | } else {
79 | return false;
80 | }
81 | },
82 | // wrapper for set and get
83 | // takes the property you wanna set, and calls the method on 'this'
84 | // optional placeholder to init with
85 | setter : function(prop,method,placeHolder) {
86 | var placeHolder = placeHolder;
87 | Object.defineProperty(this,prop,{
88 | get : function() {return placeHolder},
89 | set : function(val) {
90 | placeHolder = val;
91 | this[method](val);
92 | }
93 | });
94 | },
95 | // convience method
96 | define : function() {
97 | return this.inherit.apply(this,arguments);
98 | },
99 | // object inheritance method
100 | // takes an object to extend the class with, &| a dom element to use
101 | inherit: function(values, el) {
102 |
103 | // create a copy of the parent and copy over new values
104 | // normally Object.create() takes a 2nd param to extend properties. But when you use that,
105 | // you can't use use an easy JSON structure, you have to define enumerable, writable, and configurable and value
106 | // FOR EVERY PROPERTY. So.. we just do it ourselves with this.extend
107 | var parent = this, instance;
108 |
109 | if(typeof el != "undefined") values.el = el;
110 |
111 | // handle arrays
112 | if(Array.isArray(values)) {
113 | instance = values;
114 | this.extend(instance, parent);
115 | } else {
116 | instance = Object.create(parent);
117 | // now do a deep copy
118 | this.extend(instance, values);
119 | }
120 |
121 | // if the parent element has a constructor, call it on the instances
122 | if(parent.hasOwnProperty("_construct")) { parent._construct.apply(instance); }
123 |
124 | // if i have an _init function, call me
125 | if(instance.hasOwnProperty("_init")) { instance._init(); }
126 |
127 | // return the new instance
128 | return instance;
129 |
130 | },
131 | jQueryPlugin : function(name) {
132 |
133 | // pull the name out of the first argument
134 | var args = Array.prototype.slice.call(arguments);
135 | args.splice(0,1);
136 |
137 | // this does our Grid = Root.inherit(stuff)
138 | // so this is just like the main definition of the main object
139 | var Obj = this.inherit.apply(this,args);
140 |
141 | // create the jQuery Plugin
142 | $.fn[name] = function(user_opts) {
143 |
144 | var args = Array.prototype.slice.call(arguments),
145 | $ret = $(),
146 | el;
147 |
148 | for(var i=0;i "City.CountryCode = '$countryCode'",
22 | "joins" => array(
23 | "LEFT JOIN Country ON (Country.Code = City.CountryCode)"
24 | ),
25 | "fields" => array(
26 | "CountryName" => "Country.Name"
27 | )
28 |
29 | ));
30 |
31 |
32 | }
33 |
34 | ?>
--------------------------------------------------------------------------------