From 9b0b5fab24db9f435b278ad0b9e77d5895135700 Mon Sep 17 00:00:00 2001 From: Alex Forcier Date: Wed, 23 Mar 2016 10:28:33 -0400 Subject: [PATCH 1/4] Squashed 'libs/editor/' changes from eb8d070..bed2a4e bed2a4e Merge pull request #326 from wordpress-mobile/issue/314-html-mode-toggle-cursor 7b5bdb4 Force cursor to move to start of content field when switching back from HTML mode dca8254 Merge pull request #3896 from wordpress-mobile/issue/300editor-broken-images-after-upload-2 ffeec5f remove debug logs d695c87 Use remoteurl in the link wrapper 373467f Merge branch 'develop' into issue/300editor-broken-images-after-upload-2 1dd8be0 Merge branch 'develop' into issue/297editor-backspace-media bf525c8 update to com.android.tools.build:gradle:2.0.0-beta7 196f82c Merge branch 'develop' into issue/297editor-backspace-media c576833 Merge commit '8db246f15ce6f4d2c7f7f7ec51c68b87e9a66c2f' into develop ddcb207 Changed MutationObserver handling to check if the WebView supports it, rather than rely on API levels d79bc00 Refactor: grouped mutation observation methods together eeb6373 Refactored DOM element mutation listening, delegating everything to one trackNodeForMutation method 7b54573 Changed MutationObserver behavior to track individual media nodes instead of each contenteditable div a933e5b Moved failed media methods to the generic media method group cfb836b Parse for failed media when returning from HTML to visual mode c0df468 Track DOMNodeRemoved events when parsing for failed media 839b3e5 Fixed a variable name error in ZSSEditor.removeImage 392e1d7 On API<19, use DOMNodeRemoved events to track media deletions (instead of the unsupported MutationObserver used for newer APIs) 5c3bb59 Merge branch 'develop' into issue/297editor-backspace-media 4c7ca43 Merge pull request #3804 from wordpress-mobile/issue/enable-editor-debug-mode cc332a9 Consume KEYCODE_VOLUME_UP event when debug print is called cf492d0 broken retries 449abbd Merge branch 'issue/enable-editor-debug-mode' into issue/300editor-broken-images-after-upload-2 63bb901 use a remoteUrl attribute to avoid seeing broken image if download failed 55677b6 remove debug action bar button and log raw html when volume up button is pressed 7ba5069 fix function call errors 886e274 Add back image swapping onError 920c8d3 fix wordpress-mobile/WordPress-Editor-Android#300: Retry download onError after an upload 7fa191f add missing comment 01fbeed Fixes an issue where manually deleting uploading/failed media will cause the caret to disappear 5904f03 Notify native through a callback whenever uploading/failed media are manually deleted git-subtree-dir: libs/editor git-subtree-split: bed2a4ef1ea0a86e4237791da8e867f17590c5b3 --- WordPressEditor/build.gradle | 2 +- .../android/editor/EditorFragment.java | 51 +-- .../android/editor/EditorWebViewAbstract.java | 13 + .../android/editor/JsCallbackReceiver.java | 8 + .../OnJsEditorStateChangedListener.java | 1 + .../main/res/drawable-hdpi/ic_log_html.png | Bin 180 -> 0 bytes .../main/res/drawable-xhdpi/ic_log_html.png | Bin 214 -> 0 bytes .../main/res/drawable-xxhdpi/ic_log_html.png | Bin 295 -> 0 bytes example/build.gradle | 2 +- .../editor-common/assets/ZSSRichTextEditor.js | 332 ++++++++++++++---- 10 files changed, 301 insertions(+), 108 deletions(-) delete mode 100644 WordPressEditor/src/main/res/drawable-hdpi/ic_log_html.png delete mode 100644 WordPressEditor/src/main/res/drawable-xhdpi/ic_log_html.png delete mode 100644 WordPressEditor/src/main/res/drawable-xxhdpi/ic_log_html.png diff --git a/WordPressEditor/build.gradle b/WordPressEditor/build.gradle index a8109cc0c5e3..f47a62fe3b56 100644 --- a/WordPressEditor/build.gradle +++ b/WordPressEditor/build.gradle @@ -3,7 +3,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.0.0-beta6' + classpath 'com.android.tools.build:gradle:2.0.0-beta7' } } diff --git a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java index d5145c6679e5..65c7c9869759 100755 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java @@ -19,9 +19,6 @@ import android.text.Spanned; import android.text.TextUtils; import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; @@ -74,8 +71,6 @@ public class EditorFragment extends EditorFragmentAbstract implements View.OnCli private static final float TOOLBAR_ALPHA_ENABLED = 1; private static final float TOOLBAR_ALPHA_DISABLED = 0.5f; - protected static final int BUTTON_ID_LOG_HTML = 555; - private String mTitle = ""; private String mContentHtml = ""; @@ -409,8 +404,6 @@ protected void initJsEditor() { if (mDebugModeEnabled) { enableWebDebugging(true); - // Enable the HTML logging button - setHasOptionsMenu(true); } } @@ -457,7 +450,11 @@ public void onClick(View v) { mContentHtml = mSourceViewContent.getText().toString(); updateVisualEditorFields(); - mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').focus();"); + // Update the list of failed media uploads + mWebView.execJavaScriptFromString("ZSSEditor.getFailedMedia();"); + + // Reset selection to avoid buggy cursor behavior + mWebView.execJavaScriptFromString("ZSSEditor.resetSelectionOnField('zss_field_content');"); } } else if (id == R.id.format_bar_button_media) { mEditorFragmentListener.onTrackableEvent(TrackableEvent.MEDIA_BUTTON_TAPPED); @@ -640,38 +637,11 @@ public void run() { @SuppressLint("NewApi") private void enableWebDebugging(boolean enable) { - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { AppLog.i(T.EDITOR, "Enabling web debugging"); WebView.setWebContentsDebuggingEnabled(enable); } - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - menu.add(0, BUTTON_ID_LOG_HTML, 0, "Log HTML") - .setIcon(R.drawable.ic_log_html) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - super.onCreateOptionsMenu(menu, inflater); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == BUTTON_ID_LOG_HTML) { - if (mDebugModeEnabled) { - // Log the raw html - mWebView.post(new Runnable() { - @Override - public void run() { - mWebView.execJavaScriptFromString("console.log(document.body.innerHTML);"); - } - }); - } else { - AppLog.d(T.EDITOR, "Could not execute JavaScript - debug mode not enabled"); - } - return true; - } else { - return super.onOptionsItemSelected(item); - } + mWebView.setDebugModeEnabled(mDebugModeEnabled); } @Override @@ -1191,6 +1161,13 @@ public void onLinkTapped(String url, String title) { linkDialogFragment.show(getFragmentManager(), "LinkDialogFragment"); } + @Override + public void onMediaRemoved(String mediaId) { + mUploadingMedia.remove(mediaId); + mFailedMediaIds.remove(mediaId); + mEditorFragmentListener.onMediaUploadCancelClicked(mediaId, true); + } + @Override public void onMediaReplaced(String mediaId) { mUploadingMedia.remove(mediaId); diff --git a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewAbstract.java b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewAbstract.java index afdfd4508278..7b94adfe3250 100644 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewAbstract.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewAbstract.java @@ -23,6 +23,7 @@ import org.wordpress.android.util.AppLog.T; import org.wordpress.android.util.HTTPUtils; import org.wordpress.android.util.StringUtils; +import org.wordpress.android.util.ToastUtils; import org.wordpress.android.util.UrlUtils; import java.io.IOException; @@ -40,6 +41,7 @@ public abstract class EditorWebViewAbstract extends WebView { private AuthHeaderRequestListener mAuthHeaderRequestListener; private ErrorListener mErrorListener; private JsCallbackReceiver mJsCallbackReceiver; + private boolean mDebugModeEnabled; private Map mHeaderMap = new HashMap<>(); @@ -175,6 +177,10 @@ public void setVisibility(int visibility) { super.setVisibility(visibility); } + public void setDebugModeEnabled(boolean enabled) { + mDebugModeEnabled = enabled; + } + /** * Handles events that should be triggered when the WebView is hidden or is shown to the user * @@ -214,6 +220,13 @@ public boolean onKeyPreIme(int keyCode, KeyEvent event) { mOnImeBackListener.onImeBack(); } } + if (mDebugModeEnabled && event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_UP + && event.getAction() == KeyEvent.ACTION_DOWN) { + // Log the raw html + execJavaScriptFromString("console.log(document.body.innerHTML);"); + ToastUtils.showToast(getContext(), "Debug: Raw HTML has been logged"); + return true; + } return super.onKeyPreIme(keyCode, event); } diff --git a/WordPressEditor/src/main/java/org/wordpress/android/editor/JsCallbackReceiver.java b/WordPressEditor/src/main/java/org/wordpress/android/editor/JsCallbackReceiver.java index 9f0cb30b3ab1..f7ca4c0914de 100755 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/JsCallbackReceiver.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/JsCallbackReceiver.java @@ -32,6 +32,7 @@ public class JsCallbackReceiver { private static final String CALLBACK_VIDEO_REPLACED = "callback-video-replaced"; private static final String CALLBACK_IMAGE_TAP = "callback-image-tap"; private static final String CALLBACK_LINK_TAP = "callback-link-tap"; + private static final String CALLBACK_MEDIA_REMOVED = "callback-media-removed"; private static final String CALLBACK_VIDEOPRESS_INFO_REQUEST = "callback-videopress-info-request"; @@ -179,6 +180,13 @@ public void executeCallback(String callbackId, String params) { mListener.onLinkTapped(url, title); break; + case CALLBACK_MEDIA_REMOVED: + AppLog.d(AppLog.T.EDITOR, "Media removed, " + params); + // Extract the media id from the callback string (stripping the 'id=' part of the callback string) + if (params.length() > 3) { + mListener.onMediaRemoved(params.substring(3)); + } + break; case CALLBACK_VIDEOPRESS_INFO_REQUEST: // Extract the VideoPress id from the callback string (stripping the 'id=' part of the callback string) if (params.length() > 3) { diff --git a/WordPressEditor/src/main/java/org/wordpress/android/editor/OnJsEditorStateChangedListener.java b/WordPressEditor/src/main/java/org/wordpress/android/editor/OnJsEditorStateChangedListener.java index bcf332857fbe..a412581901cb 100755 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/OnJsEditorStateChangedListener.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/OnJsEditorStateChangedListener.java @@ -12,6 +12,7 @@ public interface OnJsEditorStateChangedListener { void onSelectionStyleChanged(Map changeSet); void onMediaTapped(String mediaId, MediaType mediaType, JSONObject meta, String uploadStatus); void onLinkTapped(String url, String title); + void onMediaRemoved(String mediaId); void onMediaReplaced(String mediaId); void onVideoPressInfoRequested(String videoId); void onGetHtmlResponse(Map responseArgs); diff --git a/WordPressEditor/src/main/res/drawable-hdpi/ic_log_html.png b/WordPressEditor/src/main/res/drawable-hdpi/ic_log_html.png deleted file mode 100644 index e8f96367c8751a93a9e21dd2cf0e0f99b833641a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 180 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8wWo_?NCo5Di-vqm3IZ+{*FMRT z%l3C*Qh(%@k#2tNNX_AptSA1ipPe>+RA0Fwv&)M$zt(K8e!1emvlp6IhIe?!&iu4M z;Z*yCPl`)wI5gy$PPK=fYB!m3x{be(o$JPX=jU1T+s|EnS2T;K)H1ZKYggXv{VR7F eq`cd`P(5M&c|+fuuChR9FnGH9xvXUoe8A{&)H)RJQcKPI`!t@1*uLj5cgsMmLLe~L4BUp)6BjxhP=Ok18; when the WebView doesn't recognize the MutationObserver, +// we fall back to the deprecated DOMNodeRemoved event +ZSSEditor.mutationObserver; + +ZSSEditor.defaultMutationObserverConfig = { attributes: false, childList: true, characterData: false }; + /** * The initializer function that must be called onLoad */ @@ -111,6 +118,15 @@ ZSSEditor.init = function() { } }, false); + // Attempt to instantiate a MutationObserver. This should fail for API<19, unless the OEM of the device has + // modified the WebView. If it fails, the editor will fall back to DOMNodeRemoved events. + try { + ZSSEditor.mutationObserver = new MutationObserver(function(mutations) { + ZSSEditor.onMutationObserved(mutations);} ); + } catch(e) { + // no op + } + }; //end // MARK: - Debugging logs @@ -193,7 +209,66 @@ ZSSEditor.execFunctionForResult = function(methodName) { var functionArgument = "function=" + methodName; var resultArgument = "result=" + window["ZSSEditor"][methodName].apply(); ZSSEditor.callback('callback-response-string', functionArgument + defaultCallbackSeparator + resultArgument); -} +}; + +// MARK: - Mutation observing + +/** + * @brief Register a node to be tracked for modifications + */ +ZSSEditor.trackNodeForMutation = function(target) { + if (ZSSEditor.mutationObserver != undefined) { + ZSSEditor.mutationObserver.observe(target[0], ZSSEditor.defaultMutationObserverConfig); + } else { + // The WebView doesn't support MutationObservers - fall back to DOMNodeRemoved events + target.bind("DOMNodeRemoved", function(event) { ZSSEditor.onDomNodeRemoved(event); }); + } +}; + +/** + * @brief Called when the MutationObserver registers a mutation to a node it's listening to + */ +ZSSEditor.onMutationObserved = function(mutations) { + mutations.forEach(function(mutation) { + for (var i = 0; i < mutation.removedNodes.length; i++) { + var removedNode = mutation.removedNodes[i]; + if (ZSSEditor.isMediaContainerNode(removedNode)) { + // An uploading or failed container node was deleted manually - notify native + var mediaIdentifier = ZSSEditor.extractMediaIdentifier(removedNode); + ZSSEditor.sendMediaRemovedCallback(mediaIdentifier); + } else if (removedNode.attributes.getNamedItem("data-wpid")) { + // An uploading or failed image was deleted manually - remove its container and send the callback + var mediaIdentifier = removedNode.attributes.getNamedItem("data-wpid").value; + var parentRange = ZSSEditor.getParentRangeOfFocusedNode(); + ZSSEditor.removeImage(mediaIdentifier); + ZSSEditor.setRange(parentRange); + ZSSEditor.sendMediaRemovedCallback(mediaIdentifier); + } else if (removedNode.attributes.getNamedItem("data-video_wpid")) { + // An uploading or failed video was deleted manually - remove its container and send the callback + var mediaIdentifier = removedNode.attributes.getNamedItem("data-video_wpid").value; + var parentRange = ZSSEditor.getParentRangeOfFocusedNode(); + ZSSEditor.removeVideo(mediaIdentifier); + ZSSEditor.setRange(parentRange); + ZSSEditor.sendMediaRemovedCallback(mediaIdentifier); + } + } + }); +}; + +/** + * @brief Called when a DOMNodeRemoved event is triggered for an element we're tracking + * (only used when MutationObserver is unsupported by the WebView) + */ +ZSSEditor.onDomNodeRemoved = function(event) { + if (event.target.id.length > 0) { + var mediaId = ZSSEditor.extractMediaIdentifier(event.target); + } else if (event.target.parentNode.id.length > 0) { + var mediaId = ZSSEditor.extractMediaIdentifier(event.target.parentNode); + } else { + return; + } + ZSSEditor.sendMediaRemovedCallback(mediaId); +}; // MARK: - Logging @@ -301,6 +376,18 @@ ZSSEditor.restoreRange = function(){ } }; +ZSSEditor.resetSelectionOnField = function(fieldId) { + var query = "div#" + fieldId; + var field = document.querySelector(query); + var range = document.createRange(); + range.setStart(field, 0); + range.setEnd(field, 0); + + var selection = document.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); +}; + ZSSEditor.getSelectedText = function() { var selection = window.getSelection(); @@ -817,6 +904,91 @@ ZSSEditor.turnBlockquoteOnForNode = function(node) { } }; +// MARK: - Generic media + +ZSSEditor.isMediaContainerNode = function(node) { + if (node.id === undefined) { + return false; + } + return (node.id.search("img_container_") == 0) || (node.id.search("video_container_") == 0); +}; + +ZSSEditor.extractMediaIdentifier = function(node) { + if (node.id.search("img_container_") == 0) { + return node.id.replace("img_container_", ""); + } else if (node.id.search("video_container_") == 0) { + return node.id.replace("video_container_", ""); + } + return ""; +}; + +ZSSEditor.getMediaContainerNodeWithIdentifier = function(mediaNodeIdentifier) { + var imageContainerNode = ZSSEditor.getImageContainerNodeWithIdentifier(mediaNodeIdentifier); + if (imageContainerNode.length > 0) { + return imageContainerNode; + } else { + return ZSSEditor.getVideoContainerNodeWithIdentifier(mediaNodeIdentifier); + } +}; + +ZSSEditor.sendMediaRemovedCallback = function(mediaNodeIdentifier) { + var arguments = ['id=' + encodeURIComponent(mediaNodeIdentifier)]; + var joinedArguments = arguments.join(defaultCallbackSeparator); + this.callback("callback-media-removed", joinedArguments); +}; + +/** + * @brief Marks all in-progress images as failed to upload + */ +ZSSEditor.markAllUploadingMediaAsFailed = function(message) { + var html = ZSSEditor.getField("zss_field_content").getHTML(); + var tmp = document.createElement( "div" ); + var tmpDom = $( tmp ).html( html ); + var matches = tmpDom.find("img.uploading"); + + for(var i = 0; i < matches.size(); i++) { + if (matches[i].hasAttribute('data-wpid')) { + var mediaId = matches[i].getAttribute('data-wpid'); + ZSSEditor.markImageUploadFailed(mediaId, message); + } else if (matches[i].hasAttribute('data-video_wpid')) { + var videoId = matches[i].getAttribute('data-video_wpid'); + ZSSEditor.markVideoUploadFailed(videoId, message); + } + } +}; + +/** + * @brief Sends a callback with a list of failed images + */ +ZSSEditor.getFailedMedia = function() { + var html = ZSSEditor.getField("zss_field_content").getHTML(); + var tmp = document.createElement( "div" ); + var tmpDom = $( tmp ).html( html ); + var matches = tmpDom.find("img.failed"); + + var functionArgument = "function=getFailedMedia"; + var mediaIdArray = []; + + for (var i = 0; i < matches.size(); i++) { + var mediaId; + if (matches[i].hasAttribute("data-wpid")) { + mediaId = matches[i].getAttribute("data-wpid"); + } else if (matches[i].hasAttribute("data-video_wpid")) { + mediaId = matches[i].getAttribute("data-video_wpid"); + } + + // Track pre-existing failed media nodes for manual deletion events + ZSSEditor.trackNodeForMutation(this.getMediaContainerNodeWithIdentifier(mediaId)); + + if (mediaId.length > 0) { + mediaIdArray.push(mediaId); + } + } + + var joinedArguments = functionArgument + defaultCallbackSeparator + "ids=" + mediaIdArray.toString(); + ZSSEditor.callback('callback-response-string', joinedArguments); +}; + // MARK: - Images ZSSEditor.updateImage = function(url, alt) { @@ -873,6 +1045,9 @@ ZSSEditor.insertLocalImage = function(imageNodeIdentifier, localImageUrl) { var html = imgContainerStart + progressElement + image + imgContainerEnd; this.insertHTML(this.wrapInParagraphTags(html)); + + ZSSEditor.trackNodeForMutation(this.getImageContainerNodeWithIdentifier(imageNodeIdentifier)); + this.sendEnabledStyles(); }; @@ -919,29 +1094,54 @@ ZSSEditor.replaceLocalImageWithRemoteImage = function(imageNodeIdentifier, remot var image = new Image; image.onload = function () { - imageNode.attr('src', image.src); - imageNode.addClass("wp-image-" + remoteImageId); - ZSSEditor.markImageUploadDone(imageNodeIdentifier); - var joinedArguments = ZSSEditor.getJoinedFocusedFieldIdAndCaretArguments(); - ZSSEditor.callback("callback-input", joinedArguments); - + ZSSEditor.finishLocalImageSwap(image, imageNode, imageNodeIdentifier, remoteImageId) + image.classList.add("image-loaded"); + console.log("Image Loaded!"); } image.onerror = function () { - // Even on an error, we swap the image for the time being. This is because private - // blogs are currently failing to download images due to access privilege issues. - // - imageNode.attr('src', image.src); - imageNode.addClass("wp-image-" + remoteImageId); - ZSSEditor.markImageUploadDone(imageNodeIdentifier); - var joinedArguments = ZSSEditor.getJoinedFocusedFieldIdAndCaretArguments(); - ZSSEditor.callback("callback-input", joinedArguments); - + // Add a remoteUrl attribute, remoteUrl and src must be swapped before publishing. + image.setAttribute('remoteurl', image.src); + // Try to reload the image on error. + ZSSEditor.tryToReload(image, imageNode, imageNodeIdentifier, remoteImageId, 1); } image.src = remoteImageUrl; }; +ZSSEditor.finishLocalImageSwap = function(image, imageNode, imageNodeIdentifier, remoteImageId) { + imageNode.addClass("wp-image-" + remoteImageId); + if (image.getAttribute("remoteurl")) { + imageNode.attr('remoteurl', image.getAttribute("remoteurl")); + } + imageNode.attr('src', image.src); + ZSSEditor.markImageUploadDone(imageNodeIdentifier); + var joinedArguments = ZSSEditor.getJoinedFocusedFieldIdAndCaretArguments(); + ZSSEditor.callback("callback-input", joinedArguments); + image.onerror = null; +} + +ZSSEditor.reloadImage = function(image, imageNode, imageNodeIdentifier, remoteImageId, nCall) { + if (image.classList.contains("image-loaded")) { + return; + } + image.onerror = ZSSEditor.tryToReload(image, imageNode, imageNodeIdentifier, remoteImageId, nCall + 1); + // Force reloading by updating image src + image.src = image.getAttribute("remoteurl") + "?retry=" + nCall; + console.log("Reloading image:" + nCall + " - " + image.src); +} + +ZSSEditor.tryToReload = function (image, imageNode, imageNodeIdentifier, remoteImageId, nCall) { + if (nCall > 8) { // 7 tries: 22500 ms total + ZSSEditor.finishLocalImageSwap(image, imageNode, imageNodeIdentifier, remoteImageId); + return; + } + image.onerror = null; + console.log("Image not loaded"); + // reload the image with a variable delay: 500ms, 1000ms, 1500ms, 2000ms, etc. + setTimeout(ZSSEditor.reloadImage, nCall * 500, image, imageNode, imageNodeIdentifier, remoteImageId, nCall); +} + /** * @brief Update the progress indicator for the image identified with the value in progress. * @@ -989,12 +1189,15 @@ ZSSEditor.markImageUploadDone = function(imageNodeIdentifier) { // Remove all extra formatting nodes for progress if (imageNode.parent().attr("id") == this.getImageContainerIdentifier(imageNodeIdentifier)) { + // Reset id before removal to avoid triggering the manual media removal callback + imageNode.parent().attr("id", ""); imageNode.parent().replaceWith(imageNode); } // Wrap link around image - var linkTag = ''; - imageNode.wrap(linkTag); - + var link = $('', { href: imageNode.attr("src") } ); + imageNode.wrap(link); + // We invoke the sendImageReplacedCallback with a delay to avoid for + // it to be ignored by the webview because of the previous callback being done. var thisObj = this; setTimeout(function() { thisObj.sendImageReplacedCallback(imageNodeIdentifier);}, 500); }; @@ -1076,55 +1279,6 @@ ZSSEditor.unmarkImageUploadFailed = function(imageNodeIdentifier) { imageContainerNode.find("span.upload-overlay").removeClass("failed"); }; -/** - * @brief Marks all in-progress images as failed to upload - */ -ZSSEditor.markAllUploadingMediaAsFailed = function(message) { - var html = ZSSEditor.getField("zss_field_content").getHTML(); - var tmp = document.createElement( "div" ); - var tmpDom = $( tmp ).html( html ); - var matches = tmpDom.find("img.uploading"); - - for(var i = 0; i < matches.size(); i++) { - if (matches[i].hasAttribute('data-wpid')) { - var mediaId = matches[i].getAttribute('data-wpid'); - ZSSEditor.markImageUploadFailed(mediaId, message); - } else if (matches[i].hasAttribute('data-video_wpid')) { - var videoId = matches[i].getAttribute('data-video_wpid'); - ZSSEditor.markVideoUploadFailed(videoId, message); - } - } -}; - -/** - * @brief Sends a callback with a list of failed images - */ -ZSSEditor.getFailedMedia = function() { - var html = ZSSEditor.getField("zss_field_content").getHTML(); - var tmp = document.createElement( "div" ); - var tmpDom = $( tmp ).html( html ); - var matches = tmpDom.find("img.failed"); - - var functionArgument = "function=getFailedMedia"; - var mediaIdArray = []; - - for (var i = 0; i < matches.size(); i++) { - var mediaId; - if (matches[i].hasAttribute("data-wpid")) { - mediaId = matches[i].getAttribute("data-wpid"); - } else if (matches[i].hasAttribute("data-video_wpid")) { - mediaId = matches[i].getAttribute("data-video_wpid"); - } - - if (mediaId.length > 0) { - mediaIdArray.push(mediaId); - } - } - - var joinedArguments = functionArgument + defaultCallbackSeparator + "ids=" + mediaIdArray.toString(); - ZSSEditor.callback('callback-response-string', joinedArguments); -}; - /** * @brief Remove the image from the DOM. * @@ -1133,6 +1287,8 @@ ZSSEditor.getFailedMedia = function() { ZSSEditor.removeImage = function(imageNodeIdentifier) { var imageNode = this.getImageNodeWithIdentifier(imageNodeIdentifier); if (imageNode.length != 0){ + // Reset id before removal to avoid triggering the manual media removal callback + imageNode.attr("id",""); imageNode.remove(); } @@ -1208,6 +1364,9 @@ ZSSEditor.insertLocalVideo = function(videoNodeIdentifier, posterURL) { var html = videoContainerStart + progressElement + image + videoContainerEnd; this.insertHTML(this.wrapInParagraphTags(html)); + + ZSSEditor.trackNodeForMutation(this.getVideoContainerNodeWithIdentifier(videoNodeIdentifier)); + this.sendEnabledStyles(); }; @@ -1406,7 +1565,7 @@ ZSSEditor.removeVideo = function(videoNodeIdentifier) { // if Video is inside options container we need to remove the container var videoContainerNode = this.getVideoContainerNodeWithIdentifier(videoNodeIdentifier); if (videoContainerNode.length != 0){ - //reset id before removal to avoid detection of user removal + // Reset id before removal to avoid triggering the manual media removal callback videoContainerNode.attr("id",""); videoContainerNode.remove(); } @@ -1630,6 +1789,28 @@ ZSSEditor.removeImageSelectionFormattingFromHTML = function( html ) { return tmpDom.html(); } +ZSSEditor.removeImageRemoteUrl = function(html) { + var tmp = document.createElement("div"); + var tmpDom = $(tmp).html(html); + + var matches = tmpDom.find("img"); + if (matches.length == 0) { + return html; + } + + for (var i = 0; i < matches.length; i++) { + if (matches[i].getAttribute('remoteurl')) { + if (matches[i].parentNode && matches[i].parentNode.href === matches[i].src) { + matches[i].parentNode.href = matches[i].getAttribute('remoteurl') + } + matches[i].src = matches[i].getAttribute('remoteurl'); + matches[i].removeAttribute('remoteurl'); + } + } + + return tmpDom.html(); +} + /** * @brief Finds all related caption nodes for the specified image node. * @@ -2014,6 +2195,7 @@ ZSSEditor.removeCaptionFormattingCallback = function( match, content ) { } // MARK: - Galleries + ZSSEditor.insertGallery = function( imageIds, type, columns ) { var shortcode; if (type) { @@ -2070,6 +2252,7 @@ ZSSEditor.applyVisualFormatting = function( html ) { */ ZSSEditor.removeVisualFormatting = function( html ) { var str = html; + str = ZSSEditor.removeImageRemoteUrl( str ); str = ZSSEditor.removeImageSelectionFormattingFromHTML( str ); str = ZSSEditor.removeCaptionFormatting( str ); str = ZSSEditor.replaceVideoPressVideosForShortcode( str ); @@ -2639,6 +2822,17 @@ ZSSEditor.parentTags = function() { return parentTags; }; +// MARK: - Range handling + +ZSSEditor.getParentRangeOfFocusedNode = function() { + var selection = window.getSelection(); + return selection.getRangeAt(selection.focusNode.parentNode); +}; + +ZSSEditor.setRange = function(range) { + window.getSelection().removeAllRanges(); + window.getSelection().addRange(range); +}; // MARK: - ZSSField Constructor function ZSSField(wrappedObject) { From c6efe0a9190244d40e64300efc9cca56ae5acd5c Mon Sep 17 00:00:00 2001 From: Alex Forcier Date: Thu, 24 Mar 2016 09:08:24 -0400 Subject: [PATCH 2/4] Squashed 'libs/editor/' changes from bed2a4e..f1f6d10 f1f6d10 Merge pull request #325 from wordpress-mobile/issue/fix-rare-crash-when-editorfragment-is-not-attached fd11532 Merge pull request #327 from wordpress-mobile/sync-wpandroid c1991d7 Adjusted expected callbacks in ZssEditorTest to account for different null handling between the two callback methods 2e3b075 Fixed integration tests to listen for the iframe callback method for API<17 8caccda Make placeholder replacements for android-editor.html in integration tests f2bb3f0 Fix unit tests broken by unimplemented EditorFragmentAbstract method c5dbd53 Merge commit '9b0b5fab24db9f435b278ad0b9e77d5895135700' into develop 512d940 Merge pull request #3897 from wordpress-mobile/issue/260editor-clear-failed-images-on-upload ed89332 Null check getParentRangeOfFocusedNode/setRange in onMutationObserved - in case editor is not in focus 414e55f Make sure fragment is added 33215ad New EditorFragment method to removeAllFailedMediaUploads() 6564195 New JS function ZSSEditor.removeAllFailedMediaUploads git-subtree-dir: libs/editor git-subtree-split: f1f6d10e2381ccd4b4d7aabe277ec4b3f41fad9b --- .../ZssEditorTest.java | 39 +++++++++++++--- .../android/editor/EditorFragment.java | 9 ++++ .../editor/EditorFragmentAbstract.java | 1 + .../android/editor/LegacyEditorFragment.java | 3 ++ .../editor/EditorFragmentAbstractTest.java | 5 ++ .../editor-common/assets/ZSSRichTextEditor.js | 46 +++++++++++++------ 6 files changed, 83 insertions(+), 20 deletions(-) diff --git a/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/ZssEditorTest.java b/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/ZssEditorTest.java index 5e337f5d84bd..9989cbd65ac2 100644 --- a/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/ZssEditorTest.java +++ b/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/ZssEditorTest.java @@ -3,6 +3,7 @@ import android.annotation.SuppressLint; import android.app.Activity; import android.app.Instrumentation; +import android.os.Build; import android.test.ActivityInstrumentationTestCase2; import android.webkit.JavascriptInterface; @@ -46,13 +47,31 @@ protected void setUp() throws Exception { mSetUpLatch.countDown(); - final String htmlEditor = Utils.getHtmlFromFile(activity, "android-editor.html"); + String htmlEditor = Utils.getHtmlFromFile(activity, "android-editor.html"); + + if (htmlEditor != null) { + htmlEditor = htmlEditor.replace("%%TITLE%%", getActivity().getString(R.string.visual_editor)); + htmlEditor = htmlEditor.replace("%%ANDROID_API_LEVEL%%", String.valueOf(Build.VERSION.SDK_INT)); + htmlEditor = htmlEditor.replace("%%LOCALIZED_STRING_INIT%%", + "nativeState.localizedStringEdit = '" + getActivity().getString(R.string.edit) + "';\n" + + "nativeState.localizedStringUploading = '" + getActivity().getString(R.string.uploading) + "';\n" + + "nativeState.localizedStringUploadingGallery = '" + + getActivity().getString(R.string.uploading_gallery_placeholder) + "';\n"); + } + + final String finalHtmlEditor = htmlEditor; + activity.runOnUiThread(new Runnable() { @Override public void run() { mWebView = new EditorWebView(mInstrumentation.getContext(), null); - mWebView.addJavascriptInterface(new MockJsCallbackReceiver(), JS_CALLBACK_HANDLER); - mWebView.loadDataWithBaseURL("file:///android_asset/", htmlEditor, "text/html", "utf-8", ""); + if (Build.VERSION.SDK_INT < 17) { + mWebView.setJsCallbackReceiver(new MockJsCallbackReceiver(new EditorFragmentForTests())); + } else { + mWebView.addJavascriptInterface(new MockJsCallbackReceiver(new EditorFragmentForTests()), + JS_CALLBACK_HANDLER); + } + mWebView.loadDataWithBaseURL("file:///android_asset/", finalHtmlEditor, "text/html", "utf-8", ""); mSetUpLatch.countDown(); } }); @@ -74,12 +93,16 @@ public void testInitialization() throws InterruptedException { Set expectedSet = new HashSet<>(); expectedSet.add("callback-new-field:id=zss_field_title"); expectedSet.add("callback-new-field:id=zss_field_content"); - expectedSet.add("callback-dom-loaded:undefined"); + expectedSet.add("callback-dom-loaded:"); assertEquals(expectedSet, mCallbackSet); } - private class MockJsCallbackReceiver { + private class MockJsCallbackReceiver extends JsCallbackReceiver { + public MockJsCallbackReceiver(EditorFragmentAbstract editorFragmentAbstract) { + super(editorFragmentAbstract); + } + @JavascriptInterface public void executeCallback(String callbackId, String params) { if (callbackId.equals("callback-dom-loaded")) { @@ -90,10 +113,12 @@ public void executeCallback(String callbackId, String params) { // Handle callbacks and count down latches according to the currently running test switch(mTestMethod) { case INIT: - if (callbackId.equals("callback-new-field") || callbackId.equals("callback-dom-loaded")) { + if (callbackId.equals("callback-dom-loaded")) { + mCallbackSet.add(callbackId + ":"); + } else if (callbackId.equals("callback-new-field")) { mCallbackSet.add(callbackId + ":" + params); - mCallbackLatch.countDown(); } + mCallbackLatch.countDown(); break; default: throw(new RuntimeException("Unknown calling method")); diff --git a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java index 65c7c9869759..3c8af4143254 100755 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java @@ -821,6 +821,11 @@ public boolean hasFailedMediaUploads() { return (mFailedMediaIds.size() > 0); } + @Override + public void removeAllFailedMediaUploads() { + mWebView.execJavaScriptFromString("ZSSEditor.removeAllFailedMediaUploads();"); + } + @Override public Spanned getSpannedContent() { return null; @@ -929,6 +934,10 @@ public void onDomLoaded() { mWebView.post(new Runnable() { public void run() { + if (!isAdded()) { + return; + } + mDomHasLoaded = true; mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').setMultiline('true');"); diff --git a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java index e4dd55e9babe..09605ec7b0f0 100644 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java @@ -22,6 +22,7 @@ public abstract class EditorFragmentAbstract extends Fragment { public abstract void setUrlForVideoPressId(String videoPressId, String url, String posterUrl); public abstract boolean isUploadingMedia(); public abstract boolean hasFailedMediaUploads(); + public abstract void removeAllFailedMediaUploads(); public abstract void setTitlePlaceholder(CharSequence text); public abstract void setContentPlaceholder(CharSequence text); diff --git a/WordPressEditor/src/main/java/org/wordpress/android/editor/LegacyEditorFragment.java b/WordPressEditor/src/main/java/org/wordpress/android/editor/LegacyEditorFragment.java index b667568271ea..e3d142e96b59 100644 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/LegacyEditorFragment.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/LegacyEditorFragment.java @@ -1143,6 +1143,9 @@ public boolean hasFailedMediaUploads() { return false; } + @Override + public void removeAllFailedMediaUploads() {} + @Override public void setTitlePlaceholder(CharSequence text) { diff --git a/WordPressEditor/src/test/java/org/wordpress/android/editor/EditorFragmentAbstractTest.java b/WordPressEditor/src/test/java/org/wordpress/android/editor/EditorFragmentAbstractTest.java index 2bebebe0aab2..46870bcbd1c0 100644 --- a/WordPressEditor/src/test/java/org/wordpress/android/editor/EditorFragmentAbstractTest.java +++ b/WordPressEditor/src/test/java/org/wordpress/android/editor/EditorFragmentAbstractTest.java @@ -84,6 +84,11 @@ public boolean hasFailedMediaUploads() { return false; } + @Override + public void removeAllFailedMediaUploads() { + + } + @Override public void setTitlePlaceholder(CharSequence text) { diff --git a/libs/editor-common/assets/ZSSRichTextEditor.js b/libs/editor-common/assets/ZSSRichTextEditor.js index 2ddcd1df4af2..dc1caf996d04 100755 --- a/libs/editor-common/assets/ZSSRichTextEditor.js +++ b/libs/editor-common/assets/ZSSRichTextEditor.js @@ -241,14 +241,18 @@ ZSSEditor.onMutationObserved = function(mutations) { var mediaIdentifier = removedNode.attributes.getNamedItem("data-wpid").value; var parentRange = ZSSEditor.getParentRangeOfFocusedNode(); ZSSEditor.removeImage(mediaIdentifier); - ZSSEditor.setRange(parentRange); + if (parentRange != null) { + ZSSEditor.setRange(parentRange); + } ZSSEditor.sendMediaRemovedCallback(mediaIdentifier); } else if (removedNode.attributes.getNamedItem("data-video_wpid")) { // An uploading or failed video was deleted manually - remove its container and send the callback var mediaIdentifier = removedNode.attributes.getNamedItem("data-video_wpid").value; var parentRange = ZSSEditor.getParentRangeOfFocusedNode(); ZSSEditor.removeVideo(mediaIdentifier); - ZSSEditor.setRange(parentRange); + if (parentRange != null) { + ZSSEditor.setRange(parentRange); + } ZSSEditor.sendMediaRemovedCallback(mediaIdentifier); } } @@ -957,34 +961,39 @@ ZSSEditor.markAllUploadingMediaAsFailed = function(message) { } }; -/** - * @brief Sends a callback with a list of failed images - */ -ZSSEditor.getFailedMedia = function() { +ZSSEditor.getFailedMediaIdArray = function() { var html = ZSSEditor.getField("zss_field_content").getHTML(); var tmp = document.createElement( "div" ); var tmpDom = $( tmp ).html( html ); var matches = tmpDom.find("img.failed"); - var functionArgument = "function=getFailedMedia"; var mediaIdArray = []; for (var i = 0; i < matches.size(); i++) { - var mediaId; + var mediaId = null; if (matches[i].hasAttribute("data-wpid")) { mediaId = matches[i].getAttribute("data-wpid"); } else if (matches[i].hasAttribute("data-video_wpid")) { mediaId = matches[i].getAttribute("data-video_wpid"); } - - // Track pre-existing failed media nodes for manual deletion events - ZSSEditor.trackNodeForMutation(this.getMediaContainerNodeWithIdentifier(mediaId)); - - if (mediaId.length > 0) { + if (mediaId !== null) { mediaIdArray.push(mediaId); } } + return mediaIdArray; +}; + +/** + * @brief Sends a callback with a list of failed images + */ +ZSSEditor.getFailedMedia = function() { + var mediaIdArray = ZSSEditor.getFailedMediaIdArray(); + for (var i = 0; i < mediaIdArray.length; i++) { + // Track pre-existing failed media nodes for manual deletion events + ZSSEditor.trackNodeForMutation(this.getMediaContainerNodeWithIdentifier(mediaIdArray[i])); + } + var functionArgument = "function=getFailedMedia"; var joinedArguments = functionArgument + defaultCallbackSeparator + "ids=" + mediaIdArray.toString(); ZSSEditor.callback('callback-response-string', joinedArguments); }; @@ -1299,6 +1308,14 @@ ZSSEditor.removeImage = function(imageNodeIdentifier) { } }; +ZSSEditor.removeAllFailedMediaUploads = function() { + console.log("Remove all failed media"); + var failedMediaArray = ZSSEditor.getFailedMediaIdArray(); + for (var i = 0; i < failedMediaArray.length; i++) { + ZSSEditor.removeImage(failedMediaArray[i]); + } +} + /** * @brief Inserts a video tag using the videoURL as source and posterURL as the * image to show while video is loading. @@ -2826,6 +2843,9 @@ ZSSEditor.parentTags = function() { ZSSEditor.getParentRangeOfFocusedNode = function() { var selection = window.getSelection(); + if (selection.focusNode == null) { + return null; + } return selection.getRangeAt(selection.focusNode.parentNode); }; From 549a3cd37759bd6b68215814c57ffa03018655b1 Mon Sep 17 00:00:00 2001 From: Maxime Biais Date: Wed, 30 Mar 2016 10:01:00 +0200 Subject: [PATCH 3/4] upgrade to gradle plugin 2.0.0-beta7 --- WordPressUtils/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressUtils/build.gradle b/WordPressUtils/build.gradle index e8f4c34ad823..28c308390402 100644 --- a/WordPressUtils/build.gradle +++ b/WordPressUtils/build.gradle @@ -3,7 +3,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.0.0-beta6' + classpath 'com.android.tools.build:gradle:2.0.0-beta7' } } From 4c9324cf1eee00b66c76e0d5a917c86e1293a845 Mon Sep 17 00:00:00 2001 From: Alex Forcier Date: Fri, 1 Apr 2016 14:06:54 -0400 Subject: [PATCH 4/4] Squashed 'libs/editor/' changes from f1f6d10..a35d654 a35d654 0.8 version bump 7c242ad Merge pull request #330 from wordpress-mobile/issue/299-exit-blockquote c0a8f79 Merge branch 'develop' into issue/299-exit-blockquote dd267de Merge pull request #329 from wordpress-mobile/issue/302-fix-dom-errors dc084a2 Don't process the second Enter when exiting a blockquote, to avoid adding an extra new line 8a2f91e Allow double Enter press to exit a blockquote 400a5f7 Sanitize all calls to window.getSelection() that are followed by getRangeAt(), which can cause a DOM error 607c010 Fix typo in node traversal method f2d468f If getFocusedField can't select the current contenteditable div, force focus on the content field d2e8a78 Avoid DOM error console logs by adding some null checks cf01d93 Merge commit 'c6efe0a9190244d40e64300efc9cca56ae5acd5c' into develop ab52e25 Merge branch 'develop' into issue/120editor-initial-focus 6301484 Show the software keyboard once the DOM has loaded 359edf6 Focus on the title field when opening posts git-subtree-dir: libs/editor git-subtree-split: a35d654234ed2691a62289d9f554333446f1fa0d --- WordPressEditor/build.gradle | 4 +- .../android/editor/EditorFragment.java | 12 ++++ .../editor-common/assets/ZSSRichTextEditor.js | 59 ++++++++++++++----- 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/WordPressEditor/build.gradle b/WordPressEditor/build.gradle index f47a62fe3b56..63cd3a5c681b 100644 --- a/WordPressEditor/build.gradle +++ b/WordPressEditor/build.gradle @@ -23,8 +23,8 @@ android { buildToolsVersion "23.0.2" defaultConfig { - versionCode 6 - versionName "0.6" + versionCode 8 + versionName "0.8" minSdkVersion 14 targetSdkVersion 23 } diff --git a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java index 3c8af4143254..ccef283f6791 100755 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java @@ -968,11 +968,14 @@ public void run() { button.setChecked(false); } + boolean editorHasFocus = false; + // Add any media files that were placed in a queue due to the DOM not having loaded yet if (mWaitingMediaFiles.size() > 0) { // Image insertion will only work if the content field is in focus // (for a new post, no field is in focus until user action) mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').focus();"); + editorHasFocus = true; for (Map.Entry entry : mWaitingMediaFiles.entrySet()) { appendMediaFile(entry.getValue(), entry.getKey(), null); @@ -985,6 +988,7 @@ public void run() { // Gallery insertion will only work if the content field is in focus // (for a new post, no field is in focus until user action) mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').focus();"); + editorHasFocus = true; for (MediaGallery mediaGallery : mWaitingGalleries) { appendGallery(mediaGallery); @@ -993,6 +997,14 @@ public void run() { mWaitingGalleries.clear(); } + if (!editorHasFocus) { + mWebView.execJavaScriptFromString("ZSSEditor.focusFirstEditableField();"); + } + + // Show the keyboard + ((InputMethodManager)getActivity().getSystemService(Context.INPUT_METHOD_SERVICE)) + .showSoftInput(mWebView, InputMethodManager.SHOW_IMPLICIT); + ProfilingUtils.split("EditorFragment.onDomLoaded completed"); ProfilingUtils.dump(); ProfilingUtils.stop(); diff --git a/libs/editor-common/assets/ZSSRichTextEditor.js b/libs/editor-common/assets/ZSSRichTextEditor.js index dc1caf996d04..5cde6455bf63 100755 --- a/libs/editor-common/assets/ZSSRichTextEditor.js +++ b/libs/editor-common/assets/ZSSRichTextEditor.js @@ -193,13 +193,22 @@ ZSSEditor.getField = function(fieldId) { ZSSEditor.getFocusedField = function() { var currentField = $(this.closerParentNodeWithName('div')); - var currentFieldId = currentField.attr('id'); + var currentFieldId; - while (currentField - && (!currentFieldId || this.editableFields[currentFieldId] == null)) { - currentField = this.closerParentNodeStartingAtNode('div', currentField); + if (currentField) { currentFieldId = currentField.attr('id'); + } + while (currentField && (!currentFieldId || this.editableFields[currentFieldId] == null)) { + currentField = this.closerParentNodeStartingAtNode('div', currentField); + if (currentField) { + currentFieldId = currentField.attr('id'); + } + } + + if (!currentFieldId) { + ZSSEditor.resetSelectionOnField('zss_field_content'); + currentFieldId = 'zss_field_content'; } return this.editableFields[currentFieldId]; @@ -357,6 +366,9 @@ ZSSEditor.stylesCallback = function(stylesArray) { ZSSEditor.backupRange = function(){ var selection = window.getSelection(); + if (selection.rangeCount < 1) { + return; + } var range = selection.getRangeAt(0); ZSSEditor.currentSelection = @@ -411,7 +423,6 @@ ZSSEditor.getCaretArguments = function() { }; ZSSEditor.getJoinedFocusedFieldIdAndCaretArguments = function() { - var joinedArguments = ZSSEditor.getJoinedCaretArguments(); var idArgument = "id=" + ZSSEditor.getFocusedField().getNodeId(); @@ -430,6 +441,9 @@ ZSSEditor.getJoinedCaretArguments = function() { ZSSEditor.getCaretYPosition = function() { var selection = window.getSelection(); + if (selection.rangeCount == 0) { + return 0; + } var range = selection.getRangeAt(0); var span = document.createElement("span"); // Ensure span has dimensions and position by @@ -543,7 +557,7 @@ ZSSEditor.setStrikeThrough = function() { var mustHandleWebKitIssue = (isDisablingStrikeThrough && ZSSEditor.isCommandEnabled(commandName)); - if (mustHandleWebKitIssue) { + if (mustHandleWebKitIssue && window.getSelection().rangeCount > 0) { var troublesomeNodeNames = ['del']; var selection = window.getSelection(); @@ -2730,6 +2744,9 @@ ZSSEditor.closerParentNode = function() { var parentNode = null; var selection = window.getSelection(); + if (selection.rangeCount < 1) { + return null; + } var range = selection.getRangeAt(0).cloneRange(); var currentNode = range.commonAncestorContainer; @@ -2752,7 +2769,7 @@ ZSSEditor.closerParentNodeStartingAtNode = function(nodeName, startingNode) { nodeName = nodeName.toLowerCase(); var parentNode = null; - var currentNode = startingNode,parentElement; + var currentNode = startingNode.parentElement; while (currentNode) { @@ -2760,7 +2777,7 @@ ZSSEditor.closerParentNodeStartingAtNode = function(nodeName, startingNode) { break; } - if (currentNode.nodeName.toLowerCase() == nodeName + if (currentNode.nodeName && currentNode.nodeName.toLowerCase() == nodeName && currentNode.nodeType == document.ELEMENT_NODE) { parentNode = currentNode; @@ -2779,6 +2796,9 @@ ZSSEditor.closerParentNodeWithName = function(nodeName) { var parentNode = null; var selection = window.getSelection(); + if (selection.rangeCount < 1) { + return null; + } var range = selection.getRangeAt(0).cloneRange(); var referenceNode = range.commonAncestorContainer; @@ -2820,6 +2840,9 @@ ZSSEditor.parentTags = function() { var parentTags = []; var selection = window.getSelection(); + if (selection.rangeCount < 1) { + return null; + } var range = selection.getRangeAt(0); var currentNode = range.commonAncestorContainer; @@ -2937,21 +2960,29 @@ ZSSField.prototype.handleKeyDownEvent = function(e) { } else if (this.isMultiline()) { this.wrapCaretInParagraphIfNecessary(); - // If enter was pressed to end a UL or OL, let's double check and handle it accordingly if so if (wasEnterPressed) { - sel = window.getSelection(); - node = $(sel.anchorNode); - children = $(sel.anchorNode.childNodes); + var sel = window.getSelection(); + if (sel.rangeCount < 1) { + return null; + } + var node = $(sel.anchorNode); + var children = $(sel.anchorNode.childNodes); + var parentNode = rangy.getSelection().anchorNode.parentNode; + // If enter was pressed to end a UL or OL, let's double check and handle it accordingly if so if (sel.isCollapsed && node.is(NodeName.LI) && (!children.length || (children.length == 1 && children.first().is(NodeName.BR)))) { e.preventDefault(); - var parentNode = rangy.getSelection().anchorNode.parentNode; if (parentNode && parentNode.nodeName === NodeName.OL) { ZSSEditor.setOrderedList(); } else if (parentNode && parentNode.nodeName === NodeName.UL) { ZSSEditor.setUnorderedList(); } + // Exit blockquote when the user presses Enter inside a blockquote on a new line + // (main use case is to allow double Enter to exit blockquote) + } else if (sel.isCollapsed && sel.baseOffset == 0 && parentNode && parentNode.nodeName == 'BLOCKQUOTE') { + e.preventDefault(); + ZSSEditor.setBlockquote(); } } } @@ -3211,7 +3242,7 @@ ZSSField.prototype.wrapCaretInParagraphIfNecessary = function() if (parentNodeShouldBeParagraph) { var selection = window.getSelection(); - if (selection) { + if (selection && selection.rangeCount > 0) { var range = selection.getRangeAt(0); if (range.startContainer == range.endContainer) {