From 44fcdaf55bfd25a10409f962adf690020e3f90d0 Mon Sep 17 00:00:00 2001 From: Aris Feryanto Date: Thu, 21 Jul 2011 18:22:47 +0700 Subject: [PATCH] Add new feature: grid editing --- js/functions.js | 13 + js/makegrid.js | 604 +++++++++++++++++++++++- libraries/display_tbl.lib.php | 7 + themes/original/css/theme_right.css.php | 38 +- themes/pmahomme/css/theme_right.css.php | 38 +- 5 files changed, 697 insertions(+), 3 deletions(-) diff --git a/js/functions.js b/js/functions.js index bf695367c3..8d841cf524 100644 --- a/js/functions.js +++ b/js/functions.js @@ -2741,3 +2741,16 @@ function PMA_createqTip($elements, content, options) { $elements.qtip($.extend(true, o, options)); } +/** + * Return value of a cell in a table. + */ +function PMA_getCellValue(td) { + if ($(td).is('.null')) { + return ''; + } else if ($(td).is(':not(.truncated, .transformed, .relation, .enum, .set, .null)')) { + return $(td).find('span').html().replace(/
/g, "\n"); + } else { + return $(td).text(); + } +} + diff --git a/js/makegrid.js b/js/makegrid.js index a5bbdb89cf..1997bdf8ee 100644 --- a/js/makegrid.js +++ b/js/makegrid.js @@ -21,6 +21,11 @@ showColVisibHint: false, showAllColText: '', // string, text for "show all" button under column visibility list visibleHeadersCount: 0, // number of visible data headers + isCellEditActive: false, // true if current focus is in edit cell + isEditCellTextEditable: false, // true if current edit cell is editable in the text input box (not textarea) + currentEditCell: null, // reference to that currently being edited + inEditMode: false, // true if grid is in edit mode + cellEditHint: '', // text hint when doing grid edit // functions dragStartRsz: function(e, obj) { // start column resize @@ -34,6 +39,9 @@ }; $('body').css('cursor', 'col-resize'); $('body').noSelect(); + if (g.isInEditMode) { + g.hideEditCell(); + } }, dragStartMove: function(e, obj) { // start column move @@ -65,6 +73,9 @@ this.qtip.hide(); $('body').css('cursor', 'move'); $('body').noSelect(); + if (g.isInEditMode) { + g.hideEditCell(); + } }, dragMove: function(e) { @@ -437,6 +448,534 @@ } } this.afterToggleCol(); + }, + + /** + * Show edit cell, if it can be shown or it is forced. + */ + showEditCell: function(cell, force) { + if (g.isInEditMode && + $(cell).is('.inline_edit') && + !g.colRsz && !g.colMov) + { + if (!g.isCellEditActive || force) { + $cell = $(cell); + // remove all edit area and hide it + $(g.cEdit).find('.edit_area').empty().hide(); + // reposition the cEdit element + $(g.cEdit).css({ + top: $cell.position().top, + left: $cell.position().left, + }) + .show() + .find('input') + .css({ + width: $cell.outerWidth() - 16, + height: $cell.outerHeight() + }); + // fill the cell edit with text from , if it is not null + var value = $cell.is(':not(.null)') ? PMA_getCellValue(cell) : ''; + $(g.cEdit).find('input') + .val(value); + + g.isCellEditActive = false; + g.currentEditCell = cell; + } + } else { + g.hideEditCell(); + } + }, + + /** + * Remove edit cell and the edit area, if it is shown. + * + * @param force Optional, force to hide edit cell without saving edited field. + * @param data Optional, data from the POST AJAX request to save the edited field. + */ + hideEditCell: function(force, data) { + if (g.isCellEditActive && !force) { + // cell is being edited, post the edited data + g.isCellEditActive = false; + g.postEditedCell(); + return; + } + $(g.cEdit).hide(); + $(g.cEdit).find('input[type=text]').blur(); + g.isCellEditActive = false; + + if (data) { + // Cell edit post has been successful. + $this_field = $(g.currentEditCell); + $this_field_span = $this_field.children('span'); + + var is_null = $(g.cEdit).find('input:checkbox').is(':checked'); + if (is_null) { + $this_field_span.html('NULL'); + $this_field.addClass('null'); + } else { + $this_field.removeClass('null'); + if($this_field.is(':not(.relation, .enum, .set)')) { + /** + * @var new_html String containing value of the data field after edit + */ + var new_html = $(g.cEdit).find('textarea').val(); + + if($this_field.is('.transformed')) { + var field_name = getFieldName($this_field); + if (typeof data.transformations != 'undefined') { + $.each(data.transformations, function(key, value) { + if(key == field_name) { + if($this_field.is('.text_plain, .application_octetstream')) { + new_html = value; + return false; + } else { + var new_value = $(g.cEdit).find('textarea').val(); + new_html = $(value).append(new_value); + return false; + } + } + }) + } + } + // replace '\n' with
+ new_html = new_html.replace(/\n/g, '
'); + } else { + var new_html = ''; + var new_value = ''; + $test_element = $(g.cEdit).find('select'); + if ($test_element.length != 0) { + new_value = $test_element.val(); + } + $test_element = $this_field.find('span.curr_value'); + if ($test_element.length != 0) { + new_value = $test_element.text(); + } + + if($this_field.is('.relation')) { + var field_name = getFieldName($this_field); + if (typeof data.relations != 'undefined') { + $.each(data.relations, function(key, value) { + if(key == field_name) { + new_html = $(value); + return false; + } + }) + } + } else if ($this_field.is('.enum')) { + new_html = new_value; + } else if ($this_field.is('.set')) { + if (new_value != null) { + $.each(new_value, function(key, value) { + new_html = new_html + value + ','; + }) + new_html = new_html.substring(0, new_html.length-1); + } + } + } + $this_field_span.html(new_html); + } + // refresh the grid + this.reposRsz(); + this.reposDrop(); + } // end of if "data" is defined, i.e. post successful + }, + + /** + * Show drop-down edit area when edit cell is clicked. + */ + showEditArea: function() { + if (!this.isCellEditActive) { // make sure we don't have focus on other edit cell + g.isCellEditActive = true; + g.isEditCellTextEditable = false; + var $td = $(g.currentEditCell); + var $editArea = $(this.cEdit).find('.edit_area'); + var where_clause = $td.parent('tr').find('.where_clause').val(); + /** + * @var field_name String containing the name of this field. + * @see getFieldName() + */ + var field_name = getFieldName($td); + /** + * @var relation_curr_value String current value of the field (for fields that are foreign keyed). + */ + var relation_curr_value = $td.find('a').text(); + /** + * @var relation_key_or_display_column String relational key if in 'Relational display column' mode, + * relational display column if in 'Relational key' mode (for fields that are foreign keyed). + */ + var relation_key_or_display_column = $td.find('a').attr('title'); + + // empty all edit area, then rebuild it based on $td classes + $editArea.empty(); + + if ($td.is(':not(.not_null)')) { + // append a null checkbox + $editArea.append('
Null :
'); + var $checkbox = $editArea.find('.null_div input'); + // check if current is NULL + if ($td.is('.null')) { + $checkbox.attr('checked', true); + } + + // if the select/editor is changed un-check the 'checkbox_null__'. + if ($td.is('.enum, .set')) { + $editArea.find('select').live('change', function(e) { + $checkbox.attr('checked', false); + }) + } else if ($td.is('.relation')) { + $editArea.find('select').live('change', function(e) { + $checkbox.attr('checked', false); + }) + $editArea.find('.browse_foreign').live('click', function(e) { + $checkbox.attr('checked', false); + }) + } else { + $(g.cEdit).find('input[type=text]').live('change', function(e) { + $checkbox.attr('checked', false); + }) + $editArea.find('textarea').live('keydown', function(e) { + $checkbox.attr('checked', false); + }) + } + + // if 'checkbox_null__' is clicked empty the corresponding select/editor. + $checkbox.click(function(e) { + if ($td.is('.enum')) { + $editArea.find('select').attr('value', ''); + } else if ($td.is('.set')) { + $editArea.find('select').find('option').each(function() { + var $option = $(this); + $option.attr('selected', false); + }) + } else if ($td.is('.relation')) { + // if the dropdown is there to select the foreign value + if ($editArea.find('select').length > 0) { + $editArea.find('select').attr('value', ''); + } + } else { + $editArea.find('textarea').val(''); + } + $(g.cEdit).find('input[type=text]').val(''); + }) + } + + if($td.is('.truncated, .transformed')) { + /** @lends jQuery */ + //handle truncated/transformed values values + $editArea.addClass('edit_area_loading'); + + /** + * @var sql_query String containing the SQL query used to retrieve value of truncated/transformed data + */ + var sql_query = 'SELECT `' + field_name + '` FROM `' + window.parent.table + '` WHERE ' + PMA_urldecode(where_clause); + + // Make the Ajax call and get the data, wrap it and insert it + $.post('sql.php', { + 'token' : window.parent.token, + 'db' : window.parent.db, + 'ajax_request' : true, + 'sql_query' : sql_query, + 'inline_edit' : true + }, function(data) { + $editArea.removeClass('edit_area_loading'); + if(data.success == true) { + $(g.cEdit).find('input[type=text]').val(data.value); + $editArea.append(''); + $editArea.find('textarea').live('keyup', function(e) { + $(g.cEdit).find('input[type=text]').val($(this).val()); + }); + $(g.cEdit).find('input[type=text]').live('keyup', function(e) { + $editArea.find('textarea').val($(this).val()); + }); + $editArea.append('
' + g.cellEditHint + '
'); + } + else { + PMA_ajaxShowMessage(data.error); + } + }) // end $.post() + g.isEditCellTextEditable = true; + } + else if($td.is('.relation')) { + /** @lends jQuery */ + //handle relations + $editArea.addClass('edit_area_loading'); + + /** + * @var post_params Object containing parameters for the POST request + */ + var post_params = { + 'ajax_request' : true, + 'get_relational_values' : true, + 'db' : window.parent.db, + 'table' : window.parent.table, + 'column' : field_name, + 'token' : window.parent.token, + 'curr_value' : relation_curr_value, + 'relation_key_or_display_column' : relation_key_or_display_column + } + + $.post('sql.php', post_params, function(data) { + $editArea.removeClass('edit_area_loading'); + $editArea.append(data.dropdown); + $editArea.append('
' + g.cellEditHint + '
'); + }) // end $.post() + + $editArea.find('select').live('change', function(e) { + $(g.cEdit).find('input[type=text]').val($(this).val()); + }) + } + else if($td.is('.enum')) { + /** @lends jQuery */ + //handle enum fields + $editArea.addClass('edit_area_loading'); + + /** + * @var post_params Object containing parameters for the POST request + */ + var post_params = { + 'ajax_request' : true, + 'get_enum_values' : true, + 'db' : window.parent.db, + 'table' : window.parent.table, + 'column' : field_name, + 'token' : window.parent.token, + 'curr_value' : curr_value + } + $.post('sql.php', post_params, function(data) { + $editArea.removeClass('edit_area_loading'); + $editArea.append(data.dropdown); + $editArea.append('
' + g.cellEditHint + '
'); + }) // end $.post() + + $editArea.find('select').live('change', function(e) { + $(g.cEdit).find('input[type=text]').val($(this).val()); + }) + } + else if($td.is('.set')) { + /** @lends jQuery */ + //handle set fields + $editArea.addClass('edit_area_loading'); + + /** + * @var post_params Object containing parameters for the POST request + */ + var post_params = { + 'ajax_request' : true, + 'get_set_values' : true, + 'db' : window.parent.db, + 'table' : window.parent.table, + 'column' : field_name, + 'token' : window.parent.token, + 'curr_value' : curr_value + } + + $.post('sql.php', post_params, function(data) { + $editArea.removeClass('edit_area_loading'); + $editArea.append(data.select); + $editArea.append('
' + g.cellEditHint + '
'); + }) // end $.post() + + $editArea.find('select').live('change', function(e) { + $(g.cEdit).find('input[type=text]').val($(this).val()); + }) + } else { + $editArea.append(''); + $editArea.find('textarea').live('keyup', function(e) { + $(g.cEdit).find('input[type=text]').val($(this).val()); + }); + $(g.cEdit).find('input[type=text]').live('keyup', function(e) { + $editArea.find('textarea').val($(this).val()); + }); + $editArea.append('
' + g.cellEditHint + '
'); + g.isEditCellTextEditable = true; + } + + $editArea.show(); + } + }, + + /** + * Post the content of edited cell. + */ + postEditedCell: function() { + + event.preventDefault(); + + /** + * @var $this_field Object referring to the td that is being edited + */ + var $this_field = $(g.currentEditCell); + var $test_element = ''; // to test the presence of a element + + // Initialize variables + var where_clause = $this_field.parent('tr').find('.where_clause').val(); + + /** + * @var nonunique Boolean, whether this row is unique or not + */ + var nonunique = $this_field.is('.nonunique') ? 0 : 1; + /** + * @var relation_fields Array containing the name/value pairs of relational fields + */ + var relation_fields = {}; + /** + * @var relational_display string 'K' if relational key, 'D' if relational display column + */ + var relational_display = $("#relational_display_K").attr('checked') ? 'K' : 'D'; + /** + * @var transform_fields Array containing the name/value pairs for transformed fields + */ + var transform_fields = {}; + /** + * @var transformation_fields Boolean, if there are any transformed fields in this row + */ + var transformation_fields = false; + + /** + * @var sql_query String containing the SQL query to update this row + */ + var sql_query = 'UPDATE `' + window.parent.table + '` SET '; + + var need_to_post = false; + + var new_clause = ''; + + /** + * @var field_name String containing the name of this field. + * @see getFieldName() + */ + var field_name = getFieldName($this_field); + + /** + * @var this_field_params Array temporary storage for the name/value of current field + */ + var this_field_params = {}; + + if($this_field.is('.transformed')) { + transformation_fields = true; + } + /** + * @var is_null String capturing whether 'checkbox_null__' is checked. + */ + var is_null = $(g.cEdit).find('input:checkbox').is(':checked'); + var value; + var addQuotes = true; + + if (is_null) { + sql_query += ' `' + field_name + "`=NULL , "; + need_to_post = true; + } else { + if($this_field.is(":not(.relation, .enum, .set, .bit)")) { + this_field_params[field_name] = $(g.cEdit).find('textarea').val(); + if($this_field.is('.transformed')) { + $.extend(transform_fields, this_field_params); + } + } else if ($this_field.is('.bit')) { + this_field_params[field_name] = '0b' + $(g.cEdit).find('textarea').val(); + addQuotes = false; + } else if ($this_field.is('.set')) { + $test_element = $(g.cEdit).find('select'); + this_field_params[field_name] = $test_element.map(function(){ + return $(this).val(); + }).get().join(","); + } else { + // results from a drop-down + $test_element = $(g.cEdit).find('select'); + if ($test_element.length != 0) { + this_field_params[field_name] = $test_element.val(); + } + + // results from Browse foreign value + $test_element = $(g.cEdit).find('textarea'); + if ($test_element.length != 0) { + this_field_params[field_name] = $test_element.val(); + } + + if($this_field.is('.relation')) { + $.extend(relation_fields, this_field_params); + } + } + if (where_clause.indexOf(field_name) > -1) { + new_clause += '`' + window.parent.table + '`.' + '`' + field_name + "` = '" + this_field_params[field_name].replace(/'/g,"''") + "'" + ' AND '; + } + if (this_field_params[field_name] != PMA_getCellValue(g.currentEditCell)) { + if (addQuotes == true) { + sql_query += ' `' + field_name + "`='" + this_field_params[field_name].replace(/'/g, "''") + "', "; + } else { + sql_query += ' `' + field_name + "`=" + this_field_params[field_name].replace(/'/g, "''") + ", "; + } + need_to_post = true; + } + } + + /* + * update the where_clause, remove the last appended ' AND ' + * */ + + //Remove the last ',' appended in the above loop + sql_query = sql_query.replace(/,\s$/, ''); + //Fix non-escaped backslashes + sql_query = sql_query.replace(/\\/g, '\\\\'); + new_clause = new_clause.substring(0, new_clause.length-5); + new_clause = PMA_urlencode(new_clause); + sql_query += ' WHERE ' + PMA_urldecode(where_clause); + // Avoid updating more than one row in case there is no primary key + // (happened only for duplicate rows) + sql_query += ' LIMIT 1'; + /** + * @var rel_fields_list String, url encoded representation of {@link relations_fields} + */ + var rel_fields_list = $.param(relation_fields); + + /** + * @var transform_fields_list String, url encoded representation of {@link transform_fields} + */ + var transform_fields_list = $.param(transform_fields); + + if (need_to_post) { + // Make the Ajax post after setting all parameters + /** + * @var post_params Object containing parameters for the POST request + */ + var post_params = {'ajax_request' : true, + 'sql_query' : sql_query, + 'token' : window.parent.token, + 'db' : window.parent.db, + 'table' : window.parent.table, + 'clause_is_unique' : nonunique, + 'where_clause' : where_clause, + 'rel_fields_list' : rel_fields_list, + 'do_transformations' : transformation_fields, + 'transform_fields_list' : transform_fields_list, + 'relational_display' : relational_display, + 'goto' : 'sql.php', + 'submit_type' : 'save' + }; + + $.post('tbl_replace.php', post_params, function(data) { + if(data.success == true) { + PMA_ajaxShowMessage(data.message); + if (new_clause != '') { + $this_field.parent('tr').find('.where_clause').attr('value', new_clause); + } + // remove possible previous feedback message + $('#result_query').remove(); + if (typeof data.sql_query != 'undefined') { + // display feedback + $('#sqlqueryresults').prepend(data.sql_query); + } + //PMA_unInlineEditRow($del_hide, $chg_submit, $this_field, $input_siblings, data); + g.hideEditCell(true, data); + } else { + PMA_ajaxShowMessage(data.error); + }; + }) // end $.post() + } else { + // no posting was done but still need to display the row + // in its previous format + //PMA_unInlineEditRow($del_hide, $chg_submit, $this_field, $input_siblings, ''); + g.hideEditCell(); + } } } @@ -450,6 +989,7 @@ g.cPointer = document.createElement('div'); // column pointer, used when reordering column g.cDrop = document.createElement('div'); // column drop-down arrows g.cList = document.createElement('div'); // column visibility list + g.cEdit = document.createElement('div'); // cell edit // adjust g.cCpy g.cCpy.className = 'cCpy'; @@ -466,6 +1006,11 @@ g.cList.className = 'cList'; $(g.cList).hide(); + // adjust g.cEdit + g.cEdit.className = 'cEdit'; + $(g.cEdit).html('
'); + $(g.cEdit).hide(); + // chain table and grid together t.grid = g; g.t = t; @@ -494,6 +1039,9 @@ g.colVisibHint = $('#col_visib_hint').val(); g.showAllColText = $('#show_all_col_text').val(); + // assign cell editing hint + g.cellEditHint = $('#cell_edit_hint').val(); + // initialize column order $col_order = $('#col_order'); if ($col_order.length > 0) { @@ -596,6 +1144,16 @@ // create qtip for each with draggable class PMA_createqTip($(t).find('th.draggable')); + // enable "Edit table" button + $('.edit_mode').removeClass('hide') + .click(function(e) { + g.isInEditMode = !g.isInEditMode; + $('.edit_mode input').toggleClass('edit_mode_active', g.isInEditMode); + if (!g.isInEditMode) { + g.hideEditCell(); + } + }); + // register events if (g.reorderHint) { // make sure columns is reorderable $(t).find('th.draggable') @@ -662,7 +1220,50 @@ $(t).find('td, th.draggable').mouseenter(function() { g.hideColList(); }); - + // edit cell event + $(t).find('td.data') + .mouseenter(function() { + g.showEditCell(this); + }) + .click(function(e) { + if (g.isCellEditActive) { + g.postEditedCell(); + e.stopPropagation(); + } else { + g.showEditCell(this); + $(g.cEdit).find('input[type=text]').focus(); + e.stopPropagation(); + } + }); + $(g.cEdit).find('input[type=text]').focus(function(e) { + g.showEditArea(); + }); + $(g.cEdit).find('input[type=text], select').live('keydown', function(e) { + if (e.which == 13) { + // post on pressing "Enter" + e.preventDefault(); + g.postEditedCell(); + } + }); + $(g.cEdit).keydown(function(e) { + if (e.which == 27) { + // cancel on pressing "Esc" + g.hideEditCell(true); + } else if (!g.isEditCellTextEditable) { + // prevent text editing + e.preventDefault(); + } + }); + $(g.cEdit).click(function(e) { + // prevent click to be handled by $('html').click below + e.stopPropagation(); + }); + $('html').click(function(e) { + // post edited cell + if (g.isCellEditActive) { + g.postEditedCell(); + } + }); // add table class $(t).addClass('pma_table'); @@ -674,6 +1275,7 @@ $(g.gDiv).append(g.cDrop); $(g.gDiv).append(g.cList); $(g.gDiv).append(g.cCpy); + $(g.gDiv).append(g.cEdit); // some adjustment g.refreshRestoreButton(); diff --git a/libraries/display_tbl.lib.php b/libraries/display_tbl.lib.php index a120037ebc..7124207995 100644 --- a/libraries/display_tbl.lib.php +++ b/libraries/display_tbl.lib.php @@ -392,6 +392,13 @@ function PMA_displayTableNavigation($pos_next, $pos_prev, $sql_query, $id_for_di echo ''; } ?> + +
+ + +
+ '; ?> +
diff --git a/themes/original/css/theme_right.css.php b/themes/original/css/theme_right.css.php index 70a3746a58..4f8432a013 100644 --- a/themes/original/css/theme_right.css.php +++ b/themes/original/css/theme_right.css.php @@ -2225,7 +2225,7 @@ span.mysql-number { font-weight: bold; } -.navigation input[type=submit]:hover { +.navigation input[type=submit]:hover, .navigation input.edit_mode_active { background: #333; color: white; cursor: pointer; @@ -2234,3 +2234,39 @@ span.mysql-number { .navigation select { margin: 0 0.8em; } + +.cEdit { + margin: 0; + padding: 0; + position: absolute; +} + +.cEdit input[type=text] { + background: #FFF url(getImgPath(); ?>b_more.png) no-repeat right; + height: 100%; + margin: 0; + padding: 0 16px 0 0; +} + +.cEdit .edit_area { + background: #FFF; + border: 1px solid #CCC; + min-width: 10em; + padding: 0.3em 0.5em; +} + +.cEdit .edit_area select, .cEdit .edit_area textarea { + width: 97%; +} + +.cEdit .cell_edit_hint { + color: #555; + font-size: 0.8em; + margin: 0.3em 0.2em; +} + +.edit_area_loading { + background: #FFF url(getImgPath(); ?>ajax_clock_small.gif) no-repeat center; + height: 10em; +} + diff --git a/themes/pmahomme/css/theme_right.css.php b/themes/pmahomme/css/theme_right.css.php index af26481f4d..2afcf57e4e 100644 --- a/themes/pmahomme/css/theme_right.css.php +++ b/themes/pmahomme/css/theme_right.css.php @@ -2600,7 +2600,7 @@ span.mysql-number { -moz-border-radius: 0; } -.navigation input[type=submit]:hover { +.navigation input[type=submit]:hover, .navigation input.edit_mode_active { color: white; cursor: pointer; text-shadow: none; @@ -2616,3 +2616,39 @@ span.mysql-number { .navigation select { margin: 0 0.8em; } + +.cEdit { + margin: 0; + padding: 0; + position: absolute; +} + +.cEdit input[type=text] { + background: #FFF url(./themes/pmahomme/img/b_more.png) no-repeat right; + height: 100%; + margin: 0; + padding: 0 16px 0 0; +} + +.cEdit .edit_area { + background: #FFF; + border: 1px solid #CCC; + min-width: 10em; + padding: 0.3em 0.5em; +} + +.cEdit .edit_area select, .cEdit .edit_area textarea { + width: 97%; +} + +.cEdit .cell_edit_hint { + color: #555; + font-size: 0.8em; + margin: 0.3em 0.2em; +} + +.edit_area_loading { + background: #FFF url(./themes/pmahomme/img/ajax_clock_small.gif) no-repeat center; + height: 10em; +} +