t('Paragraphs item'),
'label callback' => 'entity_class_label',
'uri callback' => 'entity_class_uri',
'entity class' => 'ParagraphsItemEntity',
'controller class' => 'EntityAPIController',
'base table' => 'paragraphs_item',
'revision table' => 'paragraphs_item_revision',
'fieldable' => TRUE,
// For integration with Redirect module.
// @see http://drupal.org/node/1263884
'redirect' => FALSE,
'entity keys' => array(
'id' => 'item_id',
'revision' => 'revision_id',
'bundle' => 'bundle',
'field_name' => 'field_name',
),
'module' => 'paragraphs',
'view modes' => array(
'full' => array(
'label' => t('Full content'),
'custom settings' => FALSE,
),
'paragraphs_editor_preview' => array(
'label' => t('Paragraphs Editor Preview'),
'custom settings' => TRUE,
),
),
'bundle keys' => array(
'bundle' => 'bundle',
),
'access callback' => 'paragraphs_item_access',
'metadata controller class' => 'ParagraphsItemMetadataController',
);
$bundles = paragraphs_bundle_load();
// Add info about the bundles. We do not use field_info_fields() but directly
// use field_read_fields() as field_info_fields() requires built entity info
// to work.
foreach ($bundles as $machine_name => $bundle) {
$return['paragraphs_item']['bundles'][$bundle->bundle] = array(
'label' => t('Paragraphs bundle @bundle', array('@bundle' => $bundle->bundle)),
'admin' => array(
'path' => 'admin/structure/paragraphs/%paragraphs_bundle',
'real path' => 'admin/structure/paragraphs/' . strtr($machine_name, array('_' => '-')),
'bundle argument' => 3,
'access arguments' => array('administer paragraphs bundles'),
),
);
}
if (module_exists('entitycache')) {
$return['paragraphs_item']['field cache'] = FALSE;
$return['paragraphs_item']['entity cache'] = TRUE;
}
return $return;
}
/**
* Access check for paragraphs.
*
* Most of the time the access callback is on the host entity.
* In some cases you want specific access checks on paragraphs.
* You can do this by implementing hook_paragraphs_item_access().
*
* @return bool
* Whether the user has access to a paragraphs item.
*/
function paragraphs_item_access($op, $entity, $account, $entity_type) {
// If no user object is supplied, the access check is for the current user.
if (empty($account)) {
$account = $GLOBALS['user'];
}
$permissions = &drupal_static(__FUNCTION__, array());
// If the $op was not one of the supported ones, we return access denied.
if (!in_array($op, array('view', 'update', 'delete', 'create'), TRUE)) {
return FALSE;
}
// When we have no entity, create a generic cid.
if (empty($entity)) {
$cid = 'all_entities:' . $op;
}
// When OP is create, or the entity is new, the bundle is the cache key.
elseif ($op == 'create' || (isset($entity->is_new) && $entity->is_new)) {
$cid = $entity->bundle;
}
// Else our cid is entity specific.
else {
$cid = $entity->item_id . '_' . $entity->revision_id;
}
// If we've already checked access for this bundle, user and op, return from
// cache. Otherwise, we are optimistic and consider that the user can
// view / update / delete or create a paragraph.
if (isset($permissions[$account->uid][$cid][$op])) {
return $permissions[$account->uid][$cid][$op];
}
// We grant access to the paragraph item if both of the following conditions are met:
// - No modules say to deny access.
// - At least one module says to grant access.
// If no module specified either allow or deny, we always allow.
$access = module_invoke_all('paragraphs_item_access', $entity, $op, $account);
if (in_array(PARAGRAPHS_ITEM_ACCESS_DENY, $access, TRUE)) {
$user_access_permission = FALSE;
}
elseif (in_array(PARAGRAPHS_ITEM_ACCESS_ALLOW, $access, TRUE)) {
$user_access_permission = TRUE;
} else {
// Deny access by default.
$user_access_permission = FALSE;
}
// Store the result of the permission in our matrix.
$permissions[$account->uid][$cid][$op] = $user_access_permission;
return $permissions[$account->uid][$cid][$op];
}
/**
* Access check for paragraphs.
*
* Finds the parent entity and then checks for access on the parent entity.
*
* @param ParagraphsItemEntity $entity
* The entity to check for.
*
* @param string $op
* The operation to check for.
*
* @param $account
* The account to check for.
*
* @return bool
* Whether the user has access to a paragraphs item.
*/
function paragraphs_paragraphs_item_access($entity, $op, $account) {
$permissions = &drupal_static(__FUNCTION__, array());
$parent_permissions = &drupal_static(__FUNCTION__ . '_parents', array());
if (!in_array($op, array('view', 'update', 'delete', 'create'), TRUE)) {
// If there was no bundle to check against, or the $op was not one of the
// supported ones, we return access denied.
return PARAGRAPHS_ITEM_ACCESS_IGNORE;
}
$check_parent_op = $op;
// Update/Delete/Create access requires update access on the parent.
if (in_array($op, array('update', 'delete', 'create'), TRUE)) {
$check_parent_op = 'update';
}
// When we have no entity, create a generic cid.
if (empty($entity)) {
$cid = 'all_entities:' . $op;
}
// When OP is create, or the entity is new, the bundle is the cache key.
elseif ($op == 'create' || (isset($entity->is_new) && $entity->is_new)) {
$cid = $entity->bundle;
}
// Else our cid is entity specific.
else {
$cid = $entity->item_id . '_' . $entity->revision_id;
}
// Check if we cached permission check.
if (isset($permissions[$account->uid][$cid][$op])) {
return $permissions[$account->uid][$cid][$op];
}
if (empty($entity)) {
// Ignore access when we don't have a host entity.
$permissions[$account->uid][$cid][$op] = PARAGRAPHS_ITEM_ACCESS_IGNORE;
} elseif ($host_entity = $entity->hostEntity()) {
$host_entity_info = entity_get_info($entity->hostEntityType());
$host_id_key = $host_entity_info['entity keys']['id'];
$parent_cid = $entity->hostEntityType() . '_' . implode('_', entity_extract_ids($entity->hostEntityType(), $host_entity));
// Check if we have an ID key set, if not parent entity is new, we check for create access.
if (!isset($host_entity->{$host_id_key}) || empty($host_entity->{$host_id_key})) {
$check_parent_op = 'create';
}
if (isset($parent_permissions[$account->uid][$parent_cid][$check_parent_op])) {
return $parent_permissions[$account->uid][$parent_cid][$check_parent_op];
}
if (entity_access($check_parent_op, $entity->hostEntityType(), $host_entity)) {
$permissions[$account->uid][$cid][$op] = PARAGRAPHS_ITEM_ACCESS_ALLOW;
$parent_permissions[$account->uid][$parent_cid][$check_parent_op] = PARAGRAPHS_ITEM_ACCESS_ALLOW;
} else {
// Deny access as parent entity access failed.
$permissions[$account->uid][$cid][$op] = PARAGRAPHS_ITEM_ACCESS_DENY;
$parent_permissions[$account->uid][$parent_cid][$check_parent_op] = PARAGRAPHS_ITEM_ACCESS_DENY;
}
} else {
// Ignore access when we don't have a host entity.
$permissions[$account->uid][$cid][$op] = PARAGRAPHS_ITEM_ACCESS_IGNORE;
}
return $permissions[$account->uid][$cid][$op];
}
/**
* Implements hook_permission().
*/
function paragraphs_permission() {
$perms = array(
'administer paragraphs bundles' => array(
'title' => t('Administer paragraphs bundles'),
'description' => t('Is able to administer paragraph bundles for the Paragraphs module'),
),
);
return $perms;
}
/**
* Implements hook_menu().
*/
function paragraphs_menu() {
$items = array();
$items['admin/structure/paragraphs'] = array(
'title' => 'Paragraph Bundles',
'description' => 'Manage Paragraph bundles',
'page callback' => 'paragraphs_admin_bundle_overview',
'access arguments' => array('administer paragraphs bundles'),
'file' => 'paragraphs.admin.inc',
);
$items['admin/structure/paragraphs/list'] = array(
'title' => 'List',
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => -10,
);
$items['admin/structure/paragraphs/add'] = array(
'title' => 'Add Paragraph Bundle',
'page callback' => 'drupal_get_form',
'page arguments' => array('paragraphs_admin_bundle_form'),
'access arguments' => array('administer paragraphs bundles'),
'type' => MENU_LOCAL_ACTION,
'file' => 'paragraphs.admin.inc',
);
$items['admin/structure/paragraphs/%paragraphs_bundle'] = array(
'title' => 'Edit paragraph bundle',
'title callback' => 'paragraphs_bundle_title_callback',
'title arguments' => array(3),
'page callback' => 'drupal_get_form',
'page arguments' => array('paragraphs_admin_bundle_form', 3),
'access arguments' => array('administer paragraphs bundles'),
'file' => 'paragraphs.admin.inc',
);
$items['admin/structure/paragraphs/%paragraphs_bundle/edit'] = array(
'title' => 'Edit',
'type' => MENU_DEFAULT_LOCAL_TASK,
);
$items['admin/structure/paragraphs/%paragraphs_bundle/delete'] = array(
'title' => 'Delete Paragraph Bundle',
'page callback' => 'drupal_get_form',
'page arguments' => array('paragraphs_admin_bundle_delete_form', 3),
'access arguments' => array('administer paragraphs bundles'),
'file' => 'paragraphs.admin.inc',
);
$items['paragraphs/edit/ajax'] = array(
'title' => 'Edit item callback',
'page callback' => 'paragraphs_edit_js',
'delivery callback' => 'ajax_deliver',
'access callback' => TRUE,
'theme callback' => 'ajax_base_page_theme',
'type' => MENU_CALLBACK,
'file' => 'paragraphs.ajax.inc',
);
$items['paragraphs/collapse/ajax'] = array(
'title' => 'Close item callback',
'page callback' => 'paragraphs_collapse_js',
'delivery callback' => 'ajax_deliver',
'access callback' => TRUE,
'theme callback' => 'ajax_base_page_theme',
'type' => MENU_CALLBACK,
'file' => 'paragraphs.ajax.inc',
);
$items['paragraphs/remove/ajax'] = array(
'title' => 'Remove item callback',
'page callback' => 'paragraphs_remove_js',
'delivery callback' => 'ajax_deliver',
'access callback' => TRUE,
'theme callback' => 'ajax_base_page_theme',
'type' => MENU_CALLBACK,
'file' => 'paragraphs.ajax.inc',
);
$items['paragraphs/deleteconfirm/ajax'] = array(
'title' => 'Remove item callback',
'page callback' => 'paragraphs_deleteconfirm_js',
'delivery callback' => 'ajax_deliver',
'access callback' => TRUE,
'theme callback' => 'ajax_base_page_theme',
'type' => MENU_CALLBACK,
'file' => 'paragraphs.ajax.inc',
);
$items['paragraphs/restore/ajax'] = array(
'title' => 'Restore item callback',
'page callback' => 'paragraphs_restore_js',
'delivery callback' => 'ajax_deliver',
'access callback' => TRUE,
'theme callback' => 'ajax_base_page_theme',
'type' => MENU_CALLBACK,
'file' => 'paragraphs.ajax.inc',
);
return $items;
}
/**
* Implements hook_field_info().
*/
function paragraphs_field_info() {
$info = array();
$info['paragraphs'] = array(
'label' => t('Paragraphs'),
'description' => t('Paragraphs field using the paragraph bundles.'),
'instance_settings' => array(
'title' => PARAGRAPHS_DEFAULT_TITLE,
'title_multiple' => PARAGRAPHS_DEFAULT_TITLE_MULTIPLE,
'allowed_bundles' => array(),
'bundle_weights' => array(),
),
'default_widget' => 'paragraphs_hidden',
'default_formatter' => 'paragraphs_view',
'settings' => array(),
'property_type' => 'paragraphs_item',
'property_callbacks' => array('paragraphs_entity_metadata_property_callback'),
);
return $info;
}
/**
* Implements hook_form_field_ui_field_edit_form_alter().
*/
function paragraphs_form_field_ui_field_edit_form_alter(&$form, $form_state) {
if ($form['#field']['type'] == 'paragraphs') {
$form['#theme'] = array('paragraphs_bundle_settings_form');
array_unshift($form['#submit'], 'paragraphs_bundle_settings_form_submit');
}
}
function paragraphs_bundle_settings_form_submit($form, &$form_state) {
$bundle_settings = array();
$bundle_weights = array();
if (isset($form_state['values']['instance']['settings']['allowed_bundles_table'])) {
$bundle_settings_table = $form_state['values']['instance']['settings']['allowed_bundles_table'];
uasort($bundle_settings_table, 'drupal_sort_weight');
foreach ($bundle_settings_table as $machine_name => $value) {
$bundle_settings[$machine_name] = (($value['enabled'] === 1) ? $machine_name : -1);
$bundle_weights[$machine_name] = $value['weight'];
}
}
$form_state['values']['instance']['settings']['allowed_bundles'] = $bundle_settings;
$form_state['values']['instance']['settings']['bundle_weights'] = $bundle_weights;
unset($form_state['values']['instance']['settings']['allowed_bundles_table']);
}
/**
* Implements hook_field_instance_settings_form().
*/
function paragraphs_field_instance_settings_form($field, $instance) {
$settings = $instance['settings'];
$bundles = array();
$_bundles = paragraphs_bundle_load();
$form_delta = count($_bundles) * 2;
$max_weight = 0;
$weights = array();
foreach ($_bundles as $machine_name => $bundle) {
$bundles[$machine_name] = $bundle->name;
if (isset($settings['bundle_weights'][$machine_name])) {
$weights[$machine_name] = $settings['bundle_weights'][$machine_name];
if ($settings['bundle_weights'][$machine_name] > $max_weight) {
$max_weight = $settings['bundle_weights'][$machine_name];
}
}
}
$max_weight++;
$element['allowed_bundles_table'] = array(
'#tree' => TRUE,
'#prefix' => '',
'#suffix' => '
' . t('If no bundle is selected, all the bundles will be available.') . '
',
);
$weight = 1;
foreach ($_bundles as $machine_name => $bundle) {
$enabled = FALSE;
if (isset($settings['allowed_bundles'][$machine_name]) && $settings['allowed_bundles'][$machine_name] === $machine_name) {
$enabled = TRUE;
}
$element['allowed_bundles_table'][$machine_name] = array(
'enabled' => array(
'#type' => 'checkbox',
'#title' => check_plain($bundle->name),
'#title_display' => 'after',
'#default_value' => $enabled,
),
'weight' => array(
'#type' => 'weight',
'#title' => t('Weight'),
'#default_value' => (isset($weights[$machine_name]) ? $weights[$machine_name] : $weight + $max_weight),
'#delta' => $form_delta,
'#title_display' => 'invisible',
),
);
$element['allowed_bundles_table'][$machine_name]['#weight'] = $element['allowed_bundles_table'][$machine_name]['weight']['#default_value'];
$weight++;
}
$element['title'] = array(
'#type' => 'textfield',
'#title' => t('Item Title'),
'#description' => t('Label to appear as title on the button as "Add new [title]", this label is translatable'),
'#default_value' => isset($settings['title']) ? $settings['title'] : PARAGRAPHS_DEFAULT_TITLE,
'#required' => TRUE,
);
$element['title_multiple'] = array(
'#type' => 'textfield',
'#title' => t('Plural Item Title'),
'#description' => t('Title in its plural form.'),
'#default_value' => isset($settings['title_multiple']) ? $settings['title_multiple'] : PARAGRAPHS_DEFAULT_TITLE_MULTIPLE,
'#required' => TRUE,
);
$element['default_edit_mode'] = array(
'#type' => 'select',
'#title' => t('Default edit mode'),
'#description' => t('The default edit mode the paragraph item is in. Preview will render the paragraph in the preview view mode.'),
'#options' => array(
'open' => t('Open'),
'closed' => t('Closed'),
'preview' => t('Preview'),
),
'#default_value' => isset($settings['default_edit_mode']) ? $settings['default_edit_mode'] : PARAGRAPHS_DEFAULT_EDIT_MODE,
'#required' => TRUE,
);
$element['add_mode'] = array(
'#type' => 'select',
'#title' => t('Add mode'),
'#description' => t('The way to add new paragraphs.'),
'#options' => array(
'select' => t('Select List'),
'button' => t('Buttons'),
),
'#default_value' => isset($settings['add_mode']) ? $settings['add_mode'] : PARAGRAPHS_DEFAULT_ADD_MODE,
'#required' => TRUE,
);
if (!count($bundles)) {
$element['allowed_bundles_explain'] = array(
'#type' => 'markup',
'#markup' => t('You did not add any paragraph bundles yet, click !here to add one.', array('!here' => l(t('here'), 'admin/structure/paragraphs/add', array('query' => drupal_get_destination()))))
);
}
$element['fieldset'] = array(
'#type' => 'fieldset',
'#title' => t('Default value'),
'#collapsible' => FALSE,
// As field_ui_default_value_widget() does, we change the #parents so that
// the value below is writing to $instance in the right location.
'#parents' => array('instance'),
);
// Be sure to set the default value to NULL, for example to repair old fields
// that still have one.
$element['fieldset']['default_value'] = array(
'#type' => 'value',
'#value' => NULL,
);
$element['fieldset']['content'] = array(
'#pre' => '
',
'#markup' => t('To specify a default value, configure it via the regular default value setting of each field that is part of the paragraph bundle. To do so, go to the Manage fields screen of the paragraph bundle.', array('!url' => url('admin/structure/paragraphs'))),
'#suffix' => '
',
);
return $element;
}
function theme_paragraphs_bundle_settings_form($variables) {
$form = $variables['form'];
// Initialize the variable which will store our table rows.
$rows = array();
uasort($form['instance']['settings']['allowed_bundles_table'], 'element_sort');
// Iterate over each element in our $form['example_items'] array.
foreach (element_children($form['instance']['settings']['allowed_bundles_table']) as $id) {
// Before we add our 'weight' column to the row, we need to give the
// element a custom class so that it can be identified in the
// drupal_add_tabledrag call.
//
// This could also have been done during the form declaration by adding
// '#attributes' => array('class' => 'example-item-weight'),
// directy to the 'weight' element in tabledrag_example_simple_form().
$form['instance']['settings']['allowed_bundles_table'][$id]['weight']['#attributes']['class'] = array('paragraphs-bundle-item-weight');
// We are now ready to add each element of our $form data to the $rows
// array, so that they end up as individual table cells when rendered
// in the final table. We run each element through the drupal_render()
// function to generate the final html markup for that element.
$rows[] = array(
'data' => array(
// Add our 'enabled' column.
drupal_render($form['instance']['settings']['allowed_bundles_table'][$id]['enabled']),
// Add our 'weight' column.
drupal_render($form['instance']['settings']['allowed_bundles_table'][$id]['weight']),
),
// To support the tabledrag behaviour, we need to assign each row of the
// table a class attribute of 'draggable'. This will add the 'draggable'
// class to the
element for that row when the final table is
// rendered.
'class' => array('draggable'),
);
}
// We now define the table header values. Ensure that the 'header' count
// matches the final column count for your table.
$header = array(t('Bundle'), t('Weight'));
// We also need to pass the drupal_add_tabledrag() function an id which will
// be used to identify the
element containing our tabledrag form.
// Because an element's 'id' should be unique on a page, make sure the value
// you select is NOT the same as the form ID used in your form declaration.
$table_id = drupal_html_id('paragraphs-bundle-table');
// We can render our tabledrag table for output.
$output = theme('table', array(
'header' => $header,
'rows' => $rows,
'attributes' => array('id' => $table_id),
));
$form['instance']['settings']['allowed_bundles_table']['#markup'] = $output;
// And then render any remaining form elements (such as our submit button).
$output = drupal_render_children($form);
// We now call the drupal_add_tabledrag() function in order to add the
// tabledrag.js goodness onto our page.
//
// For a basic sortable table, we need to pass it:
// - the $table_id of our
element,
// - the $action to be performed on our form items ('order'),
// - a string describing where $action should be applied ('siblings'),
// - and the class of the element containing our 'weight' element.
drupal_add_tabledrag($table_id, 'order', 'sibling', 'paragraphs-bundle-item-weight');
return $output;
}
/**
* Implements hook_field_settings_form().
*/
function paragraphs_field_settings_form($field, $instance) {
$form = array();
return $form;
}
/**
* Implements hook_field_presave().
*
* Support saving paragraph items in @code $item['entity'] @endcode. This
* may be used to seamlessly create paragraph items during host-entity
* creation or to save changes to the host entity and its collections at once.
*/
function paragraphs_field_presave($host_entity_type, $host_entity, $field, $instance, $langcode, &$items) {
foreach ($items as $key => &$item) {
// In case the entity has been changed / created, save it and set the id.
// If the host entity creates a new revision, save new item-revisions as
// well.
$entity = FALSE;
if (isset($item['entity'])) {
$entity = paragraphs_field_get_entity($item);
} elseif (isset($item['revision_id'])) {
$entity = paragraphs_item_revision_load($item['revision_id']);
}
if ($entity) {
$entity->setHostEntity($host_entity_type, $host_entity, $langcode, FALSE);
// If the host entity supports revisions and is saved as new revision, do the same for the item.
if (!empty($host_entity->revision)) {
$entity->revision = TRUE;
$is_default = entity_revision_is_default($host_entity_type, $host_entity);
// If an entity type does not support saving non-default entities,
// assume it will be saved as default.
if (!isset($is_default) || $is_default) {
$entity->default_revision = TRUE;
$entity->archived = FALSE;
}
}
if (isset($entity->removed) && $entity->removed) {
unset($items[$key]);
}
else {
$entity->save(TRUE);
$item = array(
'value' => $entity->item_id,
'revision_id' => $entity->revision_id,
);
}
}
}
}
/**
* Implements hook_field_update().
*
* Care about removed paragraph items.
*/
function paragraphs_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) {
// Prevent workbench moderation from deleting paragraphs on node_save() during
// workbench_moderation_store(), when $host_entity->revision == 0.
if (!empty($entity->workbench_moderation['updating_live_revision'])) {
return;
}
$items_original = !empty($entity->original->{$field['field_name']}[$langcode]) ? $entity->original->{$field['field_name']}[$langcode] : array();
$original_by_id = array_flip(paragraphs_field_item_to_ids($items_original));
foreach ($items as $item) {
unset($original_by_id[$item['value']]);
}
// If there are removed items, care about deleting the item entities.
if ($original_by_id) {
$ids = array_flip($original_by_id);
// If we are creating a new revision, the old-items should be kept but get
// marked as archived now.
if (!empty($entity->revision)) {
db_update('paragraphs_item')
->fields(array('archived' => 1))
->condition('item_id', $ids, 'IN')
->execute();
}
else {
// Delete unused paragraph items now.
foreach (paragraphs_item_load_multiple($ids) as $item) {
$item->setHostEntity($entity_type, $entity, $langcode, FALSE);
$item->deleteRevision(TRUE);
}
}
}
}
/**
* Implements hook_field_delete().
*/
function paragraphs_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) {
if ($field['type'] == 'paragraphs') {
// Also delete all embedded entities.
if ($ids = paragraphs_field_item_to_ids($items)) {
// We filter out entities that are still being referenced by other
// host-entities. This should never be the case, but it might happened e.g.
// when modules cloned a node without knowing about paragraphs.
$entity_info = entity_get_info($entity_type);
$entity_id_name = $entity_info['entity keys']['id'];
$field_column = key($field['columns']);
// Extra check to make sure our field exists.
if (is_scalar($field)) {
$field_definition = field_info_field($field['field_name']);
if (!empty($field_definition)) {
foreach ($ids as $id_key => $id) {
$query = new EntityFieldQuery();
$entities = $query
->fieldCondition($field['field_name'], $field_column, $id)
->execute();
unset($entities[$entity_type][$entity->$entity_id_name]);
if (!empty($entities[$entity_type])) {
// Filter this $id out.
unset($ids[$id_key]);
}
}
}
}
entity_delete_multiple('paragraphs_item', $ids);
}
}
}
/**
* Implements hook_field_delete_revision().
*/
function paragraphs_field_delete_revision($entity_type, $entity, $field, $instance, $langcode, &$items) {
if ($field['type'] == 'paragraphs') {
foreach ($items as $item) {
if (!empty($item['revision_id'])) {
if ($paragraphs_item = paragraphs_item_revision_load($item['revision_id'])) {
$paragraphs_item->setHostEntity($entity_type, $entity, $langcode, FALSE);
$paragraphs_item->deleteRevision(TRUE);
}
}
}
}
}
/**
* Get an array of paragraph item IDs stored in the given field items.
*/
function paragraphs_field_item_to_ids($items) {
$ids = array();
foreach ($items as $item) {
if (!empty($item['value'])) {
$ids[] = $item['value'];
}
}
return $ids;
}
/**
* Implements hook_field_is_empty().
*/
function paragraphs_field_is_empty($item, $field) {
// Item is empty when we removed it.
if (isset($item['entity']) && ((isset($item['entity']->removed) && $item['entity']->removed) || (isset($item['entity']->confirmed_removed) && $item['entity']->confirmed_removed))) {
return TRUE;
}
if (!empty($item['value'])) {
return FALSE;
}
elseif (isset($item['entity'])) {
return FALSE;
}
return TRUE;
}
/**
* Determines whether a field paragraphs item entity is empty based on the paragraphs-field.
*/
function paragraphs_item_is_empty(ParagraphsItemEntity $item) {
$instances = field_info_instances('paragraphs_item', $item->bundle);
$is_empty = TRUE;
foreach ($instances as $instance) {
$field_name = $instance['field_name'];
$field = field_info_field($field_name);
// Determine the list of languages to iterate on.
$languages = field_available_languages('paragraphs_item', $field);
foreach ($languages as $langcode) {
if (!empty($item->{$field_name}[$langcode])) {
// If at least one paragraph is not empty; the
// paragraph item is not empty.
foreach ($item->{$field_name}[$langcode] as $field_item) {
if (!module_invoke($field['module'], 'field_is_empty', $field_item, $field)) {
$is_empty = FALSE;
}
}
}
}
}
// Allow other modules a chance to alter the value before returning.
drupal_alter('paragraphs_is_empty', $is_empty, $item);
return $is_empty;
}
/**
* Load a specific bundle or a list of bundles
*
* @param string|null $name
* The machine name or list of bundles to load when null.
*
* @param bool $rebuild
* Whether to use cache or not.
*
* @return array|stdClass|bool
* The bundle, a list of bundles, or FALSE when not found.
*/
function paragraphs_bundle_load($name = NULL, $rebuild = FALSE) {
$cid = 'paragraphs_bundles';
$bundles = array();
// Load bundles from static or from Drupal cache
$_bundles = &drupal_static($cid);
if (isset($_bundles) && !$rebuild) {
$bundles = $_bundles;
}
else {
$_bundles = cache_get($cid);
if ($_bundles && !$rebuild) {
$bundles = $_bundles->data;
}
else {
$query = db_select('paragraphs_bundle', 'pb')
->fields('pb')
->orderBy('pb.bundle', 'ASC');
foreach ($query->execute() as $bundle_object) {
$bundles[$bundle_object->bundle] = $bundle_object;
}
cache_set($cid, $bundles);
}
$_bundles = $bundles;
}
if ($name) {
$name = strtr($name, array('-' => '_'));
if (isset($bundles[$name])) {
return $bundles[$name];
}
return FALSE;
}
else {
return $bundles;
}
}
/**
* Menu load callback to scrub a paragraphs bundle from the URL safe equivalent.
*/
function paragraphs_panelizer_bundle_name_load($name) {
if (($bundle = paragraphs_bundle_load($name))) {
return $bundle->bundle;
}
}
/**
* Function to create or update an paragraphs bundle.
*
* @param stdClass $bundle
* The object of the bundle to create/update.
*
* @return int
* SAVED_UPDATED when updated, SAVED_NEW when created.
*
* @throws Exception
*/
function paragraphs_bundle_save($bundle) {
$is_existing = (bool) db_query_range('SELECT 1 FROM {paragraphs_bundle} WHERE bundle = :bundle', 0, 1, array(':bundle' => $bundle->bundle))->fetchField();
$fields = array(
'bundle' => (string) $bundle->bundle,
'name' => (string) $bundle->name,
'locked' => (int) $bundle->locked,
);
if ($is_existing) {
db_update('paragraphs_bundle')
->fields($fields)
->condition('bundle', $bundle->bundle)
->execute();
$status = SAVED_UPDATED;
}
else {
db_insert('paragraphs_bundle')
->fields($fields)
->execute();
$status = SAVED_NEW;
}
paragraphs_bundle_load(NULL, TRUE);
entity_info_cache_clear();
variable_set('menu_rebuild_needed', TRUE);
return $status;
}
/**
* Function to delete a bundle.
*
* @param $bundle_machine_name
* Machine name of the bundle to delete.
*/
function paragraphs_bundle_delete($bundle_machine_name) {
$bundle = paragraphs_bundle_load($bundle_machine_name);
if ($bundle) {
db_delete('paragraphs_bundle')
->condition('bundle', $bundle->bundle)
->execute();
field_attach_delete_bundle('paragraphs_item', $bundle->bundle);
paragraphs_bundle_load(NULL, TRUE);
entity_info_cache_clear();
variable_set('menu_rebuild_needed', TRUE);
}
}
/**
* Entity property info setter callback for the host entity property.
*
* As the property is of type entity, the value will be passed as a wrapped
* entity.
*/
function paragraphs_item_set_host_entity($item, $property_name, $wrapper) {
if (empty($item->is_new)) {
throw new EntityMetadataWrapperException('The host entity may be set only during creation of a paragraphs item.');
}
$item->setHostEntity($wrapper->type(), $wrapper->value());
}
/**
* Entity property info getter callback for the host entity property.
*/
function paragraphs_item_get_host_entity($item) {
// As the property is defined as 'entity', we have to return a wrapped entity.
return entity_metadata_wrapper($item->hostEntityType(), $item->hostEntity());
}
/**
* Callback for generating entity metadata property info for our field instances.
*
* @see paragraphs_field_info()
*/
function paragraphs_entity_metadata_property_callback(&$info, $entity_type, $field, $instance, $field_type) {
$property = &$info[$entity_type]['bundles'][$instance['bundle']]['properties'][$field['field_name']];
$property['field_name'] = $field['field_name'];
$property['getter callback'] = 'paragraphs_field_property_get';
}
/**
* Entity property info getter callback for the paragraph items.
*
* Like entity_metadata_field_property_get(), but additionally supports getting
* not-yet saved collection items from @code $item['entity'] @endcode.
*/
function paragraphs_field_property_get($entity, array $options, $name, $entity_type, $info) {
$field = field_info_field($name);
$langcode = field_language($entity_type, $entity, $name, isset($options['language']) ? $options['language']->language : NULL);
$values = array();
if (isset($entity->{$name}[$langcode])) {
foreach ($entity->{$name}[$langcode] as $delta => $data) {
// Wrappers do not support multiple entity references being revisions or
// not yet saved entities. In the case of a single reference we can return
// the entity object though.
if ($field['cardinality'] == 1) {
$values[$delta] = paragraphs_field_get_entity($data);
}
elseif (isset($data['value'])) {
$values[$delta] = $data['value'];
}
}
}
// For an empty single-valued field, we have to return NULL.
return $field['cardinality'] == 1 ? ($values ? reset($values) : NULL) : $values;
}
/**
* Gets a paragraphs item entity for a given field item.
*
* @param $field_name
* (optional) If given and there is no entity yet, a new entity object is
* created for the given item.
*
* @return
* The entity object or FALSE.
*/
function paragraphs_field_get_entity(&$item, $bundle = NULL, $field_name = NULL) {
if (isset($item['entity'])) {
return $item['entity'];
}
elseif (isset($item['value'])) {
// By default always load the default revision, so caches get used.
$entity = paragraphs_item_load($item['value']);
if ($entity && $entity->revision_id != $item['revision_id']) {
// A non-default revision is a referenced, so load this one.
$entity = paragraphs_item_revision_load($item['revision_id']);
}
return $entity;
}
elseif (!isset($item['entity']) && isset($bundle) && isset($field_name)) {
$item['entity'] = entity_create('paragraphs_item', array('bundle' => $bundle, 'field_name' => $field_name));
return $item['entity'];
}
return FALSE;
}
/**
* Returns HTML for an individual form element.
*
* Combine multiple values into a table with drag-n-drop reordering.
* TODO : convert to a template.
*
* @param $variables
* An associative array containing:
* - element: A render element representing the form element.
*
* @ingroup themeable
*/
function theme_paragraphs_field_multiple_value_form($variables) {
$element = $variables['element'];
$output = '';
$instance = $element['#instance'];
if (!isset($instance['settings']['title'])) {
$instance['settings']['title'] = PARAGRAPHS_DEFAULT_TITLE;
}
if (!isset($instance['settings']['title_multiple'])) {
$instance['settings']['title_multiple'] = PARAGRAPHS_DEFAULT_TITLE_MULTIPLE;
}
$add_mode = (isset($instance['settings']['add_mode']) ? $instance['settings']['add_mode'] : PARAGRAPHS_DEFAULT_ADD_MODE);
$table_id = drupal_html_id($element['#field_name'] . '_values');
$order_class = $element['#field_name'] . '-delta-order';
$required = !empty($element['#required']) ? theme('form_required_marker', $variables) : '';
$header = array(
array(
'data' => '",
'colspan' => 2,
'class' => array('field-label'),
),
t('Order'),
);
$rows = array();
// Sort items according to '_weight' (needed when the form comes back after
// preview or failed validation)
$items = array();
foreach (element_children($element) as $key) {
if ($key === 'add_more') {
$add_more_button = &$element[$key];
}
elseif ($key === 'add_more_type') {
$add_more_button_type = &$element[$key];
}
else {
if (!isset($element[$key]['#access']) || $element[$key]['#access']) {
$items[] = &$element[$key];
}
}
}
usort($items, '_field_sort_items_value_helper');
// Add the items as table rows.
foreach ($items as $key => $item) {
$item['_weight']['#attributes']['class'] = array($order_class);
$delta_element = drupal_render($item['_weight']);
$cells = array(
array('data' => '', 'class' => array('field-multiple-drag')),
drupal_render($item),
array('data' => $delta_element, 'class' => array('delta-order')),
);
$rows[] = array(
'data' => $cells,
'class' => array('draggable', drupal_html_class('paragraphs_item_type_' . $item['#bundle'])),
);
}
$output = '
';
if (count($items)) {
$output .= theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => $table_id, 'class' => array('field-multiple-table'))));
}
else {
$add_text = 'No !title_multiple added yet. Select a !title type and press the button below to add one.';
if ($add_mode == 'button') {
$add_text = 'No !title_multiple added yet. Select a !title type and press a button below to add one.';
}
$output .= '";
$output .= '
';
drupal_add_tabledrag($table_id, 'order', 'sibling', $order_class);
return $output;
}
/**
* Implements hook_theme().
*/
function paragraphs_theme() {
return array(
'paragraphs_field_multiple_value_form' => array(
'render element' => 'element',
),
'paragraphs_bundle_settings_form' => array(
'render element' => 'form',
),
'paragraphs_items' => array(
'render element' => 'element',
'template' => 'paragraphs-items',
'path' => drupal_get_path('module', 'paragraphs') . '/theme',
'file' => 'paragraphs.theme.inc',
),
'paragraphs_item' => array(
'render element' => 'elements',
'template' => 'paragraphs-item',
'path' => drupal_get_path('module', 'paragraphs') . '/theme',
'file' => 'paragraphs.theme.inc',
),
);
}
/**
* Implements hook_field_create_field().
*/
function paragraphs_field_create_field($field) {
if ($field['type'] == 'paragraphs') {
// Clear caches.
entity_info_cache_clear();
// Do not directly issue menu rebuilds here to avoid potentially multiple
// rebuilds. Instead, let menu_get_item() issue the rebuild on the next
// request.
variable_set('menu_rebuild_needed', TRUE);
}
}
/**
* Implements hook_field_delete_field().
*/
function paragraphs_field_delete_field($field) {
if ($field['type'] == 'paragraphs') {
// Clear caches.
entity_info_cache_clear();
// Do not directly issue menu rebuilds here to avoid potentially multiple
// rebuilds. Instead, let menu_get_item() issue the rebuild on the next
// request.
variable_set('menu_rebuild_needed', TRUE);
}
}
/**
* Implements hook_views_api().
*/
function paragraphs_views_api() {
return array(
'api' => '3.0-alpha1',
'path' => drupal_get_path('module', 'paragraphs') . '/views',
);
}
/**
* Implements hook_module_implements_alter().
*/
function paragraphs_module_implements_alter(&$implementations, $hook) {
switch ($hook) {
case 'field_attach_form':
// We put the implementation of field_attach_form implementation of
// paragraphs at the end, so it has a chance to disable the implementation
// of entity_translation that provides the form changes that will break
// paragraphs.
$group = $implementations['paragraphs'];
unset($implementations['paragraphs']);
$implementations['paragraphs'] = $group;
break;
}
}
/**
* Implements hook_field_attach_form().
*/
function paragraphs_field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode) {
// We make sure paragraphs don't use the entity translation defaults, as those
// are not implemented properly yet in paragraphs. So we better show an empty
// initial field for a translation of an existing entity, than making
// paragraphs break completely.
// A proper implementation of entity_translation has still to be discussed.
// @see https://drupal.org/node/2152931
list( , , $bundle) = entity_extract_ids($entity_type, $entity);
foreach (field_info_instances($entity_type, $bundle) as $instance) {
$field_name = $instance['field_name'];
$field_info = field_info_field($field_name);
if ($field_info['type'] == 'paragraphs') {
if (isset($form[$field_name])) {
$element = &$form[$field_name];
// Remove the entity_translation preparion for the element. This way we
// avoid that there will be form elements that do not have a
// corresponding form state for the field.
if (!empty($element['#process'])) {
$key = array_search('entity_translation_prepare_element', $element['#process']);
if ($key !== FALSE) {
unset($element['#process'][$key]);
}
}
}
}
}
}
/**
* Implements hook_field_prepare_translation().
*
* @see field_attach_prepare_translation()
*/
function paragraphs_field_prepare_translation($entity_type, $entity, $field, $instance, $langcode, &$items, $source_entity, $source_langcode) {
if (!module_exists("paragraphs_i18n")) {
list($id, , ) = entity_extract_ids($entity_type, $entity);
// field_attach_prepare_translation() copied the entity ids from the source,
// as we need a new entity for a new translation, we cannot reuse that.
// @todo clone existing paragraphs to new translation
if (empty($id)) {
$items = array();
}
} else {
paragraphs_i18n_field_prepare_translation($entity_type, $entity, $field, $instance, $langcode, $items, $source_entity, $source_langcode);
}
}
/**
* Implements hook_features_api().
*/
function paragraphs_features_api() {
return array(
'paragraphs' => array(
'name' => t('Paragraphs Bundles'),
'feature_source' => TRUE,
'default_hook' => 'paragraphs_info',
'file' => drupal_get_path('module', 'paragraphs') . '/paragraphs.features.inc',
),
);
}
/**
* Implements hook_bundle_copy_info to provide a bundle copy
* export and import tab.
*/
function paragraphs_bundle_copy_info() {
return array(
'paragraphs_item' => array(
'bundle_export_callback' => 'paragraphs_bundle_load',
'bundle_save_callback' => 'paragraphs_bundle_save',
'export_menu' => array(
'path' => 'admin/structure/paragraphs/export',
'access arguments' => 'administer content types',
),
'import_menu' => array(
'path' => 'admin/structure/paragraphs/import',
'access arguments' => 'administer content types',
),
),
);
}