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:

', - 'Flag of Indiana', + '

Flag of Indiana

', '

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":[