array( 'IE' => 'lte IE 7', '!IE' => FALSE ), 'preprocess' => FALSE, ); $list[$module_path . '/css/views-admin.theme.css'] = array(); // Add in any theme specific CSS files we have $themes = list_themes(); $theme_key = $GLOBALS['theme']; while ($theme_key) { // Try to find the admin css file for non-core themes. if (!in_array($theme_key, array('garland', 'seven', 'bartik'))) { $theme_path = drupal_get_path('theme', $theme_key); // First search in the css directory, then in the root folder of the theme. if (file_exists($theme_path . "/css/views-admin.$theme_key.css")) { $list[$theme_path . "/css/views-admin.$theme_key.css"] = array( 'group' => CSS_THEME, ); } else if (file_exists($theme_path . "/views-admin.$theme_key.css")) { $list[$theme_path . "/views-admin.$theme_key.css"] = array( 'group' => CSS_THEME, ); } } else { $list[$module_path . "/css/views-admin.$theme_key.css"] = array( 'group' => CSS_THEME, ); } $theme_key = isset($themes[$theme_key]->base_theme) ? $themes[$theme_key]->base_theme : ''; } // Views contains style overrides for the following modules $module_list = array('contextual', 'advanced_help', 'ctools'); foreach ($module_list as $module) { if (module_exists($module)) { $list[$module_path . '/css/views-admin.' . $module . '.css'] = array(); } } return $list; } /** * Adds standard Views administration CSS to the current page. */ function views_ui_add_admin_css() { foreach (views_ui_get_admin_css() as $file => $options) { drupal_add_css($file, $options); } } /** * Check to see if the advanced help module is installed, and if not put up * a message. * * Only call this function if the user is already in a position for this to * be useful. */ function views_ui_check_advanced_help() { if (!variable_get('views_ui_show_advanced_help_warning', TRUE)) { return; } if (!module_exists('advanced_help')) { $filename = db_query_range("SELECT filename FROM {system} WHERE type = 'module' AND name = 'advanced_help'", 0, 1) ->fetchField(); if ($filename && file_exists($filename)) { drupal_set_message(t('If you enable the advanced help module, Views will provide more and better help. You can disable this message at the Views settings page.', array('@modules' => url('admin/modules'),'@hide' => url('admin/structure/views/settings')))); } else { drupal_set_message(t('If you install the advanced help module from !href, Views will provide more and better help. You can disable this message at the Views settings page.', array('!href' => l('http://drupal.org/project/advanced_help', 'http://drupal.org/project/advanced_help'), '@hide' => url('admin/structure/views/settings')))); } } } /** * Returns the results of the live preview. */ function views_ui_preview($view, $display_id, $args = array()) { // When this function is invoked as a page callback, each Views argument is // passed separately. if (!is_array($args)) { $args = array_slice(func_get_args(), 2); } // Save $_GET['q'] so it can be restored before returning from this function. $q = $_GET['q']; // Determine where the query and performance statistics should be output. $show_query = variable_get('views_ui_show_sql_query', FALSE); $show_info = variable_get('views_ui_show_preview_information', FALSE); $show_location = variable_get('views_ui_show_sql_query_where', 'above'); $show_stats = variable_get('views_ui_show_performance_statistics', FALSE); if ($show_stats) { $show_stats = variable_get('views_ui_show_sql_query_where', 'above'); } $combined = $show_query && $show_stats; $rows = array('query' => array(), 'statistics' => array()); $output = ''; $errors = $view->validate(); if ($errors === TRUE) { $view->ajax = TRUE; $view->live_preview = TRUE; $view->views_ui_context = TRUE; // AJAX happens via $_POST but everything expects exposed data to // be in GET. Copy stuff but remove ajax-framework specific keys. // If we're clicking on links in a preview, though, we could actually // still have some in $_GET, so we use $_REQUEST to ensure we get it all. $exposed_input = $_REQUEST; foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', 'ajax_html_ids', 'ajax_page_state', 'form_id', 'form_build_id', 'form_token') as $key) { if (isset($exposed_input[$key])) { unset($exposed_input[$key]); } } $view->set_exposed_input($exposed_input); if (!$view->set_display($display_id)) { return t('Invalid display id @display', array('@display' => $display_id)); } $view->set_arguments($args); // Store the current view URL for later use: if ($view->display_handler->get_option('path')) { $path = $view->get_url(); } // Make view links come back to preview. $view->override_path = 'admin/structure/views/nojs/preview/' . $view->name . '/' . $display_id; // Also override $_GET['q'] so we get the pager. $original_path = current_path(); $_GET['q'] = $view->override_path; if ($args) { $_GET['q'] .= '/' . implode('/', $args); } // Suppress contextual links of entities within the result set during a // Preview. // @todo We'll want to add contextual links specific to editing the View, so // the suppression may need to be moved deeper into the Preview pipeline. views_ui_contextual_links_suppress_push(); $preview = $view->preview($display_id, $args); views_ui_contextual_links_suppress_pop(); // Reset variables. unset($view->override_path); $_GET['q'] = $original_path; // Prepare the query information and statistics to show either above or // below the view preview. if ($show_info || $show_query || $show_stats) { // Get information from the preview for display. if (!empty($view->build_info['query'])) { if ($show_query) { $query = $view->build_info['query']; // Only the sql default class has a method getArguments. $quoted = array(); if (get_class($view->query) == 'views_plugin_query_default') { $quoted = $query->getArguments(); $connection = Database::getConnection(); foreach ($quoted as $key => $val) { if (is_array($val)) { $quoted[$key] = implode(', ', array_map(array($connection, 'quote'), $val)); } else { $quoted[$key] = $connection->quote($val); } } } $rows['query'][] = array('' . t('Query') . '', '
' . check_plain(strtr($query, $quoted)) . '
'); if (!empty($view->additional_queries)) { $queries = '' . t('These queries were run during view rendering:') . ''; foreach ($view->additional_queries as $query) { if ($queries) { $queries .= "\n"; } $queries .= t('[@time ms]', array('@time' => intval($query[1] * 100000) / 100)) . ' ' . $query[0]; } $rows['query'][] = array('' . t('Other queries') . '', '
' . $queries . '
'); } } if ($show_info) { $rows['query'][] = array('' . t('Title') . '', filter_xss_admin($view->get_title())); if (isset($path)) { $path = l($path, $path); } else { $path = t('This display has no path.'); } $rows['query'][] = array('' . t('Path') . '', $path); } if ($show_stats) { $rows['statistics'][] = array('' . t('Query build time') . '', t('@time ms', array('@time' => intval($view->build_time * 100000) / 100))); $rows['statistics'][] = array('' . t('Query execute time') . '', t('@time ms', array('@time' => intval($view->execute_time * 100000) / 100))); $rows['statistics'][] = array('' . t('View render time') . '', t('@time ms', array('@time' => intval($view->render_time * 100000) / 100))); } drupal_alter('views_preview_info', $rows, $view); } else { // No query was run. Display that information in place of either the // query or the performance statistics, whichever comes first. if ($combined || ($show_location === 'above')) { $rows['query'] = array(array('' . t('Query') . '', t('No query was run'))); } else { $rows['statistics'] = array(array('' . t('Query') . '', t('No query was run'))); } } } } else { foreach ($errors as $error) { drupal_set_message($error, 'error'); } $preview = t('Unable to preview due to validation errors.'); } // Assemble the preview, the query info, and the query statistics in the // requested order. if ($show_location === 'above') { if ($combined) { $output .= '
' . theme('table', array('rows' => array_merge($rows['query'], $rows['statistics']))) . '
'; } else { $output .= '
' . theme('table', array('rows' => $rows['query'])) . '
'; } } elseif ($show_stats === 'above') { $output .= '
' . theme('table', array('rows' => $rows['statistics'])) . '
'; } $output .= $preview; if ($show_location === 'below') { if ($combined) { $output .= '
' . theme('table', array('rows' => array_merge($rows['query'], $rows['statistics']))) . '
'; } else { $output .= '
' . theme('table', array('rows' => $rows['query'])) . '
'; } } elseif ($show_stats === 'below') { $output .= '
' . theme('table', array('rows' => $rows['statistics'])) . '
'; } $_GET['q'] = $q; return $output; } /** * Page callback to add a new view. */ function views_ui_add_page() { views_ui_add_admin_css(); drupal_set_title(t('Add new view')); return drupal_get_form('views_ui_add_form'); } /** * Form builder for the "add new view" page. */ function views_ui_add_form($form, &$form_state) { ctools_include('dependent'); $form['#attached']['js'][] = drupal_get_path('module', 'views_ui') . '/js/views-admin.js'; $form['#attributes']['class'] = array('views-admin'); $form['human_name'] = array( '#type' => 'textfield', '#title' => t('View name'), '#required' => TRUE, '#size' => 32, '#default_value' => !empty($form_state['view']) ? $form_state['view']->human_name : '', '#maxlength' => 255, ); $form['name'] = array( '#type' => 'machine_name', '#maxlength' => 128, '#machine_name' => array( 'exists' => 'views_get_view', 'source' => array('human_name'), ), '#description' => t('A unique machine-readable name for this View. It must only contain lowercase letters, numbers, and underscores.'), ); $form['description_enable'] = array( '#type' => 'checkbox', '#title' => t('Description'), ); $form['description'] = array( '#type' => 'textfield', '#title' => t('Provide description'), '#title_display' => 'invisible', '#size' => 64, '#default_value' => !empty($form_state['view']) ? $form_state['view']->description : '', '#dependency' => array( 'edit-description-enable' => array(1), ), ); // Create a wrapper for the entire dynamic portion of the form. Everything // that can be updated by AJAX goes somewhere inside here. For example, this // is needed by "Show" dropdown (below); it changes the base table of the // view and therefore potentially requires all options on the form to be // dynamically updated. $form['displays'] = array(); // Create the part of the form that allows the user to select the basic // properties of what the view will display. $form['displays']['show'] = array( '#type' => 'fieldset', '#tree' => TRUE, '#attributes' => array('class' => array('container-inline')), ); // Create the "Show" dropdown, which allows the base table of the view to be // selected. $wizard_plugins = views_ui_get_wizards(); $options = array(); foreach ($wizard_plugins as $key => $wizard) { $options[$key] = $wizard['title']; } $form['displays']['show']['wizard_key'] = array( '#type' => 'select', '#title' => t('Show'), '#options' => $options, ); $show_form = &$form['displays']['show']; $show_form['wizard_key']['#default_value'] = views_ui_get_selected($form_state, array('show', 'wizard_key'), 'node', $show_form['wizard_key']); // Changing this dropdown updates the entire content of $form['displays'] via // AJAX. views_ui_add_ajax_trigger($show_form, 'wizard_key', array('displays')); // Build the rest of the form based on the currently selected wizard plugin. $wizard_key = $show_form['wizard_key']['#default_value']; $get_instance = $wizard_plugins[$wizard_key]['get_instance']; $wizard_instance = $get_instance($wizard_plugins[$wizard_key]); $form = $wizard_instance->build_form($form, $form_state); $form['save'] = array( '#type' => 'submit', '#value' => t('Save & exit'), '#validate' => array('views_ui_wizard_form_validate'), '#submit' => array('views_ui_add_form_save_submit'), ); $form['continue'] = array( '#type' => 'submit', '#value' => t('Continue & edit'), '#validate' => array('views_ui_wizard_form_validate'), '#submit' => array('views_ui_add_form_store_edit_submit'), '#process' => array_merge(array('views_ui_default_button'), element_info_property('submit', '#process', array())), ); $form['cancel'] = array( '#type' => 'submit', '#value' => t('Cancel'), '#submit' => array('views_ui_add_form_cancel_submit'), '#limit_validation_errors' => array(), ); return $form; } /** * Helper form element validator: integer. * * The problem with this is that the function is private so it's not guaranteed * that it might not be renamed/changed. In the future field.module or something else * should provide a public validate function. * * @see _element_validate_integer_positive() */ function views_element_validate_integer($element, &$form_state) { $value = $element['#value']; if ($value !== '' && (!is_numeric($value) || intval($value) != $value || abs($value) != $value)) { form_error($element, t('%name must be a positive integer.', array('%name' => $element['#title']))); } } /** * Gets the current value of a #select element, from within a form constructor function. * * This function is intended for use in highly dynamic forms (in particular the * add view wizard) which are rebuilt in different ways depending on which * triggering element (AJAX or otherwise) was most recently fired. For example, * sometimes it is necessary to decide how to build one dynamic form element * based on the value of a different dynamic form element that may not have * even been present on the form the last time it was submitted. This function * takes care of resolving those conflicts and gives you the proper current * value of the requested #select element. * * By necessity, this function sometimes uses non-validated user input from * $form_state['input'] in making its determination. Although it performs some * minor validation of its own, it is not complete. The intention is that the * return value of this function should only be used to help decide how to * build the current form the next time it is reloaded, not to be saved as if * it had gone through the normal, final form validation process. Do NOT use * the results of this function for any other purpose besides deciding how to * build the next version of the form. * * @param $form_state * The standard associative array containing the current state of the form. * @param $parents * An array of parent keys that point to the part of the submitted form * values that are expected to contain the element's value (in the case where * this form element was actually submitted). In a simple case (assuming * #tree is TRUE throughout the form), if the select element is located in * $form['wrapper']['select'], so that the submitted form values would * normally be found in $form_state['values']['wrapper']['select'], you would * pass array('wrapper', 'select') for this parameter. * @param $default_value * The default value to return if the #select element does not currently have * a proper value set based on the submitted input. * @param $element * An array representing the current version of the #select element within * the form. * * @return * The current value of the #select element. A common use for this is to feed * it back into $element['#default_value'] so that the form will be rendered * with the correct value selected. */ function views_ui_get_selected($form_state, $parents, $default_value, $element) { // For now, don't trust this to work on anything but a #select element. if (!isset($element['#type']) || $element['#type'] != 'select' || !isset($element['#options'])) { return $default_value; } // If there is a user-submitted value for this element that matches one of // the currently available options attached to it, use that. We need to check // $form_state['input'] rather than $form_state['values'] here because the // triggering element often has the #limit_validation_errors property set to // prevent unwanted errors elsewhere on the form. This means that the // $form_state['values'] array won't be complete. We could make it complete // by adding each required part of the form to the #limit_validation_errors // property individually as the form is being built, but this is difficult to // do for a highly dynamic and extensible form. This method is much simpler. if (!empty($form_state['input'])) { $key_exists = NULL; $submitted = drupal_array_get_nested_value($form_state['input'], $parents, $key_exists); // Check that the user-submitted value is one of the allowed options before // returning it. This is not a substitute for actual form validation; // rather it is necessary because, for example, the same select element // might have #options A, B, and C under one set of conditions but #options // D, E, F under a different set of conditions. So the form submission // might have occurred with option A selected, but when the form is rebuilt // option A is no longer one of the choices. In that case, we don't want to // use the value that was submitted anymore but rather fall back to the // default value. if ($key_exists && in_array($submitted, array_keys($element['#options']))) { return $submitted; } } // Fall back on returning the default value if nothing was returned above. return $default_value; } /** * Converts a form element in the add view wizard to be AJAX-enabled. * * This function takes a form element and adds AJAX behaviors to it such that * changing it triggers another part of the form to update automatically. It * also adds a submit button to the form that appears next to the triggering * element and that duplicates its functionality for users who do not have * JavaScript enabled (the button is automatically hidden for users who do have * JavaScript). * * To use this function, call it directly from your form builder function * immediately after you have defined the form element that will serve as the * JavaScript trigger. Calling it elsewhere (such as in hook_form_alter()) may * mean that the non-JavaScript fallback button does not appear in the correct * place in the form. * * @param $wrapping_element * The element whose child will server as the AJAX trigger. For example, if * $form['some_wrapper']['triggering_element'] represents the element which * will trigger the AJAX behavior, you would pass $form['some_wrapper'] for * this parameter. * @param $trigger_key * The key within the wrapping element that identifies which of its children * serves as the AJAX trigger. In the above example, you would pass * 'triggering_element' for this parameter. * @param $refresh_parents * An array of parent keys that point to the part of the form that will be * refreshed by AJAX. For example, if triggering the AJAX behavior should * cause $form['dynamic_content']['section'] to be refreshed, you would pass * array('dynamic_content', 'section') for this parameter. */ function views_ui_add_ajax_trigger(&$wrapping_element, $trigger_key, $refresh_parents) { $seen_ids = &drupal_static(__FUNCTION__ . ':seen_ids', array()); $seen_buttons = &drupal_static(__FUNCTION__ . ':seen_buttons', array()); // Add the AJAX behavior to the triggering element. $triggering_element = &$wrapping_element[$trigger_key]; $triggering_element['#ajax']['callback'] = 'views_ui_ajax_update_form'; // We do not use drupal_html_id() to get an ID for the AJAX wrapper, because // it remembers IDs across AJAX requests (and won't reuse them), but in our // case we need to use the same ID from request to request so that the // wrapper can be recognized by the AJAX system and its content can be // dynamically updated. So instead, we will keep track of duplicate IDs // (within a single request) on our own, later in this function. $triggering_element['#ajax']['wrapper'] = 'edit-view-' . implode('-', $refresh_parents) . '-wrapper'; // Add a submit button for users who do not have JavaScript enabled. It // should be displayed next to the triggering element on the form. $button_key = $trigger_key . '_trigger_update'; $wrapping_element[$button_key] = array( '#type' => 'submit', // Hide this button when JavaScript is enabled. '#attributes' => array('class' => array('js-hide')), '#submit' => array('views_ui_nojs_submit'), // Add a process function to limit this button's validation errors to the // triggering element only. We have to do this in #process since until the // form API has added the #parents property to the triggering element for // us, we don't have any (easy) way to find out where its submitted values // will eventually appear in $form_state['values']. '#process' => array_merge(array('views_ui_add_limited_validation'), element_info_property('submit', '#process', array())), // Add an after-build function that inserts a wrapper around the region of // the form that needs to be refreshed by AJAX (so that the AJAX system can // detect and dynamically update it). This is done in #after_build because // it's a convenient place where we have automatic access to the complete // form array, but also to minimize the chance that the HTML we add will // get clobbered by code that runs after we have added it. '#after_build' => array_merge(element_info_property('submit', '#after_build', array()), array('views_ui_add_ajax_wrapper')), ); // Copy #weight and #access from the triggering element to the button, so // that the two elements will be displayed together. foreach (array('#weight', '#access') as $property) { if (isset($triggering_element[$property])) { $wrapping_element[$button_key][$property] = $triggering_element[$property]; } } // For easiest integration with the form API and the testing framework, we // always give the button a unique #value, rather than playing around with // #name. $button_title = !empty($triggering_element['#title']) ? $triggering_element['#title'] : $trigger_key; if (empty($seen_buttons[$button_title])) { $wrapping_element[$button_key]['#value'] = t('Update "@title" choice', array( '@title' => $button_title, )); $seen_buttons[$button_title] = 1; } else { $wrapping_element[$button_key]['#value'] = t('Update "@title" choice (@number)', array( '@title' => $button_title, '@number' => ++$seen_buttons[$button_title], )); } // Attach custom data to the triggering element and submit button, so we can // use it in both the process function and AJAX callback. $ajax_data = array( 'wrapper' => $triggering_element['#ajax']['wrapper'], 'trigger_key' => $trigger_key, 'refresh_parents' => $refresh_parents, // Keep track of duplicate wrappers so we don't add the same wrapper to the // page more than once. 'duplicate_wrapper' => !empty($seen_ids[$triggering_element['#ajax']['wrapper']]), ); $seen_ids[$triggering_element['#ajax']['wrapper']] = TRUE; $triggering_element['#views_ui_ajax_data'] = $ajax_data; $wrapping_element[$button_key]['#views_ui_ajax_data'] = $ajax_data; } /** * Processes a non-JavaScript fallback submit button to limit its validation errors. */ function views_ui_add_limited_validation($element, &$form_state) { // Retrieve the AJAX triggering element so we can determine its parents. (We // know it's at the same level of the complete form array as the submit // button, so all we have to do to find it is swap out the submit button's // last array parent.) $array_parents = $element['#array_parents']; array_pop($array_parents); $array_parents[] = $element['#views_ui_ajax_data']['trigger_key']; $ajax_triggering_element = drupal_array_get_nested_value($form_state['complete form'], $array_parents); // Limit this button's validation to the AJAX triggering element, so it can // update the form for that change without requiring that the rest of the // form be filled out properly yet. $element['#limit_validation_errors'] = array($ajax_triggering_element['#parents']); // If we are in the process of a form submission and this is the button that // was clicked, the form API workflow in form_builder() will have already // copied it to $form_state['triggering_element'] before our #process // function is run. So we need to make the same modifications in $form_state // as we did to the element itself, to ensure that #limit_validation_errors // will actually be set in the correct place. if (!empty($form_state['triggering_element'])) { $clicked_button = &$form_state['triggering_element']; if ($clicked_button['#name'] == $element['#name'] && $clicked_button['#value'] == $element['#value']) { $clicked_button['#limit_validation_errors'] = $element['#limit_validation_errors']; } } return $element; } /** * After-build function that adds a wrapper to a form region (for AJAX refreshes). * * This function inserts a wrapper around the region of the form that needs to * be refreshed by AJAX, based on information stored in the corresponding * submit button form element. */ function views_ui_add_ajax_wrapper($element, &$form_state) { // Don't add the wrapper
if the same one was already inserted on this // form. if (empty($element['#views_ui_ajax_data']['duplicate_wrapper'])) { // Find the region of the complete form that needs to be refreshed by AJAX. // This was earlier stored in a property on the element. $complete_form = &$form_state['complete form']; $refresh_parents = $element['#views_ui_ajax_data']['refresh_parents']; $refresh_element = drupal_array_get_nested_value($complete_form, $refresh_parents); // The HTML ID that AJAX expects was also stored in a property on the // element, so use that information to insert the wrapper
here. $id = $element['#views_ui_ajax_data']['wrapper']; $refresh_element += array( '#prefix' => '', '#suffix' => '', ); $refresh_element['#prefix'] = '
' . $refresh_element['#prefix']; $refresh_element['#suffix'] .= '
'; // Copy the element that needs to be refreshed back into the form, with our // modifications to it. drupal_array_set_nested_value($complete_form, $refresh_parents, $refresh_element); } return $element; } /** * Updates a part of the add view form via AJAX. * * @return * The part of the form that has changed. */ function views_ui_ajax_update_form($form, $form_state) { // The region that needs to be updated was stored in a property of the // triggering element by views_ui_add_ajax_trigger(), so all we have to do is // retrieve that here. return drupal_array_get_nested_value($form, $form_state['triggering_element']['#views_ui_ajax_data']['refresh_parents']); } /** * Non-Javascript fallback for updating the add view form. */ function views_ui_nojs_submit($form, &$form_state) { $form_state['rebuild'] = TRUE; } /** * Validate the add view form. */ function views_ui_wizard_form_validate($form, &$form_state) { $wizard = views_ui_get_wizard($form_state['values']['show']['wizard_key']); $form_state['wizard'] = $wizard; $get_instance = $wizard['get_instance']; $form_state['wizard_instance'] = $get_instance($wizard); $errors = $form_state['wizard_instance']->validate($form, $form_state); foreach ($errors as $name => $message) { form_set_error($name, $message); } } /** * Process the add view form, 'save'. */ function views_ui_add_form_save_submit($form, &$form_state) { try { $view = $form_state['wizard_instance']->create_view($form, $form_state); } catch (ViewsWizardException $e) { drupal_set_message($e->getMessage(), 'error'); $form_state['redirect'] = 'admin/structure/views'; } $view->save(); $form_state['redirect'] = 'admin/structure/views'; if (!empty($view->display['page'])) { $display = $view->display['page']; if ($display->handler->has_path()) { $one_path = $display->handler->get_option('path'); if (strpos($one_path, '%') === FALSE) { $form_state['redirect'] = $one_path; // PATH TO THE VIEW IF IT HAS ONE return; } } } drupal_set_message(t('Your view was saved. You may edit it from the list below.')); } /** * Process the add view form, 'continue'. */ function views_ui_add_form_store_edit_submit($form, &$form_state) { try { $view = $form_state['wizard_instance']->create_view($form, $form_state); } catch (ViewsWizardException $e) { drupal_set_message($e->getMessage(), 'error'); $form_state['redirect'] = 'admin/structure/views'; } // Just cache it temporarily to edit it. views_ui_cache_set($view); // If there is a destination query, ensure we still redirect the user to the // edit view page, and then redirect the user to the destination. $destination = array(); if (isset($_GET['destination'])) { $destination = drupal_get_destination(); unset($_GET['destination']); } $form_state['redirect'] = array('admin/structure/views/view/' . $view->name, array('query' => $destination)); } /** * Cancel the add view form. */ function views_ui_add_form_cancel_submit($form, &$form_state) { $form_state['redirect'] = 'admin/structure/views'; } /** * Form element validation handler for a taxonomy autocomplete field. * * This allows a taxonomy autocomplete field to be validated outside the * standard Field API workflow, without passing in a complete field widget. * Instead, all that is required is that $element['#field_name'] contain the * name of the taxonomy autocomplete field that is being validated. * * This function is currently not used for validation directly, although it * could be. Instead, it is only used to store the term IDs and vocabulary name * in the element value, based on the tags that the user typed in. * * @see taxonomy_autocomplete_validate() */ function views_ui_taxonomy_autocomplete_validate($element, &$form_state) { $value = array(); if ($tags = $element['#value']) { // Get the machine names of the vocabularies we will search, keyed by the // vocabulary IDs. $field = field_info_field($element['#field_name']); $vocabularies = array(); if (!empty($field['settings']['allowed_values'])) { foreach ($field['settings']['allowed_values'] as $tree) { if ($vocabulary = taxonomy_vocabulary_machine_name_load($tree['vocabulary'])) { $vocabularies[$vocabulary->vid] = $tree['vocabulary']; } } } // Store the term ID of each (valid) tag that the user typed. $typed_terms = drupal_explode_tags($tags); foreach ($typed_terms as $typed_term) { if ($terms = taxonomy_term_load_multiple(array(), array('name' => trim($typed_term), 'vid' => array_keys($vocabularies)))) { $term = array_pop($terms); $value['tids'][] = $term->tid; } } // Store the term IDs along with the name of the vocabulary. Currently // Views (as well as the Field UI) assumes that there will only be one // vocabulary, although technically the API allows there to be more than // one. if (!empty($value['tids'])) { $value['tids'] = array_unique($value['tids']); $value['vocabulary'] = array_pop($vocabularies); } } form_set_value($element, $value, $form_state); } /** * Theme function; returns basic administrative information about a view. * * TODO: template + preprocess */ function theme_views_ui_view_info($variables) { $view = $variables['view']; $title = $view->get_human_name(); $displays = _views_ui_get_displays_list($view); $displays = empty($displays) ? t('None') : format_plural(count($displays), 'Display', 'Displays') . ': ' . '' . implode(', ', $displays) . ''; switch ($view->type) { case t('Default'): default: $type = t('In code'); break; case t('Normal'): $type = t('In database'); break; case t('Overridden'): $type = t('Database overriding code'); } $output = ''; $output .= '
' . check_plain($title) . "
\n"; $output .= '
' . $displays . "
\n"; $output .= '
' . $type . "
\n"; $output .= '
' . t('Type') . ': ' . check_plain($variables['base']). "
\n"; return $output; } /** * Page to delete a view. */ function views_ui_break_lock_confirm($form, &$form_state, $view) { $form_state['view'] = &$view; $form = array(); if (empty($view->locked)) { $form['message']['#markup'] = t('There is no lock on view %name to break.', array('%name' => $view->name)); return $form; } $cancel = 'admin/structure/views/view/' . $view->name . '/edit'; if (!empty($_REQUEST['cancel'])) { $cancel = $_REQUEST['cancel']; } $account = user_load($view->locked->uid); return confirm_form($form, t('Are you sure you want to break the lock on view %name?', array('%name' => $view->name)), $cancel, t('By breaking this lock, any unsaved changes made by !user will be lost!', array('!user' => theme('username', array('account' => $account)))), t('Break lock'), t('Cancel')); } /** * Submit handler to break_lock a view. */ function views_ui_break_lock_confirm_submit(&$form, &$form_state) { ctools_object_cache_clear_all('view', $form_state['view']->name); $form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit'; drupal_set_message(t('The lock has been broken and you may now edit this view.')); } /** * Helper function to return the used display_id for the edit page * * This function handles access to the display. */ function views_ui_edit_page_display($view, $display_id) { // Determine the displays available for editing. if ($tabs = views_ui_edit_page_display_tabs($view, $display_id)) { // If a display isn't specified, use the first one. if (empty($display_id)) { foreach ($tabs as $id => $tab) { if (!isset($tab['#access']) || $tab['#access']) { $display_id = $id; break; } } } // If a display is specified, but we don't have access to it, return // an access denied page. if ($display_id && (!isset($tabs[$display_id]) || (isset($tabs[$display_id]['#access']) && !$tabs[$display_id]['#access']))) { return MENU_ACCESS_DENIED; } return $display_id; } elseif ($display_id) { return MENU_ACCESS_DENIED; } else { $display_id = NULL; } return $display_id; } /** * Page callback for the Edit View page. */ function views_ui_edit_page($view, $display_id = NULL) { $display_id = views_ui_edit_page_display($view, $display_id); if (!in_array($display_id, array(MENU_ACCESS_DENIED, MENU_NOT_FOUND))) { $build = array(); $build['edit_form'] = drupal_get_form('views_ui_edit_form', $view, $display_id); $build['preview'] = views_ui_build_preview($view, $display_id, FALSE); } else { $build = $display_id; } return $build; } function views_ui_build_preview($view, $display_id, $render = TRUE) { if (isset($_POST['ajax_html_ids'])) { unset($_POST['ajax_html_ids']); } $build = array( '#theme_wrappers' => array('container'), '#attributes' => array('id' => 'views-preview-wrapper', 'class' => 'views-admin clearfix'), ); $form_state = array('build_info' => array('args' => array($view, $display_id))); $build['controls'] = drupal_build_form('views_ui_preview_form', $form_state); $args = array(); if (!empty($form_state['values']['view_args'])) { $args = explode('/', $form_state['values']['view_args']); } $build['preview'] = array( '#theme_wrappers' => array('container'), '#attributes' => array('id' => 'views-live-preview'), '#markup' => $render ? views_ui_preview($view->clone_view(), $display_id, $args) : '', ); return $build; } /** * Form builder callback for editing a View. * * @todo Remove as many #prefix/#suffix lines as possible. Use #theme_wrappers * instead. * * @todo Rename to views_ui_edit_view_form(). See that function for the "old" * version. * * @see views_ui_ajax_get_form() */ function views_ui_edit_form($form, &$form_state, $view, $display_id = NULL) { // Do not allow the form to be cached, because $form_state['view'] can become // stale between page requests. // See views_ui_ajax_get_form() for how this affects #ajax. // @todo To remove this and allow the form to be cacheable: // - Change $form_state['view'] to $form_state['temporary']['view']. // - Add a #process function to initialize $form_state['temporary']['view'] // on cached form submissions. // - Update ctools_include() to support cached forms, or else use // form_load_include(). $form_state['no_cache'] = TRUE; if ($display_id) { if (!$view->set_display($display_id)) { $form['#markup'] = t('Invalid display id @display', array('@display' => $display_id)); return $form; } $view->fix_missing_relationships(); } ctools_include('dependent'); $form['#attached']['js'][] = ctools_attach_js('dependent'); $form['#attached']['js'][] = ctools_attach_js('collapsible-div'); $form['#tree'] = TRUE; // @todo When more functionality is added to this form, cloning here may be // too soon. But some of what we do with $view later in this function // results in making it unserializable due to PDO limitations. $form_state['view'] = clone($view); $form['#attached']['library'][] = array('system', 'ui.tabs'); $form['#attached']['library'][] = array('system', 'ui.dialog'); $form['#attached']['library'][] = array('system', 'drupal.ajax'); $form['#attached']['library'][] = array('system', 'jquery.form'); // TODO: This should be getting added to the page when an ajax popup calls // for it, instead of having to add it manually here. $form['#attached']['js'][] = 'misc/tabledrag.js'; $form['#attached']['css'] = views_ui_get_admin_css(); $module_path = drupal_get_path('module', 'views_ui'); $form['#attached']['js'][] = $module_path . '/js/views-admin.js'; $form['#attached']['js'][] = array( 'data' => array('views' => array('ajax' => array( 'id' => '#views-ajax-body', 'title' => '#views-ajax-title', 'popup' => '#views-ajax-popup', 'defaultForm' => views_ui_get_default_ajax_message(), ))), 'type' => 'setting', ); $form += array( '#prefix' => '', '#suffix' => '', ); $form['#prefix'] = $form['#prefix'] . '
'; $form['#suffix'] = '
' . $form['#suffix']; $form['#attributes']['class'] = array('form-edit'); if (isset($view->locked) && is_object($view->locked)) { $form['locked'] = array( '#theme_wrappers' => array('container'), '#attributes' => array('class' => array('view-locked', 'messages', 'warning')), '#markup' => t('This view is being edited by user !user, and is therefore locked from editing by others. This lock is !age old. Click here to break this lock.', array('!user' => theme('username', array('account' => user_load($view->locked->uid))), '!age' => format_interval(REQUEST_TIME - $view->locked->updated), '!break' => url('admin/structure/views/view/' . $view->name . '/break-lock'))), ); } if (isset($view->vid) && $view->vid == 'new') { $message = t('* All changes are stored temporarily. Click Save to make your changes permanent. Click Cancel to discard the view.'); } else { $message = t('* All changes are stored temporarily. Click Save to make your changes permanent. Click Cancel to discard your changes.'); } $form['changed'] = array( '#theme_wrappers' => array('container'), '#attributes' => array('class' => array('view-changed', 'messages', 'warning')), '#markup' => $message, ); if (empty($view->changed)) { $form['changed']['#attributes']['class'][] = 'js-hide'; } $form['help_text'] = array( '#prefix' => '
', '#suffix' => '
', '#markup' => t('Modify the display(s) of your view below or add new displays.'), ); $form['actions'] = array( '#type' => 'actions', '#weight' => 0, ); if (empty($view->changed)) { $form['actions']['#attributes'] = array( 'class' => array( 'js-hide', ), ); } $form['actions']['save'] = array( '#type' => 'submit', '#value' => t('Save'), // Taken from the "old" UI. @TODO: Review and rename. '#validate' => array('views_ui_edit_view_form_validate'), '#submit' => array('views_ui_edit_view_form_submit'), ); $form['actions']['cancel'] = array( '#type' => 'submit', '#value' => t('Cancel'), '#submit' => array('views_ui_edit_view_form_cancel'), ); $form['displays'] = array( '#prefix' => '

