Merge pull request #2386 from adrexia/tinymce-image-resize

BUG: Image resize allows skewing of image in IE (fixes CMS #791)
This commit is contained in:
Ingo Schommer 2013-09-05 04:08:06 -07:00
commit ef2fc46eb2
3 changed files with 456 additions and 0 deletions

View File

@ -34,6 +34,7 @@ HtmlEditorConfig::get('cms')->enablePlugins('media', 'fullscreen', 'inlinepopups
HtmlEditorConfig::get('cms')->enablePlugins(array(
'ssbuttons' => sprintf('../../../%s/tinymce_ssbuttons/editor_plugin_src.js', THIRDPARTY_DIR)
));
HtmlEditorConfig::get('cms')->enablePlugins('advimagescale');
HtmlEditorConfig::get('cms')->insertButtonsBefore('formatselect', 'styleselect');
HtmlEditorConfig::get('cms')->addButtonsToLine(2,

View File

@ -0,0 +1 @@
(function(){function e(e,t){var n=e.dom;var r=n.getAttrib(t,"mce_advimageresize_id");if(!e.originalDimensions[r]){e.originalDimensions[r]=e.lastDimensions[r]={width:n.getAttrib(t,"width",t.width),height:n.getAttrib(t,"height",t.height)}}return true}function t(t,n){var r=t.dom;var i=r.getAttrib(n,"mce_advimageresize_id");if(!i){var i=t.id+"_"+t.dom.uniqueId();r.setAttrib(n,"mce_advimageresize_id",i);e(t,n)}return i}function n(n,o,u){var l=n.dom;var c=t(n,o);var h=l.getAttrib(o,"width")!=n.lastDimensions[c].width||l.getAttrib(o,"height")!=n.lastDimensions[c].height;if(!h)return;if(l.getAttrib(o,"mce_noresize")||l.hasClass(o,n.getParam("advimagescale_noresize_class","noresize"))||n.getParam("advimagescale_noresize_all")){l.setAttrib(o,"width",n.lastDimensions[c].width);l.setAttrib(o,"height",n.lastDimensions[c].height);if(tinymce.isGecko)i(n);return}if(n.getParam("advimagescale_fix_border_glitch",true)){r(n,o);e(n,o)}var p=n.getParam("advimagescale_filter_src");if(p){var d=new RegExp(p);if(!o.src.match(d)){return}}var v=n.getParam("advimagescale_filter_class");if(v){if(!l.hasClass(o,v)){return}}var m={width:l.getAttrib(o,"width",o.width),height:l.getAttrib(o,"height",o.height)};if(n.getParam("advimagescale_maintain_aspect_ratio",true)){m=a(n,o,m.width,m.height)}m=f(n,o,m.width,m.height);var g=l.getAttrib(o,"width",o.width)!=m.width||l.getAttrib(o,"height",o.height)!=m.height;if(g){l.setAttrib(o,"width",m.width);l.setAttrib(o,"height",m.height);if(tinymce.isGecko)i(n)}if(n.getParam("advimagescale_append_to_url")){s(n,o,l.getAttrib(o,"width",o.width),l.getAttrib(o,"height",o.height))}if(n.lastDimensions[c].width!=l.getAttrib(o,"width",o.width)||n.lastDimensions[c].height!=l.getAttrib(o,"height",o.height)){if(n.getParam("advimagescale_resize_callback")){n.getParam("advimagescale_resize_callback")(n,o)}}n.lastDimensions[c]={width:l.getAttrib(o,"width",o.width),height:l.getAttrib(o,"height",o.height)}}function r(e,t){var n=e.dom;var r=n.getAttrib(t,"mce_advimageresize_id");var s=n.getAttrib(t,"width",t.width);var o=n.getAttrib(t,"height",t.height);var u=false;if(s!=e.lastDimensions[r].width){var a=0;a+=parseInt(n.getStyle(t,"borderLeftWidth","borderLeftWidth"));a+=parseInt(n.getStyle(t,"borderRightWidth","borderRightWidth"));if(a>0){n.setAttrib(t,"width",s-a);u=true}}if(o!=e.lastDimensions[r].height){var f=0;f+=parseInt(n.getStyle(t,"borderTopWidth","borderTopWidth"));f+=parseInt(n.getStyle(t,"borderBottomWidth","borderBottomWidth"));if(f>0){n.setAttrib(t,"height",o-f);u=true}}if(u&&tinymce.isGecko)i(e)}function i(e){e.execCommand("mceRepaint",false)}function s(e,t,n,r){var i=e.dom;var s=i.getAttrib(t,"src");var a=e.getParam("advimagescale_url_width_key","w");s=u(s,a,n);var f=e.getParam("advimagescale_url_height_key","h");s=u(s,f,r);if(s==i.getAttrib(t,"src")){return}if(e.getParam("advimagescale_loading_callback")){e.getParam("advimagescale_loading_callback")(t)}if(e.getParam("advimagescale_loaded_callback")){tinymce.dom.Event.add(t,"load",o,{el:t,ed:e})}i.setAttrib(t,"src",s)}function o(e){var t=this.el;var n=this.ed;var r=n.getParam("advimagescale_loaded_callback");r(t);tinymce.dom.Event.remove(t,"load",o)}function u(e,t,n){if(!e.match(/\?/))e+="?";if(!e.match(new RegExp("([?&])"+t+"="))){if(!e.match(/[&\?]$/))e+="&";e+=t+"="+escape(n)}else{e=e.replace(new RegExp("([?&])"+t+"=[^&]*"),"$1"+t+"="+escape(n))}return e}function a(e,t,n,r){var i=e.dom.getAttrib(t,"mce_advimageresize_id");var s=e.originalDimensions[i].width/e.originalDimensions[i].height;var o=e.lastDimensions[i].width;var u=e.lastDimensions[i].height;var a=Math.abs(o-n);var f=Math.abs(u-r);var l=Math.abs(a/o);var c=Math.abs(f/u);if(a||f){if(l>c){return{width:n,height:Math.round(n/s)}}else{return{width:Math.round(r*s),height:r}}}return{width:n,height:r}}function f(e,t,n,r){var i=e.dom.getAttrib(t,"mce_advimageresize_id");var s=e.getParam("advimagescale_max_width");var o=e.getParam("advimagescale_max_height");var u=e.getParam("advimagescale_min_width");var a=e.getParam("advimagescale_min_height");var f=e.getParam("advimagescale_maintain_aspect_ratio",true);var l=e.originalDimensions[i].width;var c=e.originalDimensions[i].height;var h=l/c;if(s&&n>s){n=s;r=f?Math.round(n/h):r}if(o&&r>o){r=o;n=f?Math.round(r*h):n}if(u&&n<u){n=u;r=f?Math.round(n/h):r}if(a&&r<a){r=a;n=f?Math.round(r*h):r}return{width:n,height:r}}tinymce.create("tinymce.plugins.AdvImageScale",{init:function(e,r){e.originalDimensions=new Array;e.lastDimensions=new Array;e.edMouseDown=false;e.onMouseDown.add(function(e,n){var r=tinyMCE.activeEditor.selection.getNode();if(r!=null&&r.nodeName=="IMG"){t(e,n.target)}return true});e.onMouseUp.add(function(e,t){var r=tinyMCE.activeEditor.selection.getNode();if(r!=null&&r.nodeName=="IMG"){setTimeout(function(){n(e,r)},100)}return true});e.onPreProcess.add(function(e,t){if(!t.set)return;tinymce.each(e.dom.select("img",t.node),function(t){n(e,t)})});e.onInit.add(function(e){e.selection.onSetContent.add(function(t,r){var i=t.getNode();tinymce.each(e.dom.select("img",i),function(t){if(t.id!="__mce_tmp")n(e,t)})})});if(e.getParam("advimagescale_reject_external_dragdrop",true)){e.onMouseDown.add(function(t){e.edMouseDown=true});e.onMouseUp.add(function(t){e.edMouseDown=false});e.onInit.add(function(e,t){tinymce.dom.Event.add(e.getBody().parentNode,"dragdrop",function(t){e.edMouseDown=false})});var i=tinymce.isIE?"dragenter":"dragover";e.onInit.add(function(e,t){tinymce.dom.Event.add(e.getBody().parentNode,i,function(t){if(!e.edMouseDown){return tinymce.dom.Event.cancel(t)}})})}},getInfo:function(){return{longname:"Advanced Image Resize Helper",author:"Marc Hodgins",authorurl:"http://www.hodginsmedia.com",infourl:"http://code.google.com/p/tinymce-plugin-advimagescale",version:"1.1.3"}}});tinymce.PluginManager.add("advimagescale",tinymce.plugins.AdvImageScale)})()

View File

@ -0,0 +1,454 @@
/**
* TinyMCE Advanced Image Resize Helper Plugin
*
* Forces images to maintain aspect ratio while scaling - also optionally enforces
* min/max image dimensions, and appends width/height to the image URL for server-side
* resizing
*
* @author Marc Hodgins (modified to remove global variables: http://sourceforge.net/p/tinymce/plugins/186/)
* @link http://www.hodginsmedia.com Hodgins Media Ventures Inc.
* @copyright Copyright (C) 2008-2010 Hodgins Media Ventures Inc., All right reserved.
* @license http://www.opensource.org/licenses/lgpl-3.0.html LGPLv3
*/
(function() {
tinymce.create('tinymce.plugins.AdvImageScale', {
/**
* Initializes the plugin, this will be executed after the plugin has been created.
*
* @param {tinymce.Editor} ed Editor instance that the plugin is initialized in.
* @param {string} url Absolute URL to where the plugin is located.
*/
init : function(ed, url) {
/**
* Stores pre-resize image dimensions
* @var {array} (w,h)
*/
ed.originalDimensions = new Array();
/**
* Stores last dimensions before a resize
* @var {array} (w,h)
*/
ed.lastDimensions = new Array();
/**
* Track mousedown status in editor
* @var {boolean}
*/
ed.edMouseDown = false;
// Watch for mousedown (as a fall through to ensure that prepareImage() definitely
// got called on an image tag before mouseup).
//
// Normally this should have happened via the onPreProcess/onSetContent listeners, but
// for completeness we check once more here in case there are edge cases we've missed.
ed.onMouseDown.add(function(ed, e) {
var el = tinyMCE.activeEditor.selection.getNode();
if (el != null && el.nodeName == 'IMG') {
// prepare image for resizing
prepareImage(ed, e.target);
}
return true;
});
// Watch for mouseup (catch image resizes)
ed.onMouseUp.add(function(ed, e) {
var el = tinyMCE.activeEditor.selection.getNode();
if (el != null && el.nodeName == 'IMG') {
// setTimeout is necessary to allow the browser to complete the resize so we have new dimensions
setTimeout(function() {
constrainSize(ed, el);
}, 100);
}
return true;
});
/*****************************************************
* ENFORCE CONSTRAINTS ON CONTENT INSERTED INTO EDITOR
*****************************************************/
// Catch editor.setContent() events via onPreProcess (because onPreProcess allows us to
// modify the DOM before it is inserted, unlike onSetContent)
ed.onPreProcess.add(function(ed, o) {
if (!o.set) return; // only 'set' operations let us modify the nodes
// loop in each img node and run constrainSize
tinymce.each(ed.dom.select('img', o.node), function(currentNode) {
constrainSize(ed, currentNode);
});
});
// To be complete, we also need to watch for setContent() calls on the selection object so that
// constraints are enforced (i.e. in case an <img> tag is inserted via mceInsertContent).
// So, catch all insertions using the editor's selection object
ed.onInit.add(function(ed) {
// http://wiki.moxiecode.com/index.php/TinyMCE:API/tinymce.dom.Selection/onSetContent
ed.selection.onSetContent.add(function(se, o) {
// @todo This seems to grab the entire editor contents - it works but could
// perform poorly on large documents
var currentNode = se.getNode();
tinymce.each(ed.dom.select('img', currentNode), function (currentNode) {
// IF condition required as tinyMCE inserts 24x24 placeholders uner some conditions
if (currentNode.id != "__mce_tmp")
constrainSize(ed, currentNode);
});
});
});
/*****************************
* DISALLOW EXTERNAL IMAGE DRAG/DROPS
*****************************/
// This is a hack. Listening for drag events wasn't working.
//
// Watches for mousedown and mouseup/dragdrop events within the editor. If a mouseup or
// dragdrop occurs in the editor without a preceeding mousedown, we assume it is an external
// dragdrop that should be rejected.
if (ed.getParam('advimagescale_reject_external_dragdrop', true)) {
// catch mousedowns mouseups and dragdrops (which are basically mouseups too..)
ed.onMouseDown.add(function(e) { ed.edMouseDown = true; });
ed.onMouseUp.add(function(e) { ed.edMouseDown = false; });
ed.onInit.add(function(ed, o) {
tinymce.dom.Event.add(ed.getBody().parentNode, 'dragdrop', function(e) { ed.edMouseDown = false; });
});
// watch for drag attempts
var evt = (tinymce.isIE) ? 'dragenter' : 'dragover'; // IE allows dragdrop reject on dragenter (more efficient)
ed.onInit.add(function(ed, o) {
// use parentNode to go above editor content, to cover entire editor area
tinymce.dom.Event.add(ed.getBody().parentNode, evt, function (e) {
if (!ed.edMouseDown) {
// disallow drop
return tinymce.dom.Event.cancel(e);
}
});
});
}
},
/**
* Returns information about the plugin as a name/value array.
* The current keys are longname, author, authorurl, infourl and version.
*
* @return {Object} Name/value array containing information about the plugin.
*/
getInfo : function() {
return {
longname : 'Advanced Image Resize Helper',
author : 'Marc Hodgins',
authorurl : 'http://www.hodginsmedia.com',
infourl : 'http://code.google.com/p/tinymce-plugin-advimagescale',
version : '1.1.3'
};
}
});
// Register plugin
tinymce.PluginManager.add('advimagescale', tinymce.plugins.AdvImageScale);
/**
* Store image dimensions, pre-resize
*
* @param {object} el HTMLDomNode
*/
function storeDimensions(ed, el) {
var dom = ed.dom;
var elId = dom.getAttrib(el, 'mce_advimageresize_id');
// store original dimensions if this is the first resize of this element
if (!ed.originalDimensions[elId]) {
ed.originalDimensions[elId] = ed.lastDimensions[elId] = {width: dom.getAttrib(el, 'width', el.width), height: dom.getAttrib(el, 'height', el.height)};
}
return true;
}
/**
* Prepare image for resizing
* Check to see if we've seen this IMG tag before; does tasks such as adding
* unique IDs to image tags, saving "original" image dimensions, etc.
* @param {object} e is optional
*/
function prepareImage(ed, el) {
var dom = ed.dom;
var elId = dom.getAttrib(el, 'mce_advimageresize_id');
// is this the first time this image tag has been seen?
if (!elId) {
var elId = ed.id + "_" + ed.dom.uniqueId();
dom.setAttrib(el, 'mce_advimageresize_id', elId);
storeDimensions(ed, el);
}
return elId;
}
/**
* Adjusts width and height to keep within min/max bounds and also maintain aspect ratio
* If mce_noresize attribute is set to image tag, then image resize is disallowed
*/
function constrainSize(ed, el, e) {
var dom = ed.dom;
var elId = prepareImage(ed, el); // also calls storeDimensions
var resized = (dom.getAttrib(el, 'width') != ed.lastDimensions[elId].width || dom.getAttrib(el, 'height') != ed.lastDimensions[elId].height);
if (!resized)
return; // nothing to do
// disallow image resize if mce_noresize or the noresize class is set on the image tag
if (dom.getAttrib(el, 'mce_noresize') || dom.hasClass(el, ed.getParam('advimagescale_noresize_class', 'noresize')) || ed.getParam('advimagescale_noresize_all')) {
dom.setAttrib(el, 'width', ed.lastDimensions[elId].width);
dom.setAttrib(el, 'height', ed.lastDimensions[elId].height);
if (tinymce.isGecko)
fixGeckoHandles(ed);
return;
}
// Both IE7 and Gecko (as of FF3.0.03) has a "expands image by border width" bug before doing anything else
if (ed.getParam('advimagescale_fix_border_glitch', true /* default to true */)) {
fixImageBorderGlitch(ed, el);
storeDimensions(ed, el); // store adjusted dimensions
}
// filter by regexp so only some images get constrained
var src_filter = ed.getParam('advimagescale_filter_src');
if (src_filter) {
var r = new RegExp(src_filter);
if (!el.src.match(r)) {
return; // skip this element
}
}
// allow filtering by classname
var class_filter = ed.getParam('advimagescale_filter_class');
if (class_filter) {
if (!dom.hasClass(el, class_filter)) {
return; // skip this element, doesn't have the class we want
}
}
// populate new dimensions object
var newDimensions = { width: dom.getAttrib(el, 'width', el.width), height: dom.getAttrib(el, 'height', el.height) };
// adjust w/h to maintain aspect ratio
if (ed.getParam('advimagescale_maintain_aspect_ratio', true /* default to true */)) {
newDimensions = maintainAspect(ed, el, newDimensions.width, newDimensions.height);
}
// enforce minW/minH/maxW/maxH
newDimensions = checkBoundaries(ed, el, newDimensions.width, newDimensions.height);
// was an adjustment made?
var adjusted = (dom.getAttrib(el, 'width', el.width) != newDimensions.width || dom.getAttrib(el, 'height', el.height) != newDimensions.height);
// apply new w/h
if (adjusted) {
dom.setAttrib(el, 'width', newDimensions.width);
dom.setAttrib(el, 'height', newDimensions.height);
if (tinymce.isGecko) fixGeckoHandles(ed);
}
if (ed.getParam('advimagescale_append_to_url')) {
appendToUri(ed, el, dom.getAttrib(el, 'width', el.width), dom.getAttrib(el, 'height', el.height));
}
// was the image resized?
if (ed.lastDimensions[elId].width != dom.getAttrib(el, 'width', el.width) || ed.lastDimensions[elId].height != dom.getAttrib(el, 'height', el.height)) {
// call "image resized" callback (if set)
if (ed.getParam('advimagescale_resize_callback')) {
ed.getParam('advimagescale_resize_callback')(ed, el);
}
}
// remember "last dimensions" for next time
ed.lastDimensions[elId] = { width: dom.getAttrib(el, 'width', el.width), height: dom.getAttrib(el, 'height', el.height) };
}
/**
* Fixes IE7 and Gecko border width glitch
*
* Both "add" the border width to an image after the resize handles have been
* dropped. This reverses it by looking at the "previous" known size and comparing
* to the current size. If they don't match, then a resize has taken place and the browser
* has (probably) messed it up. So, we reverse it. Note, this will probably need to be
* wrapped in a conditional statement if/when each browser fixes this bug.
*/
function fixImageBorderGlitch(ed, el) {
var dom = ed.dom;
var elId = dom.getAttrib(el, 'mce_advimageresize_id');
var currentWidth = dom.getAttrib(el, 'width', el.width);
var currentHeight = dom.getAttrib(el, 'height', el.height);
var adjusted = false;
// if current dimensions do not match what we last saw, then a resize has taken place
if (currentWidth != ed.lastDimensions[elId].width) {
var adjustWidth = 0;
// get computed border left/right widths
adjustWidth += parseInt(dom.getStyle(el, 'borderLeftWidth', 'borderLeftWidth'));
adjustWidth += parseInt(dom.getStyle(el, 'borderRightWidth', 'borderRightWidth'));
// reset the width height to NOT include these amounts
if (adjustWidth > 0) {
dom.setAttrib(el, 'width', (currentWidth - adjustWidth));
adjusted = true;
}
}
if (currentHeight != ed.lastDimensions[elId].height) {
var adjustHeight = 0;
// get computed border top/bottom widths
adjustHeight += parseInt(dom.getStyle(el, 'borderTopWidth', 'borderTopWidth'));
adjustHeight += parseInt(dom.getStyle(el, 'borderBottomWidth', 'borderBottomWidth'));
if (adjustHeight > 0) {
dom.setAttrib(el, 'height', (currentHeight - adjustHeight));
adjusted = true;
}
}
if (adjusted && tinymce.isGecko) fixGeckoHandles(ed);
}
/**
* Fix gecko resize handles glitch
*/
function fixGeckoHandles(ed) {
ed.execCommand('mceRepaint', false);
}
/**
* Set image dimensions on into a uri as querystring params
*/
function appendToUri(ed, el, w, h) {
var dom = ed.dom;
var uri = dom.getAttrib(el, 'src');
var wKey = ed.getParam('advimagescale_url_width_key', 'w');
uri = setQueryParam(uri, wKey, w);
var hKey = ed.getParam('advimagescale_url_height_key', 'h');
uri = setQueryParam(uri, hKey, h);
// no need to continue if URL didn't change
if (uri == dom.getAttrib(el, 'src')) {
return;
}
// trigger image loading callback (if set)
if (ed.getParam('advimagescale_loading_callback')) {
// call loading callback
ed.getParam('advimagescale_loading_callback')(el);
}
// hook image load(ed) callback (if set)
if (ed.getParam('advimagescale_loaded_callback')) {
// hook load event on the image tag to call the loaded callback
tinymce.dom.Event.add(el, 'load', imageLoadedCallback, {el: el, ed: ed});
}
// set new src
dom.setAttrib(el, 'src', uri);
}
/**
* Callback event when an image is (re)loaded
* @param {object} e Event (use e.target or this.el to access element, this.ed to access editor instance)
*/
function imageLoadedCallback(e) {
var el = this.el; // image element
var ed = this.ed; // editor
var callback = ed.getParam('advimagescale_loaded_callback'); // user specified callback
// call callback, pass img as param
callback(el);
// remove callback event
tinymce.dom.Event.remove(el, 'load', imageLoadedCallback);
}
/**
* Sets URL querystring parameters by appending or replacing existing params of same name
*/
function setQueryParam(uri, key, value) {
if (!uri.match(/\?/)) uri += '?';
if (!uri.match(new RegExp('([\?&])' + key + '='))) {
if (!uri.match(/[&\?]$/)) uri += '&';
uri += key + '=' + escape(value);
} else {
uri = uri.replace(new RegExp('([\?\&])' + key + '=[^&]*'), '$1' + key + '=' + escape(value));
}
return uri;
}
/**
* Returns w/h that maintain aspect ratio
*/
function maintainAspect(ed, el, w, h) {
var elId = ed.dom.getAttrib(el, 'mce_advimageresize_id');
// calculate aspect ratio of original so we can maintain it
var ratio = ed.originalDimensions[elId].width / ed.originalDimensions[elId].height;
// decide which dimension changed more (percentage), because that's the
// one we'll respect (the other we'll adjust to keep aspect ratio)
var lastW = ed.lastDimensions[elId].width;
var lastH = ed.lastDimensions[elId].height;
var deltaW = Math.abs(lastW - w); // absolute
var deltaH = Math.abs(lastH - h); // absolute
var pctW = Math.abs(deltaW / lastW); // percentage
var pctH = Math.abs(deltaH / lastH); // percentage
if (deltaW || deltaH) {
if (pctW > pctH) {
// width changed more - use that as the locked point and adjust height
return { width: w, height: Math.round(w / ratio) };
} else {
// height changed more - use that as the locked point and adjust width
return { width: Math.round(h * ratio), height: h };
}
}
// nothing changed
return { width: w, height: h };
}
/**
* Enforce min/max boundaries
*
* Returns true if an adjustment was made
*/
function checkBoundaries(ed, el, w, h) {
var elId = ed.dom.getAttrib(el, 'mce_advimageresize_id');
var maxW = ed.getParam('advimagescale_max_width');
var maxH = ed.getParam('advimagescale_max_height');
var minW = ed.getParam('advimagescale_min_width');
var minH = ed.getParam('advimagescale_min_height');
var maintainAspect = ed.getParam('advimagescale_maintain_aspect_ratio', true);
var oW = ed.originalDimensions[elId].width;
var oH = ed.originalDimensions[elId].height;
var ratio = oW/oH;
// max
if (maxW && w > maxW) {
w = maxW;
h = maintainAspect ? Math.round(w / ratio) : h;
}
if (maxH && h > maxH) {
h = maxH;
w = maintainAspect ? Math.round(h * ratio) : w;
}
// min
if (minW && w < minW) {
w = minW;
h = maintainAspect ? Math.round(w / ratio) : h;
}
if (minH && h < minH) {
h = minH;
w = maintainAspect ? Math.round(h * ratio) : h;
}
return { width: w, height:h };
}
})();