Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.723 diff -u -r1.723 common.inc --- includes/common.inc 26 Nov 2007 16:36:42 -0000 1.723 +++ includes/common.inc 26 Nov 2007 17:39:58 -0000 @@ -2012,9 +2012,10 @@ * group. Depth updates the target element with the current indentation. * @param $relationship * String describing where the $action variable should be performed. Either - * 'parent', 'sibling', or 'self'. Parent will only look for fields up the - * tree. Sibling will look for fields in the same group in rows above and - * below it. Self affects the dragged row itself. + * 'parent', 'sibling', 'group', or 'self'. Parent will only look for fields + * up the tree. Sibling will look for fields in the same group in rows above + * and below it. Self affects the dragged row itself. Group affects the + * dragged row, plus any children below it (the entire dragged group). * @param $group * A class name applied on all related form elements for this action. * @param $subgroup Index: misc/tabledrag.js =================================================================== RCS file: /cvs/drupal/drupal/misc/tabledrag.js,v retrieving revision 1.4 diff -u -r1.4 tabledrag.js --- misc/tabledrag.js 26 Nov 2007 16:36:42 -0000 1.4 +++ misc/tabledrag.js 26 Nov 2007 17:39:58 -0000 @@ -413,7 +413,20 @@ var droppedRow = self.rowObject.element; // The row is already in the right place so we just release it. if (self.rowObject.changed == true) { + // Update the fields in the dropped row. self.updateFields(droppedRow); + + // If a setting exists for affecting the entire group, update all the + // fields in the entire dragged group. + for (var group in self.tableSettings) { + var rowSettings = self.rowSettings(group, droppedRow); + if (rowSettings.relationship == 'group') { + for (n in self.rowObject.children) { + self.updateField(self.rowObject.children[n], group); + } + } + } + self.rowObject.markChanged(); if (self.changed == false) { $(Drupal.theme('tableDragChangedWarning')).insertAfter(self.table).hide().fadeIn('slow'); @@ -562,114 +575,127 @@ for (var group in this.tableSettings) { // Each group may have a different setting for relationship, so we find // the source rows for each seperately. - var rowSettings = this.rowSettings(group, changedRow); + this.updateField(changedRow, group); + } +} - // Set the row as it's own target. - if (rowSettings.relationship == 'self') { - var sourceRow = changedRow; - } - // Siblings are easy, check previous and next rows. - else if (rowSettings.relationship == 'sibling') { - var previousRow = $(changedRow).prev('tr').get(0); - var nextRow = $(changedRow).next('tr').get(0); - var sourceRow = changedRow; - if ($(previousRow).is('.draggable') && $('.' + group, previousRow).length) { - if (this.indentEnabled) { - if ($('.indentations', previousRow).size() == $('.indentations', changedRow)) { - sourceRow = previousRow; - } - } - else { +/** + * After the row is dropped, update a single table field according to specific + * settings. + * + * @param changedRow + * DOM object for the row that was just dropped. + * @param group + * The settings group on which field updates will occur. + */ +Drupal.tableDrag.prototype.updateField = function(changedRow, group) { + var rowSettings = this.rowSettings(group, changedRow); + + // Set the row as it's own target. + if (rowSettings.relationship == 'self' || rowSettings.relationship == 'group') { + var sourceRow = changedRow; + } + // Siblings are easy, check previous and next rows. + else if (rowSettings.relationship == 'sibling') { + var previousRow = $(changedRow).prev('tr').get(0); + var nextRow = $(changedRow).next('tr').get(0); + var sourceRow = changedRow; + if ($(previousRow).is('.draggable') && $('.' + group, previousRow).length) { + if (this.indentEnabled) { + if ($('.indentations', previousRow).size() == $('.indentations', changedRow)) { sourceRow = previousRow; } } - else if ($(nextRow).is('.draggable') && $('.' + group, nextRow).length) { - if (this.indentEnabled) { - if ($('.indentations', nextRow).size() == $('.indentations', changedRow)) { - sourceRow = nextRow; - } - } - else { - sourceRow = nextRow; - } + else { + sourceRow = previousRow; } } - // Parents, look up the tree until we find a field not in this group. - // Go up as many parents as indentations in the changed row. - else if (rowSettings.relationship == 'parent') { - var previousRow = $(changedRow).prev('tr'); - while (previousRow.length && $('.indentation', previousRow).length >= this.rowObject.indents) { - previousRow = previousRow.prev('tr'); - } - // If we found a row. - if (previousRow.length) { - sourceRow = previousRow[0]; + else if ($(nextRow).is('.draggable') && $('.' + group, nextRow).length) { + if (this.indentEnabled) { + if ($('.indentations', nextRow).size() == $('.indentations', changedRow)) { + sourceRow = nextRow; + } } - // Otherwise we went all the way to the left of the table without finding - // a parent, meaning this item has been placed at the root level. else { - // Use the first row in the table as source, because it's garanteed to - // be at the root level. Find the first item, then compare this row - // against it as a sibling. - sourceRow = $('tr.draggable:first').get(0); - if (sourceRow == this.rowObject.element) { - sourceRow = $(this.rowObject.group[this.rowObject.group.length - 1]).next('tr.draggable').get(0); - } - var useSibling = true; + sourceRow = nextRow; } } - - // Because we may have moved the row from one category to another, - // take a look at our sibling and borrow its sources and targets. - this.copyDragClasses(sourceRow, changedRow, group); - rowSettings = this.rowSettings(group, changedRow); - - // In the case that we're looking for a parent, but the row is at the top - // of the tree, copy our sibling's values. - if (useSibling) { - rowSettings.relationship = 'sibling'; - rowSettings.source = rowSettings.target; + } + // Parents, look up the tree until we find a field not in this group. + // Go up as many parents as indentations in the changed row. + else if (rowSettings.relationship == 'parent') { + var previousRow = $(changedRow).prev('tr'); + while (previousRow.length && $('.indentation', previousRow).length >= this.rowObject.indents) { + previousRow = previousRow.prev('tr'); } + // If we found a row. + if (previousRow.length) { + sourceRow = previousRow[0]; + } + // Otherwise we went all the way to the left of the table without finding + // a parent, meaning this item has been placed at the root level. + else { + // Use the first row in the table as source, because it's garanteed to + // be at the root level. Find the first item, then compare this row + // against it as a sibling. + sourceRow = $('tr.draggable:first').get(0); + if (sourceRow == this.rowObject.element) { + sourceRow = $(this.rowObject.group[this.rowObject.group.length - 1]).next('tr.draggable').get(0); + } + var useSibling = true; + } + } - var targetClass = '.' + rowSettings.target; - var targetElement = $(targetClass, changedRow).get(0); - - // Check if a target element exists in this row. - if (targetElement) { - var sourceClass = '.' + rowSettings.source; - var sourceElement = $(sourceClass, sourceRow).get(0); - switch (rowSettings.action) { - case 'depth': - // Get the depth of the target row. - targetElement.value = $('.indentation', $(sourceElement).parents('tr:first')).size(); - break; - case 'match': - // Update the value. - targetElement.value = sourceElement.value; - break; - case 'order': - var siblings = this.rowObject.findSiblings(rowSettings); - if ($(targetElement).is('select')) { - // Get a list of acceptable values. - var values = new Array(); - $('option', targetElement).each(function() { - values.push(this.value); - }); - // Populate the values in the siblings. - $(targetClass, siblings).each(function() { - this.value = values.shift(); - }); - } - else { - // Assume a numeric input field. - var weight = parseInt($(targetClass, siblings[0]).val()) || 0; - $(targetClass, siblings).each(function() { - this.value = weight; - weight++; - }); - } - break; - } + // Because we may have moved the row from one category to another, + // take a look at our sibling and borrow its sources and targets. + this.copyDragClasses(sourceRow, changedRow, group); + rowSettings = this.rowSettings(group, changedRow); + + // In the case that we're looking for a parent, but the row is at the top + // of the tree, copy our sibling's values. + if (useSibling) { + rowSettings.relationship = 'sibling'; + rowSettings.source = rowSettings.target; + } + + var targetClass = '.' + rowSettings.target; + var targetElement = $(targetClass, changedRow).get(0); + + // Check if a target element exists in this row. + if (targetElement) { + var sourceClass = '.' + rowSettings.source; + var sourceElement = $(sourceClass, sourceRow).get(0); + switch (rowSettings.action) { + case 'depth': + // Get the depth of the target row. + targetElement.value = $('.indentation', $(sourceElement).parents('tr:first')).size(); + break; + case 'match': + // Update the value. + targetElement.value = sourceElement.value; + break; + case 'order': + var siblings = this.rowObject.findSiblings(rowSettings); + if ($(targetElement).is('select')) { + // Get a list of acceptable values. + var values = new Array(); + $('option', targetElement).each(function() { + values.push(this.value); + }); + // Populate the values in the siblings. + $(targetClass, siblings).each(function() { + this.value = values.shift(); + }); + } + else { + // Assume a numeric input field. + var weight = parseInt($(targetClass, siblings[0]).val()) || 0; + $(targetClass, siblings).each(function() { + this.value = weight; + weight++; + }); + } + break; } } }; Index: modules/taxonomy/taxonomy.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.admin.inc,v retrieving revision 1.11 diff -u -r1.11 taxonomy.admin.inc --- modules/taxonomy/taxonomy.admin.inc 23 Nov 2007 13:34:55 -0000 1.11 +++ modules/taxonomy/taxonomy.admin.inc 26 Nov 2007 17:40:21 -0000 @@ -7,30 +7,79 @@ */ /** - * List and manage vocabularies. + * Form builder to list and manage vocabularies. + * + * @ingroup forms + * @see taxonomy_overview_vocabularies_submit(). + * @see theme_taxonomy_overview_vocabularies(). */ function taxonomy_overview_vocabularies() { + $form = array('#tree' => TRUE); $vocabularies = taxonomy_get_vocabularies(); - $rows = array(); foreach ($vocabularies as $vocabulary) { $types = array(); foreach ($vocabulary->nodes as $type) { $node_type = node_get_types('name', $type); $types[] = $node_type ? $node_type : $type; } - $rows[] = array('name' => check_plain($vocabulary->name), - 'type' => implode(', ', $types), - 'edit' => l(t('edit vocabulary'), "admin/content/taxonomy/edit/vocabulary/$vocabulary->vid"), - 'list' => l(t('list terms'), "admin/content/taxonomy/$vocabulary->vid"), - 'add' => l(t('add terms'), "admin/content/taxonomy/$vocabulary->vid/add/term") - ); + $form[$vocabulary->vid]['#vocabulary'] = (array)$vocabulary; + $form[$vocabulary->vid]['name'] = array('#value' => check_plain($vocabulary->name)); + $form[$vocabulary->vid]['types'] = array('#value' => implode(', ', $types)); + $form[$vocabulary->vid]['weight'] = array('#type' => 'weight', '#delta' => 10, '#default_value' => $vocabulary->weight); + $form[$vocabulary->vid]['edit'] = array('#value' => l(t('edit vocabulary'), "admin/content/taxonomy/edit/vocabulary/$vocabulary->vid")); + $form[$vocabulary->vid]['list'] = array('#value' => l(t('list terms'), "admin/content/taxonomy/$vocabulary->vid")); + $form[$vocabulary->vid]['add'] = array('#value' => l(t('add terms'), "admin/content/taxonomy/$vocabulary->vid/add/term")); + } + + $form['submit'] = array('#type' => 'submit', '#value' => t('Submit')); + return $form; +} + +/** + * Submit handler for vocabularies overview. Updates changed vocabulary weights. + * + * @see taxonomy_overview_vocabularies(). + */ +function taxonomy_overview_vocabularies_submit($form, &$form_state) { + foreach ($form_state['values'] as $vid => $vocabulary) { + if (is_numeric($vid) && $form[$vid]['#vocabulary']['weight'] != $form_state['values'][$vid]['weight']) { + $form[$vid]['#vocabulary']['weight'] = $form_state['values'][$vid]['weight']; + taxonomy_save_vocabulary($form[$vid]['#vocabulary']); + } + } +} + +/** + * Theme the vocabulary overview as a sortable list of vocabularies. + * + * @ingroup themeable + * @see taxonomy_overview_vocabularies(). + */ +function theme_taxonomy_overview_vocabularies($form) { + drupal_add_tabledrag('taxonomy', 'order', 'sibling', 'vocabulary-weight'); + + $rows = array(); + foreach (element_children($form) as $key) { + if (isset($form[$key]['name'])) { + $vocabulary = &$form[$key]; + $vocabulary['weight']['#attributes']['class'] = 'vocabulary-weight'; + + $row = array(); + $row[] = drupal_render($vocabulary['name']); + $row[] = drupal_render($vocabulary['types']); + $row[] = drupal_render($vocabulary['weight']); + $row[] = drupal_render($vocabulary['edit']); + $row[] = drupal_render($vocabulary['list']); + $row[] = drupal_render($vocabulary['add']); + $rows[] = array('data' => $row, 'class' => 'draggable'); + } } if (empty($rows)) { - $rows[] = array(array('data' => t('No terms available.'), 'colspan' => '5')); + $rows[] = array(array('data' => t('No terms available.'), 'colspan' => '6')); } - $header = array(t('Name'), t('Type'), array('data' => t('Operations'), 'colspan' => '3')); + $header = array(t('Name'), t('Type'), t('Weight'), array('data' => t('Operations'), 'colspan' => '3')); - return theme('table', $header, $rows, array('id' => 'taxonomy')); + return theme('table', $header, $rows, array('id' => 'taxonomy')) . drupal_render($form); } /** @@ -114,7 +163,7 @@ // Set the hierarchy to "multiple parents" by default. This simplifies the // vocabulary form and standardizes the term form. $form['hierarchy'] = array('#type' => 'value', - '#value' => '2', + '#value' => '0', ); // Enable "related terms" by default. $form['relations'] = array('#type' => 'value', @@ -166,9 +215,6 @@ * Page to edit a vocabulary term. */ function taxonomy_admin_term_edit($tid) { - if ((isset($_POST['op']) && $_POST['op'] == t('Delete')) || isset($_POST['confirm'])) { - return drupal_get_form('taxonomy_term_confirm_delete', $tid); - } if ($term = (array)taxonomy_get_term($tid)) { return drupal_get_form('taxonomy_form_term', taxonomy_vocabulary_load($term['vid']), $term); } @@ -176,58 +222,388 @@ } /** + * Form builder for the taxonomy terms overview. + * * Display a tree of all the terms in a vocabulary, with options to edit - * each one. + * each one. The form is made drag and drop by the theme function. + * + * @ingroup forms + * @see taxonomy_overview_terms_submit(). + * @see theme_taxonomy_overview_terms(). */ -function taxonomy_overview_terms($vocabulary) { - $destination = drupal_get_destination(); +function taxonomy_overview_terms(&$form_state, $vocabulary) { + global $pager_page_array, $pager_total, $pager_total_items; - $header = array(t('Name'), t('Operations')); + // Check for confirmation forms. + if (isset($form_state['confirm_reset_alphabetical'])) { + return taxonomy_vocabulary_confirm_reset_alphabetical($form_state, $vocabulary->vid); + } drupal_set_title(t('Terms in %vocabulary', array('%vocabulary' => $vocabulary->name))); - $start_from = isset($_GET['page']) ? $_GET['page'] : 0; - $total_entries = 0; // total count for pager - $page_increment = 25; // number of tids per page - $displayed_count = 0; // number of tids shown + $form = array( + '#vocabulary' => (array)$vocabulary, + '#tree' => TRUE, + '#parent_fields' => FALSE, + ); + + $page = isset($_GET['page']) ? $_GET['page'] : 0; + $page_increment = 10; // Number of terms per page. + $page_entries = 0; // Elements shown on this page. + $before_entries = 0; // Elements at the root level before this page. + $after_entries = 0; // Elements at the root level after this page. + $root_entries = 0; // Elements at the root level on this page. + + // Terms from previous and next pages are shown if the term tree would have + // been cut in the middle. Keep track of how many extra terms we show on each + // page of terms. + $back_peddle = NULL; + $forward_peddle = 0; + // An array of the terms to be displayed on this page. + $current_page = array(); + + // Case for free tagging. if ($vocabulary->tags) { // We are not calling taxonomy_get_tree because that might fail with a big // number of tags in the freetagging vocabulary. $results = pager_query(db_rewrite_sql('SELECT t.*, h.parent FROM {term_data} t INNER JOIN {term_hierarchy} h ON t.tid = h.tid WHERE t.vid = %d ORDER BY weight, name', 't', 'tid'), $page_increment, 0, NULL, $vocabulary->vid); + $total_entries = db_query(db_rewrite_sql('SELECT count(*) FROM {term_data} t INNER JOIN {term_hierarchy} h ON t.tid = h.tid WHERE t.vid = %d'), $page_increment, 0, NULL, $vocabulary->vid); while ($term = db_fetch_object($results)) { - $rows[] = array( - l($term->name, "taxonomy/term/$term->tid"), - l(t('edit'), "admin/content/taxonomy/edit/term/$term->tid", array('query' => $destination)), - ); + $key = 'tid:'. $term->tid .':0'; + $current_page[$key] = $term; + $page_entries++; } } + // Case for restricted vocabulary. else { + $term_deltas = array(); $tree = taxonomy_get_tree($vocabulary->vid); - foreach ($tree as $term) { - $total_entries++; // we're counting all-totals, not displayed - if (($start_from && ($start_from * $page_increment) >= $total_entries) || ($displayed_count == $page_increment)) { + $term = current($tree); + do { + // In case this tree is completely empty. + if (empty($term)) { + break; + } + // Count entries before the current page. + if ($page && ($page * $page_increment) > $before_entries && !isset($back_peddle)) { + $before_entries++; + continue; + } + // Count entries after the current page. + elseif ($page_entries > $page_increment && isset($complete_tree)) { + $after_entries++; continue; } - $rows[] = array(str_repeat('--', $term->depth) .' '. l($term->name, "taxonomy/term/$term->tid"), l(t('edit'), "admin/content/taxonomy/edit/term/$term->tid", array('query' => $destination))); - $displayed_count++; // we're counting tids displayed + + // Do not let a term start the page that is not at the root. + if (isset($term->depth) && ($term->depth > 0) && !isset($back_peddle)) { + $back_peddle = 0; + while ($pterm = prev($tree)) { + $before_entries--; + $back_peddle++; + if ($pterm->depth == 0) { + prev($tree); + continue 2; // Jump back to the start of the root level parent. + } + } + } + $back_peddle = isset($back_peddle) ? $back_peddle : 0; + + // Continue rendering the tree until we reach the a new root item. + if ($page_entries >= $page_increment + $back_peddle + 1 && $term->depth == 0 && $root_entries > 1) { + $complete_tree = TRUE; + // This new item at the root level is the first item on the next page. + $after_entries++; + continue; + } + if ($page_entries >= $page_increment + $back_peddle) { + $forward_peddle++; + } + + // Finally, if we've gotten down this far, we're rendering a term on this page. + $page_entries++; + $term_deltas[$term->tid] = isset($term_deltas[$term->tid]) ? $term_deltas[$term->tid] + 1 : 0; + $key = 'tid:'. $term->tid .':'. $term_deltas[$term->tid]; + + // Keep track of the first term displayed on this page. + if ($page_entries == 1) { + $form['#first_tid'] = $term->tid; + } + // Keep a variable to make sure at least 2 root elements are displayed. + if ($term->parents[0] == 0) { + $root_entries++; + } + $current_page[$key] = $term; + } while ($term = next($tree)); + + // Because we didn't use a pager query, set the necessary pager variables. + $total_entries = $before_entries + $page_entries + $after_entries; + $pager_total_items[0] = $total_entries; + $pager_page_array[0] = $page; + $pager_total[0] = ceil($total_entries / $page_increment); + } + + // If this form was already submitted once, it's probably hit a validation + // error. Ensure the form is rebuilt in the same order as the user submitted. + if (!empty($form_state['post'])) { + $order = array_flip(array_keys($form_state['post'])); // Get the $_POST order. + $current_page = array_merge($order, $current_page); // Update our form with the new order. + foreach ($current_page as $key => $term) { + // Verify this is a term for the current page and set at the current depth. + if (is_array($form_state['post'][$key]) && is_numeric($form_state['post'][$key]['tid'])) { + $current_page[$key]->depth = $form_state['post'][$key]['depth']; + } + else { + unset($current_page[$key]); + } } + } - if (empty($rows)) { - $rows[] = array(array('data' => t('No terms available.'), 'colspan' => '2')); + // Build the actual form. + foreach ($current_page as $key => $term) { + // Save the term for the current page so we don't have to load it a second time. + $form[$key]['#term'] = (array)$term; + if (isset($term->parents)) { + $form[$key]['#term']['parent'] = $term->parent = $term->parents[0]; + unset($form[$key]['#term']['parents'], $term->parents); } - $GLOBALS['pager_page_array'][] = $start_from; // FIXME - $GLOBALS['pager_total'][] = intval($total_entries / $page_increment) + 1; // FIXME + $form[$key]['view'] = array('#value' => l($term->name, "taxonomy/term/$term->tid")); + if (!$vocabulary->tags && $vocabulary->hierarchy < 2 && count($tree) > 1) { + $form['#parent_fields'] = TRUE; + $form[$key]['tid'] = array( + '#type' => 'hidden', + '#value' => $term->tid + ); + $form[$key]['parent'] = array( + '#type' => 'hidden', + // Yes, default_value on a hidden. It needs to be changeable by the javascript. + '#default_value' => $term->parent, + ); + $form[$key]['depth'] = array( + '#type' => 'hidden', + // Same as above, the depth is modified by javascript, so it's a default_value. + '#default_value' => $term->depth, + ); + } + $form[$key]['edit'] = array('#value' => l(t('edit'), "admin/content/taxonomy/edit/term/$term->tid", array('query' => drupal_get_destination()))); } - $output = theme('table', $header, $rows, array('id' => 'taxonomy')); - if ($vocabulary->tags || $total_entries >= $page_increment) { - $output .= theme('pager', NULL, $page_increment); + $form['#total_entries'] = $total_entries; + $form['#page_increment'] = $page_increment; + $form['#page_entries'] = $page_entries; + $form['#back_peddle'] = $back_peddle; + $form['#forward_peddle'] = $forward_peddle; + $form['#empty_text'] = t('No terms available.'); + + if (!$vocabulary->tags && $vocabulary->hierarchy < 2 && count($tree) > 1) { + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Submit') + ); + $form['reset_alphabetical'] = array( + '#type' => 'submit', + '#value' => t('Reset to Alphabetical') + ); + $form['destination'] = array( + '#type' => 'hidden', + '#value' => $_GET['q'] . (isset($_GET['page']) ? '?page='. $_GET['page'] : '') + ); } - return $output; + return $form; } +/** + * Submit handler for terms overview form. + * + * Rather than using a textfield or weight field, this form depends entirely + * upon the order of form elements on the page to determine new weights. + * + * Because there might be hundreds or thousands of taxonomy terms that need to + * be ordered, terms are weighted from 0 to the number of terms in the + * vocabulary, rather than the standard -10 to 10 scale. Numbers are sorted + * lowest to highest, but are not necessarily sequential. Numbers may be skipped + * when a term has children so that reordering is minimal when a child is + * added or removed from a term. + * + * @see taxonomy_overview_terms(). + */ +function taxonomy_overview_terms_submit($form, &$form_state) { + if ($form_state['clicked_button']['#value'] == t('Reset to Alphabetical')) { + // Execute the term deletion. + if ($form_state['values']['reset_alphabetical'] === TRUE) { + return taxonomy_vocabulary_confirm_reset_alphabetical_submit($form, $form_state); + } + // Rebuild the form to confirm term deletion. + $form_state['rebuild'] = TRUE; + $form_state['confirm_reset_alphabetical'] = TRUE; + return; + } + + $order = array_flip(array_keys($form['#post'])); // Get the $_POST order. + $form_state['values'] = array_merge($order, $form_state['values']); // Update our original form with the new order. + + $vocabulary = $form['#vocabulary']; + $hierarchy = 0; // Update the current hierarchy type as we go. + + $changed_terms = array(); + $tree = taxonomy_get_tree($vocabulary['vid']); + + if (empty($tree)) { + return; + } + + // Build a list of all terms that need to be updated on previous pages. + $weight = 0; + $term = (array)$tree[0]; + while ($term['tid'] != $form['#first_tid']) { + if ($term['parents'][0] == 0 && $term['weight'] != $weight) { + $term['parent'] = $term['parents'][0]; + $term['weight'] = $weight; + $changed_terms[$term['tid']] = $term; + } + $weight++; + $hierarchy = $term['parents'][0] != 0 ? 1 : $hierarchy; + $term = (array)$tree[$weight]; + } + + // Renumber the current page weights and assign any new parents. + $level_weights = array(); + foreach ($form_state['values'] as $tid => $values) { + if (isset($form[$tid]['#term'])) { + $term = $form[$tid]['#term']; + // Give terms at the root level a weight in sequence with terms on previous pages. + if ($values['parent'] == 0 && $term['weight'] != $weight) { + $term['weight'] = $weight; + $changed_terms[$term['tid']] = $term; + } + // Terms not at the root level can safely start from 0 because they're all on this page. + elseif ($values['parent'] > 0) { + $level_weights[$values['parent']] = isset($level_weights[$values['parent']]) ? $level_weights[$values['parent']] + 1 : 0; + if ($level_weights[$values['parent']] != $term['weight']) { + $term['weight'] = $level_weights[$values['parent']]; + $changed_terms[$term['tid']] = $term; + } + } + // Update any changed parents. + if ($values['parent'] != $term['parent']) { + $term['parent'] = $values['parent']; + $changed_terms[$term['tid']] = $term; + } + $hierarchy = $term['parent'] != 0 ? 1 : $hierarchy; + $weight++; + } + } + + // Build a list of all terms that need to be updated on following pages. + for ($weight; $weight < count($tree); $weight++) { + $term = (array)$tree[$weight]; + if ($term['parents'][0] == 0 && $term['weight'] != $weight) { + $term['parent'] = $term['parents'][0]; + $term['weight'] = $weight; + $changed_terms[$term['tid']] = $term; + } + $hierarchy = $term['parents'][0] != 0 ? 1 : $hierarchy; + } + + // Save all updated terms. + foreach ($changed_terms as $term) { + taxonomy_save_term($term); + } + + // Update the vocabulary hierarchy to flat or single hierarchy. + if ($vocabulary['hierarchy'] != $hierarchy) { + $vocabulary['hierarchy'] = $hierarchy; + taxonomy_save_vocabulary($vocabulary); + } +} + +/** + * Theme the terms overview as a sortable list of terms. + * + * @ingroup themeable + * @see taxonomy_overview_terms(). + */ +function theme_taxonomy_overview_terms($form) { + $page_increment = $form['#page_increment']; + $page_entries = $form['#page_entries']; + $back_peddle = $form['#back_peddle']; + $forward_peddle = $form['#forward_peddle']; + + // Add drag and drop if parent fields are present in the form. + if ($form['#parent_fields']) { + drupal_add_tabledrag('taxonomy', 'match', 'parent', 'term-parent', 'term-parent', 'term-id', FALSE); + drupal_add_tabledrag('taxonomy', 'depth', 'group', 'term-depth', NULL, NULL, FALSE); + drupal_add_js(drupal_get_path('module', 'taxonomy') .'/taxonomy.js'); + drupal_add_js(array('taxonomy' => array('backPeddle' => $back_peddle, 'forwardPeddle' => $forward_peddle)), 'setting'); + drupal_add_css(drupal_get_path('module', 'taxonomy') .'/taxonomy.css'); + } + + $errors = form_get_errors() != FALSE ? form_get_errors() : array(); + $rows = array(); + foreach (element_children($form) as $key) { + if (isset($form[$key]['#term'])) { + $term = &$form[$key]; + + $row = array(); + $row[] = (isset($term['#term']['depth']) && $term['#term']['depth'] > 0 ? theme('indentation', $term['#term']['depth']) : '') . drupal_render($term['view']); + if ($form['#parent_fields']) { + $term['tid']['#attributes']['class'] = 'term-id'; + $term['parent']['#attributes']['class'] = 'term-parent'; + $term['depth']['#attributes']['class'] = 'term-depth'; + $row[0] .= drupal_render($term['parent']) . drupal_render($term['tid']) . drupal_render($term['depth']); + } + $row[] = drupal_render($term['edit']); + + $row = array('data' => $row); + $rows[$key] = $row; + } + } + + // Add necessary classes to rows. + $row_position = 0; + foreach ($rows as $key => $row) { + $classes = array(); + if (isset($form['#parent_fields'])) { + $classes[] = 'draggable'; + } + + // Add classes that mark which terms belong to previous and next pages. + if ($row_position < $back_peddle || $row_position >= $page_entries - $forward_peddle) { + $classes[] = 'taxonomy-term-preview'; + } + + if ($row_position !== 0 && $row_position !== count($rows) - 1) { + if ($row_position == $back_peddle - 1 || $row_position == $page_entries - $forward_peddle - 1) { + $classes[] = 'taxonomy-term-divider-top'; + } + elseif ($row_position == $back_peddle || $row_position == $page_entries - $forward_peddle) { + $classes[] = 'taxonomy-term-divider-bottom'; + } + } + + // Add an error class if this row contains a form error. + foreach ($errors as $error_key => $error) { + if (strpos($error_key, $key) === 0) { + $classes[] = 'error'; + } + } + $rows[$key]['class'] = implode(' ', $classes); + $row_position++; + } + + if (empty($rows)) { + $rows[] = array(array('data' => $form['#empty_text'], 'colspan' => '2')); + } + + $header = array(t('Name'), t('Operations')); + $output = theme('table', $header, $rows, array('id' => 'taxonomy')); + $output .= drupal_render($form); + $output .= theme('pager', NULL, $page_increment); + + return $output; +} /** * Menu callback; return the edit form for a new term after setting the title. @@ -250,6 +626,21 @@ 'tid' => NULL, 'weight' => 0, ); + + $parent = array_keys(taxonomy_get_parents($edit['tid'])); + $form['#term'] = $edit; + $form['#term']['parent'] = $parent; + $form['#vocabulary'] = (array)$vocabulary; + $form['#vocabulary']['nodes'] = drupal_map_assoc($vocabulary->nodes);; + + // Check for confirmation forms. + if (isset($form_state['confirm_delete'])) { + return array_merge($form, taxonomy_term_confirm_delete($form_state, $edit['tid'])); + } + elseif (isset($form_state['confirm_parents'])) { + return array_merge($form, taxonomy_term_confirm_parents($form_state, $vocabulary)); + } + $form['identification'] = array( '#type' => 'fieldset', '#title' => t('Identification'), @@ -272,7 +663,7 @@ '#type' => 'fieldset', '#title' => 'Advanced options', '#collapsible' => TRUE, - '#collapsed' => TRUE, + '#collapsed' => $vocabulary->hierarchy > 1 ? FALSE : TRUE, ); // taxonomy_get_tree and taxonomy_get_parents may contain large numbers of @@ -298,10 +689,12 @@ '#default_value' => implode("\n", taxonomy_get_synonyms($edit['tid'])), '#description' => t('Synonyms of this term, one synonym per line.')); $form['advanced']['weight'] = array( - '#type' => 'weight', + '#type' => 'textfield', '#title' => t('Weight'), + '#size' => 6, '#default_value' => $edit['weight'], - '#description' => t('Vocabularies are displayed in ascending order by weight.')); + '#description' => t('Terms are displayed in ascending order by weight.'), + '#required' => TRUE); $form['vid'] = array( '#type' => 'value', '#value' => $vocabulary->vid); @@ -312,7 +705,8 @@ if ($edit['tid']) { $form['delete'] = array( '#type' => 'submit', - '#value' => t('Delete')); + '#value' => t('Delete'), + '#submit' => array('taxonomy_term_confirm_delete')); $form['tid'] = array( '#type' => 'value', '#value' => $edit['tid']); @@ -325,9 +719,39 @@ } /** - * Accept the form submission for a taxonomy term and save the result. + * Validation handler for the term edit form. Ensure numeric weight values. + * + * @see taxonomy_form_term(). + */ +function taxonomy_form_term_validate($form, &$form_state) { + if (isset($form_state['values']['weight']) && !is_numeric($form_state['values']['weight'])) { + form_set_error('weight', t('Weight value must be numeric.')); + } +} + +/** + * Submit handler to insert or update a term. + * + * @see taxonomy_form_term(). */ function taxonomy_form_term_submit($form, &$form_state) { + if ($form_state['clicked_button']['#value'] == t('Delete')) { + // Execute the term deletion. + if ($form_state['values']['delete'] === TRUE) { + return taxonomy_term_confirm_delete_submit($form, $form_state); + } + // Rebuild the form to confirm term deletion. + $form_state['rebuild'] = TRUE; + $form_state['confirm_delete'] = TRUE; + return; + } + // Rebuild the form to confirm enabling multiple parents. + elseif ($form_state['clicked_button']['#value'] == t('Save') && !$form['#vocabulary']['tags'] && count($form_state['values']['parent']) > 1 && $form['#vocabulary']['hierarchy'] < 2) { + $form_state['rebuild'] = TRUE; + $form_state['confirm_parents'] = TRUE; + return; + } + switch (taxonomy_save_term($form_state['values'])) { case SAVED_NEW: drupal_set_message(t('Created new term %term.', array('%term' => $form_state['values']['name']))); @@ -339,12 +763,54 @@ break; } + if (!$form['#vocabulary']['tags']) { + $current_parent_count = count($form_state['values']['parent']); + $previous_parent_count = count($form['#term']['parent']); + // Root doesn't count if it's the only parent. + if ($current_parent_count == 1 && isset($form_state['values']['parent'][''])) { + $current_parent_count = 0; + $form_state['values']['parent'] = array(); + } + + // If the number of parents has been reduced to one or none, do a check on the + // parents of every term in the vocabulary value. + if ($current_parent_count < $previous_parent_count && $current_parent_count < 2) { + taxonomy_check_vocabulary_hierarchy($form['#vocabulary'], $form_state['values']); + } + // If we've increased the number of parents and this is a single or flat + // hierarchy, update the vocabulary immediately. + elseif ($current_parent_count > $previous_parent_count && $form['#vocabulary']['hierarchy'] < 2) { + $form['#vocabulary']['hierarchy'] = $current_parent_count == 1 ? 1 : 2; + taxonomy_save_vocabulary($form['#vocabulary']); + } + } + $form_state['tid'] = $form_state['values']['tid']; $form_state['redirect'] = 'admin/content/taxonomy'; return; } /** + * Form builder for the confirmation of multiple term parents. + * + * @ingroup forms + * @see taxonomy_form_term(). + */ +function taxonomy_term_confirm_parents(&$form_state, $vocabulary) { + $form = array(); + foreach (element_children($form_state['values']) as $key) { + $form[$key] = array( + '#type' => 'value', + '#value' => $form_state['values'][$key], + ); + } + $question = t('Set multiple term parents?'); + $description = '

