diff --git a/css/media.css b/css/media.css
index 2ea3699..2397a17 100644
--- a/css/media.css
+++ b/css/media.css
@@ -62,30 +62,34 @@
/* Media item display */
.media-item {
- background: #FFFFFF;
+ background: #eee;
border: 1px solid #CCCCCC;
+ box-shadow: inset 0 0 15px rgba(0,0,0,.1), inset 0 0 0 1px rgba(0,0,0,.05);
+ display: inline-block;
padding: 5px;
- position: relative;
+ position: relative;
}
.media-item img {
- margin-bottom: 10px;
+ display: block;
}
.media-item .label-wrapper {
- text-align: center;
- position: absolute;
+ background: rgba(255,255,255,.8);
bottom: 0;
- margin-left: auto;
- margin-right: auto;
- width: 90%;
+ box-shadow: inset 0 0 0 1px rgba(0,0,0,.15);
+ left: 0;
+ max-height: 100%;
+ overflow: hidden;
+ position: absolute;
+ right: 0;
+ text-align: center;
+ word-wrap: break-word;
}
.media-item .label-wrapper label {
font-size: 10px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+ padding: 5px 10px;
}
/* Media item lists */
@@ -124,8 +128,8 @@
}
.media-list-thumbnails .form-type-checkbox {
- bottom: 135px;
- left: 8px;
+ bottom: 117px;
+ left: 6px;
margin: 0;
padding: 0;
position: relative;
@@ -135,26 +139,6 @@
.media-widget .preview {
display: inline-block;
- vertical-align: middle;
-}
-
-.media-widget .preview a {
- text-decoration: none;
-}
-
-.media-widget .preview .media-item {
margin-right: 10px;
-}
-
-.media-widget .preview .media-item:hover {
- border-color: #058AC5;
- cursor: pointer;
-}
-
-.media-widget .preview .media-item .label-wrapper label {
- color: #058AC5;
-}
-
-.media-widget .preview .media-item .label-wrapper label:hover {
- cursor: pointer;
+ vertical-align: middle;
}
diff --git a/includes/media.fields.inc b/includes/media.fields.inc
index 29b63a9..65abe28 100644
--- a/includes/media.fields.inc
+++ b/includes/media.fields.inc
@@ -12,16 +12,15 @@
function media_field_widget_info() {
return array(
'media_generic' => array(
- 'label' => t('Media file selector'),
+ 'label' => t('Media browser'),
'field types' => array('file', 'image'),
'settings' => array(
- 'progress_indicator' => 'throbber',
'allowed_types' => array('image'),
'browser_plugins' => array(),
'allowed_schemes' => array('public', 'private'),
),
'behaviors' => array(
- 'multiple values' => FIELD_BEHAVIOR_DEFAULT,
+ 'multiple values' => FIELD_BEHAVIOR_CUSTOM,
'default value' => FIELD_BEHAVIOR_NONE,
),
),
@@ -77,45 +76,43 @@ function media_field_formatter_view($entity_type, $entity, $field, $instance, $l
function media_field_widget_settings_form($field, $instance) {
$widget = $instance['widget'];
$settings = $widget['settings'];
- $form = array();
- $streams = file_get_stream_wrappers(STREAM_WRAPPERS_VISIBLE);
+ $plugins = media_get_browser_plugin_info();
+ $options = array();
+ foreach ($plugins as $key => $plugin) {
+ $options[$key] = check_plain($plugin['title']);
+ }
+
+ $form['browser_plugins'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Enabled browser plugins'),
+ '#options' => $options,
+ '#default_value' => $settings['browser_plugins'],
+ '#description' => t('Media browser plugins which are allowed for this field. If no plugins are selected, they will all be available.'),
+ );
$form['allowed_types'] = array(
'#type' => 'checkboxes',
- '#title' => t('Allowed remote media types'),
+ '#title' => t('Allowed file types'),
'#options' => file_entity_type_get_names(),
'#default_value' => $settings['allowed_types'],
- '#description' => t('Media types which are allowed for this field when using remote streams.'),
- '#weight' => 1,
- '#access' => count(file_get_stream_wrappers(STREAM_WRAPPERS_VISIBLE | STREAM_WRAPPERS_LOCAL)) != count($streams),
+ '#description' => t('File types which are allowed for this field. If no file types are selected, they will all be available.'),
);
+ $visible_steam_wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_VISIBLE);
$options = array();
- foreach ($streams as $scheme => $data) {
- $options[$scheme] = t('@scheme (@name)', array('@scheme' => $scheme . '://', '@name' => $data['name']));
+ foreach ($visible_steam_wrappers as $scheme => $information) {
+ $options[$scheme] = check_plain($information['name']);
}
+
$form['allowed_schemes'] = array(
'#type' => 'checkboxes',
'#title' => t('Allowed URI schemes'),
'#options' => $options,
'#default_value' => $settings['allowed_schemes'],
- '#description' => t('URI schemes include public:// and private:// which are the Drupal files directories, and may also refer to remote sites.'),
- '#weight' => 2,
+ '#description' => t('URI schemes which are allowed for this field. If no schemes are selected, they will all be available.'),
);
- $plugins = media_get_browser_plugin_info();
- $form['browser_plugins'] = array(
- '#type' => 'checkboxes',
- '#title' => t('Enabled browser plugins'),
- '#options' => array(),
- '#default_value' => $settings['browser_plugins'],
- '#description' => t('If no plugins are selected, they will all be available.'),
- );
- foreach ($plugins as $key => $plugin) {
- $form['browser_plugins']['#options'][$key] = !empty($plugin['title']) ? $plugin['title'] : $key;
- }
-
return $form;
}
@@ -123,33 +120,35 @@ function media_field_widget_settings_form($field, $instance) {
* Implements hook_field_widget_form().
*/
function media_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
- $field_settings = $instance['settings'];
- $widget_settings = $instance['widget']['settings'];
- // @todo The Field API supports automatic serialization / unserialization, so
- // this should no longer be needed. After verifying with a module that uses
- // the 'data' column, remove this.
- // @see media_field_widget_value()
- $current_value = array();
- if (isset($items[$delta])) {
- $current_value = $items[$delta];
- // @todo $items[$delta] is sometimes a loaded media entity (an object)
- // rather than an array. This conflicts with Field API expectations (for
- // example, it results in fatal errors when previewing a node with a
- // multi-valued media field), so should be fixed. In the meantime, don't
- // assume that $current_value is an array.
- if (is_array($current_value) && isset($current_value['data']) && is_string($current_value['data'])) {
- $current_value['data'] = unserialize($current_value['data']);
- }
+ // Add display_field setting to field because media_field_widget_form() assumes it is set.
+ if (!isset($field['settings']['display_field'])) {
+ $field['settings']['display_field'] = 0;
+ }
+
+ $defaults = array(
+ 'fid' => 0,
+ 'display' => !empty($field['settings']['display_default']),
+ 'description' => '',
+ );
+
+ // Load the items for form rebuilds from the field state as they might not be
+ // in $form_state['values'] because of validation limitations. Also, they are
+ // only passed in as $items when editing existing entities.
+ $field_state = field_form_get_state($element['#field_parents'], $field['field_name'], $langcode, $form_state);
+ if (isset($field_state['items'])) {
+ $items = $field_state['items'];
}
+ $field_settings = $instance['settings'];
+ $widget_settings = $instance['widget']['settings'];
+
+ // Essentially we use the media type, extended with some enhancements.
+ $element_info = element_info('media');
$element += array(
- // @todo This should be a fieldset, but throws a warning about
- // element_children.
'#type' => 'media',
- '#collapsed' => TRUE,
- '#default_value' => $current_value,
- '#required' => $instance['required'],
+ '#value_callback' => 'media_field_widget_value',
+ '#process' => array_merge($element_info['#process'], array('media_field_widget_process')),
'#media_options' => array(
'global' => array(
'types' => array_filter($widget_settings['allowed_types']),
@@ -161,57 +160,386 @@ function media_field_widget_form(&$form, &$form_state, $field, $instance, $langc
'uri_scheme' => !empty($field['settings']['uri_scheme']) ? $field['settings']['uri_scheme'] : file_default_scheme(),
),
),
+ // Allows this field to return an array instead of a single value.
+ '#extended' => TRUE,
);
- if ($field['cardinality'] != 1) {
- $element['#title'] = check_plain($instance['label']);
- $element['#title_display'] = 'invisible';
+ // Add image field specific validators.
+ if ($field['type'] == 'image') {
+ if ($field_settings['min_resolution'] || $field_settings['max_resolution']) {
+ $element['#media_options']['global']['min_resolution'] = $field_settings['min_resolution'];
+ $element['#media_options']['global']['max_resolution'] = $field_settings['max_resolution'];
+ }
+ }
+
+ if ($field['cardinality'] == 1) {
+ // Set the default value.
+ $element['#default_value'] = !empty($items) ? $items[0] : $defaults;
+ // If there's only one field, return it as delta 0.
+ $elements = array($element);
+ }
+ else {
+ // If there are multiple values, add an element for each existing one.
+ foreach ($items as $item) {
+ $elements[$delta] = $element;
+ $elements[$delta]['#default_value'] = $item;
+ $elements[$delta]['#weight'] = $delta;
+ $delta++;
+ }
+ // And then add one more empty row for new uploads except when this is a
+ // programmed form as it is not necessary.
+ if (($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta < $field['cardinality']) && empty($form_state['programmed'])) {
+ $elements[$delta] = $element;
+ $elements[$delta]['#default_value'] = $defaults;
+ $elements[$delta]['#weight'] = $delta;
+ $elements[$delta]['#required'] = ($element['#required'] && $delta == 0);
+ }
+ // The group of elements all-together need some extra functionality
+ // after building up the full list (like draggable table rows).
+ $elements['#file_upload_delta'] = $delta;
+ $elements['#theme'] = 'media_widget_multiple';
+ $elements['#theme_wrappers'] = array('fieldset');
+ $elements['#process'] = array('media_field_widget_process_multiple');
+ $elements['#title'] = $element['#title'];
+ $elements['#description'] = $element['#description'];
+ $elements['#field_name'] = $element['#field_name'];
+ $elements['#language'] = $element['#language'];
+ $elements['#display_field'] = $field['settings']['display_field'];
+
+ // Add some properties that will eventually be added to the file upload
+ // field. These are added here so that they may be referenced easily through
+ // a hook_form_alter().
+ $elements['#file_upload_title'] = t('Attach media');
+ }
+
+ return $elements;
+}
+
+/**
+ * The #value_callback for the media_generic field element.
+ */
+function media_field_widget_value($element, $input = FALSE, $form_state) {
+ if ($input) {
+ // Checkboxes lose their value when empty.
+ // If the display field is present make sure its unchecked value is saved.
+ $field = field_widget_field($element, $form_state);
+ if (empty($input['display'])) {
+ $input['display'] = $field['settings']['display_field'] ? 0 : 1;
+ }
}
- if ($field['type'] == 'file') {
+ // We depend on the managed file element to handle uploads.
+ $return = media_file_value($element, $input, $form_state);
+
+ // Ensure that all the required properties are returned even if empty.
+ $return += array(
+ 'fid' => 0,
+ 'display' => 1,
+ 'description' => '',
+ );
+
+ return $return;
+}
+
+/**
+ * An element #process callback for the file_generic field type.
+ *
+ * Expands the file_generic type to include the description and display fields.
+ */
+function media_field_widget_process($element, &$form_state, $form) {
+ $item = $element['#value'];
+ $item['fid'] = $element['fid']['#value'];
+
+ $field = field_widget_field($element, $form_state);
+ $instance = field_widget_instance($element, $form_state);
+ $settings = $instance['widget']['settings'];
+
+ $element['#theme'] = 'media_widget';
+
+ // Add the display field if enabled.
+ if (!empty($field['settings']['display_field']) && $item['fid']) {
+ $element['display'] = array(
+ '#type' => empty($item['fid']) ? 'hidden' : 'checkbox',
+ '#title' => t('Include file in display'),
+ '#value' => isset($item['display']) ? $item['display'] : $field['settings']['display_default'],
+ '#attributes' => array('class' => array('file-display')),
+ );
+ }
+ else {
$element['display'] = array(
- '#type' => 'value',
- '#value' => 1,
+ '#type' => 'hidden',
+ '#value' => '1',
);
}
- // Add image field specific validators.
- if ($field['type'] == 'image') {
- if ($field_settings['min_resolution'] || $field_settings['max_resolution']) {
- $element['#media_options']['global']['min_resolution'] = $field_settings['min_resolution'];
- $element['#media_options']['global']['max_resolution'] = $field_settings['max_resolution'];
+ // Add the description field if enabled.
+ if (!empty($instance['settings']['description_field']) && $item['fid']) {
+ $element['description'] = array(
+ '#type' => variable_get('file_description_type', 'textfield'),
+ '#title' => t('Description'),
+ '#value' => isset($item['description']) ? $item['description'] : '',
+ '#maxlength' => variable_get('file_description_length', 128),
+ '#description' => t('The description may be used as the label of the link to the file.'),
+ );
+ }
+
+ // Adjust the Ajax settings so that on upload and remove of any individual
+ // file, the entire group of file fields is updated together.
+ if ($field['cardinality'] != 1) {
+ $parents = array_slice($element['#array_parents'], 0, -1);
+ $new_path = 'media/ajax/' . implode('/', $parents) . '/' . $form['form_build_id']['#value'];
+ $field_element = drupal_array_get_nested_value($form, $parents);
+ $new_wrapper = $field_element['#id'] . '-ajax-wrapper';
+ foreach (element_children($element) as $key) {
+ if (isset($element[$key]['#ajax'])) {
+ $element[$key]['#ajax']['path'] = $new_path;
+ $element[$key]['#ajax']['wrapper'] = $new_wrapper;
+ }
}
+ unset($element['#prefix'], $element['#suffix']);
+ }
+
+ // Add another submit handler to the upload and remove buttons, to implement
+ // functionality needed by the field widget. This submit handler, along with
+ // the rebuild logic in media_field_widget_form() requires the entire field,
+ // not just the individual item, to be valid.
+ foreach (array('attach_button', 'edit_button', 'remove_button') as $key) {
+ $element[$key]['#submit'][] = 'media_field_widget_submit';
+ $element[$key]['#limit_validation_errors'] = array(array_slice($element['#parents'], 0, -1));
}
return $element;
}
/**
- * Widget value.
+ * An element #process callback for a group of file_generic fields.
*
- * @todo Is this function ever called? If not, remove it. The Field API now
- * supports automatic serialization / unserialization, so this should no
- * longer be needed. After verifying with a module that uses the 'data'
- * column, remove this.
+ * Adds the weight field to each row so it can be ordered and adds a new Ajax
+ * wrapper around the entire group so it can be replaced all at once.
+ */
+function media_field_widget_process_multiple($element, &$form_state, $form) {
+ $element_children = element_children($element, TRUE);
+ $count = count($element_children);
+
+ foreach ($element_children as $delta => $key) {
+ if ($key != $element['#file_upload_delta']) {
+ $description = _media_field_get_description_from_element($element[$key]);
+ $element[$key]['_weight'] = array(
+ '#type' => 'weight',
+ '#title' => $description ? t('Weight for @title', array('@title' => $description)) : t('Weight for new file'),
+ '#title_display' => 'invisible',
+ '#delta' => $count,
+ '#default_value' => $delta,
+ );
+ }
+ else {
+ // The title needs to be assigned to the upload field so that validation
+ // errors include the correct widget label.
+ $element[$key]['#title'] = $element['#title'];
+ $element[$key]['_weight'] = array(
+ '#type' => 'hidden',
+ '#default_value' => $delta,
+ );
+ }
+ }
+
+ // Add a new wrapper around all the elements for Ajax replacement.
+ $element['#prefix'] = '
';
+ $element['#suffix'] = '
';
+
+ return $element;
+}
+
+/**
+ * Retrieves the file description from a field field element.
+ *
+ * This helper function is used by media_field_widget_process_multiple().
+ *
+ * @param $element
+ * The element being processed.
+ *
+ * @return
+ * A description of the file suitable for use in the administrative interface.
+ */
+function _media_field_get_description_from_element($element) {
+ // Use the actual file description, if it's available.
+ if (!empty($element['#default_value']['description'])) {
+ return $element['#default_value']['description'];
+ }
+ // Otherwise, fall back to the filename.
+ if (!empty($element['#default_value']['filename'])) {
+ return $element['#default_value']['filename'];
+ }
+ // This is probably a newly uploaded file; no description is available.
+ return FALSE;
+}
+
+/**
+ * Form submission handler for upload/remove button of media_field_widget_form().
+ *
+ * This runs in addition to and after media_field_widget_submit().
*
+ * @see media_field_widget_submit()
* @see media_field_widget_form()
+ * @see media_field_widget_process()
*/
-function media_field_widget_value($element, $input, $form_state) {
- $return = $input;
+function media_field_widget_submit($form, &$form_state) {
+ // During the form rebuild, media_field_widget_form() will create field item
+ // widget elements using re-indexed deltas, so clear out $form_state['input']
+ // to avoid a mismatch between old and new deltas. The rebuilt elements will
+ // have #default_value set appropriately for the current state of the field,
+ // so nothing is lost in doing this.
+ $parents = array_slice($form_state['triggering_element']['#parents'], 0, -2);
+ drupal_array_set_nested_value($form_state['input'], $parents, NULL);
+
+ $button = $form_state['triggering_element'];
- if (!is_array($return)) {
- $return = array();
+ // Go one level up in the form, to the widgets container.
+ $element = drupal_array_get_nested_value($form, array_slice($button['#array_parents'], 0, -1));
+ $field_name = $element['#field_name'];
+ $langcode = $element['#language'];
+ $parents = $element['#field_parents'];
+
+ $submitted_values = drupal_array_get_nested_value($form_state['values'], array_slice($button['#array_parents'], 0, -2));
+ foreach ($submitted_values as $delta => $submitted_value) {
+ if (!$submitted_value['fid']) {
+ unset($submitted_values[$delta]);
+ }
}
- if (isset($return['data'])) {
- $return['data'] = serialize($return['data']);
+ // Re-index deltas after removing empty items.
+ $submitted_values = array_values($submitted_values);
+
+ // Update form_state values.
+ drupal_array_set_nested_value($form_state['values'], array_slice($button['#array_parents'], 0, -2), $submitted_values);
+
+ // Update items.
+ $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state);
+ $field_state['items'] = $submitted_values;
+ field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state);
+}
+
+/**
+ * Returns HTML for an individual file upload widget.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - element: A render element representing the widget.
+ *
+ * @ingroup themeable
+ */
+function theme_media_widget($variables) {
+ $element = $variables['element'];
+ $output = '';
+
+ // The "form-media" class is required for proper Ajax functionality.
+ $output .= '';
+ $output .= drupal_render_children($element);
+ $output .= '
';
+
+ return $output;
+}
+
+/**
+ * Returns HTML for a group of file upload widgets.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - element: A render element representing the widgets.
+ *
+ * @ingroup themeable
+ */
+function theme_media_widget_multiple($variables) {
+ $element = $variables['element'];
+
+ // Special ID and classes for draggable tables.
+ $weight_class = $element['#id'] . '-weight';
+ $table_id = $element['#id'] . '-table';
+
+ // Build up a table of applicable fields.
+ $headers = array();
+ $headers[] = t('File information');
+ if ($element['#display_field']) {
+ $headers[] = array(
+ 'data' => t('Display'),
+ 'class' => array('checkbox'),
+ );
}
+ $headers[] = t('Weight');
+ $headers[] = t('Operations');
- $return += array(
- 'fid' => 0,
- 'title' => '',
- 'data' => NULL,
- );
+ // Get our list of widgets in order (needed when the form comes back after
+ // preview or failed validation).
+ $widgets = array();
+ foreach (element_children($element) as $key) {
+ $widgets[] = &$element[$key];
+ }
+ usort($widgets, '_field_sort_items_value_helper');
- return $return;
+ $rows = array();
+ foreach ($widgets as $key => &$widget) {
+ // Save the uploading row for last.
+ if ($widget['#file'] == FALSE) {
+ $widget['#title'] = $element['#file_upload_title'];
+ continue;
+ }
+
+ // Delay rendering of the buttons, so that they can be rendered later in the
+ // "operations" column.
+ $operations_elements = array();
+ foreach (element_children($widget) as $sub_key) {
+ if (isset($widget[$sub_key]['#type']) && $widget[$sub_key]['#type'] == 'submit') {
+ hide($widget[$sub_key]);
+ $operations_elements[] = &$widget[$sub_key];
+ }
+ }
+
+ // Delay rendering of the "Display" option and the weight selector, so that
+ // each can be rendered later in its own column.
+ if ($element['#display_field']) {
+ hide($widget['display']);
+ }
+ hide($widget['_weight']);
+
+ // Render everything else together in a column, without the normal wrappers.
+ $widget['#theme_wrappers'] = array();
+ $information = drupal_render($widget);
+
+ // Render the previously hidden elements, using render() instead of
+ // drupal_render(), to undo the earlier hide().
+ $operations = '';
+ foreach ($operations_elements as $operation_element) {
+ $operations .= render($operation_element);
+ }
+ $display = '';
+ if ($element['#display_field']) {
+ unset($widget['display']['#title']);
+ $display = array(
+ 'data' => render($widget['display']),
+ 'class' => array('checkbox'),
+ );
+ }
+ $widget['_weight']['#attributes']['class'] = array($weight_class);
+ $weight = render($widget['_weight']);
+
+ // Arrange the row with all of the rendered columns.
+ $row = array();
+ $row[] = $information;
+ if ($element['#display_field']) {
+ $row[] = $display;
+ }
+ $row[] = $weight;
+ $row[] = $operations;
+ $rows[] = array(
+ 'data' => $row,
+ 'class' => isset($widget['#attributes']['class']) ? array_merge($widget['#attributes']['class'], array('draggable')) : array('draggable'),
+ );
+ }
+
+ drupal_add_tabledrag($table_id, 'order', 'sibling', $weight_class);
+
+ $output = '';
+ $output = empty($rows) ? '' : theme('table', array('header' => $headers, 'rows' => $rows, 'attributes' => array('id' => $table_id)));
+ $output .= drupal_render_children($element);
+ return $output;
}
diff --git a/includes/media.pages.inc b/includes/media.pages.inc
deleted file mode 100644
index b22a8b1..0000000
--- a/includes/media.pages.inc
+++ /dev/null
@@ -1,33 +0,0 @@
-';
+ $output .= drupal_render_children($element);
+ $output .= '';
+ return $output;
+}
+
+/**
* Add messages to the page.
*/
function template_preprocess_media_dialog_page(&$variables) {
diff --git a/js/media.js b/js/media.js
index 04585bd..33e1c5a 100644
--- a/js/media.js
+++ b/js/media.js
@@ -1,97 +1,64 @@
-
/**
- * @file
- * This file handles the JS for Media Module functions.
+ * @file
+ * Provides JavaScript additions to the media field widget.
+ *
+ * This file provides support for launching the media browser to select and
+ * attach existing files.
*/
(function ($) {
-/**
- * Loads media browsers and callbacks, specifically for media as a field.
- */
-Drupal.behaviors.mediaElement = {
- attach: function (context, settings) {
- // Options set from media.fields.inc for the types, etc to show in the browser.
-
- // For each widget (in case of multi-entry)
- $('.media-widget', context).once('mediaBrowserLaunch', function () {
- var options = settings.media.elements[this.id];
- globalOptions = {};
- if (options.global != undefined) {
- var globalOptions = options.global;
+ /**
+ * Attach behaviors to media element browse fields.
+ */
+ Drupal.behaviors.media = {
+ attach: function (context, settings) {
+ if (settings.media && settings.media.elements) {
+ $.each(settings.media.elements, function(selector) {
+ var configuration = settings.media.elements[selector];
+ $(selector, context).bind('click', {configuration: configuration}, Drupal.behaviors.media.openBrowser);
+ });
+ }
+ },
+ detach: function (context, settings) {
+ if (settings.media && settings.media.elements) {
+ $.each(settings.media.elements, function(selector) {
+ $(selector, context).unbind('click', Drupal.behaviors.media.openBrowser);
+ });
}
- //options = Drupal.settings.media.fields[this.id];
- var fidField = $('.fid', this);
- var previewField = $('.preview', this);
- var editButton = $('.edit', this);
- var removeButton = $('.remove', this);
+ },
+ openBrowser: function (event) {
+ var clickedButton = this;
+ var configuration = event.data.configuration.global;
+
+ // Find the file ID and preview fields.
+ var fidField = $(this).siblings('.fid');
+ var previewField = $(this).siblings('.preview');
- // Hide the edit and remove buttons if there is no file data yet.
- if (fidField.val() == 0) {
- if (editButton.length) {
- editButton.hide();
+ // Find the edit and remove buttons.
+ var editButton = $(this).siblings('.edit');
+ var removeButton = $(this).siblings('.remove');
+
+ // Launch the media browser.
+ Drupal.media.popups.mediaBrowser(function (mediaFiles) {
+ // Ensure that there was at least one media file selected.
+ if (mediaFiles.length < 0) {
+ return;
}
- removeButton.hide();
- }
- // When someone clicks the link to pick media (or clicks on an existing thumbnail)
- $('.launcher', this).bind('click', function (e) {
- // Launch the browser, providing the following callback function
- // @TODO: This should not be an anomyous function.
- Drupal.media.popups.mediaBrowser(function (mediaFiles) {
- if (mediaFiles.length < 0) {
- return;
- }
- var mediaFile = mediaFiles[0];
- // Set the value of the filefield fid (hidden) and trigger a change.
- fidField.val(mediaFile.fid);
- fidField.trigger('change');
- // Set the preview field HTML.
- previewField.html(mediaFile.preview);
- }, globalOptions);
- e.preventDefault();
- });
+ // Grab the first of the selected media files.
+ var mediaFile = mediaFiles[0];
- // When someone clicks the Remove button.
- $('.remove', this).bind('click', function (e) {
- // Set the value of the filefield fid (hidden) and trigger change.
- fidField.val(0);
+ // Set the value of the hidden file ID field and trigger a change.
+ fidField.val(mediaFile.fid);
fidField.trigger('change');
- // Set the preview field HTML.
- previewField.html('');
- e.preventDefault();
- });
- // Show or hide the edit/remove buttons if the field has a file or not.
- $('.fid', this).bind('change', function() {
- if (fidField.val() == 0) {
- if (editButton.length) {
- editButton.hide();
- }
- removeButton.hide();
- }
- else {
- if (editButton.length) {
- var url = Drupal.settings.basePath + 'file/' + fidField.val() + '/edit';
- $.ajax({
- url: location.protocol + '//' + location.host + url,
- type: 'HEAD',
- success: function(data) {
- editButton.attr('href', editButton.attr('href').replace(/media\/\d+\/edit/, 'media/' + fidField.val() + '/edit'));
- // Re-process the edit link through CTools modal behaviors.
- editButton.unbind('click');
- editButton.removeClass('ctools-use-modal-processed');
- // @todo Maybe add support for Drupal.detachBehaviors in Drupal.behaviors.ZZCToolsModal?
- Drupal.attachBehaviors(editButton.parent(), Drupal.settings);
- editButton.show();
- }
- });
- }
- removeButton.show();
- }
- });
- });
- }
-};
+ // Display a preview of the file using the selected media file's display.
+ previewField.html(mediaFile.preview);
+ }, configuration);
+
+ return false;
+ }
+ };
})(jQuery);
diff --git a/media.module b/media.module
index 197f62c..1db30de 100644
--- a/media.module
+++ b/media.module
@@ -103,6 +103,14 @@ function media_menu() {
'weight' => 10,
);
+ $items['media/ajax'] = array(
+ 'page callback' => 'media_ajax_upload',
+ 'delivery callback' => 'ajax_deliver',
+ 'access arguments' => array('access content'),
+ 'theme callback' => 'ajax_base_page_theme',
+ 'type' => MENU_CALLBACK,
+ );
+
$items['media/browser'] = array(
'title' => 'Media browser',
'description' => 'Media Browser for picking media and uploading new media',
@@ -125,18 +133,6 @@ function media_menu() {
'file' => 'includes/media.browser.inc',
);
- // We could re-use the file/%file/edit path for the modal callback, but
- // it is just easier to use our own namespace here.
- $items['media/%file/edit/%ctools_js'] = array(
- 'title' => 'Edit',
- 'page callback' => 'drupal_get_form',
- 'page arguments' => array('media_file_edit_modal', 1, 3),
- 'access callback' => 'file_entity_access',
- 'access arguments' => array('update', 1),
- 'file' => 'includes/media.pages.inc',
- 'type' => MENU_CALLBACK,
- );
-
return $items;
}
@@ -215,10 +211,84 @@ function media_theme() {
'variables' => array('file' => NULL, 'attributes' => array(), 'style_name' => 'media_thumbnail'),
'file' => 'includes/media.theme.inc',
),
+
+ // media.field.inc.
+ 'media_widget' => array(
+ 'render element' => 'element',
+ ),
+ 'media_widget_multiple' => array(
+ 'render element' => 'element',
+ ),
+ 'media_formatter_table' => array(
+ 'variables' => array('items' => NULL),
+ ),
);
}
/**
+ * Menu callback; Shared Ajax callback for file uploads and deletions.
+ *
+ * This rebuilds the form element for a particular field item. As long as the
+ * form processing is properly encapsulated in the widget element the form
+ * should rebuild correctly using FAPI without the need for additional callbacks
+ * or processing.
+ */
+function media_ajax_upload() {
+ $form_parents = func_get_args();
+ $form_build_id = (string) array_pop($form_parents);
+
+ if (empty($_POST['form_build_id']) || $form_build_id != $_POST['form_build_id']) {
+ // Invalid request.
+ drupal_set_message(t('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (@size) that this server supports.', array('@size' => format_size(file_upload_max_size()))), 'error');
+ $commands = array();
+ $commands[] = ajax_command_replace(NULL, theme('status_messages'));
+ return array('#type' => 'ajax', '#commands' => $commands);
+ }
+
+ list($form, $form_state) = ajax_get_form();
+
+ if (!$form) {
+ // Invalid form_build_id.
+ drupal_set_message(t('An unrecoverable error occurred. Use of this form has expired. Try reloading the page and submitting again.'), 'error');
+ $commands = array();
+ $commands[] = ajax_command_replace(NULL, theme('status_messages'));
+ return array('#type' => 'ajax', '#commands' => $commands);
+ }
+
+ // Get the current element and count the number of files.
+ $current_element = $form;
+ foreach ($form_parents as $parent) {
+ $current_element = $current_element[$parent];
+ }
+ $current_file_count = isset($current_element['#file_upload_delta']) ? $current_element['#file_upload_delta'] : 0;
+
+ // Process user input. $form and $form_state are modified in the process.
+ drupal_process_form($form['#form_id'], $form, $form_state);
+
+ // Retrieve the element to be rendered.
+ foreach ($form_parents as $parent) {
+ $form = $form[$parent];
+ }
+
+ // Add the special Ajax class if a new file was added.
+ if (isset($form['#file_upload_delta']) && $current_file_count < $form['#file_upload_delta']) {
+ $form[$current_file_count]['#attributes']['class'][] = 'ajax-new-content';
+ }
+ // Otherwise just add the new content class on a placeholder.
+ else {
+ $form['#suffix'] .= '';
+ }
+
+ $output = theme('status_messages') . drupal_render($form);
+ $js = drupal_add_js();
+ $settings = call_user_func_array('array_merge_recursive', $js['settings']['data']);
+
+ $commands = array();
+ $commands[] = ajax_command_replace(NULL, $output, $settings);
+ return array('#type' => 'ajax', '#commands' => $commands);
+}
+
+/**
* Implements hook_image_default_styles().
*/
function media_image_default_styles() {
@@ -259,38 +329,12 @@ function media_page_alter(&$page) {
}
/**
- * Implements hook_form_FIELD_UI_FIELD_SETTINGS_FORM_alter().
- *
- * @todo: Respect field settings in 7.x-2.x and handle them in the media widget
- * UI.
- */
-function media_form_field_ui_field_settings_form_alter(&$form, &$form_state) {
- // On file fields that use the media widget we need remove specific fields.
- if ($form['field']['type']['#value'] == 'file') {
- $fields = field_info_instances($form['#entity_type'], $form['#bundle']);
- if ($fields[$form['field']['field_name']['#value']]['widget']['type'] == 'media_generic') {
- $form['field']['settings']['display_field']['#access'] = FALSE;
- $form['field']['settings']['display_default']['#access'] = FALSE;
- }
- }
-}
-
-/**
* Implements hook_form_FIELD_UI_FIELD_EDIT_FORM_alter().
*
* @todo: Respect field settings in 7.x-2.x and handle them in the media widget
* UI.
*/
function media_form_field_ui_field_edit_form_alter(&$form, &$form_state) {
- // On file fields that use the media widget we need remove specific fields.
- if ($form['#field']['type'] == 'file' && $form['instance']['widget']['type']['#value'] == 'media_generic') {
- $form['field']['settings']['display_field']['#access'] = FALSE;
- $form['field']['settings']['display_default']['#access'] = FALSE;
- $form['instance']['settings']['description_field']['#access'] = FALSE;
- $form['instance']['settings']['file_extensions']['#title'] = t('Allowed file extensions for uploaded files');
- $form['instance']['settings']['file_extensions']['#maxlength'] = 255;
- }
-
// On image fields using the media widget we remove the alt/title fields.
if ($form['#field']['type'] == 'image' && $form['instance']['widget']['type']['#value'] == 'media_generic') {
$form['instance']['settings']['alt_field']['#access'] = FALSE;
@@ -310,23 +354,6 @@ function media_form_field_ui_field_edit_form_alter(&$form, &$form_state) {
}
/**
- * Implements hook_form_FORM_ID_alter().
- */
-function media_form_file_entity_edit_alter(&$form, &$form_state) {
- // Make adjustments to the file edit form when used in a CTools modal.
- if (!empty($form_state['ajax'])) {
- // Remove the preview and the delete button.
- $form['preview']['#access'] = FALSE;
- $form['actions']['delete']['#access'] = FALSE;
-
- // Convert the cancel link to a button which triggers a modal close.
- $form['actions']['cancel']['#attributes']['class'][] = 'button';
- $form['actions']['cancel']['#attributes']['class'][] = 'button-no';
- $form['actions']['cancel']['#attributes']['class'][] = 'ctools-close-modal';
- }
-}
-
-/**
* Implements hook_form_alter().
*/
function media_form_alter(&$form, &$form_state, $form_id) {
@@ -561,12 +588,12 @@ function media_element_info() {
$types['media'] = array(
'#input' => TRUE,
'#process' => array('media_element_process'),
- // '#value_callback' => 'media_element_value',
+ '#value_callback' => 'media_file_value',
'#element_validate' => array('media_element_validate'),
- '#theme_wrappers' => array('container'),
- '#progress_indicator' => 'throbber',
+ '#pre_render' => array('media_element_pre_render'),
+ '#theme' => 'media_element',
+ '#theme_wrappers' => array('form_element'),
'#extended' => FALSE,
- '#required' => FALSE,
'#media_options' => array(
'global' => array(
// Example: array('image', 'audio');
@@ -575,112 +602,192 @@ function media_element_info() {
'schemes' => array(),
),
),
- '#attributes' => array(
- 'class' => array('media-widget', 'form-item'),
- ),
'#attached' => array(
'library' => array(
array('media', 'media_browser'),
),
),
);
+
+ $setting = array();
+ $setting['media']['global'] = $types['media']['#media_options'];
+
+ $types['media']['#attached']['js'][] = array(
+ 'type' => 'setting',
+ 'data' => $setting,
+ );
+
return $types;
}
/**
* Process callback for the media form element.
*/
-function media_element_process(&$element, &$form_state, $form) {
- $fid = isset($element['#value']['fid']) ? $element['#value']['fid'] : 0;
- $file = $fid ? file_load($fid) : FALSE;
-
- // Add the CTools modal JavaScript for the edit button if necessary.
- ctools_include('modal');
+function media_element_process($element, &$form_state, $form) {
+ // Add the CTools modal JavaScript for the edit button.
ctools_include('ajax');
+ ctools_include('modal');
ctools_modal_add_js();
+ $fid = isset($element['#value']['fid']) ? $element['#value']['fid'] : 0;
+
// Set some default element properties.
- $element['#file'] = $file;
-
- $element['title'] = array(
- '#type' => 'item',
- '#title' => $element['#title'],
- '#description' => $element['#description'],
- '#required' => $element['#required'],
- '#weight' => -100,
+ $element['#file'] = $fid ? file_load($fid) : FALSE;
+ $element['#tree'] = TRUE;
+
+ $ajax_settings = array(
+ 'path' => 'media/ajax/' . implode('/', $element['#array_parents']) . '/' . $form['form_build_id']['#value'],
+ 'wrapper' => $element['#id'] . '-ajax-wrapper',
+ 'effect' => 'fade',
+ );
+
+ // Set up the buttons first since we need to check if they were clicked.
+ $element['attach_button'] = array(
+ '#name' => implode('_', $element['#parents']) . '_attach_button',
+ '#type' => 'submit',
+ '#value' => t('Attach'),
+ '#validate' => array(),
+ '#submit' => array('media_file_submit'),
+ '#limit_validation_errors' => array($element['#parents']),
+ '#ajax' => $ajax_settings,
+ '#attributes' => array('class' => array('button', 'attach')),
+ '#weight' => -5,
);
- if (isset($element['#title_display'])) {
- $element['title']['#title_display'] = $element['#title_display'];
- }
- // @todo This should load from the JS in case of a failed form submission.
$element['preview'] = array(
- '#prefix' => '',
+ 'content' => array(),
+ '#prefix' => '
',
'#suffix' => '
',
- '#weight' => 0,
- 'content' => $file ? media_get_thumbnail_preview($file) : array(),
+ '#ajax' => $ajax_settings,
+ '#weight' => -10,
);
- // @todo: Perhaps this language logic should be handled by JS since the state
- // changes on client side after choosing an item.
- $element['select'] = array(
+ // Substitute the JS preview for a true file thumbnail and disable browsing
+ // for files once the file is attached.
+ if ($element['#file']) {
+ $element['preview']['content'] = media_get_thumbnail_preview($element['#file']);
+ $element['preview']['#prefix'] = '';
+ $element['preview']['#suffix'] = '';
+ }
+
+ $element['browse_button'] = array(
'#type' => 'link',
'#href' => '',
- '#title' => t('Select'),
- '#attributes' => array('class' => array('button', 'launcher')),
+ '#title' => t('Browse'),
+ '#attributes' => array('class' => array('button', 'browse')),
'#options' => array('fragment' => FALSE, 'external' => TRUE),
- '#weight' => 10,
+ '#weight' => -9,
);
- // @todo Figure out how to update the preview image after the Edit modal is
- // closed.
- $element['edit'] = array(
- '#type' => 'link',
- '#href' => 'media/' . $fid . '/edit/nojs',
- '#title' => t('Edit'),
- '#attributes' => array(
- 'class' => array(
- // Required for CTools modal to work.
- 'ctools-use-modal', 'use-ajax',
- 'ctools-modal-media-file-edit', 'button', 'edit',
- ),
- ),
- '#weight' => 20,
- '#access' => $file ? file_entity_access('update', $file) : TRUE, // only do perm check for existing files
- );
- $element['remove'] = array(
- '#type' => 'link',
- '#href' => '',
- '#title' => t('Remove'),
+
+ // Force the progress indicator for the remove button to be either 'none' or
+ // 'throbber', even if the upload button is using something else.
+ $ajax_settings['progress']['type'] = 'throbber';
+ $ajax_settings['progress']['message'] = NULL;
+ $ajax_settings['effect'] = 'none';
+ $element['remove_button'] = array(
+ '#name' => implode('_', $element['#parents']) . '_remove_button',
+ '#type' => 'submit',
+ '#value' => t('Remove'),
+ '#validate' => array(),
+ '#submit' => array('media_file_submit'),
+ '#limit_validation_errors' => array($element['#parents']),
+ '#ajax' => $ajax_settings,
'#attributes' => array('class' => array('button', 'remove')),
- '#options' => array('fragment' => FALSE, 'external' => TRUE),
- '#weight' => 30,
+ '#weight' => -3,
+ '#access' => $element['#file'] ? file_entity_access('delete', $element['#file']) : TRUE, // only do perm check for existing files
);
$element['fid'] = array(
'#type' => 'hidden',
'#value' => $fid,
'#attributes' => array('class' => array('fid')),
- '#weight' => 100,
);
// Media browser attach code.
$element['#attached']['js'][] = drupal_get_path('module', 'media') . '/js/media.js';
- $setting = array();
- $setting['media']['elements'][$element['#id']] = $element['#media_options'];
-
- $element['#attached']['js'][] = array(
- 'type' => 'setting',
- 'data' => $setting,
+ // Add the media options to the page as JavaScript settings.
+ $element['browse_button']['#attached']['js'] = array(
+ array(
+ 'type' => 'setting',
+ 'data' => array('media' => array('elements' => array('#' . $element['#id'] . '-browse-button' => $element['#media_options'])))
+ )
);
- // @todo: Might need to think about this. All settings would likely apply to
- // all media in a multi-value, but what about passing the existing fid?
module_load_include('inc', 'media', 'includes/media.browser');
media_attach_browser_js($element);
+ // Prefix and suffix used for Ajax replacement.
+ $element['#prefix'] = '
';
+ $element['#suffix'] = '
';
+
return $element;
- // @todo: make this work for file and image fields.
+}
+
+/**
+ * The #value_callback for a media type element.
+ */
+function media_file_value(&$element, $input = FALSE, $form_state = NULL) {
+ $fid = 0;
+
+ // Find the current value of this field from the form state.
+ $form_state_fid = $form_state['values'];
+ foreach ($element['#parents'] as $parent) {
+ $form_state_fid = isset($form_state_fid[$parent]) ? $form_state_fid[$parent] : 0;
+ }
+
+ if ($element['#extended'] && isset($form_state_fid['fid'])) {
+ $fid = $form_state_fid['fid'];
+ }
+ elseif (is_numeric($form_state_fid)) {
+ $fid = $form_state_fid;
+ }
+
+ // Process any input and save new uploads.
+ if ($input !== FALSE) {
+ $return = $input;
+
+ // Uploads take priority over all other values.
+ if ($file = file_managed_file_save_upload($element)) {
+ $fid = $file->fid;
+ }
+ else {
+ // Check for #filefield_value_callback values.
+ // Because FAPI does not allow multiple #value_callback values like it
+ // does for #element_validate and #process, this fills the missing
+ // functionality to allow File fields to be extended through FAPI.
+ if (isset($element['#file_value_callbacks'])) {
+ foreach ($element['#file_value_callbacks'] as $callback) {
+ $callback($element, $input, $form_state);
+ }
+ }
+ // Load file if the FID has changed to confirm it exists.
+ if (isset($input['fid']) && $file = file_load($input['fid'])) {
+ $fid = $file->fid;
+ }
+ }
+ }
+
+ // If there is no input, set the default value.
+ else {
+ if ($element['#extended']) {
+ $default_fid = isset($element['#default_value']['fid']) ? $element['#default_value']['fid'] : 0;
+ $return = isset($element['#default_value']) ? $element['#default_value'] : array('fid' => 0);
+ }
+ else {
+ $default_fid = isset($element['#default_value']) ? $element['#default_value'] : 0;
+ $return = array('fid' => 0);
+ }
+
+ // Confirm that the file exists when used as a default value.
+ if ($default_fid && $file = file_load($default_fid)) {
+ $fid = $file->fid;
+ }
+ }
+
+ $return['fid'] = $fid;
+
+ return $return;
}
/**
@@ -690,20 +797,96 @@ function media_element_process(&$element, &$form_state, $form) {
* necessary in order to respect the #required property.
*/
function media_element_validate(&$element, &$form_state) {
- if ($element['#required']) {
- $has_value = FALSE;
- $widget_parents = $element['#array_parents'];
- array_pop($widget_parents);
- $items = drupal_array_get_nested_value($form_state['values'], $widget_parents);
- foreach ($items as $value) {
- if (is_array($value) && !empty($value['fid'])) {
- $has_value = TRUE;
- }
- }
- if (!$has_value) {
- form_error($element, t('%element_title field is required.', array('%element_title' => $element['#title'])));
+ $clicked_button = end($form_state['triggering_element']['#parents']);
+
+ // Check required property based on the FID.
+ if ($element['#required'] && empty($element['fid']['#value']) && !in_array($clicked_button, array('attach_button', 'edit_button', 'remove_button'))) {
+ form_error($element['browse_button'], t('!name field is required.', array('!name' => $element['#title'])));
+ }
+
+ // Consolidate the array value of this field to a single FID.
+ if (!$element['#extended']) {
+ form_set_value($element, $element['fid']['#value'], $form_state);
+ }
+}
+
+/**
+ * Form submission handler for attach / edit / remove buttons of media elements.
+ *
+ * @see media_element_process()
+ */
+function media_file_submit($form, &$form_state) {
+ // Determine whether it was the attach, edit or remove button that was
+ // clicked, and set $element to the managed_file element that contains that
+ // button.
+ $parents = $form_state['triggering_element']['#array_parents'];
+ $button_key = array_pop($parents);
+ $element = drupal_array_get_nested_value($form, $parents);
+
+ // No action is needed here for the attach button, because all file uploads on
+ // the form are processed by media_file_value() regardless of which button was
+ // clicked. Action is needed here for the remove button, because we only
+ // remove a file in response to its remove button being clicked.
+ if ($button_key == 'remove_button') {
+ // If it's a temporary file we can safely remove it immediately, otherwise
+ // it's up to the implementing module to clean up files that are in use.
+ if ($element['#file'] && $element['#file']->status == 0) {
+ file_delete($element['#file']);
}
+ // Update both $form_state['values'] and $form_state['input'] to reflect
+ // that the file has been removed, so that the form is rebuilt correctly.
+ // $form_state['values'] must be updated in case additional submit handlers
+ // run, and for form building functions that run during the rebuild, such as
+ // when the media element is part of a field widget.
+ // $form_state['input'] must be updated so that media_file_value() has
+ // correct information during the rebuild.
+ $values_element = $element['#extended'] ? $element['fid'] : $element;
+ form_set_value($values_element, NULL, $form_state);
+ drupal_array_set_nested_value($form_state['input'], $values_element['#parents'], NULL);
}
+
+ // Set the form to rebuild so that $form is correctly updated in response to
+ // processing the file removal. Since this function did not change $form_state
+ // if the upload button was clicked, a rebuild isn't necessary in that
+ // situation and setting $form_state['redirect'] to FALSE would suffice.
+ // However, we choose to always rebuild, to keep the form processing workflow
+ // consistent between the two buttons.
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * #pre_render callback to hide display of the browse/attach or edit/remove
+ * controls.
+ *
+ * Browse/attach controls are hidden when a file is already uploaded.
+ * Edit/Remove controls are hidden when there is no file attached. Controls are
+ * hidden here instead of in media_element_process(), because #access for these
+ * buttons depends on the media element's #value. See the documentation of
+ * form_builder() for more detailed information about the relationship between
+ * #process, #value, and #access.
+ *
+ * Because #access is set here, it affects display only and does not prevent
+ * JavaScript or other untrusted code from submitting the form as though access
+ * were enabled. The form processing functions for these elements should not
+ * assume that the buttons can't be "clicked" just because they are not
+ * displayed.
+ *
+ * @see media_element_process()
+ * @see form_builder()
+ */
+function media_element_pre_render($element) {
+ // If we already have a file, we don't want to show the browse and attach
+ // controls.
+ if (!empty($element['#value']['fid'])) {
+ $element['browse_button']['#access'] = FALSE;
+ $element['attach_button']['#access'] = FALSE;
+ }
+ // If we don't already have a file, there is nothing to edit or remove.
+ else {
+ $element['edit_button']['#access'] = FALSE;
+ $element['remove_button']['#access'] = FALSE;
+ }
+ return $element;
}
/**