diff --git a/.jshintrc b/.jshintrc
index 40d78b67..7f078319 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -51,7 +51,7 @@
"jquery" : true,
"nomen" : false,
- "onevar" : true,
+ "onevar" : false,
"passfail" : false,
"white" : true,
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d3340fe7..99d77ddc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,9 @@ WYMeditor.
*release-date* TBD
+* [#747](https://github.com/wymeditor/wymeditor/pull/747)
+ New: Image resizing
+
## 1.0.7
*release-date* September 22 2015
diff --git a/Gruntfile.js b/Gruntfile.js
index 8b874bff..11a9b73a 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -274,6 +274,7 @@ module.exports = function (grunt) {
"<%= yeoman.app %>/wymeditor/editor/base.js",
"<%= yeoman.app %>/wymeditor/editor/" +
"document-structure-manager.js",
+ "<%= yeoman.app %>/wymeditor/editor/image-handler.js",
"<%= yeoman.app %>/wymeditor/editor/gecko.js",
"<%= yeoman.app %>/wymeditor/editor/webkit.js",
"<%= yeoman.app %>/wymeditor/editor/blink.js",
diff --git a/src/examples/20-seamless.html b/src/examples/20-seamless.html
index a9aba0be..457fb463 100644
--- a/src/examples/20-seamless.html
+++ b/src/examples/20-seamless.html
@@ -51,7 +51,7 @@
Look ma! No double scrollbar!
html: [
'Some initial content...
',
'And an image for asynchronously added size:
',
- '',
+ '',
'Type in a few paragraphs and see the editor grow in height.
'
].join(''),
skin: 'seamless',
diff --git a/src/test/load-src.dev.js b/src/test/load-src.dev.js
index dc1835a3..dc91e35c 100644
--- a/src/test/load-src.dev.js
+++ b/src/test/load-src.dev.js
@@ -21,6 +21,7 @@ function loadWymSrc(srcPath, extraRequirements, jqueryVersion) {
srcPath + 'wymeditor/editor/dialogs.js',
srcPath + 'wymeditor/editor/base.js',
srcPath + 'wymeditor/editor/document-structure-manager.js',
+ srcPath + 'wymeditor/editor/image-handler.js',
srcPath + 'wymeditor/editor/gecko.js',
srcPath + 'wymeditor/editor/trident-pre-7.js',
srcPath + 'wymeditor/editor/trident-7.js',
diff --git a/src/test/unit/index.html b/src/test/unit/index.html
index b640c357..57de3d2c 100644
--- a/src/test/unit/index.html
+++ b/src/test/unit/index.html
@@ -70,6 +70,7 @@
"specific_feature_tests/undo_redo.js",
"specific_feature_tests/selection.js",
"specific_feature_tests/images.js",
+ "specific_feature_tests/image-resize-handle.js",
"specific_feature_tests/class_toggling.js",
"specific_feature_tests/links.js",
"specific_feature_tests/dialogs.js",
diff --git a/src/test/unit/specific_feature_tests/image-resize-handle.js b/src/test/unit/specific_feature_tests/image-resize-handle.js
new file mode 100644
index 00000000..e2e23000
--- /dev/null
+++ b/src/test/unit/specific_feature_tests/image-resize-handle.js
@@ -0,0 +1,129 @@
+/* jshint maxlen: 100 */
+/* global
+ manipulationTestHelper,
+ prepareUnitTestModule,
+ skipKeyboardShortcutTests,
+ SKIP_THIS_TEST,
+ test,
+ expectOneMore,
+ simulateKeyCombo,
+ stop,
+ start,
+ strictEqual,
+ ok,
+ IMG_SRC
+*/
+'use strict';
+
+module("images-resize_handle", {setup: prepareUnitTestModule});
+
+test("Resize handle is prepended to body on image `mousemove`", function () {
+ manipulationTestHelper({
+ startHtml: '',
+ manipulationFunc: function (wymeditor) {
+ wymeditor.$body().find('img').mousemove();
+ },
+ additionalAssertionsFunc: function (wymeditor) {
+ var $resizeHandle = wymeditor.$body().children().first();
+ expectOneMore();
+ ok($resizeHandle.hasClass('wym-resize-handle'));
+ }
+ });
+});
+
+test("image marker is immediately after image", function () {
+ manipulationTestHelper({
+ startHtml: '',
+ manipulationFunc: function (wymeditor) {
+ wymeditor.$body().find('img').mousemove();
+ },
+ additionalAssertionsFunc: function (wymeditor) {
+ expectOneMore();
+ var $imgMarker = wymeditor.$body().find('.wym-image-marker');
+ ok($imgMarker.prev('img').length);
+ }
+ });
+});
+
+test("resize handle has editor-only class", function () {
+ manipulationTestHelper({
+ startHtml: '',
+ manipulationFunc: function (wymeditor) {
+ wymeditor.$body().find('img').mousemove();
+ },
+ additionalAssertionsFunc: function (wymeditor) {
+ var $resizeHandle = wymeditor.$body().find('.wym-resize-handle');
+ expectOneMore();
+ ok($resizeHandle.hasClass('wym-editor-only'));
+ }
+ });
+});
+
+test("resize handle hidden on `mousemove` outside image and handle", function () {
+ manipulationTestHelper({
+ startHtml: '',
+ manipulationFunc: function (wymeditor) {
+ wymeditor.$body().find('img')
+ .mousemove()
+ .parent().mousemove();
+ },
+ additionalAssertionsFunc: function (wymeditor) {
+ expectOneMore();
+ strictEqual(wymeditor.$body().find('.wym-resize-handle').css('display'), 'none');
+ }
+ });
+});
+
+test("resize handle not hidden on `mousemove` over handle", function () {
+ manipulationTestHelper({
+ startHtml: '',
+ manipulationFunc: function (wymeditor) {
+ wymeditor.$body().find('img').mousemove();
+ wymeditor.$body().find('.wym-resize-handle').mousemove();
+ },
+ additionalAssertionsFunc: function (wymeditor) {
+ expectOneMore();
+ strictEqual(wymeditor.$body().find('.wym-resize-handle').css('display'), 'block');
+ }
+ });
+});
+
+test("resize handle with no image is hidden async after 'keypress'", function () {
+ manipulationTestHelper({
+ startHtml: '',
+ setCaretInSelector: 'p',
+ manipulationFunc: function (wymeditor) {
+ wymeditor.$body().find('img').mousemove().remove();
+ simulateKeyCombo(wymeditor, 'a');
+ },
+ additionalAssertionsFunc: function (wymeditor) {
+ expectOneMore();
+ stop();
+ setTimeout(function () {
+ start();
+ strictEqual(wymeditor.$body().find('.wym-resize-handle').css('display'), 'none');
+ }, 0);
+ },
+ skipFunc: function () {
+ if (skipKeyboardShortcutTests) {
+ return SKIP_THIS_TEST;
+ }
+ }
+ });
+});
+
+test("resize handle with no image is hidden on `mousemove`", function () {
+ manipulationTestHelper({
+ startHtml: '',
+ manipulationFunc: function (wymeditor) {
+ wymeditor.$body().find('img')
+ .mousemove()
+ .remove();
+ wymeditor.$body().find('.wym-resize-handle').mousemove();
+ },
+ additionalAssertionsFunc: function (wymeditor) {
+ expectOneMore();
+ strictEqual(wymeditor.$body().find('.wym-resize-handle').css('display'), 'none');
+ }
+ });
+});
diff --git a/src/test/unit/specific_feature_tests/images.js b/src/test/unit/specific_feature_tests/images.js
index eb01a7fc..b31bcb28 100644
--- a/src/test/unit/specific_feature_tests/images.js
+++ b/src/test/unit/specific_feature_tests/images.js
@@ -1,17 +1,14 @@
/* jshint evil: true */
/* global
manipulationTestHelper,
+ stop,
+ start,
prepareUnitTestModule,
test,
- QUnit,
expectOneMore,
expectMore,
strictEqual,
makeSelection,
- inPhantomjs,
- SKIP_THIS_TEST,
- stop,
- start,
IMG_SRC
*/
"use strict";
@@ -188,99 +185,32 @@ test("Returns an image when it is exclusively selected", function () {
module("images-selection", {setup: prepareUnitTestModule});
-test("Image is selected via `mouseup` in non pre-7 Trident", function () {
+test("Image is selected via `click`", function () {
manipulationTestHelper({
startHtml: getSelectedImageHtml,
- prepareFunc: function (wymeditor) {
- wymeditor.deselect();
- },
manipulationFunc: function (wymeditor) {
- wymeditor.$body().find("img").mouseup();
+ wymeditor.$body().find("img")
+ .click();
},
- expectedResultHtml: getSelectedImageHtml,
additionalAssertionsFunc: function (wymeditor) {
- var img = wymeditor.$body().find("img")[0];
expectOneMore();
- strictEqual(
- wymeditor.getSelectedImage(),
- img
- );
- },
- skipFunc: function () {
- if (inPhantomjs) {
- return SKIP_THIS_TEST;
- }
- if (jQuery.browser.msie && jQuery.browser.versionNumber <= 10) {
- return SKIP_THIS_TEST;
- }
- }
- });
-});
-
-test("Image is selected via `mouseup` in pre-7 trident", function () {
- var wymeditor,
- _selectSingleNode,
- resumeManipulationTestHelper;
-
- if (
- jQuery.browser.msie !== true ||
- jQuery.browser.versionNumber > 10
- ) {
- QUnit.expect(0);
- return;
- }
-
- wymeditor = jQuery.wymeditors(0);
-
- // Stop QUnit from running the next test
- stop();
- // Save the original
- _selectSingleNode = wymeditor._selectSingleNode;
- // Replace it with a wrapper
- wymeditor._selectSingleNode = function (node) {
- // Call the original
- _selectSingleNode.call(wymeditor, node);
- // Unwrap
- wymeditor._selectSingleNode = _selectSingleNode;
- // Resume `manipulationTestHelper`
- resumeManipulationTestHelper();
- // Allow QUnit to run the next test
- start();
- };
-
- resumeManipulationTestHelper = manipulationTestHelper({
- async: true,
- startHtml: getSelectedImageHtml,
- prepareFunc: function (wymeditor) {
- wymeditor.deselect();
- },
- manipulationFunc: function (wymeditor) {
- wymeditor.$body().find("img").mouseup();
- },
- expectedResultHtml: getSelectedImageHtml,
- additionalAssertionsFunc: function (wymeditor) {
var img = wymeditor.$body().find("img")[0];
- expectOneMore();
- strictEqual(
- wymeditor.getSelectedImage(),
- img
- );
- }
- });
-});
-
-test("Image is selected via `dragend` in IE", function () {
- manipulationTestHelper({
- startHtml: getSelectedImageHtml,
- prepareFunc: function (wymeditor) {
- wymeditor.deselect();
- },
- manipulationFunc: function (wymeditor) {
- wymeditor.$body().find("img").trigger("dragend");
- },
- expectedResultHtml: getSelectedImageHtml,
- skipFunc: function () {
- return jQuery.browser.msie ? false : SKIP_THIS_TEST;
+ var assertImageIsSelected = function () {
+ strictEqual(
+ wymeditor.getSelectedImage(),
+ img
+ );
+ };
+ if (jQuery.browser.msie && jQuery.browser.versionNumber === 8) {
+ // image is selected async
+ stop();
+ setTimeout(function () {
+ start();
+ assertImageIsSelected();
+ });
+ return;
+ }
+ assertImageIsSelected();
}
});
});
diff --git a/src/test/unit/specific_feature_tests/xml_parser.js b/src/test/unit/specific_feature_tests/xml_parser.js
index a5b5dad0..ab9e8080 100644
--- a/src/test/unit/specific_feature_tests/xml_parser.js
+++ b/src/test/unit/specific_feature_tests/xml_parser.js
@@ -2,6 +2,8 @@
/* global
wymEqual,
prepareUnitTestModule,
+ manipulationTestHelper,
+ IMG_SRC,
testNoChangeInHtmlArray,
test,
QUnit,
@@ -934,6 +936,18 @@ test("Class removal is case insensitive", function () {
WYMeditor.CLASSES_REMOVED_BY_PARSER = defaultClassesRemovedByParser;
});
+module("XmlParser-remove_style_attribute", {setup: prepareUnitTestModule});
+
+test("Style attribute is removed from images", function () {
+ var expectedResultHtml = '';
+ manipulationTestHelper({
+ startHtml: '',
+ expectedStartHtml: expectedResultHtml,
+ parseHtml: true,
+ expectedResultHtml: expectedResultHtml
+ });
+});
+
module("XmlParser-unwrap_single_tag_in_list_item", {setup: prepareUnitTestModule});
var tagsToUnwrapInLists =
diff --git a/src/wymeditor/core.js b/src/wymeditor/core.js
index e7d6cf4d..0901c3f6 100644
--- a/src/wymeditor/core.js
+++ b/src/wymeditor/core.js
@@ -420,6 +420,9 @@ jQuery.extend(WYMeditor, {
// within the editor.
EDITOR_ONLY_CLASS: "wym-editor-only",
+ // Class for resize handles
+ RESIZE_HANDLE_CLASS: "wym-resize-handle",
+
// Classes that will be removed from all tags' class attribute by the
// parser.
CLASSES_REMOVED_BY_PARSER: [
@@ -1087,7 +1090,7 @@ jQuery.fn.parentsOrSelf = function (selector) {
}
};
-// String & array helpers
+// Various helpers
WYMeditor.Helper = {
@@ -1141,6 +1144,20 @@ WYMeditor.Helper = {
}
}
return null;
+ },
+
+ // naively returns all event types
+ // of the provided an element
+ // according the its property keys that
+ // begin with 'on'
+ getAllEventTypes: function (elem) {
+ var result = [];
+ for (var key in elem) {
+ if (key.indexOf('on') === 0 && key !== 'onmousemove') {
+ result.push(key.slice(2));
+ }
+ }
+ return result.join(' ');
}
};
diff --git a/src/wymeditor/editor/base.js b/src/wymeditor/editor/base.js
index 86054f87..6b8b75fa 100644
--- a/src/wymeditor/editor/base.js
+++ b/src/wymeditor/editor/base.js
@@ -398,6 +398,8 @@ WYMeditor.editor.prototype._afterDesignModeOn = function () {
wym.nativeEditRegistration = new WYMeditor.NativeEditRegistration(wym);
+ wym.ih = new WYMeditor.ImageHandler(wym);
+
jQuery(wym.element).trigger(
WYMeditor.EVENTS.postIframeInitialization,
wym
@@ -3812,10 +3814,6 @@ WYMeditor.editor.prototype._afterInsertTable = function () {
WYMeditor.editor.prototype._listen = function () {
var wym = this;
- wym.$body().bind("mouseup", function (e) {
- wym._mouseup(e);
- });
-
jQuery(wym._doc).bind('paste', function () {
wym._handlePasteEvent();
});
@@ -3857,14 +3855,8 @@ WYMeditor.editor.prototype._selectSingleNode = function (node) {
selection = wym.selection();
nodeRange = rangy.createRangyRange();
nodeRange.selectNode(node);
- selection.setSingleRange(nodeRange);
-};
-WYMeditor.editor.prototype._mouseup = function (evt) {
- var wym = this;
- if (evt.target.tagName.toLowerCase() === WYMeditor.IMG) {
- wym._selectSingleNode(evt.target);
- }
+ selection.setSingleRange(nodeRange);
};
/**
diff --git a/src/wymeditor/editor/gecko.js b/src/wymeditor/editor/gecko.js
index 2a8bdb33..eaf091e2 100644
--- a/src/wymeditor/editor/gecko.js
+++ b/src/wymeditor/editor/gecko.js
@@ -19,16 +19,14 @@ WYMeditor.WymClassGecko.NEEDS_CELL_FIX = parseInt(
WYMeditor.WymClassGecko.prototype._docEventQuirks = function () {
var wym = this;
+ var $doc = jQuery(wym._doc);
- jQuery(wym._doc).bind("keyup", function (evt) {
- wym._keyup(evt);
- });
- jQuery(wym._doc).bind("click", function (evt) {
- wym._click(evt);
- });
- jQuery(wym._doc).bind("focus", function () {
+ $doc.keyup(wym._keyup.bind(wym));
+ $doc.focus(function () {
+ // not providing _onBodyFocus here because it doesn't exist yet
wym.undoRedo._onBodyFocus();
});
+ $doc.click(wym._click.bind(wym));
};
// Keyup handler, mainly used for cleanups
diff --git a/src/wymeditor/editor/image-handler.js b/src/wymeditor/editor/image-handler.js
new file mode 100644
index 00000000..1dd0ef2d
--- /dev/null
+++ b/src/wymeditor/editor/image-handler.js
@@ -0,0 +1,681 @@
+/* jshint maxlen:100 */
+"use strict";
+
+/*
+ * # The Image Handler
+ *
+ * Give it an editor instance and it will make
+ * most of your image resizing dreams come true.
+ *
+ * ## IE8 Shenanigans
+ *
+ * When IE8 is not longer supported,
+ * `rem` could be used for more accurate UI element dimensions
+ *
+ * ## IE9 Shenanigans
+ *
+ * Dragging and dropping of images is disabled.
+ * See the `_isImgDragDropAllowed` function.
+ *
+ * ## IE8-11 Shenanigans
+ *
+ * SVG images are not scaled.
+ * They are cropped. That's right.
+ * And applying style to them does not help,
+ * as well. Ideas are welcome.
+ *
+ * ## General Shenanigans (http://i.imgur.com/wbQ6U5C.jpg)
+ *
+ * This module is covered by only a few basic tests
+ * so any change must be meticulously manually tested
+ * in all the supported browsers
+ * by psychologically stable individuals.
+ *
+ * Dragging and dropping of images
+ * might produce undesired results on drop.
+ * Uncharted territory.
+ *
+ * In event handlers of events that
+ * are not expected to perform useful default actions
+ * `return false` is used to prevent any bad feelings
+ * towards unexpected browser behavior.
+ */
+
+// the image handler class.
+WYMeditor.ImageHandler = function (wym) {
+ var ih = this;
+ ih._wym = wym;
+
+ ih._$resizeHandle = ih._createResizeHandle();
+
+ ih._$currentImageMarker = null;
+
+ // references the image that
+ // has the resize handle placed on it
+ ih._$currentImg = null;
+
+ // flags whether a resize operation is
+ // occurring at this moment
+ ih._resizingNow = false;
+
+ ih._imgDragDropAllowed = WYMeditor.ImageHandler._isImgDragDropAllowed();
+
+ ih._addEventListeners();
+
+ return ih;
+};
+
+WYMeditor.ImageHandler._isImgDragDropAllowed = function () {
+ var browser = jQuery.browser;
+ if (browser.msie) {
+ if (browser.versionNumber === 9) {
+ // dragging and dropping seems to not consistently work.
+ // the image would only some times get picked up by the mouse drag attempt.
+ // to prevent confusion
+ return false;
+ }
+ }
+ return true;
+};
+
+WYMeditor.ImageHandler._RESIZE_HANDLE_HR_HTML = jQuery('
')
+ .addClass(WYMeditor.EDITOR_ONLY_CLASS)
+ .css({margin: 0, padding: 0})
+ .attr('outerHTML');
+
+WYMeditor.ImageHandler._RESIZE_HANDLE_INNER_HTML = [
+ 'drag this to resize',
+ 'click on image to select'
+].join(WYMeditor.ImageHandler._RESIZE_HANDLE_HR_HTML);
+
+WYMeditor.ImageHandler._IMAGE_HIGHLIGHT_COLOR = 'yellow';
+
+// creates and returns
+// a yet detached UI resize handle element
+// in a jQuery object
+WYMeditor.ImageHandler.prototype._createResizeHandle = function () {
+ var $handle = jQuery('');
+
+ // In IE11 it was very easy to
+ // accidentally enter into editing mode
+ // in the resize handle.
+ // This seamlessly prevents it.
+ $handle.attr('contentEditable', 'false');
+
+ $handle.html(WYMeditor.ImageHandler._RESIZE_HANDLE_INNER_HTML);
+
+ $handle
+ .addClass(WYMeditor.RESIZE_HANDLE_CLASS)
+ .addClass(WYMeditor.EDITOR_ONLY_CLASS);
+
+ $handle.css({
+ margin: '0',
+ padding: '0',
+
+ // when IE9 is no longer supported
+ // this could be `ns-resize`
+ cursor: 'row-resize',
+
+ 'text-align': 'center',
+
+ // this means that
+ // elements after the resize handle
+ // will not be pushed down because of its presence.
+ // we later use the `left` and `top` properties
+ // to keep the resize handle exactly
+ // below its current image
+ position: 'absolute',
+
+ 'background-color': WYMeditor.ImageHandler._IMAGE_HIGHLIGHT_COLOR,
+
+ // override default iframe stylesheet
+ // so that a 'div' does not appear
+ 'background-image': 'none',
+
+ // so that the text inside the resize handle
+ // fits in one line.
+ // in the theoretical future
+ // the more appropriate value would be
+ // `min-content`
+ 'min-width': '13em',
+
+ width: '100%'
+ });
+
+ return $handle;
+};
+
+WYMeditor.ImageHandler.prototype._getCurrentImageMarker = function () {
+ var ih = this;
+ if (
+ // a marker was not yet created
+ !ih._$currentImageMarker ||
+ // a marker was destroyed via native edit
+ !ih._$currentImageMarker.length
+ ) {
+ ih._$currentImageMarker = ih._createCurrentImageMarker();
+ }
+ return ih._$currentImageMarker;
+};
+
+WYMeditor.ImageHandler._IMAGE_MARKER_CLASS = 'wym-image-marker';
+
+WYMeditor.ImageHandler.prototype._createCurrentImageMarker = function () {
+ return jQuery('')
+ .addClass(WYMeditor.EDITOR_ONLY_CLASS)
+ .addClass(WYMeditor.ImageHandler._IMAGE_MARKER_CLASS)
+ .hide();
+};
+
+WYMeditor.ImageHandler.prototype._addEventListeners = function () {
+ var ih = this;
+ var $doc = jQuery(ih._wym._doc);
+
+ $doc.delegate(
+ 'img', 'mouseover',
+ ih._onImgMouseover.bind(ih)
+ );
+ $doc.delegate(
+ 'img', 'click',
+ ih._onImgClick.bind(ih)
+ );
+ $doc.delegate(
+ '.' + WYMeditor.RESIZE_HANDLE_CLASS, 'mousedown',
+ ih._onResizeHandleMousedown.bind(ih)
+ );
+ $doc.delegate(
+ 'img',
+ 'mousedown',
+ ih._onImgMousedown.bind(ih)
+ );
+ $doc.delegate(
+ 'img',
+ 'dragstart',
+ ih._onImgDragstart.bind(ih)
+ );
+ $doc.bind(
+ 'mousemove',
+ ih._onMousemove.bind(ih)
+ );
+ $doc.bind(
+ 'mouseup',
+ ih._onMouseup.bind(ih)
+ );
+ ih._edited = new WYMeditor.EXTERNAL_MODULES.Edited(
+ $doc[0],
+ function () {}, // do not do anything with strictly sensible edits
+ ih._onAnyNativeEdit.bind(ih) // handle all edits
+ );
+ $doc.delegate(
+ '.' + WYMeditor.RESIZE_HANDLE_CLASS,
+ 'click dblclick',
+ ih._onResizeHandleClickDblclick.bind(ih)
+ );
+ // useful for debugging
+ if (false) {
+ ih._wym.$body().delegate(
+ '*',
+ WYMeditor.Helper.getAllEventTypes(ih._wym.$body()[0]),
+ ih._onAllEvents.bind(ih)
+ );
+ }
+};
+
+WYMeditor.ImageHandler.prototype._onImgMouseover = function (evt) {
+ var ih = this;
+ var $img = jQuery(evt.target);
+ if (
+ !$img.data('cE disabled') &&
+ jQuery.browser.msie
+ ) {
+ // in IE8-11 it seems that the default cursor for images
+ // (in `designMode`) is 'move' (4 directions arrow)
+ // and simply setting a different cursor
+ // does not change that default.
+ // this works around the issue.
+ // the result is still not just any cursor we'd like,
+ // but only the 'default' cursor,
+ // which is better than the default 'move' cursor.
+ // this workaround does not seem to have obvious side effects
+ $img.attr('contentEditable', 'false');
+ $img.data('cE disabled', true);
+ }
+ ih._setImgCursor($img);
+};
+
+WYMeditor.ImageHandler.prototype._setImgCursor = function ($img) {
+ var ih = this;
+ if (ih._wym.getSelectedImage() !== $img[0]) {
+ // hint that image is selectable by click
+ $img.css('cursor', 'pointer');
+ return;
+ }
+ // image is selected
+ if (ih._imgDragDropAllowed) {
+ // in IE8-11 this does not work
+ // and the cursor remains 'default'.
+ // see the `_onImgMouseover` handler
+ $img.css('cursor', 'move');
+ } else {
+ $img.css('cursor', 'default');
+ }
+};
+
+WYMeditor.ImageHandler.prototype._onImgClick = function (evt) {
+ var ih = this;
+
+ // firefox seems to natively select the image on mousedown
+ // this means that by the time this handler executes,
+ // the image is already selected.
+ //
+ // in IE8, by this point
+ // the image is always deselected,
+ // even if it was selected just before the click,
+ // because the mouse event itself
+ // causes the deselection of the image
+ // (see the `_selectImage` method).
+ //
+ // because of the above browser limitations,
+ // it is more simple to always select the image here,
+ // regardless of whether it is selected already or not
+
+ ih._selectImage(evt.target);
+ ih._indicateOnResizeHandleThatImageIsSelected();
+ return false;
+};
+
+WYMeditor.ImageHandler.prototype._selectImage = function (img) {
+ var ih = this;
+ var $img = jQuery(img);
+
+ if (jQuery.browser.msie && jQuery.browser.versionNumber === 8) {
+ // in IE8 when the right side of an img is clicked
+ // (you can't make this up),
+ // any selection that was set on click is discarded.
+ // scheduling the image selection
+ // for after synchronous execution
+ // works around the issue
+ setTimeout(function () {
+ //ih._isAnImgSelected('IE8 ASYNC `_selectImage` (before select)'); // for debugging
+ ih._wym._selectSingleNode(img);
+ //ih._isAnImgSelected('IE8 ASYNC `_selectImage` (after select)'); // for debugging
+ }, 0);
+ } else {
+ //ih._isAnImgSelected('`_selectImage` (before select)'); // for debugging
+ ih._wym._selectSingleNode(img);
+ //ih._isAnImgSelected('`_selectImage` (after select)'); // for debugging
+ }
+
+ ih._setImgCursor($img);
+};
+
+WYMeditor.ImageHandler.prototype._indicateOnResizeHandleThatImageIsSelected = function () {
+ var ih = this;
+
+ var indication = 'image is selected';
+ if (ih._imgDragDropAllowed) {
+ indication = [
+ indication,
+ 'drag image to move it'
+ ].join(WYMeditor.ImageHandler._RESIZE_HANDLE_HR_HTML);
+ }
+
+ ih._$resizeHandle
+ .css('font-weight', 'bold')
+ .html(indication);
+
+ // ideally, the above indication text would remain
+ // until the image is no longer selected.
+ // since it is not easy to detect when that happens,
+ // the indication text is replaced with the initial text
+ // after a short moment.
+ setTimeout(function () {
+ ih._$resizeHandle
+ .css('font-weight', 'normal')
+ .html(WYMeditor.ImageHandler._RESIZE_HANDLE_INNER_HTML);
+ }, 1000);
+};
+
+WYMeditor.ImageHandler.prototype._placeResizeHandleOnImg = function (img) {
+ var ih = this;
+ var IMAGE_PADDING = '0.8em';
+ var $img = jQuery(img);
+
+ ih._$currentImg = $img;
+
+ ih._getCurrentImageMarker().insertAfter($img);
+
+ // colored padding around the image and the handle
+ // visually marks the image
+ // that currently has the resize handle placed on it.
+ // it also makes it possible to resize very small images
+ // (see the `_detachResizeHandle` method)
+ $img.css({
+ 'background-color': WYMeditor.ImageHandler._IMAGE_HIGHLIGHT_COLOR,
+
+ 'padding-top': IMAGE_PADDING,
+ 'padding-right': IMAGE_PADDING,
+ 'padding-bottom': '0',
+ 'padding-left': IMAGE_PADDING,
+ 'margin-top': '-' + IMAGE_PADDING,
+ 'margin-right': '-' + IMAGE_PADDING,
+ 'margin-bottom': '0',
+ 'margin-left': '-' + IMAGE_PADDING
+ });
+
+ // the resize handle, prepended to the body in this way,
+ // can be removed from the body using DOM manipulation
+ // such as setting the content with the `html` method.
+ // so we place it there in case that occurred.
+ // this could be done conditionally
+ // but there is practically no performance hit so keeping it simple
+ ih._$resizeHandle.prependTo(ih._wym.$body());
+
+ // it is important that the resize handle's offset
+ // is updated after the above style modification
+ // adds top padding to the image
+ // because that alters the image's outside height
+ ih._correctResizeHandleOffsetAndWidth();
+
+ ih._$resizeHandle.show();
+};
+
+WYMeditor.ImageHandler.prototype._correctResizeHandleOffsetAndWidth = function () {
+ var ih = this;
+
+ ih._$resizeHandle.css('max-width', ih._$currentImg.outerWidth());
+
+ var offset = ih._$currentImg.offset();
+
+ ih._$resizeHandle.css('left', offset.left);
+
+ // the Y position of the first pixel after the image's outer Y dimension.
+ // in other words, just below the image's margin (if it had a margin)
+ var yAfterImg = offset.top + ih._$currentImg.outerHeight();
+
+ if (jQuery.browser.msie) {
+ // in IE8-11 there might be a visible 1 pixes gap
+ // between the image and the resize handle
+ // possibly this issue:
+ // https://github.com/jquery/jquery/issues/1724
+ yAfterImg--;
+ }
+
+ ih._$resizeHandle.css('top', yAfterImg);
+};
+
+WYMeditor.ImageHandler.prototype._onResizeHandleMousedown = function (evt) {
+ var ih = this;
+
+ if (!ih._resizingNow) {
+ ih._startResize(evt.clientY);
+ }
+ return false;
+};
+
+WYMeditor.ImageHandler.prototype._startResize = function (startMouseY) {
+ var ih = this;
+
+ ih._startMouseY = startMouseY;
+ ih._$currentImg.data('StartHeight', ih._$currentImg.attr('height'));
+ ih._resizingNow = true;
+};
+
+WYMeditor.ImageHandler.prototype._onMousemove = function (evt) {
+ var ih = this;
+
+ if (!evt.target.tagName) {
+ // IE8 may fire such an event.
+ // what element was it fired on?
+ return false;
+ }
+
+ if (ih._resizingNow) {
+ // this is up high in this method for performance
+ ih._resizeImage(evt.clientY);
+ return false;
+ }
+
+ if (
+ evt.target.tagName.toLowerCase() === 'img' &&
+ !ih._isResizeHandleAttached()
+ ) {
+ ih._placeResizeHandleOnImg(evt.target);
+ return false;
+ }
+
+ if (!ih._isResizeHandleAttached()) {
+ return false;
+ }
+
+ // from this point on, this event handler is all about
+ // checking whether the resize handle should be detached
+
+ if (
+ !jQuery(evt.target).hasClass(WYMeditor.EDITOR_ONLY_CLASS) &&
+ !ih._isCurrentImg(evt.target)
+ ) {
+ // this must be after the above check for whether resizing now
+ // because, while the resize operation does begin
+ // with the mouse pointing on the resize handle,
+ // the mouse might leave the resize handle during the resize operation.
+ // in that case, we would like the operation to continue
+ ih._detachResizeHandle();
+ return false;
+ }
+
+ if (!ih._isCurrentImgAtMarker()) {
+ ih._detachResizeHandle();
+ return false;
+ }
+
+ // returning false here would disable image dragging
+};
+
+WYMeditor.ImageHandler.prototype._isCurrentImgAtMarker = function () {
+ var ih = this;
+ var $marker = ih._$currentImageMarker;
+ if (!$marker.length) {
+ // the marker was removed by some DOM manipulation
+ return false;
+ }
+ var $img = ih._$currentImg;
+ var $imgPrevToMarker = $marker.prev('img');
+ if (
+ $img.length &&
+ $imgPrevToMarker.length &&
+ $imgPrevToMarker[0] === $img[0]
+ ) {
+ return true;
+ }
+ // this happens when:
+ //
+ // * the image was selected and
+ // * replaced by pasted content
+ // * replaced by character insertion from key press
+ // * removed with backspace/delete
+ // * caret was before/after image and delete/backspace pressed
+ // * the image was dragged and dropped somewhere
+ return false;
+};
+
+WYMeditor.ImageHandler.prototype._isResizeHandle = function (elem) {
+ return jQuery(elem).hasClass(WYMeditor.RESIZE_HANDLE_CLASS);
+};
+
+WYMeditor.ImageHandler.prototype._isCurrentImg = function (img) {
+ var ih = this;
+ return img === ih._$currentImg[0];
+};
+
+WYMeditor.ImageHandler.prototype._resizeImage = function (currentMouseY) {
+ var ih = this;
+ var $img = ih._$currentImg;
+
+ var dimensionsRatio = $img.data('DimensionsRatio');
+
+ if (!dimensionsRatio) {
+ // in order to prevent dimensions ratio corruption
+ var originalHeight = $img.attr('height');
+ var originalWidth = $img.attr('width');
+ dimensionsRatio = originalWidth / originalHeight;
+ $img.data('DimensionsRatio', dimensionsRatio);
+ }
+
+ // calculate the new dimensions
+ var startHeight = $img.data('StartHeight');
+ var newHeight = startHeight - ih._startMouseY + currentMouseY;
+ newHeight = newHeight > 0 ? newHeight : 0;
+ var newWidth = newHeight * dimensionsRatio;
+
+ // update the dimensions
+ $img.attr('height', newHeight);
+ $img.attr('width', newWidth);
+
+ ih._correctResizeHandleOffsetAndWidth();
+};
+
+WYMeditor.ImageHandler.prototype._onMouseup = function () {
+ var ih = this;
+
+ if (ih._resizingNow) {
+ ih._stopResize();
+ }
+ return false;
+};
+
+WYMeditor.ImageHandler.prototype._stopResize = function () {
+ var ih = this;
+
+ ih._resizingNow = false;
+ ih._startMouseY = null;
+ ih._wym.registerModification();
+};
+
+WYMeditor.ImageHandler.prototype._onImgMousedown = function (evt) {
+ var ih = this;
+
+ if (jQuery.browser.msie && jQuery.browser.versionNumber === 11) {
+ // IE11 on image mousedown places native resize handles around the image.
+ // selecting the image both here and on `click` refrains from those handles
+ // completely and seemingly without side effects.
+ ih._selectImage(evt.target);
+ // another way to refrain from the handles is preventing default.
+ // but that would have an undesired side effect
+ // of not allowing dragging and dropping of images.
+ }
+
+ // returning false here prevents drag of image
+ return ih._imgDragDropAllowed;
+};
+
+WYMeditor.ImageHandler.prototype._onAnyNativeEdit = function () {
+ var ih = this;
+ // modifications possibly not have occurred yet.
+ // schedule immediate async in order to execute
+ // after the possible modifications may have occurred
+ setTimeout(ih._handlePossibleModification.bind(ih), 0);
+};
+
+WYMeditor.ImageHandler.prototype._handlePossibleModification = function () {
+ var ih = this;
+
+ if (!ih._isResizeHandleAttached()) {
+ return;
+ }
+
+ if (!ih._isCurrentImgAtMarker()) {
+ ih._detachResizeHandle();
+ return;
+ }
+
+ // any edit to the document might result
+ // in the image ending up in a different position than before.
+ // for example, inserting a character before the image
+ // pushes it to the right.
+ ih._correctResizeHandleOffsetAndWidth();
+};
+
+WYMeditor.ImageHandler.prototype._isResizeHandleAttached = function () {
+ var ih = this;
+ var $handle = ih._getResizeHandle();
+ return $handle && $handle.css('display') !== 'none';
+};
+
+WYMeditor.ImageHandler.prototype._getResizeHandle = function () {
+ var ih = this;
+ var $handle = ih._wym.$body().find('.' + WYMeditor.RESIZE_HANDLE_CLASS);
+ return $handle.length ? $handle : false;
+};
+
+WYMeditor.ImageHandler.prototype._detachResizeHandle = function () {
+ var ih = this;
+
+ ih._$currentImageMarker.detach();
+ if (
+ // the size of the image might be so small,
+ // that it would be hard to mouse over it
+ // in order to make the resize handle appear.
+ // in that case (an arbitrary number of pixels)
+ // leave the padding on, as it will allow
+ // easy mouse over the image,
+ // even when the image is 0 in size
+ ih._$currentImg.attr('height') >= 16 &&
+ ih._$currentImg.attr('width') >= 16
+ ) {
+ ih._$currentImg.css({padding: 0, margin: 0});
+ }
+ ih._$currentImg = null;
+ ih._$resizeHandle.hide();
+};
+
+WYMeditor.ImageHandler.prototype._onImgDragstart = function () {
+ var ih = this;
+ ih._detachResizeHandle();
+};
+
+WYMeditor.ImageHandler.prototype._onResizeHandleClickDblclick = function () {
+ var ih = this;
+
+ if (jQuery.browser.msie && jQuery.browser.versionNumber === 11) {
+ // in IE11 some mouse events on the resize handle
+ // result in native resize handles on it (eight small squares around it).
+ // trying to resize the resize handle using these native handles
+ // fails quite gracefully, as they seem to have no effect at all.
+ // it fails most likely due to prevented native actions
+ // in one of our event handlers.
+ // however, deselecting here completely prevents these handles
+ ih._wym.deselect();
+ }
+ // prevents entering edit mode in the handle
+ return false;
+};
+
+// useful for debugging
+WYMeditor.ImageHandler.prototype._isAnImgSelected = function (message) {
+ var ih = this;
+ message = message.toUpperCase();
+
+ function check(prefix) {
+ var result = ih._wym.getSelectedImage() ? '***YES***' : '';
+ prefix = prefix ? prefix + ' ' : '';
+ WYMeditor.console.log(prefix + message + ' ' + result);
+ }
+
+ check('sync');
+
+ setTimeout(function () {
+ check('async');
+ }, 0);
+};
+
+// for debugging
+WYMeditor.ImageHandler._onAllEvents = function (evt) {
+ var ih = this;
+
+ ih._isAnImgSelected([
+ evt.type,
+ evt.target.tagName,
+ jQuery(evt.target).attr('className')
+ ].join(' '));
+};
diff --git a/src/wymeditor/editor/trident-7.js b/src/wymeditor/editor/trident-7.js
index f4782d57..898b6c49 100644
--- a/src/wymeditor/editor/trident-7.js
+++ b/src/wymeditor/editor/trident-7.js
@@ -51,9 +51,6 @@ WYMeditor.WymClassTrident7.prototype._docEventQuirks = function () {
jQuery(wym._doc).bind("keyup", function (evt) {
wym._keyup(evt);
});
- jQuery(wym._doc).bind("click", function (evt) {
- wym._click(evt);
- });
// https://github.com/wymeditor/wymeditor/pull/641
wym.$body().bind("dragend", function (evt) {
diff --git a/src/wymeditor/editor/trident-pre-7.js b/src/wymeditor/editor/trident-pre-7.js
index 82c80f69..0e046d5c 100644
--- a/src/wymeditor/editor/trident-pre-7.js
+++ b/src/wymeditor/editor/trident-pre-7.js
@@ -62,30 +62,12 @@ WYMeditor.WymClassTridentPre7.prototype._docEventQuirks = function () {
wym.deselect();
}
});
-};
-
-WYMeditor.WymClassTridentPre7.prototype._mouseup = function (evt) {
- var wym = this;
-
- if (evt.target.tagName.toLowerCase() !== WYMeditor.IMG) {
- return;
- }
- // In other browsers, where the object resize handles can be disabled,
- // this doesn't have to be wrapped in `setTimeout`. In pre-7 Trident, the
- // resize handles can't be disabled.
- // The resize handles are called by the `controlselect` event, which is
- // synchronously triggered after the `mouseup` event. Thus, whatever
- // selection we make in `mouseup` will be overridden by `controlselect`'s
- // undesired resize handles.
- // Wrapping the selection call in an immediate `setTimeout` makes
- // reasonably certain that the "control selection" will be very quickly
- // replaced by our desired, regular selection.
- // For more inforamtion, see:
- // https://github.com/wymeditor/wymeditor/pull/641
- window.setTimeout(function () {
- wym._selectSingleNode(evt.target);
- }, 0);
+ wym._doc.oncontrolselect = function () {
+ // this prevents resize handles on various element
+ // such as images, at least in IE8
+ return false;
+ };
};
WYMeditor.WymClassTridentPre7.prototype._setButtonsUnselectable = function () {
diff --git a/src/wymeditor/iframe/default/wymiframe.css b/src/wymeditor/iframe/default/wymiframe.css
index ec70183b..a6b6ae03 100644
--- a/src/wymeditor/iframe/default/wymiframe.css
+++ b/src/wymeditor/iframe/default/wymiframe.css
@@ -38,7 +38,6 @@ blockquote {
margin-left: 30px;
}
img {
- margin-right: 5px;
border-style: solid;
border-color: gray;
border-width: 0;
diff --git a/src/wymeditor/iframe/legacy/wymiframe.css b/src/wymeditor/iframe/legacy/wymiframe.css
index ec70183b..a6b6ae03 100644
--- a/src/wymeditor/iframe/legacy/wymiframe.css
+++ b/src/wymeditor/iframe/legacy/wymiframe.css
@@ -38,7 +38,6 @@ blockquote {
margin-left: 30px;
}
img {
- margin-right: 5px;
border-style: solid;
border-color: gray;
border-width: 0;
diff --git a/src/wymeditor/iframe/pretty/wymiframe.css b/src/wymeditor/iframe/pretty/wymiframe.css
index 99841b77..5120b73b 100644
--- a/src/wymeditor/iframe/pretty/wymiframe.css
+++ b/src/wymeditor/iframe/pretty/wymiframe.css
@@ -86,8 +86,7 @@
/* specific HTML elements */
caption { text-align: left; }
- img { margin-right: 5px;
- border-style: solid;
+ img { border-style: solid;
border-color: gray;
border-width: 0; }
a img { border-width: 1px; border-color: blue; }
diff --git a/src/wymeditor/parser/xhtml-validator.js b/src/wymeditor/parser/xhtml-validator.js
index c4e4fd07..2a91364d 100644
--- a/src/wymeditor/parser/xhtml-validator.js
+++ b/src/wymeditor/parser/xhtml-validator.js
@@ -21,13 +21,21 @@ WYMeditor.XhtmlValidator = {
"attributes":[
"class",
"id",
- "style",
"title",
"accesskey",
"tabindex",
"/^data-.*/"
]
},
+ "styleAttr":
+ {
+ "except":[
+ "img"
+ ],
+ "attributes":[
+ "style"
+ ]
+ },
"language":
{
"except":[