├── .gitmodules ├── DBTableEditor.class.php ├── LICENSE ├── README.md ├── README.txt ├── ReleaseNotes.txt ├── SplClassLoader.php ├── assets ├── db-table-editor.css ├── db-table-editor.js ├── dbte-date-editor.js ├── images │ ├── accept.png │ ├── add.png │ ├── arrow_right.png │ ├── arrow_undo.png │ ├── calendar.png │ ├── clear.png │ ├── database_edit.png │ ├── delete.png │ ├── download.png │ ├── find.png │ └── loading.gif ├── jquery.event.drag.js ├── jquery.event.drag.live.js ├── jquery.event.drop.js ├── jquery.event.drop.live.js ├── moment.js └── sprintf.js ├── db-table-editor.php ├── examples ├── cf7dbsubmit_integration.php ├── custom-buttons-and-js.php ├── custom-buttons.js ├── paging.php └── payment-paging.js ├── languages ├── wp-db-table-editor-en_US.mo ├── wp-db-table-editor-en_US.po ├── wp-db-table-editor-fr_FR.mo ├── wp-db-table-editor-fr_FR.po └── wp-db-table-editor.pot └── screenshot-1.png /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "assets/SlickGrid"] 2 | path = assets/SlickGrid 3 | url = git@github.com:AccelerationNet/SlickGrid.git 4 | [submodule "php-sql-parser"] 5 | path = php-sql-parser 6 | url = git@github.com:greenlion/PHP-SQL-Parser.git 7 | -------------------------------------------------------------------------------- /DBTableEditor.class.php: -------------------------------------------------------------------------------- 1 | parse($sql); 35 | 36 | // a bit of a hack because we are abusing the constant node, but whatever 37 | $whereExp = Array("expr_type"=>"const", "base_expr"=>$where, "sub_tree"=>null); 38 | if(!@$parsed['WHERE']) $parsed['WHERE'] = Array(); 39 | else $whereExp['base_expr']=' AND ('.$whereExp['base_expr'].')'; 40 | $parsed['WHERE'][]=$whereExp; 41 | $sql = $sqlcreator->create($parsed); 42 | return $sql; 43 | } 44 | 45 | class DBTE_DataTable { 46 | var $rows,$columns, $columnNames; 47 | function __construct($args=null){ 48 | global $wpdb; 49 | $args = wp_parse_args($args); 50 | $sql = @$args['sql']; 51 | $where = @$args['where']; 52 | if($sql){ 53 | if($where){ 54 | // this has sometimes gone wrong 55 | $sql = insert_where ($sql, $where); 56 | } 57 | $haslimit = preg_match('/limit\s+\d+/i', $sql); 58 | if(!$haslimit){ 59 | $sql .= ' LIMIT '.$limit.' OFFSET '.$offset; 60 | } 61 | // error_log("Got sql:\n".$sql); 62 | $this->rows = $wpdb->get_results($sql, ARRAY_N); 63 | if($wpdb->last_error){ 64 | error_log("Failed to execute query:\n$sql\nERR:".$wpdb->last_error."\n"); 65 | return null; 66 | } 67 | $this->offset = $offset; 68 | if(!@$args['columns']){ 69 | $this->columnNames = $cnames = $wpdb->get_col_info('name'); 70 | $ctypes = $wpdb->get_col_info('type'); 71 | $this->columns = Array(); 72 | for($i=0; $i < count($cnames) ; $i++){ 73 | $this->columns[]=Array('name'=>$cnames[$i], 'type'=>$ctypes[$i]); 74 | } 75 | } 76 | }else if(@$args['rows']){ 77 | $this->rows = $args['rows']; 78 | } 79 | if(@$args['columns']){ 80 | $this->columns = $args['columns']; 81 | } 82 | else{ // handle building columns from wpdb 83 | $this->columnNames = $cnames = $wpdb->get_col_info('name'); 84 | $ctypes = $wpdb->get_col_info('type'); 85 | $this->columns = Array(); 86 | for($i=0; $i < count($cnames) ; $i++){ 87 | $this->columns[]=Array('name'=>$cnames[$i], 'type'=>$ctypes[$i]); 88 | } 89 | } 90 | } 91 | } 92 | 93 | /** 94 | * A class to contain the configuration state for each DBTableEditor 95 | * that is available 96 | * @access public 97 | */ 98 | class DBTableEditor { 99 | var $table, $title, $sql, $dataFn, $id, $data, $cap, $jsFile, 100 | $noedit, $editcap, $noedit_columns, $hide_columns, $default_values, 101 | $columnFilters, $columnNameMap, $save_cb, $insert_cb, $update_cb, $delete_cb, 102 | $export_id_field, 103 | $id_column, $auto_date, $async_data; 104 | function __construct($args=null){ 105 | $args = wp_parse_args($args, array('cap'=>'edit_others_posts')); 106 | foreach($args as $k => $v) $this->{$k} = $v; 107 | if(!$this->id) $this->id = $this->table; 108 | if(!$this->title) $this->title = $this->table; 109 | if(!$this->id_column) $this->id_column = 'id'; 110 | if(!isset($args['auto_date'])) $this->auto_date=true; 111 | } 112 | /* 113 | * Gets data from the data source (either sql, or dataFn (prefering sql) 114 | * default is to SELECT * FROM {table} 115 | */ 116 | function getData($args=null){ 117 | if(!$args)$args=Array(); 118 | $fn = $this->dataFn; 119 | $sql = $this->sql; 120 | 121 | if($sql) $args['sql'] = $sql; 122 | else if($fn) $args['rows'] = $fn($args); 123 | else $args["sql"] ="SELECT * FROM $this->table"; 124 | $this->data = new DBTE_DataTable($args); 125 | return $this->data; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Russ Tyndall, Acceleration.net 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### WP-DB-Table-Editor ### 2 | 3 | Contributers: bobbysmith007 4 | Donate link: https://www.acceleration.net/programming/donate-to-acceleration-net/ 5 | Tags: admin screens, database, editor 6 | Requires at least: 3.0.0 7 | Tested Up To: 4.2.2 8 | Stable tag: trunk 9 | License: BSD 10 | URL: https://github.com/AccelerationNet/wp-db-table-editor/ 11 | 12 | ## Description ## 13 | 14 | This is a Wordpress plugin that allows direct excel-like editing of 15 | tables in your Wordpress database. It's goals are to provide useful, 16 | simple, flexible database table admin screens. 17 | 18 | It supports: 19 | 20 | * one table per admin screen, as many admin screens as desired 21 | * These are organized under a new "DB Table Editor" menu item 22 | * excel spreadsheet like interface using SlickGrid 23 | * Filter and Sort results 24 | * Add, Edit & Delete records 25 | * Custom buttons extensibility 26 | * Custom permissions per interface for viewing and editing 27 | (defaults to: edit_others_posts) 28 | * editing defaults to the same permission as viewing if not specified 29 | * CSV exports of filtered grid 30 | * Custom primary key names (but must be a single value / column) 31 | 32 | # Reasons and Expectations # 33 | 34 | Previously my company had been using DB-toolkit to provide minimal 35 | database interfaces for custom tables through the Wordpress admin. 36 | While the configuration was cumbersome for what we were doing, it did 37 | work and was easier than writing anything. However, when DB-Toolkit 38 | stopped being maintained and I couldn't find a simple, but suitable 39 | replacement, I decided to tackle my goals more head on 40 | 41 | Use of this plugin requires a basic knowledge of PHP, and SQL. It was 42 | written by a programmer to help accomplish his work and does not 43 | currently provide admin configuration screens (instead simple function 44 | calls in your theme's functions file are used to configure the 45 | screens). This was preferable to me, because my configuration is 46 | safely in source control (a problem I had when DB-toolkit would 47 | upgrade and lose all configuration). 48 | 49 | ## Screenshots ## 50 | 51 | 1. Here's a screenshot of it in action 52 | 53 | ## Installation ## 54 | 55 | This is installed the same way all wordpress plugins: 56 | 57 | * Drop the unzipped plugin directory into your wordpress install at 58 | `wp-content/plugins/wp-db-table-editor` 59 | 60 | * Activate the plugin via the Wordpress Admin > "Plugins" menu 61 | 62 | 63 | 64 | # Adding an interface # 65 | 66 | DB-Table Editor Interfaces are added by calling the 67 | add_db_table_editor function in your theme's `functions.php` file. 68 | This supports `wp_parse_args` style arguments. 69 | 70 | * `title`: what shows up in the H1 on the screen and in menues 71 | ** ex: `title=>"My Product Reports Page"` 72 | * `table`: the table we wish to display / edit 73 | ** ex: `table=>"wp_my_custom_table"` 74 | * `id`: the admin interface id (defaults to table) 75 | ** ex: `id=>"custom_table_interface_1"` 76 | * `id_column`: the column in each row that names the id for the row 77 | ** ex: `id_column=>"id"` 78 | * `dataFn`: a function that returns the data to be displayed / 79 | edited, defaults to `select * from {table}`. This should return ARRAY_N 80 | through wpdb->get_results. Alternatively it may return a DBTE_DataTable. 81 | `dataFn` is called with the arguemnts array to add_db_table_editor; 82 | ** ex: `dataFn=>"myCustomInterfaceFunction"` 83 | * `jsFile`: the name of a registered script that will be enqueued for 84 | this interface 85 | ** ex: `jsFile=>"my-custom-interface-js"` 86 | * `cap`: the capability a user needs to view/edit this interface, 87 | defaults to edit_others_posts 88 | ** ex: `cap=>"edit_others_posts"` 89 | * `editcap`: the capability required to edit the grid, if not set 90 | all viewers are assumed to be editors 91 | ** ex: `editcap=>"edit_others_posts"` 92 | * `noedit`: turns off the editing abilities (same as editcap=nosuchcapability) 93 | ** ex: `noedit=>true` 94 | * `columnFilters`: Default column filters, this is an array of column->val 95 | to be applied as default column fitlers when the page is loaded 96 | ** ex: `columnFilters=>Array("Year"=>"2017")` 97 | * `columnNameMap`: A map of actual column names to displayed label 98 | ** Ex: `columnNameMap=>Array('column_name'=>'Column Alias')` 99 | * `noedit_columns`, `hide_columns`: You may wish to hide some columns 100 | or prevent edit. You may do so by setting these fields to the name 101 | of columns you wish hidden or uneditable (eg: the id) 102 | ** Ex:`noedit_columns=>"data,id"` or `noedit_columns=>Array('data', 'id')` 103 | * `save_cb`, `delete_cb`: function names to be called with an array of data: 104 | the dbte, update array, column array and modified indexes array 105 | `call_user_func($cur->save_cb,Array('table'=>$cur, 'update'=>$up, 106 | 'columns'=>$cols, 'indexes'=>$idxs, 'id'=>$id));` 107 | `call_user_func($cur->delete_cb,$cur,$id);` 108 | If your call back inserts data it should fill in $data['id'] and accept data 109 | by reference 110 | * `auto_date`: should columns that appear to be datetimes, be treated as such 111 | This is based on the columns data type 112 | ** Sort of buggy but allows some different date formats than iso8601 113 | ** Ex:`auto_date=>true` 114 | * `autoHeight`: passes the autoHeight option to slickgrid (makes 115 | there not be a vertical scrollbar on the grid and instead in the 116 | window) 117 | ** Ex:`auto_height=>true` 118 | * `async_data`: request data asyncronously instead of inlining 119 | it. Makes slow queries "seem" faster. 120 | ** Ex:`async_data=>true` 121 | * `default_values`: an a_array of default values that new rows should have 122 | ** Ex:`default_values=>Array("name"=>"First M Last")` 123 | * `export_id_field`: the field to use when limiting the export results 124 | ** some sql needs a specific field - defaults to `table`.`id_col` 125 | ** Ex:`"export_id_field"=>"mytbl.fooid"` 126 | 127 | Example: 128 | 129 | ``` 130 | if(function_exists('add_db_table_editor')){ 131 | add_db_table_editor('title=Employees&table=employees'); 132 | 133 | add_db_table_editor(array( 134 | 'title'=>'Event Registrations', 135 | 'table'=>'event_registrations', 136 | 'sql'=>'SELECT * FROM event_registrations ORDER BY date_entered DESC' 137 | )); 138 | 139 | } 140 | ``` 141 | 142 | ## Adding an Interface on the fly ## 143 | 144 | If we go to look up a database table editor and we dont find it, but 145 | there is a function named dbte_create_$tbl that matches, we will call 146 | that function expecting it to return a dbte instance. This is useful 147 | in situations where we may not have the data for a table editor in all 148 | circumstances (EG: not every page has a member id, so only do it on 149 | that particular page). 150 | 151 | ## Adding an Interface from a plugin ## 152 | 153 | If you need to add an interface from a plugin, you should use the 154 | `db_table_editor_init` action. 155 | 156 | eg: `add_action( 'db_table_editor_init', 'my_load_tables' );` 157 | 158 | Inside of the `my_load_tables` function you would include all the 159 | calls to add_db_table_editor 160 | 161 | 162 | ## Custom Buttons 163 | 164 | Buttons can be created by pushing functions into 165 | `DBTableEditor.extraButtons`. Each of these is a slick grid 166 | rowButtonFormatter and should return a string of html. 167 | 168 | eg: 169 | out += fn(row, cell, value, columnDef, dataContext); 170 | 171 | The button column width can be set by setting: 172 | DBTableEditor.buttonColumnWidth before the ready function is called 173 | 174 | 175 | # Hooks / Actions # 176 | 177 | * `db_table_editor_init` is called during init so that other plugins 178 | can rely on this 179 | 180 | * `db-table-editor_enqueue_scripts` is an action that will be called 181 | after enqueueing all plugin scripts and before enqueueing `jsFile` 182 | (if it exists) 183 | 184 | ``` 185 | function dbTableEditorScripts(){ 186 | wp_register_script('employee-table-extensions-js', 187 | get_stylesheet_directory_uri().'/employee-table.js', 188 | array('db-table-editor-js')); 189 | } 190 | add_action('db-table-editor_enqueue_scripts', 'dbTableEditorScripts'); 191 | ``` 192 | 193 | ## dbte_row_saved, dbte_row_deleted 194 | 195 | Called after a row is deleted, updated, or inserted passes 196 | 197 | ``` 198 | add_action('dbte_row_deleted', 'my_dbte_row_deleted', 10, 2); 199 | 200 | function my_dbte_row_deleted($currentTable, $idRemoved){ 201 | // do things 202 | } 203 | 204 | add_action('dbte_row_saved', 'my_dbte_row_saved', 10); 205 | 206 | $args = Array('table'=>$cur, 'update'=>$up, 207 | 'columns'=>$cols, 'indexes'=>$idxs, 'id'=>$id));` 208 | function my_dbte_row_saved($args){ 209 | // do things 210 | } 211 | 212 | ``` 213 | 214 | # Shortcodes # 215 | 216 | You can use a shortcode to include a dbte interface on a wordpress 217 | page. Please use with care. 218 | 219 | [dbte id=table-editor-id] - (id defaults to table) 220 | 221 | 222 | ## Caveats ## 223 | 224 | * Dont put an editable table editor on your public facing screens using the shortcode! 225 | 226 | ## Troubleshooting ## 227 | 228 | Feel free to ask support questions / open trouble tickets 229 | 230 | * https://wordpress.org/support/plugin/wp-db-table-editor 231 | * https://github.com/AccelerationNet/wp-db-table-editor/issues 232 | 233 | ### FAQ ### 234 | 235 | * I dont see any interface / nothing changed? 236 | ** Did you complete the installation process, including appropriate 237 | `add_db_table_editor` calls? 238 | * My delete button is missing / I Can't Edit 239 | ** You either dont have `editcap` or `id_column` is misconfigured 240 | ** https://github.com/AccelerationNet/wp-db-table-editor/issues/5 241 | 242 | # Advanced Examples 243 | 244 | ## Custom Javascript and Buttons on the table editorcc 245 | 246 | See: examples/custom-buttons-and-js.php 247 | examples/custom-buttons.js 248 | 249 | Shows how to add custom javascript to a report page and adds a custom 250 | load button on the grid 251 | 252 | 253 | ## CF7 DB Submit Plugin integration 254 | 255 | See: examples/cf7dbsubmit_integration.php 256 | 257 | This is not a fully runnable example, but should give good examples of 258 | 259 | * working cf7dbsubmit plugin 260 | * Custom save delete hooks 261 | * custom complex sql building with this plugin 262 | * sending notifications on edit of specific fields 263 | 264 | cf7dbsubmit stores its data in a "hashtable" format of: 265 | 266 | form, submit_time, field_name, field_value 267 | 268 | but we want to present this in a more excel fasion of each field being 269 | a column of our spreadsheet and each row being a different submission 270 | 271 | 272 | # Contributers and Thanks 273 | * bobbysmith007 / Acceleration.net - Primary developer of plugin 274 | * nikomuse - i18n support 275 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | === WP-DB-Table-Editor === 2 | 3 | Contributers: bobbysmith007 4 | Donate link: https://www.acceleration.net/programming/donate-to-acceleration-net/ 5 | Tags: admin screens, database, editor 6 | Requires at least: 3.0.0 7 | Tested Up To: 4.2.2 8 | Stable tag: trunk 9 | License: BSD 10 | URL: https://github.com/AccelerationNet/wp-db-table-editor/ 11 | 12 | == Description == 13 | 14 | This is a Wordpress plugin that allows direct excel-like editing of 15 | tables in your Wordpress database. It's goals are to provide useful, 16 | simple, flexible database table admin screens. 17 | 18 | It supports: 19 | 20 | * one table per admin screen, as many admin screens as desired 21 | * These are organized under a new "DB Table Editor" menu item 22 | * excel spreadsheet like interface using SlickGrid 23 | * Filter and Sort results 24 | * Add, Edit & Delete records 25 | * Custom buttons extensibility 26 | * Custom permissions per interface for viewing and editing 27 | (defaults to: edit_others_posts) 28 | * editing defaults to the same permission as viewing if not specified 29 | * CSV exports of filtered grid 30 | * Custom primary key names (but must be a single value / column) 31 | 32 | == Installation == 33 | 34 | This is installed the same way all wordpress plugins: 35 | 36 | * Drop the unzipped plugin directory into your wordpress install at 37 | `wp-content/plugins/wp-db-table-editor` 38 | 39 | * Activate the plugin via the Wordpress Admin > "Plugins" menu 40 | 41 | 42 | 43 | = Adding an interface = 44 | DB-Table Editor Interfaces are added by calling the 45 | add_db_table_editor function in your theme's `functions.php` file. 46 | This supports `wp_parse_args` style arguments. 47 | 48 | * `title`: what shows up in the H1 on the screen and in menues 49 | * ex: `title=>"My Product Reports Page"` 50 | * `table`: the table we wish to display / edit 51 | * ex: `table=>"wp_my_custom_table"` 52 | * `id`: the admin interface id (defaults to table) 53 | * ex: `id=>"custom_table_interface_1"` 54 | * `id_column`: the column in each row that names the id for the row 55 | * ex: `id_column=>"id"` 56 | * `dataFn`: a function that returns the data to be displayed / 57 | edited, defaults to `select * from {table}`. This should return ARRAY_N 58 | through wpdb->get_results. Alternatively it may return a DBTE_DataTable. 59 | `dataFn` is called with the arguemnts array to add_db_table_editor; 60 | * ex: `dataFn=>"myCustomInterfaceFunction"` 61 | * `jsFile`: the name of a registered script that will be enqueued for 62 | this interface 63 | * ex: `jsFile=>"my-custom-interface-js"` 64 | * `cap`: the capability a user needs to view/edit this interface, 65 | defaults to edit_others_posts 66 | * ex: `cap=>"edit_others_posts"` 67 | * `editcap`: the capability required to edit the grid, if not set 68 | all viewers are assumed to be editors 69 | * ex: `editcap=>"edit_others_posts"` 70 | * `noedit`: turns off the editing abilities (same as editcap=nosuchcapability) 71 | * ex: `noedit=>true` 72 | * `columnFilters`: Default column filters, this is an array of column->val 73 | to be applied as default column fitlers when the page is loaded 74 | * ex: `columnFilters=>Array("Year"=>"2017")` 75 | * `columnNameMap`: A map of actual column names to displayed label 76 | * Ex: `columnNameMap=>Array('column_name'=>'Column Alias')` 77 | * `noedit_columns`, `hide_columns`: You may wish to hide some columns 78 | or prevent edit. You may do so by setting these fields to the name 79 | of columns you wish hidden or uneditable (eg: the id) 80 | * Ex:`noedit_columns=>"data,id"` or `noedit_columns=>Array('data', 'id')` 81 | * `save_cb`, `delete_cb`: function names to be called with an array of data: 82 | the dbte, update array, column array and modified indexes array 83 | `call_user_func($cur->save_cb,Array('table'=>$cur, 'update'=>$up, 84 | 'columns'=>$cols, 'indexes'=>$idxs, 'id'=>$id));` 85 | `call_user_func($cur->delete_cb,$cur,$id);` 86 | If your call back inserts data it should fill in $data['id'] and accept data 87 | by reference 88 | * `auto_date`: should columns that appear to be datetimes, be treated as such 89 | This is based on the columns data type 90 | * Sort of buggy but allows some different date formats than iso8601 91 | * Ex:`auto_date=>true` 92 | * `autoHeight`: passes the autoHeight option to slickgrid (makes 93 | there not be a vertical scrollbar on the grid and instead in the 94 | window) 95 | * Ex:`auto_height=>true` 96 | * `async_data`: request data asyncronously instead of inlining 97 | it. Makes slow queries "seem" faster. 98 | * Ex:`async_data=>true` 99 | * `default_values`: an a_array of default values that new rows should have 100 | * Ex:`default_values=>Array("name"=>"First M Last")` 101 | * `export_id_field`: the field to use when limiting the export results 102 | * some sql needs a specific field - defaults to `table`.`id_col` 103 | * Ex:`"export_id_field"=>"mytbl.fooid"` 104 | 105 | Example: 106 | 107 | ``` 108 | if(function_exists('add_db_table_editor')){ 109 | add_db_table_editor('title=Employees&table=employees'); 110 | 111 | add_db_table_editor(array( 112 | 'title'=>'Event Registrations', 113 | 'table'=>'event_registrations', 114 | 'sql'=>'SELECT * FROM event_registrations ORDER BY date_entered DESC' 115 | )); 116 | 117 | } 118 | ``` 119 | 120 | = Reasons and Expectations = 121 | 122 | Previously my company had been using DB-toolkit to provide minimal 123 | database interfaces for custom tables through the Wordpress admin. 124 | While the configuration was cumbersome for what we were doing, it did 125 | work and was easier than writing anything. However, when DB-Toolkit 126 | stopped being maintained and I couldn't find a simple, but suitable 127 | replacement, I decided to tackle my goals more head on 128 | 129 | Use of this plugin requires a basic knowledge of PHP, and SQL. It was 130 | written by a programmer to help accomplish his work and does not 131 | currently provide admin configuration screens (instead simple function 132 | calls in your theme's functions file are used to configure the 133 | screens). This was preferable to me, because my configuration is 134 | safely in source control (a problem I had when DB-toolkit would 135 | upgrade and lose all configuration). 136 | 137 | == Screenshots == 138 | 139 | 1. Here's a screenshot of it in action 140 | 141 | 142 | == Adding an Interface on the fly == 143 | 144 | If we go to look up a database table editor and we dont find it, but 145 | there is a function named dbte_create_$tbl that matches, we will call 146 | that function expecting it to return a dbte instance. This is useful 147 | in situations where we may not have the data for a table editor in all 148 | circumstances (EG: not every page has a member id, so only do it on 149 | that particular page). 150 | 151 | == Adding an Interface from a plugin == 152 | 153 | If you need to add an interface from a plugin, you should use the 154 | `db_table_editor_init` action. 155 | 156 | eg: `add_action( 'db_table_editor_init', 'my_load_tables' );` 157 | 158 | Inside of the `my_load_tables` function you would include all the 159 | calls to add_db_table_editor 160 | 161 | 162 | == Custom Buttons == 163 | 164 | Buttons can be created by pushing functions into 165 | `DBTableEditor.extraButtons`. Each of these is a slick grid 166 | rowButtonFormatter and should return a string of html. 167 | 168 | eg: 169 | out += fn(row, cell, value, columnDef, dataContext); 170 | 171 | The button column width can be set by setting: 172 | DBTableEditor.buttonColumnWidth before the ready function is called 173 | 174 | 175 | = Hooks / Actions = 176 | 177 | * `db-table-editor_enqueue_scripts` is an action that will be called 178 | after enqueueing all plugin scripts and before enqueueing `jsFile` 179 | (if it exists) 180 | 181 | ``` 182 | function dbTableEditorScripts(){ 183 | wp_register_script('employee-table-extensions-js', 184 | get_stylesheet_directory_uri().'/employee-table.js', 185 | array('db-table-editor-js')); 186 | } 187 | add_action('db-table-editor_enqueue_scripts', 'dbTableEditorScripts'); 188 | ``` 189 | 190 | == dbte_row_deleted, dbte_row_updated, dbte_row_inserted == 191 | 192 | Called after a row is deleted, updated, or inserted passes 193 | 194 | ``` 195 | add_action('dbte_row_deleted', 'my_dbte_row_deleted', 10, 2); 196 | 197 | function my_dbte_row_deleted($currentTable, $idRemoved){ 198 | // do things 199 | } 200 | 201 | add_action('dbte_row_updated', 'my_dbte_row_upserted', 10, 4); 202 | add_action('dbte_row_inserted', 'my_dbte_row_upserted', 10, 4); 203 | 204 | function my_dbte_row_upserted($currentTable, $values, $columns, $indexedModified){ 205 | // do things 206 | } 207 | 208 | ``` 209 | 210 | = Shortcodes = 211 | 212 | You can use a shortcode to include a dbte interface on a wordpress 213 | page. Please use with care. 214 | 215 | [dbte id=table-editor-id] - (id defaults to table) 216 | 217 | 218 | == Caveats == 219 | 220 | * Dont put an editable table editor on your public facing screens using the shortcode! 221 | 222 | == Troubleshooting == 223 | 224 | Feel free to ask support questions / open trouble tickets 225 | 226 | * https://wordpress.org/support/plugin/wp-db-table-editor 227 | * https://github.com/AccelerationNet/wp-db-table-editor/issues 228 | 229 | === FAQ === 230 | 231 | * I dont see any interface / nothing changed? 232 | * Did you complete the installation process, including appropriate 233 | `add_db_table_editor` calls? 234 | * My delete button is missing / I Can't Edit 235 | * You either dont have `editcap` or `id_column` is misconfigured 236 | * https://github.com/AccelerationNet/wp-db-table-editor/issues/5 237 | 238 | == Advanced Examples == 239 | 240 | === Custom Javascript and Buttons on the table editor === 241 | 242 | See: examples/custom-buttons-and-js.php 243 | examples/custom-buttons.js 244 | 245 | Shows how to add custom javascript to a report page and adds a custom 246 | load button on the grid 247 | 248 | === CF7 DB Submit Plugin integration === 249 | 250 | See: examples/cf7dbsubmit_integration.php 251 | 252 | This is not a fully runnable example, but should give good examples of 253 | 254 | * working cf7dbsubmit plugin 255 | * Custom save delete hooks 256 | * custom complex sql building with this plugin 257 | * sending notifications on edit of specific fields 258 | 259 | cf7dbsubmit stores its data in a "hashtable" format of: 260 | 261 | form, submit_time, field_name, field_value 262 | 263 | but we want to present this in a more excel fasion of each field being 264 | a column of our spreadsheet and each row being a different submission 265 | 266 | 267 | == ChangeLog == 268 | 269 | For detailed information, please view: 270 | 271 | https://github.com/AccelerationNet/wp-db-table-editor/commits 272 | 273 | Version: 1.6.0 - 2017-02-23 274 | * New (customized) version of SlickGrid (branched from 6pac@github) 275 | * allow access to the default filter and the filtered items 276 | * Handle exports by sending a list of ids to export rather than 277 | trying to recreate the full filter set serverside. Allows rather 278 | arbitrary JS filtering functions without having to get nitty gritty 279 | on the server 280 | 281 | Version: 1.5.6 - 2017-02-22 282 | * allow easy button column width configuration 283 | 284 | Version: 1.5.5 - 2016-12-16 285 | * fix deprecated constructor 286 | 287 | Version: 1.5.4 - 2016-12-06 288 | * trim noedit_columns! 289 | 290 | Version: 1.5.3 - 2016-03-14 291 | * Better numeric sorting 292 | 293 | Version: 1.5.2 - 2015-11-28 294 | * fixed confused ajax-vs-async nomenclature 295 | 296 | Version: 1.5.1 - 2015-09-21 297 | * Fixed bug with new rows without default values 298 | 299 | Version: 1.5 - 2015-09-15 300 | * replace update & insert call backs and actions with dbte_save 301 | that passes an argument array instead of list of arguments. 302 | should hopefully make upgrading easier (updated example). 303 | This also allows the callback to set the "id" of the argument 304 | array (should be passed by ref) in case of insert. 305 | * async_data: option makes the grid pull data using ajax instead of 306 | inlining it. This might make the page appear more responsive if 307 | the query takes forever. You are probably better off improving 308 | your query. 309 | * when calling save ajax, pass all query arguments for the current 310 | page. Also set default values along the way 311 | 312 | Version: 1.4.2 - 2015-08-17 313 | * bug fixes: new rows were not updating their id 314 | * saving now submits the currently active cell if there is one 315 | 316 | Version: 1.4.1 - 2015-06-19 317 | * added some missing files for translation 318 | 319 | Version: 1.4 - 2015-06-18 320 | * nikomuse provided i18n support 321 | 322 | Version: 1.3.2 - 2015-03-30 323 | * introduce action `db_table_editor_init`, for other plugins 324 | to use 325 | 326 | Version: 1.3.1 - 2015-03-30 327 | * Introduce PhpSqlParser and use it instead of my 328 | half-implemented index scanning, for inserting the where clause 329 | 330 | Version: 1.3 - 2015-02-18 10:30 331 | * !! API CHANGE `update_cb`, `delete_cb`, and `dbte_row_updated` 332 | all accept ID arguments -- TODO: perhaps these should accept 333 | keyword arg arrays, to make it handle upgrades more gracefully? 334 | 335 | Version: 1.2.8 - 2015-02-04 10:30 336 | * better docs 337 | * better examples 338 | * dbte_row_inserted, dbte_row_updated, dbte_row_deleted actions 339 | 340 | 341 | 342 | 343 | 344 | == Contributers and Thanks == 345 | * bobbysmith007 / Acceleration.net - Primary developer of plugin 346 | * nikomuse - i18n support 347 | -------------------------------------------------------------------------------- /ReleaseNotes.txt: -------------------------------------------------------------------------------- 1 | Version: 1.8.0 - 2019-02-27 2 | * Update PHPSQLParser so there are less errors on export 3 | 4 | 5 | Version: 1.7.1 - 2019-01-28 6 | * fix sql limit safeguard 7 | 8 | Version: 1.7.0 - 2019-01-24 9 | * Add page_size, and next and previous buttons to page 10 | large result sets into manageable chunks 11 | 12 | Version: 1.6.1 - 2017-05-15 13 | * Change export to post to get around URL length restrictions 14 | 15 | Version: 1.6.0 - 2017-02-23 16 | * New (customized) version of SlickGrid (branched from 6pac@github) 17 | * allow access to the default filter and the filtered items 18 | * Handle exports by sending a list of ids to export rather than 19 | trying to recreate the full filter set serverside. Allows rather 20 | arbitrary JS filtering functions without having to get nitty gritty 21 | on the server 22 | 23 | Version: 1.5.6 - 2017-02-22 24 | * allow easy button column width configuration 25 | 26 | Version: 1.5.5 - 2016-12-16 27 | * fix deprecated constructor 28 | 29 | Version: 1.5.4 - 2016-12-06 30 | * trim noedit_columns! 31 | 32 | Version: 1.5.3 - 2016-03-14 33 | * Better numeric sorting 34 | 35 | Version: 1.5.2 - 2015-11-28 36 | * fixed confused ajax-vs-async nomenclature 37 | 38 | Version: 1.5.1 - 2015-09-21 39 | * Fixed bug with new rows without default values 40 | 41 | Version: 1.5 - 2015-09-15 42 | * replace update & insert call backs and actions with dbte_save 43 | that passes an argument array instead of list of arguments. 44 | should hopefully make upgrading easier (updated example). 45 | This also allows the callback to set the "id" of the argument 46 | array (should be passed by ref) in case of insert. 47 | * async_data: option makes the grid pull data using ajax instead of 48 | inlining it. This might make the page appear more responsive if 49 | the query takes forever. You are probably better off improving 50 | your query. 51 | * when calling save ajax, pass all query arguments for the current 52 | page. Also set default values along the way 53 | 54 | Version: 1.4.2 - 2015-08-17 55 | * bug fixes: new rows were not updating their id 56 | * saving now submits the currently active cell if there is one 57 | 58 | Version: 1.4.1 - 2015-06-19 59 | * added some missing files for translation 60 | 61 | Version: 1.4 - 2015-06-18 62 | * nikomuse provided i18n support 63 | 64 | Version: 1.3.2 - 2015-03-30 65 | * introduce action `db_table_editor_init`, for other plugins 66 | to use 67 | 68 | Version: 1.3.1 - 2015-03-30 69 | * Introduce PhpSqlParser and use it instead of my 70 | half-implemented index scanning, for inserting the where clause 71 | 72 | Version: 1.3 - 2015-02-18 10:30 73 | !! API CHANGE 74 | * `update_cb`, `delete_cb`, and `dbte_row_updated` all accept ID 75 | arguments -- TODO: perhaps these should accept keyword arg 76 | arrays, to make it handle upgrades more gracefully? 77 | 78 | Version: 1.2.8 - 2015-02-04 10:30 79 | 80 | * better docs 81 | * better examples 82 | * dbte_row_inserted, dbte_row_updated, dbte_row_deleted actions 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /SplClassLoader.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | 21 | /** 22 | * SplClassLoader implementation that implements the technical interoperability 23 | * standards for PHP 5.3 namespaces and class names. 24 | * 25 | * https://gist.github.com/jwage/221634 26 | * https://gist.githubusercontent.com/jwage/221634/raw/9ce0d631443c95016c996222f25ec11b80b57dd4/SplClassLoader.php 27 | * http://groups.google.com/group/php-standards/web/psr-0-final-proposal?pli=1 28 | * 29 | * // Example which loads classes for the Doctrine Common package in the 30 | * // Doctrine\Common namespace. 31 | * $classLoader = new SplClassLoader('Doctrine\Common', '/path/to/doctrine'); 32 | * $classLoader->register(); 33 | * 34 | * @license http://www.opensource.org/licenses/mit-license.html MIT License 35 | * @author Jonathan H. Wage 36 | * @author Roman S. Borschel 37 | * @author Matthew Weier O'Phinney 38 | * @author Kris Wallsmith 39 | * @author Fabien Potencier 40 | */ 41 | class SplClassLoader 42 | { 43 | private $_fileExtension = '.php'; 44 | private $_namespace; 45 | private $_includePath; 46 | private $_namespaceSeparator = '\\'; 47 | 48 | /** 49 | * Creates a new SplClassLoader that loads classes of the 50 | * specified namespace. 51 | * 52 | * @param string $ns The namespace to use. 53 | */ 54 | public function __construct($ns = null, $includePath = null) 55 | { 56 | $this->_namespace = $ns; 57 | $this->_includePath = $includePath; 58 | } 59 | 60 | /** 61 | * Sets the namespace separator used by classes in the namespace of this class loader. 62 | * 63 | * @param string $sep The separator to use. 64 | */ 65 | public function setNamespaceSeparator($sep) 66 | { 67 | $this->_namespaceSeparator = $sep; 68 | } 69 | 70 | /** 71 | * Gets the namespace seperator used by classes in the namespace of this class loader. 72 | * 73 | * @return void 74 | */ 75 | public function getNamespaceSeparator() 76 | { 77 | return $this->_namespaceSeparator; 78 | } 79 | 80 | /** 81 | * Sets the base include path for all class files in the namespace of this class loader. 82 | * 83 | * @param string $includePath 84 | */ 85 | public function setIncludePath($includePath) 86 | { 87 | $this->_includePath = $includePath; 88 | } 89 | 90 | /** 91 | * Gets the base include path for all class files in the namespace of this class loader. 92 | * 93 | * @return string $includePath 94 | */ 95 | public function getIncludePath() 96 | { 97 | return $this->_includePath; 98 | } 99 | 100 | /** 101 | * Sets the file extension of class files in the namespace of this class loader. 102 | * 103 | * @param string $fileExtension 104 | */ 105 | public function setFileExtension($fileExtension) 106 | { 107 | $this->_fileExtension = $fileExtension; 108 | } 109 | 110 | /** 111 | * Gets the file extension of class files in the namespace of this class loader. 112 | * 113 | * @return string $fileExtension 114 | */ 115 | public function getFileExtension() 116 | { 117 | return $this->_fileExtension; 118 | } 119 | 120 | /** 121 | * Installs this class loader on the SPL autoload stack. 122 | */ 123 | public function register() 124 | { 125 | spl_autoload_register(array($this, 'loadClass')); 126 | } 127 | 128 | /** 129 | * Uninstalls this class loader from the SPL autoloader stack. 130 | */ 131 | public function unregister() 132 | { 133 | spl_autoload_unregister(array($this, 'loadClass')); 134 | } 135 | 136 | /** 137 | * Loads the given class or interface. 138 | * 139 | * @param string $className The name of the class to load. 140 | * @return void 141 | */ 142 | public function loadClass($className) 143 | { 144 | if (null === $this->_namespace || $this->_namespace.$this->_namespaceSeparator === substr($className, 0, strlen($this->_namespace.$this->_namespaceSeparator))) { 145 | $fileName = ''; 146 | $namespace = ''; 147 | if (false !== ($lastNsPos = strripos($className, $this->_namespaceSeparator))) { 148 | $namespace = substr($className, 0, $lastNsPos); 149 | $className = substr($className, $lastNsPos + 1); 150 | $fileName = str_replace($this->_namespaceSeparator, DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR; 151 | } 152 | $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . $this->_fileExtension; 153 | 154 | require ($this->_includePath !== null ? $this->_includePath . DIRECTORY_SEPARATOR : '') . $fileName; 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /assets/db-table-editor.css: -------------------------------------------------------------------------------- 1 | .db-table-editor { border:1px solid black; min-height: 500px; margin:25px; } 2 | .db-table-editor .delete { padding:0px; margin:0 3px; line-height:20px; height:20px;} 3 | 4 | .db-table-editor .slick-row.active > *, 5 | .db-table-editor .slick-row.selected > * {background-color: #DFD;} 6 | 7 | .db-table-editor .slick-row .slick-cell.selected, 8 | .db-table-editor .slick-row .slick-cell.active {background-color: #DDF;} 9 | 10 | .slick-header-column{font-weight:bold; color: #339;} 11 | 12 | .slick-headerrow-column.ui-state-default,.slick-headerrow-column.ui-state-default > input {background-color :#FFD;} 13 | .slick-headerrow-column.ui-state-default {padding:0px;box-sizing:border-box; 14 | -moz-box-sizing:border-box;} 15 | .slick-headerrow-column.ui-state-default > input {width:99%;box-sizing:border-box; 16 | -moz-box-sizing:border-box;} 17 | .db-table-editor {font-size:12px;} 18 | 19 | .ui-datepicker-trigger { 20 | background:url(images/calendar.png); height:20px; width:20px; padding:2px; 21 | } 22 | 23 | 24 | .slick-row.new-row .slick-cell.r0:after{ content: " < new > "}; 25 | -------------------------------------------------------------------------------- /assets/db-table-editor.js: -------------------------------------------------------------------------------- 1 | if(typeof(console)=='undefined')console={log:function(){}}; 2 | if(typeof(DBTableEditor)=='undefined') DBTableEditor={}; 3 | 4 | // JS Extension Points 5 | /* 6 | * DBTableEditor.getItemMetadata 7 | * DBTableEditor.toLocaleDate 8 | * DBTableEditor.dateFormats 9 | */ 10 | 11 | 12 | // based on https://github.com/brondavies/SlickGrid/commit/d5966858cd4f7591ba3da5789009b488ad05b021#diff-7f1ab5db3c0316e19a9ee635a1e2f2d0R1374 13 | DBTableEditor.defaultValueFormatter = function (row, cell, value, columnDef, dataContext) { 14 | if(value == "0000-00-00 00:00:00") return null; 15 | var dv = DBTableEditor.toLocaleDate(value); 16 | //console.log(row, cell, value, columnDef, dv); 17 | if (value == null)return ""; 18 | else if ( dv ) return dv; 19 | else if (value.toLocaleDateString ) return value.toLocaleDateString(); 20 | else return (value + "").replace(/&/g,"&").replace(//g,">"); 21 | }; 22 | 23 | DBTableEditor.parseQuery = function(query) { 24 | var obj = {}; 25 | if(!query || query.length < 1) return obj; 26 | var vars = query.split('&'); 27 | for (var i = 0; i < vars.length; i++) { 28 | var pair = vars[i].split('='); 29 | obj[decodeURIComponent(pair[0])]=decodeURIComponent(pair[1]); 30 | } 31 | return obj; 32 | }; 33 | DBTableEditor.commandQueue =[]; 34 | DBTableEditor.queueAndExecuteCommand = function(item, column, editCommand){ 35 | DBTableEditor.commandQueue.push(editCommand); 36 | editCommand.execute(); 37 | }; 38 | 39 | DBTableEditor.saveFailCB = function(err, resp){ 40 | console.log('SAVE FAILED', err, resp); 41 | jQuery('button.save').attr("disabled", null); 42 | var src = jQuery('button.save img').attr('src'); 43 | jQuery('button.save img').attr('src',src.replace('loading.gif','accept.png')); 44 | 45 | }; 46 | DBTableEditor.makeSaveCB = function(rows){ 47 | return function(newIds) { 48 | console.log('Save Success', newIds); 49 | jQuery('button.save').attr("disabled", null); 50 | var src = jQuery('button.save img').attr('src'); 51 | jQuery('button.save img').attr('src',src.replace('loading.gif','accept.png')); 52 | 53 | // reset save tracking 54 | jQuery.each(rows, function(idx, item) { 55 | item.newRow = false; 56 | item.dirty = false; 57 | item.modifiedIdxs =[]; 58 | }); 59 | // update ids 60 | var pair; 61 | jQuery.each(newIds, function(idx, pair){ 62 | if(!pair.rowId) return true; 63 | var item = DBTableEditor.dataView.getItemById( pair.rowId ); 64 | // console.log(item, pair.rowId); 65 | item[DBTableEditor.columnMap[DBTableEditor.id_column]] = pair.dbid; 66 | DBTableEditor.dataView.updateItem(pair.rowId, item); 67 | }); 68 | DBTableEditor.clearPendingSaves(); 69 | }; 70 | }; 71 | 72 | DBTableEditor.clearFilters = function(){ 73 | DBTableEditor.columnFilters = {}; 74 | jQuery(DBTableEditor.grid.getHeaderRow()) 75 | .find(':input').each(function(){jQuery(this).val('');}); 76 | DBTableEditor.dataView.refresh(); 77 | }; 78 | 79 | DBTableEditor.buttonColumnWidth=85; 80 | 81 | DBTableEditor.save = function(){ 82 | if (Slick.GlobalEditorLock.isActive() && !Slick.GlobalEditorLock.commitCurrentEdit()) 83 | return; 84 | jQuery('button.save').attr("disabled", "disabled"); 85 | var src = jQuery('button.save img').attr('src'); 86 | jQuery('button.save img').attr('src',src.replace('accept.png','loading.gif')); 87 | 88 | // the last time we modified a row should contain all the final modifications 89 | var it,h = {},i,r, toSave=[], rows=[], mod = DBTableEditor.modifiedRows.slice(0), modified; 90 | while(( r = mod.pop() )){ 91 | var column = DBTableEditor.data.columns[r.cell]; 92 | // console.log(column, r.cell); 93 | if(column && column.isDate){ 94 | r.item[r.cell-1] = DBTableEditor.toISO8601(r.item[r.cell-1], true); 95 | //console.log('Saving date', r.item, r.cell, column, r.item[r.cell-1]); 96 | } 97 | 98 | // cells have a delete idx to be removed 99 | if((it = h[r.item.id])){ 100 | it.modifiedIdxs.push(r.cell-1); 101 | continue; 102 | } 103 | r.item.modifiedIdxs = [r.cell-1]; 104 | h[r.item.id] = r.item; 105 | // the extend ensures we send object json which includes ".id" 106 | // instead of array json which elides it 107 | toSave.push(jQuery.extend({}, DBTableEditor.default_values, r.item)); 108 | rows.push(r.item); 109 | } 110 | //console.log('trying to save: ', toSave); 111 | var cols = DBTableEditor.data.columns.map(function(c){return c.originalName;}); 112 | cols.shift(); // remove buttons 113 | var toSend = JSON.stringify({ 114 | modifiedIdxs:toSave.map(function(it){return it.modifiedIdxs;}), 115 | columns:cols, 116 | rows:toSave 117 | }); 118 | 119 | jQuery.post(ajaxurl, 120 | jQuery.extend({},DBTableEditor.query,DBTableEditor.hashQuery, 121 | {action:'dbte_save', 122 | data:toSend, 123 | table:DBTableEditor.id})) 124 | .success(DBTableEditor.makeSaveCB(rows)) 125 | .error(DBTableEditor.saveFailCB); 126 | 127 | }; 128 | 129 | DBTableEditor.undo = function () { 130 | var command = DBTableEditor.commandQueue.pop(); 131 | if (command && Slick.GlobalEditorLock.cancelCurrentEdit()) { 132 | command.undo(); 133 | DBTableEditor.popPendingSaves(2);// remove the undo, and the undone thing 134 | DBTableEditor.grid.gotoCell(command.row, command.cell, false); 135 | } 136 | return false; 137 | }; 138 | DBTableEditor.gotoNewRow = function () { 139 | var cols = DBTableEditor.grid.getColumns(); 140 | var i=0,c=null; 141 | while ((c=cols[i])){ 142 | if (c.editor && c.selectable){ break;} 143 | else{ i++;} 144 | } 145 | // console.log('going to new row and cell', i) 146 | DBTableEditor.grid.gotoCell(DBTableEditor.grid.getDataLength(), i, true); 147 | }; 148 | 149 | 150 | DBTableEditor.filterRow = function (item) { 151 | //console.log(item); 152 | var columnFilters = DBTableEditor.columnFilters, 153 | grid = DBTableEditor.grid; 154 | // dont filter the new row 155 | if(item.newRow || item.dirty) return true; 156 | for (var columnId in columnFilters) { 157 | if (columnId !== undefined && columnFilters[columnId] !== "") { 158 | var cidx = grid.getColumnIndex(columnId); 159 | var c = grid.getColumns()[cidx]; 160 | if(!c) continue; 161 | var filterVal = columnFilters[columnId]; 162 | var val = item[c.field] && item[c.field].toString(); 163 | if(!(filterVal && filterVal.length > 0) ) continue; 164 | if( !val ) return false; 165 | if( !c.formatter ){ 166 | var re = new RegExp(filterVal,'i'); 167 | if (val.search(re) < 0) { 168 | return false; 169 | } 170 | } 171 | // if we have a formatted value, lets check our value both formatted 172 | // and unformatted against the search term both formatted and unformatted 173 | // primarily to standardize dates currently 174 | else if( c.formatter ){ 175 | var re = new RegExp(filterVal,'i'); 176 | // row, cell, value, columnDef, dataContext 177 | var formatted = c.formatter(item, cidx, val, c, null); 178 | var formattedFilter = c.formatter(item, cidx, filterVal, c, null); 179 | var reformattedFilter = new RegExp(formattedFilter,'i'); 180 | return ((val && val.search(re) >= 0) 181 | || (val && val.search(reformattedFilter) >= 0) 182 | || (formatted && formatted.search(re) >= 0) 183 | || (formatted && formatted.search(reformattedFilter) >= 0)); 184 | } 185 | } 186 | } 187 | return true; 188 | }; 189 | 190 | DBTableEditor.deleteSuccess = function(data, id, rowId){ 191 | //console.log('Removed', data, id, rowId); 192 | DBTableEditor.dataView.deleteItem(rowId); 193 | }; 194 | 195 | DBTableEditor.deleteFail = function(err, resp){ 196 | console.log('delete failed', err, resp); 197 | }; 198 | 199 | DBTableEditor.deleteHandler = function(el){ 200 | var btn = jQuery(el); 201 | var id = btn.data("id"); 202 | var rowid = btn.data('rowid'); 203 | var row = DBTableEditor.dataView.getItemById(rowid); 204 | var rObj = {}; 205 | if(!id){ 206 | DBTableEditor.dataView.deleteItem(rowid); 207 | return; 208 | } 209 | btn.parents('.slick-row').addClass('active'); 210 | if(!btn.is('button'))btn = btn.parents('button'); 211 | if (!confirm(translations['confirm_delete_row'])) return; 212 | 213 | // we have an empty column first for delete buttons 214 | for(var i=0,c=null,v=null;c=DBTableEditor.data.columns[i+1];i++) 215 | rObj[c.originalName]=row[i]; 216 | 217 | var reqArgs = jQuery.extend({action:'dbte_delete', dataid:id, rowid:rowid, table:DBTableEditor.id}, rObj); 218 | //console.log(rObj, reqArgs); 219 | jQuery.post(ajaxurl, reqArgs) 220 | .success(function(data){DBTableEditor.deleteSuccess(data, id, rowid);}) 221 | .error(DBTableEditor.deleteFail); 222 | return false; 223 | }; 224 | DBTableEditor.extraButtons=[]; 225 | DBTableEditor.rowButtonFormatter = function(row, cell, value, columnDef, dataContext) { 226 | // if(row==0)console.log(row,cell, value, columnDef, dataContext); 227 | var id = dataContext[DBTableEditor.columnMap[DBTableEditor.id_column]]; 228 | var rowid = dataContext.id; // uses id, NOT id_column 229 | var out=""; 230 | // console.log(id, row, cell, value, columnDef); 231 | if(!id){ out += "< new > "; } 232 | var url = DBTableEditor.baseUrl+'/assets/images/delete.png'; 233 | out += ''; 237 | jQuery.each(DBTableEditor.extraButtons, function(i,fn){ 238 | out += fn(row, cell, value, columnDef, dataContext); 239 | }); 240 | return out; 241 | }; 242 | 243 | DBTableEditor.exportCSV = function(){ 244 | var args=jQuery.extend({}, DBTableEditor.query, DBTableEditor.hashQuery); 245 | var cols = DBTableEditor.data.columns; 246 | args.ids=[]; 247 | jQuery.each(DBTableEditor.dataView.getFilteredItems(), function(idx, item){ 248 | args.ids.push(item.id); 249 | }); 250 | args.ids = args.ids.join(','); 251 | delete(args["page"]); 252 | var url = ajaxurl+'?action=dbte_export_csv&table='+DBTableEditor.id; 253 | var $form = jQuery('
'); 254 | jQuery.each(args, function(k,v){ 255 | var $in = jQuery(""); 256 | $in.val(v); 257 | $form.append($in); 258 | }); 259 | jQuery(document.body).append($form); 260 | $form.submit(); 261 | // window.location=url; 262 | }; 263 | 264 | DBTableEditor.updatePagingInfo = function(){ 265 | var cnt = DBTableEditor.dataView.getPagingInfo()["totalRows"]; 266 | jQuery('.db-table-editor-row-count').text (sprintf(translations['row_count'], cnt, DBTableEditor.data.rows.length)); 267 | }; 268 | 269 | DBTableEditor._ids_ ={}; 270 | DBTableEditor.newId = function(id){ 271 | var newid=id; 272 | while(!newid || DBTableEditor._ids_[newid]){ 273 | newid = Math.floor(Math.random() * 100000)*100000;} 274 | DBTableEditor._ids_[newid]=true; 275 | return newid; 276 | }; 277 | 278 | DBTableEditor.popPendingSaves = function(n){ 279 | if(n==null) n = 1; 280 | var rtn, it; 281 | if(n == 1) 282 | rtn = DBTableEditor.modifiedRows.pop(); 283 | else { 284 | rtn=[]; 285 | // while we havent popped enough and there are things to pop 286 | while(n-->0 && (it=DBTableEditor.modifiedRows.pop())) rtn.push(it); 287 | } 288 | jQuery('.pending-save-count').text(DBTableEditor.modifiedRows.length); 289 | return rtn; 290 | }; 291 | 292 | DBTableEditor.clearPendingSaves = function(){ 293 | DBTableEditor.modifiedRows = []; 294 | jQuery('.pending-save-count').text(DBTableEditor.modifiedRows.length); 295 | }; 296 | DBTableEditor.addPendingSave = function(args){ 297 | DBTableEditor.modifiedRows.push(args); 298 | jQuery('.pending-save-count').text(DBTableEditor.modifiedRows.length); 299 | }; 300 | 301 | // These three function were culled from various stack traces 302 | // as ways to disable cell nav while using an auto completing editor 303 | // While not called in here, custom editors might wish to 304 | // disable/enableGridCellNavigation 305 | DBTableEditor.disableCellNavHandler = function (e, args) { 306 | if (!e.shiftKey && !e.altKey && !e.ctrlKey) { 307 | if (e.keyCode === jQuery.ui.keyCode.LEFT 308 | || e.keyCode === jQuery.ui.keyCode.RIGHT 309 | || e.keyCode === jQuery.ui.keyCode.UP 310 | || e.keyCode === jQuery.ui.keyCode.DOWN) { 311 | e.stopImmediatePropagation(); 312 | } 313 | } 314 | }; 315 | 316 | DBTableEditor.disableGridCellNavigation = function() { 317 | DBTableEditor.grid.onKeyDown.subscribe(DBTableEditor.disableCellNavHandler); 318 | }; 319 | 320 | DBTableEditor.enableGridCellNavigation = function() { 321 | DBTableEditor.grid.onKeyDown.unsubscribe(DBTableEditor.disableCellNavHandler); 322 | }; 323 | 324 | DBTableEditor.afterLoadDataHandler = function(data){ 325 | DBTableEditor.data = data; 326 | DBTableEditor.afterLoadData(); 327 | jQuery(".status .loading").hide(); 328 | }; 329 | 330 | // copied from slick.grid#L1502 for debugging 331 | DBTableEditor.defaultFormatter = function (row, cell, value, columnDef, dataContext){ 332 | if (value == null) { 333 | return ""; 334 | } else { 335 | return (value + "").replace(/&/g,"&").replace(//g,">"); 336 | } 337 | }; 338 | 339 | DBTableEditor.afterLoadData = function(){ 340 | var opts = DBTableEditor.options; 341 | var rows = DBTableEditor.data.rows; 342 | var columns = DBTableEditor.data.columns; 343 | var columnMap = DBTableEditor.columnMap = {}; 344 | DBTableEditor.columnNameMap = DBTableEditor.columnNameMap||{}; 345 | if(typeof(DBTableEditor.noedit_columns)=="string") 346 | DBTableEditor.noedit_columns = DBTableEditor.noedit_columns.split(/\s*,\s*/); 347 | if(typeof(DBTableEditor.hide_columns)=="string") 348 | DBTableEditor.hide_columns = DBTableEditor.hide_columns.split(/\s*,\s*/); 349 | DBTableEditor.default_values = opts.default_values; 350 | 351 | // init columns 352 | for( var i=0, c ; c=columns[i] ; i++){ 353 | c.id=c.name.toLowerCase(); 354 | if(c.isDate === null) c.isDate = false; 355 | if(DBTableEditor.auto_date 356 | && (c.type=="timestamp" 357 | || c.type==12 // mysql datetime 358 | || c.type==7 // mysql timestamp 359 | || c.type=="datetime" 360 | || c.type=="date")) c.isDate = true; 361 | if(c.isDate){ 362 | if(!c.formatter) c.formatter = DBTableEditor.defaultValueFormatter; 363 | if(!c.editor) c.editor = DBTableEditor.DateEditor; 364 | } 365 | 366 | 367 | 368 | c.originalName = c.name; 369 | if(DBTableEditor.columnNameMap[c.name]){ 370 | c.name = DBTableEditor.columnNameMap[c.name]; 371 | } 372 | else{ 373 | c.name = c.name.replace("_"," "); 374 | } 375 | c.field = i; 376 | c.sortable = true; 377 | if(jQuery.inArray(c.originalName, DBTableEditor.hide_columns)>-1){ 378 | c.maxWidth=c.minWidth=c.width=5; 379 | c.resizable=c.selectable=c.focusable=false; 380 | } 381 | if(jQuery.inArray(c.originalName, DBTableEditor.noedit_columns)>-1){ 382 | c.focusable=false; 383 | c.selectable=false; 384 | c.cannotTriggerInsert=true; 385 | } 386 | //account for buttons column at 0 if needed 387 | columnMap[c.id] = i; //DBTableEditor.noedit ? i : i+1; 388 | 389 | if(c.id!=DBTableEditor.id_column && !c.editor){ 390 | var maxLen = 0; 391 | for(var j=0 ; j < 100 ; j++){ 392 | if(rows[j] && rows[j][c.field]){ 393 | maxLen = Math.max(rows[j][c.field].toString().length, maxLen); 394 | } 395 | else{ 396 | // console.log(j, rows[j], c.field, rows[j][c.field]); 397 | } 398 | } 399 | if(maxLen < 65) c.editor = Slick.Editors.Text; 400 | else c.editor = Slick.Editors.LongText; 401 | } 402 | } 403 | if(columnMap[DBTableEditor.id_column]==null){ 404 | console.log('Couldnt find a column:', DBTableEditor.id_column," defaulting to noedit"); 405 | DBTableEditor.noedit = true; 406 | } 407 | 408 | 409 | //init rows 410 | for(var i=0, r ; r=rows[i] ; i++){ 411 | // r.shift(null); 412 | var rid = DBTableEditor.newId((columnMap[DBTableEditor.id_column]!=null) && r[columnMap[DBTableEditor.id_column]]); 413 | // THIS MUST BE named ID in order for slickgrid to work 414 | r.id = rid; 415 | } 416 | // init columns 417 | if(!DBTableEditor.noedit){ 418 | //console.log('Adding buttons column', DBTableEditor.buttonColumnWidth); 419 | columns.unshift({id: 'buttons', 420 | formatter:DBTableEditor.rowButtonFormatter, 421 | width:DBTableEditor.buttonColumnWidth}); 422 | } 423 | 424 | 425 | var options = { 426 | enableCellNavigation: true, 427 | enableColumnReorder: true, 428 | editable: !DBTableEditor.noedit, 429 | enableAddRow: !DBTableEditor.noedit, 430 | multiColumnSort:true, 431 | autoEdit:false, 432 | editCommandHandler: DBTableEditor.queueAndExecuteCommand, 433 | showHeaderRow: true, 434 | headerRowHeight: 30, 435 | defaultColumnWidth:120, 436 | explicitInitialization: true, 437 | autoHeight:DBTableEditor.autoHeight, 438 | defaultFormatter : DBTableEditor.defaultFormatter 439 | }; 440 | 441 | DBTableEditor.columnFilters = jQuery.extend(DBTableEditor.columnFilters,DBTableEditor.query,DBTableEditor.hashQuery); 442 | delete(DBTableEditor.columnFilters["page"]); 443 | var dataView = DBTableEditor.dataView = new Slick.Data.DataView({ inlineFilters: true }); 444 | if(DBTableEditor.getItemMetadata) 445 | DBTableEditor.dataView.getItemMetadata = DBTableEditor.getItemMetadata(DBTableEditor.dataView.getItemMetadata); 446 | var grid = DBTableEditor.grid = new Slick.Grid('.db-table-editor', dataView, columns, options); 447 | //grid.setSelectionModel(new Slick.CellSelectionModel()); 448 | var nextCell = function (args){ 449 | if(!args) return; 450 | var ri = args.row === null ? rows.length-1 : args.row, 451 | ci = args.cell=== null ? 1 : args.cell + 1 ; 452 | if(ci >= columns.length){ 453 | ci=0; 454 | ri++; 455 | } 456 | //console.log("going to:", ri, ci, args); 457 | grid.gotoCell(ri, ci, true); 458 | }; 459 | 460 | DBTableEditor.clearPendingSaves(); 461 | grid.onAddNewRow.subscribe(function (e, args) { 462 | var newItem = args.item; 463 | var item = [], v, max=0; 464 | for(var i=0; i < DBTableEditor.data.columns.length ; i++){ 465 | v = newItem[i]; 466 | if(v){ 467 | max = i; 468 | item[i] = v; 469 | } 470 | } 471 | args.item = item; 472 | if(DBTableEditor.default_values){ 473 | jQuery.each(DBTableEditor.default_values,function(k,v){ 474 | item[DBTableEditor.columnMap[k]]=v; 475 | }); 476 | } 477 | grid.invalidateRow(rows.length); 478 | item.rowId = item.id = DBTableEditor.newId(); 479 | item.newRow = true; 480 | dataView.addItem(item); 481 | grid.updateRowCount(); 482 | grid.render(); 483 | var cidx = grid.getColumnIndex(args.column.id); 484 | var ridx = DBTableEditor.grid.getDataLength()-1; 485 | //match the other save args 486 | DBTableEditor.addPendingSave( 487 | { item:item, 488 | row: ridx, 489 | cell: cidx, 490 | grid: args.grid 491 | }); 492 | DBTableEditor.mostRecentEdit = new Date(); 493 | nextCell({row:ridx, cell:cidx}); 494 | }); 495 | 496 | grid.onCellChange.subscribe(function(e, args){ 497 | var item = args.item, column = grid.getColumns()[args.cell]; 498 | if(column.isDate){ 499 | item[args.cell-1] = DBTableEditor.toISO8601(item[args.cell-1], true); 500 | } 501 | //console.log('edit', e, args, item); 502 | item.dirty = true; 503 | DBTableEditor.addPendingSave(args); 504 | DBTableEditor.mostRecentEdit = new Date(); 505 | nextCell(args); 506 | }); 507 | 508 | grid.onSort.subscribe(function(e, args){ // args: sort information. 509 | var cols = args.sortCols; 510 | var typedVal = function(c, r, n){ 511 | var v = r[n]; 512 | var vN = Number(v); 513 | if(!isNaN(vN)) return vN; 514 | else if(c.id.search('date')>=0) return new Date(v); 515 | return v && v.toString().toLowerCase(); 516 | }; 517 | var rowSorter = function (r1, r2) { 518 | for (var c, i=0; c=cols[i]; i++) { 519 | var field = c.sortCol.field; 520 | var sign = c.sortAsc ? 1 : -1; 521 | var value1 = typedVal(c.sortCol,r1,field), 522 | value2 = typedVal(c.sortCol,r2,field); 523 | if( typeof(value1) != typeof(value2) ){ 524 | value1=value2.toString(); 525 | value2=value2.toString(); 526 | } 527 | var result = (value1 == value2 ? 0 : (value1 > value2 ? 1 : -1)) * sign; 528 | if (result != 0) { 529 | return result; 530 | } 531 | } 532 | return 0; 533 | }; 534 | dataView.sort(rowSorter); 535 | grid.invalidate(); 536 | grid.render(); 537 | }); 538 | 539 | dataView.onRowCountChanged.subscribe(function (e, args) { 540 | grid.updateRowCount(); 541 | grid.render(); 542 | DBTableEditor.updatePagingInfo(); 543 | }); 544 | 545 | dataView.onRowsChanged.subscribe(function (e, args) { 546 | grid.invalidateRows(args.rows); 547 | grid.render(); 548 | DBTableEditor.updatePagingInfo(); 549 | }); 550 | 551 | jQuery(grid.getHeaderRow()).delegate(":input", "change keyup", function (e) { 552 | var columnId = jQuery(this).data("columnId"); 553 | if (columnId != null) { 554 | DBTableEditor.columnFilters[columnId] = jQuery.trim(jQuery(this).val()); 555 | dataView.refresh(); 556 | } 557 | }); 558 | 559 | grid.onHeaderRowCellRendered.subscribe(function(e, args) { 560 | jQuery(args.node).empty(); 561 | if(args.column.id == "buttons") return; 562 | jQuery("") 563 | .data("columnId", args.column.id) 564 | .val(DBTableEditor.columnFilters[args.column.id]) 565 | .attr('name','filter-'+args.column.id) 566 | .appendTo(args.node); 567 | }); 568 | 569 | grid.init(); 570 | if(columns.length < 8) grid.autosizeColumns(); 571 | 572 | dataView.beginUpdate(); 573 | dataView.setItems(rows); 574 | dataView.setFilter(DBTableEditor.filterRow); 575 | dataView.endUpdate(); 576 | dataView.refresh(); 577 | 578 | DBTableEditor.updatePagingInfo(); 579 | 580 | jQuery('button.save').attr("disabled", null); 581 | }; 582 | 583 | DBTableEditor.onload = function(opts){ 584 | // TODO: switch to objects so there can be more than one table to edit *sigh* 585 | //console.log('Loading db table'); 586 | DBTableEditor.query = DBTableEditor.parseQuery(window.location.search.substring(1)); 587 | DBTableEditor.hashQuery = DBTableEditor.parseQuery(window.location.hash.substring(1)); 588 | 589 | jQuery.extend(DBTableEditor, opts); 590 | DBTableEditor.options = opts; 591 | if(!DBTableEditor.id_column) DBTableEditor.id_column='id'; 592 | DBTableEditor.id_column = DBTableEditor.id_column.toLowerCase(); 593 | 594 | if(DBTableEditor.data){ 595 | DBTableEditor.afterLoadData(); 596 | } 597 | else if (DBTableEditor.dataUrl){ 598 | jQuery(".status .loading").show(); 599 | jQuery.get(DBTableEditor.dataUrl).then(DBTableEditor.afterLoadDataHandler); 600 | } 601 | else return console.log("No Data for DBTableEditor"); 602 | }; 603 | -------------------------------------------------------------------------------- /assets/dbte-date-editor.js: -------------------------------------------------------------------------------- 1 | if(typeof(console)=='undefined')console={log:function(){}}; 2 | if(typeof(DBTableEditor)=='undefined') DBTableEditor={}; 3 | 4 | DBTableEditor.defaultOffset = new Date().getTimezoneOffset(); 5 | DBTableEditor.dateFormats = ["MM-DD-YYYY", "YYYY-MM-DD", moment.ISO_8601]; 6 | DBTableEditor.parseMoment = function(ds){ 7 | if(!ds) return null; 8 | if(ds.toDate) return ds; 9 | if(ds.getTime) return moment(ds); 10 | var m; 11 | // try MY locale -- dont know how to get it to parse to the actual locale 12 | // Locale Dates are very device / browser dependednt 13 | m = moment(ds, DBTableEditor.dateFormats); 14 | if(!m.isValid) return null; 15 | return m; 16 | }; 17 | DBTableEditor.parseDate = function(ds){ 18 | if(!ds || (ds.length && ds.length==0)) return null; 19 | if(ds && ds.getTime) return ds; 20 | var d = DBTableEditor.parseMoment(ds); 21 | return d && d.toDate(); 22 | }; 23 | 24 | DBTableEditor.toISO8601 = function(ds, return_unmodified){ 25 | var d = DBTableEditor.parseMoment(ds); 26 | if(d) return d.format(); 27 | if(return_unmodified) return ds; 28 | return null; 29 | }; 30 | 31 | DBTableEditor.toLocaleDate = function(ds, return_unmodified){ 32 | var d = DBTableEditor.parseMoment(ds); 33 | if(d) return d.toDate().toLocaleDateString(); 34 | if(return_unmodified) return ds; 35 | return null; 36 | }; 37 | 38 | (function($){ 39 | DBTableEditor.DateEditor = function(args) { 40 | var $input; 41 | var defaultValue; 42 | var scope = this; 43 | var calendarOpen = false; 44 | 45 | this.init = function () { 46 | $input = $(""); 47 | $input.appendTo(args.container); 48 | $input.focus().select(); 49 | $input.datepicker({ 50 | showOn: "button", 51 | beforeShow: function () { 52 | calendarOpen = true; 53 | }, 54 | onClose: function () { 55 | calendarOpen = false; 56 | $input.focus().select(); 57 | } 58 | }); 59 | $input.width($input.width() - 18); 60 | }; 61 | 62 | this.destroy = function () { 63 | $.datepicker.dpDiv.stop(true, true); 64 | $input.datepicker("hide"); 65 | $input.datepicker("destroy"); 66 | $input.remove(); 67 | }; 68 | 69 | this.show = function () { 70 | if (calendarOpen) { 71 | $.datepicker.dpDiv.stop(true, true).show(); 72 | } 73 | }; 74 | 75 | this.hide = function () { 76 | if (calendarOpen) { 77 | $.datepicker.dpDiv.stop(true, true).hide(); 78 | } 79 | }; 80 | 81 | this.position = function (position) { 82 | if (!calendarOpen) { 83 | return; 84 | } 85 | $.datepicker.dpDiv 86 | .css("top", position.top + 30) 87 | .css("left", position.left); 88 | }; 89 | 90 | this.focus = function () { 91 | $input.focus(); 92 | }; 93 | 94 | this.loadValue = function (item) { 95 | defaultValue = DBTableEditor.toLocaleDate(item[args.column.field], true); 96 | // console.log('loading ',defaultValue); 97 | $input.val(defaultValue); 98 | $input[0].defaultValue = defaultValue; 99 | $input.select(); 100 | }; 101 | 102 | this.serializeValue = function () { 103 | var dv = DBTableEditor.toISO8601($input.val(), true); 104 | // console.log('saving ',dv); 105 | return dv; 106 | }; 107 | 108 | this.applyValue = function (item, state) { 109 | item[args.column.field] = state; 110 | }; 111 | 112 | this.isValueChanged = function () { 113 | return (!($input.val() == "" && defaultValue == null)) && ($input.val() != defaultValue); 114 | }; 115 | 116 | this.validate = function () { 117 | return { 118 | valid: true, 119 | msg: null 120 | }; 121 | }; 122 | 123 | this.init(); 124 | }; 125 | })(jQuery); -------------------------------------------------------------------------------- /assets/images/accept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccelerationNet/wp-db-table-editor/775a84db32f4ef1b299b8542836c04e099f92966/assets/images/accept.png -------------------------------------------------------------------------------- /assets/images/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccelerationNet/wp-db-table-editor/775a84db32f4ef1b299b8542836c04e099f92966/assets/images/add.png -------------------------------------------------------------------------------- /assets/images/arrow_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccelerationNet/wp-db-table-editor/775a84db32f4ef1b299b8542836c04e099f92966/assets/images/arrow_right.png -------------------------------------------------------------------------------- /assets/images/arrow_undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccelerationNet/wp-db-table-editor/775a84db32f4ef1b299b8542836c04e099f92966/assets/images/arrow_undo.png -------------------------------------------------------------------------------- /assets/images/calendar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccelerationNet/wp-db-table-editor/775a84db32f4ef1b299b8542836c04e099f92966/assets/images/calendar.png -------------------------------------------------------------------------------- /assets/images/clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccelerationNet/wp-db-table-editor/775a84db32f4ef1b299b8542836c04e099f92966/assets/images/clear.png -------------------------------------------------------------------------------- /assets/images/database_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccelerationNet/wp-db-table-editor/775a84db32f4ef1b299b8542836c04e099f92966/assets/images/database_edit.png -------------------------------------------------------------------------------- /assets/images/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccelerationNet/wp-db-table-editor/775a84db32f4ef1b299b8542836c04e099f92966/assets/images/delete.png -------------------------------------------------------------------------------- /assets/images/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccelerationNet/wp-db-table-editor/775a84db32f4ef1b299b8542836c04e099f92966/assets/images/download.png -------------------------------------------------------------------------------- /assets/images/find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccelerationNet/wp-db-table-editor/775a84db32f4ef1b299b8542836c04e099f92966/assets/images/find.png -------------------------------------------------------------------------------- /assets/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccelerationNet/wp-db-table-editor/775a84db32f4ef1b299b8542836c04e099f92966/assets/images/loading.gif -------------------------------------------------------------------------------- /assets/jquery.event.drag.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccelerationNet/wp-db-table-editor/775a84db32f4ef1b299b8542836c04e099f92966/assets/jquery.event.drag.js -------------------------------------------------------------------------------- /assets/jquery.event.drag.live.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jquery.event.drag.live - v 2.2 3 | * Copyright (c) 2010 Three Dub Media - http://threedubmedia.com 4 | * Open Source MIT License - http://threedubmedia.com/code/license 5 | */ 6 | // Created: 2010-06-07 7 | // Updated: 2012-05-21 8 | // REQUIRES: jquery 1.7.x, event.drag 2.2 9 | 10 | ;(function( $ ){ 11 | 12 | // local refs (increase compression) 13 | var $event = $.event, 14 | // ref the special event config 15 | drag = $event.special.drag, 16 | // old drag event add method 17 | origadd = drag.add, 18 | // old drag event teradown method 19 | origteardown = drag.teardown; 20 | 21 | // allow events to bubble for delegation 22 | drag.noBubble = false; 23 | 24 | // the namespace for internal live events 25 | drag.livekey = "livedrag"; 26 | 27 | // new drop event add method 28 | drag.add = function( obj ){ 29 | // call the old method 30 | origadd.apply( this, arguments ); 31 | // read the data 32 | var data = $.data( this, drag.datakey ); 33 | // bind the live "draginit" delegator 34 | if ( !data.live && obj.selector ){ 35 | data.live = true; 36 | $event.add( this, "draginit."+ drag.livekey, drag.delegate ); 37 | } 38 | }; 39 | 40 | // new drop event teardown method 41 | drag.teardown = function(){ 42 | // call the old method 43 | origteardown.apply( this, arguments ); 44 | // read the data 45 | var data = $.data( this, drag.datakey ) || {}; 46 | // bind the live "draginit" delegator 47 | if ( data.live ){ 48 | // remove the "live" delegation 49 | $event.remove( this, "draginit."+ drag.livekey, drag.delegate ); 50 | data.live = false; 51 | } 52 | }; 53 | 54 | // identify potential delegate elements 55 | drag.delegate = function( event ){ 56 | // local refs 57 | var elems = [], target, 58 | // element event structure 59 | events = $.data( this, "events" ) || {}; 60 | // query live events 61 | $.each( events || [], function( key, arr ){ 62 | // no event type matches 63 | if ( key.indexOf("drag") !== 0 ) 64 | return; 65 | $.each( arr || [], function( i, obj ){ 66 | // locate the element to delegate 67 | target = $( event.target ).closest( obj.selector, event.currentTarget )[0]; 68 | // no element found 69 | if ( !target ) 70 | return; 71 | // add an event handler 72 | $event.add( target, obj.origType+'.'+drag.livekey, obj.origHandler || obj.handler, obj.data ); 73 | // remember new elements 74 | if ( $.inArray( target, elems ) < 0 ) 75 | elems.push( target ); 76 | }); 77 | }); 78 | // if there are no elements, break 79 | if ( !elems.length ) 80 | return false; 81 | // return the matched results, and clenup when complete 82 | return $( elems ).bind("dragend."+ drag.livekey, function(){ 83 | $event.remove( this, "."+ drag.livekey ); // cleanup delegation 84 | }); 85 | }; 86 | 87 | })( jQuery ); -------------------------------------------------------------------------------- /assets/jquery.event.drop.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jquery.event.drop - v 2.2 3 | * Copyright (c) 2010 Three Dub Media - http://threedubmedia.com 4 | * Open Source MIT License - http://threedubmedia.com/code/license 5 | */ 6 | // Created: 2008-06-04 7 | // Updated: 2012-05-21 8 | // REQUIRES: jquery 1.7.x, event.drag 2.2 9 | 10 | ;(function($){ // secure $ jQuery alias 11 | 12 | // Events: drop, dropstart, dropend 13 | 14 | // add the jquery instance method 15 | $.fn.drop = function( str, arg, opts ){ 16 | // figure out the event type 17 | var type = typeof str == "string" ? str : "", 18 | // figure out the event handler... 19 | fn = $.isFunction( str ) ? str : $.isFunction( arg ) ? arg : null; 20 | // fix the event type 21 | if ( type.indexOf("drop") !== 0 ) 22 | type = "drop"+ type; 23 | // were options passed 24 | opts = ( str == fn ? arg : opts ) || {}; 25 | // trigger or bind event handler 26 | return fn ? this.bind( type, opts, fn ) : this.trigger( type ); 27 | }; 28 | 29 | // DROP MANAGEMENT UTILITY 30 | // returns filtered drop target elements, caches their positions 31 | $.drop = function( opts ){ 32 | opts = opts || {}; 33 | // safely set new options... 34 | drop.multi = opts.multi === true ? Infinity : 35 | opts.multi === false ? 1 : !isNaN( opts.multi ) ? opts.multi : drop.multi; 36 | drop.delay = opts.delay || drop.delay; 37 | drop.tolerance = $.isFunction( opts.tolerance ) ? opts.tolerance : 38 | opts.tolerance === null ? null : drop.tolerance; 39 | drop.mode = opts.mode || drop.mode || 'intersect'; 40 | }; 41 | 42 | // local refs (increase compression) 43 | var $event = $.event, 44 | $special = $event.special, 45 | // configure the drop special event 46 | drop = $.event.special.drop = { 47 | 48 | // these are the default settings 49 | multi: 1, // allow multiple drop winners per dragged element 50 | delay: 20, // async timeout delay 51 | mode: 'overlap', // drop tolerance mode 52 | 53 | // internal cache 54 | targets: [], 55 | 56 | // the key name for stored drop data 57 | datakey: "dropdata", 58 | 59 | // prevent bubbling for better performance 60 | noBubble: true, 61 | 62 | // count bound related events 63 | add: function( obj ){ 64 | // read the interaction data 65 | var data = $.data( this, drop.datakey ); 66 | // count another realted event 67 | data.related += 1; 68 | }, 69 | 70 | // forget unbound related events 71 | remove: function(){ 72 | $.data( this, drop.datakey ).related -= 1; 73 | }, 74 | 75 | // configure the interactions 76 | setup: function(){ 77 | // check for related events 78 | if ( $.data( this, drop.datakey ) ) 79 | return; 80 | // initialize the drop element data 81 | var data = { 82 | related: 0, 83 | active: [], 84 | anyactive: 0, 85 | winner: 0, 86 | location: {} 87 | }; 88 | // store the drop data on the element 89 | $.data( this, drop.datakey, data ); 90 | // store the drop target in internal cache 91 | drop.targets.push( this ); 92 | }, 93 | 94 | // destroy the configure interaction 95 | teardown: function(){ 96 | var data = $.data( this, drop.datakey ) || {}; 97 | // check for related events 98 | if ( data.related ) 99 | return; 100 | // remove the stored data 101 | $.removeData( this, drop.datakey ); 102 | // reference the targeted element 103 | var element = this; 104 | // remove from the internal cache 105 | drop.targets = $.grep( drop.targets, function( target ){ 106 | return ( target !== element ); 107 | }); 108 | }, 109 | 110 | // shared event handler 111 | handler: function( event, dd ){ 112 | // local vars 113 | var results, $targets; 114 | // make sure the right data is available 115 | if ( !dd ) 116 | return; 117 | // handle various events 118 | switch ( event.type ){ 119 | // draginit, from $.event.special.drag 120 | case 'mousedown': // DROPINIT >> 121 | case 'touchstart': // DROPINIT >> 122 | // collect and assign the drop targets 123 | $targets = $( drop.targets ); 124 | if ( typeof dd.drop == "string" ) 125 | $targets = $targets.filter( dd.drop ); 126 | // reset drop data winner properties 127 | $targets.each(function(){ 128 | var data = $.data( this, drop.datakey ); 129 | data.active = []; 130 | data.anyactive = 0; 131 | data.winner = 0; 132 | }); 133 | // set available target elements 134 | dd.droppable = $targets; 135 | // activate drop targets for the initial element being dragged 136 | $special.drag.hijack( event, "dropinit", dd ); 137 | break; 138 | // drag, from $.event.special.drag 139 | case 'mousemove': // TOLERATE >> 140 | case 'touchmove': // TOLERATE >> 141 | drop.event = event; // store the mousemove event 142 | if ( !drop.timer ) 143 | // monitor drop targets 144 | drop.tolerate( dd ); 145 | break; 146 | // dragend, from $.event.special.drag 147 | case 'mouseup': // DROP >> DROPEND >> 148 | case 'touchend': // DROP >> DROPEND >> 149 | drop.timer = clearTimeout( drop.timer ); // delete timer 150 | if ( dd.propagates ){ 151 | $special.drag.hijack( event, "drop", dd ); 152 | $special.drag.hijack( event, "dropend", dd ); 153 | } 154 | break; 155 | 156 | } 157 | }, 158 | 159 | // returns the location positions of an element 160 | locate: function( elem, index ){ 161 | var data = $.data( elem, drop.datakey ), 162 | $elem = $( elem ), 163 | posi = $elem.offset() || {}, 164 | height = $elem.outerHeight(), 165 | width = $elem.outerWidth(), 166 | location = { 167 | elem: elem, 168 | width: width, 169 | height: height, 170 | top: posi.top, 171 | left: posi.left, 172 | right: posi.left + width, 173 | bottom: posi.top + height 174 | }; 175 | // drag elements might not have dropdata 176 | if ( data ){ 177 | data.location = location; 178 | data.index = index; 179 | data.elem = elem; 180 | } 181 | return location; 182 | }, 183 | 184 | // test the location positions of an element against another OR an X,Y coord 185 | contains: function( target, test ){ // target { location } contains test [x,y] or { location } 186 | return ( ( test[0] || test.left ) >= target.left && ( test[0] || test.right ) <= target.right 187 | && ( test[1] || test.top ) >= target.top && ( test[1] || test.bottom ) <= target.bottom ); 188 | }, 189 | 190 | // stored tolerance modes 191 | modes: { // fn scope: "$.event.special.drop" object 192 | // target with mouse wins, else target with most overlap wins 193 | 'intersect': function( event, proxy, target ){ 194 | return this.contains( target, [ event.pageX, event.pageY ] ) ? // check cursor 195 | 1e9 : this.modes.overlap.apply( this, arguments ); // check overlap 196 | }, 197 | // target with most overlap wins 198 | 'overlap': function( event, proxy, target ){ 199 | // calculate the area of overlap... 200 | return Math.max( 0, Math.min( target.bottom, proxy.bottom ) - Math.max( target.top, proxy.top ) ) 201 | * Math.max( 0, Math.min( target.right, proxy.right ) - Math.max( target.left, proxy.left ) ); 202 | }, 203 | // proxy is completely contained within target bounds 204 | 'fit': function( event, proxy, target ){ 205 | return this.contains( target, proxy ) ? 1 : 0; 206 | }, 207 | // center of the proxy is contained within target bounds 208 | 'middle': function( event, proxy, target ){ 209 | return this.contains( target, [ proxy.left + proxy.width * .5, proxy.top + proxy.height * .5 ] ) ? 1 : 0; 210 | } 211 | }, 212 | 213 | // sort drop target cache by by winner (dsc), then index (asc) 214 | sort: function( a, b ){ 215 | return ( b.winner - a.winner ) || ( a.index - b.index ); 216 | }, 217 | 218 | // async, recursive tolerance execution 219 | tolerate: function( dd ){ 220 | // declare local refs 221 | var i, drp, drg, data, arr, len, elem, 222 | // interaction iteration variables 223 | x = 0, ia, end = dd.interactions.length, 224 | // determine the mouse coords 225 | xy = [ drop.event.pageX, drop.event.pageY ], 226 | // custom or stored tolerance fn 227 | tolerance = drop.tolerance || drop.modes[ drop.mode ]; 228 | // go through each passed interaction... 229 | do if ( ia = dd.interactions[x] ){ 230 | // check valid interaction 231 | if ( !ia ) 232 | return; 233 | // initialize or clear the drop data 234 | ia.drop = []; 235 | // holds the drop elements 236 | arr = []; 237 | len = ia.droppable.length; 238 | // determine the proxy location, if needed 239 | if ( tolerance ) 240 | drg = drop.locate( ia.proxy ); 241 | // reset the loop 242 | i = 0; 243 | // loop each stored drop target 244 | do if ( elem = ia.droppable[i] ){ 245 | data = $.data( elem, drop.datakey ); 246 | drp = data.location; 247 | if ( !drp ) continue; 248 | // find a winner: tolerance function is defined, call it 249 | data.winner = tolerance ? tolerance.call( drop, drop.event, drg, drp ) 250 | // mouse position is always the fallback 251 | : drop.contains( drp, xy ) ? 1 : 0; 252 | arr.push( data ); 253 | } while ( ++i < len ); // loop 254 | // sort the drop targets 255 | arr.sort( drop.sort ); 256 | // reset the loop 257 | i = 0; 258 | // loop through all of the targets again 259 | do if ( data = arr[ i ] ){ 260 | // winners... 261 | if ( data.winner && ia.drop.length < drop.multi ){ 262 | // new winner... dropstart 263 | if ( !data.active[x] && !data.anyactive ){ 264 | // check to make sure that this is not prevented 265 | if ( $special.drag.hijack( drop.event, "dropstart", dd, x, data.elem )[0] !== false ){ 266 | data.active[x] = 1; 267 | data.anyactive += 1; 268 | } 269 | // if false, it is not a winner 270 | else 271 | data.winner = 0; 272 | } 273 | // if it is still a winner 274 | if ( data.winner ) 275 | ia.drop.push( data.elem ); 276 | } 277 | // losers... 278 | else if ( data.active[x] && data.anyactive == 1 ){ 279 | // former winner... dropend 280 | $special.drag.hijack( drop.event, "dropend", dd, x, data.elem ); 281 | data.active[x] = 0; 282 | data.anyactive -= 1; 283 | } 284 | } while ( ++i < len ); // loop 285 | } while ( ++x < end ) // loop 286 | // check if the mouse is still moving or is idle 287 | if ( drop.last && xy[0] == drop.last.pageX && xy[1] == drop.last.pageY ) 288 | delete drop.timer; // idle, don't recurse 289 | else // recurse 290 | drop.timer = setTimeout(function(){ 291 | drop.tolerate( dd ); 292 | }, drop.delay ); 293 | // remember event, to compare idleness 294 | drop.last = drop.event; 295 | } 296 | 297 | }; 298 | 299 | // share the same special event configuration with related events... 300 | $special.dropinit = $special.dropstart = $special.dropend = drop; 301 | 302 | })(jQuery); // confine scope -------------------------------------------------------------------------------- /assets/jquery.event.drop.live.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jquery.event.drop.live - v 2.2 3 | * Copyright (c) 2010 Three Dub Media - http://threedubmedia.com 4 | * Open Source MIT License - http://threedubmedia.com/code/license 5 | */ 6 | // Created: 2010-06-07 7 | // Updated: 2012-05-21 8 | // REQUIRES: jquery 1.7.x, event.drag 2.2, event.drop 2.2 9 | 10 | ;(function($){ // secure $ jQuery alias 11 | 12 | // local refs (increase compression) 13 | var $event = $.event, 14 | // ref the drop special event config 15 | drop = $event.special.drop, 16 | // old drop event add method 17 | origadd = drop.add, 18 | // old drop event teradown method 19 | origteardown = drop.teardown; 20 | 21 | // allow events to bubble for delegation 22 | drop.noBubble = false; 23 | 24 | // the namespace for internal live events 25 | drop.livekey = "livedrop"; 26 | 27 | // new drop event add method 28 | drop.add = function( obj ){ 29 | // call the old method 30 | origadd.apply( this, arguments ); 31 | // read the data 32 | var data = $.data( this, drop.datakey ); 33 | // bind the live "dropinit" delegator 34 | if ( !data.live && obj.selector ){ 35 | data.live = true; 36 | $event.add( this, "dropinit."+ drop.livekey, drop.delegate ); 37 | } 38 | }; 39 | 40 | // new drop event teardown method 41 | drop.teardown = function(){ 42 | // call the old method 43 | origteardown.apply( this, arguments ); 44 | // read the data 45 | var data = $.data( this, drop.datakey ) || {}; 46 | // remove the live "dropinit" delegator 47 | if ( data.live ){ 48 | // remove the "live" delegation 49 | $event.remove( this, "dropinit", drop.delegate ); 50 | data.live = false; 51 | } 52 | }; 53 | 54 | // identify potential delegate elements 55 | drop.delegate = function( event, dd ){ 56 | // local refs 57 | var elems = [], $targets, 58 | // element event structure 59 | events = $.data( this, "events" ) || {}; 60 | // query live events 61 | $.each( events || [], function( key, arr ){ 62 | // no event type matches 63 | if ( key.indexOf("drop") !== 0 ) 64 | return; 65 | $.each( arr, function( i, obj ){ 66 | // locate the elements to delegate 67 | $targets = $( event.currentTarget ).find( obj.selector ); 68 | // no element found 69 | if ( !$targets.length ) 70 | return; 71 | // take each target... 72 | $targets.each(function(){ 73 | // add an event handler 74 | $event.add( this, obj.origType +'.'+ drop.livekey, obj.origHandler || obj.handler, obj.data ); 75 | // remember new elements 76 | if ( $.inArray( this, elems ) < 0 ) 77 | elems.push( this ); 78 | }); 79 | }); 80 | }); 81 | // may not exist when artifically triggering dropinit event 82 | if ( dd ) 83 | // clean-up after the interaction ends 84 | $event.add( dd.drag, "dragend."+drop.livekey, function(){ 85 | $.each( elems.concat( this ), function(){ 86 | $event.remove( this, '.'+ drop.livekey ); 87 | }); 88 | }); 89 | //drop.delegates.push( elems ); 90 | return elems.length ? $( elems ) : false; 91 | }; 92 | 93 | })( jQuery ); // confine scope -------------------------------------------------------------------------------- /assets/moment.js: -------------------------------------------------------------------------------- 1 | //! moment.js 2 | //! version : 2.8.3 3 | //! authors : Tim Wood, Iskren Chernev, Moment.js contributors 4 | //! license : MIT 5 | //! momentjs.com 6 | (function(a){function b(a,b,c){switch(arguments.length){case 2:return null!=a?a:b;case 3:return null!=a?a:null!=b?b:c;default:throw new Error("Implement me")}}function c(a,b){return zb.call(a,b)}function d(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function e(a){tb.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+a)}function f(a,b){var c=!0;return m(function(){return c&&(e(a),c=!1),b.apply(this,arguments)},b)}function g(a,b){qc[a]||(e(b),qc[a]=!0)}function h(a,b){return function(c){return p(a.call(this,c),b)}}function i(a,b){return function(c){return this.localeData().ordinal(a.call(this,c),b)}}function j(){}function k(a,b){b!==!1&&F(a),n(this,a),this._d=new Date(+a._d)}function l(a){var b=y(a),c=b.year||0,d=b.quarter||0,e=b.month||0,f=b.week||0,g=b.day||0,h=b.hour||0,i=b.minute||0,j=b.second||0,k=b.millisecond||0;this._milliseconds=+k+1e3*j+6e4*i+36e5*h,this._days=+g+7*f,this._months=+e+3*d+12*c,this._data={},this._locale=tb.localeData(),this._bubble()}function m(a,b){for(var d in b)c(b,d)&&(a[d]=b[d]);return c(b,"toString")&&(a.toString=b.toString),c(b,"valueOf")&&(a.valueOf=b.valueOf),a}function n(a,b){var c,d,e;if("undefined"!=typeof b._isAMomentObject&&(a._isAMomentObject=b._isAMomentObject),"undefined"!=typeof b._i&&(a._i=b._i),"undefined"!=typeof b._f&&(a._f=b._f),"undefined"!=typeof b._l&&(a._l=b._l),"undefined"!=typeof b._strict&&(a._strict=b._strict),"undefined"!=typeof b._tzm&&(a._tzm=b._tzm),"undefined"!=typeof b._isUTC&&(a._isUTC=b._isUTC),"undefined"!=typeof b._offset&&(a._offset=b._offset),"undefined"!=typeof b._pf&&(a._pf=b._pf),"undefined"!=typeof b._locale&&(a._locale=b._locale),Ib.length>0)for(c in Ib)d=Ib[c],e=b[d],"undefined"!=typeof e&&(a[d]=e);return a}function o(a){return 0>a?Math.ceil(a):Math.floor(a)}function p(a,b,c){for(var d=""+Math.abs(a),e=a>=0;d.lengthd;d++)(c&&a[d]!==b[d]||!c&&A(a[d])!==A(b[d]))&&g++;return g+f}function x(a){if(a){var b=a.toLowerCase().replace(/(.)s$/,"$1");a=jc[a]||kc[b]||b}return a}function y(a){var b,d,e={};for(d in a)c(a,d)&&(b=x(d),b&&(e[b]=a[d]));return e}function z(b){var c,d;if(0===b.indexOf("week"))c=7,d="day";else{if(0!==b.indexOf("month"))return;c=12,d="month"}tb[b]=function(e,f){var g,h,i=tb._locale[b],j=[];if("number"==typeof e&&(f=e,e=a),h=function(a){var b=tb().utc().set(d,a);return i.call(tb._locale,b,e||"")},null!=f)return h(f);for(g=0;c>g;g++)j.push(h(g));return j}}function A(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=b>=0?Math.floor(b):Math.ceil(b)),c}function B(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function C(a,b,c){return hb(tb([a,11,31+b-c]),b,c).week}function D(a){return E(a)?366:365}function E(a){return a%4===0&&a%100!==0||a%400===0}function F(a){var b;a._a&&-2===a._pf.overflow&&(b=a._a[Bb]<0||a._a[Bb]>11?Bb:a._a[Cb]<1||a._a[Cb]>B(a._a[Ab],a._a[Bb])?Cb:a._a[Db]<0||a._a[Db]>23?Db:a._a[Eb]<0||a._a[Eb]>59?Eb:a._a[Fb]<0||a._a[Fb]>59?Fb:a._a[Gb]<0||a._a[Gb]>999?Gb:-1,a._pf._overflowDayOfYear&&(Ab>b||b>Cb)&&(b=Cb),a._pf.overflow=b)}function G(a){return null==a._isValid&&(a._isValid=!isNaN(a._d.getTime())&&a._pf.overflow<0&&!a._pf.empty&&!a._pf.invalidMonth&&!a._pf.nullInput&&!a._pf.invalidFormat&&!a._pf.userInvalidated,a._strict&&(a._isValid=a._isValid&&0===a._pf.charsLeftOver&&0===a._pf.unusedTokens.length)),a._isValid}function H(a){return a?a.toLowerCase().replace("_","-"):a}function I(a){for(var b,c,d,e,f=0;f0;){if(d=J(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&w(e,c,!0)>=b-1)break;b--}f++}return null}function J(a){var b=null;if(!Hb[a]&&Jb)try{b=tb.locale(),require("./locale/"+a),tb.locale(b)}catch(c){}return Hb[a]}function K(a,b){return b._isUTC?tb(a).zone(b._offset||0):tb(a).local()}function L(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function M(a){var b,c,d=a.match(Nb);for(b=0,c=d.length;c>b;b++)d[b]=pc[d[b]]?pc[d[b]]:L(d[b]);return function(e){var f="";for(b=0;c>b;b++)f+=d[b]instanceof Function?d[b].call(e,a):d[b];return f}}function N(a,b){return a.isValid()?(b=O(b,a.localeData()),lc[b]||(lc[b]=M(b)),lc[b](a)):a.localeData().invalidDate()}function O(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(Ob.lastIndex=0;d>=0&&Ob.test(a);)a=a.replace(Ob,c),Ob.lastIndex=0,d-=1;return a}function P(a,b){var c,d=b._strict;switch(a){case"Q":return Zb;case"DDDD":return _b;case"YYYY":case"GGGG":case"gggg":return d?ac:Rb;case"Y":case"G":case"g":return cc;case"YYYYYY":case"YYYYY":case"GGGGG":case"ggggg":return d?bc:Sb;case"S":if(d)return Zb;case"SS":if(d)return $b;case"SSS":if(d)return _b;case"DDD":return Qb;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Ub;case"a":case"A":return b._locale._meridiemParse;case"X":return Xb;case"Z":case"ZZ":return Vb;case"T":return Wb;case"SSSS":return Tb;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"ww":case"WW":return d?$b:Pb;case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"W":case"e":case"E":return Pb;case"Do":return Yb;default:return c=new RegExp(Y(X(a.replace("\\","")),"i"))}}function Q(a){a=a||"";var b=a.match(Vb)||[],c=b[b.length-1]||[],d=(c+"").match(hc)||["-",0,0],e=+(60*d[1])+A(d[2]);return"+"===d[0]?-e:e}function R(a,b,c){var d,e=c._a;switch(a){case"Q":null!=b&&(e[Bb]=3*(A(b)-1));break;case"M":case"MM":null!=b&&(e[Bb]=A(b)-1);break;case"MMM":case"MMMM":d=c._locale.monthsParse(b),null!=d?e[Bb]=d:c._pf.invalidMonth=b;break;case"D":case"DD":null!=b&&(e[Cb]=A(b));break;case"Do":null!=b&&(e[Cb]=A(parseInt(b,10)));break;case"DDD":case"DDDD":null!=b&&(c._dayOfYear=A(b));break;case"YY":e[Ab]=tb.parseTwoDigitYear(b);break;case"YYYY":case"YYYYY":case"YYYYYY":e[Ab]=A(b);break;case"a":case"A":c._isPm=c._locale.isPM(b);break;case"H":case"HH":case"h":case"hh":e[Db]=A(b);break;case"m":case"mm":e[Eb]=A(b);break;case"s":case"ss":e[Fb]=A(b);break;case"S":case"SS":case"SSS":case"SSSS":e[Gb]=A(1e3*("0."+b));break;case"X":c._d=new Date(1e3*parseFloat(b));break;case"Z":case"ZZ":c._useUTC=!0,c._tzm=Q(b);break;case"dd":case"ddd":case"dddd":d=c._locale.weekdaysParse(b),null!=d?(c._w=c._w||{},c._w.d=d):c._pf.invalidWeekday=b;break;case"w":case"ww":case"W":case"WW":case"d":case"e":case"E":a=a.substr(0,1);case"gggg":case"GGGG":case"GGGGG":a=a.substr(0,2),b&&(c._w=c._w||{},c._w[a]=A(b));break;case"gg":case"GG":c._w=c._w||{},c._w[a]=tb.parseTwoDigitYear(b)}}function S(a){var c,d,e,f,g,h,i;c=a._w,null!=c.GG||null!=c.W||null!=c.E?(g=1,h=4,d=b(c.GG,a._a[Ab],hb(tb(),1,4).year),e=b(c.W,1),f=b(c.E,1)):(g=a._locale._week.dow,h=a._locale._week.doy,d=b(c.gg,a._a[Ab],hb(tb(),g,h).year),e=b(c.w,1),null!=c.d?(f=c.d,g>f&&++e):f=null!=c.e?c.e+g:g),i=ib(d,e,f,h,g),a._a[Ab]=i.year,a._dayOfYear=i.dayOfYear}function T(a){var c,d,e,f,g=[];if(!a._d){for(e=V(a),a._w&&null==a._a[Cb]&&null==a._a[Bb]&&S(a),a._dayOfYear&&(f=b(a._a[Ab],e[Ab]),a._dayOfYear>D(f)&&(a._pf._overflowDayOfYear=!0),d=db(f,0,a._dayOfYear),a._a[Bb]=d.getUTCMonth(),a._a[Cb]=d.getUTCDate()),c=0;3>c&&null==a._a[c];++c)a._a[c]=g[c]=e[c];for(;7>c;c++)a._a[c]=g[c]=null==a._a[c]?2===c?1:0:a._a[c];a._d=(a._useUTC?db:cb).apply(null,g),null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()+a._tzm)}}function U(a){var b;a._d||(b=y(a._i),a._a=[b.year,b.month,b.day,b.hour,b.minute,b.second,b.millisecond],T(a))}function V(a){var b=new Date;return a._useUTC?[b.getUTCFullYear(),b.getUTCMonth(),b.getUTCDate()]:[b.getFullYear(),b.getMonth(),b.getDate()]}function W(a){if(a._f===tb.ISO_8601)return void $(a);a._a=[],a._pf.empty=!0;var b,c,d,e,f,g=""+a._i,h=g.length,i=0;for(d=O(a._f,a._locale).match(Nb)||[],b=0;b0&&a._pf.unusedInput.push(f),g=g.slice(g.indexOf(c)+c.length),i+=c.length),pc[e]?(c?a._pf.empty=!1:a._pf.unusedTokens.push(e),R(e,c,a)):a._strict&&!c&&a._pf.unusedTokens.push(e);a._pf.charsLeftOver=h-i,g.length>0&&a._pf.unusedInput.push(g),a._isPm&&a._a[Db]<12&&(a._a[Db]+=12),a._isPm===!1&&12===a._a[Db]&&(a._a[Db]=0),T(a),F(a)}function X(a){return a.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e})}function Y(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function Z(a){var b,c,e,f,g;if(0===a._f.length)return a._pf.invalidFormat=!0,void(a._d=new Date(0/0));for(f=0;fg)&&(e=g,c=b));m(a,c||b)}function $(a){var b,c,d=a._i,e=dc.exec(d);if(e){for(a._pf.iso=!0,b=0,c=fc.length;c>b;b++)if(fc[b][1].exec(d)){a._f=fc[b][0]+(e[6]||" ");break}for(b=0,c=gc.length;c>b;b++)if(gc[b][1].exec(d)){a._f+=gc[b][0];break}d.match(Vb)&&(a._f+="Z"),W(a)}else a._isValid=!1}function _(a){$(a),a._isValid===!1&&(delete a._isValid,tb.createFromInputFallback(a))}function ab(a,b){var c,d=[];for(c=0;ca&&h.setFullYear(a),h}function db(a){var b=new Date(Date.UTC.apply(null,arguments));return 1970>a&&b.setUTCFullYear(a),b}function eb(a,b){if("string"==typeof a)if(isNaN(a)){if(a=b.weekdaysParse(a),"number"!=typeof a)return null}else a=parseInt(a,10);return a}function fb(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function gb(a,b,c){var d=tb.duration(a).abs(),e=yb(d.as("s")),f=yb(d.as("m")),g=yb(d.as("h")),h=yb(d.as("d")),i=yb(d.as("M")),j=yb(d.as("y")),k=e0,k[4]=c,fb.apply({},k)}function hb(a,b,c){var d,e=c-b,f=c-a.day();return f>e&&(f-=7),e-7>f&&(f+=7),d=tb(a).add(f,"d"),{week:Math.ceil(d.dayOfYear()/7),year:d.year()}}function ib(a,b,c,d,e){var f,g,h=db(a,0,1).getUTCDay();return h=0===h?7:h,c=null!=c?c:e,f=e-h+(h>d?7:0)-(e>h?7:0),g=7*(b-1)+(c-e)+f+1,{year:g>0?a:a-1,dayOfYear:g>0?g:D(a-1)+g}}function jb(b){var c=b._i,d=b._f;return b._locale=b._locale||tb.localeData(b._l),null===c||d===a&&""===c?tb.invalid({nullInput:!0}):("string"==typeof c&&(b._i=c=b._locale.preparse(c)),tb.isMoment(c)?new k(c,!0):(d?u(d)?Z(b):W(b):bb(b),new k(b)))}function kb(a,b){var c,d;if(1===b.length&&u(b[0])&&(b=b[0]),!b.length)return tb();for(c=b[0],d=1;d=0?"+":"-";return b+p(Math.abs(a),6)},gg:function(){return p(this.weekYear()%100,2)},gggg:function(){return p(this.weekYear(),4)},ggggg:function(){return p(this.weekYear(),5)},GG:function(){return p(this.isoWeekYear()%100,2)},GGGG:function(){return p(this.isoWeekYear(),4)},GGGGG:function(){return p(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.localeData().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.localeData().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return A(this.milliseconds()/100)},SS:function(){return p(A(this.milliseconds()/10),2)},SSS:function(){return p(this.milliseconds(),3)},SSSS:function(){return p(this.milliseconds(),3)},Z:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+p(A(a/60),2)+":"+p(A(a)%60,2)},ZZ:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+p(A(a/60),2)+p(A(a)%60,2)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()},Q:function(){return this.quarter()}},qc={},rc=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"];nc.length;)vb=nc.pop(),pc[vb+"o"]=i(pc[vb],vb);for(;oc.length;)vb=oc.pop(),pc[vb+vb]=h(pc[vb],2);pc.DDDD=h(pc.DDD,3),m(j.prototype,{set:function(a){var b,c;for(c in a)b=a[c],"function"==typeof b?this[c]=b:this["_"+c]=b},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(a){return this._months[a.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(a){return this._monthsShort[a.month()]},monthsParse:function(a){var b,c,d;for(this._monthsParse||(this._monthsParse=[]),b=0;12>b;b++)if(this._monthsParse[b]||(c=tb.utc([2e3,b]),d="^"+this.months(c,"")+"|^"+this.monthsShort(c,""),this._monthsParse[b]=new RegExp(d.replace(".",""),"i")),this._monthsParse[b].test(a))return b},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(a){return this._weekdays[a.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(a){return this._weekdaysShort[a.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(a){return this._weekdaysMin[a.day()]},weekdaysParse:function(a){var b,c,d;for(this._weekdaysParse||(this._weekdaysParse=[]),b=0;7>b;b++)if(this._weekdaysParse[b]||(c=tb([2e3,1]).day(b),d="^"+this.weekdays(c,"")+"|^"+this.weekdaysShort(c,"")+"|^"+this.weekdaysMin(c,""),this._weekdaysParse[b]=new RegExp(d.replace(".",""),"i")),this._weekdaysParse[b].test(a))return b},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY LT",LLLL:"dddd, MMMM D, YYYY LT"},longDateFormat:function(a){var b=this._longDateFormat[a];return!b&&this._longDateFormat[a.toUpperCase()]&&(b=this._longDateFormat[a.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a]=b),b},isPM:function(a){return"p"===(a+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(a,b){var c=this._calendar[a];return"function"==typeof c?c.apply(b):c},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(a,b,c,d){var e=this._relativeTime[c];return"function"==typeof e?e(a,b,c,d):e.replace(/%d/i,a)},pastFuture:function(a,b){var c=this._relativeTime[a>0?"future":"past"];return"function"==typeof c?c(b):c.replace(/%s/i,b)},ordinal:function(a){return this._ordinal.replace("%d",a)},_ordinal:"%d",preparse:function(a){return a},postformat:function(a){return a},week:function(a){return hb(a,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),tb=function(b,c,e,f){var g;return"boolean"==typeof e&&(f=e,e=a),g={},g._isAMomentObject=!0,g._i=b,g._f=c,g._l=e,g._strict=f,g._isUTC=!1,g._pf=d(),jb(g)},tb.suppressDeprecationWarnings=!1,tb.createFromInputFallback=f("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(a){a._d=new Date(a._i)}),tb.min=function(){var a=[].slice.call(arguments,0);return kb("isBefore",a)},tb.max=function(){var a=[].slice.call(arguments,0);return kb("isAfter",a)},tb.utc=function(b,c,e,f){var g;return"boolean"==typeof e&&(f=e,e=a),g={},g._isAMomentObject=!0,g._useUTC=!0,g._isUTC=!0,g._l=e,g._i=b,g._f=c,g._strict=f,g._pf=d(),jb(g).utc()},tb.unix=function(a){return tb(1e3*a)},tb.duration=function(a,b){var d,e,f,g,h=a,i=null;return tb.isDuration(a)?h={ms:a._milliseconds,d:a._days,M:a._months}:"number"==typeof a?(h={},b?h[b]=a:h.milliseconds=a):(i=Lb.exec(a))?(d="-"===i[1]?-1:1,h={y:0,d:A(i[Cb])*d,h:A(i[Db])*d,m:A(i[Eb])*d,s:A(i[Fb])*d,ms:A(i[Gb])*d}):(i=Mb.exec(a))?(d="-"===i[1]?-1:1,f=function(a){var b=a&&parseFloat(a.replace(",","."));return(isNaN(b)?0:b)*d},h={y:f(i[2]),M:f(i[3]),d:f(i[4]),h:f(i[5]),m:f(i[6]),s:f(i[7]),w:f(i[8])}):"object"==typeof h&&("from"in h||"to"in h)&&(g=r(tb(h.from),tb(h.to)),h={},h.ms=g.milliseconds,h.M=g.months),e=new l(h),tb.isDuration(a)&&c(a,"_locale")&&(e._locale=a._locale),e},tb.version=wb,tb.defaultFormat=ec,tb.ISO_8601=function(){},tb.momentProperties=Ib,tb.updateOffset=function(){},tb.relativeTimeThreshold=function(b,c){return mc[b]===a?!1:c===a?mc[b]:(mc[b]=c,!0)},tb.lang=f("moment.lang is deprecated. Use moment.locale instead.",function(a,b){return tb.locale(a,b)}),tb.locale=function(a,b){var c;return a&&(c="undefined"!=typeof b?tb.defineLocale(a,b):tb.localeData(a),c&&(tb.duration._locale=tb._locale=c)),tb._locale._abbr},tb.defineLocale=function(a,b){return null!==b?(b.abbr=a,Hb[a]||(Hb[a]=new j),Hb[a].set(b),tb.locale(a),Hb[a]):(delete Hb[a],null)},tb.langData=f("moment.langData is deprecated. Use moment.localeData instead.",function(a){return tb.localeData(a)}),tb.localeData=function(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return tb._locale;if(!u(a)){if(b=J(a))return b;a=[a]}return I(a)},tb.isMoment=function(a){return a instanceof k||null!=a&&c(a,"_isAMomentObject")},tb.isDuration=function(a){return a instanceof l};for(vb=rc.length-1;vb>=0;--vb)z(rc[vb]);tb.normalizeUnits=function(a){return x(a)},tb.invalid=function(a){var b=tb.utc(0/0);return null!=a?m(b._pf,a):b._pf.userInvalidated=!0,b},tb.parseZone=function(){return tb.apply(null,arguments).parseZone()},tb.parseTwoDigitYear=function(a){return A(a)+(A(a)>68?1900:2e3)},m(tb.fn=k.prototype,{clone:function(){return tb(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var a=tb(this).utc();return 00:!1},parsingFlags:function(){return m({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(a){return this.zone(0,a)},local:function(a){return this._isUTC&&(this.zone(0,a),this._isUTC=!1,a&&this.add(this._dateTzOffset(),"m")),this},format:function(a){var b=N(this,a||tb.defaultFormat);return this.localeData().postformat(b)},add:s(1,"add"),subtract:s(-1,"subtract"),diff:function(a,b,c){var d,e,f,g=K(a,this),h=6e4*(this.zone()-g.zone());return b=x(b),"year"===b||"month"===b?(d=432e5*(this.daysInMonth()+g.daysInMonth()),e=12*(this.year()-g.year())+(this.month()-g.month()),f=this-tb(this).startOf("month")-(g-tb(g).startOf("month")),f-=6e4*(this.zone()-tb(this).startOf("month").zone()-(g.zone()-tb(g).startOf("month").zone())),e+=f/d,"year"===b&&(e/=12)):(d=this-g,e="second"===b?d/1e3:"minute"===b?d/6e4:"hour"===b?d/36e5:"day"===b?(d-h)/864e5:"week"===b?(d-h)/6048e5:d),c?e:o(e)},from:function(a,b){return tb.duration({to:this,from:a}).locale(this.locale()).humanize(!b)},fromNow:function(a){return this.from(tb(),a)},calendar:function(a){var b=a||tb(),c=K(b,this).startOf("day"),d=this.diff(c,"days",!0),e=-6>d?"sameElse":-1>d?"lastWeek":0>d?"lastDay":1>d?"sameDay":2>d?"nextDay":7>d?"nextWeek":"sameElse";return this.format(this.localeData().calendar(e,this))},isLeapYear:function(){return E(this.year())},isDST:function(){return this.zone()+a):+this.clone().startOf(b)>+tb(a).startOf(b)},isBefore:function(a,b){return b=x("undefined"!=typeof b?b:"millisecond"),"millisecond"===b?(a=tb.isMoment(a)?a:tb(a),+a>+this):+this.clone().startOf(b)<+tb(a).startOf(b)},isSame:function(a,b){return b=x(b||"millisecond"),"millisecond"===b?(a=tb.isMoment(a)?a:tb(a),+this===+a):+this.clone().startOf(b)===+K(a,this).startOf(b)},min:f("moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(a){return a=tb.apply(null,arguments),this>a?this:a}),max:f("moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(a){return a=tb.apply(null,arguments),a>this?this:a}),zone:function(a,b){var c,d=this._offset||0;return null==a?this._isUTC?d:this._dateTzOffset():("string"==typeof a&&(a=Q(a)),Math.abs(a)<16&&(a=60*a),!this._isUTC&&b&&(c=this._dateTzOffset()),this._offset=a,this._isUTC=!0,null!=c&&this.subtract(c,"m"),d!==a&&(!b||this._changeInProgress?t(this,tb.duration(d-a,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,tb.updateOffset(this,!0),this._changeInProgress=null)),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return this._tzm?this.zone(this._tzm):"string"==typeof this._i&&this.zone(this._i),this},hasAlignedHourOffset:function(a){return a=a?tb(a).zone():0,(this.zone()-a)%60===0},daysInMonth:function(){return B(this.year(),this.month())},dayOfYear:function(a){var b=yb((tb(this).startOf("day")-tb(this).startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")},quarter:function(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)},weekYear:function(a){var b=hb(this,this.localeData()._week.dow,this.localeData()._week.doy).year;return null==a?b:this.add(a-b,"y")},isoWeekYear:function(a){var b=hb(this,1,4).year;return null==a?b:this.add(a-b,"y")},week:function(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")},isoWeek:function(a){var b=hb(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")},weekday:function(a){var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")},isoWeekday:function(a){return null==a?this.day()||7:this.day(this.day()%7?a:a-7)},isoWeeksInYear:function(){return C(this.year(),1,4)},weeksInYear:function(){var a=this.localeData()._week;return C(this.year(),a.dow,a.doy)},get:function(a){return a=x(a),this[a]()},set:function(a,b){return a=x(a),"function"==typeof this[a]&&this[a](b),this},locale:function(b){var c;return b===a?this._locale._abbr:(c=tb.localeData(b),null!=c&&(this._locale=c),this)},lang:f("moment().lang() is deprecated. Use moment().localeData() instead.",function(b){return b===a?this.localeData():this.locale(b)}),localeData:function(){return this._locale},_dateTzOffset:function(){return 15*Math.round(this._d.getTimezoneOffset()/15)}}),tb.fn.millisecond=tb.fn.milliseconds=ob("Milliseconds",!1),tb.fn.second=tb.fn.seconds=ob("Seconds",!1),tb.fn.minute=tb.fn.minutes=ob("Minutes",!1),tb.fn.hour=tb.fn.hours=ob("Hours",!0),tb.fn.date=ob("Date",!0),tb.fn.dates=f("dates accessor is deprecated. Use date instead.",ob("Date",!0)),tb.fn.year=ob("FullYear",!0),tb.fn.years=f("years accessor is deprecated. Use year instead.",ob("FullYear",!0)),tb.fn.days=tb.fn.day,tb.fn.months=tb.fn.month,tb.fn.weeks=tb.fn.week,tb.fn.isoWeeks=tb.fn.isoWeek,tb.fn.quarters=tb.fn.quarter,tb.fn.toJSON=tb.fn.toISOString,m(tb.duration.fn=l.prototype,{_bubble:function(){var a,b,c,d=this._milliseconds,e=this._days,f=this._months,g=this._data,h=0;g.milliseconds=d%1e3,a=o(d/1e3),g.seconds=a%60,b=o(a/60),g.minutes=b%60,c=o(b/60),g.hours=c%24,e+=o(c/24),h=o(pb(e)),e-=o(qb(h)),f+=o(e/30),e%=30,h+=o(f/12),f%=12,g.days=e,g.months=f,g.years=h},abs:function(){return this._milliseconds=Math.abs(this._milliseconds),this._days=Math.abs(this._days),this._months=Math.abs(this._months),this._data.milliseconds=Math.abs(this._data.milliseconds),this._data.seconds=Math.abs(this._data.seconds),this._data.minutes=Math.abs(this._data.minutes),this._data.hours=Math.abs(this._data.hours),this._data.months=Math.abs(this._data.months),this._data.years=Math.abs(this._data.years),this},weeks:function(){return o(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*A(this._months/12)},humanize:function(a){var b=gb(this,!a,this.localeData());return a&&(b=this.localeData().pastFuture(+this,b)),this.localeData().postformat(b)},add:function(a,b){var c=tb.duration(a,b);return this._milliseconds+=c._milliseconds,this._days+=c._days,this._months+=c._months,this._bubble(),this},subtract:function(a,b){var c=tb.duration(a,b);return this._milliseconds-=c._milliseconds,this._days-=c._days,this._months-=c._months,this._bubble(),this},get:function(a){return a=x(a),this[a.toLowerCase()+"s"]()},as:function(a){var b,c;if(a=x(a),"month"===a||"year"===a)return b=this._days+this._milliseconds/864e5,c=this._months+12*pb(b),"month"===a?c:c/12;switch(b=this._days+qb(this._months/12),a){case"week":return b/7+this._milliseconds/6048e5;case"day":return b+this._milliseconds/864e5;case"hour":return 24*b+this._milliseconds/36e5;case"minute":return 24*b*60+this._milliseconds/6e4;case"second":return 24*b*60*60+this._milliseconds/1e3;case"millisecond":return Math.floor(24*b*60*60*1e3)+this._milliseconds;default:throw new Error("Unknown unit "+a)}},lang:tb.fn.lang,locale:tb.fn.locale,toIsoString:f("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",function(){return this.toISOString()}),toISOString:function(){var a=Math.abs(this.years()),b=Math.abs(this.months()),c=Math.abs(this.days()),d=Math.abs(this.hours()),e=Math.abs(this.minutes()),f=Math.abs(this.seconds()+this.milliseconds()/1e3);return this.asSeconds()?(this.asSeconds()<0?"-":"")+"P"+(a?a+"Y":"")+(b?b+"M":"")+(c?c+"D":"")+(d||e||f?"T":"")+(d?d+"H":"")+(e?e+"M":"")+(f?f+"S":""):"P0D"},localeData:function(){return this._locale}}),tb.duration.fn.toString=tb.duration.fn.toISOString;for(vb in ic)c(ic,vb)&&rb(vb.toLowerCase());tb.duration.fn.asMilliseconds=function(){return this.as("ms")},tb.duration.fn.asSeconds=function(){return this.as("s")},tb.duration.fn.asMinutes=function(){return this.as("m")},tb.duration.fn.asHours=function(){return this.as("h")},tb.duration.fn.asDays=function(){return this.as("d")},tb.duration.fn.asWeeks=function(){return this.as("weeks")},tb.duration.fn.asMonths=function(){return this.as("M")},tb.duration.fn.asYears=function(){return this.as("y")},tb.locale("en",{ordinal:function(a){var b=a%10,c=1===A(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th"; 7 | return a+c}}),Jb?module.exports=tb:"function"==typeof define&&define.amd?(define("moment",function(a,b,c){return c.config&&c.config()&&c.config().noGlobal===!0&&(xb.moment=ub),tb}),sb(!0)):sb()}).call(this); -------------------------------------------------------------------------------- /assets/sprintf.js: -------------------------------------------------------------------------------- 1 | function sprintf() { 2 | // discuss at: http://phpjs.org/functions/sprintf/ 3 | // original by: Ash Searle (http://hexmen.com/blog/) 4 | // improved by: Michael White (http://getsprink.com) 5 | // improved by: Jack 6 | // improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 7 | // improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 8 | // improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 9 | // improved by: Dj 10 | // improved by: Allidylls 11 | // input by: Paulo Freitas 12 | // input by: Brett Zamir (http://brett-zamir.me) 13 | // example 1: sprintf("%01.2f", 123.1); 14 | // returns 1: 123.10 15 | // example 2: sprintf("[%10s]", 'monkey'); 16 | // returns 2: '[ monkey]' 17 | // example 3: sprintf("[%'#10s]", 'monkey'); 18 | // returns 3: '[####monkey]' 19 | // example 4: sprintf("%d", 123456789012345); 20 | // returns 4: '123456789012345' 21 | // example 5: sprintf('%-03s', 'E'); 22 | // returns 5: 'E00' 23 | 24 | var regex = /%%|%(\d+\$)?([-+\'#0 ]*)(\*\d+\$|\*|\d+)?(\.(\*\d+\$|\*|\d+))?([scboxXuideEfFgG])/g; 25 | var a = arguments; 26 | var i = 0; 27 | var format = a[i++]; 28 | 29 | // pad() 30 | var pad = function (str, len, chr, leftJustify) { 31 | if (!chr) { 32 | chr = ' '; 33 | } 34 | var padding = (str.length >= len) ? '' : new Array(1 + len - str.length >>> 0) 35 | .join(chr); 36 | return leftJustify ? str + padding : padding + str; 37 | }; 38 | 39 | // justify() 40 | var justify = function (value, prefix, leftJustify, minWidth, zeroPad, customPadChar) { 41 | var diff = minWidth - value.length; 42 | if (diff > 0) { 43 | if (leftJustify || !zeroPad) { 44 | value = pad(value, minWidth, customPadChar, leftJustify); 45 | } else { 46 | value = value.slice(0, prefix.length) + pad('', diff, '0', true) + value.slice(prefix.length); 47 | } 48 | } 49 | return value; 50 | }; 51 | 52 | // formatBaseX() 53 | var formatBaseX = function (value, base, prefix, leftJustify, minWidth, precision, zeroPad) { 54 | // Note: casts negative numbers to positive ones 55 | var number = value >>> 0; 56 | prefix = prefix && number && { 57 | '2': '0b', 58 | '8': '0', 59 | '16': '0x' 60 | }[base] || ''; 61 | value = prefix + pad(number.toString(base), precision || 0, '0', false); 62 | return justify(value, prefix, leftJustify, minWidth, zeroPad); 63 | }; 64 | 65 | // formatString() 66 | var formatString = function (value, leftJustify, minWidth, precision, zeroPad, customPadChar) { 67 | if (precision != null) { 68 | value = value.slice(0, precision); 69 | } 70 | return justify(value, '', leftJustify, minWidth, zeroPad, customPadChar); 71 | }; 72 | 73 | // doFormat() 74 | var doFormat = function (substring, valueIndex, flags, minWidth, _, precision, type) { 75 | var number, prefix, method, textTransform, value; 76 | 77 | if (substring === '%%') { 78 | return '%'; 79 | } 80 | 81 | // parse flags 82 | var leftJustify = false; 83 | var positivePrefix = ''; 84 | var zeroPad = false; 85 | var prefixBaseX = false; 86 | var customPadChar = ' '; 87 | var flagsl = flags.length; 88 | for (var j = 0; flags && j < flagsl; j++) { 89 | switch (flags.charAt(j)) { 90 | case ' ': 91 | positivePrefix = ' '; 92 | break; 93 | case '+': 94 | positivePrefix = '+'; 95 | break; 96 | case '-': 97 | leftJustify = true; 98 | break; 99 | case "'": 100 | customPadChar = flags.charAt(j + 1); 101 | break; 102 | case '0': 103 | zeroPad = true; 104 | customPadChar = '0'; 105 | break; 106 | case '#': 107 | prefixBaseX = true; 108 | break; 109 | } 110 | } 111 | 112 | // parameters may be null, undefined, empty-string or real valued 113 | // we want to ignore null, undefined and empty-string values 114 | if (!minWidth) { 115 | minWidth = 0; 116 | } else if (minWidth === '*') { 117 | minWidth = +a[i++]; 118 | } else if (minWidth.charAt(0) == '*') { 119 | minWidth = +a[minWidth.slice(1, -1)]; 120 | } else { 121 | minWidth = +minWidth; 122 | } 123 | 124 | // Note: undocumented perl feature: 125 | if (minWidth < 0) { 126 | minWidth = -minWidth; 127 | leftJustify = true; 128 | } 129 | 130 | if (!isFinite(minWidth)) { 131 | throw new Error('sprintf: (minimum-)width must be finite'); 132 | } 133 | 134 | if (!precision) { 135 | precision = 'fFeE'.indexOf(type) > -1 ? 6 : (type === 'd') ? 0 : undefined; 136 | } else if (precision === '*') { 137 | precision = +a[i++]; 138 | } else if (precision.charAt(0) == '*') { 139 | precision = +a[precision.slice(1, -1)]; 140 | } else { 141 | precision = +precision; 142 | } 143 | 144 | // grab value using valueIndex if required? 145 | value = valueIndex ? a[valueIndex.slice(0, -1)] : a[i++]; 146 | 147 | switch (type) { 148 | case 's': 149 | return formatString(String(value), leftJustify, minWidth, precision, zeroPad, customPadChar); 150 | case 'c': 151 | return formatString(String.fromCharCode(+value), leftJustify, minWidth, precision, zeroPad); 152 | case 'b': 153 | return formatBaseX(value, 2, prefixBaseX, leftJustify, minWidth, precision, zeroPad); 154 | case 'o': 155 | return formatBaseX(value, 8, prefixBaseX, leftJustify, minWidth, precision, zeroPad); 156 | case 'x': 157 | return formatBaseX(value, 16, prefixBaseX, leftJustify, minWidth, precision, zeroPad); 158 | case 'X': 159 | return formatBaseX(value, 16, prefixBaseX, leftJustify, minWidth, precision, zeroPad) 160 | .toUpperCase(); 161 | case 'u': 162 | return formatBaseX(value, 10, prefixBaseX, leftJustify, minWidth, precision, zeroPad); 163 | case 'i': 164 | case 'd': 165 | number = +value || 0; 166 | // Plain Math.round doesn't just truncate 167 | number = Math.round(number - number % 1); 168 | prefix = number < 0 ? '-' : positivePrefix; 169 | value = prefix + pad(String(Math.abs(number)), precision, '0', false); 170 | return justify(value, prefix, leftJustify, minWidth, zeroPad); 171 | case 'e': 172 | case 'E': 173 | case 'f': // Should handle locales (as per setlocale) 174 | case 'F': 175 | case 'g': 176 | case 'G': 177 | number = +value; 178 | prefix = number < 0 ? '-' : positivePrefix; 179 | method = ['toExponential', 'toFixed', 'toPrecision']['efg'.indexOf(type.toLowerCase())]; 180 | textTransform = ['toString', 'toUpperCase']['eEfFgG'.indexOf(type) % 2]; 181 | value = prefix + Math.abs(number)[method](precision); 182 | return justify(value, prefix, leftJustify, minWidth, zeroPad)[textTransform](); 183 | default: 184 | return substring; 185 | } 186 | }; 187 | 188 | return format.replace(regex, doFormat); 189 | } -------------------------------------------------------------------------------- /db-table-editor.php: -------------------------------------------------------------------------------- 1 | nor the 26 | names of its contributors may be used to endorse or promote products 27 | derived from this software without specific prior written permission. 28 | 29 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 30 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 31 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 32 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 33 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 34 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 35 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 36 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 37 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 38 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 39 | 40 | */ 41 | 42 | /* Variables to store the list of configred wp-db-table-editors and 43 | * the currently selected instance 44 | */ 45 | global $DBTE_INSTANCES, $DBTE_CURRENT; 46 | $DBTE_INSTANCES = Array(); 47 | $DBTE_CURRENT = null; 48 | 49 | include('SplClassLoader.php'); 50 | $loader = new SplClassLoader('PHPSQLParser', 'php-sql-parser/src/'); 51 | $loader->register(); 52 | include('DBTableEditor.class.php'); 53 | 54 | 55 | add_action('init','db_table_editor_init'); 56 | function db_table_editor_init(){ 57 | /* TODO: could be used to pull current config from the db if needed */ 58 | // this is mostly not needed, but allows a somewhat clearer 59 | // place to hook this from a plugin 60 | do_action('db_table_editor_init'); 61 | } 62 | 63 | function wp_db_load_plugin_textdomain() { 64 | load_plugin_textdomain( 'wp-db-table-editor', FALSE, basename( dirname( __FILE__ ) ) . '/languages/' ); 65 | } 66 | add_action( 'plugins_loaded', 'wp_db_load_plugin_textdomain' ); 67 | 68 | /* 69 | * Gets the DBTE_DataTable of the current DBTE instance 70 | */ 71 | function dbte_get_data_table(){ 72 | global $wpdb; 73 | $cur = dbte_current(); 74 | if(!$cur){ return "null"; } 75 | $data = $cur->getData(array('type'=>ARRAY_N)); 76 | if(!is_a($data, "DBTE_DataTable")) echo __('DB-Table-Editor Cannot READ DATA SOURCE', 'wp-db-table-editor'); 77 | $rows = $data->rows; 78 | $columns = $data->columns; 79 | return Array('columns'=>$columns, 'rows'=>$rows); 80 | } 81 | 82 | /* 83 | * Enqueue all scripts and styles need to render this plugin 84 | * 85 | * we will also doaction db-table-editor_enqueue_scripts 86 | * so that users can enqueue their own files 87 | * 88 | * we will also enqueue the current DBTE instance's jsFile 89 | * which should be the name of a registered script 90 | */ 91 | function dbte_scripts($hook){ 92 | global $DBTE_INSTANCES, $DBTE_CURRENT; 93 | $tbl = preg_replace('/^.*_page_dbte_/', '', $hook); 94 | $cur = dbte_current($tbl); 95 | if(!$cur) return; 96 | $base = plugins_url('wp-db-table-editor'); 97 | 98 | wp_enqueue_style('slick-grid-css', 99 | $base.'/assets/SlickGrid/slick.grid.css'); 100 | wp_enqueue_style('slick-grid-jquery-ui', 101 | $base.'/assets/SlickGrid/css/smoothness/jquery-ui-1.8.16.custom.css'); 102 | wp_enqueue_style('db-table-editor-css', 103 | $base.'/assets/db-table-editor.css'); 104 | 105 | wp_enqueue_script('jquery-event-drag', 106 | $base.'/assets/jquery.event.drag.js', 107 | array('jquery')); 108 | wp_enqueue_script('jquery-event-drop', 109 | $base.'/assets/jquery.event.drop.js', 110 | array('jquery-event-drag')); 111 | wp_enqueue_script('slick-core-js', 112 | $base.'/assets/SlickGrid/slick.core.js', 113 | array('jquery-event-drop','jquery-event-drag', 'jquery-ui-sortable')); 114 | 115 | 116 | wp_enqueue_script('slick-grid-cellrangedecorator', 117 | $base.'/assets/SlickGrid/plugins/slick.cellrangedecorator.js', 118 | array('slick-core-js')); 119 | wp_enqueue_script('slick-grid-cellrangeselector', 120 | $base.'/assets/SlickGrid/plugins/slick.cellrangeselector.js', 121 | array('slick-core-js')); 122 | wp_enqueue_script('slick-grid-cellselectionmodel', 123 | $base.'/assets/SlickGrid/plugins/slick.cellselectionmodel.js', 124 | array('slick-core-js')); 125 | wp_enqueue_script('slick-grid-formatters', 126 | $base.'/assets/SlickGrid/slick.formatters.js', 127 | array('slick-core-js')); 128 | wp_enqueue_script('slick-grid-editors', 129 | $base.'/assets/SlickGrid/slick.editors.js', 130 | array('slick-core-js', 'jquery-ui-datepicker')); 131 | 132 | wp_enqueue_style('dbte-jquery-ui-css', 133 | '//ajax.googleapis.com/ajax/libs/jqueryui/1.8.21/themes/smoothness/jquery-ui.css'); 134 | 135 | wp_enqueue_script('slick-dataview', 136 | $base.'/assets/SlickGrid/slick.dataview.js', 137 | array('slick-core-js')); 138 | 139 | wp_enqueue_script('slick-grid-js', 140 | $base.'/assets/SlickGrid/slick.grid.js', 141 | array('slick-core-js', 'slick-grid-cellrangedecorator', 142 | 'slick-grid-cellrangeselector', 'slick-grid-cellselectionmodel', 143 | 'slick-grid-formatters', 'slick-grid-editors', 'slick-dataview')); 144 | 145 | wp_enqueue_script('moment-js', 146 | $base.'/assets/moment.js', 147 | array()); 148 | wp_enqueue_script('dbte-date-editor-js', 149 | $base.'/assets/dbte-date-editor.js', 150 | array('slick-grid-js', 'moment-js', 'jquery', 'jquery-ui-datepicker')); 151 | 152 | wp_enqueue_script('sprintf-js', $base.'/assets/sprintf.js'); 153 | 154 | wp_register_script('db-table-editor-js', 155 | $base.'/assets/db-table-editor.js', 156 | array('slick-grid-js', 'jquery', 'json2', 'moment-js', 'dbte-date-editor-js', 'sprintf-js')); 157 | $translation_array = array( 158 | 'row_count' => __( 'Showing %d of %d rows - items with unsaved changes are not filtered', 'wp-db-table-editor' ), 159 | 'confirm_delete_row' => __( 'Are you sure you wish to remove this row', 'wp-db-table-editor' ), 160 | 'delete_button' => __( 'Delete this Row', 'wp-db-table-editor' ) 161 | ); 162 | wp_localize_script( 'db-table-editor-js', 'translations', $translation_array ); 163 | wp_enqueue_script('db-table-editor-js'); 164 | 165 | do_action('db-table-editor_enqueue_scripts'); 166 | if($cur->jsFile) wp_enqueue_script($cur->jsFile); 167 | } 168 | add_action('admin_enqueue_scripts','dbte_scripts'); 169 | 170 | /* 171 | * Looks up and sets the current table instance (by id) 172 | */ 173 | function dbte_current($tbl=null){ 174 | global $DBTE_CURRENT, $DBTE_INSTANCES; 175 | $cur = $DBTE_CURRENT; 176 | if($cur && ($cur->id == $tbl || !$tbl)) return $cur; 177 | if($tbl){ 178 | foreach($DBTE_INSTANCES as $o){ 179 | if($tbl == $o->id) $cur = $DBTE_CURRENT = $o; 180 | } 181 | if(!$cur && function_exists('dbte_create_'.$tbl)){ 182 | $cur = $DBTE_CURRENT = call_user_func('dbte_create_'.$tbl); 183 | } 184 | } 185 | return $cur; 186 | } 187 | 188 | function dbte_shortcode($args){ 189 | $id = @$args["id"]; 190 | $o=""; 191 | if(!is_admin()){ 192 | $url = admin_url('admin-ajax.php'); 193 | $o.=''; 194 | } 195 | if($id){ 196 | dbte_scripts($id); 197 | $o .= dbte_render($id); 198 | } 199 | return $o; 200 | } 201 | add_shortcode('dbte','dbte_shortcode'); 202 | 203 | 204 | 205 | function echo_dbte_render(){ 206 | echo dbte_render(); 207 | } 208 | /* 209 | * Renders the DBTE page for the current instance 210 | */ 211 | function dbte_render($id=null){ 212 | $cur = dbte_current($id); 213 | if(!$cur){ 214 | return __('No Database Table Configured to Edit', 'wp-db-table-editor'); 215 | } 216 | $base = plugins_url('wp-db-table-editor'); 217 | $noedit = $cur->noedit; 218 | $pendingSaveCnt = ""; 219 | $pendingSaveHeader = ""; 220 | $buttons=""; 221 | if($cur->editcap && !current_user_can($cur->editcap)){ 222 | $noedit = true; 223 | $cur->noedit = true; 224 | } 225 | if( !$noedit ){ 226 | $pendingSaveCnt = '0'; 227 | $pendingSaveHeader = '
'.sprintf(__('There are %s unsaved changes', 'wp-db-table-editor'), $pendingSaveCnt).'
'; 228 | $saveButtonLabel = sprintf(__('Save %s Changes', 'wp-db-table-editor'), $pendingSaveCnt); 229 | $newButtonLabel = __('New', 'wp-db-table-editor'); 230 | $undoButtonLabel = __('Undo', 'wp-db-table-editor'); 231 | $buttons = <<$saveButtonLabel 233 | 234 | 235 | EOT; 236 | } 237 | $dataUrl = null; 238 | if($cur->async_data){ 239 | $dataUrl = admin_url( 'admin-ajax.php')."?".$_SERVER["QUERY_STRING"] 240 | .'&action=dbte_data&table='.$cur->id; 241 | } 242 | 243 | $args = Array( 244 | "baseUrl"=>$base, 245 | "data" => $cur->async_data ? null : dbte_get_data_table(), 246 | "dataUrl" => $dataUrl 247 | ); 248 | // copy all DBTE slots to the json array 249 | foreach($cur as $k => $v) { $args[$k] = $v; } 250 | unset($args['sql']); 251 | 252 | $json = json_encode($args); 253 | $loadingLabel = __('Loading data...', 'wp-db-table-editor'); 254 | $exportButtonLabel = __('Export to CSV', 'wp-db-table-editor'); 255 | $clearFiltersButtonLabel = __('Clear Filters', 'wp-db-table-editor'); 256 | $rowCountLabel = __('Showing 0 of 0 rows', 'wp-db-table-editor'); 257 | $confirmationMessage = __('You have unsaved data, are you sure you want to quit', 'wp-db-table-editor'); 258 | $o = << 260 |

