root / drupal7 / sites / all / modules / media / modules / media_wysiwyg / js / media_wysiwyg.filter.js @ 05237dd8
1 |
/**
|
---|---|
2 |
* @file
|
3 |
* File with utilities to handle media in html editing.
|
4 |
*/
|
5 |
(function ($) { |
6 |
|
7 |
Drupal.media = Drupal.media || {}; |
8 |
/**
|
9 |
* Utility to deal with media tokens / placeholders.
|
10 |
*/
|
11 |
Drupal.media.filter = { |
12 |
/**
|
13 |
* Replaces media tokens with the placeholders for html editing.
|
14 |
* @param content
|
15 |
*/
|
16 |
replaceTokenWithPlaceholder: function(content) { |
17 |
Drupal.media.filter.ensure_tagmap(); |
18 |
var matches = content.match(/\[\[.*?\]\]/g); |
19 |
|
20 |
if (matches) {
|
21 |
for (var i = 0; i < matches.length; i++) { |
22 |
var match = matches[i];
|
23 |
if (match.indexOf('"type":"media"') == -1) { |
24 |
continue;
|
25 |
} |
26 |
|
27 |
// Check if the macro exists in the tagmap. This ensures backwards
|
28 |
// compatibility with existing media and is moderately more efficient
|
29 |
// than re-building the element.
|
30 |
var media = Drupal.settings.tagmap[match];
|
31 |
var media_json = match.replace('[[', '').replace(']]', ''); |
32 |
|
33 |
// Ensure that the media JSON is valid.
|
34 |
try {
|
35 |
var media_definition = JSON.parse(media_json);
|
36 |
} |
37 |
catch (err) {
|
38 |
// @todo: error logging.
|
39 |
// Content should be returned to prevent an empty editor.
|
40 |
return content;
|
41 |
} |
42 |
|
43 |
// Re-build the media if the macro has changed from the tagmap.
|
44 |
if (!media && media_definition.fid) {
|
45 |
Drupal.media.filter.ensureSourceMap(); |
46 |
var source;
|
47 |
if (source = Drupal.settings.mediaSourceMap[media_definition.fid]) {
|
48 |
media = document.createElement(source.tagName); |
49 |
media.src = source.src; |
50 |
media.innerHTML = source.innerHTML; |
51 |
} |
52 |
else {
|
53 |
// If the media element can't be found, leave it in to be resolved
|
54 |
// by the user later.
|
55 |
continue;
|
56 |
} |
57 |
} |
58 |
|
59 |
// Apply attributes.
|
60 |
var element = Drupal.media.filter.create_element(media, media_definition);
|
61 |
var markup = Drupal.media.filter.outerHTML(element);
|
62 |
|
63 |
// Use split and join to replace all instances of macro with markup.
|
64 |
content = content.split(match).join(markup); |
65 |
} |
66 |
} |
67 |
|
68 |
return content;
|
69 |
}, |
70 |
|
71 |
/**
|
72 |
* Returns alt and title field attribute data from the corresponding fields.
|
73 |
*
|
74 |
* Specifically looks for file_entity module's file_image_alt_text and
|
75 |
* file_image_title_text fields as those are by default used to store
|
76 |
* override values for image alt and title attributes.
|
77 |
*
|
78 |
* @param options (array)
|
79 |
* Options passed through a popup form submission.
|
80 |
* @param includeFieldID (bool)
|
81 |
* If set, the returned object will have extra keys with the IDs of the
|
82 |
* found fields.
|
83 |
*
|
84 |
* If the alt or title fields were not found, their keys will be excluded
|
85 |
* from the returned array.
|
86 |
*
|
87 |
* @return
|
88 |
* An object with the following keys:
|
89 |
* - alt: The value of the alt field.
|
90 |
* - altField: The id of the alt field.
|
91 |
* - title: The value of the title field.
|
92 |
* - titleField: The id of the title field.
|
93 |
*/
|
94 |
parseAttributeFields: function(options, includeFieldID) { |
95 |
var attributes = {};
|
96 |
|
97 |
for (var field in options) { |
98 |
// If the field is set to false, use an empty string for output.
|
99 |
options[field] = options[field] === false ? '' : options[field]; |
100 |
//if (field.match(/^field_file_image_alt_text/)) {
|
101 |
if (field.match(new RegExp('^' + Drupal.settings.media.img_alt_field))) { |
102 |
attributes.alt = options[field]; |
103 |
if (includeFieldID) {
|
104 |
attributes.altField = field; |
105 |
} |
106 |
} |
107 |
|
108 |
//if (field.match(/^field_file_image_title_text/)) {
|
109 |
if (field.match(new RegExp('^' + Drupal.settings.media.img_title_field))) { |
110 |
attributes.title = options[field]; |
111 |
if (includeFieldID) {
|
112 |
attributes.titleField = field; |
113 |
} |
114 |
} |
115 |
} |
116 |
|
117 |
return attributes;
|
118 |
}, |
119 |
|
120 |
/**
|
121 |
* Ensures changes made to fielded attributes are done on the fields too.
|
122 |
*
|
123 |
* This should be called when creating a macro tag from a placeholder.
|
124 |
*
|
125 |
* Changed made to attributes represented by fields are synced back to the
|
126 |
* corresponding fields, if they exist. The alt/title attribute
|
127 |
* values encoded in the macro will override the alt/title field values (set
|
128 |
* in the Media dialog) during rendering of both WYSIWYG placeholders and
|
129 |
* the final file entity on the server. Syncing makes changes applied to a
|
130 |
* placeholder's alt/title attribute using native WYSIWYG tools visible in
|
131 |
* the fields shown in the Media dialog.
|
132 |
*
|
133 |
* The reverse should be done when creating a placeholder from a macro tag
|
134 |
* so changes made in the Media dialog are reflected in the placeholder's
|
135 |
* alt and title attributes or the values there become stale and the change
|
136 |
* appears uneffective.
|
137 |
*
|
138 |
* @param file_info (object)
|
139 |
* A JSON decoded object of the file being inserted/updated.
|
140 |
*/
|
141 |
syncAttributesToFields: function(file_info) { |
142 |
if (!file_info) {
|
143 |
file_info = {}; |
144 |
} |
145 |
if (!file_info.attributes) {
|
146 |
file_info.attributes = {}; |
147 |
} |
148 |
if (!file_info.fields) {
|
149 |
file_info.fields = {}; |
150 |
} |
151 |
var fields = Drupal.media.filter.parseAttributeFields(file_info.fields, true); |
152 |
|
153 |
// If the title attribute has changed, ensure the title field is updated.
|
154 |
var titleAttr = file_info.attributes.title || false; |
155 |
if (fields.titleField && (titleAttr !== fields.title)) {
|
156 |
file_info.fields[fields.titleField] = titleAttr; |
157 |
} |
158 |
|
159 |
// If the alt attribute has changed, ensure the alt field is updated.
|
160 |
var altAttr = file_info.attributes.alt || false; |
161 |
if (fields.altField && (altAttr !== fields.alt)) {
|
162 |
file_info.fields[fields.altField] = altAttr; |
163 |
} |
164 |
|
165 |
return file_info;
|
166 |
}, |
167 |
|
168 |
/**
|
169 |
* Replaces media elements with tokens.
|
170 |
*
|
171 |
* @param content (string)
|
172 |
* The markup within the wysiwyg instance.
|
173 |
*/
|
174 |
replacePlaceholderWithToken: function(content) { |
175 |
Drupal.media.filter.ensure_tagmap(); |
176 |
|
177 |
// Locate and process all the media placeholders in the WYSIWYG content.
|
178 |
var contentElements = $('<div/>'); // TODO: once baseline jQuery is 1.8+, switch to using $.parseHTML(content) |
179 |
contentElements.get(0).innerHTML = content;
|
180 |
var mediaElements = contentElements.find('.media-element'); |
181 |
if (mediaElements) {
|
182 |
$(mediaElements).each(function (i) { |
183 |
// Attempt to derive a JSON macro representation of the media placeholder.
|
184 |
// Note: Drupal 7 ships with JQuery 1.4.4, which allows $(this).attr('outerHTML') to retrieve the eement's HTML,
|
185 |
// but many sites use JQuery update to increate this to 1.6+, which insists on $(this).prop('outerHTML).
|
186 |
// Until the minimum jQuery is >= 1.6, we need to do this the old-school way.
|
187 |
// See http://stackoverflow.com/questions/2419749/get-selected-elements-outer-html
|
188 |
var markup = $(this).get(0).outerHTML; |
189 |
if (markup === undefined) { |
190 |
// Browser does not support outerHTML DOM property. Use the more expensive clone method instead.
|
191 |
markup = $(this).clone().wrap('<div>').parent().html(); |
192 |
} |
193 |
var macro = Drupal.media.filter.create_macro($(markup)); |
194 |
if (macro) {
|
195 |
// Replace the placeholder with the macro in the parsed content.
|
196 |
// (Can't just replace the string section, because the outerHTML may be subtly different,
|
197 |
// depending on the browser. Parsing tends to convert <img/> to <img>, for instance.)
|
198 |
Drupal.settings.tagmap[macro] = markup; |
199 |
$(this).replaceWith(macro); |
200 |
} |
201 |
}); |
202 |
content = $(contentElements).html();
|
203 |
} |
204 |
|
205 |
return content;
|
206 |
}, |
207 |
|
208 |
/**
|
209 |
* Serializes file information as a url-encoded JSON object and stores it
|
210 |
* as a data attribute on the html element.
|
211 |
*
|
212 |
* @param html (string)
|
213 |
* A html element to be used to represent the inserted media element.
|
214 |
* @param info (object)
|
215 |
* A object containing the media file information (fid, view_mode, etc).
|
216 |
*/
|
217 |
create_element: function (html, info) { |
218 |
if ($('<div>').append(html).text().length === html.length) { |
219 |
// Element is not an html tag. Surround it in a span element so we can
|
220 |
// pass the file attributes.
|
221 |
html = '<span>' + html + '</span>'; |
222 |
} |
223 |
var element = $(html); |
224 |
|
225 |
// Parse out link wrappers. They will be re-applied when the image is
|
226 |
// rendered on the front-end.
|
227 |
if (element.is('a') && element.find('img').length) { |
228 |
element = element.children(); |
229 |
} |
230 |
|
231 |
// Extract attributes represented by fields and use those values to keep
|
232 |
// them in sync, usually alt and title.
|
233 |
var attributes = Drupal.media.filter.parseAttributeFields(info.fields);
|
234 |
info.attributes = $.extend(info.attributes, attributes);
|
235 |
|
236 |
// Move attributes from the file info array to the placeholder element.
|
237 |
if (info.attributes) {
|
238 |
$.each(Drupal.settings.media.wysiwyg_allowed_attributes, function(i, a) { |
239 |
if (info.attributes[a]) {
|
240 |
element.attr(a, info.attributes[a]); |
241 |
} |
242 |
else if (element.attr(a)) { |
243 |
// If the element has the attribute, but the value is empty, be
|
244 |
// sure to clear it.
|
245 |
element.removeAttr(a); |
246 |
} |
247 |
}); |
248 |
delete(info.attributes);
|
249 |
|
250 |
// Store information to rebuild the element later, if necessary.
|
251 |
Drupal.media.filter.ensureSourceMap(); |
252 |
Drupal.settings.mediaSourceMap[info.fid] = { |
253 |
tagName: element[0].tagName, |
254 |
src: element[0].src, |
255 |
innerHTML: element[0].innerHTML |
256 |
} |
257 |
} |
258 |
|
259 |
info.type = info.type || "media";
|
260 |
|
261 |
// Store the data in the data map.
|
262 |
Drupal.media.filter.ensureDataMap(); |
263 |
|
264 |
// Generate a "delta" to allow for multiple embeddings of the same file.
|
265 |
var delta = Drupal.media.filter.fileEmbedDelta(info.fid, element);
|
266 |
if (Drupal.settings.mediaDataMap[info.fid]) {
|
267 |
info.field_deltas = Drupal.settings.mediaDataMap[info.fid].field_deltas || {}; |
268 |
} |
269 |
else {
|
270 |
info.field_deltas = {}; |
271 |
} |
272 |
info.field_deltas[delta] = info.fields; |
273 |
element.attr('data-delta', delta);
|
274 |
|
275 |
Drupal.settings.mediaDataMap[info.fid] = info; |
276 |
|
277 |
// Store the fid in the DOM to retrieve the data from the info map.
|
278 |
element.attr('data-fid', info.fid);
|
279 |
|
280 |
// Add data-media-element attribute so we can find the markup element later.
|
281 |
element.attr('data-media-element', '1') |
282 |
|
283 |
var classes = ['media-element']; |
284 |
if (info.view_mode) {
|
285 |
// Remove any existing view mode classes.
|
286 |
element.removeClass (function (index, css) {
|
287 |
return (css.match (/\bfile-\S+/g) || []).join(' '); |
288 |
}); |
289 |
classes.push('file-' + info.view_mode.replace(/_/g, '-')); |
290 |
} |
291 |
// Check for alignment info, after removing any existing alignment class.
|
292 |
element.removeClass (function (index, css) {
|
293 |
return (css.match (/\bmedia-wysiwyg-align-\S+/g) || []).join(' '); |
294 |
}); |
295 |
if (info.fields && info.fields.alignment) {
|
296 |
classes.push('media-wysiwyg-align-' + info.fields.alignment);
|
297 |
} |
298 |
element.addClass(classes.join(' '));
|
299 |
|
300 |
// Attempt to override the link_title if the user has chosen to do this.
|
301 |
info.link_text = this.overrideLinkTitle(info);
|
302 |
// Apply link_text if present.
|
303 |
if (info.link_text) {
|
304 |
$('a', element).html(info.link_text); |
305 |
} |
306 |
|
307 |
return element;
|
308 |
}, |
309 |
|
310 |
/**
|
311 |
* Create a macro representation of the inserted media element.
|
312 |
*
|
313 |
* @param element (jQuery object)
|
314 |
* A media element with attached serialized file info.
|
315 |
*/
|
316 |
create_macro: function (element) { |
317 |
var file_info = Drupal.media.filter.extract_file_info(element);
|
318 |
if (file_info) {
|
319 |
if (typeof file_info.link_text == 'string') { |
320 |
file_info.link_text = this.overrideLinkTitle(file_info);
|
321 |
// Make sure the link_text-html-tags are properly escaped.
|
322 |
file_info.link_text = file_info.link_text.replace(/</g, '<').replace(/>/g, '>'); |
323 |
} |
324 |
return '[[' + JSON.stringify(file_info) + ']]'; |
325 |
} |
326 |
return false; |
327 |
}, |
328 |
|
329 |
/**
|
330 |
* Extract the file info from a WYSIWYG placeholder element as JSON.
|
331 |
*
|
332 |
* @param element (jQuery object)
|
333 |
* A media element with associated file info via a file id (fid).
|
334 |
*/
|
335 |
extract_file_info: function (element) { |
336 |
var fid, file_info, value, delta;
|
337 |
|
338 |
if (fid = element.data('fid')) { |
339 |
Drupal.media.filter.ensureDataMap(); |
340 |
|
341 |
if (file_info = Drupal.settings.mediaDataMap[fid]) {
|
342 |
file_info.attributes = {}; |
343 |
|
344 |
$.each(Drupal.settings.media.wysiwyg_allowed_attributes, function(i, a) { |
345 |
if (value = element.attr(a)) {
|
346 |
// Replace " by \" to avoid error with JSON format.
|
347 |
if (typeof value == 'string') { |
348 |
value = value.replace('"', '\\"'); |
349 |
} |
350 |
file_info.attributes[a] = value; |
351 |
} |
352 |
}); |
353 |
|
354 |
// Extract the link text, if there is any.
|
355 |
file_info.link_text = (Drupal.settings.mediaDoLinkText) ? element.find('a').html() : false; |
356 |
|
357 |
// When a file is embedded, its fields can be overridden. To allow for
|
358 |
// the edge case where the same file is embedded multiple times with
|
359 |
// different field overrides, we look for a data-delta attribute on
|
360 |
// the element, and use that to decide which set of data in the
|
361 |
// "field_deltas" property to use.
|
362 |
if (delta = element.data('delta')) { |
363 |
if (file_info.field_deltas && file_info.field_deltas[delta]) {
|
364 |
file_info.fields = file_info.field_deltas[delta]; |
365 |
|
366 |
// Also look for an overridden view mode, aka "format".
|
367 |
// Check for existance of fields to make it backward compatible.
|
368 |
if (file_info.fields && file_info.fields.format && file_info.view_mode) {
|
369 |
file_info.view_mode = file_info.fields.format; |
370 |
} |
371 |
} |
372 |
} |
373 |
} |
374 |
else {
|
375 |
return false; |
376 |
} |
377 |
} |
378 |
else {
|
379 |
return false; |
380 |
} |
381 |
|
382 |
return Drupal.media.filter.syncAttributesToFields(file_info);
|
383 |
}, |
384 |
|
385 |
/**
|
386 |
* Gets the HTML content of an element.
|
387 |
*
|
388 |
* @param element (jQuery object)
|
389 |
*/
|
390 |
outerHTML: function (element) { |
391 |
return element[0].outerHTML || $('<div>').append(element.eq(0).clone()).html(); |
392 |
}, |
393 |
|
394 |
/**
|
395 |
* Gets the wrapped HTML content of an element to insert into the wysiwyg.
|
396 |
*
|
397 |
* It also registers the element in the tag map so that the token
|
398 |
* replacement works.
|
399 |
*
|
400 |
* @param element (jQuery object) The element to insert.
|
401 |
*
|
402 |
* @see Drupal.media.filter.replacePlaceholderWithToken()
|
403 |
*/
|
404 |
getWysiwygHTML: function (element) { |
405 |
// Create the markup and the macro.
|
406 |
var markup = Drupal.media.filter.outerHTML(element),
|
407 |
macro = Drupal.media.filter.create_macro(element); |
408 |
|
409 |
// Store macro/markup in the tagmap.
|
410 |
Drupal.media.filter.ensure_tagmap(); |
411 |
Drupal.settings.tagmap[macro] = markup; |
412 |
|
413 |
// Return the html code to insert in an editor and use it with
|
414 |
// replacePlaceholderWithToken()
|
415 |
return markup;
|
416 |
}, |
417 |
|
418 |
/**
|
419 |
* Ensures the src tracking has been initialized and returns it.
|
420 |
*/
|
421 |
ensureSourceMap: function() { |
422 |
Drupal.settings.mediaSourceMap = Drupal.settings.mediaSourceMap || {}; |
423 |
return Drupal.settings.mediaSourceMap;
|
424 |
}, |
425 |
|
426 |
/**
|
427 |
* Ensures the data tracking has been initialized and returns it.
|
428 |
*/
|
429 |
ensureDataMap: function() { |
430 |
Drupal.settings.mediaDataMap = Drupal.settings.mediaDataMap || {}; |
431 |
return Drupal.settings.mediaDataMap;
|
432 |
}, |
433 |
|
434 |
/**
|
435 |
* Ensures the tag map has been initialized and returns it.
|
436 |
*/
|
437 |
ensure_tagmap: function () { |
438 |
Drupal.settings.tagmap = Drupal.settings.tagmap || {}; |
439 |
return Drupal.settings.tagmap;
|
440 |
}, |
441 |
|
442 |
/**
|
443 |
* Return the overridden link title based on the file_entity title field
|
444 |
* set.
|
445 |
* @param file the file object.
|
446 |
* @returns the overridden link_title or the existing link text if no
|
447 |
* overridden.
|
448 |
*/
|
449 |
overrideLinkTitle: function(file) { |
450 |
var file_title_field = Drupal.settings.media.img_title_field.replace('field_', ''); |
451 |
var file_title_field_machine_name = ''; |
452 |
if (typeof(file.fields) != 'undefined') { |
453 |
jQuery.each(file.fields, function(field, fieldValue) {
|
454 |
if (field.indexOf(file_title_field) != -1) { |
455 |
file_title_field_machine_name = field; |
456 |
} |
457 |
}); |
458 |
|
459 |
if (typeof(file.fields[file_title_field_machine_name]) != 'undefined' && file.fields[file_title_field_machine_name] != '') { |
460 |
return file.fields[file_title_field_machine_name];
|
461 |
} |
462 |
else {
|
463 |
return file.link_text;
|
464 |
} |
465 |
} |
466 |
else {
|
467 |
return file.link_text;
|
468 |
} |
469 |
}, |
470 |
|
471 |
/**
|
472 |
* Generates a unique "delta" for each embedding of a particular file.
|
473 |
*/
|
474 |
fileEmbedDelta: function(fid, element) { |
475 |
// Ensure we have an object to track our deltas.
|
476 |
Drupal.settings.mediaDeltas = Drupal.settings.mediaDeltas || {}; |
477 |
Drupal.settings.maxMediaDelta = Drupal.settings.maxMediaDelta || 0;
|
478 |
|
479 |
// Check to see if the element already has one.
|
480 |
if (element && element.data('delta')) { |
481 |
var existingDelta = element.data('delta'); |
482 |
// If so, make sure that it is being tracked in mediaDeltas. If we're
|
483 |
// going to create new deltas later on, make sure they do not overwrite
|
484 |
// other mediaDeltas.
|
485 |
if (!Drupal.settings.mediaDeltas[existingDelta]) {
|
486 |
Drupal.settings.mediaDeltas[existingDelta] = fid; |
487 |
Drupal.settings.maxMediaDelta = Math.max(Drupal.settings.maxMediaDelta, existingDelta); |
488 |
} |
489 |
return existingDelta;
|
490 |
} |
491 |
// Otherwise, generate a new one.
|
492 |
var newDelta = Drupal.settings.maxMediaDelta + 1; |
493 |
Drupal.settings.mediaDeltas[newDelta] = fid; |
494 |
Drupal.settings.maxMediaDelta = newDelta; |
495 |
return newDelta;
|
496 |
} |
497 |
} |
498 |
|
499 |
})(jQuery); |