'. t('Interface allows you to define custom interfaces for content entry forms through a drag-and-drop interface.') .'
'; case 'admin/build/interface': return ''. t('Interface provides the ability to control the look and feel of content entry forms through drag-and-drop templates, allowing form elements to be placed in regions that include AJAX controls.') .'
'; } } /** * Implementation of hook_menu(). */ function interface_menu() { $items['admin/build/interface'] = array( 'access arguments' => array('author interface'), 'description' => "Apply templates to the look and feel of content entry forms.", 'file' => 'interface.admin.inc', 'page callback' => 'interface_select', 'title' => 'Interface', ); $items['admin/build/interface/list'] = array( 'description' => "Apply templates to the look and feel of content entry forms.", 'title' => 'Content entry forms', 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10, ); $items['admin/build/interface/themes'] = array( 'access arguments' => array('author interface'), 'description' => "Installed templates for modifying the look and feel of forms.", 'file' => 'interface.admin.inc', 'page callback' => 'interface_themes', 'title' => 'Templates', 'type' => MENU_LOCAL_TASK, ); $items['admin/build/interface/edit'] = array( 'access arguments' => array('author interface'), 'description' => "Edit an existing interface.", 'page callback' => 'interface_author', 'title' => 'Edit interface', 'type' => MENU_CALLBACK, ); $items['interface/save'] = array( 'access arguments' => array('author interface'), 'description' => "AJAX callback for handling interface submissions.", 'page callback' => 'interface_form_ajax_submit', 'type' => MENU_CALLBACK, ); $items['admin/build/interface/delete/%'] = array( 'access arguments' => array('author interface'), 'description' => "Delete an existing interface.", 'file' => 'interface.admin.inc', 'page callback' => 'drupal_get_form', 'page arguments' => array('interface_delete_confirm', 4), 'title' => 'Delete', 'type' => MENU_CALLBACK, ); return $items; } /** * Implementation of hook_perm(). */ function interface_perm() { return array( 'author interface', ); } /** * Implementation of hook_theme(). */ function interface_theme() { return array( 'interface_list' => array( 'arguments' => array('content' => NULL), ), ); } /** * Implementation of hook_form_alter(). */ function interface_form_alter(&$form, $form_state, $form_id) { if ($form['#node']) { $context = 'Default'; if ($form['#node']->interface_context) { $context = $form['#node']->interface_context; } // Associate prerender to allow us to place stuff in regions. // @todo find a way to cache this to avoid database lookups... for right now, // should not be a big deal for the majority of sites, but when contexts are // enabled, it could make a big difference in performance. if (interface_node_type_check($form['#node']->type, $context)) { if (!$form['#pre_render']['interface']) { $form['#pre_render']['interface'] = 'interface_render_regions'; } } } } /** * Loads all behaviors for a given template. * * @param $behaviors * An array containing all behaviors registered for the template. */ function interface_load_template_behaviors($behaviors) { // Load behaviors. Each template can have one or more behaviors associated // with it. The list of behaviors is passed as an array, including a path to // the behavior. This allows us to have a general behaviors folder as well // as behaviors located within specific template subdirectories. foreach ($behaviors as $key => $row) { drupal_add_js(drupal_get_path('module', 'interface') . $row['path'] .'.js', 'module'); $settings['interface_behaviors'][] = $key; // If there is additional media associated with the behavior, load it. Additional // media is used exclusively for public interfaces, not just the authoring form. if ($row['js']) { drupal_add_js(drupal_get_path('module', 'interface') . $row['js'], 'module'); } if ($row['css']) { drupal_add_css(drupal_get_path('module', 'interface') . $row['css']); } } // add settings to the page. drupal_add_js($settings, 'setting'); } /** * Loads all behaviors for the authoring interface. * * Used to ensure a uniform load of all authoring behaviors and * prevents them from being used during content authoring. */ function interface_load_authoring_behaviors() { // Selectors used for drag-and-drop placement. Interface uses a common set of // CSS selectors for the main authoring tool. Behaviors can implement custom // selectors using hook_register_global_selector(). This hook will add new // selectors once the authoring tool has been loaded. $settings['interface_selectors'][] = '.form-item'; $settings['interface_selectors'][] = '.form-button-holder'; // Exclusions: form elements that cannot be dragged and dropped. Exclusions // are registered by various behaviors and nothing specific is within Interface. $settings['interface_exclusions'][] = '.interface-exclude'; // @todo replace this with the name of the form being edited // This is used in all selector calls to limit the selection and placement of // form elements to the node form itself. Prevents form elements from being // placed in other forms on the page. $settings['interface_author_limit'] = '#node-form'; // add settings to the page drupal_add_js($settings, 'setting'); // @todo Hrm... not sure what this is for, but will investigate. Deletable? $settings['dragndrop'] = array(); // load core and fixes. jquery_ui_add(array('ui.core')); drupal_add_js(drupal_get_path('module', 'interface') .'/behaviors/ui.core.fix.js'); jquery_ui_add(array('ui.draggable')); drupal_add_js(drupal_get_path('module', 'interface') .'/behaviors/ui.draggable.fix.js'); jquery_ui_add(array('ui.droppable', 'ui.resizable')); drupal_add_js(drupal_get_path('module', 'interface') .'/behaviors/ui.resizable.fix.js'); // Load various utility functions. drupal_add_js(drupal_get_path('module', 'interface') .'/behaviors/interface_utils.js', 'module'); // This script loads core interface settings and triggers several hooks in // behavior files for loading custom selectors and initializing behaviors // associated with custom markup. Think of it as bootstrap for Interface. drupal_add_js(drupal_get_path('module', 'interface') .'/behaviors/interface_load_settings.js', 'module'); // This script controls the panel, implementing resizing and functions for saving an interface. drupal_add_js(drupal_get_path('module', 'interface') .'/behaviors/interface-panel.js', 'module'); // This script initializes the drag and drop interface, sets up various methods // for placement in regions,and handles marker placement when authoring an interface. drupal_add_js(drupal_get_path('module', 'interface') .'/behaviors/dragndrop.js', 'module'); } /** * Creates the authoring interface for node forms. * * This function does several things: * * 1) Creates the environment for authoring interfaces using * the selected template and global behaviors. * * 2) Creates a node form using credentials of the user authoring the * interface. It is a good idea to author interfaces through a user * with sufficient privileges for seeing all form fields in a node form. * * 3) Repositions form elements within template regions for authored * interfaces. There is a lot of work involved in getting form elements * into the right places once a template has been saved to the database. * * @param $node_type * The node form to be edited. * @param $theme * The theme used to render the form output. * @param $interface_context * The context to display the form. */ function interface_author($node_type = '', $theme = '', $interface_context = '') { interface_load_authoring_behaviors(); // load the various libraries necessary. include_once 'modules/node/node.pages.inc'; // so we can work with the form. global $user; // so we can initialize a fake form owned by this user. $output = ''; $form_template = $theme; $template_data = interface_get_template($form_template); // set information about regions, to be used in processing form elements. $info['regions'] = $template_data['info']['regions']; // add regions as a setting. we are going to expose all regions through a // javascript setting. this requires each region to have a css class // matching the name of the region. $interface['regions'] = ''; foreach ($info['regions'] as $key => $item) { $interface['regions'][] = $key; } drupal_add_js($interface, 'setting'); // add css for interface controls. // these controls are only used in authoring, not in normal form usage. drupal_add_css( drupal_get_path('module', 'interface') .'/interface.css'); // add the jquery forms plugin. drupal_add_js('misc/jquery.form.js'); // load behaviors for the authoring form. moving this out into its own // function for a couple of reasons. First, to reduce the size of interface_author, // which is getting monstrous. Second, to work on behaviors a little more closely // - the order of operations is important, and several things need to load // before the main interface authoring stuff. interface_load_template_behaviors($template_data['info']['behaviors']); // load the form array. this creates a form object to wrap with markup received // from the template. form elements will be placed into regions, which will be // displayed within the markup of the form. most of this comes out of node.module. $form_state = array('storage' => NULL, 'submitted' => FALSE); $form_id = $node_type .'_node_form'; // get the name of the node form $node = array('uid' => $user->uid, 'name' => (isset($user->name) ? $user->name : ''), 'type' => $node_type, 'language' => '', 'interface_context' => $interface_context); $form_state['post'] = $_POST; // Use a copy of the function's arguments for manipulation. Forcing the // generation of the form regardless of options passed into it. This makes // interface work ONLY with single page forms, may not be very useful for webform, etc. $args = func_get_args(); $args_temp = $args; $args_temp[0] = &$form_state; $args_temp[1] = &$node; // fake node rendered. array_unshift($args_temp, $form_id); // Actually retrieve the form. $nodeform = call_user_func_array('drupal_retrieve_form', $args_temp); $form_build_id = 'form-'. md5(uniqid(mt_rand(), TRUE)); $form['#build_id'] = $form_build_id; drupal_prepare_form($form_id, $nodeform, $form_state); // Process the form, adding stuff like titles and body fields. // It APPEARS hook_form_alter() runs when this is called. drupal_process_form($form_id, $nodeform, $form_state); // Process the form to add contextual information to all elements. // This gives us position data for all form elements. $node_element_data = interface_get_contextual_information($nodeform); // Position elements within the new form structure. $newform = interface_render_regions($nodeform, $form_template, $interface_context); // Add in the interface panel, for saving the form once it is configured. $output .= interface_panel($node_type, $form_template, $node_element_data, $interface_context); // Render the new form with the markup included. $output .= drupal_render_form($form_id, $newform); return $output; } /** * Pulls behaviors defined for a form and pre-populates an array with them. * * @param $newform * Array used for holding the structure of the form that will be rendered. * @param $behaviors * Array of all the behaviors that will be applied. */ function interface_add_behaviors(&$newform, &$behaviors, $template, $interface_context) { foreach ($behaviors as $behavior) { $behavior_name = $behavior['id'] . '_interface_load_behavior'; if (function_exists($behavior_name)) { $behavior_name($newform, $template, $interface_context); } } // @todo figure out what Marc was thinking here... interface_invoke_interface_behaviors($behaviors, 'load', $newform); } /** * Pulls regions from a template and pre-populates an array with them. * * @param $region_data * A list of all regions, from the info file. * @param $template_markup * Contents of the template file. * @param $newform * Array used for holding the structure of the form that will be rendered. */ function interface_construct_regions($region_data, $template_markup, &$newform) { // split the markup regions of the form at the indicator and // store in an array. the elements of this array will be used to layout the form. $regions = split('', $template_markup); // loop around the regions defined in the info file and match them up with // markup regions. there should be regions + 1 elements within the markup // array. This is just an arbitrary number, and should be higher than the // weight of any conceivable form element. $idx = -1000; // @todo need to rewrite region handling stuff: throwing errors in weird places. // Going to take the list of regions and assign stuff automatically. First // off, we always know what closure is, let's just put that at the end. $newform['closure'] = array( '#type' => 'markup', '#value' => $regions[count($regions)], '#weight' => 0 ); unset($regions[count($regions)]); // for everything else, there must be an rid identifying the region. foreach ($regions as $elem) { foreach (array_keys($region_data) as $key) { if (strpos($elem, $key) && !$newform[$key]) { // adding to ensure it will display. $newform[$key] = array( '#type' => 'markup', '#prefix' => $elem, '#value' => ''; var_export($row . ' - '); var_export($item); print ''; if (is_array($item)) { print '
'; var_export('the item is an array'); print ''; } if (!in_array($row, $exclude)) { print '
'; var_export('the item is not excluded'); print ''; } else { print '
'; var_export('the item is excluded - '); var_export(array_search($row, $exclude)); print ''; print '
'; var_export($row); print ''; print '
'; var_export($exclude); print ''; } if ($item['#type'] != 'hidden') { print '
'; var_export('the item is not hidden'); print ''; } print '
'; var_export('----------------------'); print ''; } */ } } } /** * Place form elements within their appropriate regions. * * Repositioning elements is triggered through a pre_render element, * placed in the form array through a hook_form_alter(). * * @param $form * The form constructor, which should be unaltered at this point. * @return $form * The modified form (also changed by reference). */ function interface_render_regions(&$form, $template = '', $context = '') { // All you have is the form here, no information about the template authored // for the interface. Going to need to determine what template is being used // with the form in order to proceed. In the initial version of interface, // we will allow one form to be associated with one template. This will eliminate // issues with needing to know what template to use when there are multiple // interfaces associated with a form. // // In later editions of interface, we will allow separate forms to be associated // with each template based on the role of the user. This will change the way // interfaces are processed. For now, it means we can get by with only the form. $node_type = $form['#node']->type; $context = 'Default'; if ($form['#node']->interface_context) { $context = $form['#node']->interface_context; } // Load information about the node form being presented. This will give us the template for further processing. $result = db_query("SELECT * FROM {interface_template} WHERE content_type = '%s' AND interface_context = '%s' ORDER BY src ASC", $node_type, $context); // transform the results into an array, for easier processing. while ($row = db_fetch_array($result)) { $rows[] = $row; } // Determine what template to use for the interface. Take the first value // from the array as there can only be one template for a single form. $template = ($template == '') ? $rows[0]['template'] : $template; $template_data = interface_get_template($template, $context); // move key elements from the node form to the new form. these items need to remain constant. // keeping these elements in the main constructor function rather than use references. unsetting // a variable in a reference only removes the reference, it does not affect the source variable. $newform['#id'] = $nodeform['#id']; $newform['nid'] = $nodeform['nid']; $newform['vid'] = $nodeform['vid']; $newform['uid'] = $nodeform['uid']; $newform['created'] = $nodeform['created']; $newform['type'] = $nodeform['type']; $newform['#type'] = $node_type; $newform['language'] = $nodeform['language']; $newform['changed'] = $nodeform['changed']; unset($nodeform['#id']); unset($nodeform['nid']); unset($nodeform['vid']); unset($nodeform['uid']); unset($nodeform['created']); unset($nodeform['type']); unset($nodeform['#type']); unset($nodeform['language']); unset($nodeform['changed']); // at this point, we have the following: // info_regions: a list of regions from the info file // regions: an array containing all of the markup from the template file, // split with the tag. // // in order to present form elements within regions, we need to reconcile the // keys from the info file with the markup elements from the template. This // happens by looping through each region element (info_regions) and checking // whether or not the key exists within the region markup (from regions). It // is a straight string comparision to reconcile these elements. // // This should provide the structure for a new form, where form // elements can be placed within the appropriate markup section. interface_construct_regions($template_data['info']['regions'], $template_data['contents'], $newform); // fire off any initialization functions associated with behaviors, pass in // the new form constructor to store any markup or carry out any operations. interface_add_behaviors($newform, $template_data['info']['behaviors'], $template, $context); // Reconcile the placement of form elements within the newform. There // is a simple order of operations for proper form element placement: // // 1) Loop through all form elements placed in the interface for the form. // // 2) For each item, construct a string refencing the element in the original form. // This will be used within various eval functions later in processing. // // 3) For each item, constuct a string referencing the element's placement // in the new form. This will be used within various eval functions. // // 4) Move each item from the original form to the new form using an // eval function. This will place each element in the new form. // // 5) Once each element from the original form has been placed in the new form, // remove each element from the original form using unset in an eval function. // This ensures the item has been removed properly and avoids weird ref issues. // // 6) Merge the original form with the new one. // source_elements: Storage for element placement within the original form. // We are going to store all source strings in this array, then loop through // them later and delete each one prior to merging the forms. $source_elements = array(); // Loop through each form element in the interface to place elements correctly. // The following code constructs an elaborate eval statement that will be used // to move elements from the original constructor to the new form constructor. if ($rows) { foreach ($rows as $row) { // RIGHT SIDE: this is what will be placed in new form. Searching for an // item in the original constructor. Explode the original to get an array. $src_tree = explode('.', $row['src']); $source = "\$form"; // loop around each source element to create a string. This will // be used to reference the original form item later in processing. foreach ($src_tree as $item) { $source .= '["' . $item . '"]'; } // LEFT SIDE: this tells us where to plant the form item // in the new form. This string will be eval'd later. $dest_tree = explode('.', $row['placement']); $dest = "\$newform"; foreach ($dest_tree as $item) { $item = str_replace('#', '', $item); $dest .= '["' . $item . '"]'; } // create the string for actually moving the element. Do not delete elements // from the original form at this stage, in order to preserve any sub-elements // that may be referenced later in form generation. $move_string = $dest . " = " . $source . ";"; eval($move_string); error_log($move_string); // set the weight of the element. $weight_string = $dest . '["#weight"] = ' . $row['element_order'] . ';'; eval($weight_string); // TRAFFIC COP: for each new element, remove any child elements that may // be appearing. this ensures child elements of groupers will not be // duplicated in the new form layout. // Get the destination element and remove any child elements. // DO NOT remove hidden fields, only form elements users can position. eval('$dat = ' . $dest . ';'); if (isset($dat)) { foreach ($dat as $child_idx => $child_elem) { if (is_array($dat[$child_idx]) && isset($dat[$child_idx]['#type']) && $dat[$child_idx]['#type'] != 'hidden') { eval('unset(' . $dest . '[\'' . $child_idx . '\']);'); // heh heh } } } $source_elements[] = $source; // so it can be unset later before a merge. } } // unset source elements from the original form. This happens in order to allow // us to merge hidden fields from the original form into the interface form. foreach ($source_elements as $remove) { $formremove = "unset(" . $remove . ");"; eval($formremove); } // Remove the prerender function set in hook form alter. // This prevents the region rendering code from firing twice. unset($form['#pre_render']['interface']); // these elements must be contained in the root level element of the form to allow proper processing $exclude = array('#id', 'nid', 'vid', 'uid', 'created', 'type', 'language', 'changed', '#node', '#validate', '#theme', '#parameters', '#type', '#programmed', '#token', 'form_token', 'form_id', '#description', '#attributes', '#required', '#tree', '#parents', '#method', '#action', '#cache', '#submit', '#processed', '#defaults_loaded', '#prefix', '#suffix'); // everything else from the form should be moved to a default region in the form display. if ($template_data['info']['default_region']) { foreach ($form as $key => $elem) { if (in_array($key, $exclude)) { $newform[$key] = $elem; } else { $newform[$template_data['info']['default_region']][$key] = $elem; } unset($form[$key]); } } // Merge our rearranged elements back into the original form and return it. return array_merge($newform, $form); } /** * Creates the interface panel, a utility for authoring form interfaces. * * The panel presents users with basic information about the form, and serve as * a display for attributes and actions associated with specific form elements. * * @param $node_type * The node type for the form. Used to associate interfaces with * specific node types when the form is presented to users. * @param $template * The template associated with the form. This information is saved to the * database when the interface is saved. This is being done in anticipation of * multiple interfaces for specific forms, implemented in later iterations of * the module. * @param $elem_data * Data about the placement of form elements, in the form of [ element id ] => * [ placement in the form constructor heirarchy ]. This placement is used as * reference to the original element when positioning elements in node forms. * @return * The actual markup used for the panel. */ function interface_panel($node_type, $template, $elem_data = array(), $interface_context = '') { // Create the basic settings form. this will display the name of the content // type you are editing, the selected template for the form, and controls. $context = $interface_context; if ($context == '') { // @todo this is really sloppy, bubs. $context = 'Default'; $interface_context = 'Default'; $edit['interface_context'] = 'Default'; } $basic_details = array( 'markup' => t('You are editing the %type form using the %template template.