diff --git a/includes/media.filter.inc b/includes/media.filter.inc index 64b72a3..d5f0d87 100644 --- a/includes/media.filter.inc +++ b/includes/media.filter.inc @@ -17,7 +17,6 @@ function media_wysiwyg_include_directory($type) { switch ($type) { case 'plugins': return 'wysiwyg_plugins'; - break; } } @@ -313,12 +312,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; @@ -379,6 +377,7 @@ function media_token_to_markup($match, $wysiwyg = FALSE) { // Display the field elements. $element = array(); $element['content']['file'] = media_get_file_without_label($file, $tag_info['view_mode'], $settings); + // Overwrite or set the file #alt attribute if it has been set in this // instance. if (!empty($element['content']['file']['#attributes']['alt'])) { @@ -392,6 +391,7 @@ function media_token_to_markup($match, $wysiwyg = FALSE) { field_attach_prepare_view('file', array($file->fid => $file), $tag_info['view_mode']); entity_prepare_view('file', array($file->fid => $file)); $element['content'] += field_attach_view('file', $file, $tag_info['view_mode']); + if (count(element_children($element['content'])) > 1) { // Add surrounding divs to group them together. // We dont want divs when there are no additional fields to allow files @@ -404,6 +404,7 @@ function media_token_to_markup($match, $wysiwyg = FALSE) { ); } } + drupal_alter('media_token_to_markup', $element, $tag_info, $settings); return drupal_render($element); } @@ -668,18 +669,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..74c013c 100644 --- a/js/media.filter.js +++ b/js/media.filter.js @@ -14,36 +14,40 @@ * @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(/\[\[.*?\]\]/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. + } + + if (!media) { + // Create the element if it has changed from the tagmap. + media = document.createElement(media_definition.tagName); + media.src = media_definition.src; } - i++; + + // Apply attributes. + var element = Drupal.media.filter.createElement(media, media_definition); + var markup = Drupal.media.filter.safeMarkup(element); + + content = content.replace(match, markup); } } + return content; }, @@ -52,29 +56,25 @@ * @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); - } - i++; - } - return content; - }, + Drupal.settings.tagmap = []; - getWrapperStart: function(i) { - return ''; - }, + // Convert all xhtml markup to html for reliable matching/replacing. + content = content.replace(/[\s]\/\>/g, '>'); - getWrapperEnd: function(i) { - return ''; + // 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.safeMarkup($(element)); + macro = Drupal.media.filter.createMacro($(element)); + + // 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); + }); + + return content; }, /** @@ -87,32 +87,53 @@ * @return The registered element. */ registerNewElement: function (formattedMedia, fid) { - var element = Drupal.media.filter.create_element(formattedMedia.html, { + var element = Drupal.media.filter.createElement(formattedMedia.html, { fid: fid, view_mode: formattedMedia.type, attributes: formattedMedia.options }); - var markup = Drupal.media.filter.outerHTML(element), - macro = Drupal.media.filter.create_macro(element); + var markup = Drupal.media.filter.safeMarkup(element), + macro = Drupal.media.filter.createMacro(element); // Store macro/markup pair in the tagmap. 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. + * + * @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. * * @param html (string) * A html element to be used to represent the inserted media element. * @param info (object) * A object containing the media file information (fid, view_mode, etc). */ - create_element: function (html, info) { + createElement: 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. @@ -138,7 +159,7 @@ // Adding 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(' ')); @@ -152,8 +173,8 @@ * @param element (jQuery object) * A media element with attached serialized file info. */ - create_macro: function (element) { - var file_info = Drupal.media.filter.extract_file_info(element); + createMacro: function (element) { + var file_info = Drupal.media.filter.extractFileInfo(element); if (file_info) { return '[[' + JSON.stringify(file_info) + ']]'; } @@ -166,7 +187,7 @@ * @param element (jQuery object) * A media element with attached serialized file info. */ - extract_file_info: function (element) { + extractFileInfo: function (element) { var file_json = $.data(element, 'file_info') || element.data('file_info'), file_info, value; @@ -188,6 +209,10 @@ } }); delete(file_info.attributes['data-file_info']); + + // Set tagName and src to rebuild later. + file_info.tagName = element[0].tagName; + file_info.src = element[0].src; } return file_info; @@ -199,7 +224,28 @@ * @param element (jQuery object) */ outerHTML: function (element) { - return $('
').append(element.eq(0).clone()).html(); + return element[0].outerHTML || $('
').append(element.eq(0).clone()).html(); + }, + + /** + * Parses an element's markup and returns it with its attributes in + * alphabetical order. Use as a wrapper when outerHTML is needed. + * + * @param element (jQuery object) The element to modify. + * + * @see http://dev.ckeditor.com/ticket/1810 + */ + safeMarkup: function(element) { + var markup = Drupal.media.filter.outerHTML(element); + + // Parse out and sort the element's attributes. + var attrs = markup.match(/([^\s]+?=".*?")/gi).sort(); + + // Create the media element markup with the sorted attributes. + var media = document.createElement($(markup)[0].tagName); + media = Drupal.media.filter.outerHTML($(media)).replace(/\>/, ' ' + attrs.join(' ') + '>'); + + return media; }, /** @@ -214,8 +260,8 @@ */ getWysiwygHTML: function (element) { // Create the markup and the macro. - var markup = Drupal.media.filter.outerHTML(element), - macro = Drupal.media.filter.create_macro(element); + var markup = Drupal.media.filter.safeMarkup(element), + macro = Drupal.media.filter.createMacro(element); // Store macro/markup in the tagmap. Drupal.media.filter.ensure_tagmap(); @@ -227,7 +273,7 @@ // 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); + return markup; }, /** @@ -244,7 +290,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..44c6508 100644 --- a/js/wysiwyg-media.js +++ b/js/wysiwyg-media.js @@ -42,7 +42,7 @@ Drupal.wysiwyg.plugins.media = { var insert = new InsertMedia(instanceId); if (this.isNode(data.node)) { // Change the view mode for already-inserted media. - var media_file = Drupal.media.filter.extract_file_info($(data.node)); + var media_file = Drupal.media.filter.extractFileInfo($(data.node)); insert.onSelect([media_file]); } else { @@ -109,12 +109,15 @@ InsertMedia.prototype = { * tagmap. */ insert: function (formatted_media) { - var element = Drupal.media.filter.create_element(formatted_media.html, { + var attributes = Drupal.media.filter.parseAttributeFields(formatted_media.options); + + var element = Drupal.media.filter.createElement(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); @@ -143,8 +146,8 @@ function ensure_tagmap () { * * @deprecated */ -function create_element (html, info) { - return Drupal.media.filter.create_element(html, info); +function createElement (html, info) { + return Drupal.media.filter.createElement(html, info); } /** @@ -155,8 +158,8 @@ function create_element (html, info) { * * @deprecated */ -function create_macro (element) { - return Drupal.media.filter.create_macro(element); +function createMacro (element) { + return Drupal.media.filter.createMacro(element); } /** @@ -167,8 +170,8 @@ function create_macro (element) { * * @deprecated */ -function extract_file_info (element) { - return Drupal.media.filter.extract_file_info(element); +function extractFileInfo (element) { + return Drupal.media.filter.extractFileInfo(element); } /** diff --git a/media.module b/media.module index 7435518..18058f5 100644 --- a/media.module +++ b/media.module @@ -1300,3 +1300,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', + ); +} 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'); }