'. t("Adding multiple parents to a term will cause the %vocabulary vocabulary to look for multiple parents on every term. Because multiple parents are not supported when using the drag and drop outline interface, drag and drop will be disabled if you enable this option. If you choose to have multiple parents, you will only be able to set parents by using the term edit form.", array('%vocabulary' => $vocabulary->name)) .'

'; + $description .= '

'. t("You may re-enable the drag and drop interface at any time by reducing multiple parents to a single parent for the terms in this vocabulary.") .'

'; + return confirm_form($form, $question, drupal_get_destination(), $description, t('Set multiple parents')); +} + +/** * Form builder for the term delete form. * * @ingroup forms @@ -356,6 +822,7 @@ $form['type'] = array('#type' => 'value', '#value' => 'term'); $form['name'] = array('#type' => 'value', '#value' => $term->name); $form['tid'] = array('#type' => 'value', '#value' => $tid); + $form['delete'] = array('#type' => 'value', '#value' => TRUE); return confirm_form($form, t('Are you sure you want to delete the term %title?', array('%title' => $term->name)), @@ -365,8 +832,14 @@ t('Cancel')); } +/** + * Submit handler to delete a term after confirmation. + * + * @see taxonomy_term_confirm_delete(). + */ function taxonomy_term_confirm_delete_submit($form, &$form_state) { taxonomy_del_term($form_state['values']['tid']); + taxonomy_check_vocabulary_hierarchy($form['#vocabulary'], $form_state['values']); drupal_set_message(t('Deleted term %name.', array('%name' => $form_state['values']['name']))); watchdog('taxonomy', 'Deleted term %name.', array('%name' => $form_state['values']['name']), WATCHDOG_NOTICE); $form_state['redirect'] = 'admin/content/taxonomy'; @@ -394,6 +867,11 @@ t('Cancel')); } +/** + * Submit handler to delete a vocabulary after confirmation. + * + * @see taxonomy_vocabulary_confirm_delete(). + */ function taxonomy_vocabulary_confirm_delete_submit($form, &$form_state) { $status = taxonomy_del_vocabulary($form_state['values']['vid']); drupal_set_message(t('Deleted vocabulary %name.', array('%name' => $form_state['values']['name']))); @@ -401,3 +879,37 @@ $form_state['redirect'] = 'admin/content/taxonomy'; return; } + +/** + * Form builder to confirm reseting a vocabulary to alphabetical order. + * + * @ingroup forms + * @see taxonomy_vocabulary_confirm_reset_alphabetical_submit(). + */ +function taxonomy_vocabulary_confirm_reset_alphabetical(&$form_state, $vid) { + $vocabulary = taxonomy_vocabulary_load($vid); + + $form['type'] = array('#type' => 'value', '#value' => 'vocabulary'); + $form['vid'] = array('#type' => 'value', '#value' => $vid); + $form['name'] = array('#type' => 'value', '#value' => $vocabulary->name); + $form['reset_alphabetical'] = array('#type' => 'value', '#value' => TRUE); + return confirm_form($form, + t('Are you sure you want to reset the vocabulary %title to alphabetical order?', + array('%title' => $vocabulary->name)), + 'admin/content/taxonomy/'. $vid, + t('Reseting a vocabulary will discard all custom ordering and sort items alphabetically.'), + t('Reset to Alphabetical'), + t('Cancel')); +} + +/** + * Submit handler to reset a vocabulary to alphabetical order after confirmation. + * + * @see taxonomy_vocabulary_confirm_reset_alphabetical(). + */ +function taxonomy_vocabulary_confirm_reset_alphabetical_submit($form, &$form_state) { + db_query('UPDATE {term_data} t SET weight = 0 WHERE vid = %d', $form_state['values']['vid']); + drupal_set_message(t('Reset vocabulary %name to alphabetical order.', array('%name' => $form_state['values']['name']))); + watchdog('taxonomy', 'Reset vocabulary %name to alphabetical order.', array('%name' => $form_state['values']['name']), WATCHDOG_NOTICE); + $form_state['redirect'] = 'admin/content/taxonomy/'. $form_state['values']['vid']; +} Index: modules/taxonomy/taxonomy.css =================================================================== RCS file: modules/taxonomy/taxonomy.css diff -N modules/taxonomy/taxonomy.css --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/taxonomy/taxonomy.css 26 Nov 2007 17:20:56 -0000 @@ -0,0 +1,9 @@ +tr.taxonomy-term-preview { + background-color: #EEE; +} +tr.taxonomy-term-divider-top { + border-bottom: none; +} +tr.taxonomy-term-divider-bottom { + border-top: 1px dotted #CCC; +} \ No newline at end of file Index: modules/taxonomy/taxonomy.js =================================================================== RCS file: modules/taxonomy/taxonomy.js diff -N modules/taxonomy/taxonomy.js --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/taxonomy/taxonomy.js 26 Nov 2007 17:20:56 -0000 @@ -0,0 +1,36 @@ +// $Id $ + +/** + * Move a block in the blocks table from one region to another via select list. + * + * This behavior is dependent on the tableDrag behavior, since it uses the + * objects initialized in that behavior to update the row. + */ +Drupal.behaviors.termDrag = function(context) { + var table = $('#taxonomy', context); + var tableDrag = Drupal.tableDrag.taxonomy; // Get the blocks tableDrag object. + var rows = $('tr', table).size(); + + // When a row is swapped, keep previous and next page classes set. + tableDrag.row.prototype.onSwap = function(swappedRow) { + $('tr.taxonomy-term-preview', table).removeClass('taxonomy-term-preview'); + $('tr.taxonomy-term-divider-top', table).removeClass('taxonomy-term-divider-top'); + $('tr.taxonomy-term-divider-bottom', table).removeClass('taxonomy-term-divider-bottom'); + + if (Drupal.settings.taxonomy.backPeddle) { + for (var n = 0; n < Drupal.settings.taxonomy.backPeddle; n++) { + $(table[0].tBodies[0].rows[n]).addClass('taxonomy-term-preview'); + } + $(table[0].tBodies[0].rows[Drupal.settings.taxonomy.backPeddle - 1]).addClass('taxonomy-term-divider-top'); + $(table[0].tBodies[0].rows[Drupal.settings.taxonomy.backPeddle]).addClass('taxonomy-term-divider-bottom'); + } + + if (Drupal.settings.taxonomy.forwardPeddle) { + for (var n = rows - Drupal.settings.taxonomy.forwardPeddle - 1; n < rows - 1; n++) { + $(table[0].tBodies[0].rows[n]).addClass('taxonomy-term-preview'); + } + $(table[0].tBodies[0].rows[rows - Drupal.settings.taxonomy.forwardPeddle - 2]).addClass('taxonomy-term-divider-top'); + $(table[0].tBodies[0].rows[rows - Drupal.settings.taxonomy.forwardPeddle - 1]).addClass('taxonomy-term-divider-bottom'); + } + }; +}; Index: modules/taxonomy/taxonomy.module =================================================================== RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.module,v retrieving revision 1.398 diff -u -r1.398 taxonomy.module --- modules/taxonomy/taxonomy.module 23 Nov 2007 13:34:55 -0000 1.398 +++ modules/taxonomy/taxonomy.module 26 Nov 2007 17:39:59 -0000 @@ -24,6 +24,12 @@ 'taxonomy_term_page' => array( 'arguments' => array('tids' => array(), 'result' => NULL), ), + 'taxonomy_overview_vocabularies' => array( + 'arguments' => array('form' => array()), + ), + 'taxonomy_overview_terms' => array( + 'arguments' => array('form' => array()), + ), ); } @@ -106,7 +112,8 @@ $items['admin/content/taxonomy'] = array( 'title' => 'Taxonomy', 'description' => 'Manage tagging, categorization, and classification of your content.', - 'page callback' => 'taxonomy_overview_vocabularies', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('taxonomy_overview_vocabularies'), 'access arguments' => array('administer taxonomy'), 'file' => 'taxonomy.admin.inc', ); @@ -158,8 +165,8 @@ ); $items['admin/content/taxonomy/%taxonomy_vocabulary'] = array( 'title' => 'List terms', - 'page callback' => 'taxonomy_overview_terms', - 'page arguments' => array(3), + 'page callback' => 'drupal_get_form', + 'page arguments' => array('taxonomy_overview_terms', 3), 'access arguments' => array('administer taxonomy'), 'type' => MENU_CALLBACK, 'file' => 'taxonomy.admin.inc', @@ -242,6 +249,47 @@ } /** + * Dynamicly check and update the hierarachy flag of a vocabulary. + * + * Checks the current parents of all terms in a vocabulary and updates the + * vocabularies hierarchy setting to the lowest possible level. A hierarchy with + * no parents in any of its terms will be given a hierarchy of 0. If terms + * contain at most a single parent, the vocabulary will be given a hierarchy of + * 1. If any term contain multiple parents, the vocabulary will be given a + * hieararchy of 2. + * + * @param $vocabulary + * An array of the vocabulary structure. + * @param $changed_term + * An array of the term structure that was updated. + */ +function taxonomy_check_vocabulary_hierarchy($vocabulary, $changed_term) { + $tree = taxonomy_get_tree($vocabulary['vid']); + $hierarchy = 0; + foreach ($tree as $term) { + // Update the changed term with the new parent value before comparision. + if ($term->tid == $changed_term['tid']) { + $term = (object)$changed_term; + $term->parents = $term->parent; + } + // Check this term's parent count. + if (count($term->parents) > 1) { + $hierarchy = 2; + break; + } + elseif (count($term->parents) == 1 && 0 !== array_shift($term->parents)) { + $hierarchy = 1; + } + } + if ($hierarchy != $vocabulary['hierarchy']) { + $vocabulary['hierarchy'] = $hierarchy; + taxonomy_save_vocabulary($vocabulary); + } + + return $hierarchy; +} + +/** * Helper function for taxonomy_form_term_submit(). * * @param $form_state['values'] @@ -412,7 +460,7 @@ // If no node types are associated with a vocabulary, the LEFT JOIN will // return a NULL value for type. if (isset($voc->type)) { - $node_types[$voc->vid][] = $voc->type; + $node_types[$voc->vid][$voc->type] = $voc->type; unset($voc->type); $voc->nodes = $node_types[$voc->vid]; } @@ -928,7 +976,7 @@ $node_types = array(); while ($voc = db_fetch_object($result)) { if (!empty($voc->type)) { - $node_types[] = $voc->type; + $node_types[$voc->type] = $voc->type; } unset($voc->type); $voc->nodes = $node_types; @@ -1182,6 +1230,19 @@ return $output; case 'admin/content/taxonomy': return '