' . t('Displays') . '

' . "\n" . '
', '#suffix' => '
', ); $form['displays']['top'] = views_ui_render_display_top($view, $display_id); // The rest requires a display to be selected. if ($display_id) { $form_state['display_id'] = $display_id; // The part of the page where editing will take place. // This element is the ctools collapsible-div container for the display edit elements. $form['displays']['settings'] = array( '#theme_wrappers' => array('container'), '#attributes' => array( 'class' => array( 'views-display-settings', 'box-margin', 'ctools-collapsible-container', ), ), '#id' => 'edit-display-settings', ); $display_title = views_ui_get_display_label($view, $display_id, FALSE); // Add a handle for the ctools collapsible-div. The handle is the title of the display $form['displays']['settings']['tab_title']['#markup'] = '

' . t('@display_title details', array('@display_title' => ucwords($display_title))) . '

'; // Add a text that the display is disabled. if (!empty($view->display[$display_id]->handler)) { $enabled = $view->display[$display_id]->handler->get_option('enabled'); if (empty($enabled)) { $form['displays']['settings']['disabled']['#markup'] = t('This display is disabled.'); } } // The ctools collapsible-div content $form['displays']['settings']['settings_content']= array( '#theme_wrappers' => array('container'), '#id' => 'edit-display-settings-content', '#attributes' => array( 'class' => array( 'ctools-collapsible-content', ), ), ); // Add the edit display content $form['displays']['settings']['settings_content']['tab_content'] = views_ui_get_display_tab($view, $display_id); $form['displays']['settings']['settings_content']['tab_content']['#theme_wrappers'] = array('container'); $form['displays']['settings']['settings_content']['tab_content']['#attributes'] = array('class' => array('views-display-tab')); $form['displays']['settings']['settings_content']['tab_content']['#id'] = 'views-tab-' . $display_id; // Mark deleted displays as such. if (!empty($view->display[$display_id]->deleted)) { $form['displays']['settings']['settings_content']['tab_content']['#attributes']['class'][] = 'views-display-deleted'; } // Mark disabled displays as such. if (empty($enabled)) { $form['displays']['settings']['settings_content']['tab_content']['#attributes']['class'][] = 'views-display-disabled'; } // The content of the popup dialog. $form['ajax-area'] = array( '#theme_wrappers' => array('container'), '#id' => 'views-ajax-popup', ); $form['ajax-area']['ajax-title'] = array( '#markup' => '

