Remember - although grouping the callbacks defined in these properties into the same .inc file can be handy, you're not bound to doing so. If you have a visibility checker that you want to share across a lot of content types, for example, consider defining the visibility checker in the main .module file while keeping the rest of the plugin in its own .inc file.
Some unnecessary duplication and inelegancies persist in the logic behind content type plugin properties; this is particularly noteworthy in the case of the add/edit property distinctions. These issues are allowed to persist largely for legacy compatibility reasons.
Plugin declaration functions must adhere to a particular naming convention; see panels_get_include_directories() for details.
Note that the plugin definition array provided by this sample does NOT define a coherent, working content type; displaying the full range of the plugin's flexibility makes it impossible to do so. For working examples, see the various plugins defined in the panels/content_types directory.
Keep in mind when choosing a type name that namespace collisions are both silent and destructive (the Panels engine will not emit any errors, it will simply overwrite data). Such collisions will always be decided in favor of the last plugin processed; processing order order is generally determined by the weight of the module (in the system table) defining the plugin.
All the plugins that come packaged with Panels necessarily obey a strict naming convention: the array key used for $items is the same as the name of the file itself, as well as being the same as the string prefixed by the module name (in this case, 'panels_'), and affixed by a string that indicates the type of plugin (in this case, 'panels_content_types'). Your modules need not follow the same conventions, although it is recommended that you do if possible; again, refer to panels_get_directories().
function panels_SAMPLE_CT_panels_content_types() {
$items['SAMPLE_CT'] = array(
'title' => t('Sample Content Type'),
'weight' => -10,
'single' => TRUE,
'content_types' => 'panels_admin_content_types_SAMPLE_CT',
'render callback' => 'panels_content_SAMPLE_CT',
'add callback' => 'panels_admin_add_SAMPLE_CT',
'edit callback' => 'panels_admin_edit_SAMPLE_CT',
'add validate callback' => 'panels_admin_validate_SAMPLE_CT',
'edit validate callback' => 'panels_admin_validate_SAMPLE_CT',
'add submit callback' => 'panels_admin_submit_SAMPLE_CT',
'edit submit callback' => 'panels_admin_submit_SAMPLE_CT',
'title callback' => 'panels_admin_title_SAMPLE_CT',
'editor render callback' => 'panels_admin_pane_render_SAMPLE_CT',
'render last' => TRUE,
'visibility control' => 'panels_admin_visibility_control_SAMPLE_CT',
'visibility submit' => 'panels_admin_visibility_submit_SAMPLE_CT',
'visibility check' => 'panels_content_visibility_check_SAMPLE_CT',
'visibility serialize' => TRUE,
'role-based access' => FALSE,
'roles and visibility' => TRUE,
'form control' => 'panels_admin_form_control_SAMPLE_CT',
);
return $items;
}
| Property Name | Data Type | Required? | Default Value | Dependencies | Notes |
| title | string | Yes | None | The title that will be used for this pane on the 'Add Content' modal form, and in the display content editor in general. This is a purely internal setting; normal users will never see it. | |
| weight | Integer | No | 0 | None | Standard drupal weighting concept at work here; all it determines is the position of this content type's icon relative to the other content type icons on the general Panels configuration forms. See panels_common_settings(). |
| single | Boolean | No | FALSE | None | Indicates that this content type plugin provides only a single content type. Currently, this setting is ONLY used in figuring out how to group the content type on the general Panels configuration forms; see panels_common_settings(). Check out panels_admin_content_types_block() for an example of how one plugin can used define multiple content types (technically, multiple subtypes) |
| render last | Boolean | No | FALSE1 | none | If set to TRUE, this pane will be pushed to the back of the line during the render routine. See panels_render_panes(). |
| visibility serialize | Boolean | Yes2,3 | FALSE | visibility control | If TRUE, then contents of $pane->visibility will be serialized before being saved to the database. This should be set as TRUE if, for example, your visibility form widget uses checkboxes (and therefore generates an array), as opposed to if your widget uses radios (and therefore generates an integer that can be stored directly). See panels_content_config_form_submit() and panels_save_display() to better understand how this works. |
| role-based access | Boolean | No | TRUE4 | none | Boolean setting to indicate whether you want the your content type to utilize the Panels API's built-in access system, which is based on drupal user roles. Set this to FALSE to disable role-based access. |
| roles and visibility | Boolean | No | FALSE1 | visibility control | If you want your content type to use both your custom visibility logic and Panels' built-in roles-based access system, then set this to TRUE. Setting 'role-based access' to TRUE is not sufficient; see panels_ajax_ct_preconfigure() to understand how this works. If you use both systems, panels_pane_access() will AND the results together when determining pane visibility. |
Parameters that are passed by reference from the function side (through call_user_func_array()) are marked with the by-reference operator.
form control is the most subject to change.
| Property Name | Return Value Type | Required? | Dependencies | Parameters | Notes |
| content_types | Array | Yes | None | Panels calls this function to find out how many content types this plugin provides, as well as some basic 'gatekeeper' information about each of those content types. Most importantly, optional and required context(s) are defined in this function. | |
| render callback | Object | Yes | None | $pane->configuration1, $panel_args, $context | Panels calls this function while preparing a display object for viewing. The callback needs to construct and return an object, which is passed along to the Style and Layout plugins for handling. |
| add callback | FAPI Array | No | None | $subtype, $parents, $pane->configuration1,2 | This function gets called when the user clicks an icon to add a new pane (from the 'Add Content' modal form). note that it is often possible to use the same, or nearly the same, callback for this as for the edit callback. |
| edit callback | FAPI Array | No | None | $subtype, $parents, $pane->configuration | This function gets called when the user clicks the 'Configure' button on an already-existing pane; it partially governs what appears on the resulting configuration modal. |
| add/edit validate callback | No | None3 | $form, $form_values | Defines a callback to be used as a FAPI validator, but only for the $form_values set by form items defined in the add/edit callback. | |
| add/edit submit callback | part of the $pane->configuration Array | No | None3 | $form_values | Defines a callback to be used as a FAPI submit handler, but only for the $form_values set by form items defined in the add/edit callback. |
| title callback | String | Yes | None | $pane->configuration, $context | This function determines the title that the pane will use in the display content editor, and ONLY that title. |
| editor render callback | String | No | None | $display, $pane | This function determines the title that the pane will use in the display content editor, and ONLY that title. |
| visibility control | FAPI Array | No | None | $contexts, $subtype, $pane->configuration, $add | This callback is fired shortly after the add/edit callbacks. Use it to create a form widget form widget from which the user can select values that will make sense when passed to your visibility check callback. |
| visibility submit | Mixed | No | visibility control | $contexts, $subtype, $pane->configuration, $add | The custom submit handler for your content type's visibility settings. This function is passed the portion of the $form_values array that was generated from the widgets created by the visibility control callback. Most plugins won't need to define this property, even if they define custom visibility control. |
| visibility check | Boolean | Yes4 | visibility control | $contexts, $subtype, $pane->configuration, $add | Panels calls this function during the pane accessibility checking routine, which is handled by primarily by panels_pane_access(). Define the logic governing your content type's visibility here. |
| form control | FAPI $form Array | No | None | &$form, &$pane, &$display | If the other callbacks governing the add/edit form (i.e., the add/edit callback properties or the visibility control property) aren't enough for your needs, then implement this callback. This function is passed virtually all of the Panels editing data by reference. Use with caution. |
function panels_admin_content_types_SAMPLE_CT() {
return array(
// As with the plugin declaration, the value of this array key is significant:
// it will become the pane's subtype, stored in $pane->subtype.
'content' => array(
// The name used for this subtype on the Add Content modal - this is what
// appears right below the icon.
'title' => t('SAMPLE CONTENT TYPE'),
// The name of the icon file to be used for this subtype.
'icon' => 'icon_node.png',
// The server path to the directory where the above icon is located.
'path' => panels_get_path('content_types/node'),
// The 'description' appears as a tooltip when the user hovers their
// mouse pointer over the icon.
'description' => t('Descriptive text for the SAMPLE CONTENT TYPE, to be used in the tooltip.'),
// This property indicates which contexts are prerequisites for the content
// type to be used. If a display lacks a context required by this content
// type, then it simply will not be displayed. Multiple required contexts
// can be declared by placing each context into an indexed array.
'required context' => new panels_required_context(t('Sample Required Context'), 'sample_context_required'),
// This property has the same syntax as 'required context', but if optional
// context requirements are not met, the content type will still be usable,
// simply in a reduced form. It's up to the plugin author to define just how
// different that functionality by writing varying the behavior of this plugin's
// other callbacks according to the presence/absence of the context.
'optional context' => new panels_optional_context(t('Sample Optional Context'), 'sample_context_optional'),
// Category is the group this subtype's icon will be placed in. The first
// item in the array is the category name, and the second is the subtype's
// weight in that category (used for ordering the subtypes in the category
// relative to one another). Omitting a value for weight will cause it to
// default to 0; if you do omit the weight, you can simply return the
// t()-wrapped string title of the content type - no need to put it in an array.
'category' => array(t('Node context'), -9),
),
);
}
The sample function below is a direct copy of the node_content plugin's render callback; abstract example cases are of little use from here on out. Note that this case only implements three parameters, but there is also a fourth. Your content type can use as few/many of these parameters as you want, although you won't be able to much if you don't implement the first parameter, $conf.
| array | $conf The contents of $pane->configuration. This will be an array with the following keys, by default:
| |
| array | $panel_args An indexed array of all arguments, if any, that have been passed to the display. | |
| mixed | $context The contents of $context can vary widely. If only one context is being passed to the pane, $context will simply be that context object. If multiple contexts are passed, however, then $context will be an indexed array of those contexts. The sort of data contained in the context is completely dependent on the how that context has been defined. | |
| $incoming_content |
function panels_content_SAMPLE_CT($conf, $panel_args, $context) {
// The node_content content_type plugin has a required context of 'node.'
// This simply double-checks to make sure that the necessary context is present;
// in particular, it excludes 'empty' contexts, which are used primarily during
// the edit process.
if (!empty($context) && empty($context->data)) {
return;
}
// The node context plugin stores an entire, fully-loaded $node object into
// its $context->data element; this pulls that node data out (via cloning, to
// ensure the original context data itself remains unchanged) and stores it in
// a correspondingly-named variable, $node.
$node = isset($context->data) ? drupal_clone($context->data) : NULL;
$block->module = 'node';
// Stores the nid from the context, to ensure it is acecssible later.
$block->delta = $node->nid;
// Just in case the context didn't load, but managed to get past the initial
// checks, this adds filler content to the $block.
if (empty($node)) {
$block->delta = 'placeholder';
$block->subject = t('Node title.');
$block->content = t('Node content goes here.');
}
else {
if (!empty($conf['identifier'])) {
$node->panel_identifier = $conf['identifier'];
}
$block->subject = $node->title;
unset($node->title);
// The pane's content is a complex enough operation that we delegate creating
// it to a helper function.
$block->content = panels_admin_SAMPLE_CT($node, $conf);
}
// If the user has the necessary permissions, an 'admin link' is generated.
// Admin links are the special links that appear above the pane's title when
// you mouse over the pane.
if (node_access('update', $node)) {
$block->admin_links['update'] = array(
'title' => t('Edit node'),
'alt' => t("Edit this node"),
'href' => "node/$node->nid/edit",
'query' => drupal_get_destination(),
);
}
if (!empty($conf['link']) && $node) {
$block->title_link = "node/$node->nid";
}
return $block;
}
Probably the most important thing to be noted about this helper function is just how similar it is to node.module's routine for node rendering. In fact, it's little more than a minor rewrite of node_view(); the first lines are lifted directly from node_build_content(), and the latter half from node_view().
function panels_admin_SAMPLE_CT($node, $conf) {
// Remove the delimiter (if any) that separates the teaser from the body.
$node->body = str_replace('<!--break-->', '', $node->body);
// The 'view' hook can be implemented to overwrite the default function
// to display nodes.
if (node_hook($node, 'view')) {
$node = node_invoke($node, 'view', $conf['teaser'], $conf['page']);
}
else {
$node = node_prepare($node, $conf['teaser']);
}
if (empty($conf['no_extras'])) {
// Allow modules to make their own additions to the node.
node_invoke_nodeapi($node, 'view', $conf['teaser'], $conf['page']);
}
if ($conf['links']) {
$node->links = module_invoke_all('link', 'node', $node, $conf['teaser']);
foreach (module_implements('link_alter') AS $module) {
$function = $module .'_link_alter';
$function($node, $node->links);
}
}
// Set the proper node part, then unset unused $node part so that a bad
// theme can not open a security hole.
$content = drupal_render($node->content);
if ($conf['teaser']) {
$node->teaser = $content;
unset($node->body);
}
else {
$node->body = $content;
unset($node->teaser);
}
// Allow modules to modify the fully-built node.
node_invoke_nodeapi($node, 'alter', $conf['teaser'], $conf['page']);
return theme('node', $node, $conf['teaser'], $conf['page']);
}
Clearly there's relatively little need to differentiate between the add and edit callbacks; the only thing this one does is make sure that $conf has some of the right values before heading into the edit form. You still need to define both the 'add callback' and 'edit callback' properties in the plugin declaration array, but you can just make them point to the same function.
See the edit callback for more detailed discussion.
function panels_admin_add_SAMPLE_CT($id, $parents, $conf = array()) {
list($conf['module'], $conf['delta']) = explode('-', $id, 2);
return panels_admin_edit_SAMPLE_CT($id, $parents, $conf);
}
This function essentially operates like a limited and targeted implementation of hook_form_alter(); the Panels API wrangles FAPI as needed, so all you need to do is add the widgets you want for your content type/subtype.
| string | $id The subtype of the pane being edited. The block panels content type plugin calls this variable '$id' for legacy reasons; we recommend you call this variable $subtype if you want your variable names to be optimally descriptive of their values. | |
| array | $parents This parameter is largely deprecated, and is included for legacy API compatibility. Its intention was to provide information to form widgets about where they live on the $form. It is likely to disappear in Panels3. For all add/edit callbacks: $parents = array('configuration');
| |
| array | $conf The contents of $pane->configuration, if any. |
function panels_admin_edit_SAMPLE_CT($id, $parents, $conf) {
$form['module'] = array(
'#type' => 'value',
'#value' => $conf['module'],
);
$form['delta'] = array(
'#type' => 'value',
'#value' => $conf['delta'],
);
if (user_access('administer advanced pane settings')) {
$form['block_visibility'] = array(
'#type' => 'checkbox',
'#title' => t('Use block visibility settings (see block config)'),
'#default_value' => $conf['block_visibility'],
'#description' => t('If checked, the block visibility settings for this block will apply to this block.'),
);
// Module-specific block configurations.
if ($settings = module_invoke($conf['module'], 'block', 'configure', $conf['delta'])) {
// Specifically modify a couple of core block forms.
if ($conf['module'] == 'block') {
unset($settings['submit']);
$settings['info']['#type'] = 'value';
$settings['info']['#value'] = $settings['info']['#default_value'];
}
panels_admin_fix_block_tree($settings);
$form['block_settings'] = array(
'#type' => 'fieldset',
'#title' => t('Block settings'),
'#description' => t('Settings in this section are global and are for all blocks of this type, anywhere in the system.'),
'#tree' => FALSE,
);
$form['block_settings'] += $settings;
}
}
return $form;
}
However, in cases where the settings being changed on this form need to be reflected in some other data structure, this callback can be used to ensure that the necessary changes are made. In this example (again from the block content type plugin), hook_block() is invoked with $op = 'save' for the module that owns the block, thereby allowing the normal block saving routine to do its thing.
function panels_admin_submit_SAMPLE_CT(&$form_values) {
if (!empty($form_values['block_settings'])) {
module_invoke($form_values['module'], 'block', 'save', $form_values['delta'], $form_values['block_settings']);
}
}
Returns the title to be used in the display editor ONLY. When the pane is rendered for viewing, the value of $obj->title or $obj->subject, as returned from the callback defined in the 'render callback' property, will become the pane's title. The only way the value returned from here will show up as the pane's title upon viewing is if this callback is explicitly called from the render callback itself (i.e.,
$obj->title = panels_admin_title_SAMPLE_CT($conf, $context);
| array | $conf The contents of $pane->configuration, if any. | |
| mixed | $context The contents of $context can vary; see other sample callback parameters for details. |
function panels_admin_title_SAMPLE_CT($conf, $context) {
return t('"@s" content', array('@s' => $context->identifier));
}
Returns object used to populate the content area of the pane in the display editor ONLY.
| object | $display The full display object being edited. | |
| object | $pane The pane object being rendered for display editing. |
$obj->title
$obj->content
function panels_admin_pane_render_SAMPLE_CT($display, $pane) {
// Pretend your content type stores a node id in the $configuration array
// and that you want the title of that node as the fieldset title, and the
// teaser for that node as the content.
$node = node_load($pane->configuration['nid']);
$block = new stdClass();
$block->title = check_plain($node->title);
$block->content = node_view($node, TRUE);
return $block;
}
If your plugin defines this property, you'll need to be cognizant of your definitions for several other properties: visibility submit, visibility check and visibility serialize. roles and visibility is also relevant, but won't be useful to the vast majority of content type plugins.
Operates quite similarly to the add and edit callbacks, with a few exceptions:
Remember, this is NOT where you define the logic behind your visibility handling - all you're doing here is providing a form widget that to get some data. It's up to your visibility checker, defined in the visibility check callback, to create the logic that can take the data from here and make the right decision about pane visibility.
If you define a more complex system that uses multiple widgets, make sure to return them all stacked inside a single array, and that you set the FAPI tree property to TRUE as needed.
| mixed | $contexts As in the render callback, this is either a context object, or an array of context objects. It's unnecessary and probably unwise to include this context data directly in the values that get saved in your form visibility function; that very same data will be available via the $display variable that's passed to the checker. Rather, $context is provided in the event that your visibility widget needs to vary depending on some information in $context. | |
| string | $subtype The contents of $pane->subtype for the pane currently being edited. | |
| array | $conf The contents of $pane->configuration, if any. | |
| bool | $add If TRUE, then a new pane is being added. If FALSE, then an existing pane is being edited. |
function panels_admin_visibility_control_SAMPLE_CT($contexts, $subtype, $conf, $add) {
return $visibility_widget = array(
'#type' => 'radios',
'#title' => t('Pane Visibility'),
'#description' => t('Who should this pane be visible to?'),
'#options' => array(
'all' => t('Everyone'),
'member' => t('Only group members'),
'nonmember' => t('Only group non-members'),
'admin' => t('Group administrators'),
),
'#default_value' => isset($conf['visibility']) ? $conf['visibility'] : 0,
);
}
This function takes advantage of cached static variables to increase performance. On any given page request, we know that only ONE group is going to be accessed, and only ONE user is going to be doing the accessing. Since the static keyword only lasts through a single page request, and nid and uid are the two variables that visibility depends upon in this case, we only have to query the database and build the $visibility array once, no matter how many panes fire this callback to determine visibility during this page request.
| object | $pane The fully-loaded $pane object that we're running the visibility check against. The value set by the widget defined in the visibility control callback is contained in $pane->visibility. | |
| object | $display The fully-loaded display object that's currently being rendered. If you need $context to figure out what action to take, you'll find it/them in $display->context. | |
| object | $user The current $user - the same as what you'd get from global $user. Passed for convenience, since visibility is often dependent on the $user. |
function panels_content_visibility_check_SAMPLE_CT($pane, $display, $user) {
// use static variable to somewhat reduce queries for complex og_panels pages
static $visible;
if (!is_array($visible)) {
$visible = array();
$visible['all'] = TRUE;
}
if (!isset($visible[$pane->visibility])) {
$members = array();
$sql = "SELECT u.uid AS uid, ogu.is_admin AS admin FROM {og_uid} ogu
INNER JOIN {users} u ON ogu.uid = u.uid WHERE ogu.nid = %d
AND ogu.is_active = 1 AND u.status = 1 ORDER BY ogu.created DESC";
// $display holds the context; the $data element of $context holds a node object,
// and we want the nid of that node.
$result = db_query($sql, $display->context->data->nid);
while ($account = db_fetch_array($result)) {
$members[$account['uid']] = $account['admin'];
}
$visible['member'] = in_array($user->uid, array_keys($members));
$visible['nonmember'] = !$visible['member'];
$visible['admin'] = $visible['member'] ? $members[$user->uid] : FALSE;
}
return $visible[$pane->visibility];
}
Implement this if you need to wrangle that data before the Panels API's data storage routines kick in, or if the API's built-in routines are inadequate and you need to build a custom storage mechanism. See panels_content_config_form() and panels_content_config_form_submit() to grok the logic behind if/when/how this callback is triggered.
Most use cases for the Panels visibility system will not need to implement a submit handler, as the built-in handler in panels_content_config_form_submit() is adequate for taking care of the data. However, if you need to manipulate the data generated by your form widget before it gets saved, or if you need to inform a custom external storage mechanism, hook, whatever, about the visibility setting, then you should do so here.
Whatever you return from this callback will be saved as the value of $pane->visibility. If it is a data type that must be serialized before being put in the database (arrays and objects), then make sure to set the 'visibility serialize' property to TRUE for this content type.
Note that in this particular sample implementation, the 'visibility serialize' property should be set to FALSE, as the value produced by the widget in the sample visibility control function returns a simple string. Because 'visibility serialize' is FALSE by default, you can also simply not set the property.
Note also that the visibility field in the panels_pane table is the standard 'text' field. The save routines in panels_save_display() will always convert the value to a string, so bear in mind: even if you send an integer in, you'll get a string of that number back out on the other end.
| string | $form_value_visibility $form_values['visibility'], is the sole argument passed to this callback. | |
| bool | $add As in the visibility control callback parameters: if TRUE, then we're in the submit phase for adding a new pane. If FALSE, then we're in the submit phase of an existing pane. | |
| object | $pane The fully-loaded $pane object that we're submitting. Provided primarily for content types with complex data storage needs that may be dependent on data contained $pane object. | |
| object | $display The fully-loaded $display object that's currently being edited. As with $pane, provided primarily for complex implementations that may need some of the data. |
function panels_admin_visibility_submit_SAMPLE_CT($form_value_visibility, $add, $pane, $display) {
return $form_value_visibility;
}
This callback allows you full control over not only the $form array, but also the current $pane and $display objects; all three are passed by reference. Note that they will be passed by reference whether or not you prefix the parameters with the reference operator; it is included in the below parameters purely for explanatory purposes. See panels_content_config_form() for the context.
The callback is fired after all other form operations have been performed immediately, before the fully-built $form is sent back to FAPI for handling. Changes you make to the $pane and $display objects will also be preserved, as a call to panels_cache_set() is made. The TRULY intrepid can extract the $cache object from the value property in $form['vars'] and play with that.
This property is provided primarily as a means of allowing significant control over the innerworkings of the Panels content editing API, without necessitating direct hacks of the API itself. Probably the most common use case is hiding some/all of the form widgets that the Panels API adds to all content forms - the sample function contains a technique for doing just that.
CAUTION: This is an advanced, dangerous feature. Improper use of it can severely break your content type, and even has the potential to wreak havoc with your permanent Panels data. Be sure to test implementations of it thoroughly before putting them in a production environment.
| array | $form The all-but-complete FAPI $form array that generates the add/edit content form. | |
| object | $pane The fully-loaded $pane object that's currently being edited. | |
| object | $display The fully-loaded $display object that's currently being edited. | |
| bool | $add As in the visibility control callback parameters: if TRUE, then we're in the submit phase for adding a new pane. If FALSE, then we're in the submit phase of an existing pane. |
function panels_admin_form_control_SAMPLE_CT(&$form, &$pane, &$display) {
// A technique for stripping all the Panels-provided form widgets from the add/edit form.
// We'll just replace them all with 'value'-type elements and store the default values.
foreach ($form['configuration'] as $element => $settings) {
// Get rid of the formatting elements, if they exist.
if (in_array($element, array('aligner_start', 'aligner_stop', 'override_title_markup'))) {
unset($form['configuration'][$element]);
continue;
}
// Skip form elements that aren't generated by the Panels API.
if (!in_array($element, array('override_title', 'override_title_text', 'css_id', 'css_class'))) {
continue;
}
// We're down to the four we want to get rid of. Now, let's pull out the default values
$defaults[$element] = $settings['#default_value'];
unset($form['configuration'][$element]);
}
reset($defaults);
while (list($element, $value) = each($defaults)) {
$form['configuration'][$element] = array(
'#type' => 'value',
'#value' => $value,
);
}
// Now, the (default) values will still be present, and the submit function will work normally
}
1.5.6