'. 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.