diff --git a/includes/media.filter.inc b/includes/media.filter.inc index 1936232..8e55bac 100644 --- a/includes/media.filter.inc +++ b/includes/media.filter.inc @@ -313,12 +313,11 @@ function media_token_to_markup($match, $wysiwyg = FALSE) { $fields = media_filter_field_parser($tag_info); $attributes = is_array($tag_info['attributes']) ? $tag_info['attributes'] : array(); - $attribute_whitelist = variable_get('media__wysiwyg_allowed_attributes', array('height', 'width', 'hspace', 'vspace', 'border', 'align', 'style', 'class', 'id', 'usemap', 'data-picture-group', 'data-picture-align')); + $attribute_whitelist = variable_get('media__wysiwyg_allowed_attributes', _media_wysiwyg_allowed_attributes_default()); $settings['attributes'] = array_intersect_key($attributes, array_flip($attribute_whitelist)); $settings['fields'] = $fields; if (!empty($tag_info['attributes']) && is_array($tag_info['attributes'])) { - $attribute_whitelist = variable_get('media__wysiwyg_allowed_attributes', array('height', 'width', 'hspace', 'vspace', 'border', 'align', 'style', 'class', 'id', 'usemap', 'data-picture-group', 'data-picture-align')); $settings['attributes'] = array_intersect_key($tag_info['attributes'], array_flip($attribute_whitelist)); $settings['fields'] = $fields; @@ -668,18 +667,46 @@ function media_get_file_without_label($file, $view_mode, $settings = array()) { // support simple formatters that don't do this, set the element attributes to // what was requested, but not if the formatter applied its own logic for // element attributes. - if (!isset($element['#attributes']) && isset($settings['attributes'])) { - $element['#attributes'] = $settings['attributes']; + if (isset($settings['attributes'])) { + if (empty($element['#attributes'])) { + $element['#attributes'] = $settings['attributes']; + } // While this function may be called for any file type, images are a common - // use-case. theme_image() and theme_image_style() require the 'alt' - // attribute to be passed separately from the 'attributes' array (see - // http://drupal.org/node/999338). Until that's fixed, implement this - // special-case logic. Image formatters using other theme functions are - // responsible for their own 'alt' attribute handling. See - // theme_media_formatter_large_icon() for an example. - if (isset($settings['attributes']['alt']) && !isset($element['#alt']) && isset($element['#theme']) && in_array($element['#theme'], array('image', 'image_style'))) { - $element['#alt'] = $settings['attributes']['alt']; + // use-case, and image theme functions have their own structures for + // render arrays. + if (isset($element['#theme'])) { + switch ($element['#theme']) { + case 'image': + case 'image_style': + // theme_image() and theme_image_style() require the 'alt' attributes to + // be passed separately from the 'attributes' array. (see + // http://drupal.org/node/999338). Until that's fixed, implement this + // special-case logic. Image formatters using other theme functions are + // responsible for their own 'alt' attribute handling. See + // theme_media_formatter_large_icon() for an example. + if (empty($element['#alt']) && isset($settings['attributes']['alt'])) { + $element['#alt'] = $settings['attributes']['alt']; + } + break; + + case 'image_formatter': + // theme_image_formatter() requires the attributes to be + // set on the item rather than the element itself. + if (empty($element['#item']['attributes'])) { + $element['#item']['attributes'] = $settings['attributes']; + } + + // theme_image_formatter() also requires alt, title, height, and + // width attributes to be set on the item rather than within its + // attributes array. + foreach (array('alt', 'title', 'width', 'height') as $attr) { + if (isset($settings['attributes'][$attr])) { + $element['#item'][$attr] = $settings['attributes'][$attr]; + } + } + break; + } } } diff --git a/js/media.filter.js b/js/media.filter.js index c1b39c9..7a0d787 100644 --- a/js/media.filter.js +++ b/js/media.filter.js @@ -14,36 +14,41 @@ * @param content */ replaceTokenWithPlaceholder: function(content) { - var tagmap = Drupal.media.filter.ensure_tagmap(), - matches = content.match(/\[\[.*?\]\]/g), - media_definition, - id = 0; - - if (matches) { - var i = 1; - for (var macro in tagmap) { - var index = matches.indexOf(macro); - if (index !== -1) { - var media_json = macro.replace('[[', '').replace(']]', ''); - - // Make sure that the media JSON is valid. - try { - media_definition = JSON.parse(media_json); - } - catch (err) { - media_definition = null; - } - if (media_definition) { - // Apply attributes. - var element = Drupal.media.filter.create_element(tagmap[macro], media_definition); - var markup = Drupal.media.filter.outerHTML(element); - - content = content.replace(macro, Drupal.media.filter.getWrapperStart(i) + markup + Drupal.media.filter.getWrapperEnd(i)); - } + var matches = content.match(/\[\[.*?"type":"media".*?\]\]/g); + + if (!!matches) { + for (i in matches) { + var match = matches[i]; + + // Check if the macro exists in the tagmap. This ensures backwards + // compatibility with existing media and is moderately more efficient + // than re-building the element. + var media = Drupal.settings.tagmap[match]; + var media_json = match.replace('[[', '').replace(']]', ''); + + // Ensure that the media JSON is valid. + try { + var media_definition = JSON.parse(media_json); } - i++; + catch (err) { + // @todo: error logging. + } + + // Only re-build the media if the macro has changed since the tagmap + // was last built. + if (!media) { + media = document.createElement(media_definition.tagName); + media.src = unescape(media_definition.src); + } + + // Apply attributes. + var element = Drupal.media.filter.create_element(media, media_definition); + var markup = Drupal.media.filter.outerHTML(element); + + content = content.replace(match, markup); } } + return content; }, @@ -52,29 +57,36 @@ * @param content */ replacePlaceholderWithToken: function(content) { - var tagmap = Drupal.media.filter.ensure_tagmap(); - var i = 1; - for (var macro in tagmap) { - var startTag = Drupal.media.filter.getWrapperStart(i), endTag = Drupal.media.filter.getWrapperEnd(i); - var startPos = content.indexOf(startTag), endPos = content.indexOf(endTag); - if (startPos !== -1 && endPos !== -1) { - // If the placeholder wrappers are empty, remove the macro too. - if (endPos - startPos - startTag.length === 0) { - macro = ''; - } - content = content.substr(0, startPos) + macro + content.substr(endPos + (new String(endTag)).length); + var markup = document.createElement('div'); + markup.innerHTML = content; + + var matches = (!!markup) ? markup.getElementsByClassName('media-element') : []; + var replacements = []; + + // Rewrite the tagmap in case any of the macros have changed. + Drupal.settings.tagmap = {}; + + for (var i = 0; i < matches.length; i++) { + var el = $(matches[i]); + var macro = Drupal.media.filter.create_macro(el); + + // Store the macro => html for more efficient rendering in + // replaceTokenWithPlaceholder(). + Drupal.settings.tagmap[macro] = Drupal.media.filter.outerHTML(el); + + replacements[i] = { + match: matches[i], + node: document.createTextNode(macro) } - i++; } - return content; - }, - getWrapperStart: function(i) { - return ''; - }, + // We have to loop through the replacements separately because + // replaceChild will shift off the replacement from the NodeList. + for (i in replacements) { + replacements[i].match.parentNode.replaceChild(replacements[i].node, replacements[i].match); + } - getWrapperEnd: function(i) { - return ''; + return markup.innerHTML; }, /** @@ -100,10 +112,35 @@ Drupal.media.filter.ensure_tagmap(); Drupal.settings.tagmap[macro] = markup; - return element; + return markup; }, /** + * Returns alt and title field values for use as html attributes. Ensures + * changes made via the media popup persist into the macro as title/alt + * attributes. + * + * @param options (array) Options passed through a popup form submission. + */ + parseAttributeFields: function(options) { + var attributes = []; + + for (field in options) { + if (field.match('image_alt')) { + attributes['alt'] = options[field]; + } + + if (field.match('image_title')) { + attributes['title'] = options[field]; + } + } + + return attributes; + }, + + /** + * Serializes file information as a url-encoded JSON object and stores it + * as a data attribute on the html element. * Serializes file information as a url-encoded JSON object and stores it as a * data attribute on the html element. * @@ -128,6 +165,13 @@ } }); delete(info.attributes); + + // Set tagName to rebuild element later. + info.tagName = element[0].tagName; + + // Escape the src to prevent editor auto-link url freature from + // interfering with our macros. + info.src = escape(element[0].src); } // Important to url-encode the file information as it is being stored in an @@ -226,8 +270,8 @@ Drupal.settings.tagmap[macro] = markup; // Return the wrapped html code to insert in an editor and use it with - // replacePlaceholderWithToken() - return Drupal.media.filter.getWrapperStart(i) + markup + Drupal.media.filter.getWrapperEnd(i); + // replacePlaceholderWithToken(). + return markup; }, /** @@ -244,7 +288,7 @@ */ allowed_attributes: function () { Drupal.settings.wysiwyg_allowed_attributes = Drupal.settings.wysiwyg_allowed_attributes || ['height', 'width', 'hspace', 'vspace', 'border', 'align', 'style', 'alt', 'title', 'class', 'id', 'usemap']; - return Drupal.settings.wysiwyg_allowed_attributes; + return Drupal.settings.wysiwyg_allowed_attributes.sort(); } } })(jQuery); diff --git a/js/wysiwyg-media.js b/js/wysiwyg-media.js index ac5bf4d..2adc4ab 100644 --- a/js/wysiwyg-media.js +++ b/js/wysiwyg-media.js @@ -109,10 +109,12 @@ InsertMedia.prototype = { * tagmap. */ insert: function (formatted_media) { + var attributes = Drupal.media.filter.parseAttributeFields(formatted_media.options); + var element = Drupal.media.filter.create_element(formatted_media.html, { fid: this.mediaFile.fid, view_mode: formatted_media.type, - attributes: formatted_media.options, + attributes: $.extend(this.mediaFile.attributes, attributes), fields: formatted_media.options }); // Get the markup and register it for the macro / placeholder handling. diff --git a/media.info b/media.info index 2675a77..d7fa046 100644 --- a/media.info +++ b/media.info @@ -6,8 +6,7 @@ core = 7.x dependencies[] = file_entity dependencies[] = image dependencies[] = views - -test_dependencies[] = token +dependencies[] = token files[] = includes/MediaReadOnlyStreamWrapper.inc files[] = includes/MediaBrowserPluginInterface.inc @@ -20,5 +19,6 @@ files[] = includes/media_views_plugin_style_media_browser.inc files[] = tests/media.test files[] = tests/media.entity.test files[] = tests/media.file.usage.test +files[] = tests/media.macro.test configure = admin/config/media/browser diff --git a/media.module b/media.module index 12dd4e0..01eac45 100644 --- a/media.module +++ b/media.module @@ -1068,6 +1068,23 @@ function media_file_displays_alter(&$displays, $file, $view_mode) { $file->{$field_name} = $value;} } } + // Alt and title are special. + // @see file_entity_file_load + $alt = variable_get('file_entity_alt', '[file:field_file_image_alt_text]'); + $title = variable_get('file_entity_title', '[file:field_file_image_title_text]'); + + $replace_options = array( + 'clear' => TRUE, + 'sanitize' => FALSE, + ); + + // Load alt and title text from fields. + if (!empty($alt)) { + $file->alt = token_replace($alt, array('file' => $file), $replace_options); + } + if (!empty($title)) { + $file->title = token_replace($title, array('file' => $file), $replace_options); + } } /** @@ -1300,3 +1317,27 @@ function _media_get_migratable_file_types() { return array_diff($types, $enabled_types); } + +/** + * Returns the default set of allowed attributes for use with WYSIWYG. + * + * @return Array of whitelisted attributes. + */ +function _media_wysiwyg_allowed_attributes_default() { + return array( + 'alt', + 'title', + 'height', + 'width', + 'hspace', + 'vspace', + 'border', + 'align', + 'style', + 'class', + 'id', + 'usemap', + 'data-picture-group', + 'data-picture-align', + ); +} \ No newline at end of file diff --git a/tests/media.file.usage.test b/tests/media.file.usage.test index db5c614..39b2ea6 100644 --- a/tests/media.file.usage.test +++ b/tests/media.file.usage.test @@ -22,7 +22,7 @@ class MediaFileUsageTest extends MediaTestHelper { * Enable media and file entity modules for testing. */ public function setUp() { - parent::setUp(array('media', 'file_entity')); + parent::setUp(); // Create and log in a user. $account = $this->drupalCreateUser(array('administer nodes', 'create article content')); @@ -30,72 +30,6 @@ class MediaFileUsageTest extends MediaTestHelper { } /** - * Generates markup to be inserted for a file. - * - * This is a PHP version of InsertMedia.insert() from js/wysiwyg-media.js. - * - * @param int $fid - * Drupal file id - * @param int $count - * Quantity of markup to insert - * - * @return string - * Filter markup. - */ - private function generateFileMarkup($fid, $count = 1) { - $file_usage_markup = ''; - - // Build the data that is used in a media tag. - $data = array( - 'fid' => $fid, - 'type' => 'media', - 'view_mode' => 'preview', - 'attributes' => array( - 'height' => 100, - 'width' => 100, - 'classes' => 'media-element file_preview', - ) - ); - - // Create the file usage markup. - for ($i = 1; $i <= $count; $i++) { - $file_usage_markup .= '

[[' . drupal_json_encode($data) . ']]

'; - } - - return $file_usage_markup; - } - - - /** - * Utility function to create a test node. - * - * @param int $fid - * Create the node with media markup in the body field - * - * @return int - * Returns the node id - */ - private function createNode($fid = FALSE) { - $markup = ''; - if (! empty($fid)) { - $markup = $this->generateFileMarkup($fid); - } - - // Create an article node with file markup in the body field. - $edit = array( - 'title' => $this->randomName(8), - 'body[und][0][value]' => $markup, - ); - // Save the article node. First argument is the URL, then the value array - // and the third is the label the button that should be "clicked". - $this->drupalPost('node/add/article', $edit, t('Save')); - - // Get the article node that was saved by the unique title. - $node = $this->drupalGetNodeByTitle($edit['title']); - return $node->nid; - } - - /** * Tests the tracking of file usages for files submitted via the WYSIWYG editor. */ public function testFileUsageIncrementing() { @@ -133,7 +67,7 @@ class MediaFileUsageTest extends MediaTestHelper { // Create a new revision that has two instances of the file. File usage will // be 4. $node = node_load($nid); - $node->body[LANGUAGE_NONE][0]['value'] = $this->generateFileMarkup($fid, 2); + $node->body[LANGUAGE_NONE][0]['value'] = $this->generateJsonTokenMarkup($fid, 2); $node->revision = TRUE; node_save($node); @@ -163,7 +97,7 @@ class MediaFileUsageTest extends MediaTestHelper { // Create a new revision that has the file on it. File usage will be 5. $node = node_load($nid); - $node->body[LANGUAGE_NONE][0]['value'] = $this->generateFileMarkup($fid, 1); + $node->body[LANGUAGE_NONE][0]['value'] = $this->generateJsonTokenMarkup($fid, 1); $node->revision = TRUE; node_save($node); @@ -194,7 +128,7 @@ class MediaFileUsageTest extends MediaTestHelper { // Create a new revision with the file on it twice. File usage will be 4. $node = node_load($nid); - $node->body[LANGUAGE_NONE][0]['value'] = $this->generateFileMarkup($fid, 2); + $node->body[LANGUAGE_NONE][0]['value'] = $this->generateJsonTokenMarkup($fid, 2); $node->revision = TRUE; node_save($node); @@ -206,7 +140,7 @@ class MediaFileUsageTest extends MediaTestHelper { // Re-save current revision with file on it once instead of twice. File // usage will be 3. $node = node_load($nid); - $node->body[LANGUAGE_NONE][0]['value'] = $this->generateFileMarkup($fid, 1); + $node->body[LANGUAGE_NONE][0]['value'] = $this->generateJsonTokenMarkup($fid, 1); $saved_vid = $node->vid; node_save($node); diff --git a/tests/media.macro.test b/tests/media.macro.test new file mode 100644 index 0000000..39f81f0 --- /dev/null +++ b/tests/media.macro.test @@ -0,0 +1,97 @@ + t('Media macro'), + 'description' => t('Tests that media macros display correctly.'), + 'group' => t('Media'), + ); + } + + public function setUp() { + parent::setUp(); + + // Create and log in a user. + $account = $this->drupalCreateUser(array('administer nodes', 'create article content', 'administer filters', 'use text format filtered_html')); + $this->drupalLogin($account); + + // Enable the media filter for full html. + $edit = array( + 'filters[media_filter][status]' => TRUE, + 'filters[filter_html][status]' => FALSE, + ); + $this->drupalPost('admin/config/content/formats/filtered_html', $edit, t('Save configuration')); + } + + /** + * Test image media overrides. + */ + public function testAttributeOverrides() { + $files = $this->drupalGetTestFiles('image'); + $file = file_save($files[0]); + + // Create a node to test with. + $nid = $this->createNode($file->fid); + + $this->drupalGet('node/' . $nid); + $this->assertRaw('height="100" width="100"', t('Image displays with default attributes.')); + + // Create a node with overriden attributes. + $attributes = array( + 'style' => 'float: left; width: 50px;', + ); + $nid = $this->createNode($file->fid, $attributes); + $this->drupalGet('node/' . $nid); + $this->assertRaw('style="float: left; width: 50px;"', t('Image displays with inline attributes.')); + + // Create a node with overriden alt/title. + $attributes = array( + 'alt' => $this->randomName(), + 'title' => $this->randomName(), + ); + $nid = $this->createNode($file->fid, $attributes); + $this->drupalGet('node/' . $nid); + $this->assertRaw(drupal_attributes($attributes), t('Image displays with alt/title set as attributes.')); + + // Create a node with overriden alt/title fields. + $fields = $attributes = array(); + $attributes['alt'] = $fields['field_file_image_alt_text[und][0][value]'] = $this->randomName(); + $attributes['title'] = $fields['field_file_image_title_text[und][0][value]'] = $this->randomName(); + + $this->drupalGet('file/' . $file->fid . '/edit'); + $this->drupalGet('file/' . $file->fid); + $this->drupalGet('admin/config/media/file-settings'); + $nid = $this->createNode($file->fid, array(), $fields); + $this->drupalGet('node/' . $nid); + // Ensure that the alt/title from attributes display rather the field ones. + $this->assertRaw(drupal_attributes($attributes), t('Image displays with alt/title set as fields.')); + + // Create a node with overriden alt/title fields as well as attributes. + $attributes = array( + 'alt' => $this->randomName(), + 'title' => $this->randomName(), + ); + $fields = array( + 'field_file_image_alt_text[und][0][value]' => $this->randomName(), + 'field_file_image_title_text[und][0][value]' => $this->randomName(), + ); + $nid = $this->createNode($file->fid, $attributes, $fields); + $this->drupalGet('node/' . $nid); + // Ensure that the alt/title from attributes display rather the field ones. + $this->assertRaw(drupal_attributes($attributes), t('Image displays with alt/title set as attributes overriding field values.')); + } + +} \ No newline at end of file diff --git a/tests/media.test b/tests/media.test index aaf6dcd..828f592 100644 --- a/tests/media.test +++ b/tests/media.test @@ -14,6 +14,83 @@ class MediaTestHelper extends DrupalWebTestCase { * Enable media and file entity modules for testing. */ public function setUp() { - parent::setUp(array('media', 'file_entity')); + parent::setUp(array('token', 'media', 'file_entity')); } + + /** + * Generates markup to be inserted for a file. + * + * This is a PHP version of InsertMedia.insert() from js/wysiwyg-media.js. + * + * @param int $fid + * Drupal file id + * @param int $count + * Quantity of markup to insert + * @param array $attributes + * Extra attributes to insert. + * @param array $fields + * Extra field values to insert. + * + * @return string + * Filter markup. + */ + protected function generateJsonTokenMarkup($fid, $count = 1, array $attributes = array(), array $fields = array()) { + $markup = ''; + // Merge default atttributes. + $attributes += array( + 'height' => 100, + 'width' => 100, + 'classes' => 'media-element file_preview', + ); + + // Build the data that is used in a media tag. + $data = array( + 'fid' => $fid, + 'type' => 'media', + 'view_mode' => 'preview', + 'attributes' => $attributes, + 'fields' => $fields, + ); + + // Create the file usage markup. + for ($i = 1; $i <= $count; $i++) { + $markup .= '

[[' . drupal_json_encode($data) . ']]

'; + } + + return $markup; + } + + /** + * Utility function to create a test node. + * + * @param int $fid + * Create the node with media markup in the body field + * @param array $attributes + * Extra attributes to insert to the file. + * @param array $fields + * Extra field values to insert. + * + * @return int + * Returns the node id + */ + protected function createNode($fid = FALSE, array $attributes = array(), array $fields = array()) { + $markup = ''; + if (! empty($fid)) { + $markup = $this->generateJsonTokenMarkup($fid, 1, $attributes, $fields); + } + + // Create an article node with file markup in the body field. + $edit = array( + 'title' => $this->randomName(8), + 'body[und][0][value]' => $markup, + ); + // Save the article node. First argument is the URL, then the value array + // and the third is the label the button that should be "clicked". + $this->drupalPost('node/add/article', $edit, t('Save')); + + // Get the article node that was saved by the unique title. + $node = $this->drupalGetNodeByTitle($edit['title']); + return $node->nid; + } + } diff --git a/wysiwyg_plugins/media.inc b/wysiwyg_plugins/media.inc index d90b79f..611ee60 100644 --- a/wysiwyg_plugins/media.inc +++ b/wysiwyg_plugins/media.inc @@ -68,7 +68,7 @@ function media_include_browser_js() { } } // Add wysiwyg-specific settings. - $settings = array('wysiwyg_allowed_attributes' => variable_get('media__wysiwyg_allowed_attributes', array('height', 'width', 'hspace', 'vspace', 'border', 'align', 'style', 'class', 'id', 'usemap', 'data-picture-group', 'data-picture-align'))); + $settings = array('wysiwyg_allowed_attributes' => variable_get('media__wysiwyg_allowed_attributes', _media_wysiwyg_allowed_attributes_default())); drupal_add_js(array('media' => $settings), 'setting'); }