$cur->title

261 | $pendingSaveHeader 262 | 263 | 267 | 268 |
269 | 270 | 273 | $buttons 274 |
275 |
$rowCountLabel
276 |
277 | 290 | 291 | EOT; 292 | return $o; 293 | } 294 | 295 | /* 296 | * Creates all the menu items based on each of the $DBTE_INSTANCES 297 | * puts them in a DB Table Editor menu on admin 298 | */ 299 | function dbte_menu(){ 300 | global $DBTE_INSTANCES; 301 | $ico = plugins_url('wp-db-table-editor/assets/images/database_edit.png'); 302 | add_menu_page(__('DB Table Editor', 'wp-db-table-editor'), 303 | __('DB Table Editor', 'wp-db-table-editor'), 304 | 'read', 'wp-db-table-editor', 305 | 'dbte_main_page', $ico, 50); 306 | 307 | $displayed = 0; 308 | foreach($DBTE_INSTANCES as $o){ 309 | $cap = $o->cap; 310 | // shouldnt be null, but lets be defensive 311 | if(!$cap) $cap = 'edit_others_posts'; 312 | if(current_user_can($cap)) $displayed++; 313 | add_submenu_page('wp-db-table-editor', $o->title, $o->title, $cap, 'dbte_'.$o->id, 'echo_dbte_render' ); 314 | } 315 | if(!$displayed){ 316 | remove_menu_page('wp-db-table-editor'); 317 | } 318 | 319 | } 320 | add_action('admin_menu', 'dbte_menu'); 321 | 322 | /* 323 | * A page for the main menu. Currently just has links to each interface 324 | * and a bit of explanitory text 325 | */ 326 | function dbte_main_page(){ 327 | global $DBTE_INSTANCES; 328 | $pluginDescription = sprintf(__('This plugin allows viewing, editing, and exporting of database tables in your wordpress database through the admin interface. See the %sREADME.md%s for information on configuring this plugin.', 'wp-db-table-editor'), 329 | '', 330 | ''); 331 | $configuredDBTablesTitle = __('Configured Database Tables', 'wp-db-table-editor'); 332 | echo <<DB Table Editor 334 |

335 | Github - DB Table Editor

336 |

337 | $pluginDescription 338 |

339 |

$configuredDBTablesTitle

340 |
    341 | 342 | EOT; 343 | foreach($DBTE_INSTANCES as $o){ 344 | $cap = $o->cap; 345 | // shouldnt be null, but lets be defensive 346 | if(!$cap) $cap = 'edit_others_posts'; 347 | if(current_user_can($cap)) 348 | echo "
  • id\">$o->title
  • "; 349 | } 350 | echo "
"; 351 | } 352 | 353 | add_action( 'wp_ajax_dbte_data', 'dbte_get_data' ); 354 | add_action( 'wp_ajax_no_priv_dbte_data', 'dbte_get_data' ); 355 | function dbte_get_data(){ 356 | $tbl= $_REQUEST['table']; 357 | $cur = dbte_current($tbl); 358 | if(!$cur) return; 359 | $cap = $cur->cap; 360 | // shouldnt be null, but lets be defensive 361 | if(!$cap) $cap = 'edit_others_posts'; 362 | if(!current_user_can($cap)){ 363 | header('HTTP/1.0 403 Forbidden'); 364 | echo 'You are forbidden!'; 365 | die(); 366 | } 367 | $data = dbte_get_data_table(); 368 | header('Content-type: application/json'); 369 | echo json_encode($data); 370 | die(); 371 | } 372 | 373 | /* 374 | * Ajax Save Handler, called with json rows/columns data 375 | */ 376 | add_action( 'wp_ajax_dbte_save', 'dbte_save_cb' ); 377 | function dbte_save_cb() { 378 | global $wpdb; // this is how you get access to the database 379 | $d = $_REQUEST['data']; 380 | $tbl= $_REQUEST['table']; 381 | $cur = dbte_current($tbl); 382 | if(!$cur) return; 383 | if($cur->noedit || ($cur->editcap && !current_user_can($cur->editcap))) return; 384 | // not sure why teh stripslashes is required, but it wont decode without it 385 | $d = json_decode(htmlspecialchars_decode(stripslashes($d)), true); 386 | 387 | 388 | //var_dump($d);die(); 389 | $cols = @$d["columns"]; 390 | $rows = @$d["rows"]; 391 | $idxs = @$d["modifiedIdxs"]; 392 | $len = count($cols); 393 | $id_col = $cur->id_column; 394 | $no_edit_cols = $cur->noedit_columns; 395 | if(is_string($no_edit_cols)) $no_edit_cols=explode(',',$no_edit_cols); 396 | 397 | $idIdx = 0; 398 | $i=0; 399 | foreach($cols as $c){ 400 | if($c==$id_col) $idIdx = $i; 401 | $i++; 402 | } 403 | 404 | $i=0; 405 | $ridx = 0; 406 | $new_ids = Array(); 407 | foreach($rows as $r){ 408 | $id=@$r[$idIdx]; 409 | $up = array(); 410 | for($i=0 ; $i < $len ; $i++) { 411 | if($i != $idIdx && array_key_exists($i, $r)){ 412 | $v = $r[$i]; 413 | $up[$cols[$i]] = $v; 414 | } 415 | } 416 | if($no_edit_cols){ 417 | foreach($no_edit_cols as $noedit){ 418 | unset($up[$noedit]); 419 | unset($up[trim($noedit)]); 420 | } 421 | } 422 | 423 | $ERR=false; 424 | if($cur->save_cb){ 425 | $isinsert = $id===null; 426 | $data = Array('table'=>$cur, 427 | 'update'=>$up, 428 | 'columns'=>$cols, 429 | 'indexes'=>$idxs[$ridx], 430 | 'id'=>$id, 431 | 'isinsert'=>$isinsert); 432 | call_user_func_array($cur->save_cb, array(&$data)); 433 | if($isinsert && $data['id']){ 434 | $ids= Array('rowId'=>@$r["id"], 'dbid'=>$wpdb->insert_id); 435 | if(!@$ids['rowId']) $ids['rowId'] = @$r["rowId"]; 436 | $new_ids[] = $ids; 437 | } 438 | do_action('dbte_row_saved', array(&$data)); 439 | } 440 | else if($id != null){ 441 | if($cur->update_cb){ 442 | call_user_func($cur->update_cb,$cur, $up, $cols, $idxs[$ridx], $id); 443 | } 444 | else{ 445 | $where = array($id_col=>$id); 446 | $res = $wpdb->update($cur->table, $up , $where); 447 | if ($res === false){ 448 | error_log("Failed to update: no_edit:".print_r( $no_edit_cols, true)); 449 | error_log(print_r($up, true)); 450 | error_log($wpdb->last_error); 451 | echo "
Update Failed - check your log
"; 452 | $ERR = true; 453 | $new_ids['ERROR'] = "Error on update"; 454 | } 455 | 456 | } 457 | do_action('dbte_row_updated', $cur, $up, $cols, $idxs, $id); 458 | } 459 | else{ 460 | if($cur->insert_cb){ 461 | call_user_func($cur->insert_cb,$cur, $up, $cols, $idxs[$ridx]); 462 | } 463 | else{ 464 | $res = $wpdb->insert($cur->table, $up); 465 | if ($res === false){ 466 | error_log("Failed to insert: no_edit:".print_r( $no_edit_cols, true)); 467 | error_log(print_r($up, true)); 468 | error_log($wpdb->last_error); 469 | echo "
Insert Failed - check your log
"; 470 | $ERR = true; 471 | $new_ids['ERROR'] = "Error on insert"; 472 | } 473 | $ids= Array('rowId'=>@$r["id"], 'dbid'=>$wpdb->insert_id); 474 | if(!@$ids['rowId']) $ids['rowId'] = @$r["rowId"]; 475 | $new_ids[] = $ids; 476 | } 477 | do_action('dbte_row_inserted', $cur, $up, $cols, $idxs); 478 | } 479 | $ridx++; 480 | } 481 | header('Content-type: application/json'); 482 | echo json_encode($new_ids); 483 | die(); 484 | } 485 | /* 486 | * Ajax Delete Handler - called from delete buttons on each row of the clickgrid 487 | */ 488 | function dbte_delete_cb(){ 489 | global $wpdb; 490 | $id = $_REQUEST['dataid']; 491 | $tbl= $_REQUEST['table']; 492 | $cur = dbte_current($tbl); 493 | if(!$cur) return; 494 | if($cur->noedit || ($cur->editcap && !current_user_can($cur->editcap))) return; 495 | $id_col = $cur->id_column; 496 | if($cur->delete_cb){ 497 | call_user_func($cur->delete_cb,$cur,$id); 498 | } 499 | else{ 500 | $wpdb->delete($cur->table, array($id_col=>$id)); 501 | } 502 | do_action('dbte_row_deleted', $cur, $id); 503 | header('Content-type: application/json'); 504 | echo json_encode(Array('deleted',$id)); 505 | die(); 506 | } 507 | add_action( 'wp_ajax_dbte_delete', 'dbte_delete_cb' ); 508 | 509 | 510 | function dbte_is_date($ds){ 511 | $d = date_parse($ds); 512 | if($d && !@$d['errors']) return $d; 513 | return null; 514 | } 515 | 516 | // Here is an adaption of the above code that adds support for double 517 | // quotes inside a field. (One double quote is replaced with a pair of 518 | // double quotes per the CSV format). - http://php.net/manual/en/function.fputcsv.php 519 | function x8_fputcsv($filePointer,$dataArray,$delimiter,$enclosure){ 520 | // Write a line to a file 521 | // $filePointer = the file resource to write to 522 | // $dataArray = the data to write out 523 | // $delimeter = the field separator 524 | 525 | // Build the string 526 | $string = ""; 527 | 528 | // No leading delimiter 529 | $writeDelimiter = FALSE; 530 | foreach($dataArray as $dataElement) { 531 | // Replaces a double quote with two double quotes 532 | $dataElement=str_replace($enclosure, $enclosure.$enclosure , $dataElement); 533 | if($writeDelimiter) $string .= $delimiter; 534 | $string .= $enclosure . $dataElement . $enclosure; 535 | $writeDelimiter = TRUE; 536 | } 537 | 538 | $string .= "\r\n"; 539 | // Write the string to the file 540 | fwrite($filePointer,$string); 541 | } 542 | 543 | /* 544 | * Written as an ajax handler because that was easy, but we usually just link to here 545 | * will export filtered results using any filter-{columnname} request parameters provided 546 | */ 547 | function dbte_export_csv(){ 548 | global $wpdb; 549 | $cur = dbte_current(@$_REQUEST['table']); 550 | if(!$cur) return; 551 | // if($cur->editcap && !current_user_can($cur->editcap)) return; 552 | $id_col = $cur->id_column; 553 | $ids = @$_REQUEST['ids']; 554 | $tbl = $cur->table; 555 | $exidf = $cur->export_id_field; 556 | if(!$exidf) $exidf = "$tbl.$id_col"; 557 | $wheres = Array(); 558 | if($ids){ 559 | $ids = explode(',', $ids); 560 | $ids[]=-1; 561 | // ensures that our sql is ok / escaped 562 | foreach($ids as $idx=>$id){$ids[$idx] = intval($id);} 563 | $ids = implode(', ', $ids); 564 | 565 | $wheres[] = " $exidf in ( $ids ) "; 566 | } 567 | $tbl = $cur->table; 568 | $title = $cur->title; 569 | $data = $cur->getData(array("where"=>$wheres)); 570 | $columns = $data->columnNames; 571 | $rows = $data->rows; 572 | header('Content-Type: application/excel'); 573 | header('Content-Disposition: attachment; filename="'.$title.'.csv"'); 574 | $fp = fopen('php://output', 'w'); 575 | x8_fputcsv($fp, $columns, ',', '"'); 576 | foreach ( $rows as $row ){ 577 | x8_fputcsv($fp, $row, ',', '"'); 578 | } 579 | fclose($fp); 580 | die(); 581 | } 582 | add_action( 'wp_ajax_dbte_export_csv', 'dbte_export_csv' ); 583 | -------------------------------------------------------------------------------- /examples/cf7dbsubmit_integration.php: -------------------------------------------------------------------------------- 1 | prefix}cf7dbplugin_submits 26 | WHERE form_name ='$formname' 27 | ORDER BY field_order ASC 28 | EOT; 29 | $fields = $wpdb->get_col($sql); 30 | $selects = array(); 31 | $joins = array(); 32 | 33 | // Do many self joins to the same table to pull up each field as a result column 34 | foreach($fields as $f){ 35 | $selects[] = " `tbl_$f`.field_value `$f` "; 36 | $joins[] = <<prefix}cf7dbplugin_submits 40 | WHERE form_name='$formname' AND field_name='$f' 41 | GROUP BY submit_time, form_name, field_name 42 | ) as `tbl_$f` ON `tbl_$f`.submit_time = submits.submit_time 43 | EOT; 44 | 45 | } 46 | 47 | // Build the final SQL that joins to our table for each field on the 48 | // contact form 49 | $selects = implode(", ", $selects); 50 | if($selects) $selects .= ", "; 51 | $joins = implode("\n", $joins); 52 | $sql = <<prefix}cf7dbplugin_submits 61 | WHERE form_name ='$formname' 62 | ) as tbl 63 | ) as submits 64 | $joins 65 | WHERE submits.submit_time IS NOT NULL 66 | $wheres 67 | ORDER BY submits.submit_time DESC 68 | $limit 69 | 70 | EOT; 71 | // echo $sql; die(); 72 | return $sql; 73 | } 74 | 75 | 76 | add_action( "wp_loaded", 'xxx_add_tables', -10 ); 77 | 78 | function xxx_add_tables() { 79 | global $wpdb; 80 | if(function_exists('add_db_table_editor')){ 81 | $base = array( 82 | 'table'=>$wpdb->prefix.'cf7dbplugin_submits', 83 | 'save_cb'=>'xxx_contacts_save', 84 | 'delete_cb'=>'xxx_contacts_delete', 85 | 'hide_columns'=>"id", 86 | 'cap'=>"edit_others_posts", 87 | 'noedit_columns'=>'Submitted Login,Submitted From,Submit Time'); 88 | 89 | // Configure the db-table-editor plugin for displaying the results of a single 90 | // contact form 91 | add_db_table_editor(array_merge(array( 92 | 'id'=>'MoreInfoRequests', 93 | 'title'=>'More Info Requsts', 94 | 'sql' => xxx_contacts_sql('MoreInfoRequests')), 95 | $base)); 96 | 97 | // If you to show all forms, replace above code with this: 98 | /*$fields = xxx_get_form_names(); 99 | foreach($fields as $f){ 100 | add_db_table_editor(array_merge(array( 101 | 'id'=> $f, 102 | 'title'=>$f, 103 | 'sql' => xxx_contacts_sql($f)), 104 | $base)); 105 | }*/ 106 | } 107 | } 108 | 109 | // When inserting a new row, we need to convert it from a row 110 | // into a more "hashtable" style database schema (that is, one 111 | // row in the db for each "column" in our incoming dataset 112 | function xxx_contacts_save($args){ 113 | global $wpdb; 114 | $dbte = $args['table']; 115 | $columns = $args['columns']; 116 | $vals = $args['update']; 117 | $idxs = $args['indexes']; 118 | $id = $dbte->id; 119 | 120 | $isinsert = $args['id'] === null; 121 | $subtime = @$vals["submit_time"]; 122 | unset($vals["submit_time"]); 123 | unset($vals["Submit Time"]); 124 | if($isinsert) $subtime = function_exists('microtime') ? microtime(true) : time(); 125 | 126 | foreach($vals as $k => $v){ 127 | if($isinsert) 128 | $wpdb->insert($wpdb->prefix.'cf7dbplugin_submits', 129 | array('field_value'=>$v, 'field_name'=>$k, 130 | 'form_name'=>$id, 'submit_time'=>$subtime, 131 | 'field_order'=>array_search($k, $columns) 132 | )); 133 | 134 | // our column was not edited continue 135 | if(!$isinsert && !in_array(array_search($k, $columns),$idxs)) continue; 136 | $wpdb->update($wpdb->prefix.'cf7dbplugin_submits', 137 | array('field_value'=>$v), 138 | array('form_name'=>$id, 'submit_time'=>$subtime, 139 | 'field_name'=>$k)); 140 | } 141 | } 142 | 143 | // Delete all the database rows for this submission 144 | function xxx_contacts_delete($dbte, $id){ 145 | global $wpdb; 146 | $id = $dbte->id; 147 | $subtime = @$_REQUEST["submit_time"]; 148 | $wpdb->delete($wpdb->prefix.'cf7dbplugin_submits', array('form_name'=>$id, 'submit_time'=>$subtime)); 149 | } 150 | 151 | // Find all the form names from cf7dbplugin_submits table 152 | function xxx_get_form_names(){ 153 | global $wpdb; 154 | 155 | $sql=<<prefix}cf7dbplugin_submits 159 | EOT; 160 | $fields = $wpdb->get_col($sql); 161 | return $fields; 162 | } 163 | -------------------------------------------------------------------------------- /examples/custom-buttons-and-js.php: -------------------------------------------------------------------------------- 1 | 'Reports', 10 | 'table'=>'reports', 11 | 'no_edit'=>true, 12 | 'auto_date'=>false, 13 | )); 14 | add_db_table_editor(array( 15 | 'title'=>'Members', 16 | 'table'=>'memberdata', 17 | 'sql'=>'SELECT * FROM memberdata ORDER BY ID desc', 18 | 'auto_date'=>true, 19 | )); 20 | function xxx_register_scripts_admin (){ 21 | $base_url = get_stylesheet_directory_uri(); 22 | wp_enqueue_script( 23 | 'custom-buttons.js', $base_url . '/custom-buttons.js', Array('jquery')); 24 | } 25 | 26 | add_action('admin_enqueue_scripts','xxx_register_scripts_admin'); 27 | } 28 | -------------------------------------------------------------------------------- /examples/custom-buttons.js: -------------------------------------------------------------------------------- 1 | if(typeof(NS) == 'undefined') NS={}; 2 | (function(){ "use strict"; 3 | var $ = jQuery; 4 | 5 | NS._doUpdateWidths = function(){ 6 | // not sure why this was required, but I was getting infinite loops without its, so whatever 7 | if(NS._widthUpdateDone) return; 8 | NS._widthUpdateDone = true; 9 | NS._widthTimeout = null; 10 | var cs = DBTableEditor.grid.getColumns(); 11 | cs[0].width = 175; 12 | DBTableEditor.grid.setColumns(cs); 13 | }; 14 | 15 | NS._updateWidthsNext = function(){ 16 | if(NS._widthTimeout) window.clearTimeout(NS._widthTimeout); 17 | NS._widthTimeout = window.setTimeout(NS._doUpdateWidths, 100); 18 | }; 19 | 20 | DBTableEditor.extraButtons.push( function(row, cell, value, col, dataContext){ 21 | var id = dataContext[DBTableEditor.columnMap[DBTableEditor.id_column]]; 22 | var rowid = dataContext.id; // uses id, NOT id_column 23 | var url = '/wp-content/themes/mytheme/arrow_turn_left.png'; 24 | var dbteurl = window.location.toString(); 25 | var out =""; 26 | if(dbteurl.search('reports')>=0){ 27 | out += '' 29 | +' Load' 30 | +''; 31 | }else if(dbteurl.search('memberdata')>=0){ 32 | out += '' 34 | +' Load' 35 | +''; 36 | } 37 | // not sure why this was required, but I was getting infinite loops without its, so whatever 38 | if(!NS._widthUpdateDone) NS._updateWidthsNext(); 39 | return out; 40 | }); 41 | 42 | })(); 43 | -------------------------------------------------------------------------------- /examples/paging.php: -------------------------------------------------------------------------------- 1 | 1900 && $yin < 3000) $year = $yin; 19 | add_db_table_editor(array( 20 | 'id' => 'payments', 21 | 'title'=>'Payments for '.$Year, 22 | 'table'=>'payments', 23 | // !!! DONT SQL INJECT !!! This is guaranteed to be a valid integer 24 | // use wpdb->prepare if you are not sure !!! 25 | 'sql'=>'SELECT * FROM payments'. 26 | ' WHERE YEAR(date_entered)='.$year. 27 | ' ORDER BY ID date_entered DESC', 28 | )); 29 | 30 | add_action('admin_enqueue_scripts','xxx_register_scripts_admin'); 31 | function xxx_register_scripts_admin (){ 32 | if(@$_REQUEST['page'] == 'dbte_payments'){ // matches 'id' or 'table' above 33 | $base_url = get_stylesheet_directory_uri(); 34 | wp_enqueue_script( 35 | 'payment-paging.js', $base_url . '/payment-paging.js', Array('jquery')); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/payment-paging.js: -------------------------------------------------------------------------------- 1 | if(typeof(NS) == 'undefined') NS={}; 2 | (function(){ 3 | "use strict"; 4 | NS._plus = new RegExp('\\+','g'); 5 | NS.parseQuery = function(query) { 6 | if(!query) query = window.location.search.substring(1); 7 | var obj = {}; 8 | var vars = query.split('&'); 9 | for (var i = 0; i < vars.length; i++) { 10 | var pair = vars[i].split('='); 11 | var k = decodeURIComponent(pair[0]).replace(NS._plus, " "); 12 | var v = decodeURIComponent(pair[1]).replace(NS._plus, " "); 13 | if(obj[k]){ 14 | if (angular.isArray(obj[k])) obj[k].push(v); 15 | else obj[k]= [obj[k],v]; 16 | } 17 | else obj[k]=v; 18 | } 19 | return obj; 20 | }; 21 | NS.encodeQuery =function(q) { 22 | var a =[]; 23 | if(!q)q=NS.QUERY; 24 | $.each(q, function(k, v) { 25 | console.log(k, v); 26 | a.push(encodeURIComponent(k)+"="+encodeURIComponent(v)); 27 | }); 28 | return '?'+a.join('&'); 29 | }; 30 | 31 | NS.QUERY = NS.parseQuery(); 32 | var $ = jQuery; 33 | NS.currentYear = (new Date().getFullYear()); 34 | NS.year = NS.QUERY['year'] || NS.currentYear; 35 | NS.year = Number(NS.year); 36 | NS.nextYear =function(){ 37 | NS.QUERY['year']=Number(NS.year)+1; 38 | window.location = window.location.toString().replace(/\?.*/,NS.encodeQuery()); 39 | }; 40 | NS.lastYear =function(){ 41 | NS.QUERY['year']=NS.year-1; 42 | window.location = window.location.toString().replace(/\?.*/,NS.encodeQuery()); 43 | }; 44 | NS.init = function(){ 45 | var btns = $('.db-table-editor-buttons'); 46 | var btnPrev = $('').click(NS.lastYear); 47 | var btnNext = ( NS.year < NS.currentYear ) ? 48 | $('').click(NS.nextYear) : null; 49 | console.log('Adding buttons', btnPrev, btnNext, btns); 50 | btns.append([ '
', btnPrev, btnNext]); 51 | }; 52 | 53 | //NS.init(); 54 | jQuery(NS.init); 55 | })(); 56 | -------------------------------------------------------------------------------- /languages/wp-db-table-editor-en_US.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccelerationNet/wp-db-table-editor/775a84db32f4ef1b299b8542836c04e099f92966/languages/wp-db-table-editor-en_US.mo -------------------------------------------------------------------------------- /languages/wp-db-table-editor-en_US.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: DB-table-editor\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: Thu Jun 18 2015 10:48:46 GMT+0200 (CEST)\n" 6 | "PO-Revision-Date: Thu Jun 18 2015 16:21:44 GMT+0200 (CEST)\n" 7 | "Last-Translator: admin_fi \n" 8 | "Language-Team: \n" 9 | "Language: English\n" 10 | "Plural-Forms: nplurals=2; plural=n != 1\n" 11 | "MIME-Version: 1.0\n" 12 | "Content-Type: text/plain; charset=UTF-8\n" 13 | "Content-Transfer-Encoding: 8bit\n" 14 | "X-Poedit-SourceCharset: UTF-8\n" 15 | "X-Poedit-Basepath: .\n" 16 | "X-Poedit-SearchPath-0: ..\n" 17 | "X-Poedit-KeywordsList: _:1;gettext:1;dgettext:2;ngettext:1,2;dngettext:2,3;" 18 | "__:1;_e:1;_c:1;_n:1,2;_n_noop:1,2;_nc:1,2;__ngettext:1,2;__ngettext_noop:1,2;" 19 | "_x:1,2c;_ex:1,2c;_nx:1,2,4c;_nx_noop:1,2,3c;_n_js:1,2;_nx_js:1,2,3c;" 20 | "esc_attr__:1;esc_html__:1;esc_attr_e:1;esc_html_e:1;esc_attr_x:1,2c;" 21 | "esc_html_x:1,2c;comments_number_link:2,3;t:1;st:1;trans:1;transChoice:1,2\n" 22 | "X-Generator: Loco - https://localise.biz/\n" 23 | "X-Loco-Target-Locale: en_US" 24 | 25 | #: ../db-table-editor.php:75 26 | msgid "DB-Table-Editor Cannot READ DATA SOURCE" 27 | msgstr "" 28 | 29 | #: ../db-table-editor.php:157 30 | #, php-format 31 | msgid "Showing %d of %d rows - items with unsaved changes are not filtered" 32 | msgstr "" 33 | 34 | #: ../db-table-editor.php:158 35 | msgid "Are you sure you wish to remove this row" 36 | msgstr "" 37 | 38 | #: ../db-table-editor.php:159 39 | msgid "Delete this Row" 40 | msgstr "" 41 | 42 | #: ../db-table-editor.php:213 43 | msgid "No Database Table Configured to Edit" 44 | msgstr "" 45 | 46 | #: ../db-table-editor.php:225 47 | #, php-format 48 | msgid "There are %s unsaved changes" 49 | msgstr "" 50 | 51 | #: ../db-table-editor.php:226 52 | #, php-format 53 | msgid "Save %s Changes" 54 | msgstr "" 55 | 56 | #: ../db-table-editor.php:227 57 | msgid "New" 58 | msgstr "" 59 | 60 | #: ../db-table-editor.php:228 61 | msgid "Undo" 62 | msgstr "" 63 | 64 | #: ../db-table-editor.php:243 65 | msgid "Export to CSV" 66 | msgstr "" 67 | 68 | #: ../db-table-editor.php:244 69 | msgid "Clear Filters" 70 | msgstr "" 71 | 72 | #: ../db-table-editor.php:245 73 | msgid "Showing 0 of 0 rows" 74 | msgstr "" 75 | 76 | #: ../db-table-editor.php:246 77 | msgid "You have unsaved data, are you sure you want to quit" 78 | msgstr "" 79 | 80 | #: ../db-table-editor.php:309 81 | #, php-format 82 | msgid "" 83 | "This plugin allows viewing, editing, and exporting of database tables in " 84 | "your wordpress database through the admin interface. See the %sREADME.md%s " 85 | "for information on configuring this plugin." 86 | msgstr "" 87 | 88 | #: ../db-table-editor.php:312 89 | msgid "Configured Database Tables" 90 | msgstr "" 91 | -------------------------------------------------------------------------------- /languages/wp-db-table-editor-fr_FR.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccelerationNet/wp-db-table-editor/775a84db32f4ef1b299b8542836c04e099f92966/languages/wp-db-table-editor-fr_FR.mo -------------------------------------------------------------------------------- /languages/wp-db-table-editor-fr_FR.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: DB-table-editor\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: Thu Jun 18 2015 10:48:46 GMT+0200 (CEST)\n" 6 | "PO-Revision-Date: Thu Jun 18 2015 16:21:26 GMT+0200 (CEST)\n" 7 | "Last-Translator: admin_fi \n" 8 | "Language-Team: \n" 9 | "Language: French (France)\n" 10 | "Plural-Forms: nplurals=2; plural=n > 1\n" 11 | "MIME-Version: 1.0\n" 12 | "Content-Type: text/plain; charset=UTF-8\n" 13 | "Content-Transfer-Encoding: 8bit\n" 14 | "X-Poedit-SourceCharset: UTF-8\n" 15 | "X-Poedit-Basepath: .\n" 16 | "X-Poedit-SearchPath-0: ..\n" 17 | "X-Poedit-KeywordsList: _:1;gettext:1;dgettext:2;ngettext:1,2;dngettext:2,3;" 18 | "__:1;_e:1;_c:1;_n:1,2;_n_noop:1,2;_nc:1,2;__ngettext:1,2;__ngettext_noop:1,2;" 19 | "_x:1,2c;_ex:1,2c;_nx:1,2,4c;_nx_noop:1,2,3c;_n_js:1,2;_nx_js:1,2,3c;" 20 | "esc_attr__:1;esc_html__:1;esc_attr_e:1;esc_html_e:1;esc_attr_x:1,2c;" 21 | "esc_html_x:1,2c;comments_number_link:2,3;t:1;st:1;trans:1;transChoice:1,2\n" 22 | "X-Generator: Loco - https://localise.biz/\n" 23 | "X-Loco-Target-Locale: fr_FR" 24 | 25 | #: ../db-table-editor.php:75 26 | msgid "DB-Table-Editor Cannot READ DATA SOURCE" 27 | msgstr "DB-Table-Editor n'arrive pas à lire la source de données" 28 | 29 | #: ../db-table-editor.php:157 30 | #, php-format 31 | msgid "Showing %d of %d rows - items with unsaved changes are not filtered" 32 | msgstr "Affichage de %d ligne(s) sur %d - les lignes modifiées ne sont pas filtrées" 33 | 34 | #: ../db-table-editor.php:158 35 | msgid "Are you sure you wish to remove this row" 36 | msgstr "Êtes-vous sûr(e) de vouloir supprimer cette ligne ?\n" 37 | 38 | #: ../db-table-editor.php:159 39 | msgid "Delete this Row" 40 | msgstr "Supprimer cette ligne\n" 41 | 42 | #: ../db-table-editor.php:213 43 | msgid "No Database Table Configured to Edit" 44 | msgstr "Aucune table de base de données n'a été configurée pour l'édition" 45 | 46 | #: ../db-table-editor.php:225 47 | #, php-format 48 | msgid "There are %s unsaved changes" 49 | msgstr "Il y a %s modification(s) non enregistrée(s)" 50 | 51 | #: ../db-table-editor.php:226 52 | #, php-format 53 | msgid "Save %s Changes" 54 | msgstr "Enregistrer %s modification(s)\n" 55 | 56 | #: ../db-table-editor.php:227 57 | msgid "New" 58 | msgstr "Ajouter" 59 | 60 | #: ../db-table-editor.php:228 61 | msgid "Undo" 62 | msgstr "Défaire\n" 63 | 64 | #: ../db-table-editor.php:243 65 | msgid "Export to CSV" 66 | msgstr "Export CSV" 67 | 68 | #: ../db-table-editor.php:244 69 | msgid "Clear Filters" 70 | msgstr "Effacer les filtres" 71 | 72 | #: ../db-table-editor.php:245 73 | msgid "Showing 0 of 0 rows" 74 | msgstr "Afficher 0 ligne(s) sur 0" 75 | 76 | #: ../db-table-editor.php:246 77 | msgid "You have unsaved data, are you sure you want to quit" 78 | msgstr "" 79 | "Il y a des données non enregistrées, êtes-vous sûr(e) de vouloir quitter " 80 | "cette page ?" 81 | 82 | #: ../db-table-editor.php:309 83 | #, php-format 84 | msgid "" 85 | "This plugin allows viewing, editing, and exporting of database tables in " 86 | "your wordpress database through the admin interface. See the %sREADME.md%s " 87 | "for information on configuring this plugin." 88 | msgstr "" 89 | "Ce plugin permet de voir, modifier et exporter des données de votre base de " 90 | "données Wordpress via l'interface d'administration. Voir le fichier %sREADME." 91 | "md%s pour avoir des informations sur la configuration de ce plugin.\n" 92 | 93 | #: ../db-table-editor.php:312 94 | msgid "Configured Database Tables" 95 | msgstr "Tableaux de base de données configurés" 96 | -------------------------------------------------------------------------------- /languages/wp-db-table-editor.pot: -------------------------------------------------------------------------------- 1 | # Loco Gettext template 2 | #, fuzzy 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: DB-table-editor\n" 6 | "Report-Msgid-Bugs-To: \n" 7 | "POT-Creation-Date: Thu Jun 18 2015 10:48:46 GMT+0200 (CEST)\n" 8 | "POT-Revision-Date: Thu Jun 18 2015 16:20:30 GMT+0200 (CEST)\n" 9 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 10 | "Last-Translator: \n" 11 | "Language-Team: \n" 12 | "Language: \n" 13 | "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "X-Poedit-SourceCharset: UTF-8\n" 18 | "X-Poedit-Basepath: .\n" 19 | "X-Poedit-SearchPath-0: ..\n" 20 | "X-Poedit-KeywordsList: _:1;gettext:1;dgettext:2;ngettext:1,2;dngettext:2,3;" 21 | "__:1;_e:1;_c:1;_n:1,2;_n_noop:1,2;_nc:1,2;__ngettext:1,2;__ngettext_noop:1,2;" 22 | "_x:1,2c;_ex:1,2c;_nx:1,2,4c;_nx_noop:1,2,3c;_n_js:1,2;_nx_js:1,2,3c;" 23 | "esc_attr__:1;esc_html__:1;esc_attr_e:1;esc_html_e:1;esc_attr_x:1,2c;" 24 | "esc_html_x:1,2c;comments_number_link:2,3;t:1;st:1;trans:1;transChoice:1,2\n" 25 | "X-Generator: Loco - https://localise.biz/" 26 | 27 | #: ../db-table-editor.php:75 28 | msgid "DB-Table-Editor Cannot READ DATA SOURCE" 29 | msgstr "" 30 | 31 | #: ../db-table-editor.php:157 32 | #, php-format 33 | msgid "Showing %d of %d rows - items with unsaved changes are not filtered" 34 | msgstr "" 35 | 36 | #: ../db-table-editor.php:158 37 | msgid "Are you sure you wish to remove this row" 38 | msgstr "" 39 | 40 | #: ../db-table-editor.php:159 41 | msgid "Delete this Row" 42 | msgstr "" 43 | 44 | #: ../db-table-editor.php:213 45 | msgid "No Database Table Configured to Edit" 46 | msgstr "" 47 | 48 | #: ../db-table-editor.php:225 49 | #, php-format 50 | msgid "There are %s unsaved changes" 51 | msgstr "" 52 | 53 | #: ../db-table-editor.php:226 54 | #, php-format 55 | msgid "Save %s Changes" 56 | msgstr "" 57 | 58 | #: ../db-table-editor.php:227 59 | msgid "New" 60 | msgstr "" 61 | 62 | #: ../db-table-editor.php:228 63 | msgid "Undo" 64 | msgstr "" 65 | 66 | #: ../db-table-editor.php:243 67 | msgid "Export to CSV" 68 | msgstr "" 69 | 70 | #: ../db-table-editor.php:244 71 | msgid "Clear Filters" 72 | msgstr "" 73 | 74 | #: ../db-table-editor.php:245 75 | msgid "Showing 0 of 0 rows" 76 | msgstr "" 77 | 78 | #: ../db-table-editor.php:246 79 | msgid "You have unsaved data, are you sure you want to quit" 80 | msgstr "" 81 | 82 | #: ../db-table-editor.php:309 83 | #, php-format 84 | msgid "" 85 | "This plugin allows viewing, editing, and exporting of database tables in " 86 | "your wordpress database through the admin interface. See the %sREADME.md%s " 87 | "for information on configuring this plugin." 88 | msgstr "" 89 | 90 | #: ../db-table-editor.php:312 91 | msgid "Configured Database Tables" 92 | msgstr "" 93 | -------------------------------------------------------------------------------- /screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccelerationNet/wp-db-table-editor/775a84db32f4ef1b299b8542836c04e099f92966/screenshot-1.png --------------------------------------------------------------------------------