mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
454 lines
19 KiB
JavaScript
454 lines
19 KiB
JavaScript
|
/**
|
||
|
* 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 };
|
||
|
}
|
||
|
|
||
|
})();
|