', ); $form['ajax-area']['ajax-body'] = array( '#theme_wrappers' => array('container'), '#id' => 'views-ajax-body', '#markup' => views_ui_get_default_ajax_message(), ); } // If relationships had to be fixed, we want to get that into the cache // so that edits work properly, and to try to get the user to save it // so that it's not using weird fixed up relationships. if (!empty($view->relationships_changed) && empty($_POST)) { drupal_set_message(t('This view has been automatically updated to fix missing relationships. While this View should continue to work, you should verify that the automatic updates are correct and save this view.')); views_ui_cache_set($view); } return $form; } /** * Provide the preview formulas and the preview output, too. */ function views_ui_preview_form($form, &$form_state, $view, $display_id = 'default') { $form_state['no_cache'] = TRUE; $form_state['view'] = $view; $form['#attributes'] = array('class' => array('clearfix',)); // Add a checkbox controlling whether or not this display auto-previews. $form['live_preview'] = array( '#type' => 'checkbox', '#id' => 'edit-displays-live-preview', '#title' => t('Auto preview'), '#default_value' => variable_get('views_ui_always_live_preview', TRUE), ); // Add the arguments textfield $form['view_args'] = array( '#type' => 'textfield', '#title' => t('Preview with contextual filters:'), '#description' => t('Separate contextual filter values with a "/". For example, %example.', array('%example' => '40/12/10')), '#id' => 'preview-args', // '#attributes' => array('class' => array('ctools-auto-submit')), ); // Add the preview button $form['button'] = array( '#type' => 'submit', '#value' => t('Update preview'), '#attributes' => array('class' => array('arguments-preview', 'ctools-auto-submit-click')), '#pre_render' => array('ctools_dependent_pre_render'), '#prefix' => '
', '#suffix' => '
', '#id' => 'preview-submit', '#submit' => array('views_ui_edit_form_submit_preview'), '#ajax' => array( 'path' => 'admin/structure/views/view/' . $view->name . '/preview/' . $display_id . '/ajax', 'wrapper' => 'views-preview-wrapper', 'event' => 'click', 'progress' => array('type' => 'throbber'), 'method' => 'replace', ), // Make ENTER in arguments textfield (and other controls) submit the form // as this button, not the Save button. // @todo This only works for JS users. To make this work for nojs users, // we may need to split Preview into a separate form. '#process' => array_merge(array('views_ui_default_button'), element_info_property('submit', '#process', array())), ); $form['#action'] = url('admin/structure/views/view/' . $view->name .'/preview/' . $display_id); return $form; } /** * Render the top of the display so it can be updated during ajax operations. */ function views_ui_render_display_top($view, $display_id) { $element['#theme_wrappers'] = array('views_container'); $element['#attributes']['class'] = array('views-display-top', 'clearfix'); $element['#attributes']['id'] = array('views-display-top'); // Extra actions for the display $element['extra_actions'] = array( '#theme' => 'links__ctools_dropbutton', '#attributes' => array( 'id' => 'views-display-extra-actions', 'class' => array( 'horizontal', 'right', 'links', 'actions', ), ), '#links' => array( 'edit-details' => array( 'title' => t('edit view name/description'), 'href' => "admin/structure/views/nojs/edit-details/$view->name", 'attributes' => array('class' => array('views-ajax-link')), ), 'analyze' => array( 'title' => t('analyze view'), 'href' => "admin/structure/views/nojs/analyze/$view->name/$display_id", 'attributes' => array('class' => array('views-ajax-link')), ), 'clone' => array( 'title' => t('clone view'), 'href' => "admin/structure/views/view/$view->name/clone", ), 'export' => array( 'title' => t('export view'), 'href' => "admin/structure/views/view/$view->name/export", ), 'reorder' => array( 'title' => t('reorder displays'), 'href' => "admin/structure/views/nojs/reorder-displays/$view->name/$display_id", 'attributes' => array('class' => array('views-ajax-link')), ), ), ); // Let other modules add additional links here. drupal_alter('views_ui_display_top_links', $element['extra_actions']['#links'], $view, $display_id); if (isset($view->type) && $view->type != t('Default')) { if ($view->type == t('Overridden')) { $element['extra_actions']['#links']['revert'] = array( 'title' => t('revert view'), 'href' => "admin/structure/views/view/$view->name/revert", 'query' => array('destination' => "admin/structure/views/view/$view->name"), ); } else { $element['extra_actions']['#links']['delete'] = array( 'title' => t('delete view'), 'href' => "admin/structure/views/view/$view->name/delete", ); } } // Determine the displays available for editing. if ($tabs = views_ui_edit_page_display_tabs($view, $display_id)) { if ($display_id) { $tabs[$display_id]['#active'] = TRUE; } $tabs['#prefix'] = '

