When setting up a site using the
Drupal content management system, you'll often find that you need to define content types that have fields attached to them, beyond the default Title and Body. In Drupal version 7.x and later, this field functionality is planned to be in the core distribution of Drupal, but in 6.x and previous versions of Drupal, it is provided by the contributed
Content Construction Kit (CCK) module and the many associated modules that provide additional field types for CCK.
For some sites, you may find that you need to define fields that have multiple values; for example, you might want to place several images along the right side of a page (each one being an Image field). That is no problem, in itself -- CCK allows you to set any field to accept multiple values, and in version 2 of CCK, you can easily re-order, delete, and add new items to the list using a very nice AJAX interface.
But what if you need to associate, for instance, a caption and a taxonomy term with each of those images? Or in other words, what if you want to add fields to your content type as a group? With the current CCK version, you haveseveral choices of how to do that:
- Several separate fields: Create a multiple-valued field for the image, another for the caption, and another for the taxonomy term, and tell the people editing content on the site to try to keep them synchronized. This is not really workable in general -- users would need to scroll up and down the content entry screen to do their data entry, and inevitably someone will make a mistake and you'll have a caption or term associated with the wrong image. Also, for this specific case, Content Taxonomy as a multiple-valued field just gives you one multi-select list, so there is no way to choose a given taxonomy term more than once, or indicate the order (term 1 goes with image 1, etc.).
- Content sub-type: Create a second content type "Image Caption Term" to hold an image and its associated data. Then add a multiple "Node Reference" field to your original content type, which will associate your page with its images. This can be made to work, but in practice the editing is at best clumsy. (The Modal Nodereferencemodule may help in making the editing less clumsy.) Also, you'll end up with a lot of these little "Image Caption Term" items cluttering up your content management screens, which will confuse novice users.
- Content Multigroup module: Try out an experimental "Content Multigroup" module that is supposed to let you group multiple CCK fields together. As of this writing (April 2010), this module is not stable (not even released as an "alpha" version), and last time I tried it (October 2008), it didn't work at all with image fields. It may have improved since then.
- Flexifield Module: This may be working better now than the last time I tested it (October 2008).
- Custom CCK field: Create your own custom CCK field module that contains the desired grouping of information. Doing this is not very difficult, works very well, and is the subject of this article.
With that motivation, in this article I describe how to create a module that implements a custom "compound" or "grouped" CCK field for Drupal 6.x and CCK 2.x. The article shows two examples, both of which allow you to add the field to a CCK content type as a multiple-valued field, which will allow a content editor to add, edit, delete, and re-order all this information as a group. The two examples in this article (both of which can be downloaded below):
- A compound image, caption, and taxonomy field: Allows a user to upload an image, provide "alt" and "title" text for the image, enter a caption (arbitrary HTML text), and choose one or more taxonomy terms. You can click the image to the left to see what it looks like in action.
- A simpler person field, with fields for displayed name, job title, phone number, and email address, which you could use if you didn't want to have full nodes for each "person". It also provides an illustration of how to make a compound field that contains just simple text fields.
If you need the exact functionality of one of the two examples here, you can just use the modules I created (download them below) -- they should work. If you need a different set of fields, you should be able to follow the steps in this article to create your own compound CCK field. I'd also recommend checking out the
Link module as an example.
If creating your own Drupal module is beyond you, you can also hire Poplar ProductivityWare to create a module for your Drupal site -- click here to
contact Poplar ProductivityWare.
Here's what this article contains:
To understand this article, you will need a basic understanding of PHP, Drupal and its terminology, Drupal module development (hooks, Forms API, and basic module file structure), and the CCK and Views modules (i.e. how to define content types and views -- you don't need to know all about the inner workings of the modules). Check the
Downloads, Background, and References section below for more information, if you encounter terminology or concepts that you are unfamiliar with. The completed module is also provided as a download in that section of the article -- it is GPL licensed, so you can use it and modify it as you wish under the terms of the license. You might want to download it now so you can refer to it as you read this article. The actual module code has more comments, and a couple of functions that were omitted from the article.
And before going any further, I'd like to thank Josh Kopel of Kolaboration Studio, who sponsored part of the development of the initial image/caption/taxonomy field module and the original article.
Getting Started
The first step in creating our module is to choose some names:
- The first module we are creating implements a compound CCK field for an image, caption, and taxonomy term. So, I have chosen to call the CCK field "Image Caption Taxonomy", with machine-readable name "img_cap_tax". To distinguish the name of the CCK field from the name of the module, I have chosen to call the module "Image Caption Taxonomy Field", and to use the machine-readable name "img_cap_tax_fld" for the module; I'll refer to this machine-readable name as "(module)".
- The second module is for a person field, so the CCK field will be called "Person", with machine readable name "person". The module will be "Person Field", with machine readable name "person_fld".
Below, when you see "(field)", it refers to the machine-readable name of the field, and "(module)" refers to the machine-readable name of the module.
Next, we need to create files "(module).info" and "(module).module", in order for this to be a Drupal module. A couple of notes:
- Because this module implements a CCK field, we can rely on the CCK module to take care of all the database tables for us. For that reason, we don't need a .install file for this module to define the database schema.
- We do need to define some module dependencies in the .info file, as we will see below. Check the completed .info file in the module download for a list.
- It is possible to define multiple CCK fields in a single module. However, having done it once, I really wouldn't recommend it -- it makes the module file confusing, and less "modular" (i.e. someone might only need one of your fields, but have to load the entire module with several fields in order to get that functionality on their site).
Defining the Field
Now that we have our files created, the next step is to implement some core and CCK-specific hooks that tell CCK about our field. These hook implementations go into the (module).module file.
CCK hook_field_info()
First, we'll give CCK the field's machine-readable name, human-readable name, and a longer description, by implementing the CCK hook_field_info(). In the first example (the second example is nearly the same):
function img_cap_tax_fld_field_info() {
return array(
'img_cap_tax' => array(
'label' => t('Image Caption Taxonomy'),
'description' => t('Stores an image file, text for alt and title tags, a caption, and a taxonomy term'),
)
);
}
As you can see, the return value for hook_field_info() is an associative array, keyed on the machine-readable name of the CCK field we are defining. Each element of the returned array is an associative array, with components 'label' giving the human-readable name of our field, and 'description' giving a more verbose description. And since all human-readable text in Drupal modules should be internationalization-ready, we enclose it in the t() function.
Core hook_install(), hook_uninstall(), hook_enable(), hook_disable()
Next, we will implement the core Drupal hook_install(), hook_uninstall(), hook_enable(), and hook_disable() so that our field will be properly added and removed from Drupal and CCK. These all work the same way: we just tell Drupal to let CCK handle the process through its content_notify() function, which takes care of all of the details. For example, here is the function for the core Drupal hook_install():
function img_cap_tax_fld_install() {
content_notify('install', 'img_cap_tax');
}
The other three hooks are exactly the same -- just substitute "uninstall", "enable", or "disable" for "install". Note that it is possible to put these functions into a module's (module).install file instead of (module).module, but since they are so small in this case (and we have nothing else to go in the (module).install file), I've chosen to leave them in (module).module.
One other detail: these functions are calling the CCK module function content_notify() directly. So, we need to make sure that function is loaded when we call it:
- Make our module dependent on the CCK module (whose machine-readable name is "content") -- that dependency goes into the .info file.
- Verify which file the content_notify() function is defined in -- if it had been in an include file rather than the main .module file, we would have needed to put an include directive in the hook_init() implementation in our module.
We'll need to do this for other module functions we'll be calling directly later in this article, so I'll just suggest that you look at the completed module's .info file (or the
reference section below) to see the module dependencies, and the img_cap_tax_fld_init() function (implementation of hook_init() ) to see what include files we needed to load explicitly.
CCK hook_field_settings()
Now, we need to tell CCK about the "settings" for this field, using CCK hook_field_settings(). This is a sort of catch-all hook for CCK, with several "operations", each one letting our field module give a different piece of information to CCK. The example CCK field based on an image needs to behave very similarly to the Image field from the existing ImageField module. That field, in turn (in the current implementation of ImageField) is actually just a FileField file field, with a special "widget" and "formatter" (see sections below) that makes it show as an image rather than a file. So, for the first example field, we will borrow heavily from the FileField module in our field settings function. For the operations that need to do something different from FileField, we'll call into other functions, which are described in the next section. Here's the function:
function img_cap_tax_fld_field_settings( $op, $field ) {
switch( $op ) {
case 'form':
return img_cap_tax_fld_field_settings_form( $field );
case 'save':
return img_cap_tax_fld_field_settings_save( $field );
default:
return filefield_field_settings( $op, $field );
}
}
(Of course, we'll need to make sure that FileField is a dependency of this module, as described above.)
For the second example, which is built up of text fields, we need to do something slightly different, because we don't have the FileField module around to help us with settings. We don't need any settings for this field, but we do need to define the database storage:
function person_fld_field_settings($op, $field) {
switch ($op) {
case 'database columns':
$columns['displayed_name'] = array('type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'sortable' => TRUE, 'default' => '');
$columns['job_title'] = array('type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'sortable' => TRUE, 'default' => '');
$columns['phone'] = array('type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'sortable' => FALSE, 'default' => '');
$columns['email'] = array('type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'sortable' => FALSE, 'default' => '');
return $columns;
}
}
For another example of a text-based field, but which has a settings form, you can check out the
Link module.
filefield_field_settings() Operations Functions (image example only)
The next step (for the image field example only) is to define the individual operations functions that are called from the field settings function in the previous section. By convention, in this module the functions are located in an include file, (module)_field.inc. Let's start with the "form" and "save" operations, which define the field settings form the user can use to customize the field (apart from the sections already provided by the FileField module), and which fields in the settings form should be saved to the database. There is also a "validate" operation, which lets you run checks on the submitted data, but we don't need anything beyond what FileField does for this, so I won't describe that here.
In our field settings form, we want to do start with what the FileField module does for its field, add in most of what the Content Taxonomy module does for its field, and also allow the user to choose between plain text and having an Input Format (filtered) for the caption field.
We'll also make a couple of changes to what Content Taxonomy does. First, Content Taxonomy has a setting that allows you to take the taxonomy choices from its field and apply them to the node as a whole. We won't allow for that option on our field, as it doesn't make sense in a compound field. Second, we also want to let the user choose whether the taxonomy term is optional, and whether it should be multiple-valued. We will also add some fieldsets to the form for grouping.
So, with all of that motivation, here are our "form" and "save" operations functions:
function img_cap_tax_fld_field_settings_form( $field ) {
$form1 = filefield_field_settings( 'form', $field );
$form2 = content_taxonomy_field_settings( 'form', $field );
$form2['save_term_node']['#type'] = 'hidden';
$form2['taxonomy_group'] = array(
'#type' => 'fieldset',
'#title' => 'Taxonomy',
'#collapsible' => 0,
);
$form2['taxonomy_group']['vid'] = $form2['vid'];
unset( $form2['vid'] );
$form2['taxonomy_group']['allow_multiple'] = array(
'#type' => 'checkbox',
'#title' => t('Allow multiple taxonomy terms'),
'#default_value' => is_numeric($field['allow_multiple']) ? $field['allow_multiple'] : 0,
'#description' => t('If this option is checked, the user can select multiple taxonomy terms for each image; otherwise, at most one.'),
);
$form2['taxonomy_group']['required_term'] = array(
'#type' => 'checkbox',
'#title' => t('Taxonomy required'),
'#default_value' => is_numeric($field['required_term']) ? $field['required_term'] : 0,
'#description' => t('If this option is checked, the user must select at least one taxonomy term for each image; otherwise, it is optional.'),
);
$form2['hierarchical_vocabulary']['#weight'] = 100;
$form3 = array( 'text_processing' => array(
'#type' => 'radios',
'#title' => t('Text processing for Caption'),
'#default_value' => is_numeric($field['text_processing']) ? $field['text_processing'] : 0,
'#options' => array( 0 => t('Plain text'), 1 => t('Filtered text (user selects input format)')),
));
return $form1 + $form3 + $form2;
}
function img_cap_tax_fld_field_settings_save( $field ) {
$flds1 = filefield_field_settings( 'save', $field );
$flds2 = content_taxonomy_field_settings( 'save', $field );
$flds2[] = 'allow_multiple';
$flds2[] = 'required_term';
$flds3 = array( 'text_processing' );
return array_merge( $flds1, $flds2, $flds3 );
}
There is also a hook_field_settings() operation for "database columns", where you can define the database columns that CCK will use to store your field's data (you can see this in the second example above). For the image-based field, however, the FileField module creates a serialized "data" field in the database for us to use. We'll put all of the alt, title, caption, and taxonomy information into the "data" array, so we don't have to add any additional database columns.
Finally, there is a hook_field_settings() operation for "views data", which allows a CCK field to return information about how it can be used for sorting and filtering in the Views module. (Any CCK field can automatically be included as a Field in Views, so we don't have to worry about that.) Keeping in mind that our field is mainly useful if it is added to a content type as a multiple-valued field (otherwise, you wouldn't really need a compound field), probably sorting the nodes in a view based on some aspect of this field doesn't really make sense. However, I can envision filtering a view using this field in a couple of ways:
- Whether or not the content has at least one image attached to it. This should come from the FileField module.
- Whether a particular taxonomy term is present or not.
As of this writing, this hasn't been implemented. Sorry!
CCK hook_field()
The next set of things we need to define is included in CCK hook_field(), which basically tells CCK if it needs to do anything special when a the field is loaded from or saved to the database. For the image field example, there is a special action: move the image file from its temporary location to a permanent location, and make note of that location in the database field (the FileField module has a function that does this). Also, all text fields need to be "sanitized", for both field examples, according to the input format (i.e. make sure it contains only the allowed HTML tags, or no text if the input format is plain text). And the email address in the person field example needs to be validated. All of these are operstaions in hook_field().
For the first (image) field example, here's the hook_field() implementation, which calls a function we'll define below for the "sanitize" operation, and lets FileField handle the rest:
function img_cap_tax_fld_field($op, $node, $field, &$items, $teaser, $page) {
if( $op == 'sanitize' ) {
img_cap_tax_fld_field_sanitize( $node, $field, $items, $teaser, $page );
}
return filefield_field( $op, $node, $field, $items, $teaser, $page );
}
In our "sanitize" operation function, we will do pretty much what the Text field module in CCK does to sanitize text fields: figure out which input format was chosen, filter using that input format, and save the result with a "safe" prefix so it can be used for theming. Here is the function (I've put it into the (module)_field.inc file):
function img_cap_tax_fld_field_sanitize($node, $field, &$items, $teaser, $page) {
$isplain = empty( $field['text_processing'] );
$check_access = is_null( $node ) ||
( isset($node->build_mode) && $node->build_mode == NODE_BUILD_PREVIEW );
foreach( $items as $delta => $item ) {
$dat = $item['data'];
if( !is_array( $dat )) {
$dat = unserialize( $dat );
}
$text = isset( $dat['caption'] ) ? $dat['caption'] : '';
if( $isplain ) {
$text = check_plain( $text );
} else {
$text = check_markup( $text, $item['format'], $check_access );
}
$items[$delta]['safe_caption'] = $text;
}
}
One detail to note here is that I found that the "data" array from FileField is sometimes coming into this function in a serialized format (I'm not sure exactly why). So this function checks to see whether it needs to be unserialized before making use of it.
In the second example, as we mentioned, we need to support sanitation and validation. The validation checks to see whether the email address is in a valid format, and the sanitation ensures that only plain text has been entered for the other field components. Here's the function:
function person_fld_field($op, &$node, $field, &$items, $teaser, $page) {
switch ($op) {
case 'validate':
if (is_array($items)) {
foreach ($items as $delta => $item) {
if ($item['email'] != '' && !valid_email_address(trim($item['email']))) {
form_set_error($field['field_name'],t('"%mail" is not a valid email address',array('%mail' => $item['email'])));
}
}
}
break;
case 'sanitize':
foreach ($items as $delta => $item) {
foreach ( $item as $col => $dat ) {
$items[$delta]['safe_' . $col ] = check_plain($item[ $col ]);
}
}
break;
}
}
CCK hook_content_is_empty(), hook_default_value()
There are a few more pieces of information CCK needs about our fields: how to tell if it is "empty" of information (CCK hook_content_is_empty() ), and what the default value should be (CCK hook_default_value() ). In the first example, since the primary component of our field is an image, we will say that the field is considered "empty" if it has no image file, and we'll let FileField handle that:
function img_cap_tax_fld_content_is_empty( $item, $field ) {
return filefield_content_is_empty( $item, $field );
}
In the second example, the person field is considered empty if there is no name entered:
function person_fld_content_is_empty($item, $field) {
if (empty($item['displayed_name'])) {
return TRUE;
}
return FALSE;
}
In our image/caption field we don't have a default value, actually (it isn't too common), but just in case FileField implements a default value setting in the future, we'll let FileField define the default value:
function img_cap_tax_fld_default_value(&$form, &$form_state, $field, $delta) {
return filefield_default_value($form, $form_state, $field, $delta);
}
Our person field doesn't have a default value either. If you want a default value for your field, you'll probably need to set something up in your settings form so that your users can define what the default value is, and then use the default value hook to set it up. I don't know of a good example for this.
Defining the Widget
Now that we have the CCK field itself defined, our next task is to define a "widget", which is the CCK name for a form used to edit the field.
For the first example, our widget will need to provide a way to upload the image file and preview/change the uploaded file; enter the alt, title, and caption text; choose an input format for the caption; and choose a taxonomy term (we'll use a drop-down select list for that). As before, we'll let FileField and Content Taxonomy handle their parts, so all we'll need to do is add the text fields and image format. We'll create a widget called "Image, Caption, Taxonomy Select", with machine-readable name "img_cap_tax_sel_widget", referred to below as "(widget)".
For the second example, our person widget will just need to let us enter text for the name, position, phone, and email address. We'll define a widget with a human-readable label of "Text fields", and machine-readable name "person_entry", referred to below as "(widget)".
I will note here that it is possible for a CCK field module to define more than one widget for a single field. For instance, the Content Taxonomy module has several options: a drop-down select list, a type-ahead auto-complete text field, etc. There are notes below on what you'd need to change to add additional widgets.
CCK hook_widget_info()
The first step in providing a widget is to give CCK the basic widget information (machine-readable name, human-readable name, what field types it applies to, etc.), by implementing the CCK hook_widget_info():
function img_cap_tax_fld_widget_info() {
return array(
'img_cap_tax_sel_widget' => array(
'label' => t('Image, Caption, Taxonomy Select'),
'field types' => array('img_cap_tax'),
'multiple values' => CONTENT_HANDLE_CORE,
'callbacks' => array('default value' => CONTENT_CALLBACK_CUSTOM),
'description' => t('An edit widget for Image Caption Taxonomy fields that allows upload/preview of the image, and chooses taxonomy terms from a drop-down select list.' ),
),
);
}
As is common in Drupal, the return value of this hook is an associative array of associative arrays, and if we wanted to provide multiple widgets in our module, we would just need to add additional elements to the outer array.
The person example is similar:
function person_fld_widget_info() {
return array(
'person_entry' => array(
'label' => t('Text fields'),
'field types' => array('person'),
'multiple values' => CONTENT_HANDLE_CORE,
'callbacks' => array(
'default value' => CONTENT_CALLBACK_DEFAULT,
),
),
);
}
FAPI hook_elements()
Next, we need to define the widget's data entry form and how to process it. This is done using the core Forms API hook_elements(), which returns an associative array with one element (or more, if you have multiple widgets) whose key is the machine-readable name of our widget, and whose value is an associative array that either defines the form completely (as is done in the Content Taxonomy module), or gives a "process" callback function and some other information (as is done in ImageField; this is also the method recommended, in general, for the Forms API). We'll use the process callback method, and in the first example, let ImageField and FileField set up most of the array:
function img_cap_tax_fld_elements() {
$imgel = imagefield_elements();
$elements = array( 'img_cap_tax_sel_widget' => $imgel[ 'imagefield_widget' ]);
$elements['img_cap_tax_sel_widget']['#process'][] = 'img_cap_tax_fld_widget_process';
$elements['img_cap_tax_sel_widget']['#element_validate']= array('img_cap_tax_fld_widget_validate');
return $elements;
}
Here's the second example, which is even simpler:
function person_fld_elements() {
$elements = array( 'person_entry' =>
array(
'#input' => TRUE,
'#process' => array( 'person_fld_person_entry_process' ),
),
);
return $elements;
}
We will need to define the "process" callback functions for both examples, which is what actually defines the form elements -- see section below. Also, for the image example we have a custom validate callback, because the one used by FileField (and ImageField) does not work for our module -- see section below. One other detail is that we'll also need to register a themeable element for this widget form; we'll cover that in the "Formatter and Theming" section below with the other theme information, and show the theming functions below.
Process and Validate Callbacks for Widget Form
The "process" callbacks from the form array of the previous section are what actually define and return the form elements (a Forms API array). In the first example, we've left ImageField to take care of its processing, and added our processing function to the end; we'll put the function in file (module)_widget.inc. So, our function needs to add a text area and input format selector for the "caption" field and a drop-down list for the Taxonomy term. All of this information will get stored in FileField's "data" array, and we'll have to be careful to choose the same array keys as the Content Taxonomy module for the taxonomy fields. For the caption's input format, we want to choose "format" as the array key, because this is the standard field name for input formats. (Some other modules depend on that convention; for example, the WYSIWYG editor module chooses which fields to attach editors to by looking for textarea fields that are followed by "format" fields.)
function img_cap_tax_sel_widget_process($element, $edit, &$form_state, $form) {
$defaults = $element['#value']['data'];
if( !is_array( $defaults )) {
$defaults = unserialize( $defaults );
}
$field = content_fields($element['#field_name'], $element['#type_name']);
$element['data']['caption'] = array(
'#title' => t( 'Caption' ),
'#type' => 'textarea',
'#rows' => $field['widget']['rows'],
'#cols' => $field['widget']['cols'],
'#default_value' => $defaults['caption'],
'#weight' => 4,
);
if (!empty($field['text_processing'])) {
$filt = isset( $defaults['format'] ) ? $defaults['format'] : FILTER_FORMAT_DEFAULT;
$par = $element['#parents'];
$par[] = 'data';
$par[] = 'format';
$element['data']['format'] = filter_form( $filt, 1, $par );
$element['data']['format']['#weight'] = 5;
}
$mult = $field['allow_multiple'];
$req = $field['required_term'];
$opts = content_taxonomy_allowed_values( $field );
if( !$req && !$mult ) {
$none = theme( 'content_taxonomy_options_widgets_none', $field );
$opts = array( '' => $none ) + $opts;
}
$element['data']['value'] = array(
'#title' => t( 'Taxonomy Terms' ),
'#type' => 'select',
'#default_value' => $defaults['value'],
'#options' => $opts,
'#weight' => 6,
);
if( $mult ) {
$element['data']['value']['#multiple'] = TRUE;
}
return $element;
}
Note that the rows and columns settings for the caption, as well the settings for whether the taxonomy term is a single or multiple select and required or not, come from the widget settings form -- see CCK hook_widget_settings() below.
In the second example, we just need to define the text fields for entering person information:
function person_fld_person_entry_process($element, $edit, &$form_state, $form) {
$defaults = $element['#value'];
$field = content_fields($element['#field_name'], $element['#type_name']);
$element['displayed_name'] = array(
'#title' => t( 'Name' ),
'#type' => 'textfield',
'#default_value' => $defaults['displayed_name'],
'#weight' => 2,
);
$element['job_title'] = array(
'#title' => t( 'Job Title' ),
'#type' => 'textfield',
'#default_value' => $defaults['job_title'],
'#weight' => 3,
);
$element['phone'] = array(
'#title' => t( 'Phone' ),
'#type' => 'textfield',
'#default_value' => $defaults['phone'],
'#weight' => 4,
);
$element['email'] = array(
'#title' => t( 'Email' ),
'#type' => 'textfield',
'#default_value' => $defaults['email'],
'#weight' => 5,
);
return $element;
}
The first example module also needs a validation callback, which will verify information when the node edit form is submitted. In FileField, the validation checks to make sure the file exists, and that if the field can only reference a file if it has been uploaded via this field module (so that the field will not be deleted erroneously if the node is deleted). Unfortunately, the existing FileField validate function (filefield_widget_validate() in filefield_widget.inc) will not work as-is for our module, because it tacitly assumes that the field type name is 'filefield', which is not the case for our field. So, we have to create our own function to replicate it. Assuming that the person using this field is uploading their own images and not doing anything crazy, all we really need to do is make sure the file exists. Here's the function:
function img_cap_tax_fld_widget_validate(&$element, &$form_state) {
if (empty($element['fid']['#value'])) {
return;
}
$field = content_fields($element['#field_name'], $element['#type_name']);
$ftitle = $field['widget']['label'];
if ( !( $file = field_file_load($element['fid']['#value']))) {
form_error($element, t('The file referenced by the %field field does not exist.', array('%field' => $ftitle )));
}
}
Widget theme function
In order to display the editing widget, we need to define a theme function, theme_(widget), which is called as an envelope for each item when it is added to the node editing form. This function is simple for both cases: we just tell Drupal to render the form, and our processing functions will take care of the rest. Here are the functions for the two examples:
function theme_img_cap_tax_sel_widget(&$element) {
return theme('form_element', $element, $element['#children']);
}
function theme_person_entry($element) {
return $element['#children'];
}
Note that there appears to be a rather annoying choice in the FileField CSS file that makes the editing widget get very narrow. You may want to add this to your theme's CSS file:
.filefield-element .widget-edit, .filefield-element .widget-preview {
float: none;
}
CCK hook_widget()
The next hook we need to implement in order to define the editing widget is CCK hook_widget(). This hook will be called each time one of our fields is added to the form, with the $delta parameter set to the multiple-value index (0 for the first item, 1 for the second, etc.). The return value is a Forms API array that should set up default values for the form and define callbacks. In the first example, as usual, we'll let FileField and ImageField handle most of the details (Content Taxonomy doesn't need to do anything special), by calling the filefield_widget() function. That function assumes that:
- Our module contains a file called (module)_widget.inc (the filefield_widget() function will load it).
- The $items['delta'] array has been set up with an array of the default values for the text fields in our compound field, before filefield_widget() is called.
So, our hook_widget() implementation is:
function img_cap_tax_fld_widget(&$form, &$form_state, $field, $items, $delta = 0) {
if (empty($items[$delta])) {
$items[$delta] = array('alt' => '', 'title' => '', 'caption' => '', 'value' => 0);
}
$element = filefield_widget($form, $form_state, $field, $items, $delta);
$element['#upload_validators'] += imagefield_widget_upload_validators($field);
return $element;
}
The person field example's hook_widget() is even simpler, since all it has to do is tell CCK to load the field type we've already defined:
function person_fld_widget(&$form, &$form_state, $field, $items, $delta = 0) {
$element = array(
'#type' => $field['widget']['type'],
'#default_value' => isset($items[$delta]) ? $items[$delta] : '',
);
return $element;
}
If your module contains more than one widget, you'll want to do something like this in your hook_widget() implementation:
switch( $field['widget']['type'] ) {
case 'first_widget_machine_name':
(code for this widget)
break;
case 'second_widget_machine_name':
(code for this widget)
break;
}
CCK hook_widget_settings() (image field only)
The final piece in defining the widget is to create a widget settings form, which is done by implementing CCK hook_widget_settings(). Like many of the other CCK hooks, hook_widget_settings() has several operations: one to create a settings form, one to validate the form, and one to save the form. Our second example has no settings for the widget, so we don't need to implement this hook. For the first example, we'll let ImageField take care of validation, and build our own functions for the form and save operations:
function img_cap_tax_fld_widget_settings( $op, $widget ) {
switch ($op) {
case 'form':
return img_cap_tax_fld_widget_settings_form($widget);
case 'validate':
return imagefield_widget_settings_validate($widget);
case 'save':
return img_cap_tax_fld_widget_settings_save($widget);
}
}
Note that if you have multiple widgets in your module, you can do a switch on $widget['type'] to handle the different widgets in your hook_widget_settings() implementation.
filefield_widget_settings() callbacks (image field only)
There are two operation callbacks defined in our hook_widget_settings() implementation for the image field example. The 'form' operation returns the widget settings form; the 'save' operation returns a list of which form data should be saved to the database.
First, let's work on the settings form. The existing FileField and ImageField modules have a widget settings form that lets the user set a file path, allowed file extensions, maximum file size, and other settings. We'll use that form, but modify it so that alt and title can always be customized rather than leaving those as options as they are in ImageField (we still want the settings to be there, so that other ImageField functions we call will have the right values set). The Content Taxonomy select list widget has settings for indentation and grouping terms, which we'll also want. Finally, most multi-line text fields let the user choose how many rows and/or columns to display, so we'll want that setting for the caption field. Putting this all together:
function img_cap_tax_fld_widget_settings_form( $widget ) {
$form = imagefield_widget_settings_form( $widget );
$form['custom_alt'] = $form['alt_settings']['custom_alt'];
$form['custom_alt']['#type'] = 'hidden';
$form['custom_alt']['#value'] = 1;
$form['alt'] = $form['alt_settings']['alt'];
$form['alt']['#type'] = 'hidden';
$form['alt']['#value'] = '';
unset( $form['alt']['#suffix'] );
unset( $form['alt_settings'] );
$form['custom_title'] = $form['title_settings']['custom_title'];
$form['custom_title']['#type'] = 'hidden';
$form['custom_title']['#value'] = 1;
$form['title'] = $form['title_settings']['title'];
$form['title']['#type'] = 'hidden';
$form['title']['#value'] = '';
unset( $form['title']['#suffix'] );
unset( $form['title_settings'] );
$rows = (isset($widget['rows']) && is_numeric($widget['rows'])) ? $widget['rows'] : 5;
$form['rows'] = array(
'#type' => 'textfield',
'#title' => t('Number of rows in caption field'),
'#default_value' => $rows,
'#element_validate' => array('_text_widget_settings_row_validate'),
'#required' => TRUE,
'#weight' => 8,
);
$cols = (isset($widget['cols']) && is_numeric($widget['cols'])) ? $widget['cols'] : 40;
$form['cols'] = array(
'#type' => 'textfield',
'#title' => t('Number of columns in caption field'),
'#default_value' => $cols,
'#element_validate' => array('_text_widget_settings_row_validate'),
'#required' => TRUE,
'#weight' => 9,
);
$form2 = content_taxonomy_options_widget_settings( 'form', $widget );
$form2['settings']['#title'] = t( 'Settings for Taxonomy' );
$form = $form + $form2;
return $form;
}
We don't need to do anything special for the 'validate' operation, as neither ImageField nor Content Taxonomy needs us to do anything beyond what FileField does. So, we won't define this function. The 'save' operation returns a list of fields to save from the settings form:
function img_cap_tax_fld_widget_settings_save( $widget ) {
$arr = imagefield_widget_settings_save( $widget );
$arr[] = 'rows';
$arr[] = 'cols';
$arr2 = content_taxonomy_options_widget_settings( 'save', $widget );
$arr2[] = 'allow_multiple';
$arr2[] = 'required_term';
return array_merge( $arr, $arr2 );
}
Defining the Formatter and Theming
Having now defined our field and the editing widget, the final thing we need to do is to define how it will look to someone who is visiting the site (of course, the theme can override this). We do this by defining a "formatter" for the field; like widgets, there can be more than one formatter defined for a field, but we'll only create one in this example. We'll give our formatter the machine name 'default', which we'll refer to as "(formatter)" below, and human-readable name 'Image with Caption and Taxonomy Terms' for the first example, and 'Person display' in the second example. Note that machine-readable names for formatters do not need to be unique, except within your module.
CCK hook_field_formatter_info()
To tell CCK about our formatter, we implement CCK hook_field_formatter_info():
function img_cap_tax_fld_field_formatter_info() {
return array(
'default' => array(
'label' => t( 'Image with Caption and Taxonomy Terms' ),
'field types' => array( 'img_cap_tax' ),
),
);
}
(This is nearly identical for the person field example.)
Core hook_theme()
Once the formatter has been defined, CCK will assume there is a corresponding themeable element called (module)_formatter_(formatter), which we need to register in the core hook_theme(). As noted above, we also need to register the themeable element for the widget form, so our hook_theme() implementation for the first example is (the second is quite similar):
function img_cap_tax_fld_theme() {
return array(
'img_cap_tax_sel_widget' => array(
'arguments' => array('element' => NULL),
),
'img_cap_tax_fld_formatter_default' => array(
'arguments' => array('element' => NULL),
),
);
}
Theme Functions
Finally, we need to create a theme_(element) function for the formatter (the widget theme functions were given in previous sections). For the first example, the formatter theme function displays the image, caption, and taxonomy term:
function theme_img_cap_tax_fld_formatter_default( $element = NULL ) {
if( empty( $element['#item'] )) {
return '';
}
$img = theme( 'imagefield_formatter_image_plain', $element );
$cap = $element['#item']['safe_caption'];
$tax = '';
$sep = '';
$val = $element['#item']['data']['value'];
if( !is_array( $val )) {
$val = array( $val );
}
foreach( $val as $tid ) {
$term = taxonomy_get_term( $tid );
$tax .= $sep . check_plain( $term->name );
$sep = ', ';
}
return '' .
'
' . $img . '
' .
'
' . $cap . '
' .
'
' . $tax . '
' .
'
';
}
For the second example:
function theme_person_fld_formatter_default($element = NULL) {
if(empty($element['#item'])) {
return '';
}
$stuff = $element['#item'];
$flds = array('displayed_name', 'job_title', 'phone');
$ret = '';
$sep = '';
foreach($flds as $fld) {
if(!empty($stuff['safe_' . $fld ])) {
$ret .= $sep . '
' . $stuff['safe_' . $fld ] . '';
$sep = "
\n";
}
}
if(!empty($stuff['safe_email' ])) {
$ret .= $sep . '
' . $stuff['safe_email' ] . "";
}
$ret .= '
';
return $ret;
}
Final Details
One further detail... If you are using the "private" file download method on your Drupal site (in the Site Configuration section of administration, under File System), you will find that the Image Caption Taxonomy field doesn't work unless you implement hook_file_download(). The reason for this is that when you use the private file download method, the system asks each module to verify that a given file can be downloaded, before allowing someone access to that file. So, our field module needs to tell the system which files it contains, to allow them to be downloaded. This function is included in the sample module.