diff --git a/includes/media.filter.inc b/includes/media.filter.inc index f2f1973..bde9533 100644 --- a/includes/media.filter.inc +++ b/includes/media.filter.inc @@ -403,6 +403,7 @@ function media_token_to_markup($match, $wysiwyg = FALSE) { ); } } + drupal_alter('media_token_to_markup', $element, $tag_info, $settings); return drupal_render($element); } @@ -676,9 +677,8 @@ function media_get_file_without_label($file, $view_mode, $settings = array()) { // use-case, and image theme functions have their own structure for render // arrays. if (isset($element['#theme'])) { - switch ($element['#theme']) { - case 'image': - case 'image_style': + switch (TRUE) { + case (in_array($element['#theme'], array('image', '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 @@ -690,7 +690,9 @@ function media_get_file_without_label($file, $view_mode, $settings = array()) { } break; - case 'image_formatter': + // theme_image_formatter() and any potential replacements, such as + // theme_colorbox_image_formatter(), also require attribute handling. + case (FALSE !== strpos($element['#theme'], 'image_formatter')): // theme_image_formatter() requires the attributes to be // set on the item rather than the element itself. if (empty($element['#item']['attributes'])) { diff --git a/js/media.filter.js b/js/media.filter.js index 47c9e77..cef3212 100644 --- a/js/media.filter.js +++ b/js/media.filter.js @@ -15,34 +15,44 @@ */ replaceTokenWithPlaceholder: function(content) { Drupal.media.filter.ensure_tagmap() - var tagmap = Drupal.settings.tagmap, - matches = content.match(/\[\[.*?\]\]/g), - media_definition; - - if (matches) { - for (var macro in tagmap) { - // We cant use indexOf because of IE. - 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); + var matches = content.match(/\[\[.*?"type":"media".*?\]\]/g); - content = content.replace(macro, markup); - } + 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; + } + + // 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,53 +62,63 @@ */ replacePlaceholderWithToken: function(content) { Drupal.media.filter.ensure_tagmap(); - // Convert all xhtml markup to html for reliable matching/replacing. - content = content.replace(/[\s]\/\>/g, '>'); + var markup = document.createElement('div'); + markup.innerHTML = content; - // Re-build the macros in case any element has changed in the editor. - $('.media-element', content).each(function(i, element) { - var markup = Drupal.media.filter.outerHTML($(element)); - macro = Drupal.media.filter.create_macro($(element)); + 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] = markup; - // Replace the media element with its macro. - content = content.replace(markup, macro); - }); + Drupal.settings.tagmap[macro] = matches[i]; - return content; + placeholders[i] = { + match: matches[i], + node: document.createTextNode(macro) + } + } + + // 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); + } + + 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. * * @param html (string) * A html element to be used to represent the inserted media element. @@ -107,20 +127,33 @@ */ 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 link wrappers. They will be re-applied when the image is + // rendered on the front-end. + if (element.is('a')) { + 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) { + $.each(Drupal.settings.media.wysiwyg_allowed_attributes, function(i, a) { if (info.attributes[a]) { element.attr(a, info.attributes[a]); } }); 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 @@ -128,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(' ')); @@ -174,8 +207,23 @@ if (file_info) { file_info.attributes = {}; + // Set the height and width in the style attribute or they will be + // overridden. + // @see https://api.drupal.org/api/drupal/modules!image!image.module/function/image_style_transform_dimensions/7 + $.each(['height', 'width'], function(i, a) { + if (!parseInt(element[0].style[a])) { + if (!!element.attr(a)) { + element.css(a, element.attr(a)); + } + else if (!!element[a]()) { + // Internet Explorer has its own needs. + element.css(a, element[a]()); + } + } + }); + // Extract whitelisted attributes. - $.each(Drupal.media.filter.allowed_attributes(), function(i, a) { + $.each(Drupal.settings.media.wysiwyg_allowed_attributes, function(i, a) { if (value = element.attr(a)) { file_info.attributes[a] = value; } @@ -224,20 +272,20 @@ }, /** - * Ensures the tag map has been initialized and returns it. + * Ensures the src tracking has been initialized and returns it. */ - ensure_tagmap: function () { - Drupal.settings.tagmap = Drupal.settings.tagmap || {}; - return Drupal.settings.tagmap; + ensureSourceMap: function() { + Drupal.settings.mediaSourceMap = Drupal.settings.mediaSourceMap || {}; + return Drupal.settings.mediaSourceMap; }, /** - * Ensures the wysiwyg_allowed_attributes and returns it. - * In case of an error the default settings are returned. + * Ensures the tag map has been initialized and returns it. */ - 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; + ensure_tagmap: function () { + Drupal.settings.tagmap = Drupal.settings.tagmap || {}; + return Drupal.settings.tagmap; } } + })(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.module b/media.module index bfd7471..36b1e3f 100644 --- a/media.module +++ b/media.module @@ -955,6 +955,23 @@ function media_element_validate(&$element, &$form_state) { form_error($element, t('%element_title field is required.', array('%element_title' => $element['#title']))); } } + // 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); + } } /** @@ -1323,4 +1340,4 @@ function _media_wysiwyg_allowed_attributes_default() { 'data-picture-group', 'data-picture-align', ); -} +} \ No newline at end of file diff --git a/tests/media.macro.test b/tests/media.macro.test index e65dca2..62ad0a6 100644 --- a/tests/media.macro.test +++ b/tests/media.macro.test @@ -48,7 +48,8 @@ class MediaWysiwygOverridesTest extends MediaTestHelper { $nid = $this->createNode($file->fid); $this->drupalGet('node/' . $nid); - $this->assertRaw('height="100" width="100"', t('Image displays with default attributes.')); + $this->assertRaw('width="100"', t('Image displays with default width attribute.')); + $this->assertRaw('height="100"', t('Image displays with default height attribute.')); // Create a node with a style attribute. $attributes = array( diff --git a/tests/media.test b/tests/media.test index c9a7a60..bffa46b 100644 --- a/tests/media.test +++ b/tests/media.test @@ -41,8 +41,8 @@ class MediaTestHelper extends DrupalWebTestCase { */ protected function generateJsonTokenMarkup($fid, $count = 1, array $attributes = array(), array $fields = array()) { $markup = ''; - // Merge default atttributes. - $attributes += array( + + $default_attributes = array( 'height' => 100, 'width' => 100, 'classes' => 'media-element file_preview', @@ -53,7 +53,7 @@ class MediaTestHelper extends DrupalWebTestCase { 'fid' => $fid, 'type' => 'media', 'view_mode' => 'preview', - 'attributes' => $attributes, + 'attributes' => array_merge($default_attributes, $attributes), 'fields' => $fields, );