Drupal

Wysiwyg button with form #4

Welcome to the second article in my tutorial about building a wysiwyg button for drupal 7 which enables use to inserting tokens.

This will be a four step tutorial

This next part will cover how to expand the form to use ajax for dynamic adding and removing fields.

I followed this exellent article about how to implement det dynamic adding and removing fields, but since we are using jquery ajax to load our for we don't have the luxury of drupals ajax to ensure ajax handling for our form and we need to add this in.

but first we need to exend our for to include ajax processing.

  1. /**
  2.  * Insert token form
  3.  */
  4. function example_insert_form($form, &$form_state) {
  5.   drupal_add_library('system', 'ui.dialog');
  6.   $form['#tree'] = TRUE;
  7.   if (empty($form_state['num_objects'])) {
  8.     $form_state['num_objects'] = 1;
  9.   }
  10.   $ids = null;
  11.   $args = $form_state['build_info']['args'];
  12.   if(isset($args[0])) {
  13.     $ids = explode(',', $args[0]);
  14.     if($form_state['num_objects'] == 1) {
  15.       $form_state['num_objects'] = count($ids);
  16.     }
  17.   }
  18.   $form['objects'] = array(
  19.     '#type' => 'fieldset',
  20.     '#title' => t('Objects'),
  21.     '#prefix' => '<div id="example-fieldset-wrapper">',
  22.     '#suffix' => '</div>',
  23.   );
  24.  
  25.   for($i = 0; $i < $form_state['num_objects']; $i++) {
  26.     $form['objects'][$i]['object'] = array(
  27.       '#type' => 'textfield',
  28.       '#title' => t('Entity id'),
  29.       '#prefix' => '<div class="col1">',
  30.       '#suffix' => '</div>',
  31.       '#default_value' => isset($form_state['values'][$i]) ? $form_state['values'][$i]['object'] : '',
  32.     );
  33.     if(isset($ids[$i])) {
  34.       $form['objects'][$i]['object']['#default_value'] = $ids[$i];
  35.     }
  36.   }
  37.  
  38.   $form['objects']['add_item'] = array(
  39.     '#type' => 'submit',
  40.     '#value' => t('Add another'),
  41.     '#submit' => array('example_add_more_add_one'),
  42.     // See the examples in ajax_example.module for more details on the
  43.     // properties of #ajax.
  44.     '#ajax' => array(
  45.       'callback' => 'example_add_more_callback',
  46.       'wrapper' => 'example-fieldset-wrapper',
  47.       'method' => 'replace',
  48.       'effect' => 'fade',
  49.     ),
  50.   );
  51.   if ($form_state['num_objects'] > 1) {
  52.     $form['objects']['remove_item'] = array(
  53.       '#type' => 'submit',
  54.       '#value' => t('Remove one'),
  55.       '#submit' => array('example_add_more_remove_one'),
  56.       '#ajax' => array(
  57.         'callback' => 'example_add_more_callback',
  58.         'wrapper' => 'example-fieldset-wrapper',
  59.         'method' => 'replace',
  60.         'effect' => 'fade',
  61.       ),
  62.     );
  63.   }
  64.   $form['submit'] = array(
  65.     '#type' => 'submit',
  66.     '#value' => t('Submit'),
  67.     '#attributes' => array(
  68.       'class' => array('form-save-ids'),
  69.     ),
  70.   );
  71.  
  72.   return $form;
  73. }
  74.  
  75. /**
  76.  * Callback for both ajax-enabled buttons.
  77.  *
  78.  * Selects and returns the fieldset with the names in it.
  79.  */
  80. function example_add_more_callback($form, $form_state) {
  81.   return $form['objects'];
  82. }
  83.  
  84. /**
  85.  * Submit handler for the "add-one-more" button.
  86.  *
  87.  * Increments the max counter and causes a rebuild.
  88.  */
  89. function example_add_more_add_one($form, &$form_state) {
  90.   $form_state['num_objects']++;
  91.   $form_state['rebuild'] = TRUE;
  92. }
  93.  
  94. /**
  95.  * Submit handler for the "remove one" button.
  96.  *
  97.  * Decrements the max counter and causes a form rebuild.
  98.  */
  99. function example_add_more_remove_one($form, &$form_state) {
  100.   if ($form_state['num_objects'] > 1) {
  101.     $form_state['num_objects']--;
  102.   }
  103.   $form_state['rebuild'] = TRUE;
  104. }