'. t("The taxonomy module allows you to categorize your content using both tags and administrator defined terms. It is a flexible tool for classifying content with many advanced features. To begin, create a 'Vocabulary' to hold one set of terms or tags. You can create one free-tagging vocabulary for everything, or seperate controlled vocabularies to define the various properties of your content, for example 'Countries' or 'Colours'.") .'

'; + case 'admin/content/taxonomy/%': + $vocabulary = taxonomy_vocabulary_load($arg[3]); + if ($vocabulary->tags) { + return '

'. t('%capital_name is a free-tagging vocabulary. To change the name or description of a term, click the edit link next to the term.', array('%capital_name' => drupal_ucfirst($vocabulary->name))) .'

'; + } + switch ($vocabulary->hierarchy) { + case 0: + return '

'. t('%capital_name is a flat vocabulary. You may organize the terms in the %name vocabulary by using the handles on the left side of the table. To change the name or description of a term, click the edit link next to the term.', array('%capital_name' => drupal_ucfirst($vocabulary->name), '%name' => $vocabulary->name)) .'

'; + case 1: + return '

'. t('%capital_name is a single hierarchy vocabulary. You may organize the terms in the %name vocabulary by using the handles on the left side of the table. To change the name or description of a term, click the edit link next to the term.', array('%capital_name' => drupal_ucfirst($vocabulary->name), '%name' => $vocabulary->name)) .'

