diff --git a/paragraphs.field_formatter.inc b/paragraphs.field_formatter.inc index b8958c8..516078c 100644 --- a/paragraphs.field_formatter.inc +++ b/paragraphs.field_formatter.inc @@ -71,6 +71,21 @@ function paragraphs_field_formatter_settings_summary($field, $instance, $view_mo } /** + * Implements hook_field_prepare_view(). + * + * Adds a dummy value to the paragraphs field to make rendering possible. + */ +function paragraphs_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) { + if ($field['type'] == 'paragraphs') { + foreach ($items as $key => $item) { + if (!isset($item[0]['value'])) { + $items[$key][0]['value'] = 'scratch_paragraph'; + } + } + } +} + +/** * Implements hook_field_formatter_view(). */ function paragraphs_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) { @@ -84,6 +99,16 @@ function paragraphs_field_formatter_view($entity_type, $entity, $field, $instanc if (empty($items)) { return $element; } + + // If we are dealing with an empty paragraphs field, render an insert point. + if ($items[0]['value'] == 'scratch_paragraph') { + if (!empty($instance['settings']['separate_create'])) { + $entity_ids = entity_extract_ids($entity_type, $entity); + $element[0]['insert_point'] = paragraphs_create_insert_point($entity_type, $entity_ids[0], 'scratch', $instance['field_name']); + } + return $element; + } + // Get view mode from entity. $display_view_mode = empty($display['settings']['view_mode']) ? 'full' : $display['settings']['view_mode']; // Get view mode from field instance (if configured). @@ -94,11 +119,24 @@ function paragraphs_field_formatter_view($entity_type, $entity, $field, $instanc $element['#attributes']['class'][] = drupal_clean_css_identifier('paragraphs-items-field-' . $instance['field_name']); $element['#view_mode'] = $view_mode; + $count = 0; foreach ($items as $delta => $item) { if ($paragraph = paragraphs_field_get_entity($item)) { $paragraph->setHostEntity($entity_type, $entity, $langcode); if (entity_access('view', 'paragraphs_item', $paragraph)) { + + // Enter an insert point before the FIRST paragraph item + if (!empty($instance['settings']['separate_create']) && $count == 0) { + $element[$delta]['insert_point_top'] = paragraphs_create_insert_point($entity_type, $paragraph->item_id, 'before', NULL); + } + + // Emter the paragraph item itself $element[$delta]['entity'] = $paragraph->view($view_mode); + + // Set insert point after every paragraph item + if (!empty($instance['settings']['separate_create'])) { + $element[$delta]['insert_point'] = paragraphs_create_insert_point($entity_type, $paragraph->item_id, 'after', NULL); + } } if (!empty($instance['settings']['separate_edit'])) { $contextual_links = array( @@ -107,9 +145,26 @@ function paragraphs_field_formatter_view($entity_type, $entity, $field, $instanc ); $element[$delta]['entity']['paragraphs_item'][$paragraph->item_id]['#contextual_links']['paragraphs'] = $contextual_links; } + $count++; } } break; } + return $element; } + +/** + * Helper function to create an insert point + */ +function paragraphs_create_insert_point($entity_type, $entity_id, $mode, $field_name) { + return array( + '#theme' => 'insert_point', + '#contextual_links' => array( + 'paragraphs' => array( + 'paragraphs_add', + array($entity_type, $entity_id, $mode, $field_name), + ), + ), + ); +} diff --git a/paragraphs.module b/paragraphs.module index 8369118..daf12ce 100644 --- a/paragraphs.module +++ b/paragraphs.module @@ -11,6 +11,8 @@ define('PARAGRAPHS_DEFAULT_TITLE_MULTIPLE', 'Paragraphs'); define('PARAGRAPHS_DEFAULT_EDIT_MODE', 'open'); define('PARAGRAPHS_DEFAULT_ADD_MODE', 'select'); define('PARAGRAPHS_DEFAULT_SEPARATE_EDIT', FALSE); +define('PARAGRAPHS_DEFAULT_SEPARATE_DELETE', FALSE); +define('PARAGRAPHS_DEFAULT_SEPARATE_CREATE', FALSE); /** * Modules should return this value from hook_paragraphs_item_access() to allow access to a paragraphs item. @@ -419,14 +421,42 @@ function paragraphs_menu() { 'type' => MENU_LOCAL_ACTION, 'context' => MENU_CONTEXT_INLINE, 'page callback' => 'drupal_get_form', - 'page arguments' => array('paragraphs_form', 1), + 'page arguments' => array('paragraphs_form_edit', 1), 'access callback' => 'paragraphs_separate_edit_access', 'access arguments' => array(1), ); + $items['paragraphs/%paragraphs_item/delete'] = array( + 'title' => t('Delete paragraph'), + 'type' => MENU_LOCAL_ACTION, + 'context' => MENU_CONTEXT_INLINE, + 'page callback' => 'drupal_get_form', + 'page arguments' => array('paragraphs_form_delete', 1), + 'access callback' => 'paragraphs_separate_delete_access', + 'access arguments' => array(1), + ); + + $items['paragraphs/add/%/%paragraphs_bundle/%/%/%'] = array( + 'title' => t('Add paragraph'), + 'title callback' => 'paragraphs_add_title', + 'title arguments' => array(3), + 'type' => MENU_LOCAL_ACTION, + 'context' => MENU_CONTEXT_INLINE, + 'page callback' => 'drupal_get_form', + 'page arguments' => array('paragraphs_form_add', 2, 3, 4, 5, 6), + 'access callback' => 'paragraphs_separate_create_access', + 'access arguments' => array(2, 3, 4, 5, 6), + ); + return $items; } +/** + * Title callback for separate add page. + */ +function paragraphs_add_title($paragraphs_bundle) { + return t('Add !bundle', array('!bundle' => $paragraphs_bundle->name)); +} /** * Implements hook_field_info(). @@ -580,10 +610,24 @@ function paragraphs_field_instance_settings_form($field, $instance) { $element['separate_edit'] = array( '#type' => 'checkbox', '#title' => t('Enable separate edit with contextual links'), - '#description' => t('Enables contextual links per paragraph for editting on a separate page.'), + '#description' => t('Enables contextual links per paragraph for editing on a separate page.'), '#default_value' => isset($settings['separate_edit']) ? $settings['separate_edit'] : PARAGRAPHS_DEFAULT_SEPARATE_EDIT, ); + $element['separate_delete'] = array( + '#type' => 'checkbox', + '#title' => t('Enable separate delete with contextual links'), + '#description' => t('Enables contextual links per paragraph for deleting on a separate page.'), + '#default_value' => isset($settings['separate_delete']) ? $settings['separate_delete'] : PARAGRAPHS_DEFAULT_SEPARATE_DELETE, + ); + + $element['separate_create'] = array( + '#type' => 'checkbox', + '#title' => t('Enable separate create with contextual links'), + '#description' => t('Enables contextual links to create new paragraphs on a separate page.'), + '#default_value' => isset($settings['separate_create']) ? $settings['separate_create'] : PARAGRAPHS_DEFAULT_SEPARATE_CREATE, + ); + if (!count($bundles)) { $element['allowed_bundles_explain'] = array( '#type' => 'markup', @@ -1233,6 +1277,12 @@ function paragraphs_theme() { 'path' => drupal_get_path('module', 'paragraphs') . '/theme', 'file' => 'paragraphs.theme.inc', ), + 'insert_point' => array( + 'render element' => 'element', + 'template' => 'paragraphs-item', + 'path' => drupal_get_path('module', 'paragraphs') . '/theme', + 'file' => 'paragraphs.theme.inc', + ), ); } @@ -1388,7 +1438,7 @@ function paragraphs_bundle_copy_info() { /** * Separate paragraphs_item edit form */ -function paragraphs_form($form, &$form_state, $paragraphs_item) { +function paragraphs_form_edit($form, &$form_state, $paragraphs_item) { if (!$paragraphs_item) { drupal_not_found(); } @@ -1414,23 +1464,176 @@ function paragraphs_form($form, &$form_state, $paragraphs_item) { } /** + * Separate paragraphs_item delete confirm page + */ +function paragraphs_form_delete($form, &$form_state, $paragraphs_item) { + $paragraphs_bundle = paragraphs_bundle_load($paragraphs_item->bundle); + drupal_set_title(t('Delete !title paragraph', array('!title' => $paragraphs_bundle->name))); + + $form['paragraphs_item'] = array( + '#type' => 'value', + '#value' => $paragraphs_item, + ); + + return confirm_form( + $form, + t('Are you sure you want to delete this !bundle paragraph?', array('!bundle' => $paragraphs_bundle->name)), + NULL, + t('This action cannot be undone.'), + t('Delete paragraph'), + t('Cancel')); +} + +/** + * Separate paragraphs_item create form + */ +function paragraphs_form_add($form, &$form_state, $mode, $paragraphs_bundle, $host_entity_type, $entity_id, $field_name) { + $new_paragraph_position = 0; + + // If we start from an empty paragraph field + if ($mode == 'scratch') { + // Load the host entity for the new paragraph + $host_entity = entity_load($host_entity_type, array($entity_id)); + $host_entity = reset($host_entity); + } + + // Inserting a paragraph before or after an existing paragraph + else { + // Load the existing paragraphs item + $paragraphs_item = paragraphs_item_load_multiple(array($entity_id)); + $paragraphs_item = reset($paragraphs_item); + $host_entity = $paragraphs_item->hostEntity(); + $field_name = $paragraphs_item->field_name; + + // If we are inserting the new paragraph after an existing one + if ($mode == 'after' && $paragraphs_item) { + // Load the paragraphs field + $paragraphs = field_get_items($host_entity_type, $host_entity, $paragraphs_item->field_name); + + // Calculate the position of the new paragraph (delta) + foreach ($paragraphs as $key => $value) { + if ($value['value'] === $paragraphs_item->item_id) { + // After the existing one + $new_paragraph_position = $key + 1; + break; + } + } + } + } + + // Create the new paragraph item and set the host entity + position. + $new_paragraph = entity_create('paragraphs_item', array('bundle' => $paragraphs_bundle->bundle, 'field_name' => $field_name)); + $new_paragraph->setHostEntity($host_entity_type, $host_entity, $host_entity->language, FALSE); + $new_paragraph->position = $new_paragraph_position; + + // Nice title for the creation page + drupal_set_title(t('Create !bundle paragraph', array('!bundle' => $paragraphs_bundle->name))); + + // Pass variables in form for usage in submit handler + $form['paragraphs_bundle'] = array( + '#type' => 'value', + '#value' => $paragraphs_bundle, + ); + $form['paragraphs_item'] = array( + '#type' => 'value', + '#value' => $new_paragraph, + ); + $form['host_entity'] = array( + '#type' => 'value', + '#value' => $host_entity, + ); + $form['host_entity_type'] = array( + '#type' => 'value', + '#value' => $host_entity_type, + ); + $form['mode'] = array( + '#type' => 'value', + '#value' => $mode, + ); + + // Attach the form to create the new paragraphs item + field_attach_form('paragraphs_item', $new_paragraph, $form, $form_state); + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#weight' => 10000, + '#value' => t('Save'), + ); + + // Use the same validation handler as the edit form + $form['#validate'][] = 'paragraphs_form_edit_validate'; + + return $form; +} + +/** * Validation function for entity form for validating the fields. */ -function paragraphs_form_validate($form, &$form_state) { +function paragraphs_form_edit_validate($form, &$form_state) { field_attach_form_validate('paragraphs_item', $form_state['values']['paragraphs_item'], $form, $form_state); } /** - * Validation function for entity form for validating the fields. + * Submit function for edit entity form. */ -function paragraphs_form_submit($form, &$form_state) { +function paragraphs_form_edit_submit($form, &$form_state) { $paragraphs_item = $form_state['values']['paragraphs_item']; field_attach_submit('paragraphs_item', $paragraphs_item, $form, $form_state); $paragraphs_item->save(TRUE); - $bundle = paragraphs_bundle_load($paragraphs_item->bundle); - drupal_set_message(t('Paragraph !title has been saved.', array('!title' => $bundle->name))); + $paragraphs_bundle = paragraphs_bundle_load($paragraphs_item->bundle); + drupal_set_message(t('Paragraph !bundle has been saved.', array('!bundle' => $paragraphs_bundle->name))); +} +/** + * Submit function for delete entity form. + */ +function paragraphs_form_delete_submit($form, &$form_state) { + $paragraphs_item = $form_state['values']['paragraphs_item']; + $paragraphs_bundle = paragraphs_bundle_load($paragraphs_item->bundle); + $paragraphs_item->deleteRevision(); + + drupal_set_message(t('Paragraph !bundle has been deleted.', array('!bundle' => $paragraphs_bundle->name))); +} + +/** + * Submit function for create entity form. + */ +function paragraphs_form_add_submit($form, &$form_state) { + // Get data from form object + $paragraphs_item = $form['paragraphs_item']['#value']; + $host_entity = $form['host_entity']['#value']; + $host_entity_type = $form['host_entity_type']['#value']; + $mode = $form['mode']['#value']; + + // We have to perform a host save if we are adding the first paragraph in the paragraph field + $skip_host_save = $mode == 'scratch' ? FALSE : TRUE; + + // Set host entity for new paragraph + $paragraphs_item->setHostEntity($host_entity_type, $host_entity, $host_entity->language, FALSE); + + // Attach submit handler for paragraph items + save paragraph item + field_attach_submit('paragraphs_item', $paragraphs_item, $form, $form_state); + $paragraphs_item->save($skip_host_save); + + // If a paragraphs item is passed, we want to add the new paragraph after it + if ($paragraphs_item && $mode != 'scratch') { + $new_paragraph = array(array('value' => $paragraphs_item->item_id, 'revision_id' => $paragraphs_item->revision_id)); + + // Load the paragraphs field from the host entity and insert the new paragraph item in the correct position + if ($paragraphs = field_get_items($host_entity_type, $host_entity, $paragraphs_item->field_name)) { + array_splice($paragraphs, $paragraphs_item->position, 0, $new_paragraph); + } + + // Overwrite the paragraph field + save the host entity + $host_entity->field_paragraphs['und'] = $paragraphs; + entity_save($host_entity_type, $host_entity); + } + + // Nice confirmation message + $paragraphs_bundle = paragraphs_bundle_load($paragraphs_item->bundle); + drupal_set_message(t('Paragraph !bundle has been saved.', array('!bundle' => $paragraphs_bundle->name))); } /** @@ -1448,6 +1651,47 @@ function paragraphs_separate_edit_access($paragraphs_item) { } /** + * Access callback for separate add form. + */ +function paragraphs_separate_delete_access($paragraphs_item) { + /** @var ParagraphsItemEntity $paragraphs_item */ + $paragraphs_item->hostEntity(); + $host_entity_type = $paragraphs_item->hostEntityType(); + $instance = $paragraphs_item->instanceInfo(); + + // Check separate forms are enabled and that the user has access to update + // the host entity as well as the paragraph item itself. + return !empty($instance['settings']['separate_delete']) && entity_access('delete', $host_entity_type, $paragraphs_item->hostEntity()) && entity_access('delete', 'paragraphs_item', $paragraphs_item); +} + +/** + * Access callback for separate add form. + */ +function paragraphs_separate_create_access($mode, $paragraphs_bundle, $host_entity_type, $entity_id, $field_name) { + if (!$entity_id && !in_array($mode, array('scratch', 'before', 'after'))) { + return FALSE; + } + + if ($mode == 'scratch') { + // Load host entity to get field instance info + $host_entity = entity_load($host_entity_type, array($entity_id)); + $host_entity = reset($host_entity); + $entity_info = entity_extract_ids($host_entity_type, $host_entity); + $instance = field_info_instance($host_entity_type, $field_name, $entity_info[2]); + } + else { + // Load paragraphs item to get field instance info + $paragraphs_item = paragraphs_item_load_multiple(array($entity_id)); + $paragraphs_item = reset($paragraphs_item); + $host_entity = $paragraphs_item->hostEntity(); + $instance = $paragraphs_item->instanceInfo(); + } + + // Check if separate forms are enabled and we can update the host entity + return !empty($instance['settings']['separate_create']) && entity_access('update', $host_entity_type, $host_entity); +} + +/** * Implements hook_contextual_links_view_alter(). * * Since there is no root path for the separate form for editing a paragraph, @@ -1458,7 +1702,12 @@ function paragraphs_separate_edit_access($paragraphs_item) { function paragraphs_contextual_links_view_alter(&$element, $items) { if (isset($element['#contextual_links']['paragraphs'])) { // Retrieve contextual menu links. - $items += paragraphs_contextual_links($element['#contextual_links']['paragraphs'][1]); + if ($element['#contextual_links']['paragraphs'][0] == 'paragraphs_add') { + $items += paragraphs_contextual_links_add($element['#contextual_links']['paragraphs'][1]); + } + else { + $items += paragraphs_contextual_links($element['#contextual_links']['paragraphs'][1]); + } // Transform contextual links into parameters suitable for theme_link(). $links = array(); @@ -1477,6 +1726,19 @@ function paragraphs_contextual_links_view_alter(&$element, $items) { } } +function paragraphs_get_router_items($base) { + // Performance: We only query available tasks once per request. + return db_select('menu_router', 'm') + ->fields('m') + ->condition('tab_root', db_like($base) . '%', 'LIKE') + ->condition('context', MENU_CONTEXT_NONE, '<>') + ->condition('context', MENU_CONTEXT_PAGE, '<>') + ->orderBy('weight') + ->orderBy('title') + ->execute() + ->fetchAllAssoc('path', PDO::FETCH_ASSOC); +} + /** * Retrieves contextual links for a path based on registered local tasks. * @@ -1485,18 +1747,9 @@ function paragraphs_contextual_links_view_alter(&$element, $items) { function paragraphs_contextual_links($args) { $links = array(); - // Performance: We only query available tasks once per request. $data = &drupal_static(__FUNCTION__); if (!isset($data)) { - $data = db_select('menu_router', 'm') - ->fields('m') - ->condition('tab_root', db_like('paragraphs/%') . '%', 'LIKE') - ->condition('context', MENU_CONTEXT_NONE, '<>') - ->condition('context', MENU_CONTEXT_PAGE, '<>') - ->orderBy('weight') - ->orderBy('title') - ->execute() - ->fetchAllAssoc('path', PDO::FETCH_ASSOC); + $data = paragraphs_get_router_items('paragraphs/%'); } array_unshift($args, 'paragraphs'); @@ -1509,6 +1762,7 @@ function paragraphs_contextual_links($args) { if (!$item['access']) { continue; } + // All contextual links are keyed by the actual "task" path argument, // prefixed with the name of the implementing module. $links['paragraphs-' . $key] = $item; @@ -1518,11 +1772,63 @@ function paragraphs_contextual_links($args) { } /** + * Retrieves contextual links for a path based on registered local tasks. + * + * @see menu_contextual_links() + */ +function paragraphs_contextual_links_add($args) { + $links = array(); + + $data = &drupal_static(__FUNCTION__); + if (!isset($data)) { + $data = paragraphs_get_router_items('paragraphs/add'); + } + + list($entity_type, $entity_id, $mode) = $args; + + foreach ($data as $item) { + if ($mode == 'scratch') { + $host_entity = entity_load($entity_type, array($entity_id)); + $host_entity = reset($host_entity); + $field_name = $args[3]; + } + else { + $paragraphs_item = paragraphs_item_load($entity_id); + $host_entity = $paragraphs_item->hostEntity(); + $field_name = $paragraphs_item->field_name; + } + + $field_info = field_info_instance($entity_type, $field_name, $host_entity->type); + foreach ($field_info['settings']['allowed_bundles'] as $allowed_bundle) { + if ($mode == 'scratch') { + $params = array('paragraphs', 'add', $mode, $allowed_bundle, $entity_type, $entity_id, $field_name); + } + else { + $params = array('paragraphs', 'add', $mode, $allowed_bundle, $entity_type, $paragraphs_item->item_id, $field_name); + } + + // Denormalize and translate the contextual link. + _menu_translate($item, $params, TRUE); + if (!$item['access']) { + continue; + } + // All contextual links are keyed by the actual "task" path argument, + // prefixed with the name of the implementing module. + $links['paragraphs-add-' . $allowed_bundle] = $item; + } + } + + return $links; +} + +/** * Implements hook_admin_paths(). */ function paragraphs_admin_paths() { $paths = array( 'paragraphs/*/edit' => TRUE, + 'paragraphs/*/delete' => TRUE, + 'paragraphs/add/*' => TRUE, ); return $paths; diff --git a/theme/paragraphs.theme.inc b/theme/paragraphs.theme.inc index dffbb18..b49959c 100644 --- a/theme/paragraphs.theme.inc +++ b/theme/paragraphs.theme.inc @@ -20,3 +20,11 @@ function template_preprocess_paragraphs_items(&$variables, $hook) { $variables['theme_hook_suggestions'][] = 'paragraphs_items__' . $variables['element']['#field_name']; $variables['theme_hook_suggestions'][] = 'paragraphs_items__' . $variables['element']['#field_name'] . '__' . $variables['view_mode']; } + +/** + * Process variables for insert-point.tpl.php + */ +function template_preprocess_insert_point(&$variables, $hook) { + // We use a simple non-breaking space to make the insert point visible. + $variables['content'] = ' '; +}