' . t('Secondary tabs') . '

'; $element['tabs'] = $tabs; } // Buttons for adding a new display. foreach (views_fetch_plugin_names('display', NULL, array($view->base_table)) as $type => $label) { $element['add_display'][$type] = array( '#type' => 'submit', '#value' => t('Add !display', array('!display' => $label)), '#limit_validation_errors' => array(), '#submit' => array('views_ui_edit_form_submit_add_display', 'views_ui_edit_form_submit_delay_destination'), '#attributes' => array('class' => array('add-display')), // Allow JavaScript to remove the 'Add ' prefix from the button label when // placing the button in a "Add" dropdown menu. '#process' => array_merge(array('views_ui_form_button_was_clicked'), element_info_property('submit', '#process', array())), '#values' => array(t('Add !display', array('!display' => $label)), $label), ); } return $element; } function views_ui_get_default_ajax_message() { return '
' . t("Click on an item to edit that item's details.") . '
'; } /** * Submit handler to add a display to a view. */ function views_ui_edit_form_submit_add_display($form, &$form_state) { $view = $form_state['view']; // Create the new display. $parents = $form_state['triggering_element']['#parents']; $display_type = array_pop($parents); $display_id = $view->add_display($display_type); views_ui_cache_set($view); // Redirect to the new display's edit page. $form_state['redirect'] = 'admin/structure/views/view/' . $view->name . '/edit/' . $display_id; } /** * Submit handler to duplicate a display for a view. */ function views_ui_edit_form_submit_duplicate_display($form, &$form_state) { $view = $form_state['view']; $display_id = $form_state['display_id']; // Create the new display. $display = $view->display[$display_id]; $new_display_id = $view->add_display($display->display_plugin); $view->display[$new_display_id] = clone $display; $view->display[$new_display_id]->id = $new_display_id; // By setting the current display the changed marker will appear on the new // display. $view->current_display = $new_display_id; views_ui_cache_set($view); // Redirect to the new display's edit page. $form_state['redirect'] = 'admin/structure/views/view/' . $view->name . '/edit/' . $new_display_id; } /** * Submit handler to delete a display from a view. */ function views_ui_edit_form_submit_delete_display($form, &$form_state) { $view = $form_state['view']; $display_id = $form_state['display_id']; // Mark the display for deletion. $view->display[$display_id]->deleted = TRUE; views_ui_cache_set($view); // Redirect to the top-level edit page. The first remaining display will // become the active display. $form_state['redirect'] = 'admin/structure/views/view/' . $view->name; } /** * Submit handler to add a restore a removed display to a view. */ function views_ui_edit_form_submit_undo_delete_display($form, &$form_state) { // Create the new display $id = $form_state['display_id']; $form_state['view']->display[$id]->deleted = FALSE; // Store in cache views_ui_cache_set($form_state['view']); // Redirect to the top-level edit page. $form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit/' . $id; } /** * Submit handler to enable a disabled display. */ function views_ui_edit_form_submit_enable_display($form, &$form_state) { $id = $form_state['display_id']; // set_option doesn't work because this would might affect upper displays $form_state['view']->display[$id]->handler->set_option('enabled', TRUE); // Store in cache views_ui_cache_set($form_state['view']); // Redirect to the top-level edit page. $form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit/' . $id; } /** * Submit handler to disable display. */ function views_ui_edit_form_submit_disable_display($form, &$form_state) { $id = $form_state['display_id']; $form_state['view']->display[$id]->handler->set_option('enabled', FALSE); // Store in cache views_ui_cache_set($form_state['view']); // Redirect to the top-level edit page. $form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit/' . $id; } /** * Submit handler when Preview button is clicked. */ function views_ui_edit_form_submit_preview($form, &$form_state) { // Rebuild the form with a pristine $view object. $form_state['build_info']['args'][0] = views_ui_cache_load($form_state['view']->name); $form_state['show_preview'] = TRUE; $form_state['rebuild'] = TRUE; } /** * Submit handler for form buttons that do not complete a form workflow. * * The Edit View form is a multistep form workflow, but with state managed by * the CTools object cache rather than $form_state['rebuild']. Without this * submit handler, buttons that add or remove displays would redirect to the * destination parameter (e.g., when the Edit View form is linked to from a * contextual link). This handler can be added to buttons whose form submission * should not yet redirect to the destination. */ function views_ui_edit_form_submit_delay_destination($form, &$form_state) { if (isset($_GET['destination']) && $form_state['redirect'] !== FALSE) { if (!isset($form_state['redirect'])) { $form_state['redirect'] = $_GET['q']; } if (is_string($form_state['redirect'])) { $form_state['redirect'] = array($form_state['redirect']); } $options = isset($form_state['redirect'][1]) ? $form_state['redirect'][1] : array(); if (!isset($options['query']['destination'])) { $options['query']['destination'] = $_GET['destination']; } $form_state['redirect'][1] = $options; unset($_GET['destination']); } } /** * Adds tabs for navigating across Displays when editing a View. * * This function can be called from hook_menu_local_tasks_alter() to implement * these tabs as secondary local tasks, or it can be called from elsewhere if * having them as secondary local tasks isn't desired. The caller is responsible * for setting the active tab's #active property to TRUE. * * @param view $view * The view which will be edited. * @param $display_id * The display_id which is edited on the current request. */ function views_ui_edit_page_display_tabs($view, $display_id = NULL) { $tabs = array(); // Create a tab for each display. foreach ($view->display as $id => $display) { $tabs[$id] = array( '#theme' => 'menu_local_task', '#link' => array( 'title' => views_ui_get_display_label($view, $id), 'href' => 'admin/structure/views/view/' . $view->name . '/edit/' . $id, 'localized_options' => array(), ), ); if (!empty($display->deleted)) { $tabs[$id]['#link']['localized_options']['attributes']['class'][] = 'views-display-deleted-link'; } if (isset($display->display_options['enabled']) && !$display->display_options['enabled']) { $tabs[$id]['#link']['localized_options']['attributes']['class'][] = 'views-display-disabled-link'; } } // If the default display isn't supposed to be shown, don't display its tab, unless it's the only display. if ((!views_ui_show_default_display($view) && $display_id != 'default') && count($tabs) > 1) { $tabs['default']['#access'] = FALSE; } // Mark the display tab as red to show validation errors. $view->validate(); foreach ($view->display as $id => $display) { if (!empty($view->display_errors[$id])) { // Always show the tab. $tabs[$id]['#access'] = TRUE; // Add a class to mark the error and a title to make a hover tip. $tabs[$id]['#link']['localized_options']['attributes']['class'][] = 'error'; $tabs[$id]['#link']['localized_options']['attributes']['title'] = t('This display has one or more validation errors; please review it.'); } } return $tabs; } /** * Controls whether or not the default display should have its own tab on edit. */ function views_ui_show_default_display($view) { // Always show the default display for advanced users who prefer that mode. $advanced_mode = variable_get('views_ui_show_master_display', FALSE); // For other users, show the default display only if there are no others, and // hide it if there's at least one "real" display. $additional_displays = (count($view->display) == 1); return $advanced_mode || $additional_displays; } /** * Returns a renderable array representing the edit page for one display. */ function views_ui_get_display_tab($view, $display_id) { $build = array(); $display = $view->display[$display_id]; // If the plugin doesn't exist, display an error message instead of an edit // page. if (empty($display->handler)) { $title = isset($display->display_title) ? $display->display_title : t('Invalid'); // @TODO: Improved UX for the case where a plugin is missing. $build['#markup'] = t("Error: Display @display refers to a plugin named '@plugin', but that plugin is not available.", array('@display' => $display->id, '@plugin' => $display->display_plugin)); } // Build the content of the edit page. else { $build['details'] = views_ui_get_display_tab_details($view, $display); } // In AJAX context, views_ui_regenerate_tab() returns this outside of form // context, so hook_form_views_ui_edit_form_alter() is insufficient. drupal_alter('views_ui_display_tab', $build, $view, $display_id); return $build; } /** * Helper function to get the display details section of the edit UI. * * @param $view * @param $display * * @return array * A renderable page build array. */ function views_ui_get_display_tab_details($view, $display) { $display_title = views_ui_get_display_label($view, $display->id, FALSE); $build = array( '#theme_wrappers' => array('container'), '#attributes' => array('id' => 'edit-display-settings-details',), ); $plugin = views_fetch_plugin_data('display', $view->display[$display->id]->display_plugin); // The following is for display purposes only. We need to determine if there is more than one button and wrap // the buttons in a .ctools-dropbutton class if more than one is present. Otherwise, we'll just wrap the // actions in the .ctools-button class. $isDisplayDeleted = !empty($display->deleted); $isDeletable = empty($plugin['no remove']); // The master display cannot be cloned. $isDefault = $display->id == 'default'; // @todo: Figure out why get_option doesn't work here. $isEnabled = $display->handler->get_option('enabled'); if (!$isDisplayDeleted && $isDeletable && !$isDefault) { $prefix = '
    '; $suffix = '