As you can see we are using a simple count setting to increase or decrease the number of text fields in the form. This works fine for our simple form and we just need to add a few changes to the js in order to forward this change from the client to the backend. So make the following change to the insert_form function:

  1.   insert_form: function (data, settings, instanceId) {
  2.     // Location, where to fetch the dialog.
  3.     var aurl = Drupal.settings.basePath + 'example/insert/ajax';
  4.     if(settings.ids) {
  5.       aurl += '/' + settings.ids.join();
  6.     }
  7.     var dialogdiv = $('<div id="example-insert-dialog"></div>');
  8.     dialogdiv.load(aurl, function(){
  9.       var dialogClose = function () {
  10.         try {
  11.           dialogdiv.dialog('destroy').remove();
  12.         } catch (e) {};
  13.       };

Now that we have this in place we need to expand our menu so we can send the id variables to the form.

  1. /**
  2.  * implements hook_menu
  3.  */
  4. function example_menu() {
  5.   $items = array();
  6.   $items['example/insert/nojs'] = array(
  7.     'page callback' => 'example_get_insert_form',
  8.     'page arguments' => array(2),
  9.     'access callback' => TRUE,
  10.     'type' => MENU_CALLBACK,
  11.   );
  12.   $items['example/insert/ajax'] = array(
  13.     'delivery callback' => 'ajax_deliver'
  14.   ) + $items['example/insert/nojs'];
  15.  
  16.   $items['example/insert/nojs/%'] = array(
  17.     'page callback' => 'example_get_insert_form',
  18.     'page arguments' => array(2, 3),
  19.     'access callback' => TRUE,
  20.     'type' => MENU_CALLBACK,
  21.   );
  22.   $items['example/insert/ajax/%'] = array(
  23.     'delivery callback' => 'ajax_deliver'
  24.   ) + $items['example/insert/nojs/%'];
  25.   return $items;
  26. }

If you implement this, you will find that the ajax callbacks aren't exactly using ajax yet. This is because as I said we don't have the luxury of drupals ajax handing when we are using the jquery ajax load function. In order to fix this we need to make a few changed to the ajax get form function so we compile the scripts and add then to the data being returned.

  1. /**
  2.  * Retrive the insert form.
  3.  */
  4. function example_get_insert_form($ajax, $ids = '') {
  5.   $is_ajax = $ajax === 'ajax';
  6.   $form = drupal_get_form('example_insert_form', $ids);
  7.   if ($is_ajax) {
  8.     $form = drupal_render($form);
  9.     // Generate the settings:
  10.     $settings = '';
  11.     $javascript = drupal_add_js();
  12.     if(isset($javascript['settings'], $javascript['settings']['data'])) {
  13.       $settings = '<script type="text/javascript">jQuery.extend(Drupal.settings, ';
  14.       $settings .= drupal_json_encode(call_user_func_array('array_merge_recursive', $javascript['settings']['data']));
  15.       $settings .=  ');</script>';
  16.     }
  17.     die($form . $settings);
  18.   }
  19.   else {
  20.     return $form;
  21.   }
  22. }

Now that this is done, the form uses ajax to add or remove fields to the form and it keeps the already entered data while doing to.

I hope this helps and if you have any questions let me now. For a working version of this see ting_object

Wysiwyg button with form #3

Welcome to the second article in my tutorial about building a wysiwyg button for drupal 7 which enables use to inserting tokens.

This will be a four step tutorial

This next part will cover how to expand the javascript wysiwyg button to include a form with multiple textfields which will the generate the token for displaying our entity.

First we need a form which will allow the user to enter the id of the entity to display using the token. For this we will make a drupal form in our .module file.

 

 

  1. function ting_token_insert_form($form, &$form_state) {
  2.   drupal_add_library('system', 'ui.dialog');
  3.   $form['#tree'] = TRUE;
  4.  
  5.   $form['objects'] = array(
  6.     '#type' => 'fieldset',
  7.     '#title' => t('Entities'),
  8.     '#prefix' => '<div id="ting-token-fieldset-wrapper">',
  9.     '#suffix' => '</div>',
  10.   );
  11.  
  12.   $form['objects']['entity1'] = array(
  13.     '#type' => 'textfield',
  14.     '#title' => t('Entity id'),
  15.     '#default_value' => '',
  16.   );
  17.   $form['objects']['entity2'] = array(
  18.     '#type' => 'textfield',
  19.     '#title' => t('Entity id'),
  20.     '#default_value' => '',
  21.   );
  22.  
  23.   $form['submit'] = array(
  24.     '#type' => 'submit',
  25.     '#value' => t('Submit'),
  26.     '#attributes' => array(
  27.       'class' => array('form-save-ids'),
  28.     ),
  29.   );
  30.  
  31.   return $form;
  32. }

Now with the form ready we need to create a way for our javascript to retrieve it, and for this we need a menu hook and a custom content function.

  1. /**
  2.  * implements hook_menu
  3.  */
  4. function example_menu() {
  5.   $items = array();
  6.   $items['example/insert/nojs'] = array(
  7.     'page callback' => 'example_get_insert_form',
  8.     'page arguments' => array(2),
  9.     'access callback' => TRUE,
  10.     'type' => MENU_CALLBACK,
  11.   );
  12.   $items['example/insert/ajax'] = array(
  13.     'delivery callback' => 'ajax_deliver'
  14.   ) + $items['example/insert/nojs'];
  15.   return $items;
  16. }
  17.  
  18. function example_get_insert_form($ajax) {
  19.   $is_ajax = $ajax === 'ajax';
  20.   $form = drupal_get_form('example_insert_form', $ids);
  21.   if ($is_ajax) {
  22.     $form = drupal_render($form);
  23.     die($form);
  24.   }
  25.   else {
  26.     return $form;
  27.   }
  28. }

The menu hook is pretty strait forward, it uses drupals ajax system to handle the data return. The example_get_insert_form returns a rendered form is the parameter is ajax and a form render array if it's not. Now we are ready to rewrite our javascript.

  1. // $Id$
  2. (function ($) {
  3.  
  4. Drupal.wysiwyg.plugins['tokenInsert'] = {
  5.  
  6.   /**
  7.    * Return whether the passed node belongs to this plugin (note that "node" in this context is a JQuery node, not a Drupal node).
  8.    *
  9.    * We identify code managed by this example plugin by giving it the HTML class
  10.    * 'tokenInsert'.
  11.    */
  12.   isNode: function(node) {
  13.     res = $(node).is('.tokenInsert');
  14.     return ($(node).is('.tokenInsert'));
  15.   },
  16.  
  17.   /**
  18.    * Invoke is called when the toolbar button is clicked.
  19.    */
  20.   invoke: function(data, settings, instanceId) {
  21.     // Typically, an icon might be added to the WYSIWYG, which HTML gets added
  22.     // to the plain-text version.
  23.     if (data.format == 'html') {
  24.       var content = this._getIds(data.content);
  25.       if(content !== '') {
  26.         settings.ids = content
  27.       } else {
  28.         settings.ids = '';
  29.       }
  30.     }
  31.     else {
  32.       var content = '<!--exampleInsert-->';
  33.     }
  34.     if (typeof content !== 'undefined') {
  35.       Drupal.wysiwyg.plugins.tokenInsert.insert_form(data, settings, instanceId);
  36.     }
  37.   },
  38.   insert_form: function (data, settings, instanceId) {
  39.     // Location, where to fetch the dialog.
  40.     var aurl = Drupal.settings.basePath + 'example/insert/ajax';
  41.     var dialogdiv = $('<div id="example-insert-dialog"></div>');
  42.     dialogdiv.load(aurl, function(){
  43.       var dialogClose = function () {
  44.         try {
  45.           dialogdiv.dialog('destroy').remove();
  46.         } catch (e) {};
  47.       };
  48.       var btns = {};
  49.       btns[Drupal.t('Cancel')] = function () {
  50.         $(this).dialog("close");
  51.       };
  52.       var $this = this;
  53.       dialogdiv.find('.form-save-ids').click(function(evt) {
  54.         evt.preventDefault();
  55.         var ids = [],
  56.           $items = dialogdiv.find('#ting-token-fieldset-wrapper .form-text');
  57.         $items.each(function() {
  58.           ids.push($(this).val());
  59.         });
  60.         settings.tingIds = ids;
  61.         var content = Drupal.wysiwyg.plugins['tokenInsert']._getPlaceholder(settings);
  62.         Drupal.wysiwyg.instances[instanceId].insert(content);
  63.         dialogdiv.dialog("close");
  64.       });
  65.       dialogdiv.dialog({
  66.         modal: true,
  67.         autoOpen: false,
  68.         closeOnEscape: true,
  69.         resizable: true,
  70.         draggable: true,
  71.         autoresize: true,
  72.         namespace: 'jquery_ui_dialog_default_ns',
  73.         dialogClass: 'jquery_ui_dialog-dialog',
  74.         title: Drupal.t('Insert'),
  75.         buttons: btns,
  76.         width: '70%',
  77.         close: dialogClose
  78.       });
  79.       dialogdiv.dialog("open");
  80.       Drupal.attachBehaviors();
  81.     });
  82.   },
  83.   /**
  84.    * Replace all <!--exampleInsert--> tags with the icon.
  85.    */
  86.   attach: function(content, settings, instanceId) {
  87.     content = content.replace(/<!--exampleInsert-->/g, this._getPlaceholder(settings));
  88.     return content;
  89.   },
  90.  
  91.   /**
  92.    * Replace the icons with <!--exampleInsert--> tags in content upon detaching editor.
  93.    */
  94.   detach: function(content, settings, instanceId) {
  95.     var $content = $('<div>' + content + '</div>');
  96.     $.each($('.exampleInsert', $content), function (i, elem) {
  97.       elem.parentNode.removeChild(elem);
  98.     });
  99.     return $content.html();
  100.   },
  101.  
  102.   /**
  103.    * Helper function to return a HTML placeholder.
  104.    */
  105.   _getPlaceholder: function (settings) {
  106.     if(settings.ids) {
  107.       return '[ting:teaser:' + settings.ids.join() + ']';
  108.     }
  109.     return '';
  110.   },
  111.  
  112.   /**
  113.    * Helper function to return ids from a placeholder.
  114.    */
  115.   _getIds: function (content) {
  116.     var ids = ''
  117.     if(content.indexOf('[ting:teaser:') === 0 && content.indexOf(']') === (content.length - 1)) {
  118.       content = content.replace('[ting:teaser:', '');
  119.       content = content.replace(']', '');
  120.       ids = content.split(',');
  121.     }
  122.     return ids;
  123.   }
  124. };
  125.  
  126. })(jQuery);

We can now insert two entities using our new insert form, but we still want more. We want to be able to insert multiple ids using ajax to add and remove id's, so stay tuned for the next tutorial.

Drupal development

Just a few modules I use when developing:

Devel

Great for debugging while developing.

Search krumo

Once you have outputted your devel data Search krumo enables you to search the data structure and to copy the path to your desired data directly.

Module filter

If your sites have lots of modules Module filter makes your life alot easier.

If you have any more suggestions leave me a comment.

Writing a custom panel context

I recently had to create a list of items based on data entered in a field and since the list would live in a panel I decided to create a new context to deliver the id to my views contextual filter.

I was guided by Yuriy Gerasimov on how to write the context and I changed a few things in order to make my case work.

Writing the context i very easy and requires only a very small module, so here is how I went about it:

First we need to create a new module, we could call it custom_context_example. I presume you know how to create a custom module otherwise lookup how on drupal.org.

Well in the custom_context_example.module we need to tell ctools that we are providing a new context type. We do this by defining the hook_ctools_plugin_directory and specifying the local path to our context folder.

  1. /**
  2.   * Implements hook_ctools_plugin_directory().
  3.   */
  4. function custom_context_example_ctools_plugin_directory($module, $plugin) {
  5.   if ($module == 'ctools' && $plugin == 'contexts') {
  6.     return "plugins/contexts";
  7.   }
  8. }

Next we create the plugins and contexts folder. In the contexts folder we can now create the context inc file. Lets call it custom_context_example.inc

The first thing we need to do here is define the plugin for ctools:

  1. /**
  2.  * Array to describe plugin.
  3.  */
  4. $plugin = array(
  5.   'title' => t('Custom context example'),
  6.   'description' => t('Provide token for referenced item.'),
  7.   'context' => 'ctools_plugin_create_custom_context_example',
  8.   'context name' => 'custom_context_example',
  9.   'keyword' => 'custom_context_example', // Provides a list of items which are exposed as keywords.
  10.   'convert list' => 'custom_context_example_convert_list', // Convert keywords into data.
  11.   'convert' => 'custom_context_example_convert',
  12. );

Next we need to write the create context function as well as the convert list and the actual convertion. 

The create context functiion ctools_plugin_create_context_example starts with defining the base context.

  1. function ctools_plugin_create_custom_context_example($empty, $data = NULL, $conf = FALSE) {
  2.   $context = new ctools_context('custom_context_example');
  3.   $context->plugin = 'custom_context_example';
  4.   $context->data = new stdClass();

next we need to get the object which contains the reference for this context, in this case I knew that the panel would be part of a term view but depending your case you would have to change the next part to match.

  1.   $term = menu_get_object('taxonomy_term', 2)
  2.  
  3.   if(!empty($term) && !empty($term->field_custom_context_example_data)) {
  4.     $field_lang = field_language('taxonomy_term', $term, 'field_custom_context_example_data');
  5.     $data= $term->field_custom_context_example_data[$field_lang][0]['value'];
  6.     $query = db_select('aggregator_category', 'a')
  7.       ->condition('a.title', $category, '=')
  8.       ->fields('a', array('cid', 'title', 'description'));
  9.     $result = $query->execute();
  10.    
  11.     foreach ($result as $record) {
  12.       $context->data->cid = $record->cid;
  13.       $context->data->title = $record->title;
  14.       $context->data->description = $record->description;
  15.     }
  16.   }  else {
  17.     $context->data->cid = null;
  18.     $context->data->title = '';
  19.     $context->data->description = '';
  20.   }
  21.   return $context;
  22. }

For this example I have allowed writers to enter the name of a aggregator category in a text field and now in the context creation function we can use the value in the field to query the aggregator_category table to get the record matching the entered value. On a side not in order for this to function a validation of the entered value should be added to the field, to ensure that the entered value exists as a category.

It this context is used where there is no term available e.g. the panel page admin UI we will set the default cid to null and the other values to empty strings, this enables us to envoke the "When the filter value is not available" options in view and it should be noted that the function must always be able to run or you will get an error in the admin UI, which is also a reason for the fallback to a null value.

Once we have the record we can create and fill the appropriate values in our context data object. This values will be used later in the token function. It is also important to note that the keys for the values should correspond to the columns in the table since this enables you to use them as contextual filter values in views.

Next we will add the two token functions:

  1. function custom_context_example_convert_list() {
  2.   return array(
  3.     'cid' => t('Feed term id'),
  4.     'title' => t('Feed term title'),
  5.     'description' => t('Feed term description'),
  6.   );
  7. }
  8.  
  9. function custom_context_example_convert($context, $type) {
  10.   switch ($type) {
  11.     case 'title':
  12.       return $context->data->title;
  13.     case 'description':
  14.       return $context->data->description;
  15.     case 'cid':
  16.       return $context->data->cid;
  17.   }
  18. }

and that is it. Now you can use the context as in your panel pages to provide context based on dynamic values. I hope this helps some of you out there.