flipping_book_reference.module in Flipping Book 7
Defines a field type for referencing one flipping_book from a node.
File
flipping_book_reference.moduleView source
<?php
/**
* @file
* Defines a field type for referencing one flipping_book from a node.
*/
define('FLIPPING_BOOK_REFERENCE_LINK_TARGET_DEFAULT', '_self');
/**
* Implements hook_menu().
*/
function flipping_book_reference_menu() {
$items['flipping_book_reference/autocomplete/%/%/%'] = array(
'page callback' => 'flipping_book_reference_autocomplete',
'page arguments' => array(
2,
3,
4,
),
'access arguments' => array(
'access content',
),
'type' => MENU_CALLBACK,
);
return $items;
}
/**
* Implements hook_field_info().
*/
function flipping_book_reference_field_info() {
return array(
'flipping_book_reference' => array(
'label' => t('Flipping Book reference'),
'description' => t('This field stores the ID of a related flipping_book as an integer value.'),
'settings' => array(
'referenceable_types' => array(),
'view' => array(
'view_name' => '',
'display_name' => '',
'args' => array(),
),
),
// It probably make more sense to have the referenceable types be
// per-field than per-instance
// 'instance settings' => array('referenceable_types' => array()),
'default_widget' => 'options_select',
'default_formatter' => 'flipping_book_reference_default',
// Support hook_entity_property_info() from contrib "Entity API".
'property_type' => 'node',
// Support default token formatter for field tokens.
'default_token_formatter' => 'flipping_book_reference_plain',
),
);
}
/**
* Implements hook_field_settings_form().
*/
function flipping_book_reference_field_settings_form($field, $instance, $has_data) {
$form = array();
return $form;
}
/**
* Implements hook_field_validate().
*
* Possible error codes:
* - 'invalid_fbid': fbid is not valid for the field
* (not a valid flipping_book id, or the flipping_book is not referenceable).
*/
function flipping_book_reference_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
// Extract fbids to check.
$ids = array();
// First check non-numeric "fbid's to avoid losing time with them.
foreach ($items as $delta => $item) {
if (is_array($item) && !empty($item['fbid'])) {
if (is_numeric($item['fbid'])) {
$ids[] = $item['fbid'];
}
else {
$errors[$field['field_name']][$langcode][$delta][] = array(
'error' => 'invalid_fbid',
'message' => t("%name: invalid input.", array(
'%name' => $instance['label'],
)),
);
}
}
}
// Prevent performance hog if there are no ids to check.
if ($ids) {
$options = array(
'ids' => $ids,
);
$refs = flipping_book_reference_potential_references($field, $options);
foreach ($items as $delta => $item) {
if (is_array($item)) {
if (!empty($item['fbid']) && !isset($refs[$item['fbid']])) {
$errors[$field['field_name']][$langcode][$delta][] = array(
'error' => 'invalid_fbid',
'message' => t("%name: this flipping_book can't be referenced.", array(
'%name' => $instance['label'],
)),
);
}
}
}
}
}
/**
* Implements hook_field_is_empty().
*/
function flipping_book_reference_field_is_empty($item, $field) {
// Fbid = 0 is empty too, which is exactly what we want.
return empty($item['fbid']);
}
/**
* Implements hook_field_formatter_info().
*/
function flipping_book_reference_field_formatter_info() {
$info = array(
'flipping_book_reference_default' => array(
'label' => t('Title (link)'),
'description' => t('Display the title of the referenced flipping_book as a link to the flipping_book page.'),
'field types' => array(
'flipping_book_reference',
),
'settings' => array(
'target' => FLIPPING_BOOK_REFERENCE_LINK_TARGET_DEFAULT,
),
),
'flipping_book_reference_plain' => array(
'label' => t('Title (no link)'),
'description' => t('Display the title of the referenced flipping_book as plain text.'),
'field types' => array(
'flipping_book_reference',
),
),
'flipping_book_reference_fbid' => array(
'label' => t('Flipping Book ID'),
'description' => t('Display the referenced flipping_book ID'),
'field types' => array(
'flipping_book_reference',
),
),
'flipping_book_reference_path' => array(
'label' => t('URL as plain text'),
'description' => t('Display the URL of the referenced flipping_book'),
'field types' => array(
'flipping_book_reference',
),
'settings' => array(
'absolute' => FALSE,
),
),
);
if (module_exists('colorbox') && variable_get('colorbox_load', FALSE)) {
$info['flipping_book_reference_colorbox'] = array(
'label' => t('Colorbox link'),
'description' => t('Display the title of the referenced flipping_book as a colorbox link to the flipping_book page.'),
'field types' => array(
'flipping_book_reference',
),
'settings' => array(
'width' => '800',
'height' => '500',
'link_title' => '',
),
);
}
return $info;
}
/**
* Implements hook_field_formatter_settings_form().
*/
function flipping_book_reference_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$element = array();
switch ($display['type']) {
case 'flipping_book_reference_default':
$element['target'] = array(
'#type' => 'select',
'#options' => flipping_book_reference_link_targets(),
'#title' => t('Choose the link target'),
'#default_value' => $settings['target'],
);
break;
case 'flipping_book_reference_path':
$element['absolute'] = array(
'#type' => 'checkbox',
'#title' => t('Display an absolute URL'),
'#default_value' => $settings['absolute'],
);
break;
case 'flipping_book_reference_colorbox':
$element['width'] = array(
'#type' => 'textfield',
'#title' => t('Width'),
'#default_value' => $settings['width'],
);
$element['height'] = array(
'#type' => 'textfield',
'#title' => t('Height'),
'#default_value' => $settings['height'],
);
$element['link_title'] = array(
'#type' => 'textfield',
'#title' => t('Link Title'),
'#default_value' => $settings['link_title'],
);
break;
}
return $element;
}
/**
* Link Targets mapping.
*/
function flipping_book_reference_link_targets() {
return array(
'_self' => t('Same window'),
'_blank' => t('New window'),
'_top' => t('Uppermost document'),
'_parent' => t('Parent frame'),
);
}
/**
* Implements hook_field_formatter_settings_summary().
*/
function flipping_book_reference_field_formatter_settings_summary($field, $instance, $view_mode) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$summary = array();
switch ($display['type']) {
case 'flipping_book_reference_default':
$targets = flipping_book_reference_link_targets();
$summary[] = t('Target: %target', array(
'%target' => $targets[$settings['target']],
));
break;
case 'flipping_book_reference_path':
$summary[] = t('Absolute URL: %yes_no', array(
'%yes_no' => $settings['absolute'] ? t('Yes') : t('No'),
));
break;
case 'flipping_book_reference_colorbox':
$summary[] = t('Width: %width', array(
'%width' => $settings['width'],
));
$summary[] = t('Height: %height', array(
'%height' => $settings['height'],
));
$summary[] = t('Link title: %link_title', array(
'%link_title' => $settings['link_title'],
));
break;
}
return implode('<br />', $summary);
}
/**
* Implements hook_field_formatter_prepare_view().
*
* Preload all flipping_books referenced by items using
* 'full entity' formatters.
*/
function flipping_book_reference_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
// Load the referenced flipping books, except for the
// 'flipping_book_reference_fbid' which does not need full objects.
// Collect ids to load.
$ids = array();
foreach ($displays as $id => $display) {
if ($display['type'] != 'flipping_book_reference_fbid') {
foreach ($items[$id] as $delta => $item) {
$ids[$item['fbid']] = $item['fbid'];
}
}
}
$flipping_books = flipping_book_load_multiple($ids);
// Add the loaded flipping_books to the items.
foreach ($displays as $id => $display) {
if ($display['type'] != 'flipping_book_reference_fbid') {
foreach ($items[$id] as $delta => $item) {
$items[$id][$delta]['flipping_book'] = $flipping_books[$item['fbid']];
}
}
}
}
/**
* Implements hook_field_formatter_view().
*/
function flipping_book_reference_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
$settings = $display['settings'];
$result = array();
switch ($display['type']) {
case 'flipping_book_reference_default':
case 'flipping_book_reference_plain':
foreach ($items as $delta => $item) {
$flipping_book = $item['flipping_book'];
if ($display['type'] == 'flipping_book_reference_default') {
$result[$delta] = array(
'#type' => 'link',
'#title' => $flipping_book->title,
'#href' => $flipping_book->url,
'#options' => array(
'attributes' => array(
'target' => $settings['target'],
),
),
);
}
else {
$result[$delta] = array(
'#markup' => check_plain($flipping_book->title),
);
}
}
break;
case 'flipping_book_reference_fbid':
foreach ($items as $delta => $item) {
$result[$delta] = array(
'#markup' => $item['fbid'],
);
}
break;
case 'flipping_book_reference_path':
foreach ($items as $delta => $item) {
$options = array(
'absolute' => $settings['absolute'],
);
$result[$delta] = array(
'#markup' => url($item['flipping_book']->path, $options),
);
}
break;
case 'flipping_book_reference_colorbox':
foreach ($items as $delta => $item) {
$flipping_book = $item['flipping_book'];
$query = array(
'width' => $settings['width'],
'height' => $settings['height'],
'iframe' => 'true',
);
$result[$delta] = array(
'#type' => 'link',
'#title' => !empty($settings['link_title']) ? t($settings['link_title']) : $flipping_book->title,
'#href' => url($flipping_book->url, array(
'query' => $query,
)),
'#options' => array(
'attributes' => array(
'class' => array(
'colorbox-load',
),
),
),
);
}
break;
}
return $result;
}
/**
* Implements hook_field_widget_info().
*/
function flipping_book_reference_field_widget_info() {
return array(
'flipping_book_reference_autocomplete' => array(
'label' => t('Autocomplete text field'),
'description' => t('Display the list of referenceable flipping_books as a textfield with autocomplete behaviour.'),
'field types' => array(
'flipping_book_reference',
),
'settings' => array(
'autocomplete_match' => 'contains',
'size' => 60,
'autocomplete_path' => 'flipping_book_reference/autocomplete',
),
),
);
}
/**
* Implements hook_field_widget_info_alter().
*/
function flipping_book_reference_field_widget_info_alter(&$info) {
$info['options_select']['field types'][] = 'flipping_book_reference';
$info['options_buttons']['field types'][] = 'flipping_book_reference';
}
/**
* Implements hook_field_widget_settings_form().
*/
function flipping_book_reference_field_widget_settings_form($field, $instance) {
$widget = $instance['widget'];
$defaults = field_info_widget_settings($widget['type']);
$settings = array_merge($defaults, $widget['settings']);
$form = array();
if ($widget['type'] == 'flipping_book_reference_autocomplete') {
$form['autocomplete_match'] = array(
'#type' => 'select',
'#title' => t('Autocomplete matching'),
'#default_value' => $settings['autocomplete_match'],
'#options' => array(
'starts_with' => t('Starts with'),
'contains' => t('Contains'),
),
'#description' => t('Select the method used to collect autocomplete suggestions. Note that <em>Contains</em> can cause performance issues on sites with thousands of flipping_books.'),
);
$form['size'] = array(
'#type' => 'textfield',
'#title' => t('Size of textfield'),
'#default_value' => $settings['size'],
'#element_validate' => array(
'_element_validate_integer_positive',
),
'#required' => TRUE,
);
}
return $form;
}
/**
* Implements hook_field_widget_form().
*/
function flipping_book_reference_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
switch ($instance['widget']['type']) {
case 'flipping_book_reference_autocomplete':
$element += array(
'#type' => 'textfield',
'#default_value' => isset($items[$delta]['fbid']) ? $items[$delta]['fbid'] : NULL,
'#autocomplete_path' => $instance['widget']['settings']['autocomplete_path'] . '/' . $instance['entity_type'] . '/' . $instance['bundle'] . '/' . $field['field_name'],
'#size' => $instance['widget']['settings']['size'],
'#maxlength' => NULL,
'#element_validate' => array(
'flipping_book_reference_autocomplete_validate',
),
'#value_callback' => 'flipping_book_reference_autocomplete_value',
);
break;
}
return array(
'fbid' => $element,
);
}
/**
* Value callback for a flipping_book_reference autocomplete element.
*
* Replace the flipping_book fbid with a flipping_book title.
*/
function flipping_book_reference_autocomplete_value($element, $input, $form_state) {
if (empty($input)) {
// We're building the displayed 'default value': expand the raw fbid into
// "flipping_book title [fbid:f]".
$fbid = $element['#default_value'];
if (!empty($fbid)) {
$q = db_select('flipping_book', 'f');
$q
->addField('f', 'title');
$q
->condition('f.fbid', $fbid, '=');
$result = $q
->execute();
$value = $result
->fetchField();
$value .= ' [fbid:' . $fbid . ']';
return $value;
}
}
}
/**
* Validation callback for a flipping_book_reference autocomplete element.
*/
function flipping_book_reference_autocomplete_validate($element, &$form_state, $form) {
$field = field_widget_field($element, $form_state);
$instance = field_widget_instance($element, $form_state);
$value = $element['#value'];
$fbid = NULL;
if (!empty($value)) {
// Check whether we have an explicit "[fbid:f]" input.
preg_match('/^(?:\\s*|(.*) )?\\[\\s*fbid\\s*:\\s*(\\d+)\\s*\\]$/', $value, $matches);
if (!empty($matches)) {
// Explicit fbid. Check that the 'title' part matches the actual title for
// the fbid.
list(, $title, $fbid) = $matches;
if (!empty($title)) {
$real_title = db_select('flipping_book', 'f')
->fields('f', array(
'title',
))
->condition('f.fbid', $fbid)
->execute()
->fetchField();
if (trim($title) != trim($real_title)) {
form_error($element, t('%name: title mismatch. Please check your selection.', array(
'%name' => $instance['label'],
)));
}
}
}
else {
// No explicit fbid (the submitted value was not populated by autocomplete
// selection). Get the fbid of a referencable flipping_book from
// the entered title.
$options = array(
'string' => $value,
'match' => 'equals',
'limit' => 1,
);
$references = flipping_book_reference_potential_references($field, $options);
if ($references) {
// @todo The best thing would be to present the user with an
// additional form, allowing the user to choose between valid
// candidates with the same title. ATM, we pick the first
// matching candidate...
$fbid = key($references);
}
else {
form_error($element, t('%name: found no valid flipping_book with that title.', array(
'%name' => $instance['label'],
)));
}
}
}
// Set the element's value as the flipping_book id that was extracted
// from the entered input.
form_set_value($element, $fbid, $form_state);
}
/**
* Implements hook_field_widget_error().
*/
function flipping_book_reference_field_widget_error($element, $error, $form, &$form_state) {
form_error($element['fbid'], $error['message']);
}
/**
* List referenceable flipping_books suitable for the '#option' FAPI prop.
*
* Warning: the function does NOT take care of encoding or escaping the
* flipping_book titles. Proper massaging needs to be performed by the caller,
* according to the destination FAPI '#type' (radios / checkboxes / select).
*
* @param array $field
* The field definition.
* @param bool $flat
* Whether optgroups are allowed.
*
* @return array
* An array of referenceable flipping_book titles, keyed by flipping_book id.
* If the $flat parameter is TRUE, the list might be nested by optgroup first.
*/
function _flipping_book_reference_options($field, $flat = TRUE) {
$references = flipping_book_reference_potential_references($field);
$options = array();
foreach ($references as $key => $value) {
// The label, displayed in selects and checkboxes/radios, should have HTML
// entities unencoded. The widgets (core's options.module) take care of
// applying the relevant filters (strip_tags() or filter_xss()).
$label = decode_entities($value['rendered']);
if (empty($value['group']) || $flat) {
$options[$key] = $label;
}
else {
// The group name, displayed in selects, cannot contain tags, and should
// have HTML entities unencoded.
$group = decode_entities(strip_tags($value['group']));
$options[$group][$key] = $label;
}
}
return $options;
}
/**
* Retrieves an array of candidate referenceable flipping_books.
*
* This info is used in various places (allowed values, autocomplete
* results, input validation...). Some of them only need the fbids,
* others fbid + titles, others yet fbid + titles + rendered row (for
* display in widgets).
*
* The array we return contains all the potentially needed information,
* and lets consumers use the parts they actually need.
*
* @param array $field
* The field definition.
* @param array $options
* An array of options to limit the scope of the returned list. The following
* key/value pairs are accepted:
* - string: string to filter titles on (used by autocomplete).
* - match: operator to match the above string against, can be any of:
* 'contains', 'equals', 'starts_with'. Defaults to 'contains'.
* - ids: array of specific flipping_book ids to lookup.
* - limit: maximum size of the the result set. Defaults to 0 (no limit).
*
* @return array
* An array of valid flipping_books in the form:
* array(
* fbid => array(
* 'title' => The flipping_book title,
* 'rendered' => The text to display in widgets (can be HTML)
* ),
* ...
* )
*/
function flipping_book_reference_potential_references($field, $options = array()) {
// Fill in default options.
$options += array(
'string' => '',
'match' => 'contains',
'ids' => array(),
'limit' => 0,
);
$results =& drupal_static(__FUNCTION__, array());
// Create unique id for static cache.
$cid = $field['field_name'] . ':' . $options['match'] . ':' . ($options['string'] !== '' ? $options['string'] : implode('-', $options['ids'])) . ':' . $options['limit'];
if (!isset($results[$cid])) {
$references = _flipping_book_reference_potential_references_standard($field, $options);
// Store the results.
$results[$cid] = !empty($references) ? $references : array();
}
return $results[$cid];
}
/**
* Helper function for flipping_book_reference_potential_references().
*
* List of referenceable flipping_books defined by content types.
*/
function _flipping_book_reference_potential_references_standard($field, $options) {
$query = db_select('flipping_book', 'f');
$query
->addField('f', 'fbid');
$flipping_book_title_alias = $query
->addField('f', 'title', 'flipping_book_title');
$flipping_book_dir_alias = $query
->addField('f', 'dir', 'flipping_book_dir');
if ($options['string'] !== '') {
switch ($options['match']) {
case 'contains':
$query
->condition('f.title', '%' . $options['string'] . '%', 'LIKE');
break;
case 'starts_with':
$query
->condition('f.title', $options['string'] . '%', 'LIKE');
break;
case 'equals':
// No match type or incorrect match type: use "="
default:
$query
->condition('f.title', $options['string']);
break;
}
}
if ($options['ids']) {
$query
->condition('f.fbid', $options['ids'], 'IN');
}
if ($options['limit']) {
$query
->range(0, $options['limit']);
}
$query
->orderBy($flipping_book_title_alias)
->orderBy($flipping_book_dir_alias);
$result = $query
->execute()
->fetchAll();
$references = array();
foreach ($result as $flipping_book) {
$references[$flipping_book->fbid] = array(
'title' => $flipping_book->flipping_book_title,
'rendered' => check_plain($flipping_book->flipping_book_title),
);
}
return $references;
}
/**
* Menu callback for the autocomplete results.
*/
function flipping_book_reference_autocomplete($entity_type, $bundle, $field_name, $string = '') {
$field = field_info_field($field_name);
$instance = field_info_instance($entity_type, $field_name, $bundle);
$options = array(
'string' => $string,
'match' => $instance['widget']['settings']['autocomplete_match'],
'limit' => 10,
);
$references = flipping_book_reference_potential_references($field, $options);
$matches = array();
foreach ($references as $id => $row) {
// Markup is fine in autocompletion results (might happen when rendered
// through Views) but we want to remove hyperlinks.
$suggestion = preg_replace('/<a href="([^<]*)">([^<]*)<\\/a>/', '$2', $row['rendered']);
// Add a class wrapper for a few required CSS overrides.
$matches[$row['title'] . " [fbid:{$id}]"] = '<div class="reference-autocomplete">' . $suggestion . '</div>';
}
drupal_json_output($matches);
drupal_exit();
}
/**
* Implements hook_options_list().
*/
function flipping_book_reference_options_list($field) {
return _flipping_book_reference_options($field, FALSE);
}
/**
* Implements hook_field_views_data().
*
* In addition to the default field information we add the relationship for
* views to connect back to the flipping_book table.
*/
function flipping_book_reference_field_views_data($field) {
// No module_load_include(): this hook is invoked from
// views/modules/field.views.inc, which is where that function is defined.
$data = field_views_field_default_views_data($field);
$storage = $field['storage']['details']['sql'];
foreach ($storage as $table_data) {
$table = key($table_data);
$columns = current($table_data);
$id_column = $columns['fbid'];
if (isset($data[$table])) {
// Filter: swap the handler to the 'in' operator. The callback receives
// the field name instead of the whole $field structure to keep views
// data to a reasonable size.
$data[$table][$id_column]['filter']['handler'] = 'views_handler_filter_in_operator';
$data[$table][$id_column]['filter']['options callback'] = 'flipping_book_reference_views_filter_options';
$data[$table][$id_column]['filter']['options arguments'] = array(
$field['field_name'],
);
// Argument: display flipping_book.title in argument titles (handled in
// our custom handler) and summary lists (handled by the base
// views_handler_argument handler).
// Both mechanisms rely on the 'name table' and 'name field' information
// below, by joining to a separate copy of the base table from the field
// data table.
$data[$table][$id_column]['argument']['handler'] = 'references_handler_argument';
$data[$table][$id_column]['argument']['name table'] = $table . '_reference';
$data[$table][$id_column]['argument']['name field'] = 'title';
$data[$table . '_reference']['table']['join'][$table] = array(
'left_field' => $id_column,
'table' => 'flipping_book',
'field' => 'fbid',
);
// Relationship.
$data[$table][$id_column]['relationship'] = array(
'handler' => 'references_handler_relationship',
'base' => 'flipping_book',
'base field' => 'fbid',
'field' => $id_column,
'label' => $field['field_name'],
'field_name' => $field['field_name'],
);
}
}
return $data;
}
/**
* Options callback for the views_handler_filter_in_operator filter.
*
* @param string $field_name
* The field name.
*/
function flipping_book_reference_views_filter_options($field_name) {
$options = array();
if ($field = field_info_field($field_name)) {
$options = _flipping_book_reference_options($field, TRUE);
// The options are displayed in checkboxes within the filter admin form, and
// in a select within an exposed filter. Checkboxes accept HTML, other
// entities should be encoded; selects require the exact opposite: no HTML,
// no encoding. We go for a middle ground: strip tags, leave entities
// unencoded.
foreach ($options as $key => $value) {
$options[$key] = strip_tags($value);
}
}
return $options;
}
Functions
Constants
Name | Description |
---|---|
FLIPPING_BOOK_REFERENCE_LINK_TARGET_DEFAULT | @file Defines a field type for referencing one flipping_book from a node. |