'; $itemElement = 'li'; } else { $prefix = '
    '; $suffix = '
'; $itemElement = 'li'; } if ($display->id != 'default') { $build['top']['#theme_wrappers'] = array('container'); $build['top']['#attributes']['id'] = 'edit-display-settings-top'; $build['top']['#attributes']['class'] = array('views-ui-display-tab-actions', 'views-ui-display-tab-bucket', 'clearfix'); // The Delete, Duplicate and Undo Delete buttons. $build['top']['actions'] = array( '#prefix' => $prefix, '#suffix' => $suffix, ); if (!$isDisplayDeleted) { if (!$isEnabled) { $build['top']['actions']['enable'] = array( '#type' => 'submit', '#value' => t('enable @display_title', array('@display_title' => $display_title)), '#limit_validation_errors' => array(), '#submit' => array('views_ui_edit_form_submit_enable_display', 'views_ui_edit_form_submit_delay_destination'), '#prefix' => '<' . $itemElement . ' class="enable">', "#suffix" => '', ); } // Add a link to view the page. elseif ($display->handler->has_path()) { $path = $display->handler->get_path(); if (strpos($path, '%') === FALSE) { $build['top']['actions']['path'] = array( '#type' => 'link', '#title' => t('view @display', array('@display' => $display->display_title)), '#options' => array('alt' => array(t("Go to the real page for this display"))), '#href' => $path, '#prefix' => '<' . $itemElement . ' class="view">', "#suffix" => '', ); } } if (!$isDefault) { $build['top']['actions']['duplicate'] = array( '#type' => 'submit', '#value' => t('clone @display_title', array('@display_title' => $display_title)), '#limit_validation_errors' => array(), '#submit' => array('views_ui_edit_form_submit_duplicate_display', 'views_ui_edit_form_submit_delay_destination'), '#prefix' => '<' . $itemElement . ' class="duplicate">', "#suffix" => '', ); } if ($isDeletable) { $build['top']['actions']['delete'] = array( '#type' => 'submit', '#value' => t('delete @display_title', array('@display_title' => $display_title)), '#limit_validation_errors' => array(), '#submit' => array('views_ui_edit_form_submit_delete_display', 'views_ui_edit_form_submit_delay_destination'), '#prefix' => '<' . $itemElement . ' class="delete">', "#suffix" => '', ); } if ($isEnabled) { $build['top']['actions']['disable'] = array( '#type' => 'submit', '#value' => t('disable @display_title', array('@display_title' => $display_title)), '#limit_validation_errors' => array(), '#submit' => array('views_ui_edit_form_submit_disable_display', 'views_ui_edit_form_submit_delay_destination'), '#prefix' => '<' . $itemElement . ' class="disable">', "#suffix" => '', ); } } else { $build['top']['actions']['undo_delete'] = array( '#type' => 'submit', '#value' => t('undo delete of @display_title', array('@display_title' => $display_title)), '#limit_validation_errors' => array(), '#submit' => array('views_ui_edit_form_submit_undo_delete_display', 'views_ui_edit_form_submit_delay_destination'), '#prefix' => '<' . $itemElement . ' class="undo-delete">', "#suffix" => '', ); } // The area above the three columns. $build['top']['display_title'] = array( '#theme' => 'views_ui_display_tab_setting', '#description' => t('Display name'), '#link' => $display->handler->option_link(check_plain($display_title), 'display_title'), ); } $build['columns'] = array(); $build['columns']['#theme_wrappers'] = array('container'); $build['columns']['#attributes'] = array('id' => 'edit-display-settings-main', 'class' => array('clearfix', 'views-display-columns'),); $build['columns']['first']['#theme_wrappers'] = array('container'); $build['columns']['first']['#attributes'] = array('class' => array('views-display-column', 'first')); $build['columns']['second']['#theme_wrappers'] = array('container'); $build['columns']['second']['#attributes'] = array('class' => array('views-display-column', 'second')); $build['columns']['second']['settings'] = array(); $build['columns']['second']['header'] = array(); $build['columns']['second']['footer'] = array(); $build['columns']['second']['pager'] = array(); // The third column buckets are wrapped in a CTools collapsible div $build['columns']['third']['#theme_wrappers'] = array('container'); $build['columns']['third']['#attributes'] = array('class' => array('views-display-column', 'third', 'ctools-collapsible-container', 'ctools-collapsible-remember')); // Specify an id that won't change after AJAX requests, so ctools can keep // track of the user's preferred collapsible state. Use the same id across // different displays of the same view, so changing displays doesn't // recollapse the column. $build['columns']['third']['#attributes']['id'] = 'views-ui-advanced-column-' . $view->name; // Collapse the div by default. if (!variable_get('views_ui_show_advanced_column', FALSE)) { $build['columns']['third']['#attributes']['class'][] = 'ctools-collapsed'; } $build['columns']['third']['advanced'] = array('#markup' => '

