faqfield.module in FAQ Field 7
Same filename and directory in other branches
FAQ Field Provides a field for frequently asked questions.
File
faqfield.moduleView source
<?php
/**
* @file
* FAQ Field
* Provides a field for frequently asked questions.
*/
// Add script for remove button.
drupal_add_js(drupal_get_path('module', 'faqfield') . '/js/faqfield.remove.js');
/**
* Returns HTML for a faqfield formatter.
*
* @param array $variables
* An associative array containing:
* - question: Prefiltered question value by check_plain.
* - answer: Prefiltered answer value by field setting format.
* - delta: Delta of field element.
*/
function theme_faqfield_formatter($variables) {
$output = '<h3 class="faqfield-question">' . $variables['question'] . '</h3>';
$output .= '<div class="faqfield-answer">' . $variables['answer'] . '</div>';
return $output;
}
/**
* Implements hook_field_info().
*
* Provides the description of the field.
*/
function faqfield_field_info() {
return array(
'faqfield' => array(
'label' => t('FAQ Field'),
'description' => t('Field for frequently asked questions.'),
'default_widget' => 'faqfield_textboxes',
'default_formatter' => 'faqfield_accordion',
// Set the default field settings.
'settings' => array(
'answer_widget' => 'textarea',
'format' => 0,
'advanced' => array(
'question_title' => t('Question'),
'question_length' => 255,
'question_rows' => 1,
'answer_title' => t('Answer'),
'answer_rows' => 3,
),
),
// Support hook_entity_property_info() from contrib "Entity API".
'property_type' => 'faqfield',
'property_callbacks' => array(
'faqfield_property_info_callback',
),
),
);
}
/**
* Additional callback to adapt the property info of faqfields.
*
* @see entity_metadata_field_entity_property_info().
*/
function faqfield_property_info_callback(&$info, $entity_type, $field, $instance, $field_type) {
$property =& $info[$entity_type]['bundles'][$instance['bundle']]['properties'][$field['field_name']];
// Define a data structure so it's possible to deal with both
// the question and answer.
$property['getter callback'] = 'entity_metadata_field_verbatim_get';
$property['setter callback'] = 'entity_metadata_field_verbatim_set';
// Auto-create the field item as soon as a property is set.
$property['auto creation'] = 'faqfield_item_create';
$property['property info'] = faqfield_item_property_info();
unset($property['query callback']);
}
/**
* Callback for creating a new, empty faqfield item.
*
* @see faqfield_property_info_callback()
*/
function faqfield_item_create() {
return array(
'question' => NULL,
'answer' => NULL,
);
}
/**
* Defines info for the properties of the faqfield item data structure.
*/
function faqfield_item_property_info() {
$properties['question'] = array(
'type' => 'text',
'label' => t('The question.'),
'getter callback' => 'entity_property_verbatim_get',
'setter callback' => 'entity_property_verbatim_set',
);
$properties['answer'] = array(
'type' => 'text',
'label' => t('The answer.'),
'getter callback' => 'entity_property_verbatim_get',
'setter callback' => 'entity_property_verbatim_set',
);
return $properties;
}
/**
* Implements hook_field_create_field().
*/
function faqfield_field_create_field($field) {
if ($field['type'] == 'faqfield') {
// Here we want to set a default cardinality of
// unlimited after a faqfield was created.
$field['cardinality'] = FIELD_CARDINALITY_UNLIMITED;
field_update_field($field);
}
}
/**
* Implements hook_field_settings_form().
*/
function faqfield_field_settings_form($field, $instance, $has_data) {
$form = array();
// Input for the count of rows for the answer field.
$form['answer_widget'] = array(
'#type' => 'select',
'#title' => t('Answer widget'),
'#default_value' => @$field['settings']['answer_widget'],
'#options' => array(
'textarea' => t('Textarea'),
'text_format' => t('Formatable textarea'),
'textfield' => t('Textfield'),
),
'#required' => TRUE,
'#description' => t('What form widget to use for answer input. Formatable textarea is needed for WYSIWYG editors.'),
);
// Get a list of formats that the current user has access to.
$formats = filter_formats();
foreach ($formats as $format) {
$options[$format->format] = $format->name;
}
// Format select input for field settings.
$form['format'] = array(
'#type' => 'select',
'#title' => t('Text format'),
'#default_value' => @$field['settings']['format'],
'#options' => $options,
'#access' => count($formats) > 1,
'#required' => TRUE,
'#description' => t('Format to filter FAQ field answer content.'),
'#states' => array(
'invisible' => array(
':input[id="edit-field-settings-answer-widget"]' => array(
'value' => 'text_format',
),
),
),
);
// We put more advanced settings into a collapsed fieldset.
$form['advanced'] = array(
'#type' => 'fieldset',
'#title' => t('Advanced settings'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
// Input for custom title of questions.
$form['advanced']['question_title'] = array(
'#type' => 'textfield',
'#title' => t('Question input title'),
'#default_value' => @$field['settings']['advanced']['question_title'],
'#description' => t('Custom title of question input.'),
'#maxlength' => 50,
'#size' => 20,
);
// Input for the maximum length of questions.
$form['advanced']['question_length'] = array(
'#type' => 'textfield',
'#title' => t('Question length'),
'#default_value' => @$field['settings']['advanced']['question_length'],
'#description' => t('Maximum length of questions (Between 10-255).'),
'#element_validate' => array(
'_faqfield_element_validate_question_length',
),
'#maxlength' => 3,
'#size' => 5,
);
// Input for the count of rows for the answer field.
$form['advanced']['question_rows'] = array(
'#type' => 'select',
'#title' => t('Question rows'),
'#default_value' => @$field['settings']['advanced']['question_rows'],
'#options' => array(
1 => '1 (Fieldset)',
2 => '2 (Textarea)',
3 => '3 (Textarea)',
4 => '4 (Textarea)',
),
'#required' => TRUE,
'#description' => t('Number of rows used for the question textfield/textarea.'),
);
// Input for custom title of answers.
$form['advanced']['answer_title'] = array(
'#type' => 'textfield',
'#title' => t('Answer input title'),
'#default_value' => @$field['settings']['advanced']['answer_title'],
'#description' => t('Custom title of answer input.'),
'#maxlength' => 50,
'#size' => 20,
);
// Input for the count of rows for the answer field.
$form['advanced']['answer_rows'] = array(
'#type' => 'select',
'#title' => t('Answer rows'),
'#default_value' => @$field['settings']['advanced']['answer_rows'],
'#options' => array(
1 => '1',
2 => '2',
3 => '3',
4 => '4',
5 => '5',
),
'#required' => TRUE,
'#description' => t('Number of rows used for the answer textarea.'),
'#states' => array(
'invisible' => array(
':input[id="edit-field-settings-answer-widget"]' => array(
'value' => 'textfield',
),
),
),
);
return $form;
}
/**
* Element validation callback for question length setting.
*/
function _faqfield_element_validate_question_length($element, &$form_state, $form) {
if ($element['#value'] > 255 || $element['#value'] < 10) {
form_error($element, t('Maximum length of question must be between 10 - 255.'));
}
}
/**
* Implements hook_field_formatter_settings_summary().
*/
function faqfield_field_formatter_settings_summary($field, $instance, $view_mode) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$summary = '';
switch ($display['type']) {
// Accordion display settings summary.
case 'faqfield_accordion':
$summary_elements = array();
if ($settings['active'] !== '') {
$summary_elements[] = t('Active: @element', array(
'@element' => $settings['active'],
));
}
if ($settings['autoHeight']) {
$summary_elements[] = t('Auto height');
}
if ($settings['collapsible']) {
$summary_elements[] = t('Collapsible');
}
$summary_elements[] = t('Event: @event', array(
'@event' => $settings['event'],
));
$summary = implode(', ', $summary_elements);
break;
// Anchor list formatter settings.
case 'faqfield_anchor_list':
$summary_elements = array();
if ($settings['anchor-list-type'] == 'ul') {
$summary_elements[] = t('Bullet list');
}
else {
$summary_elements[] = t('Numeric list');
}
$summary = implode(', ', $summary_elements);
break;
}
return $summary;
}
/**
* Implements hook_field_formatter_settings_form().
*/
function faqfield_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
$element = array();
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
switch ($display['type']) {
// Accordion display settings.
case 'faqfield_accordion':
// Number of first active element.
$element['active'] = array(
'#type' => 'textfield',
'#title' => t('Active'),
'#default_value' => $settings['active'],
'#description' => t('Index of active element starting from 0. Leave empty to display none at start.'),
// @TODO _element_validate_number() is deprecated since 7.8.
'#element_validate' => array(
function_exists('element_validate_number') ? 'element_validate_number' : '_element_validate_number',
),
'#maxlength' => 3,
'#size' => 5,
);
// Whether auto heigth is enabled.
$element['autoHeight'] = array(
'#type' => 'checkbox',
'#title' => t('Auto height'),
'#default_value' => $settings['autoHeight'],
'#description' => t('If set, the highest content part is used as height reference for all other parts. Provides more consistent animations.'),
);
// Whether elements are collabsible.
$element['collapsible'] = array(
'#type' => 'checkbox',
'#title' => t('Collapsible'),
'#default_value' => $settings['collapsible'],
'#description' => t('Whether an opened question can be collapsed (by the triggering event).'),
);
// Name of triggering event.
$element['event'] = array(
'#type' => 'textfield',
'#title' => t('Event'),
'#default_value' => $settings['event'],
'#description' => t('The event on which to trigger the accordion.'),
'#maxlength' => 10,
);
break;
// Anchor list formatter settings.
case 'faqfield_anchor_list':
// Input for the count of rows for the answer field.
$element['anchor-list-type'] = array(
'#type' => 'select',
'#title' => t('Anchor link list type'),
'#default_value' => $settings['anchor-list-type'],
'#options' => array(
'ul' => t('<ul> - Bullet list'),
'ol' => t('<ol> - Numeric list'),
),
'#description' => t('The type of HTML list used for the anchor link list.'),
);
break;
}
return $element;
}
/**
* Implements hook_field_formatter_info().
*/
function faqfield_field_formatter_info() {
return array(
// This formatter uses jQuery accordion widget.
'faqfield_accordion' => array(
'label' => t('Accordion'),
'field types' => array(
'faqfield',
),
'settings' => array(
'active' => 0,
'autoHeight' => TRUE,
'collapsible' => FALSE,
'event' => 'click',
),
),
// This formatter just displays the FAQ content as simple text.
'faqfield_simple_text' => array(
'label' => t('Simple text (themeable)'),
'field types' => array(
'faqfield',
),
),
// This formatter displays the FAQ content as definition list.
'faqfield_definition_list' => array(
'label' => t('Definition list'),
'field types' => array(
'faqfield',
),
),
// This formatter displays the FAQ content as anchors and text.
'faqfield_anchor_list' => array(
'label' => t('Anchor list and text'),
'field types' => array(
'faqfield',
),
'settings' => array(
'anchor-list-type' => 'ul',
),
),
);
}
/**
* Implements hook_field_formatter_view().
*/
function faqfield_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
$element = array();
// If there are no items to process, we've nothing to do here.
if (empty($items)) {
return $element;
}
switch ($display['type']) {
// This formatter adds jQuery accordion widget.
// This will not be themeable, because changes would break
// jQuery UI accordion functionality!
case 'faqfield_accordion':
// Generate faqfield id by fieldname and entity id.
$entity_ids = entity_extract_ids($entity_type, $entity);
$faqfield_id = 'faqfield_' . $field['field_name'] . '_' . $entity_ids[0];
// We need to convert the element active value to an integer for jQuery.
if ($display['settings']['active'] !== '') {
settype($display['settings']['active'], 'int');
}
else {
$display['settings']['active'] = FALSE;
}
// If jquery_update module is installed and a version >1.5 is used
// the "autoHeight" option has been replaced with "heightStyle".
// To be compatible to higher jQuery versions we have to switch accordingly.
if (module_exists('jquery_update') && variable_get('jquery_update_jquery_version') !== '1.5') {
if ($display['settings']['autoHeight']) {
$display['settings']['heightStyle'] = 'auto';
}
else {
$display['settings']['heightStyle'] = 'content';
}
unset($display['settings']['autoHeight']);
}
// Attach accordion JS library and related display settings.
$element[0]['#attached']['library'][] = array(
'faqfield',
'accordion',
);
$element[0]['#attached']['js'][] = array(
'type' => 'setting',
'data' => array(
'faqfield' => array(
'#' . $faqfield_id => $display['settings'],
),
),
);
// We need to put all of this within a single piece of markup,
// otherwise this would not work with jQuery accordion.
$element[0]['#markup'] = '<div id="' . $faqfield_id . '">';
foreach ($items as $key => $item) {
// Decide whether to use the default format or the custom one.
$format = !empty($item['answer_format']) ? $item['answer_format'] : $field['settings']['format'];
// Build the markup.
$name = 'faq-' . str_replace(' ', '-', $item['question']);
// Store the raw anchor name so the accordion item can be unfurled.
$anchors[$name] = $key;
$name = check_plain($name);
$element[0]['#markup'] .= '<h3 class="faqfield-question" id="' . $name . '"><a href="#' . $name . '">' . check_plain($item['question']) . '</a></h3>';
$element[0]['#markup'] .= '<div class="faqfield-answer">' . check_markup($item['answer'], $format) . '</div>';
}
$element[0]['#markup'] .= '</div>';
$element[0]['#attached']['js'][] = array(
'type' => 'setting',
'data' => array(
'faqfieldAnchors' => $anchors,
),
);
break;
// This themeable formatter displays the FAQ content as simple text.
case 'faqfield_simple_text':
foreach ($items as $delta => $item) {
// Decide whether to use the default format or the custom one.
$format = !empty($item['answer_format']) ? $item['answer_format'] : $field['settings']['format'];
// Add them as page elements, they'll be rendered automatically later.
$element[$delta] = array(
'#theme' => 'faqfield_formatter',
// Filter values before passing them to the template.
'#question' => check_plain($item['question']),
'#answer' => check_markup($item['answer'], $format),
'#delta' => $delta,
);
}
break;
/*
* This formatter displays the FAQ content as definition list, and has been
* updated with the addition of accordion functionality.
*
* Taken from the Web Accessibility for Developers course offered by
* The Chang School at Ryerson University, for details see:
* https://www.canvas.net/browse/ryersonu/courses/adv-web-accessibility
*/
case 'faqfield_definition_list':
drupal_add_js(drupal_get_path('module', 'faqfield') . '/js/faqfield.accordion_dl.js', array(
'scope' => 'header',
'weight' => 51,
));
drupal_add_css(drupal_get_path('module', 'faqfield') . '/css/faqfield.accordion_dl.css');
// Need to add "multiselect" class to work with above javascript.
$element[0]['#markup'] = '<dl class="faqfield-definition-list multiselect">';
foreach ($items as $item) {
// Decide whether to use the default format or the custom one.
$format = !empty($item['answer_format']) ? $item['answer_format'] : $field['settings']['format'];
// Build the markup; adding aria-hidden span with target inside to allow
// for designers to target with text or graphic open/close indicators.
$element[0]['#markup'] .= '<dt class="faqfield-question"><span class="indicator" aria-hidden="true"><span class="indicator-target"></span></span>' . check_plain($item['question']) . '</dt>';
$element[0]['#markup'] .= '<dd class="faqfield-answer">' . check_markup($item['answer'], $format) . '</dd>';
}
$element[0]['#markup'] .= '</dl>';
break;
// This formatter displays the FAQ content as anchors and text.
case 'faqfield_anchor_list':
$element[0]['#markup'] = '<div class="faqfield-anchor-list"><' . $display['settings']['anchor-list-type'] . '>';
$answers_markup = '';
foreach ($items as $item) {
// Build the anchor link list markup.
$name = 'faq-' . check_plain(str_replace(' ', '-', $item['question']));
$element[0]['#markup'] .= '<li><a href="#' . $name . '">' . check_plain($item['question']) . '</a></li>';
// Build the answer text markup.
$answers_markup .= '<h3 class="faqfield-question" id="' . $name . '"><a href="#' . $name . '">' . check_plain($item['question']) . '</a></h3>';
// Decide whether to use the default format or the custom one.
$format = !empty($item['answer_format']) ? $item['answer_format'] : $field['settings']['format'];
$answers_markup .= '<div class="faqfield-answer">' . check_markup($item['answer'], $format) . '</div>';
}
$element[0]['#markup'] .= '</' . $display['settings']['anchor-list-type'] . '>';
// Now attach the answers text markup.
$element[0]['#markup'] .= $answers_markup . '</div>';
break;
}
return $element;
}
/**
* Implements hook_theme().
*/
function faqfield_theme() {
// Themeable simple text formatter.
return array(
'faqfield_formatter' => array(
'variables' => array(
'question' => NULL,
'answer' => NULL,
'delta' => NULL,
),
),
);
}
/**
* Implements hook_library().
*/
function faqfield_library() {
return array(
'accordion' => array(
'title' => 'FAQField Accordion',
'version' => '1.2',
'js' => array(
drupal_get_path('module', 'faqfield') . '/js/faqfield.accordion.js' => array(),
),
'dependencies' => array(
array(
'system',
'ui.accordion',
),
),
),
);
}
/**
* Implements hook_field_widget_info().
*/
function faqfield_field_widget_info() {
return array(
// Simple text inputs.
'faqfield_textboxes' => array(
'label' => t('Default'),
'field types' => array(
'faqfield',
),
'settings' => array(
'question' => '',
'answer' => '',
),
),
);
}
/**
* Implements hook_field_widget_form().
*/
function faqfield_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
$base = $element;
if ($instance['widget']['type'] === 'faqfield_textboxes') {
// If the current form is the default value fieldset of the field settings
// form, we have to save the default value separately (Issue #1526448).
if (isset($form['#title']) && $form['#title'] == t('Default value')) {
$form['#element_validate'][] = 'faqfield_field_widget_form_default_value_element';
}
// If the current value is empty we choose the default one.
if (empty($items[$delta]) && isset($instance['default_values'])) {
$items[$delta] = $instance['default_values'];
}
// Add textfield for question.
$element['question'] = array(
'#title' => check_plain($field['settings']['advanced']['question_title']),
'#type' => @$field['settings']['advanced']['question_rows'] > 1 ? 'textarea' : 'textfield',
'#default_value' => @$items[$delta]['question'],
'#maxlength' => $field['settings']['advanced']['question_length'],
'#delta' => $delta,
'#weight' => 0,
'#rows' => @$field['settings']['advanced']['question_rows'],
) + $base;
// Add textarea / formatable textarea / textfield for answer.
$element['answer'] = array(
'#title' => check_plain($field['settings']['advanced']['answer_title']),
'#type' => $field['settings']['answer_widget'],
'#default_value' => @$items[$delta]['answer'],
'#delta' => $delta,
'#weight' => 1,
// We choose the source output format depending on the input type.
'#format' => $field['settings']['answer_widget'] == 'text_format' ? @$items[$delta]['answer_format'] : $field['settings']['format'],
'#rows' => $field['settings']['advanced']['answer_rows'],
) + $base;
$element['remove'] = array(
'#value' => t('Remove'),
'#type' => 'submit',
'#prefix' => '<p class="faqfield-remove-button">',
'#suffix' => '</p>',
'#weight' => 2,
) + $base;
}
return $element;
}
/**
* Element validation handler (field settings page).
*
* Element validation / submission handler for handling default
* values from the field settings page.
*/
function faqfield_field_widget_form_default_value_element($element, &$form_state, $form) {
$field = $form['#field'];
// As we can set a default format from the field configuration page,
// we want to save it even if question and answer are actually empty
// but we have to make sure the submitted default value is saved
// separately to avoid the widget form beeing build twice (#1526448).
if (isset($form_state['values'][$field['field_name']])) {
// Save the default value in the field instance in our own way.
$default_value = $form_state['values'][$field['field_name']][LANGUAGE_NONE][0];
// If we're using the formatable answer widget we have to
// extract the default values (value and format) out of it.
if (is_array($default_value['answer'])) {
$default_value['answer_format'] = $default_value['answer']['format'];
$default_value['answer'] = $default_value['answer']['value'];
}
$form_state['values']['instance']['default_values'] = $default_value;
$form_state['values'][$field['field_name']] = NULL;
}
}
/**
* Implements hook_field_is_empty().
*
* Here we test whether the submitted values are empty.
* Whether they are default values will be tested by hook_field_presave().
*/
function faqfield_field_is_empty($item, $field) {
$answer_value = is_array($item['answer']) ? $item['answer']['value'] : $item['answer'];
// Return TRUE only if both are empty.
if (empty($item['question']) && empty($answer_value)) {
return TRUE;
}
return FALSE;
}
/**
* Implements hook_field_presave().
*
* Prepare formatable textarea values for saving them into the database.
*/
function faqfield_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
foreach ($items as $key => &$values) {
if (is_array($values['answer'])) {
// Normal textarea's and textfields put their values simply in by
// array($name => $value); Unfortunately text_format textareas put
// them into an array so also the format gets saved: array($name
// => array('value' => $value, 'format' => $format)).
// So the API will try to save normal textfields to the 'name' field
// and text_format fields to 'answer_value' and 'answer_format'.
// To bypass this, we pull the values out of this array and force
// them to be saved in 'answer' and 'answer_format'.
$values['answer_format'] = $values['answer']['format'];
$values['answer'] = $values['answer']['value'];
}
// Here we test if the values are default ones, yes? -> remove them.
if (isset($instance['default_values'])) {
if ($values['question'] == $instance['default_values']['question']) {
if ($values['answer'] == $instance['default_values']['answer']) {
unset($items[$key]);
}
}
}
}
}
/**
* Implements hook_help().
*
* Dirty looking stuff comes in the end.. does actually anyone needs this?
*/
function faqfield_help($path, $arg) {
switch ($path) {
case 'admin/help#faqfield':
$output = '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('This module provides a field for frequently asked questions. Added, you can create simple but smooth FAQs on any piece of content.') . '</p>';
$output .= '<h3>' . t('Usage') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Configuration') . '</dt>';
$output .= '<dd>' . t("No configuration needed.") . '</dd>';
$output .= '<dt>' . t('How to use') . '</dt>';
$output .= '<dd>' . t("You can add the field to any entity (eg. content type, users, ..) as usual. After you applied the field you have to configure how its output should be filtered (eg. Filtered HTML, Plain Text). Also it is recommended to set the number of values to unlimited.") . '</dd>';
$output .= '<dt>' . t('Formatters') . '</dt>';
$output .= '<dd>' . t("You have the choise of two display formatters: jQuery accordion (animated show / hide) and simple text (none formatted, simple output for custom theming). If you are using accordion you can modify its behaviour easily by the display settings.") . '</dd>';
$output .= '</dl>';
return $output;
}
}
/**
* Implements hook_tmgmt_source_translation_structure().
*/
function faqfield_tmgmt_source_translation_structure($entity_type, $entity, $field, $instance, $langcode, $items) {
$structure = array();
if (!empty($items)) {
$structure['#label'] = check_plain($instance['label']);
foreach ($items as $delta => $value) {
$structure[$delta]['#label'] = t('Delta #@delta', array(
'@delta' => $delta,
));
$structure[$delta]['question'] = array(
'#label' => check_plain($field['settings']['advanced']['question_title']),
'#text' => $value['question'],
'#translate' => TRUE,
);
$structure[$delta]['answer_format'] = array(
'#label' => '',
'#text' => $value['answer_format'],
'#translate' => FALSE,
);
$structure[$delta]['answer'] = array(
'#label' => check_plain($field['settings']['advanced']['answer_title']),
'#text' => $value['answer'],
'#translate' => TRUE,
);
}
}
return $structure;
}