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 29936fd..311ccf8 100644 --- a/js/media.filter.js +++ b/js/media.filter.js @@ -14,36 +14,44 @@ * @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 = $.inArray(macro, matches); - 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); + } + catch (err) { + // @todo: error logging. + return; + } + + Drupal.media.filter.ensureSourceMap(); + + // Re-build the media if the macro has changed from the tagmap. + if (!media && media_definition.fid) { + var source = Drupal.settings.mediaSourceMap[media_definition.fid]; + media = document.createElement(source.tagName); + media.src = source.src; } - i++; + + // 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,58 +60,63 @@ * @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 placeholders = []; + + // Rewrite the tagmap in case any of the macros have changed. + Drupal.settings.tagmap = {}; + + for (var i = 0; i < matches.length; i++) { + var macro = Drupal.media.filter.create_macro($(matches[i])); + + // Store the macro => html for more efficient rendering in + // replaceTokenWithPlaceholder(). + Drupal.settings.tagmap[macro] = matches[i]; + + placeholders[i] = { + match: matches[i], + node: document.createTextNode(macro) } - i++; } - return content; - }, - getWrapperStart: function(i) { - return ''; - }, + // We have to loop through the placeholders separately because + // replaceChild will shift off the replacement from the NodeList. + for (i in placeholders) { + placeholders[i].match.parentNode.replaceChild(placeholders[i].node, placeholders[i].match); + } - getWrapperEnd: function(i) { - return ''; + return markup.innerHTML; }, /** - * Register new element and returns the placeholder markup. - * - * @param formattedMedia a formatted media object as given by the onSubmit - * function of the media Style popup. - * @param fid the file id. + * 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. * - * @return The registered element. + * @param options (array) Options passed through a popup form submission. */ - registerNewElement: function (formattedMedia, fid) { - var element = Drupal.media.filter.create_element(formattedMedia.html, { - fid: fid, - view_mode: formattedMedia.type, - attributes: formattedMedia.options - }); + parseAttributeFields: function(options) { + var attributes = []; - var markup = Drupal.media.filter.outerHTML(element), - macro = Drupal.media.filter.create_macro(element); + for (field in options) { + if (field.match('image_alt')) { + attributes['alt'] = options[field]; + } - // Store macro/markup pair in the tagmap. - Drupal.media.filter.ensure_tagmap(); - Drupal.settings.tagmap[macro] = markup; + if (field.match('image_title')) { + attributes['title'] = options[field]; + } + } - return element; + 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. * @@ -114,12 +127,18 @@ */ create_element: function (html, info) { if ($('
').append(html).text().length === html.length) { - // Element is not an html tag. Surround it in a span element - // so we can pass the file attributes. + // Element is not an html tag. Surround it in a span element so we can + // pass the file attributes. html = '' + html + ''; } var element = $(html); + // Parse out colorbox wrappers. They will be re-applied when the image is + // rendered on the front-end. + if (element.is('a.colorbox')) { + element = element.children(); + } + // Move attributes from the file info array to the placeholder element. if (info.attributes) { $.each(Drupal.media.filter.allowed_attributes(), function(i, a) { @@ -128,6 +147,13 @@ } }); delete(info.attributes); + + // Store information to rebuild the element later, if necessary. + Drupal.media.filter.ensureSourceMap(); + Drupal.settings.mediaSourceMap[info.fid] = { + tagName: element[0].tagName, + src: element[0].src + } } // Important to url-encode the file information as it is being stored in an @@ -135,10 +161,10 @@ info.type = info.type || "media"; element.attr('data-file_info', encodeURI(JSON.stringify(info))); - // Adding media-element class so we can find markup element later. + // Add media-element class so we can find markup element later. var classes = ['media-element']; - if(info.view_mode){ + if (info.view_mode) { classes.push('file-' + info.view_mode.replace(/_/g, '-')); } element.addClass(classes.join(' ')); @@ -226,8 +252,16 @@ 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; + }, + + /** + * Ensures the src tracking has been initialized and returns it. + */ + ensureSourceMap: function() { + Drupal.settings.mediaSourceMap = Drupal.settings.mediaSourceMap || {}; + return Drupal.settings.mediaSourceMap; }, /** @@ -244,7 +278,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..6cc2519 100644 --- a/js/wysiwyg-media.js +++ b/js/wysiwyg-media.js @@ -109,12 +109,15 @@ 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. var markup = Drupal.media.filter.getWysiwygHTML(element); 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 87d0a20..6215122 100644 --- a/media.module +++ b/media.module @@ -1049,6 +1049,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); + } } /** @@ -1281,3 +1298,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'); }