' . t('Advanced') . '

',); $build['columns']['third']['collapse']['#theme_wrappers'] = array('container'); $build['columns']['third']['collapse']['#attributes'] = array('class' => array('ctools-collapsible-content',),); // Each option (e.g. title, access, display as grid/table/list) fits into one // of several "buckets," or boxes (Format, Fields, Sort, and so on). $buckets = array(); // Fetch options from the display plugin, with a list of buckets they go into. $options = array(); $display->handler->options_summary($buckets, $options); // Place each option into its bucket. foreach ($options as $id => $option) { // Each option self-identifies as belonging in a particular bucket. $buckets[$option['category']]['build'][$id] = views_ui_edit_form_get_build_from_option($id, $option, $view, $display); } // Place each bucket into the proper column. foreach ($buckets as $id => $bucket) { // Let buckets identify themselves as belonging in a column. if (isset($bucket['column']) && isset($build['columns'][$bucket['column']])) { $column = $bucket['column']; } // If a bucket doesn't pick one of our predefined columns to belong to, put // it in the last one. else { $column = 'third'; } if (isset($bucket['build']) && is_array($bucket['build'])) { // The third column is a CTools collapsible div, so // the structure of the form is a little different for this column if ($column === 'third') { $build['columns'][$column]['collapse'][$id] = $bucket['build']; $build['columns'][$column]['collapse'][$id]['#theme_wrappers'][] = 'views_ui_display_tab_bucket'; $build['columns'][$column]['collapse'][$id]['#title'] = !empty($bucket['title']) ? $bucket['title'] : ''; $build['columns'][$column]['collapse'][$id]['#name'] = !empty($bucket['title']) ? $bucket['title'] : $id; } else { $build['columns'][$column][$id] = $bucket['build']; $build['columns'][$column][$id]['#theme_wrappers'][] = 'views_ui_display_tab_bucket'; $build['columns'][$column][$id]['#title'] = !empty($bucket['title']) ? $bucket['title'] : ''; $build['columns'][$column][$id]['#name'] = !empty($bucket['title']) ? $bucket['title'] : $id; } } } $build['columns']['first']['fields'] = views_ui_edit_form_get_bucket('field', $view, $display); $build['columns']['first']['filters'] = views_ui_edit_form_get_bucket('filter', $view, $display); $build['columns']['first']['sorts'] = views_ui_edit_form_get_bucket('sort', $view, $display); $build['columns']['second']['header'] = views_ui_edit_form_get_bucket('header', $view, $display); $build['columns']['second']['footer'] = views_ui_edit_form_get_bucket('footer', $view, $display); $build['columns']['third']['collapse']['arguments'] = views_ui_edit_form_get_bucket('argument', $view, $display); $build['columns']['third']['collapse']['relationships'] = views_ui_edit_form_get_bucket('relationship', $view, $display); $build['columns']['third']['collapse']['empty'] = views_ui_edit_form_get_bucket('empty', $view, $display); return $build; } /** * Build a renderable array representing one option on the edit form. * * This function might be more logical as a method on an object, if a suitable * object emerges out of refactoring. */ function views_ui_edit_form_get_build_from_option($id, $option, $view, $display) { $option_build = array(); $option_build['#theme'] = 'views_ui_display_tab_setting'; $option_build['#description'] = $option['title']; $option_build['#link'] = $display->handler->option_link($option['value'], $id, '', empty($option['desc']) ? '' : $option['desc']); $option_build['#links'] = array(); if (!empty($option['links']) && is_array($option['links'])) { foreach ($option['links'] as $link_id => $link_value) { $option_build['#settings_links'][] = $display->handler->option_link($option['setting'], $link_id, 'views-button-configure', $link_value); } } if (!empty($display->handler->options['defaults'][$id])) { $display_id = 'default'; $option_build['#defaulted'] = TRUE; } else { $display_id = $display->id; if (!$display->handler->is_default_display()) { if ($display->handler->defaultable_sections($id)) { $option_build['#overridden'] = TRUE; } } } $option_build['#attributes']['class'][] = drupal_clean_css_identifier($display_id . '-' . $id); if (!empty($view->changed_sections[$display_id . '-' . $id])) { $option_build['#changed'] = TRUE; } return $option_build; } function template_preprocess_views_ui_display_tab_setting(&$variables) { static $zebra = 0; $variables['zebra'] = ($zebra % 2 === 0 ? 'odd' : 'even'); $zebra++; // Put the main link to the left side array_unshift($variables['settings_links'], $variables['link']); $variables['settings_links'] = implode(' | ', $variables['settings_links']); // Add classes associated with this display tab to the overall list. $variables['classes_array'] = array_merge($variables['classes_array'], $variables['class']); if (!empty($variables['defaulted'])) { $variables['classes_array'][] = 'defaulted'; } if (!empty($variables['overridden'])) { $variables['classes_array'][] = 'overridden'; $variables['attributes_array']['title'][] = t('Overridden'); } // Append a colon to the description, if requested. if ($variables['description'] && $variables['description_separator']) { $variables['description'] .= t(':'); } } function template_preprocess_views_ui_display_tab_bucket(&$variables) { $element = $variables['element']; $variables['item_help_icon'] = ''; if (!empty($element['#item_help_icon'])) { $variables['item_help_icon'] = render($element['#item_help_icon']); } if (!empty($element['#name'])) { $variables['classes_array'][] = drupal_clean_css_identifier(strtolower($element['#name'])); } if (!empty($element['#overridden'])) { $variables['classes_array'][] = 'overridden'; $variables['attributes_array']['title'][] = t('Overridden'); } $variables['content'] = $element['#children']; $variables['title'] = $element['#title']; $variables['actions'] = !empty($element['#actions']) ? $element['#actions'] : ''; } function template_preprocess_views_ui_display_tab_column(&$variables) { $element = $variables['element']; $variables['content'] = $element['#children']; $variables['column'] = $element['#column']; } /** * Move form elements into fieldsets for presentation purposes. * * Many views forms use #tree = TRUE to keep their values in a hierarchy for * easier storage. Moving the form elements into fieldsets during form building * would break up that hierarchy. Therefore, we wait until the pre_render stage, * where any changes we make affect presentation only and aren't reflected in * $form_state['values']. */ function views_ui_pre_render_add_fieldset_markup($form) { foreach (element_children($form) as $key) { $element = $form[$key]; // In our form builder functions, we added an arbitrary #fieldset property // to any element that belongs in a fieldset. If this form element has that // property, move it into its fieldset. if (isset($element['#fieldset']) && isset($form[$element['#fieldset']])) { $form[$element['#fieldset']][$key] = $element; // Remove the original element this duplicates. unset($form[$key]); } } return $form; } /** * Flattens the structure of an element containing the #flatten property. * * If a form element has #flatten = TRUE, then all of it's children * get moved to the same level as the element itself. * So $form['to_be_flattened'][$key] becomes $form[$key], and * $form['to_be_flattened'] gets unset. */ function views_ui_pre_render_flatten_data($form) { foreach (element_children($form) as $key) { $element = $form[$key]; if (!empty($element['#flatten'])) { foreach (element_children($element) as $child_key) { $form[$child_key] = $form[$key][$child_key]; } // All done, remove the now-empty parent. unset($form[$key]); } } return $form; } /** * Moves argument options into their place. * * When configuring the default argument behavior, almost each of the radio * buttons has its own fieldset shown bellow it when the radio button is * clicked. That fieldset is created through a custom form process callback. * Each element that has #argument_option defined and pointing to a default * behavior gets moved to the appropriate fieldset. * So if #argument_option is specified as 'default', the element is moved * to the 'default_options' fieldset. */ function views_ui_pre_render_move_argument_options($form) { foreach (element_children($form) as $key) { $element = $form[$key]; if (!empty($element['#argument_option'])) { $container_name = $element['#argument_option'] . '_options'; if (isset($form['no_argument']['default_action'][$container_name])) { $form['no_argument']['default_action'][$container_name][$key] = $element; } // Remove the original element this duplicates. unset($form[$key]); } } return $form; } /** * Custom form radios process function. * * Roll out a single radios element to a list of radios, * using the options array as index. * While doing that, create a container element underneath each option, which * contains the settings related to that option. * * @see form_process_radios() */ function views_ui_process_container_radios($element) { if (count($element['#options']) > 0) { foreach ($element['#options'] as $key => $choice) { $element += array($key => array()); // Generate the parents as the autogenerator does, so we will have a // unique id for each radio button. $parents_for_id = array_merge($element['#parents'], array($key)); $element[$key] += array( '#type' => 'radio', '#title' => $choice, // The key is sanitized in drupal_attributes() during output from the // theme function. '#return_value' => $key, '#default_value' => isset($element['#default_value']) ? $element['#default_value'] : NULL, '#attributes' => $element['#attributes'], '#parents' => $element['#parents'], '#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)), '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, ); $element[$key . '_options'] = array( '#type' => 'container', '#attributes' => array('class' => array('views-admin-dependent')), ); } } return $element; } /** * Import a view from cut & paste. */ function views_ui_import_page($form, &$form_state) { $form['name'] = array( '#type' => 'textfield', '#title' => t('View name'), '#description' => t('Enter the name to use for this view if it is different from the source view. Leave blank to use the name of the view.'), ); $form['name_override'] = array( '#type' => 'checkbox', '#title' => t('Replace an existing view if one exists with the same name'), ); $form['bypass_validation'] = array( '#type' => 'checkbox', '#title' => t('Bypass view validation'), '#description' => t('Bypass the validation of plugins and handlers when importing this view.'), ); $form['view'] = array( '#type' => 'textarea', '#title' => t('Paste view code here'), '#required' => TRUE, ); $form['submit'] = array( '#type' => 'submit', '#value' => t('Import'), '#submit' => array('views_ui_import_submit'), '#validate' => array('views_ui_import_validate'), ); return $form; } /** * Validate handler to import a view. */ function views_ui_import_validate($form, &$form_state) { $view = ''; views_include('view'); // Be forgiving if someone pastes views code that starts with 'api_version) || $view->api_version < 2) { form_error($form['view'], t('That view is not compatible with this version of Views. If you have a view from views1 you have to go to a drupal6 installation and import it there.')); } elseif (version_compare($view->api_version, views_api_version(), '>')) { form_error($form['view'], t('That view is created for the version @import_version of views, but you only have @api_version', array( '@import_version' => $view->api_version, '@api_version' => views_api_version()))); } // View name must be alphanumeric or underscores, no other punctuation. if (!empty($form_state['values']['name']) && preg_match('/[^a-zA-Z0-9_]/', $form_state['values']['name'])) { form_error($form['name'], t('View name must be alphanumeric or underscores only.')); } if ($form_state['values']['name']) { $view->name = $form_state['values']['name']; } $test = views_get_view($view->name); if (!$form_state['values']['name_override']) { if ($test && $test->type != t('Default')) { form_set_error('', t('A view by that name already exists; please choose a different name')); } } else { if ($test->vid) { $view->vid = $test->vid; } } // Make sure base table gets set properly if it got moved. $view->update(); $view->init_display(); $broken = FALSE; // Bypass the validation of view pluigns/handlers if option is checked. if (!$form_state['values']['bypass_validation']) { // Make sure that all plugins and handlers needed by this view actually exist. foreach ($view->display as $id => $display) { if (empty($display->handler) || !empty($display->handler->broken)) { drupal_set_message(t('Display plugin @plugin is not available.', array('@plugin' => $display->display_plugin)), 'error'); $broken = TRUE; continue; } $plugin = views_get_plugin('style', $display->handler->get_option('style_plugin')); if (!$plugin) { drupal_set_message(t('Style plugin @plugin is not available.', array('@plugin' => $display->handler->get_option('style_plugin'))), 'error'); $broken = TRUE; } elseif ($plugin->uses_row_plugin()) { $plugin = views_get_plugin('row', $display->handler->get_option('row_plugin')); if (!$plugin) { drupal_set_message(t('Row plugin @plugin is not available.', array('@plugin' => $display->handler->get_option('row_plugin'))), 'error'); $broken = TRUE; } } foreach (views_object_types() as $type => $info) { $handlers = $display->handler->get_handlers($type); if ($handlers) { foreach ($handlers as $id => $handler) { if ($handler->broken()) { drupal_set_message(t('@type handler @table.@field is not available.', array( '@type' => $info['stitle'], '@table' => $handler->table, '@field' => $handler->field, )), 'error'); $broken = TRUE; } } } } } } if ($broken) { form_set_error('', t('Unable to import view.')); } $form_state['view'] = &$view; } /** * Submit handler for view import. */ function views_ui_import_submit($form, &$form_state) { // Store in cache and then go to edit. views_ui_cache_set($form_state['view']); $form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit'; } /** * Validate that a view is complete and whole. */ function views_ui_edit_view_form_validate($form, &$form_state) { // Do not validate cancel or delete or revert. if (empty($form_state['clicked_button']['#value']) || $form_state['clicked_button']['#value'] != t('Save')) { return; } $errors = $form_state['view']->validate(); if ($errors !== TRUE) { foreach ($errors as $error) { form_set_error('', $error); } } } /** * Submit handler for the edit view form. */ function views_ui_edit_view_form_submit($form, &$form_state) { // Go through and remove displayed scheduled for removal. foreach ($form_state['view']->display as $id => $display) { if (!empty($display->deleted)) { unset($form_state['view']->display[$id]); } } // Rename display ids if needed. foreach ($form_state['view']->display as $id => $display) { if (!empty($display->new_id)) { $form_state['view']->display[$id]->id = $display->new_id; // Redirect the user to the renamed display to be sure that the page itself exists and doesn't throw errors. $form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit/' . $display->new_id; } } // Direct the user to the right url, if the path of the display has changed. if (!empty($_GET['destination'])) { $destination = $_GET['destination']; // Find out the first display which has a changed path and redirect to this url. $old_view = views_get_view($form_state['view']->name); foreach ($old_view->display as $id => $display) { // Only check for displays with a path. if (!isset($display->display_options['path'])) { continue; } $old_path = $display->display_options['path']; if (($display->display_plugin == 'page') && ($old_path == $destination) && ($old_path != $form_state['view']->display[$id]->display_options['path'])) { $destination = $form_state['view']->display[$id]->display_options['path']; unset($_GET['destination']); } } $form_state['redirect'] = $destination; } $form_state['view']->save(); drupal_set_message(t('The view %name has been saved.', array('%name' => $form_state['view']->get_human_name()))); // Remove this view from cache so we can edit it properly. ctools_object_cache_clear('view', $form_state['view']->name); } /** * Submit handler for the edit view form. */ function views_ui_edit_view_form_cancel($form, &$form_state) { // Remove this view from cache so edits will be lost. ctools_object_cache_clear('view', $form_state['view']->name); if (empty($form['view']->vid)) { // I seem to have to drupal_goto here because I can't get fapi to // honor the redirect target. Not sure what I screwed up here. drupal_goto('admin/structure/views'); } } function views_ui_edit_view_form_delete($form, &$form_state) { unset($_REQUEST['destination']); // Redirect to the delete confirm page $form_state['redirect'] = array('admin/structure/views/view/' . $form_state['view']->name . '/delete', array('query' => drupal_get_destination() + array('cancel' => 'admin/structure/views/view/' . $form_state['view']->name . '/edit'))); } /** * Add information about a section to a display. */ function views_ui_edit_form_get_bucket($type, $view, $display) { $build = array( '#theme_wrappers' => array('views_ui_display_tab_bucket'), ); $types = views_object_types(); $build['#overridden'] = FALSE; $build['#defaulted'] = FALSE; if (module_exists('advanced_help')) { $build['#item_help_icon'] = array( '#theme' => 'advanced_help_topic', '#module' => 'views', '#topic' => $type, ); } $build['#name'] = $build['#title'] = $types[$type]['title']; // Different types now have different rearrange forms, so we use this switch // to get the right one. switch ($type) { case 'filter': $rearrange_url = "admin/structure/views/nojs/rearrange-$type/$view->name/$display->id/$type"; $rearrange_text = t('And/Or, Rearrange'); // TODO: Add another class to have another symbol for filter rearrange. $class = 'icon compact rearrange'; break; case 'field': // Fetch the style plugin info so we know whether to list fields or not. $style_plugin = $display->handler->get_plugin(); $uses_fields = $style_plugin && $style_plugin->uses_fields(); if (!$uses_fields) { $build['fields'][] = array( '#markup' => t('The selected style or row format does not utilize fields.'), '#theme_wrappers' => array('views_container'), '#attributes' => array('class' => array('views-display-setting')), ); return $build; } default: $rearrange_url = "admin/structure/views/nojs/rearrange/$view->name/$display->id/$type"; $rearrange_text = t('Rearrange'); $class = 'icon compact rearrange'; } // Create an array of actions to pass to theme_links $actions = array(); $count_handlers = count($display->handler->get_handlers($type)); $actions['add'] = array( 'title' => t('Add'), 'href' => "admin/structure/views/nojs/add-item/$view->name/$display->id/$type", 'attributes'=> array('class' => array('icon compact add', 'views-ajax-link'), 'title' => t('Add'), 'id' => 'views-add-' . $type), 'html' => TRUE, ); if ($count_handlers > 0) { $actions['rearrange'] = array( 'title' => $rearrange_text, 'href' => $rearrange_url, 'attributes' => array('class' => array($class, 'views-ajax-link'), 'title' => t('Rearrange'), 'id' => 'views-rearrange-' . $type), 'html' => TRUE, ); } // Render the array of links $build['#actions'] = theme('links__ctools_dropbutton', array( 'links' => $actions, 'attributes' => array( 'class' => array('inline', 'links', 'actions', 'horizontal', 'right') ), 'class' => array('views-ui-settings-bucket-operations'), ) ); if (!$display->handler->is_default_display()) { if (!$display->handler->is_defaulted($types[$type]['plural'])) { $build['#overridden'] = TRUE; } else { $build['#defaulted'] = TRUE; } } // If there's an options form for the bucket, link to it. if (!empty($types[$type]['options'])) { $build['#title'] = l($build['#title'], "admin/structure/views/nojs/config-type/$view->name/$display->id/$type", array('attributes' => array('class' => array('views-ajax-link'), 'id' => 'views-title-' . $type))); } static $relationships = NULL; if (!isset($relationships)) { // Get relationship labels $relationships = array(); // @todo: get_handlers() $handlers = $display->handler->get_option('relationships'); if ($handlers) { foreach ($handlers as $id => $info) { $handler = $display->handler->get_handler('relationship', $id); $relationships[$id] = $handler->label(); } } } // Filters can now be grouped so we do a little bit extra: $groups = array(); $grouping = FALSE; if ($type == 'filter') { $group_info = $view->display_handler->get_option('filter_groups'); // If there is only one group but it is using the "OR" filter, we still // treat it as a group for display purposes, since we want to display the // "OR" label next to items within the group. if (!empty($group_info['groups']) && (count($group_info['groups']) > 1 || current($group_info['groups']) == 'OR')) { $grouping = TRUE; $groups = array(0 => array()); } } $build['fields'] = array(); foreach ($display->handler->get_option($types[$type]['plural']) as $id => $field) { // Build the option link for this handler ("Node: ID = article"). $build['fields'][$id] = array(); $build['fields'][$id]['#theme'] = 'views_ui_display_tab_setting'; $handler = $display->handler->get_handler($type, $id); if (empty($handler)) { $build['fields'][$id]['#class'][] = 'broken'; $field_name = t('Broken/missing handler: @table > @field', array('@table' => $field['table'], '@field' => $field['field'])); $build['fields'][$id]['#link'] = l($field_name, "admin/structure/views/nojs/config-item/$view->name/$display->id/$type/$id", array('attributes' => array('class' => array('views-ajax-link')), 'html' => TRUE)); continue; } $field_name = check_plain($handler->ui_name(TRUE)); if (!empty($field['relationship']) && !empty($relationships[$field['relationship']])) { $field_name = '(' . $relationships[$field['relationship']] . ') ' . $field_name; } $description = filter_xss_admin($handler->admin_summary()); $link_text = $field_name . (empty($description) ? '' : " ($description)"); $link_attributes = array('class' => array('views-ajax-link')); if (!empty($field['exclude'])) { $link_attributes['class'][] = 'views-field-excluded'; } $build['fields'][$id]['#link'] = l($link_text, "admin/structure/views/nojs/config-item/$view->name/$display->id/$type/$id", array('attributes' => $link_attributes, 'html' => TRUE)); $build['fields'][$id]['#class'][] = drupal_clean_css_identifier($display->id . '-' . $type . '-' . $id); if (!empty($view->changed_sections[$display->id . '-' . $type . '-' . $id])) { // @TODO: #changed is no longer being used? $build['fields'][$id]['#changed'] = TRUE; } if ($display->handler->use_group_by() && $handler->use_group_by()) { $build['fields'][$id]['#settings_links'][] = l('' . t('Aggregation settings') . '', "admin/structure/views/nojs/config-item-group/$view->name/$display->id/$type/$id", array('attributes' => array('class' => 'views-button-configure views-ajax-link', 'title' => t('Aggregation settings')), 'html' => true)); } if ($handler->has_extra_options()) { $build['fields'][$id]['#settings_links'][] = l('' . t('Settings') . '', "admin/structure/views/nojs/config-item-extra/$view->name/$display->id/$type/$id", array('attributes' => array('class' => array('views-button-configure', 'views-ajax-link'), 'title' => t('Settings')), 'html' => true)); } if ($grouping) { $gid = $handler->options['group']; // Show in default group if the group does not exist. if (empty($group_info['groups'][$gid])) { $gid = 0; } $groups[$gid][] = $id; } } // If using grouping, re-order fields so that they show up properly in the list. if ($type == 'filter' && $grouping) { $store = $build['fields']; $build['fields'] = array(); foreach ($groups as $gid => $contents) { // Display an operator between each group. if (!empty($build['fields'])) { $build['fields'][] = array( '#theme' => 'views_ui_display_tab_setting', '#class' => array('views-group-text'), '#link' => ($group_info['operator'] == 'OR' ? t('OR') : t('AND')), ); } // Display an operator between each pair of filters within the group. $keys = array_keys($contents); $last = end($keys); foreach ($contents as $key => $pid) { if ($key != $last) { $store[$pid]['#link'] .= '  ' . ($group_info['groups'][$gid] == 'OR' ? t('OR') : t('AND')); } $build['fields'][$pid] = $store[$pid]; } } } return $build; } /** * Regenerate the current tab for AJAX updates. */ function views_ui_regenerate_tab(&$view, &$output, $display_id) { if (!$view->set_display('default')) { return; } // Regenerate the main display area. $build = views_ui_get_display_tab($view, $display_id); views_ui_add_microweights($build); $output[] = ajax_command_html('#views-tab-' . $display_id, drupal_render($build)); // Regenerate the top area so changes to display names and order will appear. $build = views_ui_render_display_top($view, $display_id); views_ui_add_microweights($build); $output[] = ajax_command_replace('#views-display-top', drupal_render($build)); } /** * Recursively adds microweights to a render array, similar to what form_builder() does for forms. * * @todo Submit a core patch to fix drupal_render() to do this, so that all * render arrays automatically preserve array insertion order, as forms do. */ function views_ui_add_microweights(&$build) { $count = 0; foreach (element_children($build) as $key) { if (!isset($build[$key]['#weight'])) { $build[$key]['#weight'] = $count/1000; } views_ui_add_microweights($build[$key]); $count++; } } /** * Provide a standard set of Apply/Cancel/OK buttons for the forms. Also provide * a hidden op operator because the forms plugin doesn't seem to properly * provide which button was clicked. * * TODO: Is the hidden op operator still here somewhere, or is that part of the * docblock outdated? */ function views_ui_standard_form_buttons(&$form, &$form_state, $form_id, $name = NULL, $third = NULL, $submit = NULL) { $form['buttons'] = array( '#prefix' => '
', '#suffix' => '
', ); if (empty($name)) { $name = t('Apply'); $view = $form_state['view']; if (!empty($view->stack) && count($view->stack) > 1) { $name = t('Apply and continue'); } $names = array(t('Apply'), t('Apply and continue')); } // Forms that are purely informational set an ok_button flag, so we know not // to create an "Apply" button for them. if (empty($form_state['ok_button'])) { $form['buttons']['submit'] = array( '#type' => 'submit', '#value' => $name, // The regular submit handler ($form_id . '_submit') does not apply if // we're updating the default display. It does apply if we're updating // the current display. Since we have no way of knowing at this point // which display the user wants to update, views_ui_standard_submit will // take care of running the regular submit handler as appropriate. '#submit' => array('views_ui_standard_submit'), ); // Form API button click detection requires the button's #value to be the // same between the form build of the initial page request, and the initial // form build of the request processing the form submission. Ideally, the // button's #value shouldn't change until the form rebuild step. However, // views_ui_ajax_form() implements a different multistep form workflow than // the Form API does, and adjusts $view->stack prior to form processing, so // we compensate by extending button click detection code to support any of // the possible button labels. if (isset($names)) { $form['buttons']['submit']['#values'] = $names; $form['buttons']['submit']['#process'] = array_merge(array('views_ui_form_button_was_clicked'), element_info_property($form['buttons']['submit']['#type'], '#process', array())); } // If a validation handler exists for the form, assign it to this button. if (function_exists($form_id . '_validate')) { $form['buttons']['submit']['#validate'][] = $form_id . '_validate'; } } // Create a "Cancel" button. For purely informational forms, label it "OK". $cancel_submit = function_exists($form_id . '_cancel') ? $form_id . '_cancel' : 'views_ui_standard_cancel'; $form['buttons']['cancel'] = array( '#type' => 'submit', '#value' => empty($form_state['ok_button']) ? t('Cancel') : t('Ok'), '#submit' => array($cancel_submit), '#validate' => array(), ); // Some forms specify a third button, with a name and submit handler. if ($third) { if (empty($submit)) { $submit = 'third'; } $third_submit = function_exists($form_id . '_' . $submit) ? $form_id . '_' . $submit : 'views_ui_standard_cancel'; $form['buttons'][$submit] = array( '#type' => 'submit', '#value' => $third, '#validate' => array(), '#submit' => array($third_submit), ); } // Compatibility, to be removed later: // TODO: When is "later"? // We used to set these items on the form, but now we want them on the $form_state: if (isset($form['#title'])) { $form_state['title'] = $form['#title']; } if (isset($form['#help_topic'])) { $form_state['help_topic'] = $form['#help_topic']; } if (isset($form['#help_module'])) { $form_state['help_module'] = $form['#help_module']; } if (isset($form['#url'])) { $form_state['url'] = $form['#url']; } if (isset($form['#section'])) { $form_state['#section'] = $form['#section']; } // Finally, we never want these cached -- our object cache does that for us. $form['#no_cache'] = TRUE; // If this isn't an ajaxy form, then we want to set the title. if (!empty($form['#title'])) { drupal_set_title($form['#title']); } } /** * Basic submit handler applicable to all 'standard' forms. * * This submit handler determines whether the user wants the submitted changes * to apply to the default display or to the current display, and dispatches * control appropriately. */ function views_ui_standard_submit($form, &$form_state) { // Determine whether the values the user entered are intended to apply to // the current display or the default display. list($was_defaulted, $is_defaulted, $revert) = views_ui_standard_override_values($form, $form_state); // Mark the changed section of the view as changed. // TODO: Document why we are doing this, and see if we still need it. if (!empty($form['#section'])) { $form_state['view']->changed_sections[$form['#section']] = TRUE; } // Based on the user's choice in the display dropdown, determine which display // these changes apply to. if ($revert) { // If it's revert just change the override and return. $display = &$form_state['view']->display[$form_state['display_id']]; $display->handler->options_override($form, $form_state); // Don't execute the normal submit handling but still store the changed view into cache. views_ui_cache_set($form_state['view']); return; } elseif ($was_defaulted === $is_defaulted) { // We're not changing which display these form values apply to. // Run the regular submit handler for this form. } elseif ($was_defaulted && !$is_defaulted) { // We were using the default display's values, but we're now overriding // the default display and saving values specific to this display. $display = &$form_state['view']->display[$form_state['display_id']]; // options_override toggles the override of this section. $display->handler->options_override($form, $form_state); $display->handler->options_submit($form, $form_state); } elseif (!$was_defaulted && $is_defaulted) { // We used to have an override for this display, but the user now wants // to go back to the default display. // Overwrite the default display with the current form values, and make // the current display use the new default values. $display = &$form_state['view']->display[$form_state['display_id']]; // options_override toggles the override of this section. $display->handler->options_override($form, $form_state); $display->handler->options_submit($form, $form_state); } $submit_handler = $form['#form_id'] . '_submit'; if (function_exists($submit_handler)) { $submit_handler($form, $form_state); } } /** * Return the was_defaulted, is_defaulted and revert state of a form. */ function views_ui_standard_override_values($form, $form_state) { // Make sure the dropdown exists in the first place. if (isset($form_state['values']['override']['dropdown'])) { // #default_value is used to determine whether it was the default value or not. // So the available options are: $display, 'default' and 'default_revert', not 'defaults'. $was_defaulted = ($form['override']['dropdown']['#default_value'] === 'defaults'); $is_defaulted = ($form_state['values']['override']['dropdown'] === 'default'); $revert = ($form_state['values']['override']['dropdown'] === 'default_revert'); if ($was_defaulted !== $is_defaulted && isset($form['#section'])) { // We're changing which display these values apply to. // Update the #section so it knows what to mark changed. $form['#section'] = str_replace('default-', $form_state['display_id'] . '-', $form['#section']); } } else { // The user didn't get the dropdown for overriding the default display. $was_defaulted = FALSE; $is_defaulted = FALSE; $revert = FALSE; } return array($was_defaulted, $is_defaulted, $revert); } /** * Submit handler for cancel button */ function views_ui_standard_cancel($form, &$form_state) { if (!empty($form_state['view']->changed) && isset($form_state['view']->form_cache)) { unset($form_state['view']->form_cache); views_ui_cache_set($form_state['view']); } $form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit'; } /** * Add a