'; + case 2: + return '

'. t('%capital_name is a multiple hierarchy vocabulary. To change the name or description of a term, click the edit link next to the term. Drag and drop of multiple hierarchies is not supported, but you can re-enable drag and drop support by editing each term to include only a single parent.', array('%capital_name' => drupal_ucfirst($vocabulary->name))) .'

'; + } case 'admin/content/taxonomy/add/vocabulary': return '

'. t('Define how your vocabulary will be presented to administrators and users, and which content types to categorize with it. Tags allows users to create terms when submitting posts by typing a comma separated list. Otherwise terms are chosen from a select list and can only be created by users with the "administer taxonomy" permission.') .'

'; } Index: themes/garland/fix-ie.css =================================================================== RCS file: /cvs/drupal/drupal/themes/garland/fix-ie.css,v retrieving revision 1.7 diff -u -r1.7 fix-ie.css --- themes/garland/fix-ie.css 9 Nov 2007 22:14:41 -0000 1.7 +++ themes/garland/fix-ie.css 26 Nov 2007 17:39:59 -0000 @@ -59,6 +59,10 @@ height: 1em; } +tr.taxonomy-term-preview { + filter: alpha(opacity=50); +} + #attach-hide label, #uploadprogress div.message { /* Fading elements in IE causes the text to bleed unless they have a background. */ background-color: #ffffff; Index: themes/garland/style.css =================================================================== RCS file: /cvs/drupal/drupal/themes/garland/style.css,v retrieving revision 1.29 diff -u -r1.29 style.css --- themes/garland/style.css 14 Nov 2007 09:49:30 -0000 1.29 +++ themes/garland/style.css 26 Nov 2007 17:39:59 -0000 @@ -976,6 +976,18 @@ color: #d3e7f4; } +tr.taxonomy-term-preview { + opacity: 0.5; +} + +tr.taxonomy-term-divider-top { + border-bottom: none; +} + +tr.taxonomy-term-divider-bottom { + border-top: 1px dotted #CCC; +} + /** * CSS support */ @@ -1002,8 +1014,9 @@ border-color: #c7f2c8; } -div.error { +div.error, tr.error { color: #c52020; + background-color: #FFCCCC; } .form-item input.error, .form-item textarea.error {