4 * Released under LGPL License.
5 * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
7 * License: http://www.tinymce.com/license
8 * Contributing: http://www.tinymce.com/contributing
11 /*global tinymce:true */
13 tinymce.PluginManager.add('image', function(editor) {
14 function getImageSize(url, callback) {
15 var img = document.createElement('img');
17 function done(width, height) {
19 img.parentNode.removeChild(img);
22 callback({width: width, height: height});
25 img.onload = function() {
26 done(Math.max(img.width, img.clientWidth), Math.max(img.height, img.clientHeight));
29 img.onerror = function() {
33 var style = img.style;
34 style.visibility = 'hidden';
35 style.position = 'fixed';
36 style.bottom = style.left = 0;
37 style.width = style.height = 'auto';
39 document.body.appendChild(img);
43 function buildListItems(inputList, itemCallback, startItems) {
44 function appendItems(values, output) {
45 output = output || [];
47 tinymce.each(values, function(item) {
48 var menuItem = {text: item.text || item.title};
51 menuItem.menu = appendItems(item.menu);
53 menuItem.value = item.value;
54 itemCallback(menuItem);
57 output.push(menuItem);
63 return appendItems(inputList, startItems || []);
66 function createImageList(callback) {
68 var imageList = editor.settings.image_list;
70 if (typeof imageList == "string") {
71 tinymce.util.XHR.send({
73 success: function(text) {
74 callback(tinymce.util.JSON.parse(text));
77 } else if (typeof imageList == "function") {
85 function showDialog(imageList) {
86 var win, data = {}, dom = editor.dom, imgElm, figureElm;
87 var width, height, imageListCtrl, classListCtrl, imageDimensions = editor.settings.image_dimensions !== false;
89 function recalcSize() {
90 var widthCtrl, heightCtrl, newWidth, newHeight;
92 widthCtrl = win.find('#width')[0];
93 heightCtrl = win.find('#height')[0];
95 if (!widthCtrl || !heightCtrl) {
99 newWidth = widthCtrl.value();
100 newHeight = heightCtrl.value();
102 if (win.find('#constrain')[0].checked() && width && height && newWidth && newHeight) {
103 if (width != newWidth) {
104 newHeight = Math.round((newWidth / width) * newHeight);
106 if (!isNaN(newHeight)) {
107 heightCtrl.value(newHeight);
110 newWidth = Math.round((newHeight / height) * newWidth);
112 if (!isNaN(newWidth)) {
113 widthCtrl.value(newWidth);
122 function onSubmitForm() {
123 var figureElm, oldImg;
125 function waitLoad(imgElm) {
126 function selectImage() {
127 imgElm.onload = imgElm.onerror = null;
129 if (editor.selection) {
130 editor.selection.select(imgElm);
131 editor.nodeChanged();
135 imgElm.onload = function() {
136 if (!data.width && !data.height && imageDimensions) {
137 dom.setAttribs(imgElm, {
138 width: imgElm.clientWidth,
139 height: imgElm.clientHeight
142 editor.fire( 'wpNewImageRefresh', { node: imgElm } );
148 imgElm.onerror = selectImage;
154 data = tinymce.extend(data, win.toJSON());
155 var wpcaption = data.wpcaption; // WP
165 if (data.width === '') {
169 if (data.height === '') {
177 // Setup new data excluding style properties
178 /*eslint dot-notation: 0*/
186 caption: data.caption,
187 "class": data["class"]
190 editor.undoManager.transact(function() {
192 var eventData = { node: imgElm, data: data, wpcaption: wpcaption };
194 editor.fire( 'wpImageFormSubmit', { imgData: eventData } );
196 if ( eventData.cancel ) {
197 waitLoad( eventData.node );
206 editor.nodeChanged();
212 if (data.title === "") {
217 data.id = '__mcenew';
219 editor.selection.setContent(dom.createHTML('img', data));
220 imgElm = dom.get('__mcenew');
221 dom.setAttrib(imgElm, 'id', null);
223 dom.setAttribs(imgElm, data);
226 editor.editorUpload.uploadImagesAuto();
228 if (data.caption === false) {
229 if (dom.is(imgElm.parentNode, 'figure.image')) {
230 figureElm = imgElm.parentNode;
231 dom.insertAfter(imgElm, figureElm);
232 dom.remove(figureElm);
236 function isTextBlock(node) {
237 return editor.schema.getTextBlockElements()[node.nodeName];
240 if (data.caption === true) {
241 if (!dom.is(imgElm.parentNode, 'figure.image')) {
243 imgElm = imgElm.cloneNode(true);
244 figureElm = dom.create('figure', {'class': 'image'});
245 figureElm.appendChild(imgElm);
246 figureElm.appendChild(dom.create('figcaption', {contentEditable: true}, 'Caption'));
247 figureElm.contentEditable = false;
249 var textBlock = dom.getParent(oldImg, isTextBlock);
251 dom.split(textBlock, oldImg, figureElm);
253 dom.replace(figureElm, oldImg);
256 editor.selection.select(figureElm);
266 function removePixelSuffix(value) {
268 value = value.replace(/px$/, '');
274 function srcChange(e) {
275 var srcURL, prependURL, absoluteURLPattern, meta = e.meta || {};
278 imageListCtrl.value(editor.convertURL(this.value(), 'src'));
281 tinymce.each(meta, function(value, key) {
282 win.find('#' + key).value(value);
285 if (!meta.width && !meta.height) {
286 srcURL = editor.convertURL(this.value(), 'src');
288 // Pattern test the src url and make sure we haven't already prepended the url
289 prependURL = editor.settings.image_prepend_url;
290 absoluteURLPattern = new RegExp('^(?:[a-z]+:)?//', 'i');
291 if (prependURL && !absoluteURLPattern.test(srcURL) && srcURL.substring(0, prependURL.length) !== prependURL) {
292 srcURL = prependURL + srcURL;
297 getImageSize(editor.documentBaseURI.toAbsolute(this.value()), function(data) {
298 if (data.width && data.height && imageDimensions) {
300 height = data.height;
302 win.find('#width').value(width);
303 win.find('#height').value(height);
309 imgElm = editor.selection.getNode();
310 figureElm = dom.getParent(imgElm, 'figure.image');
312 imgElm = dom.select('img', figureElm)[0];
315 if (imgElm && (imgElm.nodeName != 'IMG' || imgElm.getAttribute('data-mce-object') || imgElm.getAttribute('data-mce-placeholder'))) {
320 width = dom.getAttrib(imgElm, 'width');
321 height = dom.getAttrib(imgElm, 'height');
324 src: dom.getAttrib(imgElm, 'src'),
325 alt: dom.getAttrib(imgElm, 'alt'),
326 title: dom.getAttrib(imgElm, 'title'),
327 "class": dom.getAttrib(imgElm, 'class'),
334 editor.fire( 'wpLoadImageData', { imgData: { data: data, node: imgElm } } );
341 values: buildListItems(
344 item.value = editor.convertURL(item.value || item.url, 'src');
346 [{text: 'None', value: ''}]
348 value: data.src && editor.convertURL(data.src, 'src'),
349 onselect: function(e) {
350 var altCtrl = win.find('#alt');
352 if (!altCtrl.value() || (e.lastControl && altCtrl.value() == e.lastControl.text())) {
353 altCtrl.value(e.control.text());
356 win.find('#src').value(e.control.value()).fire('change');
358 onPostRender: function() {
359 /*eslint consistent-this: 0*/
360 imageListCtrl = this;
365 if (editor.settings.image_class_list) {
370 values: buildListItems(
371 editor.settings.image_class_list,
374 item.textStyle = function() {
375 return editor.formatter.getCssText({inline: 'img', classes: [item.value]});
383 // General settings shared between simple and advanced dialogs
384 var generalFormItems = [
396 if (editor.settings.image_description !== false) {
397 generalFormItems.push({name: 'alt', type: 'textbox', label: 'Image description'});
400 if (editor.settings.image_title) {
401 generalFormItems.push({name: 'title', type: 'textbox', label: 'Image Title'});
404 if (imageDimensions) {
405 generalFormItems.push({
413 {name: 'width', type: 'textbox', maxLength: 5, size: 3, onchange: recalcSize, ariaLabel: 'Width'},
414 {type: 'label', text: 'x'},
415 {name: 'height', type: 'textbox', maxLength: 5, size: 3, onchange: recalcSize, ariaLabel: 'Height'},
416 {name: 'constrain', type: 'checkbox', checked: true, text: 'Constrain proportions'}
421 generalFormItems.push(classListCtrl);
423 if (editor.settings.image_caption && tinymce.Env.ceFalse) {
424 generalFormItems.push({name: 'caption', type: 'checkbox', label: 'Caption'});
428 editor.fire( 'wpLoadImageForm', { data: generalFormItems } );
430 function mergeMargins(css) {
433 var splitMargin = css.margin.split(" ");
435 switch (splitMargin.length) {
436 case 1: //margin: toprightbottomleft;
437 css['margin-top'] = css['margin-top'] || splitMargin[0];
438 css['margin-right'] = css['margin-right'] || splitMargin[0];
439 css['margin-bottom'] = css['margin-bottom'] || splitMargin[0];
440 css['margin-left'] = css['margin-left'] || splitMargin[0];
442 case 2: //margin: topbottom rightleft;
443 css['margin-top'] = css['margin-top'] || splitMargin[0];
444 css['margin-right'] = css['margin-right'] || splitMargin[1];
445 css['margin-bottom'] = css['margin-bottom'] || splitMargin[0];
446 css['margin-left'] = css['margin-left'] || splitMargin[1];
448 case 3: //margin: top rightleft bottom;
449 css['margin-top'] = css['margin-top'] || splitMargin[0];
450 css['margin-right'] = css['margin-right'] || splitMargin[1];
451 css['margin-bottom'] = css['margin-bottom'] || splitMargin[2];
452 css['margin-left'] = css['margin-left'] || splitMargin[1];
454 case 4: //margin: top right bottom left;
455 css['margin-top'] = css['margin-top'] || splitMargin[0];
456 css['margin-right'] = css['margin-right'] || splitMargin[1];
457 css['margin-bottom'] = css['margin-bottom'] || splitMargin[2];
458 css['margin-left'] = css['margin-left'] || splitMargin[3];
465 function updateStyle() {
466 function addPixelSuffix(value) {
467 if (value.length > 0 && /^[0-9]+$/.test(value)) {
474 if (!editor.settings.image_advtab) {
478 var data = win.toJSON(),
479 css = dom.parseStyle(data.style);
481 css = mergeMargins(css);
484 css['margin-top'] = css['margin-bottom'] = addPixelSuffix(data.vspace);
487 css['margin-left'] = css['margin-right'] = addPixelSuffix(data.hspace);
490 css['border-width'] = addPixelSuffix(data.border);
493 win.find('#style').value(dom.serializeStyle(dom.parseStyle(dom.serializeStyle(css))));
496 function updateVSpaceHSpaceBorder() {
497 if (!editor.settings.image_advtab) {
501 var data = win.toJSON(),
502 css = dom.parseStyle(data.style);
504 win.find('#vspace').value("");
505 win.find('#hspace').value("");
507 css = mergeMargins(css);
509 //Move opposite equal margins to vspace/hspace field
510 if ((css['margin-top'] && css['margin-bottom']) || (css['margin-right'] && css['margin-left'])) {
511 if (css['margin-top'] === css['margin-bottom']) {
512 win.find('#vspace').value(removePixelSuffix(css['margin-top']));
514 win.find('#vspace').value('');
516 if (css['margin-right'] === css['margin-left']) {
517 win.find('#hspace').value(removePixelSuffix(css['margin-right']));
519 win.find('#hspace').value('');
524 if (css['border-width']) {
525 win.find('#border').value(removePixelSuffix(css['border-width']));
528 win.find('#style').value(dom.serializeStyle(dom.parseStyle(dom.serializeStyle(css))));
532 if (editor.settings.image_advtab) {
533 // Parse styles from img
535 if (imgElm.style.marginLeft && imgElm.style.marginRight && imgElm.style.marginLeft === imgElm.style.marginRight) {
536 data.hspace = removePixelSuffix(imgElm.style.marginLeft);
538 if (imgElm.style.marginTop && imgElm.style.marginBottom && imgElm.style.marginTop === imgElm.style.marginBottom) {
539 data.vspace = removePixelSuffix(imgElm.style.marginTop);
541 if (imgElm.style.borderWidth) {
542 data.border = removePixelSuffix(imgElm.style.borderWidth);
545 data.style = editor.dom.serializeStyle(editor.dom.parseStyle(editor.dom.getAttrib(imgElm, 'style')));
548 // Advanced dialog shows general+advanced tabs
549 win = editor.windowManager.open({
550 title: 'Insert/edit image',
552 bodyType: 'tabpanel',
557 items: generalFormItems
569 onchange: updateVSpaceHSpaceBorder
577 alignH: ['left', 'right'],
581 onchange: updateStyle
584 {label: 'Vertical space', name: 'vspace'},
585 {label: 'Horizontal space', name: 'hspace'},
586 {label: 'Border', name: 'border'}
592 onSubmit: onSubmitForm
595 // Simple default dialog
596 win = editor.windowManager.open({
597 title: 'Insert/edit image',
599 body: generalFormItems,
600 onSubmit: onSubmitForm
605 editor.on('preInit', function() {
606 function hasImageClass(node) {
607 var className = node.attr('class');
608 return className && /\bimage\b/.test(className);
611 function toggleContentEditableState(state) {
612 return function(nodes) {
613 var i = nodes.length, node;
615 function toggleContentEditable(node) {
616 node.attr('contenteditable', state ? 'true' : null);
622 if (hasImageClass(node)) {
623 node.attr('contenteditable', state ? 'false' : null);
624 tinymce.each(node.getAll('figcaption'), toggleContentEditable);
630 editor.parser.addNodeFilter('figure', toggleContentEditableState(true));
631 editor.serializer.addNodeFilter('figure', toggleContentEditableState(false));
634 editor.addButton('image', {
636 tooltip: 'Insert/edit image',
637 onclick: createImageList(showDialog),
638 stateSelector: 'img:not([data-mce-object],[data-mce-placeholder]),figure.image'
641 editor.addMenuItem('image', {
643 text: 'Insert/edit image',
644 onclick: createImageList(showDialog),
646 prependToContext: true
649 editor.addCommand('mceImage', createImageList(showDialog));