aboutsummaryrefslogtreecommitdiff
path: root/libs/editor
diff options
context:
space:
mode:
Diffstat (limited to 'libs/editor')
-rw-r--r--libs/editor/.gitignore56
-rw-r--r--libs/editor/.travis.yml24
-rw-r--r--libs/editor/LICENSE.md264
-rw-r--r--libs/editor/README.md35
-rw-r--r--libs/editor/WordPressEditor/build.gradle108
-rw-r--r--libs/editor/WordPressEditor/lint.xml10
-rw-r--r--libs/editor/WordPressEditor/proguard-rules.pro17
-rw-r--r--libs/editor/WordPressEditor/src/androidTest/AndroidManifest.xml12
-rw-r--r--libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/EditorFragmentForTests.java41
-rw-r--r--libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/EditorFragmentTest.java177
-rw-r--r--libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockActivity.java7
-rw-r--r--libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockEditorActivity.java102
-rw-r--r--libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/TestingUtils.java14
-rw-r--r--libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/ZssEditorTest.java128
-rw-r--r--libs/editor/WordPressEditor/src/main/AndroidManifest.xml7
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/ZSSRichTextEditor.js3847
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/android-editor.html48
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/editor-android.css95
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/editor-utils-formatter.js158
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/editor-utils.js26
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/editor.css456
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Bold.ttfbin0 -> 46796 bytes
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-BoldItalic.ttfbin0 -> 59316 bytes
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Italic.ttfbin0 -> 53140 bytes
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Light.ttfbin0 -> 46900 bytes
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Regular.ttfbin0 -> 46576 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/libs/fastclick.js821
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/libs/jquery-2.1.3.min.js4
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/libs/jquery.mobile-events.min.js1
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/libs/js-beautifier.js766
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/libs/rangy-classapplier.js15
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/libs/rangy-core.js11
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/libs/rangy-cssclassapplier.js32
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/libs/rangy-highlighter.js12
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/libs/rangy-selectionsaverestore.js15
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/libs/rangy-serializer.js16
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/assets/libs/rangy-textrange.js32
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/libs/shortcode.js358
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/libs/underscore-min.js6
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/libs/wpload.js108
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/libs/wpsave.js113
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/svg/delete-image.svg12
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/svg/edit-image.svg12
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/svg/more-2x.pngbin0 -> 603 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/svg/pagebreak-2x.pngbin0 -> 835 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/svg/retry-image-large.svg11
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/svg/retry-image.svg11
-rw-r--r--libs/editor/WordPressEditor/src/main/assets/svg/wpposter.svg7
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java1659
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java180
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorMediaUploadListener.java10
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebView.java35
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewAbstract.java256
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewCompatibility.java130
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/HtmlStyleTextWatcher.java245
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/HtmlStyleUtils.java150
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/ImageSettingsDialogFragment.java431
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/JsCallbackReceiver.java236
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/LegacyEditorFragment.java1194
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/LinkDialogFragment.java76
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/OnImeBackListener.java5
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/OnJsEditorStateChangedListener.java20
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/RippleToggleButton.java95
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/SourceViewEditText.java60
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/Utils.java247
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/legacy/EditLinkActivity.java76
-rw-r--r--libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/legacy/WPEditImageSpan.java74
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ab_icon_edit.pngbin0 -> 1340 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/format_bar_chevron.pngbin0 -> 991 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ic_close_white_24dp.pngbin0 -> 324 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ic_post_settings.pngbin0 -> 1506 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_admin_links.pngbin0 -> 814 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_admin_links_grey.pngbin0 -> 995 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_bold.pngbin0 -> 586 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_bold_grey.pngbin0 -> 587 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_insertmore.pngbin0 -> 398 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_insertmore_grey.pngbin0 -> 398 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_italic.pngbin0 -> 538 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_italic_grey.pngbin0 -> 558 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_strikethrough.pngbin0 -> 663 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_strikethrough_grey.pngbin0 -> 798 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_underline.pngbin0 -> 1230 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_underline_grey.pngbin0 -> 1235 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_image_big_grey.pngbin0 -> 1122 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_quote.pngbin0 -> 1491 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_quote_grey.pngbin0 -> 1564 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_icon_mediagallery_placeholder.pngbin0 -> 2404 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/list_focused_wordpress.9.pngbin0 -> 147 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/media_icon_32dp.pngbin0 -> 483 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/media_movieclip.pngbin0 -> 4208 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/noticon_picture.pngbin0 -> 1264 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-hdpi/noticon_picture_grey.pngbin0 -> 1271 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ab_icon_edit.pngbin0 -> 1439 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/format_bar_chevron.pngbin0 -> 1103 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ic_close_white_24dp.pngbin0 -> 402 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ic_post_settings.pngbin0 -> 1687 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_admin_links.pngbin0 -> 1026 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_admin_links_grey.pngbin0 -> 1288 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_bold.pngbin0 -> 653 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_bold_grey.pngbin0 -> 703 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_insertmore.pngbin0 -> 379 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_insertmore_grey.pngbin0 -> 388 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_italic.pngbin0 -> 576 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_italic_grey.pngbin0 -> 603 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_strikethrough.pngbin0 -> 773 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_strikethrough_grey.pngbin0 -> 1041 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_underline.pngbin0 -> 1279 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_underline_grey.pngbin0 -> 1309 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_image_big_grey.pngbin0 -> 1287 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_quote.pngbin0 -> 1668 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_quote_grey.pngbin0 -> 1781 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_icon_mediagallery_placeholder.pngbin0 -> 2774 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/list_focused_wordpress.9.pngbin0 -> 150 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/media_icon_32dp.pngbin0 -> 706 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/media_movieclip.pngbin0 -> 4292 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/noticon_picture.pngbin0 -> 1376 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/noticon_picture_grey.pngbin0 -> 1371 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ab_icon_edit.pngbin0 -> 1713 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/format_bar_chevron.pngbin0 -> 1810 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ic_close_white_24dp.pngbin0 -> 492 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ic_post_settings.pngbin0 -> 2050 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_admin_links.pngbin0 -> 1429 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_admin_links_grey.pngbin0 -> 1691 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_bold.pngbin0 -> 883 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_bold_grey.pngbin0 -> 915 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_insertmore.pngbin0 -> 432 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_insertmore_grey.pngbin0 -> 457 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_italic.pngbin0 -> 796 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_italic_grey.pngbin0 -> 735 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_strikethrough.pngbin0 -> 1145 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_strikethrough_grey.pngbin0 -> 1440 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_underline.pngbin0 -> 1427 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_underline_grey.pngbin0 -> 1473 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_image_big_grey.pngbin0 -> 2174 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_quote.pngbin0 -> 2087 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_quote_grey.pngbin0 -> 2218 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_icon_mediagallery_placeholder.pngbin0 -> 3826 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/media_icon_32dp.pngbin0 -> 1157 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/media_movieclip.pngbin0 -> 2240 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/noticon_picture.pngbin0 -> 1601 bytes
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/noticon_picture_grey.pngbin0 -> 1594 bytes
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold.xml18
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_disabled.xml18
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_highlighted.xml18
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_selector.xml7
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html.xml18
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_disabled.xml18
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_highlighted.xml19
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_selector.xml7
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic.xml11
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_disabled.xml11
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_highlighted.xml11
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_selector.xml7
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link.xml16
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_disabled.xml16
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_highlighted.xml16
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_selector.xml7
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media.xml15
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_disabled.xml15
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_highlighted.xml15
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_selector.xml7
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more.xml12
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more_disabled.xml12
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more_highlighted.xml12
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol.xml28
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_disabled.xml28
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_highlighted.xml28
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_selector.xml7
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote.xml20
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_disabled.xml20
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_highlighted.xml20
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_selector.xml7
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough.xml18
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_disabled.xml18
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_highlighted.xml18
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_selector.xml7
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul.xml17
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_disabled.xml17
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_highlighted.xml17
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_selector.xml7
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/ic_close_padded.xml7
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_bold_selected_state.xml12
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_bold_selector.xml8
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_italic_selected_state.xml12
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_italic_selector.xml8
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_link_selected_state.xml12
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_link_selector.xml8
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_media_selected_state.xml12
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_media_selector.xml8
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_more_selected_state.xml12
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_more_selector.xml8
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_quote_selected_state.xml12
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_quote_selector.xml8
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_strike_selected_state.xml12
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_strike_selector.xml8
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_underline_selected_state.xml12
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_underline_selector.xml8
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/list_divider.xml10
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/pressed_background_wordpress.xml22
-rw-r--r--libs/editor/WordPressEditor/src/main/res/drawable/selectable_background_wordpress.xml25
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout-v19/editor_webview.xml6
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout-w360dp/format_bar.xml113
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout-w380dp/format_bar.xml149
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout-w600dp/format_bar.xml140
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout/alert_create_link.xml42
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout/alert_image_options.xml81
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout/dialog_image_options.xml154
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout/dialog_link.xml41
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout/editor_webview.xml6
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout/format_bar.xml119
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout/fragment_edit_post_content.xml165
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/layout/fragment_editor.xml82
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout/image_settings_formatbar.xml16
-rw-r--r--libs/editor/WordPressEditor/src/main/res/layout/legacy_activity_editor.xml13
-rw-r--r--libs/editor/WordPressEditor/src/main/res/values-w1280dp/dimens.xml6
-rw-r--r--libs/editor/WordPressEditor/src/main/res/values-w1280dp/layouts.xml4
-rw-r--r--libs/editor/WordPressEditor/src/main/res/values-w720dp/dimens.xml5
-rw-r--r--libs/editor/WordPressEditor/src/main/res/values-w720dp/layouts.xml4
-rw-r--r--libs/editor/WordPressEditor/src/main/res/values-w800dp/dimens.xml5
-rw-r--r--libs/editor/WordPressEditor/src/main/res/values/attrs.xml6
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/values/colors.xml18
-rw-r--r--libs/editor/WordPressEditor/src/main/res/values/dimens.xml55
-rw-r--r--libs/editor/WordPressEditor/src/main/res/values/layouts.xml5
-rw-r--r--libs/editor/WordPressEditor/src/main/res/values/strings.xml97
-rwxr-xr-xlibs/editor/WordPressEditor/src/main/res/values/styles.xml49
-rw-r--r--libs/editor/WordPressEditor/src/main/res/values/wp_colors.xml13
-rw-r--r--libs/editor/build.gradle0
-rw-r--r--libs/editor/example/build.gradle56
-rw-r--r--libs/editor/example/proguard-rules.pro17
-rw-r--r--libs/editor/example/src/main/AndroidManifest.xml24
-rw-r--r--libs/editor/example/src/main/assets/example/cowboy-cat.jpgbin0 -> 23142 bytes
-rw-r--r--libs/editor/example/src/main/assets/example/example-content.html58
-rw-r--r--libs/editor/example/src/main/java/org/wordpress/android/editor/example/EditorExampleActivity.java348
-rw-r--r--libs/editor/example/src/main/java/org/wordpress/android/editor/example/MainExampleActivity.java86
-rw-r--r--libs/editor/example/src/main/res/layout/activity_example.xml44
-rw-r--r--libs/editor/example/src/main/res/layout/activity_legacy_editor.xml13
-rw-r--r--libs/editor/example/src/main/res/layout/activity_new_editor.xml13
-rw-r--r--libs/editor/example/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 3418 bytes
-rw-r--r--libs/editor/example/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 2206 bytes
-rw-r--r--libs/editor/example/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 4842 bytes
-rw-r--r--libs/editor/example/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 7718 bytes
-rw-r--r--libs/editor/example/src/main/res/values/colors.xml4
-rw-r--r--libs/editor/example/src/main/res/values/strings.xml21
-rw-r--r--libs/editor/example/src/main/res/values/styles.xml9
-rw-r--r--libs/editor/example/src/test/java/org/wordpress/android/editor/ApplicationTest.java13
-rw-r--r--libs/editor/example/src/test/java/org/wordpress/android/editor/EditorFragmentAbstractTest.java112
-rw-r--r--libs/editor/example/src/test/java/org/wordpress/android/editor/HtmlStyleTextWatcherTest.java496
-rw-r--r--libs/editor/example/src/test/java/org/wordpress/android/editor/HtmlStyleUtilsTest.java92
-rw-r--r--libs/editor/example/src/test/java/org/wordpress/android/editor/JsCallbackReceiverTest.java99
-rw-r--r--libs/editor/example/src/test/java/org/wordpress/android/editor/UtilsTest.java215
-rw-r--r--libs/editor/example/src/test/js/test-formatter.js135
-rw-r--r--libs/editor/gradle/wrapper/gradle-wrapper.jarbin0 -> 49896 bytes
-rw-r--r--libs/editor/gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xlibs/editor/gradlew164
-rw-r--r--libs/editor/gradlew.bat90
-rw-r--r--libs/editor/settings.gradle2
256 files changed, 17492 insertions, 0 deletions
diff --git a/libs/editor/.gitignore b/libs/editor/.gitignore
new file mode 100644
index 000000000..c73b54048
--- /dev/null
+++ b/libs/editor/.gitignore
@@ -0,0 +1,56 @@
+# OS X generated file
+.DS_Store
+
+# built application files
+*.apk
+*.ap_
+
+# files for the dex VM
+*.dex
+
+# Java class files
+*.class
+
+# generated files
+bin/
+gen/
+build/
+*/build/
+captures/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Eclipse project files
+.settings/
+.classpath
+.project
+
+# Intellij project files
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# Gradle
+.gradle/
+gradle.properties
+
+# Generated by gradle
+crashlytics.properties
+
+# Generated by Crashlytics
+WordPress/src/main/res/values/com_crashlytics_export_strings.xml
+
+# Silver Searcher ignore file
+.agignore
+
+# Monkey runner settings
+*.pyc
+
+# libs
+libs/utils
+
+# Node-based JS Tests
+node_modules
+npm-debug.log* \ No newline at end of file
diff --git a/libs/editor/.travis.yml b/libs/editor/.travis.yml
new file mode 100644
index 000000000..9a6a6a437
--- /dev/null
+++ b/libs/editor/.travis.yml
@@ -0,0 +1,24 @@
+language: android
+jdk: oraclejdk8
+
+android:
+ components:
+ - extra-android-m2repository
+ - extra-android-support
+ - platform-tools
+ - tools
+ - build-tools-24.0.2
+ - android-24
+
+env:
+ global:
+ - GRADLE_OPTS="-XX:MaxPermSize=4g -Xmx4g"
+ - ANDROID_SDKS=android-16
+ - ANDROID_TARGET=android-16
+
+before_install:
+ # TODO: Remove the following line when Travis' platform-tools are updated to v24+
+ - echo yes | android update sdk -a --filter platform-tools --no-ui --force
+
+script:
+ - ./gradlew build
diff --git a/libs/editor/LICENSE.md b/libs/editor/LICENSE.md
new file mode 100644
index 000000000..0671f06ac
--- /dev/null
+++ b/libs/editor/LICENSE.md
@@ -0,0 +1,264 @@
+The GNU General Public License, Version 2, June 1991 (GPLv2)
+============================================================
+
+> Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+> 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+Everyone is permitted to copy and distribute verbatim copies of this license
+document, but changing it is not allowed.
+
+
+Preamble
+--------
+
+The licenses for most software are designed to take away your freedom to share
+and change it. By contrast, the GNU General Public License is intended to
+guarantee your freedom to share and change free software--to make sure the
+software is free for all its users. This General Public License applies to most
+of the Free Software Foundation's software and to any other program whose
+authors commit to using it. (Some other Free Software Foundation software is
+covered by the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+When we speak of free software, we are referring to freedom, not price. Our
+General Public Licenses are designed to make sure that you have the freedom to
+distribute copies of free software (and charge for this service if you wish),
+that you receive source code or can get it if you want it, that you can change
+the software or use pieces of it in new free programs; and that you know you can
+do these things.
+
+To protect your rights, we need to make restrictions that forbid anyone to deny
+you these rights or to ask you to surrender the rights. These restrictions
+translate to certain responsibilities for you if you distribute copies of the
+software, or if you modify it.
+
+For example, if you distribute copies of such a program, whether gratis or for a
+fee, you must give the recipients all the rights that you have. You must make
+sure that they, too, receive or can get the source code. And you must show them
+these terms so they know their rights.
+
+We protect your rights with two steps: (1) copyright the software, and (2) offer
+you this license which gives you legal permission to copy, distribute and/or
+modify the software.
+
+Also, for each author's protection and ours, we want to make certain that
+everyone understands that there is no warranty for this free software. If the
+software is modified by someone else and passed on, we want its recipients to
+know that what they have is not the original, so that any problems introduced by
+others will not reflect on the original authors' reputations.
+
+Finally, any free program is threatened constantly by software patents. We wish
+to avoid the danger that redistributors of a free program will individually
+obtain patent licenses, in effect making the program proprietary. To prevent
+this, we have made it clear that any patent must be licensed for everyone's free
+use or not licensed at all.
+
+The precise terms and conditions for copying, distribution and modification
+follow.
+
+
+Terms And Conditions For Copying, Distribution And Modification
+---------------------------------------------------------------
+
+**0.** This License applies to any program or other work which contains a notice
+placed by the copyright holder saying it may be distributed under the terms of
+this General Public License. The "Program", below, refers to any such program or
+work, and a "work based on the Program" means either the Program or any
+derivative work under copyright law: that is to say, a work containing the
+Program or a portion of it, either verbatim or with modifications and/or
+translated into another language. (Hereinafter, translation is included without
+limitation in the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not covered by
+this License; they are outside its scope. The act of running the Program is not
+restricted, and the output from the Program is covered only if its contents
+constitute a work based on the Program (independent of having been made by
+running the Program). Whether that is true depends on what the Program does.
+
+**1.** You may copy and distribute verbatim copies of the Program's source code
+as you receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice and
+disclaimer of warranty; keep intact all the notices that refer to this License
+and to the absence of any warranty; and give any other recipients of the Program
+a copy of this License along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and you may at
+your option offer warranty protection in exchange for a fee.
+
+**2.** You may modify your copy or copies of the Program or any portion of it,
+thus forming a work based on the Program, and copy and distribute such
+modifications or work under the terms of Section 1 above, provided that you also
+meet all of these conditions:
+
+* **a)** You must cause the modified files to carry prominent notices stating
+ that you changed the files and the date of any change.
+
+* **b)** You must cause any work that you distribute or publish, that in whole
+ or in part contains or is derived from the Program or any part thereof, to
+ be licensed as a whole at no charge to all third parties under the terms of
+ this License.
+
+* **c)** If the modified program normally reads commands interactively when
+ run, you must cause it, when started running for such interactive use in the
+ most ordinary way, to print or display an announcement including an
+ appropriate copyright notice and a notice that there is no warranty (or
+ else, saying that you provide a warranty) and that users may redistribute
+ the program under these conditions, and telling the user how to view a copy
+ of this License. (Exception: if the Program itself is interactive but does
+ not normally print such an announcement, your work based on the Program is
+ not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If identifiable
+sections of that work are not derived from the Program, and can be reasonably
+considered independent and separate works in themselves, then this License, and
+its terms, do not apply to those sections when you distribute them as separate
+works. But when you distribute the same sections as part of a whole which is a
+work based on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the entire whole,
+and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest your
+rights to work written entirely by you; rather, the intent is to exercise the
+right to control the distribution of derivative or collective works based on the
+Program.
+
+In addition, mere aggregation of another work not based on the Program with the
+Program (or with a work based on the Program) on a volume of a storage or
+distribution medium does not bring the other work under the scope of this
+License.
+
+**3.** You may copy and distribute the Program (or a work based on it, under
+Section 2) in object code or executable form under the terms of Sections 1 and 2
+above provided that you also do one of the following:
+
+* **a)** Accompany it with the complete corresponding machine-readable source
+ code, which must be distributed under the terms of Sections 1 and 2 above on
+ a medium customarily used for software interchange; or,
+
+* **b)** Accompany it with a written offer, valid for at least three years, to
+ give any third party, for a charge no more than your cost of physically
+ performing source distribution, a complete machine-readable copy of the
+ corresponding source code, to be distributed under the terms of Sections 1
+ and 2 above on a medium customarily used for software interchange; or,
+
+* **c)** Accompany it with the information you received as to the offer to
+ distribute corresponding source code. (This alternative is allowed only for
+ noncommercial distribution and only if you received the program in object
+ code or executable form with such an offer, in accord with Subsection b
+ above.)
+
+The source code for a work means the preferred form of the work for making
+modifications to it. For an executable work, complete source code means all the
+source code for all modules it contains, plus any associated interface
+definition files, plus the scripts used to control compilation and installation
+of the executable. However, as a special exception, the source code distributed
+need not include anything that is normally distributed (in either source or
+binary form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component itself
+accompanies the executable.
+
+If distribution of executable or object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the source code
+from the same place counts as distribution of the source code, even though third
+parties are not compelled to copy the source along with the object code.
+
+**4.** You may not copy, modify, sublicense, or distribute the Program except as
+expressly provided under this License. Any attempt otherwise to copy, modify,
+sublicense or distribute the Program is void, and will automatically terminate
+your rights under this License. However, parties who have received copies, or
+rights, from you under this License will not have their licenses terminated so
+long as such parties remain in full compliance.
+
+**5.** You are not required to accept this License, since you have not signed
+it. However, nothing else grants you permission to modify or distribute the
+Program or its derivative works. These actions are prohibited by law if you do
+not accept this License. Therefore, by modifying or distributing the Program (or
+any work based on the Program), you indicate your acceptance of this License to
+do so, and all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+**6.** Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the original
+licensor to copy, distribute or modify the Program subject to these terms and
+conditions. You may not impose any further restrictions on the recipients'
+exercise of the rights granted herein. You are not responsible for enforcing
+compliance by third parties to this License.
+
+**7.** If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues), conditions
+are imposed on you (whether by court order, agreement or otherwise) that
+contradict the conditions of this License, they do not excuse you from the
+conditions of this License. If you cannot distribute so as to satisfy
+simultaneously your obligations under this License and any other pertinent
+obligations, then as a consequence you may not distribute the Program at all.
+For example, if a patent license would not permit royalty-free redistribution of
+the Program by all those who receive copies directly or indirectly through you,
+then the only way you could satisfy both it and this License would be to refrain
+entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply and the
+section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any patents or
+other property right claims or to contest validity of any such claims; this
+section has the sole purpose of protecting the integrity of the free software
+distribution system, which is implemented by public license practices. Many
+people have made generous contributions to the wide range of software
+distributed through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing to
+distribute software through any other system and a licensee cannot impose that
+choice.
+
+This section is intended to make thoroughly clear what is believed to be a
+consequence of the rest of this License.
+
+**8.** If the distribution and/or use of the Program is restricted in certain
+countries either by patents or by copyrighted interfaces, the original copyright
+holder who places the Program under this License may add an explicit
+geographical distribution limitation excluding those countries, so that
+distribution is permitted only in or among countries not thus excluded. In such
+case, this License incorporates the limitation as if written in the body of this
+License.
+
+**9.** The Free Software Foundation may publish revised and/or new versions of
+the General Public License from time to time. Such new versions will be similar
+in spirit to the present version, but may differ in detail to address new
+problems or concerns.
+
+Each version is given a distinguishing version number. If the Program specifies
+a version number of this License which applies to it and "any later version",
+you have the option of following the terms and conditions either of that version
+or of any later version published by the Free Software Foundation. If the
+Program does not specify a version number of this License, you may choose any
+version ever published by the Free Software Foundation.
+
+**10.** If you wish to incorporate parts of the Program into other free programs
+whose distribution conditions are different, write to the author to ask for
+permission. For software which is copyrighted by the Free Software Foundation,
+write to the Free Software Foundation; we sometimes make exceptions for this.
+Our decision will be guided by the two goals of preserving the free status of
+all derivatives of our free software and of promoting the sharing and reuse of
+software generally.
+
+
+No Warranty
+-----------
+
+**11.** BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
+THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
+STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM
+"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
+BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+**12.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR
+INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA
+BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER
+OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
diff --git a/libs/editor/README.md b/libs/editor/README.md
new file mode 100644
index 000000000..753794919
--- /dev/null
+++ b/libs/editor/README.md
@@ -0,0 +1,35 @@
+# WordPress-Editor-Android #
+
+[![Build Status](https://travis-ci.org/wordpress-mobile/WordPress-Editor-Android.svg?branch=develop)](https://travis-ci.org/wordpress-mobile/WordPress-Editor-Android)
+
+## Introduction ##
+
+WordPress-Editor-Android is the text editor used in the [WordPress Android app](https://github.com/wordpress-mobile/WordPress-Android) to create and edit pages & posts. In short it's a simple, straightforward way to visually edit HTML.
+
+## Testing ##
+
+This project has both unit testing and integration testing, maintained and run separately.
+
+Unit testing is done with the [Robolectric framework](http://robolectric.org/). To run unit tests simply run `gradlew testDebug`.
+
+Integration testing is done with the [Android testing framework](http://developer.android.com/tools/testing/testing_android.html). To run integration tests run `gradlew connectedAndroidTest`.
+
+Add new unit tests to `src/test/java/` and integration tests to `stc/androidTest/java/`.
+
+### JavaScript Tests ###
+
+This project also has unit tests for the JS part of the editor using [Mocha](https://mochajs.org/).
+
+To be able to run the tests, [npm](https://www.npmjs.com/) and Mocha (`npm install -g mocha`) are required.
+
+With npm and Mocha installed, from within `example/src/test/js`, run:
+
+ npm install chai
+
+And then run `mocha` inside the same folder:
+
+ cd example/src/test/js; mocha test*; cd -
+
+## LICENSE ##
+
+WordPress-Editor-Android is an Open Source project covered by the [GNU General Public License version 2](LICENSE.md).
diff --git a/libs/editor/WordPressEditor/build.gradle b/libs/editor/WordPressEditor/build.gradle
new file mode 100644
index 000000000..ccfb60927
--- /dev/null
+++ b/libs/editor/WordPressEditor/build.gradle
@@ -0,0 +1,108 @@
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.2.0'
+ }
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'maven'
+apply plugin: 'signing'
+
+repositories {
+ jcenter()
+}
+
+android {
+ publishNonDefault true
+
+ compileSdkVersion 24
+ buildToolsVersion "24.0.2"
+
+ defaultConfig {
+ versionCode 13
+ versionName "1.3"
+ minSdkVersion 16
+ targetSdkVersion 24
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ // Avoid 'duplicate files during packaging of APK' errors
+ packagingOptions {
+ exclude 'LICENSE.txt'
+ exclude 'META-INF/LICENSE.txt'
+ exclude 'META-INF/LICENSE'
+ exclude 'META-INF/NOTICE'
+ exclude 'META-INF/NOTICE.txt'
+ }
+}
+
+dependencies {
+ compile 'com.android.support:appcompat-v7:24.2.1'
+ compile 'com.android.support:support-v4:24.2.1'
+ compile 'com.android.support:design:24.2.1'
+ compile 'org.wordpress:utils:1.11.0'
+}
+
+signing {
+ required {
+ project.properties.containsKey("signing.keyId") && project.properties.containsKey("signing.secretKeyRingFile")
+ }
+ sign configurations.archives
+}
+
+version android.defaultConfig.versionName
+group = "org.wordpress"
+archivesBaseName = "editor"
+
+// http://central.sonatype.org/pages/gradle.html
+
+uploadArchives {
+ repositories {
+ mavenDeployer {
+ beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
+
+ repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") {
+ authentication(userName: project.properties.ossrhUsername, password: project.properties.ossrhPassword)
+ }
+
+ snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") {
+ authentication(userName: project.properties.ossrhUsername, password: project.properties.ossrhPassword)
+ }
+
+ pom.project {
+ name 'WordPress-Android-Editor'
+ packaging 'aar'
+ description 'A reusable Android rich text editor component'
+ url 'https://github.com/wordpress-mobile/WordPress-Android-Editor'
+ scm {
+ connection 'scm:git:https://github.com/wordpress-mobile/WordPress-Android-Editor.git'
+ developerConnection 'scm:git:https://github.com/wordpress-mobile/WordPress-Android-Editor.git'
+ url 'https://github.com/wordpress-mobile/WordPress-Android-Editor'
+ }
+
+ licenses {
+ license {
+ name 'The MIT License (MIT)'
+ url 'http://opensource.org/licenses/MIT'
+ }
+ }
+
+ developers {
+ developer {
+ id 'maxme'
+ name 'Maxime Biais'
+ email 'maxime@automattic.com'
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/libs/editor/WordPressEditor/lint.xml b/libs/editor/WordPressEditor/lint.xml
new file mode 100644
index 000000000..d377d1d00
--- /dev/null
+++ b/libs/editor/WordPressEditor/lint.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<lint>
+ <issue id="InvalidPackage">
+ <ignore regexp="robolectric-2.4.jar" />
+ </issue>
+
+ <issue id="MissingPrefix">
+ <ignore regexp="Unexpected namespace prefix &quot;app&quot; found for tag `Image.*`" />
+ </issue>
+</lint> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/proguard-rules.pro b/libs/editor/WordPressEditor/proguard-rules.pro
new file mode 100644
index 000000000..d8d549daa
--- /dev/null
+++ b/libs/editor/WordPressEditor/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/max/work/android-sdk-mac/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/libs/editor/WordPressEditor/src/androidTest/AndroidManifest.xml b/libs/editor/WordPressEditor/src/androidTest/AndroidManifest.xml
new file mode 100644
index 000000000..eeef377fb
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.wordpress.android.editor" >
+ <application>
+ <activity android:name=".MockActivity"
+ android:theme="@style/Theme.AppCompat.Light.DarkActionBar"
+ android:exported="false" />
+ <activity android:name=".MockEditorActivity"
+ android:theme="@style/Theme.AppCompat.Light.DarkActionBar"
+ android:exported="false"/>
+ </application>>
+</manifest>
diff --git a/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/EditorFragmentForTests.java b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/EditorFragmentForTests.java
new file mode 100644
index 000000000..1babaa3db
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/EditorFragmentForTests.java
@@ -0,0 +1,41 @@
+package org.wordpress.android.editor;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.Map;
+
+public class EditorFragmentForTests extends EditorFragment {
+ protected EditorWebViewAbstract mWebView;
+
+ protected boolean mInitCalled = false;
+ protected boolean mDomLoaded = false;
+ protected boolean mOnSelectionStyleChangedCalled = false;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+ mWebView = (EditorWebViewAbstract) view.findViewById(R.id.webview);
+ return view;
+ }
+
+ @Override
+ protected void initJsEditor() {
+ super.initJsEditor();
+ mInitCalled = true;
+ }
+
+ @Override
+ public void onDomLoaded() {
+ super.onDomLoaded();
+ mDomLoaded = true;
+ }
+
+ @Override
+ public void onSelectionStyleChanged(final Map<String, Boolean> changeMap) {
+ super.onSelectionStyleChanged(changeMap);
+ mOnSelectionStyleChangedCalled = true;
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/EditorFragmentTest.java b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/EditorFragmentTest.java
new file mode 100644
index 000000000..a955f0a98
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/EditorFragmentTest.java
@@ -0,0 +1,177 @@
+package org.wordpress.android.editor;
+
+import android.app.Activity;
+import android.test.ActivityInstrumentationTestCase2;
+import android.view.View;
+import android.widget.ToggleButton;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+
+import static org.wordpress.android.editor.TestingUtils.waitFor;
+
+public class EditorFragmentTest extends ActivityInstrumentationTestCase2<MockEditorActivity> {
+ private Activity mActivity;
+ private EditorFragmentForTests mFragment;
+
+ public EditorFragmentTest() {
+ super(MockEditorActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mActivity = getActivity();
+ mFragment = (EditorFragmentForTests) mActivity.getFragmentManager().findFragmentByTag("editorFragment");
+ }
+
+ public void testDomLoadedCallbackReceived() {
+ // initJsEditor() should have been called on setup
+ assertTrue(mFragment.mInitCalled);
+
+ waitForOnDomLoaded();
+
+ // The JS editor should have sent out a callback when the DOM loaded, triggering onDomLoaded()
+ assertTrue(mFragment.mDomLoaded);
+ }
+
+ public void testFormatBarToggledOnSelectedFieldChanged() {
+ Map<String, String> selectionArgs = new HashMap<>();
+
+ selectionArgs.put("id", "zss_field_title");
+ mFragment.onSelectionChanged(selectionArgs);
+
+ waitFor(100);
+
+ View view = mFragment.getView();
+
+ if (view == null) {
+ throw(new IllegalStateException("Fragment view is empty"));
+ }
+
+ // The formatting buttons should be disabled while the title field is selected
+ ToggleButton mediaButton = (ToggleButton) view.findViewById(R.id.format_bar_button_media);
+ ToggleButton boldButton = (ToggleButton) view.findViewById(R.id.format_bar_button_bold);
+ ToggleButton italicButton = (ToggleButton) view.findViewById(R.id.format_bar_button_italic);
+ ToggleButton quoteButton = (ToggleButton) view.findViewById(R.id.format_bar_button_quote);
+ ToggleButton ulButton = (ToggleButton) view.findViewById(R.id.format_bar_button_ul);
+ ToggleButton olButton = (ToggleButton) view.findViewById(R.id.format_bar_button_ol);
+ ToggleButton linkButton = (ToggleButton) view.findViewById(R.id.format_bar_button_link);
+ ToggleButton strikethroughButton = (ToggleButton) view.findViewById(R.id.format_bar_button_strikethrough);
+
+ assertFalse(mediaButton.isEnabled());
+ assertFalse(boldButton.isEnabled());
+ assertFalse(italicButton.isEnabled());
+ assertFalse(quoteButton.isEnabled());
+ assertFalse(ulButton.isEnabled());
+ assertFalse(olButton.isEnabled());
+ assertFalse(linkButton.isEnabled());
+
+ if (strikethroughButton != null) {
+ assertFalse(strikethroughButton.isEnabled());
+ }
+
+ // The HTML button should always be enabled
+ ToggleButton htmlButton = (ToggleButton) view.findViewById(R.id.format_bar_button_html);
+ assertTrue(htmlButton.isEnabled());
+
+ selectionArgs.clear();
+ selectionArgs.put("id", "zss_field_content");
+ mFragment.onSelectionChanged(selectionArgs);
+
+ waitFor(500);
+
+ // The formatting buttons should be enabled while the content field is selected
+ assertTrue(mediaButton.isEnabled());
+ assertTrue(boldButton.isEnabled());
+ assertTrue(italicButton.isEnabled());
+ assertTrue(quoteButton.isEnabled());
+ assertTrue(ulButton.isEnabled());
+ assertTrue(olButton.isEnabled());
+ assertTrue(linkButton.isEnabled());
+
+ if (strikethroughButton != null) {
+ assertTrue(strikethroughButton.isEnabled());
+ }
+
+ // The HTML button should always be enabled
+ assertTrue(htmlButton.isEnabled());
+ }
+
+ public void testHtmlModeToggleTextTransfer() throws InterruptedException {
+ waitForOnDomLoaded();
+
+ final View view = mFragment.getView();
+
+ if (view == null) {
+ throw (new IllegalStateException("Fragment view is empty"));
+ }
+
+ final ToggleButton htmlButton = (ToggleButton) view.findViewById(R.id.format_bar_button_html);
+
+ String content = mFragment.getContent().toString();
+
+ final SourceViewEditText titleText = (SourceViewEditText) view.findViewById(R.id.sourceview_title);
+ final SourceViewEditText contentText = (SourceViewEditText) view.findViewById(R.id.sourceview_content);
+
+ // -- Check that title and content text is properly loaded into the EditTexts when switching to HTML mode
+
+ final CountDownLatch uiThreadLatch1 = new CountDownLatch(1);
+
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ htmlButton.performClick(); // Turn on HTML mode
+
+ uiThreadLatch1.countDown();
+ }
+ });
+
+ uiThreadLatch1.await();
+
+ waitFor(500);
+
+ // The HTML mode fields should be populated with the raw HTML loaded into the WebView on load
+ // (see MockEditorActivity)
+ assertEquals("A title", titleText.getText().toString());
+ assertEquals(content, contentText.getText().toString());
+
+ // -- Check that the title and content text is updated in the WebView when switching back from HTML mode
+
+ final CountDownLatch uiThreadLatch2 = new CountDownLatch(1);
+
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ titleText.setText("new title");
+ contentText.setText("new <b>content</b>");
+
+ // Check that getTitle() and getContent() return latest version even in HTML mode
+ assertEquals("new title", mFragment.getTitle());
+ assertEquals("new <b>content</b>", mFragment.getContent());
+
+ htmlButton.performClick(); // Turn off HTML mode
+
+ uiThreadLatch2.countDown();
+ }
+ });
+
+ uiThreadLatch2.await();
+
+ waitFor(300); // Wait for JS to update the title/content
+
+ assertEquals("new title", mFragment.getTitle());
+ assertEquals("new <b>content</b>", mFragment.getContent());
+ }
+
+ private void waitForOnDomLoaded() {
+ long start = System.currentTimeMillis();
+ while(!mFragment.mDomLoaded) {
+ waitFor(10);
+ if (System.currentTimeMillis() - start > 5000) {
+ throw(new RuntimeException("Callback wait timed out"));
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockActivity.java b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockActivity.java
new file mode 100644
index 000000000..580ea2de4
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockActivity.java
@@ -0,0 +1,7 @@
+package org.wordpress.android.editor;
+
+import android.support.v7.app.AppCompatActivity;
+
+public class MockActivity extends AppCompatActivity {
+
+}
diff --git a/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockEditorActivity.java b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockEditorActivity.java
new file mode 100644
index 000000000..ecd515154
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockEditorActivity.java
@@ -0,0 +1,102 @@
+package org.wordpress.android.editor;
+
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.DragEvent;
+import android.widget.LinearLayout;
+
+import org.wordpress.android.editor.EditorFragmentAbstract.EditorDragAndDropListener;
+import org.wordpress.android.editor.EditorFragmentAbstract.EditorFragmentListener;
+import org.wordpress.android.editor.EditorFragmentAbstract.TrackableEvent;
+import org.wordpress.android.util.helpers.MediaFile;
+
+import java.util.ArrayList;
+
+public class MockEditorActivity extends AppCompatActivity implements EditorFragmentListener,
+ EditorDragAndDropListener {
+ public static final int LAYOUT_ID = 999;
+
+ EditorFragment mEditorFragment;
+
+ @SuppressWarnings("ResourceType")
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ LinearLayout linearLayout = new LinearLayout(this);
+ linearLayout.setId(LAYOUT_ID);
+ setContentView(linearLayout);
+
+ FragmentManager fragmentManager = getFragmentManager();
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+
+ mEditorFragment = new EditorFragmentForTests();
+ fragmentTransaction.add(linearLayout.getId(), mEditorFragment, "editorFragment");
+ fragmentTransaction.commit();
+ }
+
+ @Override
+ public void onEditorFragmentInitialized() {
+ mEditorFragment.setTitle("A title");
+ mEditorFragment.setContent("<p>Example <strong>content</strong></p>");
+ }
+
+ @Override
+ public void onSettingsClicked() {
+
+ }
+
+ @Override
+ public void onAddMediaClicked() {
+
+ }
+
+ @Override
+ public void onMediaRetryClicked(String mediaId) {
+
+ }
+
+ @Override
+ public void onMediaUploadCancelClicked(String mediaId, boolean delete) {
+
+ }
+
+ @Override
+ public void onFeaturedImageChanged(long mediaId) {
+
+ }
+
+ @Override
+ public void onVideoPressInfoRequested(String videoId) {
+
+ }
+
+ @Override
+ public String onAuthHeaderRequested(String url) {
+ return "";
+ }
+
+ @Override
+ public void saveMediaFile(MediaFile mediaFile) {
+
+ }
+
+ @Override
+ public void onTrackableEvent(TrackableEvent event) {
+
+ }
+
+ @Override
+ public void onMediaDropped(ArrayList<Uri> mediaUri) {
+
+ }
+
+ @Override
+ public void onRequestDragAndDropPermissions(DragEvent dragEvent) {
+
+ }
+}
+
diff --git a/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/TestingUtils.java b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/TestingUtils.java
new file mode 100644
index 000000000..e51cba6ba
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/TestingUtils.java
@@ -0,0 +1,14 @@
+package org.wordpress.android.editor;
+
+import org.wordpress.android.util.AppLog;
+
+public class TestingUtils {
+
+ static public void waitFor(long milliseconds) {
+ try {
+ Thread.sleep(milliseconds);
+ } catch(InterruptedException e) {
+ AppLog.e(AppLog.T.EDITOR, "Thread interrupted");
+ }
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/ZssEditorTest.java b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/ZssEditorTest.java
new file mode 100644
index 000000000..9989cbd65
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/ZssEditorTest.java
@@ -0,0 +1,128 @@
+package org.wordpress.android.editor;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.os.Build;
+import android.test.ActivityInstrumentationTestCase2;
+import android.webkit.JavascriptInterface;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests for the <code>ZSSEditor</code> inside an <code>EditorWebViewAbstract</code>, with no UI.
+ */
+public class ZssEditorTest extends ActivityInstrumentationTestCase2<MockActivity> {
+ private static final String JS_CALLBACK_HANDLER = "nativeCallbackHandler";
+
+ private Instrumentation mInstrumentation;
+ private EditorWebViewAbstract mWebView;
+
+ private CountDownLatch mSetUpLatch;
+
+ private TestMethod mTestMethod;
+ private CountDownLatch mCallbackLatch;
+ private CountDownLatch mDomLoadedCallbackLatch;
+ private Set<String> mCallbackSet;
+
+ private enum TestMethod {
+ INIT
+ }
+
+ public ZssEditorTest() {
+ super(MockActivity.class);
+ }
+
+ @SuppressLint("AddJavascriptInterface")
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mInstrumentation = getInstrumentation();
+ Activity activity = getActivity();
+ mSetUpLatch = new CountDownLatch(1);
+ mDomLoadedCallbackLatch = new CountDownLatch(1);
+
+ mSetUpLatch.countDown();
+
+ 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);
+ 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();
+ }
+ });
+ }
+
+ public void testInitialization() throws InterruptedException {
+ // Wait for setUp() to finish initializing the WebView
+ mSetUpLatch.await();
+
+ // Identify this method to the MockJsCallbackReceiver
+ mTestMethod = TestMethod.INIT;
+
+ // Expecting three startup callbacks from the ZSS editor
+ mCallbackLatch = new CountDownLatch(3);
+ mCallbackSet = new HashSet<>();
+ boolean callbacksReceived = mCallbackLatch.await(5, TimeUnit.SECONDS);
+ assertTrue(callbacksReceived);
+
+ Set<String> 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:");
+
+ assertEquals(expectedSet, mCallbackSet);
+ }
+
+ 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")) {
+ // Notify test methods that the dom has loaded
+ mDomLoadedCallbackLatch.countDown();
+ }
+
+ // Handle callbacks and count down latches according to the currently running test
+ switch(mTestMethod) {
+ case INIT:
+ if (callbackId.equals("callback-dom-loaded")) {
+ mCallbackSet.add(callbackId + ":");
+ } else if (callbackId.equals("callback-new-field")) {
+ mCallbackSet.add(callbackId + ":" + params);
+ }
+ mCallbackLatch.countDown();
+ break;
+ default:
+ throw(new RuntimeException("Unknown calling method"));
+ }
+ }
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/AndroidManifest.xml b/libs/editor/WordPressEditor/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..fcade3209
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.wordpress.android.editor" >
+ <application>
+ <activity android:name=".legacy.EditLinkActivity" />
+ </application>>
+</manifest>
diff --git a/libs/editor/WordPressEditor/src/main/assets/ZSSRichTextEditor.js b/libs/editor/WordPressEditor/src/main/assets/ZSSRichTextEditor.js
new file mode 100755
index 000000000..52dabe1e8
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/ZSSRichTextEditor.js
@@ -0,0 +1,3847 @@
+/*!
+ *
+ * ZSSRichTextEditor v1.0
+ * http://www.zedsaid.com
+ *
+ * Copyright 2013 Zed Said Studio
+ *
+ */
+
+// If we are using iOS or desktop
+var isUsingiOS = false;
+var isUsingAndroid = true;
+
+// THe default callback parameter separator
+var defaultCallbackSeparator = '~';
+
+const NodeName = {
+ BLOCKQUOTE: "BLOCKQUOTE",
+ PARAGRAPH: "P",
+ STRONG: "STRONG",
+ DEL: "DEL",
+ EM: "EM",
+ A: "A",
+ OL: "OL",
+ UL: "UL",
+ LI: "LI",
+ CODE: "CODE",
+ SPAN: "SPAN",
+ BR: "BR",
+ DIV: "DIV",
+ BODY: "BODY"
+};
+
+// The editor object
+var ZSSEditor = {};
+
+// These variables exist to reduce garbage (as in memory garbage) generation when typing real fast
+// in the editor.
+//
+ZSSEditor.caretArguments = ['yOffset=' + 0, 'height=' + 0];
+ZSSEditor.caretInfo = { y: 0, height: 0 };
+
+// Is this device an iPad
+ZSSEditor.isiPad;
+
+// The current selection
+ZSSEditor.currentSelection;
+
+// The current editing image
+ZSSEditor.currentEditingImage;
+
+// The current editing video
+ZSSEditor.currentEditingVideo;
+
+// The current editing link
+ZSSEditor.currentEditingLink;
+
+ZSSEditor.focusedField = null;
+
+// The objects that are enabled
+ZSSEditor.enabledItems = {};
+
+ZSSEditor.editableFields = {};
+
+ZSSEditor.lastTappedNode = null;
+
+// The default paragraph separator
+ZSSEditor.defaultParagraphSeparator = 'div';
+
+// We use a MutationObserver to catch user deletions of uploading or failed media
+// This is only officially supported on API>18; 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
+ */
+ZSSEditor.init = function() {
+
+ rangy.init();
+
+ // Change a few CSS values if the device is an iPad
+ ZSSEditor.isiPad = (navigator.userAgent.match(/iPad/i) != null);
+ if (ZSSEditor.isiPad) {
+ $(document.body).addClass('ipad_body');
+ $('#zss_field_title').addClass('ipad_field_title');
+ $('#zss_field_content').addClass('ipad_field_content');
+ }
+
+ document.execCommand('insertBrOnReturn', false, false);
+
+ var editor = $('div.field').each(function() {
+ var editableField = new ZSSField($(this));
+ var editableFieldId = editableField.getNodeId();
+
+ ZSSEditor.editableFields[editableFieldId] = editableField;
+ ZSSEditor.callback("callback-new-field", "id=" + editableFieldId);
+ });
+
+ document.addEventListener("selectionchange", function(e) {
+ ZSSEditor.currentEditingLink = null;
+ // DRM: only do something here if the editor has focus. The reason is that when the
+ // selection changes due to the editor loosing focus, the focusout event will not be
+ // sent if we try to load a callback here.
+ //
+ if (editor.is(":focus")) {
+ ZSSEditor.selectionChangedCallback();
+ ZSSEditor.sendEnabledStyles(e);
+ var clicked = $(e.target);
+ if (!clicked.hasClass('zs_active')) {
+ $('img').removeClass('zs_active');
+ }
+ }
+ }, 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
+
+ZSSEditor.logMainElementSizes = function() {
+ msg = 'Window [w:' + $(window).width() + '|h:' + $(window).height() + ']';
+ this.log(msg);
+
+ var msg = encodeURIComponent('Viewport [w:' + window.innerWidth + '|h:' + window.innerHeight + ']');
+ this.log(msg);
+
+ msg = encodeURIComponent('Body [w:' + $(document.body).width() + '|h:' + $(document.body).height() + ']');
+ this.log(msg);
+
+ msg = encodeURIComponent('HTML [w:' + $('html').width() + '|h:' + $('html').height() + ']');
+ this.log(msg);
+
+ msg = encodeURIComponent('Document [w:' + $(document).width() + '|h:' + $(document).height() + ']');
+ this.log(msg);
+};
+
+// MARK: - Viewport Refreshing
+
+ZSSEditor.refreshVisibleViewportSize = function() {
+ $(document.body).css('min-height', window.innerHeight + 'px');
+ $('#zss_field_content').css('min-height', (window.innerHeight - $('#zss_field_content').position().top) + 'px');
+};
+
+// MARK: - Fields
+
+ZSSEditor.focusFirstEditableField = function() {
+ $('div[contenteditable=true]:first').focus();
+};
+
+ZSSEditor.formatNewLine = function(e) {
+
+ var currentField = this.getFocusedField();
+
+ if (currentField.isMultiline()) {
+ var parentBlockQuoteNode = ZSSEditor.closerParentNodeWithName('blockquote');
+
+ if (parentBlockQuoteNode) {
+ this.formatNewLineInsideBlockquote(e);
+ } else if (!ZSSEditor.isCommandEnabled('insertOrderedList')
+ && !ZSSEditor.isCommandEnabled('insertUnorderedList')) {
+ document.execCommand('formatBlock', false, 'div');
+ }
+ } else {
+ e.preventDefault();
+ }
+};
+
+ZSSEditor.formatNewLineInsideBlockquote = function(e) {
+ this.insertBreakTagAtCaretPosition();
+ e.preventDefault();
+};
+
+ZSSEditor.getField = function(fieldId) {
+
+ var field = this.editableFields[fieldId];
+
+ return field;
+};
+
+ZSSEditor.moveCaretToCoords = function(x, y) {
+ if (document.caretRangeFromPoint) {
+ var range = document.caretRangeFromPoint(x, y);
+
+ var selection = window.getSelection();
+
+ if (range && selection.rangeCount) {
+ selection.removeAllRanges();
+ selection.addRange(range);
+ }
+ }
+};
+
+ZSSEditor.getFocusedField = function() {
+ var currentField = $(this.findParentContenteditableDiv());
+ var currentFieldId;
+
+ if (currentField) {
+ currentFieldId = currentField.attr('id');
+ }
+
+ if (!currentFieldId) {
+ ZSSEditor.resetSelectionOnField('zss_field_content');
+ currentFieldId = 'zss_field_content';
+ }
+
+ return this.editableFields[currentFieldId];
+};
+
+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 (!removedNode.attributes) {
+ // Fix for https://github.com/wordpress-mobile/WordPress-Editor-Android/issues/394
+ // If removedNode doesn't have an attributes property, it's not of type Node and we shouldn't proceed
+ continue;
+ }
+ 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);
+ 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);
+ if (parentRange != null) {
+ ZSSEditor.setRange(parentRange);
+ }
+ ZSSEditor.sendMediaRemovedCallback(mediaIdentifier);
+ } else if (mutation.target.className == "edit-container") {
+ // A media item wrapped in an edit container was deleted manually - remove its container
+ // No callback in this case since it's not an uploading or failed media item
+ var parentRange = ZSSEditor.getParentRangeOfFocusedNode();
+
+ mutation.target.remove();
+
+ if (parentRange != null) {
+ ZSSEditor.setRange(parentRange);
+ }
+
+ ZSSEditor.getFocusedField().emptyFieldIfNoContents();
+ }
+ }
+ });
+};
+
+/**
+ * @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
+
+ZSSEditor.log = function(msg) {
+ ZSSEditor.callback('callback-log', 'msg=' + msg);
+};
+
+// MARK: - Callbacks
+
+ZSSEditor.domLoadedCallback = function() {
+
+ ZSSEditor.callback("callback-dom-loaded");
+};
+
+ZSSEditor.selectionChangedCallback = function () {
+
+ var joinedArguments = ZSSEditor.getJoinedFocusedFieldIdAndCaretArguments();
+
+ ZSSEditor.callback('callback-selection-changed', joinedArguments);
+ this.callback("callback-input", joinedArguments);
+};
+
+ZSSEditor.callback = function(callbackScheme, callbackPath) {
+
+ var url = callbackScheme + ":";
+
+ if (callbackPath) {
+ url = url + callbackPath;
+ }
+
+ if (isUsingiOS) {
+ ZSSEditor.callbackThroughIFrame(url);
+ } else if (isUsingAndroid) {
+ if (nativeState.androidApiLevel < 17) {
+ ZSSEditor.callbackThroughIFrame(url);
+ } else {
+ nativeCallbackHandler.executeCallback(callbackScheme, callbackPath);
+ }
+ } else {
+ console.log(url);
+ }
+};
+
+/**
+ * @brief Executes a callback by loading it into an IFrame.
+ * @details The reason why we're using this instead of window.location is that window.location
+ * can sometimes fail silently when called multiple times in rapid succession.
+ * Found here:
+ * http://stackoverflow.com/questions/10010342/clicking-on-a-link-inside-a-webview-that-will-trigger-a-native-ios-screen-with/10080969#10080969
+ *
+ * @param url The callback URL.
+ */
+ZSSEditor.callbackThroughIFrame = function(url) {
+ var iframe = document.createElement("IFRAME");
+ iframe.setAttribute('sandbox', '');
+ iframe.setAttribute("src", url);
+
+ // IMPORTANT: the IFrame was showing up as a black box below our text. By setting its borders
+ // to be 0px transparent we make sure it's not shown at all.
+ //
+ // REF BUG: https://github.com/wordpress-mobile/WordPress-iOS-Editor/issues/318
+ //
+ iframe.style.cssText = "border: 0px transparent;";
+
+ document.documentElement.appendChild(iframe);
+ iframe.parentNode.removeChild(iframe);
+ iframe = null;
+};
+
+ZSSEditor.stylesCallback = function(stylesArray) {
+
+ var stylesString = '';
+
+ if (stylesArray.length > 0) {
+ stylesString = stylesArray.join(defaultCallbackSeparator);
+ }
+
+ ZSSEditor.callback("callback-selection-style", stylesString);
+};
+
+// MARK: - Selection
+
+ZSSEditor.backupRange = function(){
+ var selection = window.getSelection();
+ if (selection.rangeCount < 1) {
+ return;
+ }
+ var range = selection.getRangeAt(0);
+
+ ZSSEditor.currentSelection =
+ {
+ "startContainer": range.startContainer,
+ "startOffset": range.startOffset,
+ "endContainer": range.endContainer,
+ "endOffset": range.endOffset
+ };
+};
+
+ZSSEditor.restoreRange = function(){
+ if (this.currentSelection) {
+ var selection = window.getSelection();
+ selection.removeAllRanges();
+
+ var range = document.createRange();
+ range.setStart(this.currentSelection.startContainer, this.currentSelection.startOffset);
+ range.setEnd(this.currentSelection.endContainer, this.currentSelection.endOffset);
+ selection.addRange(range);
+ }
+};
+
+ZSSEditor.resetSelectionOnField = function(fieldId, offset) {
+ var query = "div#" + fieldId;
+ var field = document.querySelector(query);
+
+ this.giveFocusToElement(field, offset);
+};
+
+ZSSEditor.giveFocusToElement = function(element, offset) {
+ offset = typeof offset !== 'undefined' ? offset : 0;
+
+ var range = document.createRange();
+ range.setStart(element, offset);
+ range.setEnd(element, offset);
+
+ var selection = document.getSelection();
+ selection.removeAllRanges();
+ selection.addRange(range);
+};
+
+ZSSEditor.setFocusAfterElement = function(element) {
+ var selection = window.getSelection();
+
+ if (selection.rangeCount) {
+ var range = document.createRange();
+
+ range.setStartAfter(element);
+ range.setEndAfter(element);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ }
+};
+
+ZSSEditor.getSelectedText = function() {
+ var selection = window.getSelection();
+ return selection.toString();
+};
+
+ZSSEditor.selectWordAroundCursor = function() {
+ var selection = window.getSelection();
+ // If there is no text selected, try to expand it to the word under the cursor
+ if (selection.rangeCount == 1) {
+ var range = selection.getRangeAt(0);
+ if (range.startOffset == range.endOffset) {
+ while (ZSSEditor.canExpandBackward(range)) {
+ range.setStart(range.startContainer, range.startOffset - 1);
+ }
+ while (ZSSEditor.canExpandForward(range)) {
+ range.setEnd(range.endContainer, range.endOffset + 1);
+ }
+ selection.removeAllRanges();
+ selection.addRange(range);
+ }
+ }
+ return selection;
+};
+
+ZSSEditor.canExpandBackward = function(range) {
+ // Can't expand if focus is not a text node
+ if (!range.endContainer.nodeType == 3) {
+ return false;
+ }
+ var caretRange = range.cloneRange();
+ if (range.startOffset == 0) {
+ return false;
+ }
+ caretRange.setStart(range.startContainer, range.startOffset - 1);
+ caretRange.setEnd(range.startContainer, range.startOffset);
+ if (!caretRange.toString().match(/\w/)) {
+ return false;
+ }
+ return true;
+};
+
+ZSSEditor.canExpandForward = function(range) {
+ // Can't expand if focus is not a text node
+ if (!range.endContainer.nodeType == 3) {
+ return false;
+ }
+ var caretRange = range.cloneRange();
+ if (range.endOffset == range.endContainer.length) {
+ return false;
+ }
+ caretRange.setStart(range.endContainer, range.endOffset);
+ if (range.endOffset) {
+ caretRange.setEnd(range.endContainer, range.endOffset + 1);
+ }
+ if (!caretRange.toString().match(/\w/)) {
+ return false;
+ }
+ return true;
+};
+
+ZSSEditor.getSelectedTextToLinkify = function() {
+ ZSSEditor.selectWordAroundCursor();
+ return document.getSelection().toString();
+};
+
+ZSSEditor.getCaretArguments = function() {
+ var caretInfo = this.getYCaretInfo();
+
+ if (caretInfo == null) {
+ return null;
+ } else {
+ this.caretArguments[0] = 'yOffset=' + caretInfo.y;
+ this.caretArguments[1] = 'height=' + caretInfo.height;
+ return this.caretArguments;
+ }
+};
+
+ZSSEditor.getJoinedFocusedFieldIdAndCaretArguments = function() {
+ var joinedArguments = ZSSEditor.getJoinedCaretArguments();
+ var idArgument = "id=" + ZSSEditor.getFocusedField().getNodeId();
+
+ joinedArguments = idArgument + defaultCallbackSeparator + joinedArguments;
+
+ return joinedArguments;
+};
+
+ZSSEditor.getJoinedCaretArguments = function() {
+
+ var caretArguments = this.getCaretArguments();
+ var joinedArguments = this.caretArguments.join(defaultCallbackSeparator);
+
+ return joinedArguments;
+};
+
+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
+ // adding a zero-width space character
+ span.appendChild( document.createTextNode("\u200b") );
+ range.insertNode(span);
+ var y = span.offsetTop;
+ var spanParent = span.parentNode;
+ spanParent.removeChild(span);
+
+ // Glue any broken text nodes back together
+ spanParent.normalize();
+
+ return y;
+}
+
+ZSSEditor.getYCaretInfo = function() {
+ var selection = window.getSelection();
+ var noSelectionAvailable = selection.rangeCount == 0;
+
+ if (noSelectionAvailable) {
+ return null;
+ }
+
+ var y = 0;
+ var height = 0;
+ var range = selection.getRangeAt(0);
+ var needsToWorkAroundNewlineBug = (range.getClientRects().length == 0);
+
+ // PROBLEM: iOS seems to have problems getting the offset for some empty nodes and return
+ // 0 (zero) as the selection range top offset.
+ //
+ // WORKAROUND: To fix this problem we use a different method to obtain the Y position instead.
+ //
+ if (needsToWorkAroundNewlineBug) {
+ var closerParentNode = ZSSEditor.closerParentNode();
+ var closerDiv = ZSSEditor.findParentContenteditableDiv();
+
+ var fontSize = $(closerParentNode).css('font-size');
+ var lineHeight = Math.floor(parseInt(fontSize.replace('px','')) * 1.5);
+
+ y = this.getCaretYPosition();
+ height = lineHeight;
+ } else {
+ if (range.getClientRects) {
+ var rects = range.getClientRects();
+ if (rects.length > 0) {
+ // PROBLEM: some iOS versions differ in what is returned by getClientRects()
+ // Some versions return the offset from the page's top, some other return the
+ // offset from the visible viewport's top.
+ //
+ // WORKAROUND: see if the offset of the body's top is ever negative. If it is
+ // then it means that the offset we have is relative to the body's top, and we
+ // should add the scroll offset.
+ //
+ var addsScrollOffset = document.body.getClientRects()[0].top < 0;
+
+ if (addsScrollOffset) {
+ y = document.body.scrollTop;
+ }
+
+ y += rects[0].top;
+ height = rects[0].height;
+ }
+ }
+ }
+
+ this.caretInfo.y = y;
+ this.caretInfo.height = height;
+
+ return this.caretInfo;
+};
+
+// MARK: - Styles
+
+ZSSEditor.setBold = function() {
+ ZSSEditor.selectWordAroundCursor();
+ document.execCommand('bold', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setItalic = function() {
+ ZSSEditor.selectWordAroundCursor();
+ document.execCommand('italic', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setSubscript = function() {
+ ZSSEditor.selectWordAroundCursor();
+ document.execCommand('subscript', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setSuperscript = function() {
+ ZSSEditor.selectWordAroundCursor();
+ document.execCommand('superscript', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setStrikeThrough = function() {
+ ZSSEditor.selectWordAroundCursor();
+ var commandName = 'strikeThrough';
+ var isDisablingStrikeThrough = ZSSEditor.isCommandEnabled(commandName);
+
+ document.execCommand(commandName, false, null);
+
+ // DRM: WebKit has a problem disabling strikeThrough when the tag <del> is used instead of
+ // <strike>. The code below serves as a way to fix this issue.
+ //
+ var mustHandleWebKitIssue = (isDisablingStrikeThrough
+ && ZSSEditor.isCommandEnabled(commandName));
+
+ if (mustHandleWebKitIssue && window.getSelection().rangeCount > 0) {
+ var troublesomeNodeNames = ['del'];
+
+ var selection = window.getSelection();
+ var range = selection.getRangeAt(0).cloneRange();
+
+ var container = range.commonAncestorContainer;
+ var nodeFound = false;
+ var textNode = null;
+
+ while (container && !nodeFound) {
+ nodeFound = (container
+ && container.nodeType == document.ELEMENT_NODE
+ && troublesomeNodeNames.indexOf(container.nodeName.toLowerCase()) > -1);
+
+ if (!nodeFound) {
+ container = container.parentElement;
+ }
+ }
+
+ if (container) {
+ var newObject = $(container).replaceWith(container.innerHTML);
+
+ var finalSelection = window.getSelection();
+ var finalRange = selection.getRangeAt(0).cloneRange();
+
+ finalRange.setEnd(finalRange.startContainer, finalRange.startOffset + 1);
+
+ selection.removeAllRanges();
+ selection.addRange(finalRange);
+ }
+ }
+
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setUnderline = function() {
+ ZSSEditor.selectWordAroundCursor();
+ document.execCommand('underline', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+/**
+ * @brief Turns blockquote ON or OFF for the current selection.
+ * @details This method makes sure that the contents of the blockquotes are surrounded by the
+ * defaultParagraphSeparator tag (by default '<p>'). This ensures parity with the web
+ * editor.
+ */
+ZSSEditor.setBlockquote = function() {
+
+ var savedSelection = rangy.saveSelection();
+ var selection = document.getSelection();
+ var range = selection.getRangeAt(0).cloneRange();
+ var sendStyles = false;
+
+ // Make sure text being wrapped in blockquotes is inside paragraph tags
+ // (should be <blockquote><paragraph>contents</paragraph></blockquote>)
+ var currentHtml = ZSSEditor.focusedField.getWrappedDomNode().innerHTML;
+ if (currentHtml.search('<' + ZSSEditor.defaultParagraphSeparator) == -1) {
+ ZSSEditor.focusedField.setHTML(Util.wrapHTMLInTag(currentHtml, ZSSEditor.defaultParagraphSeparator));
+ }
+
+ var ancestorElement = this.getAncestorElementForSettingBlockquote(range);
+
+ if (ancestorElement) {
+ sendStyles = true;
+
+ var childNodes = this.getChildNodesIntersectingRange(ancestorElement, range);
+
+ // On older APIs, the rangy selection node is targeted when turning off empty blockquotes at the start of a post
+ // In that case, add the empty DIV element next to the rangy selection to the childNodes array to correctly
+ // turn the blockquote off
+ // https://github.com/wordpress-mobile/WordPress-Editor-Android/issues/401
+ var nextChildNode = childNodes[childNodes.length-1].nextSibling;
+ if (nextChildNode && nextChildNode.nodeName == NodeName.DIV && nextChildNode.innerHTML == "") {
+ childNodes.push(nextChildNode);
+ }
+
+ if (childNodes && childNodes.length) {
+ this.toggleBlockquoteForSpecificChildNodes(ancestorElement, childNodes);
+ }
+ }
+
+ rangy.restoreSelection(savedSelection);
+
+ // When turning off an empty blockquote in an empty post, ensure there aren't any leftover empty paragraph tags
+ // https://github.com/wordpress-mobile/WordPress-Editor-Android/issues/401
+ var currentContenteditableDiv = ZSSEditor.focusedField.getWrappedDomNode();
+ if (currentContenteditableDiv.children.length == 1 && currentContenteditableDiv.firstChild.innerHTML == "") {
+ ZSSEditor.focusedField.emptyFieldIfNoContents();
+ }
+
+ if (sendStyles) {
+ ZSSEditor.sendEnabledStyles();
+ }
+};
+
+ZSSEditor.removeFormating = function() {
+ document.execCommand('removeFormat', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setHorizontalRule = function() {
+ document.execCommand('insertHorizontalRule', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setHeading = function(heading) {
+ var formatTag = heading;
+ var formatBlock = document.queryCommandValue('formatBlock');
+
+ if (formatBlock.length > 0 && formatBlock.toLowerCase() == formatTag) {
+ document.execCommand('formatBlock', false, Util.buildOpeningTag(this.defaultParagraphSeparator));
+ } else {
+ document.execCommand('formatBlock', false, Util.buildOpeningTag(formatTag));
+ }
+
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setParagraph = function() {
+ var formatTag = "div";
+ var formatBlock = document.queryCommandValue('formatBlock');
+
+ if (formatBlock.length > 0 && formatBlock.toLowerCase() == formatTag) {
+ document.execCommand('formatBlock', false, Util.buildOpeningTag(this.defaultParagraphSeparator));
+ } else {
+ document.execCommand('formatBlock', false, Util.buildOpeningTag(formatTag));
+ }
+
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.undo = function() {
+ document.execCommand('undo', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.redo = function() {
+ document.execCommand('redo', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setOrderedList = function() {
+ document.execCommand('insertOrderedList', false, null);
+
+ // If the insertOrderedList is no longer enabled after running execCommand,
+ // we can assume the user is turning it off.
+ if (!ZSSEditor.isCommandEnabled('insertOrderedList')) {
+ ZSSEditor.completeListEditing();
+ }
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setUnorderedList = function() {
+ document.execCommand('insertUnorderedList', false, null);
+
+ // If the insertUnorderedList is no longer enabled after running execCommand,
+ // we can assume the user is turning it off.
+ if (!ZSSEditor.isCommandEnabled('insertUnorderedList')) {
+ ZSSEditor.completeListEditing();
+ }
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setJustifyCenter = function() {
+ document.execCommand('justifyCenter', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setJustifyFull = function() {
+ document.execCommand('justifyFull', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setJustifyLeft = function() {
+ document.execCommand('justifyLeft', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setJustifyRight = function() {
+ document.execCommand('justifyRight', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setIndent = function() {
+ document.execCommand('indent', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setOutdent = function() {
+ document.execCommand('outdent', false, null);
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.setTextColor = function(color) {
+ ZSSEditor.selectWordAroundCursor();
+ ZSSEditor.restoreRange();
+ document.execCommand("styleWithCSS", null, true);
+ document.execCommand('foreColor', false, color);
+ document.execCommand("styleWithCSS", null, false);
+ ZSSEditor.sendEnabledStyles();
+ // document.execCommand("removeFormat", false, "foreColor"); // Removes just foreColor
+};
+
+ZSSEditor.setBackgroundColor = function(color) {
+ ZSSEditor.selectWordAroundCursor();
+ ZSSEditor.restoreRange();
+ document.execCommand("styleWithCSS", null, true);
+ document.execCommand('hiliteColor', false, color);
+ document.execCommand("styleWithCSS", null, false);
+ ZSSEditor.sendEnabledStyles();
+};
+
+/**
+ * @brief Wraps given HTML in paragraph tags, appends a new line, and inserts it into the field
+ * @details This method makes sure that passed HTML is wrapped in a separate paragraph.
+ * It also appends a new opening paragraph tag and a space. This step is necessary to keep any spans or
+ * divs in the HTML from being read by the WebView as a style and applied to all future paragraphs.
+ */
+ZSSEditor.insertHTMLWrappedInParagraphTags = function(html) {
+ var space = '<br>';
+ var paragraphOpenTag = Util.buildOpeningTag(this.defaultParagraphSeparator);
+ var paragraphCloseTag = Util.buildClosingTag(this.defaultParagraphSeparator);
+
+ if (this.getFocusedField().getHTML().length == 0) {
+ html = paragraphOpenTag + html;
+ }
+
+ // Without this line, API<19 WebView will reset the caret to the start of the document, inserting the new line
+ // there instead of under the newly added media item
+ if (nativeState.androidApiLevel < 19) {
+ html = html + '&#x200b;';
+ }
+
+ // Due to the way the WebView handles divs, we need to add a new paragraph in a separate insertion - otherwise,
+ // the new paragraph will be nested within the existing paragraph.
+ this.insertHTML(html);
+
+ this.insertHTML(paragraphOpenTag + space + paragraphCloseTag);
+};
+
+ZSSEditor.insertLink = function(url, title) {
+ var html = '<a href="' + url + '">' + title + "</a>";
+
+ var parentBlockQuoteNode = ZSSEditor.closerParentNodeWithName('blockquote');
+
+ var currentRange = document.getSelection().getRangeAt(0);
+ var currentNode = currentRange.startContainer;
+ var currentNodeIsEmpty = (currentNode.innerHTML == '' || currentNode.innerHTML == '<br>');
+
+ var selectionIsAtStartOrEnd = Util.rangeIsAtStartOfParent(currentRange) || Util.rangeIsAtEndOfParent(currentRange);
+
+ if (this.getFocusedField().getHTML().length == 0
+ || (parentBlockQuoteNode && !currentNodeIsEmpty && selectionIsAtStartOrEnd)) {
+ // Wrap the link tag in paragraph tags when the post is empty, and also when inside a blockquote
+ // The latter is to fix a bug with document.execCommand('insertHTML') inside a blockquote, where the div inside
+ // the blockquote is ignored and the link tag is inserted outside it, on a new line with no wrapping div
+ // Wrapping the link in paragraph tags makes insertHTML join it to the existing div, for some reason
+ // We exclude being on an empty line inside a blockquote and when the selection isn't at the beginning or end
+ // of the line, as the fix is unnecessary in both those cases and causes paragraph formatting issues
+ html = Util.buildOpeningTag(this.defaultParagraphSeparator) + html;
+ }
+
+ this.insertHTML(html);
+};
+
+ZSSEditor.updateLink = function(url, title) {
+
+ ZSSEditor.restoreRange();
+
+ var currentLinkNode = ZSSEditor.lastTappedNode;
+
+ if (currentLinkNode) {
+ currentLinkNode.setAttribute("href", url);
+ currentLinkNode.innerHTML = title;
+ }
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.unlink = function() {
+ var savedSelection = rangy.saveSelection();
+
+ var currentLinkNode = ZSSEditor.closerParentNodeWithName('a');
+
+ if (currentLinkNode) {
+ ZSSEditor.unwrapNode(currentLinkNode);
+ }
+
+ rangy.restoreSelection(savedSelection);
+
+ ZSSEditor.sendEnabledStyles();
+};
+
+ZSSEditor.unwrapNode = function(node) {
+ $(node).contents().unwrap();
+};
+
+ZSSEditor.quickLink = function() {
+
+ var sel = document.getSelection();
+ var link_url = "";
+ var test = new String(sel);
+ var mailregexp = new RegExp("^(.+)(\@)(.+)$", "gi");
+ if (test.search(mailregexp) == -1) {
+ checkhttplink = new RegExp("^http\:\/\/", "gi");
+ if (test.search(checkhttplink) == -1) {
+ checkanchorlink = new RegExp("^\#", "gi");
+ if (test.search(checkanchorlink) == -1) {
+ link_url = "http://" + sel;
+ } else {
+ link_url = sel;
+ }
+ } else {
+ link_url = sel;
+ }
+ } else {
+ checkmaillink = new RegExp("^mailto\:", "gi");
+ if (test.search(checkmaillink) == -1) {
+ link_url = "mailto:" + sel;
+ } else {
+ link_url = sel;
+ }
+ }
+
+ var html_code = '<a href="' + link_url + '">' + sel + '</a>';
+ ZSSEditor.insertHTML(html_code);
+};
+
+// MARK: - Blockquotes
+
+/**
+ * @brief This method toggles blockquote for the specified child nodes. This is useful since
+ * we can toggle blockquote either for some or ALL of the child nodes, depending on
+ * what we need to achieve.
+ * @details CASE 1: If the parent node is a blockquote node, the child nodes will be extracted
+ * from it leaving the remaining siblings untouched (by splitting the parent blockquote
+ * node in two if necessary).
+ * CASE 2: If the parent node is NOT a blockquote node, but the first child is, the
+ * method will make sure all child nodes that are blockquote nodes will be toggled to
+ * non-blockquote nodes.
+ * CASE 3: If both the parent node and the first node are non-blockquote nodes, this
+ * method will turn all child nodes into blockquote nodes.
+ *
+ * @param parentNode The parent node. Can be either a blockquote or non-blockquote node.
+ * Cannot be null.
+ * @param nodes The child nodes. Can be any combination of blockquote and
+ * non-blockquote nodes. Cannot be null.
+ */
+ZSSEditor.toggleBlockquoteForSpecificChildNodes = function(parentNode, nodes) {
+
+ if (nodes && nodes.length > 0) {
+ if (parentNode.nodeName == NodeName.BLOCKQUOTE) {
+ for (var counter = 0; counter < nodes.length; counter++) {
+ this.turnBlockquoteOffForNode(nodes[counter]);
+ }
+ } else {
+
+ var turnOn = (nodes[0].nodeName != NodeName.BLOCKQUOTE);
+
+ for (var counter = 0; counter < nodes.length; counter++) {
+ if (turnOn) {
+ this.turnBlockquoteOnForNode(nodes[counter]);
+ } else {
+ this.turnBlockquoteOffForNode(nodes[counter]);
+ }
+ }
+ }
+ }
+};
+
+
+/**
+ * @brief Turns blockquote off for the specified node.
+ *
+ * @param node The node to turn the blockquote off for. It can either be a blockquote
+ * node (in which case it will be removed and all child nodes extracted) or
+ * have a parent blockquote node (in which case the node will be extracted
+ * from its parent).
+ */
+ZSSEditor.turnBlockquoteOffForNode = function(node) {
+
+ if (node.nodeName == NodeName.BLOCKQUOTE) {
+ for (var i = 0; i < node.childNodes.length; i++) {
+ this.extractNodeFromAncestorNode(node.childNodes[i], node);
+ }
+ } else {
+ if (node.parentNode.nodeName == NodeName.BLOCKQUOTE) {
+ this.extractNodeFromAncestorNode(node, node.parentNode);
+ }
+ }
+};
+
+/**
+ * @brief Turns blockquote on for the specified node.
+ *
+ * @param node The node to turn blockquote on for. Will attempt to attach the newly
+ * created blockquote to sibling or uncle blockquote nodes. If the node is
+ * null or it's parent is null, this method will exit without affecting it
+ * (this can actually be caused by this method modifying the surrounding
+ * nodes, if those nodes are stored in an array - and thus are not notified
+ * of DOM hierarchy changes).
+ */
+ZSSEditor.turnBlockquoteOnForNode = function(node) {
+
+ if (!node || !node.parentNode) {
+ return;
+ }
+
+ var couldJoinBlockquotes = this.joinAdjacentSiblingsOrAncestorBlockquotes(node);
+
+ if (!couldJoinBlockquotes) {
+ var blockquote = document.createElement(NodeName.BLOCKQUOTE);
+
+ node.parentNode.insertBefore(blockquote, node);
+ blockquote.appendChild(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.getMediaNodeWithIdentifier = function(mediaNodeIdentifier) {
+ var imageNode = ZSSEditor.getImageNodeWithIdentifier(mediaNodeIdentifier);
+ if (imageNode.length > 0) {
+ return imageNode;
+ } else {
+ return ZSSEditor.getVideoNodeWithIdentifier(mediaNodeIdentifier);
+ }
+};
+
+ZSSEditor.getMediaProgressNodeWithIdentifier = function(mediaNodeIdentifier) {
+ var imageProgressNode = ZSSEditor.getImageProgressNodeWithIdentifier(mediaNodeIdentifier);
+ if (imageProgressNode.length > 0) {
+ return imageProgressNode;
+ } else {
+ return ZSSEditor.getVideoProgressNodeWithIdentifier(mediaNodeIdentifier);
+ }
+};
+
+ZSSEditor.getMediaContainerNodeWithIdentifier = function(mediaNodeIdentifier) {
+ var imageContainerNode = ZSSEditor.getImageContainerNodeWithIdentifier(mediaNodeIdentifier);
+ if (imageContainerNode.length > 0) {
+ return imageContainerNode;
+ } else {
+ return ZSSEditor.getVideoContainerNodeWithIdentifier(mediaNodeIdentifier);
+ }
+};
+
+/**
+ * @brief Update the progress indicator for the media item identified with the value in progress.
+ *
+ * @param mediaNodeIdentifier This is a unique ID provided by the caller.
+ * @param progress A value between 0 and 1 indicating the progress on the media upload.
+ */
+ZSSEditor.setProgressOnMedia = function(mediaNodeIdentifier, progress) {
+ var mediaNode = this.getMediaNodeWithIdentifier(mediaNodeIdentifier);
+ var mediaProgressNode = this.getMediaProgressNodeWithIdentifier(mediaNodeIdentifier);
+
+ if (progress == 0) {
+ mediaNode.addClass("uploading");
+ }
+
+ // Don't allow the progress bar to move backward
+ if (mediaNode.length == 0 || mediaProgressNode.length == 0 || mediaProgressNode.attr("value") > progress) {
+ return;
+ }
+
+ // Revert to non-compatibility image container once image upload has begun. This centers the overlays on the image
+ // (instead of the screen), while still circumventing the small container bug the compat class was added to fix
+ if (progress > 0) {
+ this.getMediaContainerNodeWithIdentifier(mediaNodeIdentifier).removeClass("compat");
+ }
+
+ // Sometimes the progress bar can be stuck at 100% for a long time while further processing happens
+ // From a UX perspective, it's better to just keep the progress bars at 90% until the upload is really complete
+ // and the progress bar is removed entirely
+ if (progress > 0.9) {
+ return;
+ }
+
+ mediaProgressNode.attr("value", progress);
+};
+
+ZSSEditor.setupOptimisticProgressUpdate = function(mediaNodeIdentifier, nCall) {
+ setTimeout(ZSSEditor.sendOptimisticProgressUpdate, nCall * 100, mediaNodeIdentifier, nCall);
+};
+
+ZSSEditor.sendOptimisticProgressUpdate = function(mediaNodeIdentifier, nCall) {
+ if (nCall > 15) {
+ return;
+ }
+
+ var mediaNode = ZSSEditor.getMediaNodeWithIdentifier(mediaNodeIdentifier);
+
+ // Don't send progress updates to failed media
+ if (mediaNode.length != 0 && mediaNode[0].classList.contains("failed")) {
+ return;
+ }
+
+ ZSSEditor.setProgressOnMedia(mediaNodeIdentifier, nCall / 100);
+ ZSSEditor.setupOptimisticProgressUpdate(mediaNodeIdentifier, nCall + 1);
+};
+
+ZSSEditor.removeAllFailedMediaUploads = function() {
+ console.log("Remove all failed media");
+ var failedMediaArray = ZSSEditor.getFailedMediaIdArray();
+ for (var i = 0; i < failedMediaArray.length; i++) {
+ ZSSEditor.removeMedia(failedMediaArray[i]);
+ }
+};
+
+ZSSEditor.removeMedia = function(mediaNodeIdentifier) {
+ if (this.getImageNodeWithIdentifier(mediaNodeIdentifier).length != 0) {
+ this.removeImage(mediaNodeIdentifier);
+ } else if (this.getVideoNodeWithIdentifier(mediaNodeIdentifier).length != 0) {
+ this.removeVideo(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);
+ }
+ }
+};
+
+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 mediaIdArray = [];
+
+ for (var i = 0; i < matches.size(); i++) {
+ 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");
+ }
+ 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);
+};
+
+// MARK: - Images
+
+ZSSEditor.updateImage = function(url, alt) {
+
+ ZSSEditor.restoreRange();
+
+ if (ZSSEditor.currentEditingImage) {
+ var c = ZSSEditor.currentEditingImage;
+ c.attr('src', url);
+ c.attr('alt', alt);
+ }
+ ZSSEditor.sendEnabledStyles();
+
+};
+
+ZSSEditor.insertImage = function(url, remoteId, alt) {
+ var html = '<img src="' + url + '" class="wp-image-' + remoteId + ' alignnone size-full';
+ if (alt) {
+ html += '" alt="' + alt;
+ }
+ html += '"/>';
+
+ this.insertHTMLWrappedInParagraphTags(html);
+
+ this.sendEnabledStyles();
+ this.callback("callback-action-finished");
+};
+
+/**
+ * @brief Inserts a local image URL. Useful for images that need to be uploaded.
+ * @details By inserting a local image URL, we can make sure the image is shown to the user
+ * as soon as it's selected for uploading. Once the image is successfully uploaded
+ * the application should call replaceLocalImageWithRemoteImage().
+ *
+ * @param imageNodeIdentifier This is a unique ID provided by the caller. It exists as
+ * a mechanism to update the image node with the remote URL
+ * when replaceLocalImageWithRemoteImage() is called.
+ * @param localImageUrl The URL of the local image to display. Please keep in mind
+ * that a remote URL can be used here too, since this method
+ * does not check for that. It would be a mistake.
+ */
+ZSSEditor.insertLocalImage = function(imageNodeIdentifier, localImageUrl) {
+ var progressIdentifier = this.getImageProgressIdentifier(imageNodeIdentifier);
+ var imageContainerIdentifier = this.getImageContainerIdentifier(imageNodeIdentifier);
+
+ if (nativeState.androidApiLevel > 18) {
+ var imgContainerClass = 'img_container';
+ var progressElement = '<progress id="' + progressIdentifier + '" value=0 class="wp_media_indicator" contenteditable="false"></progress>';
+ } else {
+ // Before API 19, the WebView didn't support progress tags. Use an upload overlay instead of a progress bar
+ var imgContainerClass = 'img_container compat';
+ var progressElement = '<span class="upload-overlay" contenteditable="false">' + nativeState.localizedStringUploading
+ + '</span><span class="upload-overlay-bg"></span>';
+ }
+
+ var imgContainerStart = '<span id="' + imageContainerIdentifier + '" class="' + imgContainerClass
+ + '" contenteditable="false">';
+ var imgContainerEnd = '</span>';
+ var image = '<img data-wpid="' + imageNodeIdentifier + '" src="' + localImageUrl + '" alt="" />';
+ var html = imgContainerStart + progressElement + image + imgContainerEnd;
+
+ this.insertHTMLWrappedInParagraphTags(html);
+
+ ZSSEditor.trackNodeForMutation(this.getImageContainerNodeWithIdentifier(imageNodeIdentifier));
+
+ this.setProgressOnMedia(imageNodeIdentifier, 0);
+
+ if (nativeState.androidApiLevel > 18) {
+ setTimeout(ZSSEditor.setupOptimisticProgressUpdate, 300, imageNodeIdentifier, 1);
+ }
+
+ this.sendEnabledStyles();
+};
+
+ZSSEditor.getImageNodeWithIdentifier = function(imageNodeIdentifier) {
+ return $('img[data-wpid="' + imageNodeIdentifier+'"]');
+};
+
+ZSSEditor.getImageProgressIdentifier = function(imageNodeIdentifier) {
+ return 'progress_' + imageNodeIdentifier;
+};
+
+ZSSEditor.getImageProgressNodeWithIdentifier = function(imageNodeIdentifier) {
+ return $('#'+this.getImageProgressIdentifier(imageNodeIdentifier));
+};
+
+ZSSEditor.getImageContainerIdentifier = function(imageNodeIdentifier) {
+ return 'img_container_' + imageNodeIdentifier;
+};
+
+ZSSEditor.getImageContainerNodeWithIdentifier = function(imageNodeIdentifier) {
+ return $('#'+this.getImageContainerIdentifier(imageNodeIdentifier));
+};
+
+/**
+ * @brief Replaces a local image URL with a remote image URL. Useful for images that have
+ * just finished uploading.
+ * @details The remote image can be available after a while, when uploading images. This method
+ * allows for the remote URL to be loaded once the upload completes.
+ *
+ * @param imageNodeIdentifier This is a unique ID provided by the caller. It exists as
+ * a mechanism to update the image node with the remote URL
+ * when replaceLocalImageWithRemoteImage() is called.
+ * @param remoteImageUrl The URL of the remote image to display.
+ */
+ZSSEditor.replaceLocalImageWithRemoteImage = function(imageNodeIdentifier, remoteImageId, remoteImageUrl) {
+ var imageNode = this.getImageNodeWithIdentifier(imageNodeIdentifier);
+
+ if (imageNode.length == 0) {
+ // even if the image is not present anymore we must do callback
+ this.markImageUploadDone(imageNodeIdentifier);
+ return;
+ }
+
+ var image = new Image;
+
+ image.onload = function () {
+ ZSSEditor.finishLocalImageSwap(image, imageNode, imageNodeIdentifier, remoteImageId)
+ image.classList.add("image-loaded");
+ console.log("Image Loaded!");
+ }
+
+ image.onerror = function () {
+ // 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);
+ // Set extra attributes and classes used by WordPress
+ imageNode.attr({'width': image.width, 'height': image.height});
+ imageNode.addClass("alignnone size-full");
+ 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 Notifies that the image upload as finished
+ *
+ * @param imageNodeIdentifier The unique image ID for the uploaded image
+ */
+ZSSEditor.markImageUploadDone = function(imageNodeIdentifier) {
+ var imageNode = this.getImageNodeWithIdentifier(imageNodeIdentifier);
+ if (imageNode.length == 0){
+ return;
+ }
+
+ // remove identifier attributed from image
+ imageNode.removeAttr('data-wpid');
+
+ // remove uploading style
+ imageNode.removeClass("uploading");
+
+ // 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 link = $('<a>', { 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);
+};
+
+/**
+ * @brief Callbacks to native that the image upload as finished and the local url was replaced by the remote url
+ *
+ * @param imageNodeIdentifier The unique image ID for the uploaded image
+ */
+ZSSEditor.sendImageReplacedCallback = function( imageNodeIdentifier ) {
+ var arguments = ['id=' + encodeURIComponent( imageNodeIdentifier )];
+
+ var joinedArguments = arguments.join( defaultCallbackSeparator );
+
+ this.callback("callback-image-replaced", joinedArguments);
+};
+
+/**
+ * @brief Marks the image as failed to upload
+ *
+ * @param imageNodeIdentifier This is a unique ID provided by the caller.
+ * @param message A message to show to the user, overlayed on the image
+ */
+ZSSEditor.markImageUploadFailed = function(imageNodeIdentifier, message) {
+ var imageNode = this.getImageNodeWithIdentifier(imageNodeIdentifier);
+ if (imageNode.length == 0){
+ return;
+ }
+
+ var sizeClass = '';
+ if ( imageNode[0].width > 480 && imageNode[0].height > 240 ) {
+ sizeClass = "largeFail";
+ } else if ( imageNode[0].width < 100 || imageNode[0].height < 100 ) {
+ sizeClass = "smallFail";
+ }
+
+ imageNode.addClass('failed');
+
+ var imageContainerNode = this.getImageContainerNodeWithIdentifier(imageNodeIdentifier);
+ if(imageContainerNode.length != 0){
+ imageContainerNode.attr("data-failed", message);
+ imageNode.removeClass("uploading");
+ imageContainerNode.addClass('failed');
+ imageContainerNode.addClass(sizeClass);
+ }
+
+ var imageProgressNode = this.getImageProgressNodeWithIdentifier(imageNodeIdentifier);
+ if (imageProgressNode.length != 0){
+ imageProgressNode.addClass('failed');
+ imageProgressNode.attr("value", 0);
+ }
+
+ // Delete the compatibility overlay if present
+ imageContainerNode.find("span.upload-overlay").addClass("failed");
+};
+
+/**
+ * @brief Unmarks the image as failed to upload
+ *
+ * @param imageNodeIdentifier This is a unique ID provided by the caller.
+ */
+ZSSEditor.unmarkImageUploadFailed = function(imageNodeIdentifier) {
+ var imageNode = this.getImageNodeWithIdentifier(imageNodeIdentifier);
+ if (imageNode.length != 0){
+ imageNode.removeClass('failed');
+ }
+
+ var imageContainerNode = this.getImageContainerNodeWithIdentifier(imageNodeIdentifier);
+ if(imageContainerNode.length != 0){
+ imageContainerNode.removeAttr("data-failed");
+ imageContainerNode.removeClass('failed');
+ }
+
+ var imageProgressNode = this.getImageProgressNodeWithIdentifier(imageNodeIdentifier);
+ if (imageProgressNode.length != 0){
+ imageProgressNode.removeClass('failed');
+ }
+
+ // Display the compatibility overlay again if present
+ imageContainerNode.find("span.upload-overlay").removeClass("failed");
+
+ this.setProgressOnMedia(imageNodeIdentifier, 0);
+
+ if (nativeState.androidApiLevel > 18) {
+ setTimeout(ZSSEditor.setupOptimisticProgressUpdate, 300, imageNodeIdentifier, 1);
+ }
+};
+
+/**
+ * @brief Remove the image from the DOM.
+ *
+ * @param imageNodeIdentifier This is a unique ID provided by the caller.
+ */
+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();
+ }
+
+ // if image is inside options container we need to remove the container
+ var imageContainerNode = this.getImageContainerNodeWithIdentifier(imageNodeIdentifier);
+ if (imageContainerNode.length != 0){
+ imageContainerNode.remove();
+ }
+};
+
+/**
+ * @brief Inserts a video tag using the videoURL as source and posterURL as the
+ * image to show while video is loading.
+ *
+ * @param videoURL the url of the video
+ * @param posterURL the url of an image to show while the video is loading
+ * @param videoPressID the VideoPress ID of the video, when applicable
+ *
+ */
+ZSSEditor.insertVideo = function(videoURL, posterURL, videopressID) {
+ var videoId = Date.now();
+ var html = '<video id=' + videoId + ' webkit-playsinline src="' + videoURL + '" onclick="" controls="controls" preload="metadata"';
+
+ if (posterURL != '') {
+ html += ' poster="' + posterURL + '"';
+ }
+
+ if (videopressID != '') {
+ html += ' data-wpvideopress="' + videopressID + '"';
+ }
+
+ html += '></video>';
+
+ this.insertHTMLWrappedInParagraphTags('&#x200b;' + html);
+
+ // Wrap video in edit-container node for a permanent delete button overlay
+ var videoNode = $('video[id=' + videoId + ']')[0];
+ var selectionNode = this.applyEditContainer(videoNode);
+ videoNode.removeAttribute('id');
+
+ // Remove the zero-width space node (it's not needed now that the paragraph-wrapped video is in place)
+ var zeroWidthNode = selectionNode.previousSibling;
+ if (zeroWidthNode != null && zeroWidthNode.nodeType == 3) {
+ zeroWidthNode.parentNode.removeChild(zeroWidthNode);
+ }
+
+ ZSSEditor.trackNodeForMutation($(selectionNode));
+
+ this.sendEnabledStyles();
+ this.callback("callback-action-finished");
+};
+
+/**
+ * @brief Inserts a placeholder image tag for in-progress video uploads, marked with an identifier.
+ * @details The image shown can be the video's poster if available - otherwise the default poster image is used.
+ * Using an image instead of a video placeholder is a departure from iOS, necessary because the original
+ * method caused occasional WebView freezes on Android.
+ * Once the video is successfully uploaded, the application should call replaceLocalVideoWithRemoteVideo().
+ *
+ * @param videoNodeIdentifier This is a unique ID provided by the caller. It exists as
+ * a mechanism to update the video node with the remote URL
+ * when replaceLocalVideoWithRemoteVideo() is called.
+ * @param posterURL The URL of a poster image to display while the video is being uploaded.
+ */
+ZSSEditor.insertLocalVideo = function(videoNodeIdentifier, posterURL) {
+ var progressIdentifier = this.getVideoProgressIdentifier(videoNodeIdentifier);
+ var videoContainerIdentifier = this.getVideoContainerIdentifier(videoNodeIdentifier);
+
+ if (nativeState.androidApiLevel > 18) {
+ var videoContainerClass = 'video_container';
+ var progressElement = '<progress id="' + progressIdentifier + '" value=0 class="wp_media_indicator"'
+ + 'contenteditable="false"></progress>';
+ } else {
+ // Before API 19, the WebView didn't support progress tags. Use an upload overlay instead of a progress bar
+ var videoContainerClass = 'video_container compat';
+ var progressElement = '<span class="upload-overlay" contenteditable="false">' + nativeState.localizedStringUploading
+ + '</span><span class="upload-overlay-bg"></span>';
+ }
+
+ var videoContainerStart = '<span id="' + videoContainerIdentifier + '" class="' + videoContainerClass
+ + '" contenteditable="false">';
+ var videoContainerEnd = '</span>';
+
+ if (posterURL == '') {
+ posterURL = "svg/wpposter.svg";
+ }
+
+ var image = '<img data-video_wpid="' + videoNodeIdentifier + '" src="' + posterURL + '" alt="" />';
+ var html = videoContainerStart + progressElement + image + videoContainerEnd;
+
+ this.insertHTMLWrappedInParagraphTags(html);
+
+ ZSSEditor.trackNodeForMutation(this.getVideoContainerNodeWithIdentifier(videoNodeIdentifier));
+
+ this.setProgressOnMedia(videoNodeIdentifier, 0);
+
+ if (nativeState.androidApiLevel > 18) {
+ setTimeout(ZSSEditor.setupOptimisticProgressUpdate, 300, videoNodeIdentifier, 1);
+ }
+
+ this.sendEnabledStyles();
+};
+
+ZSSEditor.getVideoNodeWithIdentifier = function(videoNodeIdentifier) {
+ var videoNode = $('img[data-video_wpid="' + videoNodeIdentifier+'"]');
+ if (videoNode.length == 0) {
+ videoNode = $('video[data-wpid="' + videoNodeIdentifier+'"]');
+ }
+ return videoNode;
+};
+
+ZSSEditor.getVideoProgressIdentifier = function(videoNodeIdentifier) {
+ return 'progress_' + videoNodeIdentifier;
+};
+
+ZSSEditor.getVideoProgressNodeWithIdentifier = function(videoNodeIdentifier) {
+ return $('#'+this.getVideoProgressIdentifier(videoNodeIdentifier));
+};
+
+ZSSEditor.getVideoContainerIdentifier = function(videoNodeIdentifier) {
+ return 'video_container_' + videoNodeIdentifier;
+};
+
+ZSSEditor.getVideoContainerNodeWithIdentifier = function(videoNodeIdentifier) {
+ return $('#'+this.getVideoContainerIdentifier(videoNodeIdentifier));
+};
+
+/**
+ * @brief Replaces the image placeholder with a video element containing the uploaded video's attributes,
+ * and removes the upload container.
+ *
+ * @param videoNodeIdentifier The unique id of the video upload
+ * @param remoteVideoUrl The URL of the remote video to display
+ * @param remotePosterUrl The URL of the remote poster image to display
+ * @param videopressID The VideoPress ID of the video, where applicable
+ */
+ZSSEditor.replaceLocalVideoWithRemoteVideo = function(videoNodeIdentifier, remoteVideoUrl, remotePosterUrl, videopressID) {
+ var imagePlaceholderNode = this.getVideoNodeWithIdentifier(videoNodeIdentifier);
+
+ if (imagePlaceholderNode.length != 0) {
+ var videoNode = document.createElement("video");
+ videoNode.setAttribute('webkit-playsinline', '');
+ videoNode.setAttribute('onclick', '');
+ videoNode.setAttribute('src', remoteVideoUrl);
+ videoNode.setAttribute('controls', 'controls');
+ videoNode.setAttribute('preload', 'metadata');
+ if (videopressID != '') {
+ videoNode.setAttribute('data-wpvideopress', videopressID);
+ }
+ videoNode.setAttribute('poster', remotePosterUrl);
+
+ // Replace upload container and placeholder image with the uploaded video node
+ var containerNode = imagePlaceholderNode.parent();
+ containerNode.replaceWith(videoNode);
+ }
+
+ var selectionNode = this.applyEditContainer(videoNode);
+
+ ZSSEditor.trackNodeForMutation($(selectionNode));
+
+ var joinedArguments = ZSSEditor.getJoinedFocusedFieldIdAndCaretArguments();
+ ZSSEditor.callback("callback-input", joinedArguments);
+ // We invoke the sendVideoReplacedCallback 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.sendVideoReplacedCallback(videoNodeIdentifier);}, 500);
+};
+
+/**
+ * @brief Callbacks to native that the video upload as finished and the local url was replaced by the remote url
+ *
+ * @param videoNodeIdentifier the unique video ID for the uploaded Video
+ */
+ZSSEditor.sendVideoReplacedCallback = function( videoNodeIdentifier ) {
+ var arguments = ['id=' + encodeURIComponent( videoNodeIdentifier )];
+
+ var joinedArguments = arguments.join( defaultCallbackSeparator );
+
+ this.callback("callback-video-replaced", joinedArguments);
+};
+
+/**
+ * @brief Callbacks to native that the video upload as finished and the local url was replaced by the remote url
+ *
+ * @param videoNodeIdentifier the unique video ID for the uploaded Video
+ */
+ZSSEditor.sendVideoPressInfoRequest = function( videoPressID ) {
+ var arguments = ['id=' + encodeURIComponent( videoPressID )];
+
+ var joinedArguments = arguments.join( defaultCallbackSeparator );
+
+ this.callback("callback-videopress-info-request", joinedArguments);
+};
+
+
+/**
+ * @brief Marks the Video as failed to upload
+ *
+ * @param VideoNodeIdentifier This is a unique ID provided by the caller.
+ * @param message A message to show to the user, overlayed on the Video
+ */
+ZSSEditor.markVideoUploadFailed = function(videoNodeIdentifier, message) {
+ var videoNode = this.getVideoNodeWithIdentifier(videoNodeIdentifier);
+ if (videoNode.length == 0){
+ return;
+ }
+
+ var sizeClass = '';
+ if ( videoNode[0].width > 480 && videoNode[0].height > 240 ) {
+ sizeClass = "largeFail";
+ } else if ( videoNode[0].width < 100 || videoNode[0].height < 100 ) {
+ sizeClass = "smallFail";
+ }
+
+ videoNode.addClass('failed');
+
+ var videoContainerNode = this.getVideoContainerNodeWithIdentifier(videoNodeIdentifier);
+ if(videoContainerNode.length != 0){
+ videoContainerNode.attr("data-failed", message);
+ videoNode.removeClass("uploading");
+ videoContainerNode.addClass('failed');
+ videoContainerNode.addClass(sizeClass);
+ }
+
+ var videoProgressNode = this.getVideoProgressNodeWithIdentifier(videoNodeIdentifier);
+ if (videoProgressNode.length != 0){
+ videoProgressNode.addClass('failed');
+ videoProgressNode.attr("value", 0);
+ }
+
+ // Delete the compatibility overlay if present
+ videoContainerNode.find("span.upload-overlay").addClass("failed");
+};
+
+/**
+ * @brief Unmarks the Video as failed to upload
+ *
+ * @param VideoNodeIdentifier This is a unique ID provided by the caller.
+ */
+ZSSEditor.unmarkVideoUploadFailed = function(videoNodeIdentifier) {
+ var videoNode = this.getVideoNodeWithIdentifier(videoNodeIdentifier);
+ if (videoNode.length != 0){
+ videoNode.removeClass('failed');
+ }
+
+ var videoContainerNode = this.getVideoContainerNodeWithIdentifier(videoNodeIdentifier);
+ if(videoContainerNode.length != 0){
+ videoContainerNode.removeAttr("data-failed");
+ videoContainerNode.removeClass('failed');
+ }
+
+ var videoProgressNode = this.getVideoProgressNodeWithIdentifier(videoNodeIdentifier);
+ if (videoProgressNode.length != 0){
+ videoProgressNode.removeClass('failed');
+ }
+
+ // Display the compatibility overlay again if present
+ videoContainerNode.find("span.upload-overlay").removeClass("failed");
+
+ this.setProgressOnMedia(videoNodeIdentifier, 0);
+
+ if (nativeState.androidApiLevel > 18) {
+ setTimeout(ZSSEditor.setupOptimisticProgressUpdate, 300, videoNodeIdentifier, 1);
+ }
+};
+
+/**
+ * @brief Remove the Video from the DOM.
+ *
+ * @param videoNodeIdentifier This is a unique ID provided by the caller.
+ */
+ZSSEditor.removeVideo = function(videoNodeIdentifier) {
+ var videoNode = this.getVideoNodeWithIdentifier(videoNodeIdentifier);
+ if (videoNode.length != 0){
+ videoNode.remove();
+ }
+
+ // 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 triggering the manual media removal callback
+ videoContainerNode.attr("id","");
+ videoContainerNode.remove();
+ }
+};
+
+/**
+ * @brief Wrap the video in an edit-container with a delete button overlay.
+ */
+ZSSEditor.applyEditContainer = function(videoNode) {
+ var containerHtml = '<span class="edit-container" contenteditable="false"><span class="delete-overlay"></span></span>';
+ videoNode.insertAdjacentHTML('beforebegin', containerHtml);
+
+ var selectionNode = videoNode.previousSibling;
+ selectionNode.appendChild(videoNode);
+
+ return selectionNode;
+}
+
+ZSSEditor.replaceVideoPressVideosForShortcode = function ( html) {
+ // call methods to restore any transformed content from its visual presentation to its source code.
+ var regex = /<video[^>]*data-wpvideopress="([\s\S]+?)"[^>]*>*<\/video>/g;
+ var str = html.replace( regex, ZSSEditor.removeVideoPressVisualFormattingCallback );
+
+ return str;
+}
+
+ZSSEditor.replaceVideosForShortcode = function ( html) {
+ var regex = /<video(?:(?!data-wpvideopress).)*><\/video>/g;
+ var str = html.replace( regex, ZSSEditor.removeVideoVisualFormattingCallback );
+
+ return str;
+}
+
+ZSSEditor.removeVideoContainers = function(html) {
+ var containerRegex = /<span class="edit-container" contenteditable="false">(?:<span class="delete-overlay"[^<>]*><\/span>)?(\[[^<>]*)<\/span>/g;
+ var str = html.replace(containerRegex, ZSSEditor.removeVideoContainerCallback);
+
+ return str;
+}
+
+ZSSEditor.removeVideoPressVisualFormattingCallback = function( match, content ) {
+ return "[wpvideo " + content + "]";
+}
+
+ZSSEditor.removeVideoVisualFormattingCallback = function( match, content ) {
+ var videoElement = $.parseHTML(match)[0];
+
+ // Remove editor playback attributes
+ videoElement.removeAttribute("onclick");
+ videoElement.removeAttribute("controls");
+ videoElement.removeAttribute("webkit-playsinline");
+ if (videoElement.getAttribute("preload") == "metadata") {
+ // The "metadata" setting is the WP default and is usually automatically stripped from the shortcode.
+ // If it's present, it was probably set by this editor and we should remove it. Even if it wasn't, removing it
+ // won't affect anything as it's the default setting for the preload attribute.
+ videoElement.removeAttribute("preload");
+ }
+
+ // If filetype attributes exist, the src attribute wasn't there originally and we should remove it
+ for (var i = 0; i < Formatter.videoShortcodeFormats.length; i++) {
+ var format = Formatter.videoShortcodeFormats[i];
+ if (videoElement.hasAttribute(format)) {
+ videoElement.removeAttribute("src");
+ break;
+ }
+ }
+
+ var shortcode = videoElement.outerHTML.replace(/</g, "[");
+ shortcode = shortcode.replace(/>/g, "]");
+ return shortcode;
+}
+
+ZSSEditor.removeVideoContainerCallback = function( match, content ) {
+ return content;
+}
+
+/**
+ * @brief Sets the VideoPress video URL and poster URL on a video tag.
+ * @details When switching from HTML to visual mode, wpvideo shortcodes are replaced by video tags.
+ * A request is sent using ZSSEditor.sendVideoPressInfoRequest() to obtain the video url matching each
+ * wpvideo shortcode. This function must be called to set the url for each videopress id.
+ *
+ * @param videopressID VideoPress identifier of the video.
+ * @param videoURL URL of the video file to display.
+ * @param posterURL URL of the poster image to display
+ */
+ZSSEditor.setVideoPressLinks = function(videopressID, videoURL, posterURL ) {
+ var videoNode = $('video[data-wpvideopress="' + videopressID + '"]');
+ if (videoNode.length == 0) {
+ return;
+ }
+
+ // It's safest to drop the onError now, to avoid endless calls if the video can't be loaded
+ // Even if sendVideoPressInfoRequest failed, it's still possible to request a reload by tapping the video
+ videoNode.attr('onError', '');
+
+ if (videoURL.length == 0) {
+ return;
+ }
+
+ videoNode.attr('src', videoURL);
+ videoNode.attr('controls', '');
+ videoNode.attr('poster', posterURL);
+ var thisObj = this;
+ videoNode.load();
+};
+
+/**
+ * @brief Stops all video of playing
+ *
+ */
+ZSSEditor.pauseAllVideos = function () {
+ $('video').each(function() {
+ this.pause();
+ });
+}
+
+ZSSEditor.clearCurrentEditingImage = function() {
+ ZSSEditor.currentEditingImage = null;
+};
+
+/**
+ * @brief Updates the currently selected image, replacing its markup with
+ * new markup based on the specified meta data string.
+ *
+ * @param imageMetaString A JSON string representing the updated meta data.
+ */
+ZSSEditor.updateCurrentImageMeta = function( imageMetaString ) {
+ if ( !ZSSEditor.currentEditingImage ) {
+ return;
+ }
+
+ var imageMeta = JSON.parse( imageMetaString );
+ var html = ZSSEditor.createImageFromMeta( imageMeta );
+
+ // Insert the updated html and remove the outdated node.
+ // This approach is preferred to selecting the current node via a range,
+ // and then replacing it when calling insertHTML. The insertHTML call can,
+ // in certain cases, modify the current and inserted markup depending on what
+ // elements surround the targeted node. This approach is safer.
+ var node = ZSSEditor.findImageCaptionNode( ZSSEditor.currentEditingImage );
+ var parent = node.parentNode;
+
+ node.insertAdjacentHTML( 'afterend', html );
+ // Use {node}.{parent}.removeChild() instead of {node}.remove(), since Android API<19 doesn't support Node.remove()
+ node.parentNode.removeChild(node);
+
+ ZSSEditor.currentEditingImage = null;
+
+ ZSSEditor.setFocusAfterElement(parent);
+}
+
+ZSSEditor.applyImageSelectionFormatting = function( imageNode ) {
+ var node = ZSSEditor.findImageCaptionNode( imageNode );
+
+ var overlay = '<span class="edit-overlay" contenteditable="false"><span class="edit-icon"></span>'
+ + '<span class="edit-content">' + nativeState.localizedStringEdit + '</span></span>';
+
+ var sizeClass = "";
+ if ( imageNode.width < 100 || imageNode.height < 100 ) {
+ sizeClass = " small";
+ } else {
+ overlay = '<span class="delete-overlay" contenteditable="false"></span>' + overlay;
+ }
+
+ if (document.body.style.filter == null) {
+ // CSS Filters (including blur) are not supported
+ // Use dark semi-transparent background for edit overlay instead of blur in this case
+ overlay = overlay + '<div class="edit-overlay-bg"></div>';
+ }
+
+ var html = '<span class="edit-container' + sizeClass + '">' + overlay + '</span>';
+ node.insertAdjacentHTML( 'beforebegin', html );
+ var selectionNode = node.previousSibling;
+ selectionNode.appendChild( node );
+
+ this.trackNodeForMutation($(selectionNode));
+
+ return selectionNode;
+}
+
+ZSSEditor.removeImageSelectionFormatting = function( imageNode ) {
+ var node = ZSSEditor.findImageCaptionNode( imageNode );
+ if ( !node.parentNode || node.parentNode.className.indexOf( "edit-container" ) == -1 ) {
+ return;
+ }
+
+ var parentNode = node.parentNode;
+ var container = parentNode.parentNode;
+
+ if (container != null) {
+ container.insertBefore( node, parentNode );
+ // Use {node}.{parent}.removeChild() instead of {node}.remove(), since Android API<19 doesn't support Node.remove()
+ container.removeChild(parentNode);
+ }
+}
+
+ZSSEditor.removeImageSelectionFormattingFromHTML = function( html ) {
+ var tmp = document.createElement( "div" );
+ var tmpDom = $( tmp ).html( html );
+
+ var matches = tmpDom.find( "span.edit-container img" );
+ if ( matches.length == 0 ) {
+ return html;
+ }
+
+ for ( var i = 0; i < matches.length; i++ ) {
+ ZSSEditor.removeImageSelectionFormatting( matches[i] );
+ }
+
+ 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.
+ *
+ * @param imageNode An image node in the DOM to inspect.
+ */
+ZSSEditor.findImageCaptionNode = function( imageNode ) {
+ var node = imageNode;
+ if ( node.parentNode && node.parentNode.nodeName === 'A' ) {
+ node = node.parentNode;
+ }
+
+ if ( node.parentNode && node.parentNode.className.indexOf( 'wp-caption' ) != -1 ) {
+ node = node.parentNode;
+ }
+
+ if ( node.parentNode && (node.parentNode.className.indexOf( 'wp-temp' ) != -1 ) ) {
+ node = node.parentNode;
+ }
+
+ return node;
+}
+
+/**
+ * Modified from wp-includes/js/media-editor.js
+ * see `image`
+ *
+ * @brief Construct html markup for an image, and optionally a link an caption shortcode.
+ *
+ * @param props A dictionary of properties used to compose the markup. See comments in extractImageMeta.
+ *
+ * @return Returns the html mark up as a string
+ */
+ZSSEditor.createImageFromMeta = function( props ) {
+ var img = {},
+ options, classes, shortcode, html;
+
+ classes = props.classes || [];
+ if ( ! ( classes instanceof Array ) ) {
+ classes = classes.split( ' ' );
+ }
+
+ _.extend( img, _.pick( props, 'width', 'height', 'alt', 'src', 'title' ) );
+
+ // Only assign the align class to the image if we're not printing
+ // a caption, since the alignment is sent to the shortcode.
+ if ( props.align && ! props.caption ) {
+ classes.push( 'align' + props.align );
+ }
+
+ if ( props.size ) {
+ classes.push( 'size-' + props.size );
+ }
+
+ if ( props.attachment_id ) {
+ classes.push( 'wp-image-' + props.attachment_id );
+ }
+
+ img['class'] = _.compact( classes ).join(' ');
+
+ // Generate `img` tag options.
+ options = {
+ tag: 'img',
+ attrs: img,
+ single: true
+ };
+
+ // Generate the `a` element options, if they exist.
+ if ( props.linkUrl ) {
+ options = {
+ tag: 'a',
+ attrs: {
+ href: props.linkUrl
+ },
+ content: options
+ };
+
+ if ( props.linkClassName ) {
+ options.attrs.class = props.linkClassName;
+ }
+
+ if ( props.linkRel ) {
+ options.attrs.rel = props.linkRel;
+ }
+
+ if ( props.linkTargetBlank ) { // expects a boolean
+ options.attrs.target = "_blank";
+ }
+ }
+
+ html = wp.html.string( options );
+
+ // Generate the caption shortcode.
+ if ( props.caption ) {
+ shortcode = {};
+
+ if ( img.width ) {
+ shortcode.width = img.width;
+ }
+
+ if ( props.captionId ) {
+ shortcode.id = props.captionId;
+ }
+
+ if ( props.align ) {
+ shortcode.align = 'align' + props.align;
+ } else {
+ shortcode.align = 'alignnone';
+ }
+
+ if (props.captionClassName) {
+ shortcode.class = props.captionClassName;
+ }
+
+ html = wp.shortcode.string({
+ tag: 'caption',
+ attrs: shortcode,
+ content: html + props.caption
+ });
+
+ html = Formatter.applyVisualFormatting( html );
+ }
+
+ return html;
+};
+
+/**
+ * Modified from wp-includes/js/tinymce/plugins/wpeditimage/plugin.js
+ * see `extractImageData`
+ *
+ * @brief Extracts properties and meta data from an image, and optionally its link and caption.
+ *
+ * @param imageNode An image node in the DOM to inspect.
+ *
+ * @return Returns an object containing the extracted properties and meta data.
+ */
+ZSSEditor.extractImageMeta = function( imageNode ) {
+ var classes, extraClasses, metadata, captionBlock, caption, link, width, height,
+ captionClassName = [],
+ isIntRegExp = /^\d+$/;
+
+ // Default attributes. All values are strings, except linkTargetBlank
+ metadata = {
+ align: 'none', // Accepted values: center, left, right or empty string.
+ alt: '', // Image alt attribute
+ attachment_id: '', // Numeric attachment id of the image in the site's media library
+ caption: '', // The text of the caption for the image (if any)
+ captionClassName: '', // The classes for the caption shortcode (if any).
+ captionId: '', // The caption shortcode's ID attribute. The numeric value should match the value of attachment_id
+ classes: '', // The class attribute for the image. Does not include editor generated classes
+ height: '', // The image height attribute
+ linkClassName: '', // The class attribute for the link
+ linkRel: '', // The rel attribute for the link (if any)
+ linkTargetBlank: false, // true if the link should open in a new window.
+ linkUrl: '', // The href attribute of the link
+ size: 'custom', // Accepted values: custom, medium, large, thumbnail, or empty string
+ src: '', // The src attribute of the image
+ title: '', // The title attribute of the image (if any)
+ width: '', // The image width attribute
+ naturalWidth:'', // The natural width of the image.
+ naturalHeight:'' // The natural height of the image.
+ };
+
+ // populate metadata with values of matched attributes
+ metadata.src = $( imageNode ).attr( 'src' ) || '';
+ metadata.alt = $( imageNode ).attr( 'alt' ) || '';
+ metadata.title = $( imageNode ).attr( 'title' ) || '';
+ metadata.naturalWidth = imageNode.naturalWidth;
+ metadata.naturalHeight = imageNode.naturalHeight;
+
+ width = $(imageNode).attr( 'width' );
+ height = $(imageNode).attr( 'height' );
+
+ if ( ! isIntRegExp.test( width ) || parseInt( width, 10 ) < 1 ) {
+ width = imageNode.naturalWidth || imageNode.width;
+ }
+
+ if ( ! isIntRegExp.test( height ) || parseInt( height, 10 ) < 1 ) {
+ height = imageNode.naturalHeight || imageNode.height;
+ }
+
+ metadata.width = width;
+ metadata.height = height;
+
+ classes = imageNode.className.split( /\s+/ );
+ extraClasses = [];
+
+ $.each( classes, function( index, value ) {
+ if ( /^wp-image/.test( value ) ) {
+ metadata.attachment_id = parseInt( value.replace( 'wp-image-', '' ), 10 );
+ } else if ( /^align/.test( value ) ) {
+ metadata.align = value.replace( 'align', '' );
+ } else if ( /^size/.test( value ) ) {
+ metadata.size = value.replace( 'size-', '' );
+ } else {
+ extraClasses.push( value );
+ }
+ } );
+
+ metadata.classes = extraClasses.join( ' ' );
+
+ // Extract caption
+ var captionMeta = ZSSEditor.captionMetaForImage( imageNode )
+ if (captionMeta.caption != '') {
+ metadata = $.extend( metadata, captionMeta );
+ }
+
+ // Extract linkTo
+ if ( imageNode.parentNode && imageNode.parentNode.nodeName === 'A' ) {
+ link = imageNode.parentNode;
+ metadata.linkClassName = link.className;
+ metadata.linkRel = $( link ).attr( 'rel' ) || '';
+ metadata.linkTargetBlank = $( link ).attr( 'target' ) === '_blank' ? true : false;
+ metadata.linkUrl = $( link ).attr( 'href' ) || '';
+ }
+
+ return metadata;
+};
+
+/**
+ * @brief Extracts the caption shortcode for an image.
+ *
+ * @param imageNode An image node in the DOM to inspect.
+ *
+ * @return Returns a shortcode match (if any) for the passed image node.
+ * See shortcode.js::next for details
+ */
+ZSSEditor.getCaptionForImage = function( imageNode ) {
+ var node = ZSSEditor.findImageCaptionNode( imageNode );
+
+ // Ensure we're working with the formatted caption
+ if ( node.className.indexOf( 'wp-temp' ) == -1 ) {
+ return;
+ }
+
+ var html = node.outerHTML;
+ html = ZSSEditor.removeVisualFormatting( html );
+
+ return wp.shortcode.next( "caption", html, 0 );
+};
+
+/**
+ * @brief Extracts meta data for the caption (if any) for the passed image node.
+ *
+ * @param imageNode An image node in the DOM to inspect.
+ *
+ * @return Returns an object containing the extracted meta data.
+ * See shortcode.js::next or details
+ */
+ZSSEditor.captionMetaForImage = function( imageNode ) {
+ var attrs,
+ meta = {
+ align: '',
+ caption: '',
+ captionClassName: '',
+ captionId: ''
+ };
+
+ var caption = ZSSEditor.getCaptionForImage( imageNode );
+ if ( !caption ) {
+ return meta;
+ }
+
+ attrs = caption.shortcode.attrs.named;
+ if ( attrs.align ) {
+ meta.align = attrs.align.replace( 'align', '' );
+ }
+ if ( attrs.class ) {
+ meta.captionClassName = attrs.class;
+ }
+ if ( attrs.id ) {
+ meta.captionId = attrs.id;
+ }
+ meta.caption = caption.shortcode.content.substr( caption.shortcode.content.lastIndexOf( ">" ) + 1 );
+
+ return meta;
+}
+
+/**
+ * @brief Removes custom visual formatting for caption shortcodes.
+ *
+ * @param html The markup to process
+ *
+ * @return The html with formatted captions restored to the original shortcode markup.
+ */
+ZSSEditor.removeCaptionFormatting = function( html ) {
+ // call methods to restore any transformed content from its visual presentation to its source code.
+ var regex = /<label class="wp-temp" data-wp-temp="caption"[^>]*>([\s\S]+?)<\/label>/g;
+
+ var str = html.replace( regex, ZSSEditor.removeCaptionFormattingCallback );
+
+ return str;
+}
+
+ZSSEditor.removeCaptionFormattingCallback = function( match, content ) {
+ // TODO: check is a visual temp node
+ var out = '';
+
+ if ( content.indexOf('<img ') === -1 ) {
+ // Broken caption. The user managed to drag the image out?
+ // Try to return the caption text as a paragraph.
+ out = content.match( /\s*<span [^>]*>([\s\S]+?)<\/span>/gi );
+
+ if ( out && out[1] ) {
+ return '<p>' + out[1] + '</p>';
+ }
+
+ return '';
+ }
+
+ out = content.replace( /\s*<span ([^>]*)>([\s\S]+?)<\/span>/gi, function( ignoreMatch, attrStr, content ) {
+ if ( ! content ) {
+ return '';
+ }
+
+ var id, classes, align, width, attrs = {};
+
+ width = attrStr.match( /data-caption-width="([0-9]*)"/ );
+ width = ( width && width[1] ) ? width[1] : '';
+ if ( width ) {
+ attrs.width = width;
+ }
+
+ id = attrStr.match( /data-caption-id="([^"]*)"/ );
+ id = ( id && id[1] ) ? id[1] : '';
+ if ( id ) {
+ attrs.id = id;
+ }
+
+ classes = attrStr.match( /data-caption-class="([^"]*)"/ );
+ classes = ( classes && classes[1] ) ? classes[1] : '';
+ if ( classes ) {
+ attrs.class = classes;
+ }
+
+ align = attrStr.match( /data-caption-align="([^"]*)"/ );
+ align = ( align && align[1] ) ? align[1] : '';
+ if ( align ) {
+ attrs.align = align;
+ }
+
+ var options = {
+ 'tag':'caption',
+ 'attrs':attrs,
+ 'type':'closed',
+ 'content':content
+ };
+
+ return wp.shortcode.string( options );
+ });
+
+ return out;
+}
+
+// MARK: - Galleries
+
+ZSSEditor.insertGallery = function( imageIds, type, columns ) {
+ var shortcode;
+ if (type) {
+ shortcode = '[gallery type="' + type + '" ids="' + imageIds + '"]';
+ } else {
+ shortcode = '[gallery columns="' + columns + '" ids="' + imageIds + '"]';
+ }
+
+ this.insertHTMLWrappedInParagraphTags(shortcode);
+}
+
+ZSSEditor.insertLocalGallery = function( placeholderId ) {
+ var container = '<span id="' + placeholderId + '" class="gallery_container">['
+ + nativeState.localizedStringUploadingGallery + ']</span>';
+ this.insertHTMLWrappedInParagraphTags(container);
+}
+
+ZSSEditor.replacePlaceholderGallery = function( placeholderId, imageIds, type, columns ) {
+ var span = 'span#' + placeholderId + '.gallery_container';
+
+ var shortcode;
+ if (type) {
+ shortcode = '[gallery type="' + type + '" ids="' + imageIds + '"]';
+ } else {
+ shortcode = '[gallery columns="' + columns + '" ids="' + imageIds + '"]';
+ }
+
+ $(span).replaceWith(shortcode);
+}
+
+// MARK: - Commands
+
+/**
+ * @brief Removes editor specific visual formatting
+ *
+ * @param html The markup to remove formatting
+ *
+ * @return Returns the string with the visual formatting removed.
+ */
+ZSSEditor.removeVisualFormatting = function( html ) {
+ var str = html;
+ str = ZSSEditor.removeImageRemoteUrl( str );
+ str = ZSSEditor.removeImageSelectionFormattingFromHTML( str );
+ str = ZSSEditor.removeCaptionFormatting( str );
+ str = ZSSEditor.replaceVideoPressVideosForShortcode( str );
+ str = ZSSEditor.replaceVideosForShortcode( str );
+ str = ZSSEditor.removeVideoContainers( str );
+
+ // More tag
+ str = str.replace(/<hr class="more-tag" wp-more-data="(.*?)">/igm, "<!--more$1-->")
+ str = str.replace(/<hr class="nextpage-tag">/igm, "<!--nextpage-->")
+ return str;
+};
+
+ZSSEditor.insertHTML = function(html) {
+ document.execCommand('insertHTML', false, html);
+ this.sendEnabledStyles();
+};
+
+ZSSEditor.insertText = function(text, reformatVisually) {
+ var focusedField = ZSSEditor.getFocusedField();
+ if (focusedField.isMultiline() && focusedField.getHTML().length == 0) {
+ // when the text field is empty, we need to add an initial paragraph tag
+ text = Util.wrapHTMLInTag(text, ZSSEditor.defaultParagraphSeparator);
+ }
+
+ if (reformatVisually) {
+ text = wp.loadText(text);
+ }
+
+ ZSSEditor.insertHTML(text);
+};
+
+ZSSEditor.isCommandEnabled = function(commandName) {
+ return document.queryCommandState(commandName);
+};
+
+ZSSEditor.sendEnabledStyles = function(e) {
+
+ var items = [];
+
+ var focusedField = this.getFocusedField();
+
+ if (!focusedField.hasNoStyle) {
+ // Find all relevant parent tags
+ var parentTags = ZSSEditor.parentTags();
+
+ if (parentTags != null) {
+ for (var i = 0; i < parentTags.length; i++) {
+ var currentNode = parentTags[i];
+
+ if (currentNode.nodeName.toLowerCase() == 'a') {
+ ZSSEditor.currentEditingLink = currentNode;
+
+ var title = encodeURIComponent(currentNode.text);
+ var href = encodeURIComponent(currentNode.href);
+
+ items.push('link-title:' + title);
+ items.push('link:' + href);
+ } else if (currentNode.nodeName == NodeName.BLOCKQUOTE) {
+ items.push('blockquote');
+ }
+ }
+ }
+
+ if (ZSSEditor.isCommandEnabled('bold')) {
+ items.push('bold');
+ }
+ if (ZSSEditor.isCommandEnabled('createLink')) {
+ items.push('createLink');
+ }
+ if (ZSSEditor.isCommandEnabled('italic')) {
+ items.push('italic');
+ }
+ if (ZSSEditor.isCommandEnabled('subscript')) {
+ items.push('subscript');
+ }
+ if (ZSSEditor.isCommandEnabled('superscript')) {
+ items.push('superscript');
+ }
+ if (ZSSEditor.isCommandEnabled('strikeThrough')) {
+ items.push('strikeThrough');
+ }
+ if (ZSSEditor.isCommandEnabled('underline')) {
+ var isUnderlined = false;
+
+ // DRM: 'underline' gets highlighted if it's inside of a link... so we need a special test
+ // in that case.
+ if (!ZSSEditor.currentEditingLink) {
+ items.push('underline');
+ }
+ }
+ if (ZSSEditor.isCommandEnabled('insertOrderedList')) {
+ items.push('orderedList');
+ }
+ if (ZSSEditor.isCommandEnabled('insertUnorderedList')) {
+ items.push('unorderedList');
+ }
+ if (ZSSEditor.isCommandEnabled('justifyCenter')) {
+ items.push('justifyCenter');
+ }
+ if (ZSSEditor.isCommandEnabled('justifyFull')) {
+ items.push('justifyFull');
+ }
+ if (ZSSEditor.isCommandEnabled('justifyLeft')) {
+ items.push('justifyLeft');
+ }
+ if (ZSSEditor.isCommandEnabled('justifyRight')) {
+ items.push('justifyRight');
+ }
+ if (ZSSEditor.isCommandEnabled('insertHorizontalRule')) {
+ items.push('horizontalRule');
+ }
+ var formatBlock = document.queryCommandValue('formatBlock');
+ if (formatBlock.length > 0) {
+ items.push(formatBlock);
+ }
+
+ // Use jQuery to figure out those that are not supported
+ if (typeof(e) != "undefined") {
+
+ // The target element
+ var t = $(e.target);
+ var nodeName = e.target.nodeName.toLowerCase();
+
+ // Background Color
+ try
+ {
+ var bgColor = t.css('backgroundColor');
+ if (bgColor && bgColor.length != 0 && bgColor != 'rgba(0, 0, 0, 0)' && bgColor != 'rgb(0, 0, 0)' && bgColor != 'transparent') {
+ items.push('backgroundColor');
+ }
+ }
+ catch(e)
+ {
+ // DRM: I had to add these stupid try-catch blocks to solve an issue with t.css throwing
+ // exceptions for no reason.
+ }
+
+ // Text Color
+ try
+ {
+ var textColor = t.css('color');
+ if (textColor && textColor.length != 0 && textColor != 'rgba(0, 0, 0, 0)' && textColor != 'rgb(0, 0, 0)' && textColor != 'transparent') {
+ items.push('textColor');
+ }
+ }
+ catch(e)
+ {
+ // DRM: I had to add these stupid try-catch blocks to solve an issue with t.css throwing
+ // exceptions for no reason.
+ }
+
+ // Image
+ if (nodeName == 'img') {
+ ZSSEditor.currentEditingImage = t;
+ items.push('image:'+t.attr('src'));
+ if (t.attr('alt') !== undefined) {
+ items.push('image-alt:'+t.attr('alt'));
+ }
+ }
+ }
+ }
+
+ ZSSEditor.stylesCallback(items);
+};
+
+ZSSEditor.storeInlineStylesAsFunctions = function() {
+ var styles = [];
+
+ if (ZSSEditor.isCommandEnabled('bold')) {
+ styles.push(ZSSEditor.setBold);
+ }
+ if (ZSSEditor.isCommandEnabled('italic')) {
+ styles.push(ZSSEditor.setItalic);
+ }
+ if (ZSSEditor.isCommandEnabled('strikeThrough')) {
+ styles.push(ZSSEditor.setStrikeThrough);
+ }
+ if (ZSSEditor.isCommandEnabled('underline')) {
+ styles.push(ZSSEditor.setUnderline);
+ }
+ if (ZSSEditor.isCommandEnabled('subscript')) {
+ styles.push(ZSSEditor.setSubscript);
+ }
+ if (ZSSEditor.isCommandEnabled('superscript')) {
+ styles.push(ZSSEditor.setSuperscript);
+ }
+
+ return styles;
+};
+
+// MARK: - Commands: High Level Editing
+
+/**
+ * @brief Inserts a br tag at the caret position.
+ */
+ZSSEditor.insertBreakTagAtCaretPosition = function() {
+ // iOS IMPORTANT: we were adding <br> tags with range.insertNode() before using
+ // this method. Unfortunately this was causing issues with the first <br> tag
+ // being completely ignored under iOS:
+ //
+ // https://bugs.webkit.org/show_bug.cgi?id=23474
+ //
+ // The following line seems to work fine under iOS, so please be careful if this
+ // needs to be changed for any reason.
+ //
+ document.execCommand("insertLineBreak");
+};
+
+// MARK: - Advanced Node Manipulation
+
+/**
+ * @brief Given the specified node, find the previous node in the DOM.
+ *
+ * @param node The node used as a starting point for the "previous" search.
+ *
+ * @returns If a previous node is found, it will be returned otherwise null;
+ */
+ZSSEditor.previousNode = function(node) {
+ if (!node) {
+ return null;
+ }
+ var previous = node.previousSibling;
+ if (previous) {
+ node = previous;
+ while (node.hasChildNodes()) {
+ node = node.lastChild;
+ }
+ return node;
+ }
+ var parent = node.parentNode;
+ if (parent && parent.hasChildNodes()) {
+ return parent;
+ }
+ return null;
+};
+
+/**
+ * @brief Ends the editing of a list (either UL or OL).
+ *
+ * @details This function finds the list node, inserts a new paragraph as a sibling to the list node
+ * then scrubs any <br> tags created as part of the insertParagraph command.
+ */
+ZSSEditor.completeListEditing = function() {
+ // Get the current selection
+ var sel = window.getSelection();
+ if (sel && sel.rangeCount > 0) {
+ var range = sel.getRangeAt(0);
+ var node = range.startContainer;
+ if (node.hasChildNodes() && range.startOffset > 0) {
+ node = node.childNodes[range.startOffset - 1];
+ }
+
+ // Walk backwards through the DOM until we find an ul or ol
+ while (node) {
+ if (node.nodeType == 1 &&
+ (node.tagName.toUpperCase() == NodeName.UL
+ || node.tagName.toUpperCase() == NodeName.OL)) {
+
+ var focusedNode = document.getSelection().getRangeAt(0).startContainer;
+
+ if (focusedNode.nodeType == 3) {
+ // If the focused node is a text node, the list item was not empty when toggled off
+ // Wrap the text in a div and attach it as a sibling to the div wrapping the list
+ var parentParagraph = focusedNode.parentNode;
+ var paragraph = document.createElement('div');
+
+ paragraph.appendChild(focusedNode);
+ parentParagraph.insertAdjacentElement('afterEnd', paragraph);
+
+ ZSSEditor.giveFocusToElement(paragraph, 1);
+ } else {
+ // Attach a new paragraph node as a sibling to the parent node
+ document.execCommand('insertParagraph', false);
+ }
+
+ // Remove any superfluous <br> tags that are created
+ ZSSEditor.scrubBRFromNode(node.parentNode);
+ break;
+ }
+ node = ZSSEditor.previousNode(node);
+ }
+ }
+}
+
+/**
+ * @brief Given the specified node, remove all instances of <br> from it and it's children.
+ *
+ * @param node The node to scrub
+ */
+ZSSEditor.scrubBRFromNode = function(node) {
+ if (!node) {
+ return;
+ }
+ $(node).contents().filter(NodeName.BR).remove();
+};
+
+/**
+ * @brief Extracts a node from a parent node, and from all nodes in between the two.
+ */
+ZSSEditor.extractNodeFromAncestorNode = function(descendant, ancestor) {
+
+ while (ancestor.contains(descendant)) {
+
+ this.extractNodeFromParent(descendant);
+ break;
+ }
+};
+
+/**
+ * @brief Extract the specified node from its direct parent node.
+ * @details If the node has siblings, before or after it, the parent node is split accordingly
+ * into two new clones of it.
+ */
+ZSSEditor.extractNodeFromParent = function(node) {
+
+ var parentNode = node.parentNode;
+ var grandParentNode = parentNode.parentNode;
+ var clonedParentForPreviousSiblings = null;
+ var clonedParentForNextSiblings = null;
+
+ if (node.previousSibling != null) {
+ var clonedParentForPreviousSiblings = parentNode.cloneNode();
+
+ while (parentNode.firstChild != node) {
+ clonedParentForPreviousSiblings.appendChild(parentNode.firstChild);
+ }
+ }
+
+ if (node.nextSibling != null) {
+ var clonedParentForNextSiblings = parentNode.cloneNode();
+
+ while (node.nextSibling != null) {
+ clonedParentForNextSiblings.appendChild(node.nextSibling);
+ }
+ }
+
+ if (clonedParentForPreviousSiblings) {
+ grandParentNode.insertBefore(clonedParentForPreviousSiblings, parentNode);
+ }
+
+ grandParentNode.insertBefore(node, parentNode);
+
+ if (clonedParentForNextSiblings) {
+ grandParentNode.insertBefore(clonedParentForNextSiblings, parentNode);
+ }
+
+ grandParentNode.removeChild(parentNode);
+};
+
+ZSSEditor.getChildNodesIntersectingRange = function(parentNode, range) {
+
+ var nodes = new Array();
+
+ if (parentNode) {
+ var currentNode = parentNode.firstChild;
+ var pushNodes = false;
+ var exit = false;
+
+ while (currentNode) {
+
+ if (range.intersectsNode(currentNode)) {
+ nodes.push(currentNode);
+ }
+
+ currentNode = currentNode.nextSibling;
+ }
+ }
+
+ return nodes;
+};
+
+/**
+ * @brief Given the specified range, find the ancestor element that will be used to set the
+ * blockquote ON or OFF.
+ *
+ * @param range The range we want to set the blockquote ON or OFF for.
+ *
+ * @returns If a parent BLOCKQUOTE element is found, it will be return. Otherwise the closest
+ * parent element will be returned.
+ */
+ZSSEditor.getAncestorElementForSettingBlockquote = function(range) {
+
+ var nodes = new Array();
+ var parentElement = range.commonAncestorContainer;
+
+ while (parentElement
+ && (parentElement.nodeType != document.ELEMENT_NODE
+ || parentElement.nodeName == NodeName.PARAGRAPH
+ || parentElement.nodeName == NodeName.STRONG
+ || parentElement.nodeName == NodeName.EM
+ || parentElement.nodeName == NodeName.DEL
+ || parentElement.nodeName == NodeName.A
+ || parentElement.nodeName == NodeName.UL
+ || parentElement.nodeName == NodeName.OL
+ || parentElement.nodeName == NodeName.LI
+ || parentElement.nodeName == NodeName.CODE
+ || parentElement.nodeName == NodeName.SPAN
+ // Include nested divs, but ignore the parent contenteditable field div
+ || (parentElement.nodeName == NodeName.DIV && parentElement.parentElement.nodeName != NodeName.BODY))) {
+ parentElement = parentElement.parentNode;
+ }
+
+ var currentElement = parentElement;
+
+ while (currentElement
+ && currentElement.nodeName != NodeName.BLOCKQUOTE) {
+ currentElement = currentElement.parentElement;
+ }
+
+ var result = currentElement ? currentElement : parentElement;
+
+ return result;
+};
+
+/**
+ * @brief Joins any adjacent blockquote siblings.
+ * @details You probably want to call joinAdjacentSiblingsOrAncestorBlockquotes() instead of
+ * this.
+ *
+ * @returns true if a sibling was joined. false otherwise.
+ */
+ZSSEditor.joinAdjacentSiblingsBlockquotes = function(node) {
+
+ var shouldJoinToPreviousSibling = this.hasPreviousSiblingWithName(node, NodeName.BLOCKQUOTE);
+ var shouldJoinToNextSibling = this.hasNextSiblingWithName(node, NodeName.BLOCKQUOTE);
+ var joinedASibling = (shouldJoinToPreviousSibling || shouldJoinToNextSibling);
+
+ var previousSibling = node.previousSibling;
+ var nextSibling = node.nextSibling;
+
+ if (shouldJoinToPreviousSibling) {
+
+ previousSibling.appendChild(node);
+
+ if (shouldJoinToNextSibling) {
+
+ while (nextSibling.firstChild) {
+ previousSibling.appendChild(nextSibling.firstChild);
+ }
+
+ nextSibling.parentNode.removeChild(nextSibling);
+ }
+ } else if (shouldJoinToNextSibling) {
+
+ nextSibling.insertBefore(node, nextSibling.firstChild);
+ }
+
+ return joinedASibling;
+};
+
+/**
+ * @brief Joins any adjacent blockquote siblings, or the blockquote siblings of any ancestor.
+ * @details When turning blockquotes back on, this method makes sure that we attach new
+ * blockquotes to exiting ones.
+ *
+ * @returns true if a sibling or ancestor sibling was joined. false otherwise.
+ */
+ZSSEditor.joinAdjacentSiblingsOrAncestorBlockquotes = function(node) {
+
+ var currentNode = node;
+ var rootNode = this.getFocusedField().getWrappedDomNode();
+ var joined = false;
+
+ while (currentNode
+ && currentNode != rootNode
+ && !joined) {
+
+ joined = this.joinAdjacentSiblingsBlockquotes(currentNode);
+ currentNode = currentNode.parentNode;
+ };
+
+ return joined;
+};
+
+/**
+ * @brief Surrounds a node's contents into another node
+ * @details When creating new nodes that should force paragraphs inside of them, this method
+ * should be called.
+ *
+ * @param node The node that will have its contents wrapped into a new node.
+ * @param wrapperNodeName The nodeName of the node that will created to wrap the contents.
+ *
+ * @returns The newly created wrapper node.
+ */
+ZSSEditor.surroundNodeContentsWithNode = function(node, wrapperNodeName) {
+
+ var range = document.createRange();
+ var wrapperNode = document.createElement(wrapperNodeName);
+
+ range.selectNodeContents(node);
+ range.surroundContents(wrapperNode);
+
+ return wrapperNode;
+};
+
+/**
+ * @brief Surrounds a node's contents with a paragraph node.
+ * @details When creating new nodes that should force paragraphs inside of them, this method
+ * should be called.
+ *
+ * @returns The paragraph node.
+ */
+ZSSEditor.surroundNodeContentsWithAParagraphNode = function(node) {
+
+ return this.surroundNodeContentsWithNode(node, this.defaultParagraphSeparator);
+};
+
+// MARK: - Sibling nodes
+
+ZSSEditor.hasNextSiblingWithName = function(node, siblingNodeName) {
+ return node.nextSibling && node.nextSibling.nodeName == siblingNodeName;
+};
+
+ZSSEditor.hasPreviousSiblingWithName = function(node, siblingNodeName) {
+ return node.previousSibling && node.previousSibling.nodeName == siblingNodeName;
+};
+
+
+// MARK: - Parent nodes & tags
+
+ZSSEditor.findParentContenteditableDiv = function() {
+ var parentNode = null;
+ var selection = window.getSelection();
+ if (selection.rangeCount < 1) {
+ return null;
+ }
+ var range = selection.getRangeAt(0).cloneRange();
+
+ var referenceNode = this.closerParentNodeWithNameRelativeToNode('div', range.commonAncestorContainer);
+
+ while (referenceNode.parentNode.nodeName != NodeName.BODY) {
+ referenceNode = this.closerParentNodeWithNameRelativeToNode('div', referenceNode.parentNode);
+ }
+
+ return referenceNode;
+};
+
+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;
+
+ while (currentNode) {
+ if (currentNode.nodeType == document.ELEMENT_NODE) {
+ parentNode = currentNode;
+
+ break;
+ }
+
+ currentNode = currentNode.parentElement;
+ }
+
+ return parentNode;
+};
+
+ZSSEditor.closerParentNodeStartingAtNode = function(nodeName, startingNode) {
+
+ nodeName = nodeName.toLowerCase();
+
+ var parentNode = null;
+ var currentNode = startingNode.parentElement;
+
+ while (currentNode) {
+
+ if (currentNode.nodeName == document.body.nodeName) {
+ break;
+ }
+
+ if (currentNode.nodeName && currentNode.nodeName.toLowerCase() == nodeName
+ && currentNode.nodeType == document.ELEMENT_NODE) {
+ parentNode = currentNode;
+
+ break;
+ }
+
+ currentNode = currentNode.parentElement;
+ }
+
+ return parentNode;
+};
+
+ZSSEditor.closerParentNodeWithName = function(nodeName) {
+
+ nodeName = nodeName.toLowerCase();
+
+ var parentNode = null;
+ var selection = window.getSelection();
+ if (selection.rangeCount < 1) {
+ return null;
+ }
+ var range = selection.getRangeAt(0).cloneRange();
+
+ var referenceNode = range.commonAncestorContainer;
+
+ return this.closerParentNodeWithNameRelativeToNode(nodeName, referenceNode);
+};
+
+ZSSEditor.closerParentNodeWithNameRelativeToNode = function(nodeName, referenceNode) {
+
+ nodeName = nodeName.toUpperCase();
+
+ var parentNode = null;
+ var currentNode = referenceNode;
+
+ while (currentNode) {
+
+ if (currentNode.nodeName == document.body.nodeName) {
+ break;
+ }
+
+ if (currentNode.nodeName == nodeName
+ && currentNode.nodeType == document.ELEMENT_NODE) {
+ parentNode = currentNode;
+
+ break;
+ }
+
+ currentNode = currentNode.parentElement;
+ }
+
+ return parentNode;
+};
+
+ZSSEditor.isCloserParentNodeABlockquote = function() {
+ return this.closerParentNode().nodeName == NodeName.BLOCKQUOTE;
+};
+
+ZSSEditor.parentTags = function() {
+
+ var parentTags = [];
+ var selection = window.getSelection();
+ if (selection.rangeCount < 1) {
+ return null;
+ }
+ var range = selection.getRangeAt(0);
+
+ var currentNode = range.commonAncestorContainer;
+ while (currentNode) {
+
+ if (currentNode.nodeName == document.body.nodeName) {
+ break;
+ }
+
+ if (currentNode.nodeType == document.ELEMENT_NODE) {
+ parentTags.push(currentNode);
+ }
+
+ currentNode = currentNode.parentElement;
+ }
+
+ return parentTags;
+};
+
+// MARK: - Range handling
+
+ZSSEditor.getParentRangeOfFocusedNode = function() {
+ var selection = window.getSelection();
+ if (selection.focusNode == null) {
+ return null;
+ }
+ return selection.getRangeAt(selection.focusNode.parentNode);
+};
+
+ZSSEditor.setRange = function(range) {
+ window.getSelection().removeAllRanges();
+ window.getSelection().addRange(range);
+};
+// MARK: - ZSSField Constructor
+
+function ZSSField(wrappedObject) {
+ // When this bool is true, we are going to restrict input and certain callbacks
+ // so IME keyboards behave properly when composing.
+ this.isComposing = false;
+
+ this.multiline = false;
+ this.wrappedObject = wrappedObject;
+
+ if (this.getWrappedDomNode().hasAttribute('nostyle')) {
+ this.hasNoStyle = true;
+ }
+
+ this.useVisualFormatting = (this.wrappedObject.data("wpUseVisualFormatting") === "true")
+
+ this.bindListeners();
+};
+
+ZSSField.prototype.bindListeners = function() {
+
+ var thisObj = this;
+
+ this.wrappedObject.bind('tap', function(e) { thisObj.handleTapEvent(e); });
+ this.wrappedObject.bind('focus', function(e) { thisObj.handleFocusEvent(e); });
+ this.wrappedObject.bind('blur', function(e) { thisObj.handleBlurEvent(e); });
+ this.wrappedObject.bind('keydown', function(e) { thisObj.handleKeyDownEvent(e); });
+ this.wrappedObject.bind('input', function(e) { thisObj.handleInputEvent(e); });
+ this.wrappedObject.bind('compositionstart', function(e) { thisObj.handleCompositionStartEvent(e); });
+ this.wrappedObject.bind('compositionend', function(e) { thisObj.handleCompositionEndEvent(e); });
+
+ // Only supported on API19+
+ this.wrappedObject.on('paste', function(e) { thisObj.handlePasteEvent(e); });
+};
+
+// MARK: - Emptying the field when it should be, well... empty (HTML madness)
+
+/**
+ * @brief Sometimes HTML leaves some <br> tags or &nbsp; when the user deletes all
+ * text from a contentEditable field. This code makes sure no such 'garbage' survives.
+ * @details If the node contains child image nodes, then the content is left untouched.
+ */
+ZSSField.prototype.emptyFieldIfNoContents = function() {
+
+ var nbsp = '\xa0';
+ var text = this.wrappedObject.text().replace(nbsp, '');
+
+ if (text.length == 0 || text == '\u000A') {
+
+ var hasChildImages = (this.wrappedObject.find('img').length > 0);
+ var hasChildVideos = (this.wrappedObject.find('video').length > 0);
+ var hasUnorderedList = (this.wrappedObject.find('ul').length > 0);
+ var hasOrderedList = (this.wrappedObject.find('ol').length > 0);
+
+ if (!hasChildImages && !hasChildVideos && !hasUnorderedList && !hasOrderedList) {
+ this.wrappedObject.empty();
+ }
+ }
+};
+
+// MARK: - Handle event listeners
+
+ZSSField.prototype.handleBlurEvent = function(e) {
+ ZSSEditor.focusedField = null;
+
+ this.emptyFieldIfNoContents();
+
+ this.callback("callback-focus-out");
+};
+
+ZSSField.prototype.handleFocusEvent = function(e) {
+ ZSSEditor.focusedField = this;
+
+ this.callback("callback-focus-in");
+};
+
+ZSSField.prototype.handleKeyDownEvent = function(e) {
+
+ var wasEnterPressed = (e.keyCode == '13');
+ var isHardwareKeyboardPaste = (e.ctrlKey && e.keyCode == '86');
+
+ // Handle keyDownEvent actions that need to happen after the event has completed (and the field has been modified)
+ setTimeout(this.afterKeyDownEvent, 20, e.target.innerHTML, e);
+
+ if (this.isComposing) {
+ e.stopPropagation();
+ } else if (wasEnterPressed && !this.isMultiline()) {
+ e.preventDefault();
+ } else if (this.isMultiline()) {
+ // For hardware keyboards, don't do any paragraph handling for non-printable keyCodes
+ // https://css-tricks.com/snippets/javascript/javascript-keycodes/
+ // This avoids the filler zero-width space character from being inserted and displayed in the content field
+ // when special keys are pressed in new posts
+ var wasTabPressed = (e.keyCode == '9');
+ var intKeyCode = parseInt(e.keyCode, 10);
+ if (wasTabPressed || (intKeyCode > 13 && intKeyCode < 46) || intKeyCode == 192) {
+ return;
+ }
+
+ // The containsParagraphSeparators check is intended to work around three bugs:
+ // 1. On API19 only, paragraph wrapping the first character in a post will display a zero-width space character
+ // (from ZSSField.wrapCaretInParagraphIfNecessary)
+ // We can drop the if statement wrapping wrapCaretInParagraphIfNecessary() if we find a way to stop using
+ // zero-width space characters (e.g., autocorrect issues are fixed and we switch back to p tags)
+ //
+ // 2. On all APIs, software pasting (long press -> paste) doesn't automatically wrap the paste in paragraph
+ // tags in new posts. On API19+ this is corrected by ZSSField.handlePasteEvent(), but earlier APIs don't support
+ // it. So, instead, we allow the editor not to wrap the paste in paragraph tags and it's automatically corrected
+ // after adding a newline. But allowing wrapCaretInParagraphIfNecessary() to go through will wrap the paragraph
+ // incorrectly, so we skip it in this case.
+ //
+ // 3. On all APIs, hardware pasting (CTRL + V) doesn't automatically wrap the paste in paragraph tags in
+ // new posts. ZSSField.handlePasteEvent() won't fix the wrapping for hardware pastes if
+ // wrapCaretInParagraphIfNecessary() goes through first, so we need to skip it in that case.
+ // For API < 19, this is fixed implicitly by the 'containsParagraphSeparators' check, but for newer APIs we
+ // specifically detect hardware keyboard pastes and skip paragraph wrapping in that case
+ // case skip calling wrapCaretInParagraphIfNecessary().
+ //
+ // Previously, the check was 'if (containsParagraphSeparators)' for all API levels, but this turns out to cause
+ // an autocorrect issue for new posts on API 23+:
+ // https://github.com/wordpress-mobile/WordPress-Editor-Android/issues/389
+ // That issue can be fixed by allowing wrapCaretInParagraphIfNecessary() to go through when adding text to an
+ // empty post on those API levels, as long as we exclude the special case of hardware keyboard pasting
+ //
+ // We're using 'nativeState.androidApiLevel>19' rather than >22 because, even though the bug only appears on
+ // 23+ at the time of writing, it's entirely possible a future System WebView or Keyboard update will introduce
+ // the bug on 21+.
+ var containsParagraphSeparators = this.getWrappedDomNode().innerHTML.search(
+ '<' + ZSSEditor.defaultParagraphSeparator) > -1;
+ if (containsParagraphSeparators || (nativeState.androidApiLevel > 19 && !isHardwareKeyboardPaste)) {
+ this.wrapCaretInParagraphIfNecessary();
+ }
+
+ if (wasEnterPressed) {
+ // Wrap the existing text in paragraph tags if necessary (this should only be needed if
+ // wrapCaretInParagraphIfNecessary() was skipped earlier)
+ var currentHtml = this.getWrappedDomNode().innerHTML;
+ if (currentHtml.search('<' + ZSSEditor.defaultParagraphSeparator) == -1) {
+ ZSSEditor.focusedField.setHTML(Util.wrapHTMLInTag(currentHtml, ZSSEditor.defaultParagraphSeparator));
+ ZSSEditor.resetSelectionOnField(this.getWrappedDomNode().id, 1);
+ }
+
+ 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();
+ 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();
+ // When pressing enter inside an image caption, clear the caption styling from the new line
+ } else if (parentNode.nodeName == NodeName.SPAN && $(parentNode).hasClass('wp-caption')) {
+ setTimeout(this.handleCaptionNewLine, 100);
+ }
+ }
+ }
+};
+
+ZSSField.prototype.handleInputEvent = function(e) {
+
+ // Skip this if we are composing on an IME keyboard
+ if (this.isComposing ) { return; }
+
+ // IMPORTANT: we want the placeholder to come up if there's no text, so we clear the field if
+ // there's no real content in it. It's important to do this here and not on keyDown or keyUp
+ // as the field could become empty because of a cut or paste operation as well as a key press.
+ // This event takes care of all cases.
+ //
+ this.emptyFieldIfNoContents();
+
+ var joinedArguments = ZSSEditor.getJoinedFocusedFieldIdAndCaretArguments();
+ ZSSEditor.callback('callback-selection-changed', joinedArguments);
+ this.callback("callback-input", joinedArguments);
+};
+
+ZSSField.prototype.handleCompositionStartEvent = function(e) {
+ this.isComposing = true;
+};
+
+ZSSField.prototype.handleCompositionEndEvent = function(e) {
+ this.isComposing = false;
+};
+
+ZSSField.prototype.handleTapEvent = function(e) {
+ var targetNode = e.target;
+
+ if (targetNode) {
+
+ ZSSEditor.lastTappedNode = targetNode;
+
+ if (targetNode.nodeName.toLowerCase() == 'a') {
+ var arguments = ['url=' + encodeURIComponent(targetNode.href),
+ 'title=' + encodeURIComponent(targetNode.innerHTML)];
+ var joinedArguments = arguments.join(defaultCallbackSeparator);
+ this.callback('callback-link-tap', joinedArguments);
+ }
+
+ if (targetNode.nodeName.toLowerCase() == 'img') {
+ // If the image is uploading, or is a local image do not select it.
+ if ( targetNode.dataset.wpid || targetNode.dataset.video_wpid ) {
+ this.sendImageTappedCallback(targetNode);
+ return;
+ }
+
+ // If we're not currently editing just return. No need to apply styles
+ // or acknowledge the tap
+ if ( this.wrappedObject.attr('contenteditable') != "true" ) {
+ return;
+ }
+
+ // Is the tapped image the image we're editing?
+ if ( targetNode == ZSSEditor.currentEditingImage ) {
+ ZSSEditor.removeImageSelectionFormatting(targetNode);
+ this.sendImageTappedCallback(targetNode);
+ return;
+ }
+
+ // If there is a selected image, deselect it. A different image was tapped.
+ if ( ZSSEditor.currentEditingImage ) {
+ ZSSEditor.removeImageSelectionFormatting(ZSSEditor.currentEditingImage);
+ }
+
+ // Format and flag the image as selected.
+ ZSSEditor.currentEditingImage = targetNode;
+ var containerNode = ZSSEditor.applyImageSelectionFormatting(targetNode);
+
+ // Move the cursor to the tapped image, to prevent scrolling to the bottom of the document when the
+ // keyboard comes up. On API 19 and below does not work properly, with the image sometimes getting removed
+ // from the post instead of the edit overlay being displayed
+ if (nativeState.androidApiLevel > 19) {
+ ZSSEditor.setFocusAfterElement(containerNode);
+ }
+
+ return;
+ }
+
+ if (targetNode.className.indexOf('edit-overlay') != -1 || targetNode.className.indexOf('edit-content') != -1
+ || targetNode.className.indexOf('edit-icon') != -1) {
+ ZSSEditor.removeImageSelectionFormatting( ZSSEditor.currentEditingImage );
+
+ this.sendImageTappedCallback( ZSSEditor.currentEditingImage );
+ return;
+ }
+
+ if (targetNode.className.indexOf('upload-overlay') != -1 ||
+ targetNode.className.indexOf('upload-overlay-bg') != -1 ) {
+ // Select the image node associated with the selected upload overlay
+ var imageNode = targetNode.parentNode.getElementsByTagName('img')[0];
+
+ this.sendImageTappedCallback( imageNode );
+ return;
+ }
+
+ if (targetNode.className.indexOf('delete-overlay') != -1) {
+ var parentEditContainer = targetNode.parentElement;
+ var parentDiv = parentEditContainer.parentElement;
+
+ // If the delete button was tapped, removing the media item and its container from the document
+ if (parentEditContainer.classList.contains('edit-container')) {
+ parentEditContainer.parentElement.removeChild(parentEditContainer);
+ } else {
+ parentEditContainer.removeChild(targetNode);
+ }
+
+ this.emptyFieldIfNoContents();
+
+ ZSSEditor.currentEditingImage = null;
+ return;
+ }
+
+ if ( ZSSEditor.currentEditingImage ) {
+ ZSSEditor.removeImageSelectionFormatting( ZSSEditor.currentEditingImage );
+ ZSSEditor.currentEditingImage = null;
+ }
+
+ if (targetNode.nodeName.toLowerCase() == 'video') {
+ // If the video is uploading, or is a local image do not select it.
+ if (targetNode.dataset.wpvideopress) {
+ if (targetNode.src.length == 0 || targetNode.src == 'file:///android_asset/') {
+ // If the tapped video is a placeholder for a VideoPress video, send out an update request.
+ // This provides a way to load the video for Android API<19, where the onError property function in
+ // the placeholder video isn't being triggered, and sendVideoPressInfoRequest is never called.
+ // This is also used to manually retry loading a VideoPress video after the onError attribute has
+ // been stripped for the video tag.
+ targetNode.setAttribute("onerror", "");
+ ZSSEditor.sendVideoPressInfoRequest(targetNode.dataset.wpvideopress);
+ return;
+ }
+ }
+
+ if (targetNode.dataset.wpid) {
+ this.sendVideoTappedCallback( targetNode );
+ return;
+ }
+ }
+ }
+};
+
+ZSSField.prototype.handlePasteEvent = function(e) {
+ if (this.isMultiline() && this.getHTML().length == 0) {
+ ZSSEditor.insertHTML(Util.wrapHTMLInTag('&#x200b;', ZSSEditor.defaultParagraphSeparator));
+ }
+};
+
+/**
+ * @brief Fires after 'keydown' events, when the field contents have already been modified
+ */
+ZSSField.prototype.afterKeyDownEvent = function(beforeHTML, e) {
+ var afterHTML = e.target.innerHTML;
+ var htmlWasModified = (beforeHTML != afterHTML);
+
+ var selection = document.getSelection();
+ var range = selection.getRangeAt(0).cloneRange();
+ var focusedNode = range.startContainer;
+
+ // Correct situation where autocorrect can remove blockquotes at start of document, either when pressing enter
+ // inside a blockquote, or pressing backspace immediately after one
+ // https://github.com/wordpress-mobile/WordPress-Editor-Android/issues/385
+ if (htmlWasModified) {
+ var blockquoteMatch = beforeHTML.match('^<blockquote><div>(.*)</div></blockquote>');
+
+ if (blockquoteMatch != null && afterHTML.match('<blockquote>') == null) {
+ // Blockquote at start of post was removed
+ var newParagraphMatch = afterHTML.match('^<div>(.*?)</div><div><br></div>');
+
+ if (newParagraphMatch != null) {
+ // The blockquote was removed in a newline operation
+ var originalText = blockquoteMatch[1];
+ var newText = newParagraphMatch[1];
+
+ if (originalText != newText) {
+ // Blockquote was removed and its inner text changed - this points to autocorrect removing the
+ // blockquote when changing the text in the previous paragraph, so we replace the blockquote
+ ZSSEditor.turnBlockquoteOnForNode(focusedNode.parentNode.firstChild);
+ }
+ } else if (afterHTML.match('^<div>(.*?)</div>') != null) {
+ // The blockquote was removed in a backspace operation
+ ZSSEditor.turnBlockquoteOnForNode(focusedNode.parentNode);
+ ZSSEditor.setFocusAfterElement(focusedNode);
+ }
+ }
+ }
+
+ var focusedNodeIsEmpty = (focusedNode.innerHTML != undefined
+ && (focusedNode.innerHTML.length == 0 || focusedNode.innerHTML == '<br>'));
+
+ // Blockquote handling
+ if (focusedNode.nodeName == NodeName.BLOCKQUOTE && focusedNodeIsEmpty) {
+ if (!htmlWasModified) {
+ // We only want to handle this if the last character inside a blockquote was just deleted - if the HTML
+ // is unchanged, it might be that afterKeyDownEvent was called too soon, and we should avoid doing anything
+ return;
+ }
+
+ // When using backspace to delete the contents of a blockquote, the div within the blockquote is deleted
+ // This makes the blockquote unable to be deleted using backspace, and also causes autocorrect issues on API19+
+ range.startContainer.innerHTML = Util.wrapHTMLInTag('<br>', ZSSEditor.defaultParagraphSeparator);
+
+ // Give focus to new div
+ var newFocusElement = focusedNode.firstChild;
+ ZSSEditor.giveFocusToElement(newFocusElement, 1);
+ } else if (focusedNode.nodeName == NodeName.DIV && focusedNode.parentNode.nodeName == NodeName.BLOCKQUOTE) {
+ if (focusedNode.parentNode.previousSibling == null && focusedNode.parentNode.childNodes.length == 1
+ && focusedNodeIsEmpty) {
+ // When a post begins with a blockquote, and there's content after that blockquote, backspacing inside that
+ // blockquote will work until the blockquote is empty. After that, backspace will have no effect
+ // This fix identifies that situation and makes the call to setBlockquote() to toggle off the blockquote
+ ZSSEditor.setBlockquote();
+ } else {
+ // Remove extraneous break tags sometimes added to blockquotes by autocorrect actions
+ // https://github.com/wordpress-mobile/WordPress-Editor-Android/issues/385
+ var blockquoteChildNodes = focusedNode.parentNode.childNodes;
+
+ for (var i = 0; i < blockquoteChildNodes.length; i++) {
+ var childNode = blockquoteChildNodes[i];
+ if (childNode.nodeName == NodeName.BR) {
+ childNode.parentNode.removeChild(childNode);
+ }
+ }
+ }
+ }
+};
+
+ZSSField.prototype.sendImageTappedCallback = function(imageNode) {
+ var meta = JSON.stringify(ZSSEditor.extractImageMeta(imageNode));
+ var imageId = "", mediaType = "image";
+ if (imageNode.hasAttribute('data-wpid')){
+ imageId = imageNode.getAttribute('data-wpid');
+ } else if (imageNode.hasAttribute('data-video_wpid')){
+ imageId = imageNode.getAttribute('data-video_wpid');
+ mediaType = "video";
+ }
+ var arguments = ['id=' + encodeURIComponent(imageId),
+ 'url=' + encodeURIComponent(imageNode.src),
+ 'meta=' + encodeURIComponent(meta),
+ 'type=' + mediaType];
+
+ var joinedArguments = arguments.join(defaultCallbackSeparator);
+
+ var thisObj = this;
+
+ // WORKAROUND: force the event to become sort of "after-tap" through setTimeout()
+ //
+ setTimeout(function() { thisObj.callback('callback-image-tap', joinedArguments);}, 500);
+}
+
+ZSSField.prototype.sendVideoTappedCallback = function( videoNode ) {
+ var videoId = "";
+ if ( videoNode.hasAttribute( 'data-wpid' ) ){
+ videoId = videoNode.getAttribute( 'data-wpid' )
+ }
+ var arguments = ['id=' + encodeURIComponent( videoId ),
+ 'url=' + encodeURIComponent( videoNode.src )];
+
+ var joinedArguments = arguments.join( defaultCallbackSeparator );
+
+ ZSSEditor.callback('callback-video-tap', joinedArguments);
+}
+
+// MARK: - Callback Execution
+
+ZSSField.prototype.callback = function(callbackScheme, callbackPath) {
+ var url = callbackScheme + ":";
+
+ url = url + "id=" + this.getNodeId();
+
+ if (callbackPath) {
+ url = url + defaultCallbackSeparator + callbackPath;
+ }
+
+ if (isUsingiOS) {
+ ZSSEditor.callbackThroughIFrame(url);
+ } else if (isUsingAndroid) {
+ if (nativeState.androidApiLevel < 17) {
+ ZSSEditor.callbackThroughIFrame(url);
+ } else {
+ nativeCallbackHandler.executeCallback(callbackScheme, callbackPath);
+ }
+ } else {
+ console.log(url);
+ }
+};
+
+// MARK: - Focus
+
+ZSSField.prototype.isFocused = function() {
+
+ return this.wrappedObject.is(':focus');
+};
+
+ZSSField.prototype.focus = function() {
+
+ if (!this.isFocused()) {
+ this.wrappedObject.focus();
+ }
+};
+
+ZSSField.prototype.blur = function() {
+ if (this.isFocused()) {
+ this.wrappedObject.blur();
+ }
+};
+
+// MARK: - Multiline support
+
+ZSSField.prototype.isMultiline = function() {
+ return this.multiline;
+};
+
+ZSSField.prototype.setMultiline = function(multiline) {
+ this.multiline = multiline;
+};
+
+// MARK: - NodeId
+
+ZSSField.prototype.getNodeId = function() {
+ return this.wrappedObject.attr('id');
+};
+
+// MARK: - Editing
+
+ZSSField.prototype.enableEditing = function () {
+
+ this.wrappedObject.attr('contenteditable', true);
+
+ if (!ZSSEditor.focusedField) {
+ ZSSEditor.focusFirstEditableField();
+ }
+};
+
+ZSSField.prototype.disableEditing = function () {
+ // IMPORTANT: we're blurring the field before making it non-editable since that ensures
+ // that the iOS keyboard is dismissed through an animation, as opposed to being immediately
+ // removed from the screen.
+ //
+ this.blur();
+
+ this.wrappedObject.attr('contenteditable', false);
+};
+
+// MARK: - Caret
+
+/**
+ * @brief Whenever this method is called, a check will be performed on the caret position
+ * to figure out if it needs to be wrapped in a paragraph node.
+ * @details A parent paragraph node should be added if the current parent is either the field
+ * node itself, or a blockquote node.
+ */
+ZSSField.prototype.wrapCaretInParagraphIfNecessary = function() {
+ var closerParentNode = ZSSEditor.closerParentNode();
+
+ if (closerParentNode == null) {
+ return;
+ }
+
+ var parentNodeShouldBeParagraph = (closerParentNode == this.getWrappedDomNode()
+ || closerParentNode.nodeName == NodeName.BLOCKQUOTE);
+
+ // When starting a post with a blockquote (before any text is entered), the paragraph tags inside the blockquote
+ // won't properly wrap the text once it's entered
+ // In that case, remove the paragraph tags and re-apply them, wrapping around the newly entered text
+ var fixNewPostBlockquoteBug = (closerParentNode.nodeName == NodeName.DIV
+ && closerParentNode.parentNode.nodeName == NodeName.BLOCKQUOTE
+ && closerParentNode.innerHTML.length == 0);
+
+ // On API 19 and below, identifying the situation where the blockquote bug for new posts occurs works a little
+ // differently than above, with the focused node being the parent blockquote rather than the empty div inside it.
+ // We still remove the empty div so it can be re-applied correctly to the newly entered text, but we select it
+ // differently
+ // https://github.com/wordpress-mobile/WordPress-Editor-Android/issues/398
+ var fixNewPostBlockquoteBugOldApi = (closerParentNode.nodeName == NodeName.BLOCKQUOTE
+ && closerParentNode.parentNode.nodeName == NodeName.DIV
+ && closerParentNode.innerHTML == '<div></div>');
+
+ if (parentNodeShouldBeParagraph || fixNewPostBlockquoteBug || fixNewPostBlockquoteBugOldApi) {
+ var selection = window.getSelection();
+
+ if (selection && selection.rangeCount > 0) {
+ var range = selection.getRangeAt(0);
+
+ if (range.startContainer == range.endContainer) {
+ if (fixNewPostBlockquoteBug) {
+ closerParentNode.parentNode.removeChild(closerParentNode);
+ } else if (fixNewPostBlockquoteBugOldApi) {
+ closerParentNode.removeChild(closerParentNode.firstChild);
+ }
+
+ var storedStyles = [];
+ if (this.getWrappedDomNode().innerHTML.length == 0) {
+ // If the post is empty, store any active in-line formatting so it can be re-applied after the
+ // DOM manipulations are completed
+ // (Fix for https://github.com/wordpress-mobile/WordPress-Editor-Android/issues/204)
+ storedStyles = ZSSEditor.storeInlineStylesAsFunctions();
+ }
+
+ var paragraph = document.createElement("div");
+ var textNode = document.createTextNode("&#x200b;");
+
+ paragraph.appendChild(textNode);
+
+ range.insertNode(paragraph);
+ range.selectNode(textNode);
+
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ // Re-apply inline styles that were cleared
+ storedStyles.map(function(styleFunction) {
+ styleFunction();
+ });
+ }
+ }
+ }
+};
+
+/**
+ * @brief Called when enter is pressed inside an image caption. Clears away the span and label tags the new line
+ * inherits from the caption styling.
+ */
+ZSSField.prototype.handleCaptionNewLine = function() {
+ var selectedNode = document.getSelection().baseNode;
+
+ var contentsNode;
+ if (selectedNode.firstChild != null) {
+ contentsNode = selectedNode.firstChild.cloneNode();
+ } else {
+ contentsNode = selectedNode.cloneNode();
+ }
+
+ var parentSpan = selectedNode.parentNode.parentNode;
+ var parentDiv = parentSpan.parentNode;
+
+ var paragraph = document.createElement("div");
+ paragraph.appendChild(contentsNode);
+
+ parentDiv.insertBefore(paragraph, parentSpan);
+ parentDiv.removeChild(parentSpan);
+
+ ZSSEditor.giveFocusToElement(contentsNode);
+};
+
+// MARK: - i18n
+
+ZSSField.prototype.isRightToLeftTextEnabled = function() {
+ var textDir = this.wrappedObject.attr('dir');
+ var isRTL = (textDir != "undefined" && textDir == 'rtl');
+ return isRTL;
+};
+
+ZSSField.prototype.enableRightToLeftText = function(isRTL) {
+ var textDirectionString = isRTL ? "rtl" : "ltr";
+ this.wrappedObject.attr('dir', textDirectionString);
+ this.wrappedObject.css('direction', textDirectionString);
+};
+
+// MARK: - HTML contents
+
+ZSSField.prototype.isEmpty = function() {
+ var html = this.getHTML();
+ var isEmpty = (html.length == 0 || html == "<br>");
+
+ return isEmpty;
+};
+
+ZSSField.prototype.getHTML = function() {
+ var html = this.wrappedObject.html();
+ if (ZSSEditor.defaultParagraphSeparator == 'div') {
+ html = Formatter.convertDivToP(html);
+ }
+ html = Formatter.visualToHtml(html);
+ html = ZSSEditor.removeVisualFormatting( html );
+ return html;
+};
+
+ZSSField.prototype.getHTMLForCallback = function() {
+ var functionArgument = "function=getHTMLForCallback";
+ var idArgument = "id=" + this.getNodeId();
+ var contentsArgument;
+
+ if (this.hasNoStyle) {
+ contentsArgument = "contents=" + this.strippedHTML();
+ } else {
+ var html;
+ if (nativeState.androidApiLevel < 17) {
+ // URI Encode HTML on API < 17 because of the use of WebViewClient.shouldOverrideUrlLoading. Data must
+ // be decoded in shouldOverrideUrlLoading.
+ html = encodeURIComponent(this.getHTML());
+ } else {
+ html = this.getHTML();
+ }
+ contentsArgument = "contents=" + html;
+ }
+ var joinedArguments = functionArgument + defaultCallbackSeparator + idArgument + defaultCallbackSeparator +
+ contentsArgument;
+ ZSSEditor.callback('callback-response-string', joinedArguments);
+};
+
+ZSSField.prototype.strippedHTML = function() {
+ return this.wrappedObject.text();
+};
+
+ZSSField.prototype.setPlainText = function(text) {
+ ZSSEditor.currentEditingImage = null;
+ this.wrappedObject.text(text);
+};
+
+ZSSField.prototype.setHTML = function(html) {
+ ZSSEditor.currentEditingImage = null;
+ var mutatedHTML = Formatter.htmlToVisual(html);
+
+ if (ZSSEditor.defaultParagraphSeparator == 'div') {
+ mutatedHTML = Formatter.convertPToDiv(mutatedHTML);
+ }
+
+ this.wrappedObject.html(mutatedHTML);
+
+ // Track video container nodes for mutation
+ var videoNodes = $('span.edit-container > video');
+ for (var i = 0; i < videoNodes.length; i++) {
+ ZSSEditor.trackNodeForMutation($(videoNodes[i].parentNode));
+ }
+};
+
+// MARK: - Placeholder
+
+ZSSField.prototype.hasPlaceholderText = function() {
+ return this.wrappedObject.attr('placeholderText') != null;
+};
+
+ZSSField.prototype.setPlaceholderText = function(placeholder) {
+ this.wrappedObject.attr('placeholderText', placeholder);
+};
+
+// MARK: - Wrapped Object
+
+ZSSField.prototype.getWrappedDomNode = function() {
+ return this.wrappedObject[0];
+};
diff --git a/libs/editor/WordPressEditor/src/main/assets/android-editor.html b/libs/editor/WordPressEditor/src/main/assets/android-editor.html
new file mode 100755
index 000000000..d70c11fac
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/android-editor.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>%%TITLE%%</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0">
+ <script type="text/javascript">
+ var nativeState = new Object();
+ nativeState.androidApiLevel=%%ANDROID_API_LEVEL%%;
+ %%LOCALIZED_STRING_INIT%%
+ </script>
+ <script src="libs/jquery-2.1.3.min.js"></script>
+ <script src="libs/js-beautifier.js"></script>
+ <script src="libs/underscore-min.js"></script>
+ <script src="libs/shortcode.js"></script>
+ <script src="libs/jquery.mobile-events.min.js"></script>
+ <script src="libs/wpload.js"></script>
+ <script src="libs/wpsave.js"></script>
+ <script src="libs/rangy-core.js"></script>
+ <script src="libs/rangy-classapplier.js"></script>
+ <script src="libs/rangy-highlighter.js"></script>
+ <script src="libs/rangy-selectionsaverestore.js"></script>
+ <script src="libs/rangy-serializer.js"></script>
+ <script src="libs/rangy-textrange.js"></script>
+ <script src="editor-utils.js"></script>
+ <script src="editor-utils-formatter.js"></script>
+ <script src="ZSSRichTextEditor.js"></script>
+ <script>
+ // DRM: onLoad does not get called when offline, if there's remote content in the editor
+ // (such as remote images). We use the 'ready' event for this reason.
+ //
+ $(document).ready(function() {
+ ZSSEditor.init();
+ ZSSEditor.domLoadedCallback();
+ });
+ </script>
+ <link rel="stylesheet" href="editor.css">
+ <link rel="stylesheet" href="editor-android.css">
+ </head>
+ <body>
+ <div contenteditable="true" id="zss_field_title" class="field" nostyle>
+ </div>
+ <div contenteditable="false" id="separatorDiv" >
+ <hr>
+ </div>
+ <div contenteditable="true" id="zss_field_content" class="field">
+ </div>
+ </body>
+</html>
diff --git a/libs/editor/WordPressEditor/src/main/assets/editor-android.css b/libs/editor/WordPressEditor/src/main/assets/editor-android.css
new file mode 100644
index 000000000..cd57c2424
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/editor-android.css
@@ -0,0 +1,95 @@
+@media screen and (min-width: 720px) and (max-width: 1279px) {
+ body {
+ padding-left:90px;
+ padding-right:90px;
+ }
+}
+
+@media screen and (min-width: 1280px){
+ body {
+ padding-left:170px;
+ padding-right:170px;
+ }
+}
+
+video::-webkit-media-controls-fullscreen-button {
+ display: none;
+}
+
+/* Duplicates paragraph tag formatting for div tags, which are needed on Android API 19+ due to autocorrect issues:
+https://bugs.chromium.org/p/chromium/issues/detail?id=599890
+*/
+div:not(.field):not(#separatorDiv) {
+ line-height: 24px;
+ margin-top: 0px;
+ margin-bottom: 24px;
+}
+
+/* --- API<19 workarounds --- */
+
+/* Used only on older APIs (API<19), which don't support CSS filter effects (specifically, blur). */
+.edit-container .edit-overlay-bg {
+ position: absolute;
+ top: -6px;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: block;
+ background-color: rgba(0, 0, 0, 0.5);
+}
+
+/* Used only on older APIs (API<19), where using inline-block is buggy and sometimes displays a very small container */
+span.img_container.compat,
+span.video_container.compat {
+ display: block;
+}
+
+/* Used on API<19 to darken the image so that the 'uploading' and 'retry' overlays can still be seen when the image is
+light */
+.img_container .upload-overlay-bg,
+.video_container .upload-overlay-bg {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ display: block;
+ background-color: rgba(0, 0, 0, 0.5);
+}
+
+/* When the upload-overlay-bg element is present (API<19), a bug is exposed where the img_container is slightly larger
+than its containing image. The upload-overlay-bg is larger as well, leaving a dark line below the image. By setting
+display:block on the image and setting a width limit we get around this issue. */
+
+.img_container .upload-overlay-bg ~ img.uploading,
+.video_container .upload-overlay-bg ~ img.uploading {
+ display:block;
+ max-width:100%;
+}
+
+.img_container .upload-overlay-bg ~ img.failed,
+.video_container .upload-overlay-bg ~ img.failed{
+ display:block;
+ max-width:100%;
+}
+
+/* Used only on older APIs (API<19) instead of a progress bar for uploading images, since the WebView at those API
+ levels does not support the progress tag */
+.img_container .upload-overlay,
+.video_container .upload-overlay{
+ position: absolute;
+ top: 50%;
+ -webkit-transform: translateY(-50%);
+ width:100%;
+ z-index: 100;
+ min-width: 60px;
+ font-family: sans-serif;
+ font-size:20px;
+ font-weight:600;
+ text-align: center;
+ text-shadow: 0px 1px 2px rgba(0,0,0,.06);
+ color: white;
+}
+
+.img_container .upload-overlay.failed,
+.video_container .upload-overlay.failed{
+ visibility: hidden;
+}
diff --git a/libs/editor/WordPressEditor/src/main/assets/editor-utils-formatter.js b/libs/editor/WordPressEditor/src/main/assets/editor-utils-formatter.js
new file mode 100644
index 000000000..69b0254ca
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/editor-utils-formatter.js
@@ -0,0 +1,158 @@
+function Formatter () {}
+
+// Video format tags supported by the [video] shortcode: https://codex.wordpress.org/Video_Shortcode
+// mp4, m4v and webm prioritized since they're supported by the stock player as of Android API 23
+Formatter.videoShortcodeFormats = ["mp4", "m4v", "webm", "ogv", "wmv", "flv"];
+
+Formatter.htmlToVisual = function(html) {
+ var mutatedHTML = wp.loadText(html);
+
+ // Perform extra transformations to properly wrap captioned images in paragraphs
+ mutatedHTML = mutatedHTML.replace(/^\[caption([^\]]*\])/igm, '<p>[caption$1');
+ mutatedHTML = mutatedHTML.replace(/([^\n>])\[caption/igm, '$1<br />\n[caption');
+ mutatedHTML = mutatedHTML.replace(/\[\/caption\]\n(?=<|$)/igm, '[/caption]</p>\n');
+ mutatedHTML = mutatedHTML.replace(/\[\/caption\]\n(?=[^<])/igm, '[/caption]<br />\n');
+
+ return Formatter.applyVisualFormatting(mutatedHTML);
+}
+
+Formatter.convertPToDiv = function(html) {
+ // Replace the paragraph tags we get from wpload with divs
+ var mutatedHTML = html.replace(/(<p(?=[>\s]))/igm, '<div').replace(/<\/p>/igm, '</div>');
+
+ // Replace break tags around media items with paragraphs
+ // The break tags appear when text and media are separated by only a line break rather than a paragraph break,
+ // which can happen when inserting media inline and switching to HTML mode and back, or by deleting line breaks
+ // in HTML mode
+ mutatedHTML = mutatedHTML.replace(/<br \/>(?=\s*(<img|<a href|<label|<video|<span class="edit-container"))/igm,
+ '</div><div>');
+ mutatedHTML = mutatedHTML.replace(/(<img [^<>]*>|<\/a>|<\/label>|<\/video>|<\/span>)<br \/>/igm,
+ function replaceBrWithDivs(match) { return match.substr(0, match.length - 6) + '</div><div>'; });
+
+ // Append paragraph-wrapped break tag under media at the end of a post
+ mutatedHTML = mutatedHTML.replace(/(<img [^<>]*>|<\/a>|<\/label>|<\/video>|<\/span>)[^<>]*<\/div>\s$/igm,
+ function replaceBrWithDivs(match) { return match + '<div><br></div>'; });
+
+ return mutatedHTML;
+}
+
+Formatter.visualToHtml = function(html) {
+ return wp.saveText(html);
+ return Formatter.removeVisualFormatting(mutatedHTML);
+}
+
+Formatter.convertDivToP = function(html) {
+ return html.replace(/(<div(?=[>\s]))/igm, '<p').replace(/<\/div>/igm, '</p>');
+}
+
+/**
+ * @brief Applies editor specific visual formatting.
+ *
+ * @param html The markup to format
+ *
+ * @return Returns the string with the visual formatting applied.
+ */
+Formatter.applyVisualFormatting = function(html) {
+ var str = wp.shortcode.replace('caption', html, Formatter.applyCaptionFormatting);
+ str = wp.shortcode.replace('wpvideo', str, Formatter.applyVideoPressFormattingCallback);
+ str = wp.shortcode.replace('video', str, Formatter.applyVideoFormattingCallback);
+
+ // More tag
+ str = str.replace(/<!--more(.*?)-->/igm, "<hr class=\"more-tag\" wp-more-data=\"$1\">")
+ str = str.replace(/<!--nextpage-->/igm, "<hr class=\"nextpage-tag\">")
+ return str;
+}
+
+/**
+ * @brief Adds visual formatting to a caption shortcodes.
+ *
+ * @param html The markup containing caption shortcodes to process.
+ *
+ * @return The html with caption shortcodes replaced with editor specific markup.
+ * See shortcode.js::next or details
+ */
+Formatter.applyCaptionFormatting = function(match) {
+ var attrs = match.attrs.named;
+ // The empty 'onclick' is important. It prevents the cursor jumping to the end
+ // of the content body when `-webkit-user-select: none` is set and the caption is tapped.
+ var out = '<label class="wp-temp" data-wp-temp="caption" onclick="">';
+ out += '<span class="wp-caption"';
+
+ if (attrs.width) {
+ out += ' style="width:' + attrs.width + 'px; max-width:100% !important;"';
+ }
+ for (var key in attrs) {
+ out += " data-caption-" + key + '="' + attrs[key] + '"';
+ }
+
+ out += '>';
+ out += match.content;
+ out += '</span>';
+ out += '</label>';
+
+ return out;
+}
+
+Formatter.applyVideoPressFormattingCallback = function(match) {
+ if (match.attrs.numeric.length == 0) {
+ return match.content;
+ }
+ var videopressID = match.attrs.numeric[0];
+ var posterSVG = '"svg/wpposter.svg"';
+ // The empty 'onclick' is important. It prevents the cursor jumping to the end
+ // of the content body when `-webkit-user-select: none` is set and the video is tapped.
+ var out = '<video data-wpvideopress="' + videopressID + '" webkit-playsinline src="" preload="metadata" poster='
+ + posterSVG +' onclick="" onerror="ZSSEditor.sendVideoPressInfoRequest(\'' + videopressID +'\');"></video>';
+
+ // Wrap video in edit-container node for a permanent delete button overlay
+ var containerStart = '<span class="edit-container" contenteditable="false"><span class="delete-overlay"></span>';
+ out = containerStart + out + '</span>';
+
+ return out;
+}
+
+Formatter.applyVideoFormattingCallback = function(match) {
+ // Find the tag containing the video source
+ var srcTag = "";
+
+ if (match.attrs.named['src']) {
+ srcTag = "src";
+ } else {
+ for (var i = 0; i < Formatter.videoShortcodeFormats.length; i++) {
+ var format = Formatter.videoShortcodeFormats[i];
+ if (match.attrs.named[format]) {
+ srcTag = format;
+ break;
+ }
+ }
+ }
+
+ if (srcTag.length == 0) {
+ return match.content;
+ }
+
+ var out = '<video webkit-playsinline src="' + match.attrs.named[srcTag] + '"';
+
+ // Preserve all existing tags
+ for (var item in match.attrs.named) {
+ if (item != srcTag) {
+ out += ' ' + item + '="' + match.attrs.named[item] + '"';
+ }
+ }
+
+ if (!match.attrs.named['preload']) {
+ out += ' preload="metadata"';
+ }
+
+ out += ' onclick="" controls="controls"></video>';
+
+ // Wrap video in edit-container node for a permanent delete button overlay
+ var containerStart = '<span class="edit-container" contenteditable="false"><span class="delete-overlay"></span>';
+ out = containerStart + out + '</span>';
+
+ return out;
+}
+
+if (typeof module !== 'undefined' && module.exports != null) {
+ exports.Formatter = Formatter;
+} \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/editor-utils.js b/libs/editor/WordPressEditor/src/main/assets/editor-utils.js
new file mode 100644
index 000000000..5c211d153
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/editor-utils.js
@@ -0,0 +1,26 @@
+function Util () {}
+
+/* Tag building */
+
+Util.buildOpeningTag = function(tagName) {
+ return '<' + tagName + '>';
+};
+
+Util.buildClosingTag = function(tagName) {
+ return '</' + tagName + '>';
+};
+
+Util.wrapHTMLInTag = function(html, tagName) {
+ return Util.buildOpeningTag(tagName) + html + Util.buildClosingTag(tagName);
+};
+
+/* Selection */
+
+Util.rangeIsAtStartOfParent = function(range) {
+ return (range.startContainer.previousSibling == null && range.startOffset == 0);
+};
+
+Util.rangeIsAtEndOfParent = function(range) {
+ return ((range.startContainer.nextSibling == null || range.startContainer.nextSibling == "<br>")
+ && range.endOffset == range.endContainer.length);
+}; \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/editor.css b/libs/editor/WordPressEditor/src/main/assets/editor.css
new file mode 100644
index 000000000..4e7fa07ba
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/editor.css
@@ -0,0 +1,456 @@
+@font-face {
+ font-family: 'Merriweather';
+ src: local("Merriweather Light"),
+ url('fonts/Merriweather-Light.ttf');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'Merriweather';
+ src: local("Merriweather-Italic"),
+ url('fonts/Merriweather-Italic.ttf');
+ font-weight: normal;
+ font-style: italic;
+}
+@font-face {
+ font-family: 'Merriweather';
+ src: local("Merriweather-Bold"),
+ url('fonts/Merriweather-Bold.ttf');
+ font-weight: bold;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'Merriweather';
+ src: local("Merriweather-BoldItalic"),
+ url('fonts/Merriweather-BoldItalic.ttf');
+ font-weight: bold;
+ font-style: italic;
+}
+
+* {outline: 0px solid transparent;
+ -webkit-tap-highlight-color: rgba(0,0,0,0);
+ -webkit-touch-callout: none;
+}
+
+html {
+ padding:0;
+ box-sizing: border-box;
+}
+
+html, body {
+ margin:0;
+ font-family:'Merriweather', sans-serif;
+ font-size:1em;
+ color:#2D2D2D;
+}
+
+body {
+ padding-left:15px;
+ padding-right:15px;
+ padding-top: 15px;
+ padding-bottom: 5px;
+ overflow-y: auto;
+ min-height: 100vh;
+ word-wrap: break-word;
+}
+
+p {
+ line-height: 24px;
+ margin-top: 0px;
+ margin-bottom: 24px;
+}
+
+hr {
+ border: none;
+ height: 1px;
+ color: #E9EFF3;
+ background-color: #E9EFF3;
+ width: 100%;
+}
+
+img {
+ width: auto;
+ height: auto;
+ margin: 0px 0 0px 0;
+ min-width: 30px;
+ min-height: 30px;
+ max-width: 100%;
+ opacity:1;
+}
+
+video {
+ width: auto;
+ height: auto;
+ margin: 0px 0px 0px 0px;
+ min-width: 30px;
+ min-height: 30px;
+ max-width: 100%;
+ opacity:1;
+ background:#2e4453
+}
+
+a {
+ color: #0087be;
+ text-decoration: none;
+}
+
+blockquote {
+ background: #e8f0f5;
+ padding: 10px 10px 10px 20px;
+ margin: 10px 0 10px 0;
+ border-radius: 2px;
+}
+
+img.zs_active {
+ border: 2px dashed #000;
+}
+
+img.uploading {
+ opacity:0.3;
+ -webkit-user-select: none;
+}
+
+img.failed {
+ -webkit-filter: blur(4px) grayscale(0.3);
+ margin:-1px;
+ padding:1px;
+ z-index:-1;
+ -webkit-user-select: none;
+ overflow: hidden;
+}
+
+span.img_container {
+ position: relative;
+ display: inline-block;
+ -webkit-user-select: none;
+}
+
+span.img_container.failed {
+ overflow: hidden;
+}
+
+span.img_container.failed::before {
+ position: absolute;
+ top: 50%;
+ -webkit-transform: translateY(-50%);
+ content:"";
+ left: 0;
+ right: 0;
+ width: 48px;
+ height: 48px;
+ display: block;
+ margin: auto;
+ z-index: 100;
+ border: 2px solid #FFFFFF;
+ border-radius: 50%;
+ background: url('svg/retry-image.svg') no-repeat center;
+ background-color: rgba(0, 0, 0, 0.1);
+ -webkit-user-select: none;
+ pointer-events: none;
+}
+
+span.img_container.failed.largeFail::before {
+ height: 120px;
+ width: 120px;
+ border: 4px solid #FFFFFF;
+ background: url('svg/retry-image-large.svg') no-repeat center;
+ background-color: rgba(0, 0, 0, 0.1);
+}
+
+span.img_container.failed::after {
+ position: absolute;
+ padding: 37px 0 0 0;
+ top:50%;
+ left:0%;
+ font-family: sans-serif;
+ font-size:20px;
+ font-weight: 500;
+ text-align: center;
+ text-shadow: 0 1px 2px rgba(0,0,0,.06);
+ background:clear;
+ color: white;
+ width:100%;
+ height:50%;
+ -webkit-user-select: none;
+ pointer-events: none;
+ content:attr(data-failed);
+}
+
+span.img_container.failed.largeFail::after {
+ padding: 72px 0 0 0;
+}
+
+span.img_container.failed.smallFail::after {
+ font-family: sans-serif;
+ content:attr(data-failed);
+}
+
+video.uploading {
+ opacity:0.3;
+ -webkit-user-select: none;
+}
+
+video.failed {
+ -webkit-filter: blur(4px) grayscale(0.3);
+ margin:-1px;
+ padding:1px;
+ z-index:-1;
+ -webkit-user-select: none;
+ overflow: hidden;
+}
+
+span.video_container {
+ position: relative;
+ display: inline-block;
+ -webkit-user-select: none;
+}
+
+span.video_container.failed {
+ overflow: hidden;
+}
+
+span.video_container.failed::before {
+ position: absolute;
+ top: 50%;
+ -webkit-transform: translateY(-50%);
+ content:"";
+ left: 0;
+ right: 0;
+ width: 48px;
+ height: 48px;
+ display: block;
+ margin: auto;
+ z-index: 100;
+ border: 2px solid #FFFFFF;
+ border-radius: 50%;
+ background: url('svg/retry-image.svg') no-repeat center;
+ background-color: rgba(0, 0, 0, 0.1);
+ -webkit-user-select: none;
+ pointer-events: none;
+}
+
+span.video_container.failed.largeFail::before {
+ height: 120px;
+ width: 120px;
+ border: 4px solid #FFFFFF;
+ background: url('svg/retry-image-large.svg') no-repeat center;
+ background-color: rgba(0, 0, 0, 0.1);
+}
+
+span.video_container.failed::after {
+ position: absolute;
+ padding: 37px 0 0 0;
+ top:50%;
+ left:0%;
+ font-family: sans-serif;
+ font-size:20px;
+ font-weight: 500;
+ text-align: center;
+ text-shadow: 0 1px 2px rgba(0,0,0,.06);
+ background:clear;
+ color: white;
+ width:100%;
+ height:50%;
+ -webkit-user-select: none;
+ pointer-events: none;
+ content:attr(data-failed);
+}
+
+span.video_container.failed.largeFail::after {
+ padding: 72px 0 0 0;
+}
+
+span.video_container.failed.smallFail::after {
+ font-family: sans-serif;
+ content:attr(data-failed);
+}
+
+/* Image and video editing overlay styles */
+.edit-container {
+ position: relative;
+ display: inline-block;
+ -webkit-user-select: none;
+ overflow: hidden;
+}
+
+.edit-container img {
+ -webkit-filter: blur(4px) grayscale(0.3);
+ margin:-1px; /*tiny margin to keep crisp edges when blurring the image*/
+ padding:1px;
+}
+
+.edit-container video {
+
+}
+
+/* default. use when images are > 100px w/h */
+.edit-container .edit-overlay {
+ position: absolute;
+ width: 100%;
+ top: 50%;
+ -webkit-transform: translateY(-50%);
+ z-index: 100;
+ height:90px;
+ min-height: 90px;
+ min-width: 60px;
+}
+
+/* use when the image is < 100px w/h */
+.edit-container.small .edit-overlay {
+ height: 32px;
+ min-height: 32px;
+ min-width: 32px;
+}
+
+.edit-container .edit-icon {
+ width: 48px;
+ height: 48px;
+ display: block;
+ margin: auto;
+ z-index: 100;
+ border: 2px solid #FFFFFF;
+ border-radius: 50%;
+ background: url('svg/edit-image.svg') no-repeat center;
+ background-color: rgba(0, 0, 0, 0.1);
+}
+
+.edit-container.small .edit-icon {
+ height: 32px;
+ min-height: 32px;
+ min-width: 32px;
+ border: none;
+ border-radius: 0%;
+ background-color: initial;
+}
+
+.edit-container .delete-overlay {
+ position: absolute;
+ z-index: 100;
+ right: 0%;
+ height: 24px;
+ width: 24px;
+ min-height: 24px;
+ min-width: 24px;
+ border: 2px solid #FFFFFF;
+ border-radius: 50%;
+ background:url('svg/delete-image.svg') no-repeat center;
+ background-color: rgba(0,0,0,0.1);
+ margin-top: 12px;
+ margin-right: 12px;
+}
+
+.edit-container.small .edit-content {
+ display:none;
+}
+
+.edit-container .edit-content {
+ font-family: sans-serif;
+ font-size:20px;
+ font-weight:500;
+ text-align: center;
+ text-shadow: 0px 1px 2px rgba(0,0,0,.06);
+ color: white;
+ -webkit-user-select: none;
+ pointer-events: none;
+ position: absolute;
+ bottom: 0;
+ width:100%;
+}
+
+progress.wp_media_indicator {
+ /* Reset the default appearance */
+ -webkit-appearance: none;
+ -webkit-user-select: none;
+ appearance: none;
+ position: absolute;
+ top:-2pt; height:2pt;
+ left:0px; width:100%;
+}
+
+progress.wp_media_indicator::-webkit-progress-bar {
+ background-color: rgba(232,240,247,1.0);
+}
+
+progress.wp_media_indicator::-webkit-progress-value {
+ background-color:rgba(0,135,190,1.0);
+ border-radius: 0 2pt 2pt 0;
+}
+
+progress.wp_media_indicator.failed::-webkit-progress-bar {
+ background-color: rgba(232,232,232,1.0);
+}
+
+progress.wp_media_indicator.failed::-webkit-progress-value {
+ background-color:rgba(135,135,135,1.0);
+ border-radius: 0 2pt 2pt 0;
+}
+
+
+div.field[contenteditable] {
+ box-sizing: border-box;
+ font-size: 16px;
+}
+
+div.field[placeholderText][contenteditable=true]:empty:before {
+ content: attr(placeholderText);
+ color: #87a6bc;
+ transition: 0.2s ease opacity;
+}
+
+div.field[placeholderText][contenteditable=true]:empty:focus:before {
+ opacity: 0.6;
+}
+
+#separatorDiv {
+ -webkit-user-select: none;
+ padding-top: 5px;
+ padding-bottom: 15px;
+}
+
+#zss_field_title, #zss_field_title p {
+ font-family:'Merriweather', serif;
+ font-weight: bold;
+ font-size: 24px;
+ line-height: 32px;
+ margin-bottom: 0px;
+}
+
+#zss_field_content {
+ font-size: 16px;
+ line-height: 24px;
+ margin-bottom: 10px;
+}
+
+.wp-temp[data-wp-temp="caption"] {
+ -webkit-user-select: none;
+}
+
+.wp-caption {
+ padding: 0px 10px 10px 0px;
+ font-size: 75%;
+ font-style: italic;
+ cursor: none;
+}
+
+.ipad_body {
+ padding-left: 80px;
+ padding-right: 80px;
+}
+
+.more-tag {
+ border: 0;
+ width: 96%;
+ height: 16px;
+ display: block;
+ background: transparent url('svg/more-2x.png') repeat-y scroll center center;
+ background-size: 1900px 20px;
+}
+
+.nextpage-tag {
+ border: 0;
+ width: 96%;
+ height: 16px;
+ display: block;
+ background: transparent url('svg/pagebreak-2x.png') repeat-y scroll center center;
+ background-size: 1900px 20px;
+}
diff --git a/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Bold.ttf b/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Bold.ttf
new file mode 100755
index 000000000..2340777c8
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Bold.ttf
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-BoldItalic.ttf b/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-BoldItalic.ttf
new file mode 100755
index 000000000..f67d7e588
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-BoldItalic.ttf
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Italic.ttf b/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Italic.ttf
new file mode 100755
index 000000000..583358652
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Italic.ttf
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Light.ttf b/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Light.ttf
new file mode 100755
index 000000000..ee7adbd44
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Light.ttf
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Regular.ttf b/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Regular.ttf
new file mode 100755
index 000000000..28a80e487
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/fonts/Merriweather-Regular.ttf
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/fastclick.js b/libs/editor/WordPressEditor/src/main/assets/libs/fastclick.js
new file mode 100644
index 000000000..35a81d683
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/fastclick.js
@@ -0,0 +1,821 @@
+/**
+ * @preserve FastClick: polyfill to remove click delays on browsers with touch UIs.
+ *
+ * @version 1.0.3
+ * @codingstandard ftlabs-jsv2
+ * @copyright The Financial Times Limited [All Rights Reserved]
+ * @license MIT License (see LICENSE.txt)
+ */
+
+/*jslint browser:true, node:true*/
+/*global define, Event, Node*/
+
+
+/**
+ * Instantiate fast-clicking listeners on the specified layer.
+ *
+ * @constructor
+ * @param {Element} layer The layer to listen on
+ * @param {Object} options The options to override the defaults
+ */
+function FastClick(layer, options) {
+ 'use strict';
+ var oldOnClick;
+
+ options = options || {};
+
+ /**
+ * Whether a click is currently being tracked.
+ *
+ * @type boolean
+ */
+ this.trackingClick = false;
+
+
+ /**
+ * Timestamp for when click tracking started.
+ *
+ * @type number
+ */
+ this.trackingClickStart = 0;
+
+
+ /**
+ * The element being tracked for a click.
+ *
+ * @type EventTarget
+ */
+ this.targetElement = null;
+
+
+ /**
+ * X-coordinate of touch start event.
+ *
+ * @type number
+ */
+ this.touchStartX = 0;
+
+
+ /**
+ * Y-coordinate of touch start event.
+ *
+ * @type number
+ */
+ this.touchStartY = 0;
+
+
+ /**
+ * ID of the last touch, retrieved from Touch.identifier.
+ *
+ * @type number
+ */
+ this.lastTouchIdentifier = 0;
+
+
+ /**
+ * Touchmove boundary, beyond which a click will be cancelled.
+ *
+ * @type number
+ */
+ this.touchBoundary = options.touchBoundary || 10;
+
+
+ /**
+ * The FastClick layer.
+ *
+ * @type Element
+ */
+ this.layer = layer;
+
+ /**
+ * The minimum time between tap(touchstart and touchend) events
+ *
+ * @type number
+ */
+ this.tapDelay = options.tapDelay || 200;
+
+ if (FastClick.notNeeded(layer)) {
+ return;
+ }
+
+ // Some old versions of Android don't have Function.prototype.bind
+ function bind(method, context) {
+ return function() { return method.apply(context, arguments); };
+ }
+
+
+ var methods = ['onMouse', 'onClick', 'onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel'];
+ var context = this;
+ for (var i = 0, l = methods.length; i < l; i++) {
+ context[methods[i]] = bind(context[methods[i]], context);
+ }
+
+ // Set up event handlers as required
+ if (deviceIsAndroid) {
+ layer.addEventListener('mouseover', this.onMouse, true);
+ layer.addEventListener('mousedown', this.onMouse, true);
+ layer.addEventListener('mouseup', this.onMouse, true);
+ }
+
+ layer.addEventListener('click', this.onClick, true);
+ layer.addEventListener('touchstart', this.onTouchStart, false);
+ layer.addEventListener('touchmove', this.onTouchMove, false);
+ layer.addEventListener('touchend', this.onTouchEnd, false);
+ layer.addEventListener('touchcancel', this.onTouchCancel, false);
+
+ // Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
+ // which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick
+ // layer when they are cancelled.
+ if (!Event.prototype.stopImmediatePropagation) {
+ layer.removeEventListener = function(type, callback, capture) {
+ var rmv = Node.prototype.removeEventListener;
+ if (type === 'click') {
+ rmv.call(layer, type, callback.hijacked || callback, capture);
+ } else {
+ rmv.call(layer, type, callback, capture);
+ }
+ };
+
+ layer.addEventListener = function(type, callback, capture) {
+ var adv = Node.prototype.addEventListener;
+ if (type === 'click') {
+ adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
+ if (!event.propagationStopped) {
+ callback(event);
+ }
+ }), capture);
+ } else {
+ adv.call(layer, type, callback, capture);
+ }
+ };
+ }
+
+ // If a handler is already declared in the element's onclick attribute, it will be fired before
+ // FastClick's onClick handler. Fix this by pulling out the user-defined handler function and
+ // adding it as listener.
+ if (typeof layer.onclick === 'function') {
+
+ // Android browser on at least 3.2 requires a new reference to the function in layer.onclick
+ // - the old one won't work if passed to addEventListener directly.
+ oldOnClick = layer.onclick;
+ layer.addEventListener('click', function(event) {
+ oldOnClick(event);
+ }, false);
+ layer.onclick = null;
+ }
+}
+
+
+/**
+ * Android requires exceptions.
+ *
+ * @type boolean
+ */
+var deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0;
+
+
+/**
+ * iOS requires exceptions.
+ *
+ * @type boolean
+ */
+var deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent);
+
+
+/**
+ * iOS 4 requires an exception for select elements.
+ *
+ * @type boolean
+ */
+var deviceIsIOS4 = deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent);
+
+
+/**
+ * iOS 6.0(+?) requires the target element to be manually derived
+ *
+ * @type boolean
+ */
+var deviceIsIOSWithBadTarget = deviceIsIOS && (/OS ([6-9]|\d{2})_\d/).test(navigator.userAgent);
+
+/**
+ * BlackBerry requires exceptions.
+ *
+ * @type boolean
+ */
+var deviceIsBlackBerry10 = navigator.userAgent.indexOf('BB10') > 0;
+
+/**
+ * Determine whether a given element requires a native click.
+ *
+ * @param {EventTarget|Element} target Target DOM element
+ * @returns {boolean} Returns true if the element needs a native click
+ */
+FastClick.prototype.needsClick = function(target) {
+ 'use strict';
+ switch (target.nodeName.toLowerCase()) {
+
+ // Don't send a synthetic click to disabled inputs (issue #62)
+ case 'button':
+ case 'select':
+ case 'textarea':
+ if (target.disabled) {
+ return true;
+ }
+
+ break;
+ case 'input':
+
+ // File inputs need real clicks on iOS 6 due to a browser bug (issue #68)
+ if ((deviceIsIOS && target.type === 'file') || target.disabled) {
+ return true;
+ }
+
+ break;
+ case 'label':
+ case 'video':
+ return true;
+ }
+
+ return (/\bneedsclick\b/).test(target.className);
+};
+
+
+/**
+ * Determine whether a given element requires a call to focus to simulate click into element.
+ *
+ * @param {EventTarget|Element} target Target DOM element
+ * @returns {boolean} Returns true if the element requires a call to focus to simulate native click.
+ */
+FastClick.prototype.needsFocus = function(target) {
+ 'use strict';
+ switch (target.nodeName.toLowerCase()) {
+ case 'textarea':
+ return true;
+ case 'select':
+ return !deviceIsAndroid;
+ case 'input':
+ switch (target.type) {
+ case 'button':
+ case 'checkbox':
+ case 'file':
+ case 'image':
+ case 'radio':
+ case 'submit':
+ return false;
+ }
+
+ // No point in attempting to focus disabled inputs
+ return !target.disabled && !target.readOnly;
+ default:
+ return (/\bneedsfocus\b/).test(target.className);
+ }
+};
+
+
+/**
+ * Send a click event to the specified element.
+ *
+ * @param {EventTarget|Element} targetElement
+ * @param {Event} event
+ */
+FastClick.prototype.sendClick = function(targetElement, event) {
+ 'use strict';
+ var clickEvent, touch;
+
+ // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
+ if (document.activeElement && document.activeElement !== targetElement) {
+ document.activeElement.blur();
+ }
+
+ touch = event.changedTouches[0];
+
+ // Synthesise a click event, with an extra attribute so it can be tracked
+ clickEvent = document.createEvent('MouseEvents');
+ clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
+ clickEvent.forwardedTouchEvent = true;
+ targetElement.dispatchEvent(clickEvent);
+};
+
+FastClick.prototype.determineEventType = function(targetElement) {
+ 'use strict';
+
+ //Issue #159: Android Chrome Select Box does not open with a synthetic click event
+ if (deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') {
+ return 'mousedown';
+ }
+
+ return 'click';
+};
+
+
+/**
+ * @param {EventTarget|Element} targetElement
+ */
+FastClick.prototype.focus = function(targetElement) {
+ 'use strict';
+ var length;
+
+ // Issue #160: on iOS 7, some input elements (e.g. date datetime) throw a vague TypeError on setSelectionRange. These elements don't have an integer value for the selectionStart and selectionEnd properties, but unfortunately that can't be used for detection because accessing the properties also throws a TypeError. Just check the type instead. Filed as Apple bug #15122724.
+ if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time') {
+ length = targetElement.value.length;
+ targetElement.setSelectionRange(length, length);
+ } else {
+ targetElement.focus();
+ }
+};
+
+
+/**
+ * Check whether the given target element is a child of a scrollable layer and if so, set a flag on it.
+ *
+ * @param {EventTarget|Element} targetElement
+ */
+FastClick.prototype.updateScrollParent = function(targetElement) {
+ 'use strict';
+ var scrollParent, parentElement;
+
+ scrollParent = targetElement.fastClickScrollParent;
+
+ // Attempt to discover whether the target element is contained within a scrollable layer. Re-check if the
+ // target element was moved to another parent.
+ if (!scrollParent || !scrollParent.contains(targetElement)) {
+ parentElement = targetElement;
+ do {
+ if (parentElement.scrollHeight > parentElement.offsetHeight) {
+ scrollParent = parentElement;
+ targetElement.fastClickScrollParent = parentElement;
+ break;
+ }
+
+ parentElement = parentElement.parentElement;
+ } while (parentElement);
+ }
+
+ // Always update the scroll top tracker if possible.
+ if (scrollParent) {
+ scrollParent.fastClickLastScrollTop = scrollParent.scrollTop;
+ }
+};
+
+
+/**
+ * @param {EventTarget} targetElement
+ * @returns {Element|EventTarget}
+ */
+FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) {
+ 'use strict';
+
+ // On some older browsers (notably Safari on iOS 4.1 - see issue #56) the event target may be a text node.
+ if (eventTarget.nodeType === Node.TEXT_NODE) {
+ return eventTarget.parentNode;
+ }
+
+ return eventTarget;
+};
+
+
+/**
+ * On touch start, record the position and scroll offset.
+ *
+ * @param {Event} event
+ * @returns {boolean}
+ */
+FastClick.prototype.onTouchStart = function(event) {
+ 'use strict';
+ var targetElement, touch, selection;
+
+ // Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111).
+ if (event.targetTouches.length > 1) {
+ return true;
+ }
+
+ targetElement = this.getTargetElementFromEventTarget(event.target);
+ touch = event.targetTouches[0];
+
+ if (deviceIsIOS) {
+
+ // Only trusted events will deselect text on iOS (issue #49)
+ selection = window.getSelection();
+ if (selection.rangeCount && !selection.isCollapsed) {
+ return true;
+ }
+
+ if (!deviceIsIOS4) {
+
+ // Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23):
+ // when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched
+ // with the same identifier as the touch event that previously triggered the click that triggered the alert.
+ // Sadly, there is an issue on iOS 4 that causes some normal touch events to have the same identifier as an
+ // immediately preceeding touch event (issue #52), so this fix is unavailable on that platform.
+ // Issue 120: touch.identifier is 0 when Chrome dev tools 'Emulate touch events' is set with an iOS device UA string,
+ // which causes all touch events to be ignored. As this block only applies to iOS, and iOS identifiers are always long,
+ // random integers, it's safe to to continue if the identifier is 0 here.
+ if (touch.identifier && touch.identifier === this.lastTouchIdentifier) {
+ event.preventDefault();
+ return false;
+ }
+
+ this.lastTouchIdentifier = touch.identifier;
+
+ // If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and:
+ // 1) the user does a fling scroll on the scrollable layer
+ // 2) the user stops the fling scroll with another tap
+ // then the event.target of the last 'touchend' event will be the element that was under the user's finger
+ // when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check
+ // is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42).
+ this.updateScrollParent(targetElement);
+ }
+ }
+
+ this.trackingClick = true;
+ this.trackingClickStart = event.timeStamp;
+ this.targetElement = targetElement;
+
+ this.touchStartX = touch.pageX;
+ this.touchStartY = touch.pageY;
+
+ // Prevent phantom clicks on fast double-tap (issue #36)
+ if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
+ event.preventDefault();
+ }
+
+ return true;
+};
+
+
+/**
+ * Based on a touchmove event object, check whether the touch has moved past a boundary since it started.
+ *
+ * @param {Event} event
+ * @returns {boolean}
+ */
+FastClick.prototype.touchHasMoved = function(event) {
+ 'use strict';
+ var touch = event.changedTouches[0], boundary = this.touchBoundary;
+
+ if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) {
+ return true;
+ }
+
+ return false;
+};
+
+
+/**
+ * Update the last position.
+ *
+ * @param {Event} event
+ * @returns {boolean}
+ */
+FastClick.prototype.onTouchMove = function(event) {
+ 'use strict';
+ if (!this.trackingClick) {
+ return true;
+ }
+
+ // If the touch has moved, cancel the click tracking
+ if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {
+ this.trackingClick = false;
+ this.targetElement = null;
+ }
+
+ return true;
+};
+
+
+/**
+ * Attempt to find the labelled control for the given label element.
+ *
+ * @param {EventTarget|HTMLLabelElement} labelElement
+ * @returns {Element|null}
+ */
+FastClick.prototype.findControl = function(labelElement) {
+ 'use strict';
+
+ // Fast path for newer browsers supporting the HTML5 control attribute
+ if (labelElement.control !== undefined) {
+ return labelElement.control;
+ }
+
+ // All browsers under test that support touch events also support the HTML5 htmlFor attribute
+ if (labelElement.htmlFor) {
+ return document.getElementById(labelElement.htmlFor);
+ }
+
+ // If no for attribute exists, attempt to retrieve the first labellable descendant element
+ // the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label
+ return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea');
+};
+
+
+/**
+ * On touch end, determine whether to send a click event at once.
+ *
+ * @param {Event} event
+ * @returns {boolean}
+ */
+FastClick.prototype.onTouchEnd = function(event) {
+ 'use strict';
+ var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
+
+ if (!this.trackingClick) {
+ return true;
+ }
+
+ // Prevent phantom clicks on fast double-tap (issue #36)
+ if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
+ this.cancelNextClick = true;
+ return true;
+ }
+
+ // Reset to prevent wrong click cancel on input (issue #156).
+ this.cancelNextClick = false;
+
+ this.lastClickTime = event.timeStamp;
+
+ trackingClickStart = this.trackingClickStart;
+ this.trackingClick = false;
+ this.trackingClickStart = 0;
+
+ // On some iOS devices, the targetElement supplied with the event is invalid if the layer
+ // is performing a transition or scroll, and has to be re-detected manually. Note that
+ // for this to function correctly, it must be called *after* the event target is checked!
+ // See issue #57; also filed as rdar://13048589 .
+ if (deviceIsIOSWithBadTarget) {
+ touch = event.changedTouches[0];
+
+ // In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null
+ targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;
+ targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;
+ }
+
+ targetTagName = targetElement.tagName.toLowerCase();
+ if (targetTagName === 'label') {
+ forElement = this.findControl(targetElement);
+ if (forElement) {
+ this.focus(targetElement);
+ if (deviceIsAndroid) {
+ return false;
+ }
+
+ targetElement = forElement;
+ }
+ } else if (this.needsFocus(targetElement)) {
+
+ // Case 1: If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. Return early and unset the target element reference so that the subsequent click will be allowed through.
+ // Case 2: Without this exception for input elements tapped when the document is contained in an iframe, then any inputted text won't be visible even though the value attribute is updated as the user types (issue #37).
+ if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {
+ this.targetElement = null;
+ return false;
+ }
+
+ this.focus(targetElement);
+ this.sendClick(targetElement, event);
+
+ // Select elements need the event to go through on iOS 4, otherwise the selector menu won't open.
+ // Also this breaks opening selects when VoiceOver is active on iOS6, iOS7 (and possibly others)
+ if (!deviceIsIOS || targetTagName !== 'select') {
+ this.targetElement = null;
+ event.preventDefault();
+ }
+
+ return false;
+ }
+
+ if (deviceIsIOS && !deviceIsIOS4) {
+
+ // Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled
+ // and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42).
+ scrollParent = targetElement.fastClickScrollParent;
+ if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
+ return true;
+ }
+ }
+
+ // Prevent the actual click from going though - unless the target node is marked as requiring
+ // real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted.
+ if (!this.needsClick(targetElement)) {
+ event.preventDefault();
+ this.sendClick(targetElement, event);
+ }
+
+ return false;
+};
+
+
+/**
+ * On touch cancel, stop tracking the click.
+ *
+ * @returns {void}
+ */
+FastClick.prototype.onTouchCancel = function() {
+ 'use strict';
+ this.trackingClick = false;
+ this.targetElement = null;
+};
+
+
+/**
+ * Determine mouse events which should be permitted.
+ *
+ * @param {Event} event
+ * @returns {boolean}
+ */
+FastClick.prototype.onMouse = function(event) {
+ 'use strict';
+
+ // If a target element was never set (because a touch event was never fired) allow the event
+ if (!this.targetElement) {
+ return true;
+ }
+
+ if (event.forwardedTouchEvent) {
+ return true;
+ }
+
+ // Programmatically generated events targeting a specific element should be permitted
+ if (!event.cancelable) {
+ return true;
+ }
+
+ // Derive and check the target element to see whether the mouse event needs to be permitted;
+ // unless explicitly enabled, prevent non-touch click events from triggering actions,
+ // to prevent ghost/doubleclicks.
+ if (!this.needsClick(this.targetElement) || this.cancelNextClick) {
+
+ // Prevent any user-added listeners declared on FastClick element from being fired.
+ if (event.stopImmediatePropagation) {
+ event.stopImmediatePropagation();
+ } else {
+
+ // Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
+ event.propagationStopped = true;
+ }
+
+ // Cancel the event
+ event.stopPropagation();
+ event.preventDefault();
+
+ return false;
+ }
+
+ // If the mouse event is permitted, return true for the action to go through.
+ return true;
+};
+
+
+/**
+ * On actual clicks, determine whether this is a touch-generated click, a click action occurring
+ * naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or
+ * an actual click which should be permitted.
+ *
+ * @param {Event} event
+ * @returns {boolean}
+ */
+FastClick.prototype.onClick = function(event) {
+ 'use strict';
+ var permitted;
+
+ // It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44). In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early.
+ if (this.trackingClick) {
+ this.targetElement = null;
+ this.trackingClick = false;
+ return true;
+ }
+
+ // Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target.
+ if (event.target.type === 'submit' && event.detail === 0) {
+ return true;
+ }
+
+ permitted = this.onMouse(event);
+
+ // Only unset targetElement if the click is not permitted. This will ensure that the check for !targetElement in onMouse fails and the browser's click doesn't go through.
+ if (!permitted) {
+ this.targetElement = null;
+ }
+
+ // If clicks are permitted, return true for the action to go through.
+ return permitted;
+};
+
+
+/**
+ * Remove all FastClick's event listeners.
+ *
+ * @returns {void}
+ */
+FastClick.prototype.destroy = function() {
+ 'use strict';
+ var layer = this.layer;
+
+ if (deviceIsAndroid) {
+ layer.removeEventListener('mouseover', this.onMouse, true);
+ layer.removeEventListener('mousedown', this.onMouse, true);
+ layer.removeEventListener('mouseup', this.onMouse, true);
+ }
+
+ layer.removeEventListener('click', this.onClick, true);
+ layer.removeEventListener('touchstart', this.onTouchStart, false);
+ layer.removeEventListener('touchmove', this.onTouchMove, false);
+ layer.removeEventListener('touchend', this.onTouchEnd, false);
+ layer.removeEventListener('touchcancel', this.onTouchCancel, false);
+};
+
+
+/**
+ * Check whether FastClick is needed.
+ *
+ * @param {Element} layer The layer to listen on
+ */
+FastClick.notNeeded = function(layer) {
+ 'use strict';
+ var metaViewport;
+ var chromeVersion;
+ var blackberryVersion;
+
+ // Devices that don't support touch don't need FastClick
+ if (typeof window.ontouchstart === 'undefined') {
+ return true;
+ }
+
+ // Chrome version - zero for other browsers
+ chromeVersion = +(/Chrome\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];
+
+ if (chromeVersion) {
+
+ if (deviceIsAndroid) {
+ metaViewport = document.querySelector('meta[name=viewport]');
+
+ if (metaViewport) {
+ // Chrome on Android with user-scalable="no" doesn't need FastClick (issue #89)
+ if (metaViewport.content.indexOf('user-scalable=no') !== -1) {
+ return true;
+ }
+ // Chrome 32 and above with width=device-width or less don't need FastClick
+ if (chromeVersion > 31 && document.documentElement.scrollWidth <= window.outerWidth) {
+ return true;
+ }
+ }
+
+ // Chrome desktop doesn't need FastClick (issue #15)
+ } else {
+ return true;
+ }
+ }
+
+ if (deviceIsBlackBerry10) {
+ blackberryVersion = navigator.userAgent.match(/Version\/([0-9]*)\.([0-9]*)/);
+
+ // BlackBerry 10.3+ does not require Fastclick library.
+ // https://github.com/ftlabs/fastclick/issues/251
+ if (blackberryVersion[1] >= 10 && blackberryVersion[2] >= 3) {
+ metaViewport = document.querySelector('meta[name=viewport]');
+
+ if (metaViewport) {
+ // user-scalable=no eliminates click delay.
+ if (metaViewport.content.indexOf('user-scalable=no') !== -1) {
+ return true;
+ }
+ // width=device-width (or less than device-width) eliminates click delay.
+ if (document.documentElement.scrollWidth <= window.outerWidth) {
+ return true;
+ }
+ }
+ }
+ }
+
+ // IE10 with -ms-touch-action: none, which disables double-tap-to-zoom (issue #97)
+ if (layer.style.msTouchAction === 'none') {
+ return true;
+ }
+
+ return false;
+};
+
+
+/**
+ * Factory method for creating a FastClick object
+ *
+ * @param {Element} layer The layer to listen on
+ * @param {Object} options The options to override the defaults
+ */
+FastClick.attach = function(layer, options) {
+ 'use strict';
+ return new FastClick(layer, options);
+};
+
+
+if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) {
+
+ // AMD. Register as an anonymous module.
+ define(function() {
+ 'use strict';
+ return FastClick;
+ });
+} else if (typeof module !== 'undefined' && module.exports) {
+ module.exports = FastClick.attach;
+ module.exports.FastClick = FastClick;
+} else {
+ window.FastClick = FastClick;
+}
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/jquery-2.1.3.min.js b/libs/editor/WordPressEditor/src/main/assets/libs/jquery-2.1.3.min.js
new file mode 100644
index 000000000..25714ed29
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/jquery-2.1.3.min.js
@@ -0,0 +1,4 @@
+/*! jQuery v2.1.3 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */
+!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.3",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)+1>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=hb(),z=hb(),A=hb(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},eb=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fb){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function gb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+rb(o[l]);w=ab.test(a)&&pb(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function hb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ib(a){return a[u]=!0,a}function jb(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function kb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function lb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function nb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function ob(a){return ib(function(b){return b=+b,ib(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pb(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=gb.support={},f=gb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=gb.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",eb,!1):e.attachEvent&&e.attachEvent("onunload",eb)),p=!f(g),c.attributes=jb(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=jb(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=jb(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(jb(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\f]' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),jb(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&jb(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return lb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?lb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},gb.matches=function(a,b){return gb(a,null,null,b)},gb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return gb(b,n,null,[a]).length>0},gb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},gb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},gb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},gb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=gb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=gb.selectors={cacheLength:50,createPseudo:ib,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||gb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&gb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=gb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||gb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ib(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ib(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ib(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ib(function(a){return function(b){return gb(a,b).length>0}}),contains:ib(function(a){return a=a.replace(cb,db),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ib(function(a){return W.test(a||"")||gb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:ob(function(){return[0]}),last:ob(function(a,b){return[b-1]}),eq:ob(function(a,b,c){return[0>c?c+b:c]}),even:ob(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:ob(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:ob(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:ob(function(a,b,c){for(var d=0>c?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=mb(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=nb(b);function qb(){}qb.prototype=d.filters=d.pseudos,d.setFilters=new qb,g=gb.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){(!c||(e=S.exec(h)))&&(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=T.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(R," ")}),h=h.slice(c.length));for(g in d.filter)!(e=X[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?gb.error(a):z(a,i).slice(0)};function rb(a){for(var b=0,c=a.length,d="";c>b;b++)d+=a[b].value;return d}function sb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function tb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ub(a,b,c){for(var d=0,e=b.length;e>d;d++)gb(a,b[d],c);return c}function vb(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wb(a,b,c,d,e,f){return d&&!d[u]&&(d=wb(d)),e&&!e[u]&&(e=wb(e,f)),ib(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ub(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:vb(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=vb(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=vb(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sb(function(a){return a===b},h,!0),l=sb(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sb(tb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wb(i>1&&tb(m),i>1&&rb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xb(a.slice(i,e)),f>e&&xb(a=a.slice(e)),f>e&&rb(a))}m.push(c)}return tb(m)}function yb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=vb(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&gb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ib(f):f}return h=gb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,yb(e,d)),f.selector=a}return f},i=gb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&pb(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&rb(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&pb(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=jb(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),jb(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||kb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&jb(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||kb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),jb(function(a){return null==a.getAttribute("disabled")})||kb(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),gb}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+K.uid++}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){return M.access(a,b,c)
+},removeData:function(a,b){M.remove(a,b)},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?n.queue(this[0],a):void 0===b?this:this.each(function(){var c=n.queue(this,a,b);n._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&n.dequeue(this,a)})},dequeue:function(a){return this.each(function(){n.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=n.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=L.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var Q=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,R=["Top","Right","Bottom","Left"],S=function(a,b){return a=b||a,"none"===n.css(a,"display")||!n.contains(a.ownerDocument,a)},T=/^(?:checkbox|radio)$/i;!function(){var a=l.createDocumentFragment(),b=a.appendChild(l.createElement("div")),c=l.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return null==a.which&&(a.which=null!=b.charCode?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,b){var c,d,e,f=b.button;return null==a.pageX&&null!=b.clientX&&(c=a.target.ownerDocument||l,d=c.documentElement,e=c.body,a.pageX=b.clientX+(d&&d.scrollLeft||e&&e.scrollLeft||0)-(d&&d.clientLeft||e&&e.clientLeft||0),a.pageY=b.clientY+(d&&d.scrollTop||e&&e.scrollTop||0)-(d&&d.clientTop||e&&e.clientTop||0)),a.which||void 0===f||(a.which=1&f?1:2&f?3:4&f?2:0),a}},fix:function(a){if(a[n.expando])return a;var b,c,d,e=a.type,f=a,g=this.fixHooks[e];g||(this.fixHooks[e]=g=W.test(e)?this.mouseHooks:V.test(e)?this.keyHooks:{}),d=g.props?this.props.concat(g.props):this.props,a=new n.Event(f),b=d.length;while(b--)c=d[b],a[c]=f[c];return a.target||(a.target=l),3===a.target.nodeType&&(a.target=a.target.parentNode),g.filter?g.filter(a,f):a},special:{load:{noBubble:!0},focus:{trigger:function(){return this!==_()&&this.focus?(this.focus(),!1):void 0},delegateType:"focusin"},blur:{trigger:function(){return this===_()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return"checkbox"===this.type&&this.click&&n.nodeName(this,"input")?(this.click(),!1):void 0},_default:function(a){return n.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}},simulate:function(a,b,c,d){var e=n.extend(new n.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?n.event.trigger(e,null,b):n.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},n.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)},n.Event=function(a,b){return this instanceof n.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?Z:$):this.type=a,b&&n.extend(this,b),this.timeStamp=a&&a.timeStamp||n.now(),void(this[n.expando]=!0)):new n.Event(a,b)},n.Event.prototype={isDefaultPrevented:$,isPropagationStopped:$,isImmediatePropagationStopped:$,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=Z,a&&a.preventDefault&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=Z,a&&a.stopPropagation&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=Z,a&&a.stopImmediatePropagation&&a.stopImmediatePropagation(),this.stopPropagation()}},n.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){n.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return(!e||e!==d&&!n.contains(d,e))&&(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),k.focusinBubbles||n.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){n.event.simulate(b,a.target,n.event.fix(a),!0)};n.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=L.access(d,b);e||d.addEventListener(a,c,!0),L.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=L.access(d,b)-1;e?L.access(d,b,e):(d.removeEventListener(a,c,!0),L.remove(d,b))}}}),n.fn.extend({on:function(a,b,c,d,e){var f,g;if("object"==typeof a){"string"!=typeof b&&(c=c||b,b=void 0);for(g in a)this.on(g,b,c,a[g],e);return this}if(null==c&&null==d?(d=b,c=b=void 0):null==d&&("string"==typeof b?(d=c,c=void 0):(d=c,c=b,b=void 0)),d===!1)d=$;else if(!d)return this;return 1===e&&(f=d,d=function(a){return n().off(a),f.apply(this,arguments)},d.guid=f.guid||(f.guid=n.guid++)),this.each(function(){n.event.add(this,a,d,c,b)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,n(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return(b===!1||"function"==typeof b)&&(c=b,b=void 0),c===!1&&(c=$),this.each(function(){n.event.remove(this,a,c,b)})},trigger:function(a,b){return this.each(function(){n.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?n.event.trigger(a,b,c,!0):void 0}});var ab=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bb=/<([\w:]+)/,cb=/<|&#?\w+;/,db=/<(?:script|style|link)/i,eb=/checked\s*(?:[^=]|=\s*.checked.)/i,fb=/^$|\/(?:java|ecma)script/i,gb=/^true\/(.*)/,hb=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,ib={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ib.optgroup=ib.option,ib.tbody=ib.tfoot=ib.colgroup=ib.caption=ib.thead,ib.th=ib.td;function jb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function kb(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function lb(a){var b=gb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function mb(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function nb(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function ob(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pb(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=ob(h),f=ob(a),d=0,e=f.length;e>d;d++)pb(f[d],g[d]);if(b)if(c)for(f=f||ob(a),g=g||ob(h),d=0,e=f.length;e>d;d++)nb(f[d],g[d]);else nb(a,h);return g=ob(h,"script"),g.length>0&&mb(g,!i&&ob(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(cb.test(e)){f=f||k.appendChild(b.createElement("div")),g=(bb.exec(e)||["",""])[1].toLowerCase(),h=ib[g]||ib._default,f.innerHTML=h[1]+e.replace(ab,"<$1></$2>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=ob(k.appendChild(e),"script"),i&&mb(f),c)){j=0;while(e=f[j++])fb.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(ob(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&mb(ob(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(ob(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ab,"<$1></$2>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ob(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(ob(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&eb.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(ob(c,"script"),kb),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,ob(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,lb),j=0;g>j;j++)h=f[j],fb.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(hb,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qb,rb={};function sb(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function tb(a){var b=l,c=rb[a];return c||(c=sb(a,b),"none"!==c&&c||(qb=(qb||n("<iframe frameborder='0' width='0' height='0'/>")).appendTo(b.documentElement),b=qb[0].contentDocument,b.write(),b.close(),c=sb(a,b),qb.detach()),rb[a]=c),c}var ub=/^margin/,vb=new RegExp("^("+Q+")(?!px)[a-z%]+$","i"),wb=function(b){return b.ownerDocument.defaultView.opener?b.ownerDocument.defaultView.getComputedStyle(b,null):a.getComputedStyle(b,null)};function xb(a,b,c){var d,e,f,g,h=a.style;return c=c||wb(a),c&&(g=c.getPropertyValue(b)||c[b]),c&&(""!==g||n.contains(a.ownerDocument,a)||(g=n.style(a,b)),vb.test(g)&&ub.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function yb(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}!function(){var b,c,d=l.documentElement,e=l.createElement("div"),f=l.createElement("div");if(f.style){f.style.backgroundClip="content-box",f.cloneNode(!0).style.backgroundClip="",k.clearCloneStyle="content-box"===f.style.backgroundClip,e.style.cssText="border:0;width:0;height:0;top:0;left:-9999px;margin-top:1px;position:absolute",e.appendChild(f);function g(){f.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",f.innerHTML="",d.appendChild(e);var g=a.getComputedStyle(f,null);b="1%"!==g.top,c="4px"===g.width,d.removeChild(e)}a.getComputedStyle&&n.extend(k,{pixelPosition:function(){return g(),b},boxSizingReliable:function(){return null==c&&g(),c},reliableMarginRight:function(){var b,c=f.appendChild(l.createElement("div"));return c.style.cssText=f.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",c.style.marginRight=c.style.width="0",f.style.width="1px",d.appendChild(e),b=!parseFloat(a.getComputedStyle(c,null).marginRight),d.removeChild(e),f.removeChild(c),b}})}}(),n.swap=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};var zb=/^(none|table(?!-c[ea]).+)/,Ab=new RegExp("^("+Q+")(.*)$","i"),Bb=new RegExp("^([+-])=("+Q+")","i"),Cb={position:"absolute",visibility:"hidden",display:"block"},Db={letterSpacing:"0",fontWeight:"400"},Eb=["Webkit","O","Moz","ms"];function Fb(a,b){if(b in a)return b;var c=b[0].toUpperCase()+b.slice(1),d=b,e=Eb.length;while(e--)if(b=Eb[e]+c,b in a)return b;return d}function Gb(a,b,c){var d=Ab.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function Hb(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=n.css(a,c+R[f],!0,e)),d?("content"===c&&(g-=n.css(a,"padding"+R[f],!0,e)),"margin"!==c&&(g-=n.css(a,"border"+R[f]+"Width",!0,e))):(g+=n.css(a,"padding"+R[f],!0,e),"padding"!==c&&(g+=n.css(a,"border"+R[f]+"Width",!0,e)));return g}function Ib(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=wb(a),g="border-box"===n.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=xb(a,b,f),(0>e||null==e)&&(e=a.style[b]),vb.test(e))return e;d=g&&(k.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+Hb(a,b,c||(g?"border":"content"),d,f)+"px"}function Jb(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=L.get(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&S(d)&&(f[g]=L.access(d,"olddisplay",tb(d.nodeName)))):(e=S(d),"none"===c&&e||L.set(d,"olddisplay",e?c:n.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}n.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=xb(a,"opacity");return""===c?"1":c}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=n.camelCase(b),i=a.style;return b=n.cssProps[h]||(n.cssProps[h]=Fb(i,h)),g=n.cssHooks[b]||n.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b]:(f=typeof c,"string"===f&&(e=Bb.exec(c))&&(c=(e[1]+1)*e[2]+parseFloat(n.css(a,b)),f="number"),null!=c&&c===c&&("number"!==f||n.cssNumber[h]||(c+="px"),k.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=n.camelCase(b);return b=n.cssProps[h]||(n.cssProps[h]=Fb(a.style,h)),g=n.cssHooks[b]||n.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=xb(a,b,d)),"normal"===e&&b in Db&&(e=Db[b]),""===c||c?(f=parseFloat(e),c===!0||n.isNumeric(f)?f||0:e):e}}),n.each(["height","width"],function(a,b){n.cssHooks[b]={get:function(a,c,d){return c?zb.test(n.css(a,"display"))&&0===a.offsetWidth?n.swap(a,Cb,function(){return Ib(a,b,d)}):Ib(a,b,d):void 0},set:function(a,c,d){var e=d&&wb(a);return Gb(a,c,d?Hb(a,b,d,"border-box"===n.css(a,"boxSizing",!1,e),e):0)}}}),n.cssHooks.marginRight=yb(k.reliableMarginRight,function(a,b){return b?n.swap(a,{display:"inline-block"},xb,[a,"marginRight"]):void 0}),n.each({margin:"",padding:"",border:"Width"},function(a,b){n.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+R[d]+b]=f[d]||f[d-2]||f[0];return e}},ub.test(a)||(n.cssHooks[a+b].set=Gb)}),n.fn.extend({css:function(a,b){return J(this,function(a,b,c){var d,e,f={},g=0;if(n.isArray(b)){for(d=wb(a),e=b.length;e>g;g++)f[b[g]]=n.css(a,b[g],!1,d);return f}return void 0!==c?n.style(a,b,c):n.css(a,b)},a,b,arguments.length>1)},show:function(){return Jb(this,!0)},hide:function(){return Jb(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){S(this)?n(this).show():n(this).hide()})}});function Kb(a,b,c,d,e){return new Kb.prototype.init(a,b,c,d,e)}n.Tween=Kb,Kb.prototype={constructor:Kb,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(n.cssNumber[c]?"":"px")},cur:function(){var a=Kb.propHooks[this.prop];return a&&a.get?a.get(this):Kb.propHooks._default.get(this)},run:function(a){var b,c=Kb.propHooks[this.prop];return this.pos=b=this.options.duration?n.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Kb.propHooks._default.set(this),this}},Kb.prototype.init.prototype=Kb.prototype,Kb.propHooks={_default:{get:function(a){var b;return null==a.elem[a.prop]||a.elem.style&&null!=a.elem.style[a.prop]?(b=n.css(a.elem,a.prop,""),b&&"auto"!==b?b:0):a.elem[a.prop]},set:function(a){n.fx.step[a.prop]?n.fx.step[a.prop](a):a.elem.style&&(null!=a.elem.style[n.cssProps[a.prop]]||n.cssHooks[a.prop])?n.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},Kb.propHooks.scrollTop=Kb.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},n.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},n.fx=Kb.prototype.init,n.fx.step={};var Lb,Mb,Nb=/^(?:toggle|show|hide)$/,Ob=new RegExp("^(?:([+-])=|)("+Q+")([a-z%]*)$","i"),Pb=/queueHooks$/,Qb=[Vb],Rb={"*":[function(a,b){var c=this.createTween(a,b),d=c.cur(),e=Ob.exec(b),f=e&&e[3]||(n.cssNumber[a]?"":"px"),g=(n.cssNumber[a]||"px"!==f&&+d)&&Ob.exec(n.css(c.elem,a)),h=1,i=20;if(g&&g[3]!==f){f=f||g[3],e=e||[],g=+d||1;do h=h||".5",g/=h,n.style(c.elem,a,g+f);while(h!==(h=c.cur()/d)&&1!==h&&--i)}return e&&(g=c.start=+g||+d||0,c.unit=f,c.end=e[1]?g+(e[1]+1)*e[2]:+e[2]),c}]};function Sb(){return setTimeout(function(){Lb=void 0}),Lb=n.now()}function Tb(a,b){var c,d=0,e={height:a};for(b=b?1:0;4>d;d+=2-b)c=R[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function Ub(a,b,c){for(var d,e=(Rb[b]||[]).concat(Rb["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function Vb(a,b,c){var d,e,f,g,h,i,j,k,l=this,m={},o=a.style,p=a.nodeType&&S(a),q=L.get(a,"fxshow");c.queue||(h=n._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,l.always(function(){l.always(function(){h.unqueued--,n.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=n.css(a,"display"),k="none"===j?L.get(a,"olddisplay")||tb(a.nodeName):j,"inline"===k&&"none"===n.css(a,"float")&&(o.display="inline-block")),c.overflow&&(o.overflow="hidden",l.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],Nb.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}m[d]=q&&q[d]||n.style(a,d)}else j=void 0;if(n.isEmptyObject(m))"inline"===("none"===j?tb(a.nodeName):j)&&(o.display=j);else{q?"hidden"in q&&(p=q.hidden):q=L.access(a,"fxshow",{}),f&&(q.hidden=!p),p?n(a).show():l.done(function(){n(a).hide()}),l.done(function(){var b;L.remove(a,"fxshow");for(b in m)n.style(a,b,m[b])});for(d in m)g=Ub(p?q[d]:0,d,l),d in q||(q[d]=g.start,p&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function Wb(a,b){var c,d,e,f,g;for(c in a)if(d=n.camelCase(c),e=b[d],f=a[c],n.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=n.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function Xb(a,b,c){var d,e,f=0,g=Qb.length,h=n.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=Lb||Sb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:n.extend({},b),opts:n.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:Lb||Sb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=n.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;for(Wb(k,j.opts.specialEasing);g>f;f++)if(d=Qb[f].call(j,a,k,j.opts))return d;return n.map(k,Ub,j),n.isFunction(j.opts.start)&&j.opts.start.call(a,j),n.fx.timer(n.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}n.Animation=n.extend(Xb,{tweener:function(a,b){n.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");for(var c,d=0,e=a.length;e>d;d++)c=a[d],Rb[c]=Rb[c]||[],Rb[c].unshift(b)},prefilter:function(a,b){b?Qb.unshift(a):Qb.push(a)}}),n.speed=function(a,b,c){var d=a&&"object"==typeof a?n.extend({},a):{complete:c||!c&&b||n.isFunction(a)&&a,duration:a,easing:c&&b||b&&!n.isFunction(b)&&b};return d.duration=n.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in n.fx.speeds?n.fx.speeds[d.duration]:n.fx.speeds._default,(null==d.queue||d.queue===!0)&&(d.queue="fx"),d.old=d.complete,d.complete=function(){n.isFunction(d.old)&&d.old.call(this),d.queue&&n.dequeue(this,d.queue)},d},n.fn.extend({fadeTo:function(a,b,c,d){return this.filter(S).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=n.isEmptyObject(a),f=n.speed(b,c,d),g=function(){var b=Xb(this,n.extend({},a),f);(e||L.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=n.timers,g=L.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&Pb.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));(b||!c)&&n.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=L.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=n.timers,g=d?d.length:0;for(c.finish=!0,n.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),n.each(["toggle","show","hide"],function(a,b){var c=n.fn[b];n.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(Tb(b,!0),a,d,e)}}),n.each({slideDown:Tb("show"),slideUp:Tb("hide"),slideToggle:Tb("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){n.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),n.timers=[],n.fx.tick=function(){var a,b=0,c=n.timers;for(Lb=n.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||n.fx.stop(),Lb=void 0},n.fx.timer=function(a){n.timers.push(a),a()?n.fx.start():n.timers.pop()},n.fx.interval=13,n.fx.start=function(){Mb||(Mb=setInterval(n.fx.tick,n.fx.interval))},n.fx.stop=function(){clearInterval(Mb),Mb=null},n.fx.speeds={slow:600,fast:200,_default:400},n.fn.delay=function(a,b){return a=n.fx?n.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},function(){var a=l.createElement("input"),b=l.createElement("select"),c=b.appendChild(l.createElement("option"));a.type="checkbox",k.checkOn=""!==a.value,k.optSelected=c.selected,b.disabled=!0,k.optDisabled=!c.disabled,a=l.createElement("input"),a.value="t",a.type="radio",k.radioValue="t"===a.value}();var Yb,Zb,$b=n.expr.attrHandle;n.fn.extend({attr:function(a,b){return J(this,n.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){n.removeAttr(this,a)})}}),n.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(a&&3!==f&&8!==f&&2!==f)return typeof a.getAttribute===U?n.prop(a,b,c):(1===f&&n.isXMLDoc(a)||(b=b.toLowerCase(),d=n.attrHooks[b]||(n.expr.match.bool.test(b)?Zb:Yb)),void 0===c?d&&"get"in d&&null!==(e=d.get(a,b))?e:(e=n.find.attr(a,b),null==e?void 0:e):null!==c?d&&"set"in d&&void 0!==(e=d.set(a,c,b))?e:(a.setAttribute(b,c+""),c):void n.removeAttr(a,b))
+},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(E);if(f&&1===a.nodeType)while(c=f[e++])d=n.propFix[c]||c,n.expr.match.bool.test(c)&&(a[d]=!1),a.removeAttribute(c)},attrHooks:{type:{set:function(a,b){if(!k.radioValue&&"radio"===b&&n.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),Zb={set:function(a,b,c){return b===!1?n.removeAttr(a,c):a.setAttribute(c,c),c}},n.each(n.expr.match.bool.source.match(/\w+/g),function(a,b){var c=$b[b]||n.find.attr;$b[b]=function(a,b,d){var e,f;return d||(f=$b[b],$b[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,$b[b]=f),e}});var _b=/^(?:input|select|textarea|button)$/i;n.fn.extend({prop:function(a,b){return J(this,n.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[n.propFix[a]||a]})}}),n.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(a,b,c){var d,e,f,g=a.nodeType;if(a&&3!==g&&8!==g&&2!==g)return f=1!==g||!n.isXMLDoc(a),f&&(b=n.propFix[b]||b,e=n.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){return a.hasAttribute("tabindex")||_b.test(a.nodeName)||a.href?a.tabIndex:-1}}}}),k.optSelected||(n.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null}}),n.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){n.propFix[this.toLowerCase()]=this});var ac=/[\t\r\n\f]/g;n.fn.extend({addClass:function(a){var b,c,d,e,f,g,h="string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).addClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):" ")){f=0;while(e=b[f++])d.indexOf(" "+e+" ")<0&&(d+=e+" ");g=n.trim(d),c.className!==g&&(c.className=g)}return this},removeClass:function(a){var b,c,d,e,f,g,h=0===arguments.length||"string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).removeClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):"")){f=0;while(e=b[f++])while(d.indexOf(" "+e+" ")>=0)d=d.replace(" "+e+" "," ");g=a?n.trim(d):"",c.className!==g&&(c.className=g)}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):this.each(n.isFunction(a)?function(c){n(this).toggleClass(a.call(this,c,this.className,b),b)}:function(){if("string"===c){var b,d=0,e=n(this),f=a.match(E)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else(c===U||"boolean"===c)&&(this.className&&L.set(this,"__className__",this.className),this.className=this.className||a===!1?"":L.get(this,"__className__")||"")})},hasClass:function(a){for(var b=" "+a+" ",c=0,d=this.length;d>c;c++)if(1===this[c].nodeType&&(" "+this[c].className+" ").replace(ac," ").indexOf(b)>=0)return!0;return!1}});var bc=/\r/g;n.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=n.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,n(this).val()):a,null==e?e="":"number"==typeof e?e+="":n.isArray(e)&&(e=n.map(e,function(a){return null==a?"":a+""})),b=n.valHooks[this.type]||n.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=n.valHooks[e.type]||n.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(bc,""):null==c?"":c)}}}),n.extend({valHooks:{option:{get:function(a){var b=n.find.attr(a,"value");return null!=b?b:n.trim(n.text(a))}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],!(!c.selected&&i!==e||(k.optDisabled?c.disabled:null!==c.getAttribute("disabled"))||c.parentNode.disabled&&n.nodeName(c.parentNode,"optgroup"))){if(b=n(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=n.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=n.inArray(d.value,f)>=0)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),n.each(["radio","checkbox"],function(){n.valHooks[this]={set:function(a,b){return n.isArray(b)?a.checked=n.inArray(n(a).val(),b)>=0:void 0}},k.checkOn||(n.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})}),n.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){n.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),n.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}});var cc=n.now(),dc=/\?/;n.parseJSON=function(a){return JSON.parse(a+"")},n.parseXML=function(a){var b,c;if(!a||"string"!=typeof a)return null;try{c=new DOMParser,b=c.parseFromString(a,"text/xml")}catch(d){b=void 0}return(!b||b.getElementsByTagName("parsererror").length)&&n.error("Invalid XML: "+a),b};var ec=/#.*$/,fc=/([?&])_=[^&]*/,gc=/^(.*?):[ \t]*([^\r\n]*)$/gm,hc=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,ic=/^(?:GET|HEAD)$/,jc=/^\/\//,kc=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,lc={},mc={},nc="*/".concat("*"),oc=a.location.href,pc=kc.exec(oc.toLowerCase())||[];function qc(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(E)||[];if(n.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function rc(a,b,c,d){var e={},f=a===mc;function g(h){var i;return e[h]=!0,n.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function sc(a,b){var c,d,e=n.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&n.extend(!0,a,d),a}function tc(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function uc(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}n.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:oc,type:"GET",isLocal:hc.test(pc[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":nc,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":n.parseJSON,"text xml":n.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?sc(sc(a,n.ajaxSettings),b):sc(n.ajaxSettings,a)},ajaxPrefilter:qc(lc),ajaxTransport:qc(mc),ajax:function(a,b){"object"==typeof a&&(b=a,a=void 0),b=b||{};var c,d,e,f,g,h,i,j,k=n.ajaxSetup({},b),l=k.context||k,m=k.context&&(l.nodeType||l.jquery)?n(l):n.event,o=n.Deferred(),p=n.Callbacks("once memory"),q=k.statusCode||{},r={},s={},t=0,u="canceled",v={readyState:0,getResponseHeader:function(a){var b;if(2===t){if(!f){f={};while(b=gc.exec(e))f[b[1].toLowerCase()]=b[2]}b=f[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===t?e:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return t||(a=s[c]=s[c]||a,r[a]=b),this},overrideMimeType:function(a){return t||(k.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>t)for(b in a)q[b]=[q[b],a[b]];else v.always(a[v.status]);return this},abort:function(a){var b=a||u;return c&&c.abort(b),x(0,b),this}};if(o.promise(v).complete=p.add,v.success=v.done,v.error=v.fail,k.url=((a||k.url||oc)+"").replace(ec,"").replace(jc,pc[1]+"//"),k.type=b.method||b.type||k.method||k.type,k.dataTypes=n.trim(k.dataType||"*").toLowerCase().match(E)||[""],null==k.crossDomain&&(h=kc.exec(k.url.toLowerCase()),k.crossDomain=!(!h||h[1]===pc[1]&&h[2]===pc[2]&&(h[3]||("http:"===h[1]?"80":"443"))===(pc[3]||("http:"===pc[1]?"80":"443")))),k.data&&k.processData&&"string"!=typeof k.data&&(k.data=n.param(k.data,k.traditional)),rc(lc,k,b,v),2===t)return v;i=n.event&&k.global,i&&0===n.active++&&n.event.trigger("ajaxStart"),k.type=k.type.toUpperCase(),k.hasContent=!ic.test(k.type),d=k.url,k.hasContent||(k.data&&(d=k.url+=(dc.test(d)?"&":"?")+k.data,delete k.data),k.cache===!1&&(k.url=fc.test(d)?d.replace(fc,"$1_="+cc++):d+(dc.test(d)?"&":"?")+"_="+cc++)),k.ifModified&&(n.lastModified[d]&&v.setRequestHeader("If-Modified-Since",n.lastModified[d]),n.etag[d]&&v.setRequestHeader("If-None-Match",n.etag[d])),(k.data&&k.hasContent&&k.contentType!==!1||b.contentType)&&v.setRequestHeader("Content-Type",k.contentType),v.setRequestHeader("Accept",k.dataTypes[0]&&k.accepts[k.dataTypes[0]]?k.accepts[k.dataTypes[0]]+("*"!==k.dataTypes[0]?", "+nc+"; q=0.01":""):k.accepts["*"]);for(j in k.headers)v.setRequestHeader(j,k.headers[j]);if(k.beforeSend&&(k.beforeSend.call(l,v,k)===!1||2===t))return v.abort();u="abort";for(j in{success:1,error:1,complete:1})v[j](k[j]);if(c=rc(mc,k,b,v)){v.readyState=1,i&&m.trigger("ajaxSend",[v,k]),k.async&&k.timeout>0&&(g=setTimeout(function(){v.abort("timeout")},k.timeout));try{t=1,c.send(r,x)}catch(w){if(!(2>t))throw w;x(-1,w)}}else x(-1,"No Transport");function x(a,b,f,h){var j,r,s,u,w,x=b;2!==t&&(t=2,g&&clearTimeout(g),c=void 0,e=h||"",v.readyState=a>0?4:0,j=a>=200&&300>a||304===a,f&&(u=tc(k,v,f)),u=uc(k,u,v,j),j?(k.ifModified&&(w=v.getResponseHeader("Last-Modified"),w&&(n.lastModified[d]=w),w=v.getResponseHeader("etag"),w&&(n.etag[d]=w)),204===a||"HEAD"===k.type?x="nocontent":304===a?x="notmodified":(x=u.state,r=u.data,s=u.error,j=!s)):(s=x,(a||!x)&&(x="error",0>a&&(a=0))),v.status=a,v.statusText=(b||x)+"",j?o.resolveWith(l,[r,x,v]):o.rejectWith(l,[v,x,s]),v.statusCode(q),q=void 0,i&&m.trigger(j?"ajaxSuccess":"ajaxError",[v,k,j?r:s]),p.fireWith(l,[v,x]),i&&(m.trigger("ajaxComplete",[v,k]),--n.active||n.event.trigger("ajaxStop")))}return v},getJSON:function(a,b,c){return n.get(a,b,c,"json")},getScript:function(a,b){return n.get(a,void 0,b,"script")}}),n.each(["get","post"],function(a,b){n[b]=function(a,c,d,e){return n.isFunction(c)&&(e=e||d,d=c,c=void 0),n.ajax({url:a,type:b,dataType:e,data:c,success:d})}}),n._evalUrl=function(a){return n.ajax({url:a,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},n.fn.extend({wrapAll:function(a){var b;return n.isFunction(a)?this.each(function(b){n(this).wrapAll(a.call(this,b))}):(this[0]&&(b=n(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this)},wrapInner:function(a){return this.each(n.isFunction(a)?function(b){n(this).wrapInner(a.call(this,b))}:function(){var b=n(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=n.isFunction(a);return this.each(function(c){n(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){n.nodeName(this,"body")||n(this).replaceWith(this.childNodes)}).end()}}),n.expr.filters.hidden=function(a){return a.offsetWidth<=0&&a.offsetHeight<=0},n.expr.filters.visible=function(a){return!n.expr.filters.hidden(a)};var vc=/%20/g,wc=/\[\]$/,xc=/\r?\n/g,yc=/^(?:submit|button|image|reset|file)$/i,zc=/^(?:input|select|textarea|keygen)/i;function Ac(a,b,c,d){var e;if(n.isArray(b))n.each(b,function(b,e){c||wc.test(a)?d(a,e):Ac(a+"["+("object"==typeof e?b:"")+"]",e,c,d)});else if(c||"object"!==n.type(b))d(a,b);else for(e in b)Ac(a+"["+e+"]",b[e],c,d)}n.param=function(a,b){var c,d=[],e=function(a,b){b=n.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=n.ajaxSettings&&n.ajaxSettings.traditional),n.isArray(a)||a.jquery&&!n.isPlainObject(a))n.each(a,function(){e(this.name,this.value)});else for(c in a)Ac(c,a[c],b,e);return d.join("&").replace(vc,"+")},n.fn.extend({serialize:function(){return n.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=n.prop(this,"elements");return a?n.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!n(this).is(":disabled")&&zc.test(this.nodeName)&&!yc.test(a)&&(this.checked||!T.test(a))}).map(function(a,b){var c=n(this).val();return null==c?null:n.isArray(c)?n.map(c,function(a){return{name:b.name,value:a.replace(xc,"\r\n")}}):{name:b.name,value:c.replace(xc,"\r\n")}}).get()}}),n.ajaxSettings.xhr=function(){try{return new XMLHttpRequest}catch(a){}};var Bc=0,Cc={},Dc={0:200,1223:204},Ec=n.ajaxSettings.xhr();a.attachEvent&&a.attachEvent("onunload",function(){for(var a in Cc)Cc[a]()}),k.cors=!!Ec&&"withCredentials"in Ec,k.ajax=Ec=!!Ec,n.ajaxTransport(function(a){var b;return k.cors||Ec&&!a.crossDomain?{send:function(c,d){var e,f=a.xhr(),g=++Bc;if(f.open(a.type,a.url,a.async,a.username,a.password),a.xhrFields)for(e in a.xhrFields)f[e]=a.xhrFields[e];a.mimeType&&f.overrideMimeType&&f.overrideMimeType(a.mimeType),a.crossDomain||c["X-Requested-With"]||(c["X-Requested-With"]="XMLHttpRequest");for(e in c)f.setRequestHeader(e,c[e]);b=function(a){return function(){b&&(delete Cc[g],b=f.onload=f.onerror=null,"abort"===a?f.abort():"error"===a?d(f.status,f.statusText):d(Dc[f.status]||f.status,f.statusText,"string"==typeof f.responseText?{text:f.responseText}:void 0,f.getAllResponseHeaders()))}},f.onload=b(),f.onerror=b("error"),b=Cc[g]=b("abort");try{f.send(a.hasContent&&a.data||null)}catch(h){if(b)throw h}},abort:function(){b&&b()}}:void 0}),n.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(a){return n.globalEval(a),a}}}),n.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),n.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(d,e){b=n("<script>").prop({async:!0,charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&e("error"===a.type?404:200,a.type)}),l.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Fc=[],Gc=/(=)\?(?=&|$)|\?\?/;n.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Fc.pop()||n.expando+"_"+cc++;return this[a]=!0,a}}),n.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Gc.test(b.url)?"url":"string"==typeof b.data&&!(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&Gc.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=n.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Gc,"$1"+e):b.jsonp!==!1&&(b.url+=(dc.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||n.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Fc.push(e)),g&&n.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),n.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||l;var d=v.exec(a),e=!c&&[];return d?[b.createElement(d[1])]:(d=n.buildFragment([a],b,e),e&&e.length&&n(e).remove(),n.merge([],d.childNodes))};var Hc=n.fn.load;n.fn.load=function(a,b,c){if("string"!=typeof a&&Hc)return Hc.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>=0&&(d=n.trim(a.slice(h)),a=a.slice(0,h)),n.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&n.ajax({url:a,type:e,dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?n("<div>").append(n.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,f||[a.responseText,b,a])}),this},n.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){n.fn[b]=function(a){return this.on(b,a)}}),n.expr.filters.animated=function(a){return n.grep(n.timers,function(b){return a===b.elem}).length};var Ic=a.document.documentElement;function Jc(a){return n.isWindow(a)?a:9===a.nodeType&&a.defaultView}n.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=n.css(a,"position"),l=n(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=n.css(a,"top"),i=n.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),n.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},n.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){n.offset.setOffset(this,a,b)});var b,c,d=this[0],e={top:0,left:0},f=d&&d.ownerDocument;if(f)return b=f.documentElement,n.contains(b,d)?(typeof d.getBoundingClientRect!==U&&(e=d.getBoundingClientRect()),c=Jc(f),{top:e.top+c.pageYOffset-b.clientTop,left:e.left+c.pageXOffset-b.clientLeft}):e},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===n.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),n.nodeName(a[0],"html")||(d=a.offset()),d.top+=n.css(a[0],"borderTopWidth",!0),d.left+=n.css(a[0],"borderLeftWidth",!0)),{top:b.top-d.top-n.css(c,"marginTop",!0),left:b.left-d.left-n.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||Ic;while(a&&!n.nodeName(a,"html")&&"static"===n.css(a,"position"))a=a.offsetParent;return a||Ic})}}),n.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(b,c){var d="pageYOffset"===c;n.fn[b]=function(e){return J(this,function(b,e,f){var g=Jc(b);return void 0===f?g?g[c]:b[e]:void(g?g.scrollTo(d?a.pageXOffset:f,d?f:a.pageYOffset):b[e]=f)},b,e,arguments.length,null)}}),n.each(["top","left"],function(a,b){n.cssHooks[b]=yb(k.pixelPosition,function(a,c){return c?(c=xb(a,b),vb.test(c)?n(a).position()[b]+"px":c):void 0})}),n.each({Height:"height",Width:"width"},function(a,b){n.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){n.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return J(this,function(b,c,d){var e;return n.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?n.css(b,c,g):n.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),n.fn.size=function(){return this.length},n.fn.andSelf=n.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return n});var Kc=a.jQuery,Lc=a.$;return n.noConflict=function(b){return a.$===n&&(a.$=Lc),b&&a.jQuery===n&&(a.jQuery=Kc),n},typeof b===U&&(a.jQuery=a.$=n),n});
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/jquery.mobile-events.min.js b/libs/editor/WordPressEditor/src/main/assets/libs/jquery.mobile-events.min.js
new file mode 100755
index 000000000..b8479c6f6
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/jquery.mobile-events.min.js
@@ -0,0 +1 @@
+(function(e){function d(){var e=o();if(e!==u){u=e;i.trigger("orientationchange")}}function E(t,n,r,i){var s=r.type;r.type=n;e.event.dispatch.call(t,r,i);r.type=s}e.attrFn=e.attrFn||{};var t=navigator.userAgent.toLowerCase(),n=t.indexOf("chrome")>-1&&(t.indexOf("windows")>-1||t.indexOf("macintosh")>-1||t.indexOf("linux")>-1)&&t.indexOf("mobile")<0&&t.indexOf("nexus")<0,r={tap_pixel_range:5,swipe_h_threshold:50,swipe_v_threshold:50,taphold_threshold:750,doubletap_int:500,touch_capable:"ontouchstart"in document.documentElement&&!n,orientation_support:"orientation"in window&&"onorientationchange"in window,startevent:"ontouchstart"in document.documentElement&&!n?"touchstart":"mousedown",endevent:"ontouchstart"in document.documentElement&&!n?"touchend":"mouseup",moveevent:"ontouchstart"in document.documentElement&&!n?"touchmove":"mousemove",tapevent:"ontouchstart"in document.documentElement&&!n?"tap":"click",scrollevent:"ontouchstart"in document.documentElement&&!n?"touchmove":"scroll",hold_timer:null,tap_timer:null};e.isTouchCapable=function(){return r.touch_capable};e.getStartEvent=function(){return r.startevent};e.getEndEvent=function(){return r.endevent};e.getMoveEvent=function(){return r.moveevent};e.getTapEvent=function(){return r.tapevent};e.getScrollEvent=function(){return r.scrollevent};e.each(["tapstart","tapend","tap","singletap","doubletap","taphold","swipe","swipeup","swiperight","swipedown","swipeleft","swipeend","scrollstart","scrollend","orientationchange"],function(t,n){e.fn[n]=function(e){return e?this.on(n,e):this.trigger(n)};e.attrFn[n]=true});e.event.special.tapstart={setup:function(){var t=this,n=e(t);n.on(r.startevent,function(e){n.data("callee",arguments.callee);if(e.which&&e.which!==1){return false}var i=e.originalEvent,s={position:{x:r.touch_capable?i.touches[0].screenX:e.screenX,y:r.touch_capable?i.touches[0].screenY:e.screenY},offset:{x:r.touch_capable?i.touches[0].pageX-i.touches[0].target.offsetLeft:e.offsetX,y:r.touch_capable?i.touches[0].pageY-i.touches[0].target.offsetTop:e.offsetY},time:(new Date).getTime(),target:e.target};E(t,"tapstart",e,s);return true})},remove:function(){e(this).off(r.startevent,e(this).data.callee)}};e.event.special.tapmove={setup:function(){var t=this,n=e(t);n.on(r.moveevent,function(e){n.data("callee",arguments.callee);var i=e.originalEvent,s={position:{x:r.touch_capable?i.touches[0].screenX:e.screenX,y:r.touch_capable?i.touches[0].screenY:e.screenY},offset:{x:r.touch_capable?i.touches[0].pageX-i.touches[0].target.offsetLeft:e.offsetX,y:r.touch_capable?i.touches[0].pageY-i.touches[0].target.offsetTop:e.offsetY},time:(new Date).getTime(),target:e.target};E(t,"tapmove",e,s);return true})},remove:function(){e(this).off(r.moveevent,e(this).data.callee)}};e.event.special.tapend={setup:function(){var t=this,n=e(t);n.on(r.endevent,function(e){n.data("callee",arguments.callee);var i=e.originalEvent;var s={position:{x:r.touch_capable?i.changedTouches[0].screenX:e.screenX,y:r.touch_capable?i.changedTouches[0].screenY:e.screenY},offset:{x:r.touch_capable?i.changedTouches[0].pageX-i.changedTouches[0].target.offsetLeft:e.offsetX,y:r.touch_capable?i.changedTouches[0].pageY-i.changedTouches[0].target.offsetTop:e.offsetY},time:(new Date).getTime(),target:e.target};E(t,"tapend",e,s);return true})},remove:function(){e(this).off(r.endevent,e(this).data.callee)}};e.event.special.taphold={setup:function(){var t=this,n=e(t),i,s,o={x:0,y:0};n.on(r.startevent,function(e){if(e.which&&e.which!==1){return false}else{n.data("tapheld",false);i=e.target;var s=e.originalEvent;var u=(new Date).getTime(),a={x:r.touch_capable?s.touches[0].screenX:e.screenX,y:r.touch_capable?s.touches[0].screenY:e.screenY},f={x:r.touch_capable?s.touches[0].pageX-s.touches[0].target.offsetLeft:e.offsetX,y:r.touch_capable?s.touches[0].pageY-s.touches[0].target.offsetTop:e.offsetY};o.x=e.originalEvent.targetTouches?e.originalEvent.targetTouches[0].pageX:e.pageX;o.y=e.originalEvent.targetTouches?e.originalEvent.targetTouches[0].pageY:e.pageY;r.hold_timer=window.setTimeout(function(){var l=e.originalEvent.targetTouches?e.originalEvent.targetTouches[0].pageX:e.pageX,c=e.originalEvent.targetTouches?e.originalEvent.targetTouches[0].pageY:e.pageY;if(e.target==i&&o.x==l&&o.y==c){n.data("tapheld",true);var h=(new Date).getTime(),p={x:r.touch_capable?s.touches[0].screenX:e.screenX,y:r.touch_capable?s.touches[0].screenY:e.screenY},d={x:r.touch_capable?s.touches[0].pageX-s.touches[0].target.offsetLeft:e.offsetX,y:r.touch_capable?s.touches[0].pageY-s.touches[0].target.offsetTop:e.offsetY};duration=h-u;var v={startTime:u,endTime:h,startPosition:a,startOffset:f,endPosition:p,endOffset:d,duration:duration,target:e.target};n.data("callee1",arguments.callee);E(t,"taphold",e,v)}},r.taphold_threshold);return true}}).on(r.endevent,function(){n.data("callee2",arguments.callee);n.data("tapheld",false);window.clearTimeout(r.hold_timer)})},remove:function(){e(this).off(r.startevent,e(this).data.callee1).off(r.endevent,e(this).data.callee2)}};e.event.special.doubletap={setup:function(){var t=this,n=e(t),i,s,o,u;n.on(r.startevent,function(e){if(e.which&&e.which!==1){return false}else if(!n.data("lastTouch")){n.data("doubletapped",false);i=e.target;n.data("callee1",arguments.callee);u=e.originalEvent;o={position:{x:r.touch_capable?u.touches[0].screenX:e.screenX,y:r.touch_capable?u.touches[0].screenY:e.screenY},offset:{x:r.touch_capable?u.touches[0].pageX-u.touches[0].target.offsetLeft:e.offsetX,y:r.touch_capable?u.touches[0].pageY-u.touches[0].target.offsetTop:e.offsetY},time:(new Date).getTime(),target:e.target};return true}}).on(r.endevent,function(e){var u=(new Date).getTime();var a=n.data("lastTouch")||u+1;var f=u-a;window.clearTimeout(s);n.data("callee2",arguments.callee);if(f<r.doubletap_int&&f>0&&e.target==i&&f>100){n.data("doubletapped",true);window.clearTimeout(r.tap_timer);var l={position:{x:r.touch_capable?e.originalEvent.changedTouches[0].screenX:e.screenX,y:r.touch_capable?e.originalEvent.changedTouches[0].screenY:e.screenY},offset:{x:r.touch_capable?e.originalEvent.changedTouches[0].pageX-e.originalEvent.changedTouches[0].target.offsetLeft:e.offsetX,y:r.touch_capable?e.originalEvent.changedTouches[0].pageY-e.originalEvent.changedTouches[0].target.offsetTop:e.offsetY},time:(new Date).getTime(),target:e.target};var c={firstTap:o,secondTap:l,interval:l.time-o.time};E(t,"doubletap",e,c)}else{n.data("lastTouch",u);s=window.setTimeout(function(e){window.clearTimeout(s)},r.doubletap_int,[e])}n.data("lastTouch",u)})},remove:function(){e(this).off(r.startevent,e(this).data.callee1).off(r.endevent,e(this).data.callee2)}};e.event.special.singletap={setup:function(){var t=this,n=e(t),i=null,s=null,o={x:0,y:0};n.on(r.startevent,function(e){if(e.which&&e.which!==1){return false}else{s=(new Date).getTime();i=e.target;n.data("callee1",arguments.callee);o.x=e.originalEvent.targetTouches?e.originalEvent.targetTouches[0].pageX:e.pageX;o.y=e.originalEvent.targetTouches?e.originalEvent.targetTouches[0].pageY:e.pageY;return true}}).on(r.endevent,function(e){n.data("callee2",arguments.callee);if(e.target==i){end_pos_x=e.originalEvent.changedTouches?e.originalEvent.changedTouches[0].pageX:e.pageX;end_pos_y=e.originalEvent.changedTouches?e.originalEvent.changedTouches[0].pageY:e.pageY;r.tap_timer=window.setTimeout(function(){if(!n.data("doubletapped")&&!n.data("tapheld")&&o.x==end_pos_x&&o.y==end_pos_y){var i=e.originalEvent;var u={position:{x:r.touch_capable?i.changedTouches[0].screenX:e.screenX,y:r.touch_capable?i.changedTouches[0].screenY:e.screenY},offset:{x:r.touch_capable?i.changedTouches[0].pageX-i.changedTouches[0].target.offsetLeft:e.offsetX,y:r.touch_capable?i.changedTouches[0].pageY-i.changedTouches[0].target.offsetTop:e.offsetY},time:(new Date).getTime(),target:e.target};if(u.time-s<r.taphold_threshold){E(t,"singletap",e,u)}}},r.doubletap_int)}})},remove:function(){e(this).off(r.startevent,e(this).data.callee1).off(r.endevent,e(this).data.callee2)}};e.event.special.tap={setup:function(){var t=this,n=e(t),i=false,s=null,o,u={x:0,y:0};n.on(r.startevent,function(e){n.data("callee1",arguments.callee);if(e.which&&e.which!==1){return false}else{i=true;u.x=e.originalEvent.targetTouches?e.originalEvent.targetTouches[0].pageX:e.pageX;u.y=e.originalEvent.targetTouches?e.originalEvent.targetTouches[0].pageY:e.pageY;o=(new Date).getTime();s=e.target;return true}}).on(r.endevent,function(e){n.data("callee2",arguments.callee);var a=e.originalEvent.targetTouches?e.originalEvent.changedTouches[0].pageX:e.pageX,f=e.originalEvent.targetTouches?e.originalEvent.changedTouches[0].pageY:e.pageY;diff_x=u.x-a,diff_y=u.y-f;if(s==e.target&&i&&(new Date).getTime()-o<r.taphold_threshold&&(u.x==a&&u.y==f||diff_x>=-r.tap_pixel_range&&diff_x<=r.tap_pixel_range&&diff_y>=-r.tap_pixel_range&&diff_y<=r.tap_pixel_range)){var l=e.originalEvent;var c={position:{x:r.touch_capable?l.changedTouches[0].screenX:e.screenX,y:r.touch_capable?l.changedTouches[0].screenY:e.screenY},offset:{x:r.touch_capable?l.changedTouches[0].pageX-l.changedTouches[0].target.offsetLeft:e.offsetX,y:r.touch_capable?l.changedTouches[0].pageY-l.changedTouches[0].target.offsetTop:e.offsetY},time:(new Date).getTime(),target:e.target};E(t,"tap",e,c)}})},remove:function(){e(this).off(r.startevent,e(this).data.callee1).off(r.endevent,e(this).data.callee2)}};e.event.special.swipe={setup:function(){function f(t){n=e(t.target);n.data("callee1",arguments.callee);o.x=t.originalEvent.targetTouches?t.originalEvent.targetTouches[0].pageX:t.pageX;o.y=t.originalEvent.targetTouches?t.originalEvent.targetTouches[0].pageY:t.pageY;u.x=o.x;u.y=o.y;i=true;var s=t.originalEvent;a={position:{x:r.touch_capable?s.touches[0].screenX:t.screenX,y:r.touch_capable?s.touches[0].screenY:t.screenY},offset:{x:r.touch_capable?s.touches[0].pageX-s.touches[0].target.offsetLeft:t.offsetX,y:r.touch_capable?s.touches[0].pageY-s.touches[0].target.offsetTop:t.offsetY},time:(new Date).getTime(),target:t.target};var f=new Date;while(new Date-f<100){}}function l(t){n=e(t.target);n.data("callee2",arguments.callee);u.x=t.originalEvent.targetTouches?t.originalEvent.targetTouches[0].pageX:t.pageX;u.y=t.originalEvent.targetTouches?t.originalEvent.targetTouches[0].pageY:t.pageY;window.clearTimeout(r.hold_timer);var f;var l=n.data("xthreshold"),c=n.data("ythreshold"),h=typeof l!=="undefined"&&l!==false&&parseInt(l)?parseInt(l):r.swipe_h_threshold,p=typeof c!=="undefined"&&c!==false&&parseInt(c)?parseInt(c):r.swipe_v_threshold;if(o.y>u.y&&o.y-u.y>p){f="swipeup"}if(o.x<u.x&&u.x-o.x>h){f="swiperight"}if(o.y<u.y&&u.y-o.y>p){f="swipedown"}if(o.x>u.x&&o.x-u.x>h){f="swipeleft"}if(f!=undefined&&i){o.x=0;o.y=0;u.x=0;u.y=0;i=false;var d=t.originalEvent;endEvnt={position:{x:r.touch_capable?d.touches[0].screenX:t.screenX,y:r.touch_capable?d.touches[0].screenY:t.screenY},offset:{x:r.touch_capable?d.touches[0].pageX-d.touches[0].target.offsetLeft:t.offsetX,y:r.touch_capable?d.touches[0].pageY-d.touches[0].target.offsetTop:t.offsetY},time:(new Date).getTime(),target:t.target};var v=Math.abs(a.position.x-endEvnt.position.x),m=Math.abs(a.position.y-endEvnt.position.y);var g={startEvnt:a,endEvnt:endEvnt,direction:f.replace("swipe",""),xAmount:v,yAmount:m,duration:endEvnt.time-a.time};s=true;n.trigger("swipe",g).trigger(f,g)}}function c(t){n=e(t.target);var o="";n.data("callee3",arguments.callee);if(s){var u=n.data("xthreshold"),f=n.data("ythreshold"),l=typeof u!=="undefined"&&u!==false&&parseInt(u)?parseInt(u):r.swipe_h_threshold,c=typeof f!=="undefined"&&f!==false&&parseInt(f)?parseInt(f):r.swipe_v_threshold;var h=t.originalEvent;endEvnt={position:{x:r.touch_capable?h.changedTouches[0].screenX:t.screenX,y:r.touch_capable?h.changedTouches[0].screenY:t.screenY},offset:{x:r.touch_capable?h.changedTouches[0].pageX-h.changedTouches[0].target.offsetLeft:t.offsetX,y:r.touch_capable?h.changedTouches[0].pageY-h.changedTouches[0].target.offsetTop:t.offsetY},time:(new Date).getTime(),target:t.target};if(a.position.y>endEvnt.position.y&&a.position.y-endEvnt.position.y>c){o="swipeup"}if(a.position.x<endEvnt.position.x&&endEvnt.position.x-a.position.x>l){o="swiperight"}if(a.position.y<endEvnt.position.y&&endEvnt.position.y-a.position.y>c){o="swipedown"}if(a.position.x>endEvnt.position.x&&a.position.x-endEvnt.position.x>l){o="swipeleft"}var p=Math.abs(a.position.x-endEvnt.position.x),d=Math.abs(a.position.y-endEvnt.position.y);var v={startEvnt:a,endEvnt:endEvnt,direction:o.replace("swipe",""),xAmount:p,yAmount:d,duration:endEvnt.time-a.time};n.trigger("swipeend",v)}i=false;s=false}var t=this,n=e(t),i=false,s=false,o={x:0,y:0},u={x:0,y:0},a;n.on(r.startevent,f);n.on(r.moveevent,l);n.on(r.endevent,c)},remove:function(){e(this).off(r.startevent,e(this).data.callee1).off(r.moveevent,e(this).data.callee2).off(r.endevent,e(this).data.callee3)}};e.event.special.scrollstart={setup:function(){function o(e,n){i=n;E(t,i?"scrollstart":"scrollend",e)}var t=this,n=e(t),i,s;n.on(r.scrollevent,function(e){n.data("callee",arguments.callee);if(!i){o(e,true)}clearTimeout(s);s=setTimeout(function(){o(e,false)},50)})},remove:function(){e(this).off(r.scrollevent,e(this).data.callee)}};var i=e(window),s,o,u,a,f,l={0:true,180:true};if(r.orientation_support){var c=window.innerWidth||e(window).width(),h=window.innerHeight||e(window).height(),p=50;a=c>h&&c-h>p;f=l[window.orientation];if(a&&f||!a&&!f){l={"-90":true,90:true}}}e.event.special.orientationchange=s={setup:function(){if(r.orientation_support){return false}u=o();i.on("throttledresize",d);return true},teardown:function(){if(r.orientation_support){return false}i.off("throttledresize",d);return true},add:function(e){var t=e.handler;e.handler=function(e){e.orientation=o();return t.apply(this,arguments)}}};e.event.special.orientationchange.orientation=o=function(){var e=true,t=document.documentElement;if(r.orientation_support){e=l[window.orientation]}else{e=t&&t.clientWidth/t.clientHeight<1.1}return e?"portrait":"landscape"};e.event.special.throttledresize={setup:function(){e(this).on("resize",m)},teardown:function(){e(this).off("resize",m)}};var v=250,m=function(){b=(new Date).getTime();w=b-g;if(w>=v){g=b;e(this).trigger("throttledresize")}else{if(y){window.clearTimeout(y)}y=window.setTimeout(d,v-w)}},g=0,y,b,w;e.each({scrollend:"scrollstart",swipeup:"swipe",swiperight:"swipe",swipedown:"swipe",swipeleft:"swipe",swipeend:"swipe"},function(t,n,r){e.event.special[t]={setup:function(){e(this).on(n,e.noop)}}})})(jQuery)
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/js-beautifier.js b/libs/editor/WordPressEditor/src/main/assets/libs/js-beautifier.js
new file mode 100644
index 000000000..e0227ebeb
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/js-beautifier.js
@@ -0,0 +1,766 @@
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2013 Einar Lielmanis and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+
+ JS Beautifier
+ ---------------
+
+ */
+
+function trim(s) {
+ return s.replace(/^\s+|\s+$/g, '');
+}
+
+function ltrim(s) {
+ return s.replace(/^\s+/g, '');
+}
+
+function style_html(html_source, options) {
+ //Wrapper function to invoke all the necessary constructors and deal with the output.
+
+ var multi_parser,
+ indent_inner_html,
+ indent_size,
+ indent_character,
+ wrap_line_length,
+ brace_style,
+ unformatted,
+ preserve_newlines,
+ max_preserve_newlines;
+
+ options = options || {};
+
+ // backwards compatibility to 1.3.4
+ if ((options.wrap_line_length === undefined || parseInt(options.wrap_line_length, 10) === 0) &&
+ (options.max_char === undefined || parseInt(options.max_char, 10) === 0)) {
+ options.wrap_line_length = options.max_char;
+ }
+
+ indent_inner_html = options.indent_inner_html || false;
+ indent_size = parseInt(options.indent_size || 4, 10);
+ indent_character = options.indent_char || ' ';
+ brace_style = options.brace_style || 'collapse';
+ wrap_line_length = parseInt(options.wrap_line_length, 10) === 0 ? 32786 : parseInt(options.wrap_line_length || 250, 10);
+ unformatted = options.unformatted || ['a', 'span', 'bdo', 'em', 'strong', 'dfn', 'code', 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'q', 'sub', 'sup', 'tt', 'i', 'b', 'big', 'small', 'u', 's', 'strike', 'font', 'ins', 'del', 'pre', 'address', 'dt', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
+ preserve_newlines = options.preserve_newlines || true;
+ max_preserve_newlines = preserve_newlines ? parseInt(options.max_preserve_newlines || 32786, 10) : 0;
+ indent_handlebars = options.indent_handlebars || false;
+
+ function Parser() {
+
+ this.pos = 0; //Parser position
+ this.token = '';
+ this.current_mode = 'CONTENT'; //reflects the current Parser mode: TAG/CONTENT
+ this.tags = { //An object to hold tags, their position, and their parent-tags, initiated with default values
+ parent: 'parent1',
+ parentcount: 1,
+ parent1: ''
+ };
+ this.tag_type = '';
+ this.token_text = this.last_token = this.last_text = this.token_type = '';
+ this.newlines = 0;
+ this.indent_content = indent_inner_html;
+
+ this.Utils = { //Uilities made available to the various functions
+ whitespace: "\n\r\t ".split(''),
+ single_token: 'br,input,link,meta,!doctype,basefont,base,area,hr,wbr,param,img,isindex,?xml,embed,?php,?,?='.split(','), //all the single tags for HTML
+ extra_liners: 'head,body,/html'.split(','), //for tags that need a line of whitespace before them
+ in_array: function(what, arr) {
+ for (var i = 0; i < arr.length; i++) {
+ if (what === arr[i]) {
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+
+ this.traverse_whitespace = function() {
+ var input_char = '';
+
+ input_char = this.input.charAt(this.pos);
+ if (this.Utils.in_array(input_char, this.Utils.whitespace)) {
+ this.newlines = 0;
+ while (this.Utils.in_array(input_char, this.Utils.whitespace)) {
+ if (preserve_newlines && input_char === '\n' && this.newlines <= max_preserve_newlines) {
+ this.newlines += 1;
+ }
+
+ this.pos++;
+ input_char = this.input.charAt(this.pos);
+ }
+ return true;
+ }
+ return false;
+ };
+
+ this.get_content = function() { //function to capture regular content between tags
+
+ var input_char = '',
+ content = [],
+ space = false; //if a space is needed
+
+ while (this.input.charAt(this.pos) !== '<') {
+ if (this.pos >= this.input.length) {
+ return content.length ? content.join('') : ['', 'TK_EOF'];
+ }
+
+ if (this.traverse_whitespace()) {
+ if (content.length) {
+ space = true;
+ }
+ continue; //don't want to insert unnecessary space
+ }
+
+ if (indent_handlebars) {
+ // Handlebars parsing is complicated.
+ // {{#foo}} and {{/foo}} are formatted tags.
+ // {{something}} should get treated as content, except:
+ // {{else}} specifically behaves like {{#if}} and {{/if}}
+ var peek3 = this.input.substr(this.pos, 3);
+ if (peek3 === '{{#' || peek3 === '{{/') {
+ // These are tags and not content.
+ break;
+ } else if (this.input.substr(this.pos, 2) === '{{') {
+ if (this.get_tag(true) === '{{else}}') {
+ break;
+ }
+ }
+ }
+
+ input_char = this.input.charAt(this.pos);
+ this.pos++;
+
+ if (space) {
+ if (this.line_char_count >= this.wrap_line_length) { //insert a line when the wrap_line_length is reached
+ this.print_newline(false, content);
+ this.print_indentation(content);
+ } else {
+ this.line_char_count++;
+ content.push(' ');
+ }
+ space = false;
+ }
+ this.line_char_count++;
+ content.push(input_char); //letter at-a-time (or string) inserted to an array
+ }
+ return content.length ? content.join('') : '';
+ };
+
+ this.get_contents_to = function(name) { //get the full content of a script or style to pass to js_beautify
+ if (this.pos === this.input.length) {
+ return ['', 'TK_EOF'];
+ }
+ var input_char = '';
+ var content = '';
+ var reg_match = new RegExp('</' + name + '\\s*>', 'igm');
+ reg_match.lastIndex = this.pos;
+ var reg_array = reg_match.exec(this.input);
+ var end_script = reg_array ? reg_array.index : this.input.length; //absolute end of script
+ if (this.pos < end_script) { //get everything in between the script tags
+ content = this.input.substring(this.pos, end_script);
+ this.pos = end_script;
+ }
+ return content;
+ };
+
+ this.record_tag = function(tag) { //function to record a tag and its parent in this.tags Object
+ if (this.tags[tag + 'count']) { //check for the existence of this tag type
+ this.tags[tag + 'count']++;
+ this.tags[tag + this.tags[tag + 'count']] = this.indent_level; //and record the present indent level
+ } else { //otherwise initialize this tag type
+ this.tags[tag + 'count'] = 1;
+ this.tags[tag + this.tags[tag + 'count']] = this.indent_level; //and record the present indent level
+ }
+ this.tags[tag + this.tags[tag + 'count'] + 'parent'] = this.tags.parent; //set the parent (i.e. in the case of a div this.tags.div1parent)
+ this.tags.parent = tag + this.tags[tag + 'count']; //and make this the current parent (i.e. in the case of a div 'div1')
+ };
+
+ this.retrieve_tag = function(tag) { //function to retrieve the opening tag to the corresponding closer
+ if (this.tags[tag + 'count']) { //if the openener is not in the Object we ignore it
+ var temp_parent = this.tags.parent; //check to see if it's a closable tag.
+ while (temp_parent) { //till we reach '' (the initial value);
+ if (tag + this.tags[tag + 'count'] === temp_parent) { //if this is it use it
+ break;
+ }
+ temp_parent = this.tags[temp_parent + 'parent']; //otherwise keep on climbing up the DOM Tree
+ }
+ if (temp_parent) { //if we caught something
+ this.indent_level = this.tags[tag + this.tags[tag + 'count']]; //set the indent_level accordingly
+ this.tags.parent = this.tags[temp_parent + 'parent']; //and set the current parent
+ }
+ delete this.tags[tag + this.tags[tag + 'count'] + 'parent']; //delete the closed tags parent reference...
+ delete this.tags[tag + this.tags[tag + 'count']]; //...and the tag itself
+ if (this.tags[tag + 'count'] === 1) {
+ delete this.tags[tag + 'count'];
+ } else {
+ this.tags[tag + 'count']--;
+ }
+ }
+ };
+
+ this.indent_to_tag = function(tag) {
+ // Match the indentation level to the last use of this tag, but don't remove it.
+ if (!this.tags[tag + 'count']) {
+ return;
+ }
+ var temp_parent = this.tags.parent;
+ while (temp_parent) {
+ if (tag + this.tags[tag + 'count'] === temp_parent) {
+ break;
+ }
+ temp_parent = this.tags[temp_parent + 'parent'];
+ }
+ if (temp_parent) {
+ this.indent_level = this.tags[tag + this.tags[tag + 'count']];
+ }
+ };
+
+ this.get_tag = function(peek) { //function to get a full tag and parse its type
+ var input_char = '',
+ content = [],
+ comment = '',
+ space = false,
+ tag_start, tag_end,
+ tag_start_char,
+ orig_pos = this.pos,
+ orig_line_char_count = this.line_char_count;
+
+ peek = peek !== undefined ? peek : false;
+
+ do {
+ if (this.pos >= this.input.length) {
+ if (peek) {
+ this.pos = orig_pos;
+ this.line_char_count = orig_line_char_count;
+ }
+ return content.length ? content.join('') : ['', 'TK_EOF'];
+ }
+
+ input_char = this.input.charAt(this.pos);
+ this.pos++;
+
+ if (this.Utils.in_array(input_char, this.Utils.whitespace)) { //don't want to insert unnecessary space
+ space = true;
+ continue;
+ }
+
+ if (input_char === "'" || input_char === '"') {
+ input_char += this.get_unformatted(input_char);
+ space = true;
+
+ }
+
+ if (input_char === '=') { //no space before =
+ space = false;
+ }
+
+ if (content.length && content[content.length - 1] !== '=' && input_char !== '>' && space) {
+ //no space after = or before >
+ if (this.line_char_count >= this.wrap_line_length) {
+ this.print_newline(false, content);
+ this.print_indentation(content);
+ } else {
+ content.push(' ');
+ this.line_char_count++;
+ }
+ space = false;
+ }
+
+ if (indent_handlebars && tag_start_char === '<') {
+ // When inside an angle-bracket tag, put spaces around
+ // handlebars not inside of strings.
+ if ((input_char + this.input.charAt(this.pos)) === '{{') {
+ input_char += this.get_unformatted('}}');
+ if (content.length && content[content.length - 1] !== ' ' && content[content.length - 1] !== '<') {
+ input_char = ' ' + input_char;
+ }
+ space = true;
+ }
+ }
+
+ if (input_char === '<' && !tag_start_char) {
+ tag_start = this.pos - 1;
+ tag_start_char = '<';
+ }
+
+ if (indent_handlebars && !tag_start_char) {
+ if (content.length >= 2 && content[content.length - 1] === '{' && content[content.length - 2] == '{') {
+ if (input_char === '#' || input_char === '/') {
+ tag_start = this.pos - 3;
+ } else {
+ tag_start = this.pos - 2;
+ }
+ tag_start_char = '{';
+ }
+ }
+
+ this.line_char_count++;
+ content.push(input_char); //inserts character at-a-time (or string)
+
+ if (content[1] && content[1] === '!') { //if we're in a comment, do something special
+ // We treat all comments as literals, even more than preformatted tags
+ // we just look for the appropriate close tag
+ content = [this.get_comment(tag_start)];
+ break;
+ }
+
+ if (indent_handlebars && tag_start_char === '{' && content.length > 2 && content[content.length - 2] === '}' && content[content.length - 1] === '}') {
+ break;
+ }
+ } while (input_char !== '>');
+
+ var tag_complete = content.join('');
+ var tag_index;
+ var tag_offset;
+
+ if (tag_complete.indexOf(' ') !== -1) { //if there's whitespace, thats where the tag name ends
+ tag_index = tag_complete.indexOf(' ');
+ } else if (tag_complete[0] === '{') {
+ tag_index = tag_complete.indexOf('}');
+ } else { //otherwise go with the tag ending
+ tag_index = tag_complete.indexOf('>');
+ }
+ if (tag_complete[0] === '<' || !indent_handlebars) {
+ tag_offset = 1;
+ } else {
+ tag_offset = tag_complete[2] === '#' ? 3 : 2;
+ }
+ var tag_check = tag_complete.substring(tag_offset, tag_index).toLowerCase();
+ if (tag_complete.charAt(tag_complete.length - 2) === '/' ||
+ this.Utils.in_array(tag_check, this.Utils.single_token)) { //if this tag name is a single tag type (either in the list or has a closing /)
+ if (!peek) {
+ this.tag_type = 'SINGLE';
+ }
+ } else if (indent_handlebars && tag_complete[0] === '{' && tag_check === 'else') {
+ if (!peek) {
+ this.indent_to_tag('if');
+ this.tag_type = 'HANDLEBARS_ELSE';
+ this.indent_content = true;
+ this.traverse_whitespace();
+ }
+ } else if (tag_check === 'script') { //for later script handling
+ if (!peek) {
+ this.record_tag(tag_check);
+ this.tag_type = 'SCRIPT';
+ }
+ } else if (tag_check === 'style') { //for future style handling (for now it justs uses get_content)
+ if (!peek) {
+ this.record_tag(tag_check);
+ this.tag_type = 'STYLE';
+ }
+ } else if (this.is_unformatted(tag_check, unformatted)) { // do not reformat the "unformatted" tags
+ comment = this.get_unformatted('</' + tag_check + '>', tag_complete); //...delegate to get_unformatted function
+ content.push(comment);
+ // Preserve collapsed whitespace either before or after this tag.
+ if (tag_start > 0 && this.Utils.in_array(this.input.charAt(tag_start - 1), this.Utils.whitespace)) {
+ content.splice(0, 0, this.input.charAt(tag_start - 1));
+ }
+ tag_end = this.pos - 1;
+ if (this.Utils.in_array(this.input.charAt(tag_end + 1), this.Utils.whitespace)) {
+ content.push(this.input.charAt(tag_end + 1));
+ }
+ this.tag_type = 'SINGLE';
+ } else if (tag_check.charAt(0) === '!') { //peek for <! comment
+ // for comments content is already correct.
+ if (!peek) {
+ this.tag_type = 'SINGLE';
+ this.traverse_whitespace();
+ }
+ } else if (!peek) {
+ if (tag_check.charAt(0) === '/') { //this tag is a double tag so check for tag-ending
+ this.retrieve_tag(tag_check.substring(1)); //remove it and all ancestors
+ this.tag_type = 'END';
+ this.traverse_whitespace();
+ } else { //otherwise it's a start-tag
+ this.record_tag(tag_check); //push it on the tag stack
+ if (tag_check.toLowerCase() !== 'html') {
+ this.indent_content = true;
+ }
+ this.tag_type = 'START';
+
+ // Allow preserving of newlines after a start tag
+ this.traverse_whitespace();
+ }
+ if (this.Utils.in_array(tag_check, this.Utils.extra_liners)) { //check if this double needs an extra line
+ this.print_newline(false, this.output);
+ if (this.output.length && this.output[this.output.length - 2] !== '\n') {
+ this.print_newline(true, this.output);
+ }
+ }
+ }
+
+ if (peek) {
+ this.pos = orig_pos;
+ this.line_char_count = orig_line_char_count;
+ }
+
+ return content.join(''); //returns fully formatted tag
+ };
+
+ this.get_comment = function(start_pos) { //function to return comment content in its entirety
+ // this is will have very poor perf, but will work for now.
+ var comment = '',
+ delimiter = '>',
+ matched = false;
+
+ this.pos = start_pos;
+ input_char = this.input.charAt(this.pos);
+ this.pos++;
+
+ while (this.pos <= this.input.length) {
+ comment += input_char;
+
+ // only need to check for the delimiter if the last chars match
+ if (comment[comment.length - 1] === delimiter[delimiter.length - 1] &&
+ comment.indexOf(delimiter) !== -1) {
+ break;
+ }
+
+ // only need to search for custom delimiter for the first few characters
+ if (!matched && comment.length < 10) {
+ if (comment.indexOf('<![if') === 0) { //peek for <![if conditional comment
+ delimiter = '<![endif]>';
+ matched = true;
+ } else if (comment.indexOf('<![cdata[') === 0) { //if it's a <[cdata[ comment...
+ delimiter = ']]>';
+ matched = true;
+ } else if (comment.indexOf('<![') === 0) { // some other ![ comment? ...
+ delimiter = ']>';
+ matched = true;
+ } else if (comment.indexOf('<!--') === 0) { // <!-- comment ...
+ delimiter = '-->';
+ matched = true;
+ }
+ }
+
+ input_char = this.input.charAt(this.pos);
+ this.pos++;
+ }
+
+ return comment;
+ };
+
+ this.get_unformatted = function(delimiter, orig_tag) { //function to return unformatted content in its entirety
+
+ if (orig_tag && orig_tag.toLowerCase().indexOf(delimiter) !== -1) {
+ return '';
+ }
+ var input_char = '';
+ var content = '';
+ var min_index = 0;
+ var space = true;
+ do {
+
+ if (this.pos >= this.input.length) {
+ return content;
+ }
+
+ input_char = this.input.charAt(this.pos);
+ this.pos++;
+
+ if (this.Utils.in_array(input_char, this.Utils.whitespace)) {
+ if (!space) {
+ this.line_char_count--;
+ continue;
+ }
+ if (input_char === '\n' || input_char === '\r') {
+ content += '\n';
+ /* Don't change tab indention for unformatted blocks. If using code for html editing, this will greatly affect <pre> tags if they are specified in the 'unformatted array'
+ for (var i=0; i<this.indent_level; i++) {
+ content += this.indent_string;
+ }
+ space = false; //...and make sure other indentation is erased
+ */
+ this.line_char_count = 0;
+ continue;
+ }
+ }
+ content += input_char;
+ this.line_char_count++;
+ space = true;
+
+ if (indent_handlebars && input_char === '{' && content.length && content[content.length - 2] === '{') {
+ // Handlebars expressions in strings should also be unformatted.
+ content += this.get_unformatted('}}');
+ // These expressions are opaque. Ignore delimiters found in them.
+ min_index = content.length;
+ }
+ } while (content.toLowerCase().indexOf(delimiter, min_index) === -1);
+ return content;
+ };
+
+ this.get_token = function() { //initial handler for token-retrieval
+ var token;
+
+ if (this.last_token === 'TK_TAG_SCRIPT' || this.last_token === 'TK_TAG_STYLE') { //check if we need to format javascript
+ var type = this.last_token.substr(7);
+ token = this.get_contents_to(type);
+ if (typeof token !== 'string') {
+ return token;
+ }
+ return [token, 'TK_' + type];
+ }
+ if (this.current_mode === 'CONTENT') {
+ token = this.get_content();
+ if (typeof token !== 'string') {
+ return token;
+ } else {
+ return [token, 'TK_CONTENT'];
+ }
+ }
+
+ if (this.current_mode === 'TAG') {
+ token = this.get_tag();
+ if (typeof token !== 'string') {
+ return token;
+ } else {
+ var tag_name_type = 'TK_TAG_' + this.tag_type;
+ return [token, tag_name_type];
+ }
+ }
+ };
+
+ this.get_full_indent = function(level) {
+ level = this.indent_level + level || 0;
+ if (level < 1) {
+ return '';
+ }
+
+ return Array(level + 1).join(this.indent_string);
+ };
+
+ this.is_unformatted = function(tag_check, unformatted) {
+ //is this an HTML5 block-level link?
+ if (!this.Utils.in_array(tag_check, unformatted)) {
+ return false;
+ }
+
+ if (tag_check.toLowerCase() !== 'a' || !this.Utils.in_array('a', unformatted)) {
+ return true;
+ }
+
+ //at this point we have an tag; is its first child something we want to remain
+ //unformatted?
+ var next_tag = this.get_tag(true /* peek. */ );
+
+ // test next_tag to see if it is just html tag (no external content)
+ var tag = (next_tag || "").match(/^\s*<\s*\/?([a-z]*)\s*[^>]*>\s*$/);
+
+ // if next_tag comes back but is not an isolated tag, then
+ // let's treat the 'a' tag as having content
+ // and respect the unformatted option
+ if (!tag || this.Utils.in_array(tag, unformatted)) {
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ this.printer = function(js_source, indent_character, indent_size, wrap_line_length, brace_style) { //handles input/output and some other printing functions
+
+ this.input = js_source || ''; //gets the input for the Parser
+ this.output = [];
+ this.indent_character = indent_character;
+ this.indent_string = '';
+ this.indent_size = indent_size;
+ this.brace_style = brace_style;
+ this.indent_level = 0;
+ this.wrap_line_length = wrap_line_length;
+ this.line_char_count = 0; //count to see if wrap_line_length was exceeded
+
+ for (var i = 0; i < this.indent_size; i++) {
+ this.indent_string += this.indent_character;
+ }
+
+ this.print_newline = function(force, arr) {
+ this.line_char_count = 0;
+ if (!arr || !arr.length) {
+ return;
+ }
+ if (force || (arr[arr.length - 1] !== '\n')) { //we might want the extra line
+ arr.push('\n');
+ }
+ };
+
+ this.print_indentation = function(arr) {
+ for (var i = 0; i < this.indent_level; i++) {
+ arr.push(this.indent_string);
+ this.line_char_count += this.indent_string.length;
+ }
+ };
+
+ this.print_token = function(text) {
+ if (text || text !== '') {
+ if (this.output.length && this.output[this.output.length - 1] === '\n') {
+ this.print_indentation(this.output);
+ text = ltrim(text);
+ }
+ }
+ this.print_token_raw(text);
+ };
+
+ this.print_token_raw = function(text) {
+ if (text && text !== '') {
+ if (text.length > 1 && text[text.length - 1] === '\n') {
+ // unformatted tags can grab newlines as their last character
+ this.output.push(text.slice(0, -1));
+ this.print_newline(false, this.output);
+ } else {
+ this.output.push(text);
+ }
+ }
+
+ for (var n = 0; n < this.newlines; n++) {
+ this.print_newline(n > 0, this.output);
+ }
+ this.newlines = 0;
+ };
+
+ this.indent = function() {
+ this.indent_level++;
+ };
+
+ this.unindent = function() {
+ if (this.indent_level > 0) {
+ this.indent_level--;
+ }
+ };
+ };
+ return this;
+ }
+
+ /*_____________________--------------------_____________________*/
+
+ multi_parser = new Parser(); //wrapping functions Parser
+ multi_parser.printer(html_source, indent_character, indent_size, wrap_line_length, brace_style); //initialize starting values
+
+ while (true) {
+ var t = multi_parser.get_token();
+ multi_parser.token_text = t[0];
+ multi_parser.token_type = t[1];
+
+ if (multi_parser.token_type === 'TK_EOF') {
+ break;
+ }
+
+ switch (multi_parser.token_type) {
+ case 'TK_TAG_START':
+ multi_parser.print_newline(false, multi_parser.output);
+ multi_parser.print_token(multi_parser.token_text);
+ if (multi_parser.indent_content) {
+ multi_parser.indent();
+ multi_parser.indent_content = false;
+ }
+ multi_parser.current_mode = 'CONTENT';
+ break;
+ case 'TK_TAG_STYLE':
+ case 'TK_TAG_SCRIPT':
+ multi_parser.print_newline(false, multi_parser.output);
+ multi_parser.print_token(multi_parser.token_text);
+ multi_parser.current_mode = 'CONTENT';
+ break;
+ case 'TK_TAG_END':
+ //Print new line only if the tag has no content and has child
+ if (multi_parser.last_token === 'TK_CONTENT' && multi_parser.last_text === '') {
+ var tag_name = multi_parser.token_text.match(/\w+/)[0];
+ var tag_extracted_from_last_output = null;
+ if (multi_parser.output.length) {
+ tag_extracted_from_last_output = multi_parser.output[multi_parser.output.length - 1].match(/(?:<|{{#)\s*(\w+)/);
+ }
+ if (tag_extracted_from_last_output === null ||
+ tag_extracted_from_last_output[1] !== tag_name) {
+ multi_parser.print_newline(false, multi_parser.output);
+ }
+ }
+ multi_parser.print_token(multi_parser.token_text);
+ multi_parser.current_mode = 'CONTENT';
+ break;
+ case 'TK_TAG_SINGLE':
+ // Don't add a newline before elements that should remain unformatted.
+ var tag_check = multi_parser.token_text.match(/^\s*<([a-z]+)/i);
+ if (!tag_check || !multi_parser.Utils.in_array(tag_check[1], unformatted)) {
+ multi_parser.print_newline(false, multi_parser.output);
+ }
+ multi_parser.print_token(multi_parser.token_text);
+ multi_parser.current_mode = 'CONTENT';
+ break;
+ case 'TK_TAG_HANDLEBARS_ELSE':
+ multi_parser.print_token(multi_parser.token_text);
+ if (multi_parser.indent_content) {
+ multi_parser.indent();
+ multi_parser.indent_content = false;
+ }
+ multi_parser.current_mode = 'CONTENT';
+ break;
+ case 'TK_CONTENT':
+ multi_parser.print_token(multi_parser.token_text);
+ multi_parser.current_mode = 'TAG';
+ break;
+ case 'TK_STYLE':
+ case 'TK_SCRIPT':
+ if (multi_parser.token_text !== '') {
+ multi_parser.print_newline(false, multi_parser.output);
+ var text = multi_parser.token_text,
+ _beautifier,
+ script_indent_level = 1;
+ if (multi_parser.token_type === 'TK_SCRIPT') {
+ _beautifier = typeof js_beautify === 'function' && js_beautify;
+ } else if (multi_parser.token_type === 'TK_STYLE') {
+ _beautifier = typeof css_beautify === 'function' && css_beautify;
+ }
+
+ if (options.indent_scripts === "keep") {
+ script_indent_level = 0;
+ } else if (options.indent_scripts === "separate") {
+ script_indent_level = -multi_parser.indent_level;
+ }
+
+ var indentation = multi_parser.get_full_indent(script_indent_level);
+ if (_beautifier) {
+ // call the Beautifier if avaliable
+ text = _beautifier(text.replace(/^\s*/, indentation), options);
+ } else {
+ // simply indent the string otherwise
+ var white = text.match(/^\s*/)[0];
+ var _level = white.match(/[^\n\r]*$/)[0].split(multi_parser.indent_string).length - 1;
+ var reindent = multi_parser.get_full_indent(script_indent_level - _level);
+ text = text.replace(/^\s*/, indentation)
+ .replace(/\r\n|\r|\n/g, '\n' + reindent)
+ .replace(/\s+$/, '');
+ }
+ if (text) {
+ multi_parser.print_token_raw(indentation + trim(text));
+ multi_parser.print_newline(false, multi_parser.output);
+ }
+ }
+ multi_parser.current_mode = 'TAG';
+ break;
+ }
+ multi_parser.last_token = multi_parser.token_type;
+ multi_parser.last_text = multi_parser.token_text;
+ }
+ return multi_parser.output.join('');
+} \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/rangy-classapplier.js b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-classapplier.js
new file mode 100755
index 000000000..211e2cd2c
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-classapplier.js
@@ -0,0 +1,15 @@
+/**
+ * Class Applier module for Rangy.
+ * Adds, removes and toggles classes on Ranges and Selections
+ *
+ * Part of Rangy, a cross-browser JavaScript range and selection library
+ * https://github.com/timdown/rangy
+ *
+ * Depends on Rangy core.
+ *
+ * Copyright 2015, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.3.0
+ * Build date: 10 May 2015
+ */
+!function(e,t){"function"==typeof define&&define.amd?define(["./rangy-core"],e):"undefined"!=typeof module&&"object"==typeof exports?module.exports=e(require("rangy")):e(t.rangy)}(function(e){return e.createModule("ClassApplier",["WrappedSelection"],function(e,t){function n(e,t){for(var n in e)if(e.hasOwnProperty(n)&&t(n,e[n])===!1)return!1;return!0}function s(e){return e.replace(/^\s\s*/,"").replace(/\s\s*$/,"")}function r(e,t){return!!e&&new RegExp("(?:^|\\s)"+t+"(?:\\s|$)").test(e)}function o(e,t){if("object"==typeof e.classList)return e.classList.contains(t);var n="string"==typeof e.className,s=n?e.className:e.getAttribute("class");return r(s,t)}function a(e,t){if("object"==typeof e.classList)e.classList.add(t);else{var n="string"==typeof e.className,s=n?e.className:e.getAttribute("class");s?r(s,t)||(s+=" "+t):s=t,n?e.className=s:e.setAttribute("class",s)}}function l(e){var t="string"==typeof e.className;return t?e.className:e.getAttribute("class")}function u(e){return e&&e.split(/\s+/).sort().join(" ")}function f(e){return u(l(e))}function c(e,t){return f(e)==f(t)}function p(e,t){for(var n=t.split(/\s+/),r=0,i=n.length;i>r;++r)if(!o(e,s(n[r])))return!1;return!0}function d(e){var t=e.parentNode;return t&&1==t.nodeType&&!/^(textarea|style|script|select|iframe)$/i.test(t.nodeName)}function h(e,t,n,s,r){var i=e.node,o=e.offset,a=i,l=o;i==s&&o>r&&++l,i!=t||o!=n&&o!=n+1||(a=s,l+=r-n),i==t&&o>n+1&&--l,e.node=a,e.offset=l}function m(e,t,n){e.node==t&&e.offset>n&&--e.offset}function g(e,t,n,s){-1==n&&(n=t.childNodes.length);var r=e.parentNode,i=$.getNodeIndex(e);U(s,function(e){h(e,r,i,t,n)}),t.childNodes.length==n?t.appendChild(e):t.insertBefore(e,t.childNodes[n])}function N(e,t){var n=e.parentNode,s=$.getNodeIndex(e);U(t,function(e){m(e,n,s)}),$.removeNode(e)}function v(e,t,n,s,r){for(var i,o=[];i=e.firstChild;)g(i,t,n++,r),o.push(i);return s&&N(e,r),o}function y(e,t){return v(e,e.parentNode,$.getNodeIndex(e),!0,t)}function C(e,t){var n=e.cloneRange();n.selectNodeContents(t);var s=n.intersection(e),r=s?s.toString():"";return""!=r}function T(e){for(var t,n=e.getNodes([3]),s=0;(t=n[s])&&!C(e,t);)++s;for(var r=n.length-1;(t=n[r])&&!C(e,t);)--r;return n.slice(s,r+1)}function E(e,t){if(e.attributes.length!=t.attributes.length)return!1;for(var n,s,r,i=0,o=e.attributes.length;o>i;++i)if(n=e.attributes[i],r=n.name,"class"!=r){if(s=t.attributes.getNamedItem(r),null===n!=(null===s))return!1;if(n.specified!=s.specified)return!1;if(n.specified&&n.nodeValue!==s.nodeValue)return!1}return!0}function b(e,t){for(var n,s=0,r=e.attributes.length;r>s;++s)if(n=e.attributes[s].name,(!t||!B(t,n))&&e.attributes[s].specified&&"class"!=n)return!0;return!1}function A(e){var t;return e&&1==e.nodeType&&((t=e.parentNode)&&9==t.nodeType&&"on"==t.designMode||G(e)&&!G(e.parentNode))}function S(e){return(G(e)||1!=e.nodeType&&G(e.parentNode))&&!A(e)}function x(e){return e&&1==e.nodeType&&!J.test(F(e,"display"))}function R(e){if(0==e.data.length)return!0;if(K.test(e.data))return!1;var t=F(e.parentNode,"whiteSpace");switch(t){case"pre":case"pre-wrap":case"-moz-pre-wrap":return!1;case"pre-line":if(/[\r\n]/.test(e.data))return!1}return x(e.previousSibling)||x(e.nextSibling)}function P(e){var t,n,s=[];for(t=0;n=e[t++];)s.push(new z(n.startContainer,n.startOffset),new z(n.endContainer,n.endOffset));return s}function w(e,t){for(var n,s,r,i=0,o=e.length;o>i;++i)n=e[i],s=t[2*i],r=t[2*i+1],n.setStartAndEnd(s.node,s.offset,r.node,r.offset)}function O(e,t){return $.isCharacterDataNode(e)?0==t?!!e.previousSibling:t==e.length?!!e.nextSibling:!0:t>0&&t<e.childNodes.length}function I(e,n,s,r){var i,o,a=0==s;if($.isAncestorOf(n,e))return e;if($.isCharacterDataNode(n)){var l=$.getNodeIndex(n);if(0==s)s=l;else{if(s!=n.length)throw t.createError("splitNodeAt() should not be called with offset in the middle of a data node ("+s+" in "+n.data);s=l+1}n=n.parentNode}if(O(n,s)){i=n.cloneNode(!1),o=n.parentNode,i.id&&i.removeAttribute("id");for(var u,f=0;u=n.childNodes[s];)g(u,i,f++,r);return g(i,o,$.getNodeIndex(n)+1,r),n==e?i:I(e,o,$.getNodeIndex(i),r)}if(e!=n){i=n.parentNode;var c=$.getNodeIndex(n);return a||c++,I(e,i,c,r)}return e}function W(e,t){return e.namespaceURI==t.namespaceURI&&e.tagName.toLowerCase()==t.tagName.toLowerCase()&&c(e,t)&&E(e,t)&&"inline"==F(e,"display")&&"inline"==F(t,"display")}function L(e){var t=e?"nextSibling":"previousSibling";return function(n,s){var r=n.parentNode,i=n[t];if(i){if(i&&3==i.nodeType)return i}else if(s&&(i=r[t],i&&1==i.nodeType&&W(r,i))){var o=i[e?"firstChild":"lastChild"];if(o&&3==o.nodeType)return o}return null}}function M(e){this.isElementMerge=1==e.nodeType,this.textNodes=[];var t=this.isElementMerge?e.lastChild:e;t&&(this.textNodes[0]=t)}function H(e,t,r){var i,o,a,l,f=this;f.cssClass=f.className=e;var c=null,p={};if("object"==typeof t&&null!==t){for("undefined"!=typeof t.elementTagName&&(t.elementTagName=t.elementTagName.toLowerCase()),r=t.tagNames,c=t.elementProperties,p=t.elementAttributes,o=0;l=Y[o++];)t.hasOwnProperty(l)&&(f[l]=t[l]);i=t.normalize}else i=t;f.normalize="undefined"==typeof i?!0:i,f.attrExceptions=[];var d=document.createElement(f.elementTagName);f.elementProperties=f.copyPropertiesToElement(c,d,!0),n(p,function(e,t){f.attrExceptions.push(e),p[e]=""+t}),f.elementAttributes=p,f.elementSortedClassName=f.elementProperties.hasOwnProperty("className")?u(f.elementProperties.className+" "+e):e,f.applyToAnyTagName=!1;var h=typeof r;if("string"==h)"*"==r?f.applyToAnyTagName=!0:f.tagNames=s(r.toLowerCase()).split(/\s*,\s*/);else if("object"==h&&"number"==typeof r.length)for(f.tagNames=[],o=0,a=r.length;a>o;++o)"*"==r[o]?f.applyToAnyTagName=!0:f.tagNames.push(r[o].toLowerCase());else f.tagNames=[f.elementTagName]}function j(e,t,n){return new H(e,t,n)}var $=e.dom,z=$.DomPosition,B=$.arrayContains,D=e.util,U=D.forEach,V="span",k=D.isHostMethod(document,"createElementNS"),q=function(){function e(e,t,n){return t&&n?" ":""}return function(t,n){if("object"==typeof t.classList)t.classList.remove(n);else{var s="string"==typeof t.className,r=s?t.className:t.getAttribute("class");r=r.replace(new RegExp("(^|\\s)"+n+"(\\s|$)"),e),s?t.className=r:t.setAttribute("class",r)}}}(),F=$.getComputedStyleProperty,G=function(){var e=document.createElement("div");return"boolean"==typeof e.isContentEditable?function(e){return e&&1==e.nodeType&&e.isContentEditable}:function(e){return e&&1==e.nodeType&&"false"!=e.contentEditable?"true"==e.contentEditable||G(e.parentNode):!1}}(),J=/^inline(-block|-table)?$/i,K=/[^\r\n\t\f \u200B]/,Q=L(!1),X=L(!0);M.prototype={doMerge:function(e){var t=this.textNodes,n=t[0];if(t.length>1){var s,r=$.getNodeIndex(n),i=[],o=0;U(t,function(t,a){s=t.parentNode,a>0&&(s.removeChild(t),s.hasChildNodes()||$.removeNode(s),e&&U(e,function(e){e.node==t&&(e.node=n,e.offset+=o),e.node==s&&e.offset>r&&(--e.offset,e.offset==r+1&&len-1>a&&(e.node=n,e.offset=o))})),i[a]=t.data,o+=t.data.length}),n.data=i.join("")}return n.data},getLength:function(){for(var e=this.textNodes.length,t=0;e--;)t+=this.textNodes[e].length;return t},toString:function(){var e=[];return U(this.textNodes,function(t,n){e[n]="'"+t.data+"'"}),"[Merge("+e.join(",")+")]"}};var Y=["elementTagName","ignoreWhiteSpace","applyToEditableOnly","useExistingElements","removeEmptyElements","onElementCreate"],Z={};H.prototype={elementTagName:V,elementProperties:{},elementAttributes:{},ignoreWhiteSpace:!0,applyToEditableOnly:!1,useExistingElements:!0,removeEmptyElements:!0,onElementCreate:null,copyPropertiesToElement:function(e,t,n){var s,r,i,o,l,f,c={};for(var p in e)if(e.hasOwnProperty(p))if(o=e[p],l=t[p],"className"==p)a(t,o),a(t,this.className),t[p]=u(t[p]),n&&(c[p]=o);else if("style"==p){r=l,n&&(c[p]=i={});for(s in e[p])e[p].hasOwnProperty(s)&&(r[s]=o[s],n&&(i[s]=r[s]));this.attrExceptions.push(p)}else t[p]=o,n&&(c[p]=t[p],f=Z.hasOwnProperty(p)?Z[p]:p,this.attrExceptions.push(f));return n?c:""},copyAttributesToElement:function(e,t){for(var n in e)e.hasOwnProperty(n)&&!/^class(?:Name)?$/i.test(n)&&t.setAttribute(n,e[n])},appliesToElement:function(e){return B(this.tagNames,e.tagName.toLowerCase())},getEmptyElements:function(e){var t=this;return e.getNodes([1],function(e){return t.appliesToElement(e)&&!e.hasChildNodes()})},hasClass:function(e){return 1==e.nodeType&&(this.applyToAnyTagName||this.appliesToElement(e))&&o(e,this.className)},getSelfOrAncestorWithClass:function(e){for(;e;){if(this.hasClass(e))return e;e=e.parentNode}return null},isModifiable:function(e){return!this.applyToEditableOnly||S(e)},isIgnorableWhiteSpaceNode:function(e){return this.ignoreWhiteSpace&&e&&3==e.nodeType&&R(e)},postApply:function(e,t,n,s){var r,o,a=e[0],l=e[e.length-1],u=[],f=a,c=l,p=0,d=l.length;U(e,function(e){o=Q(e,!s),o?(r||(r=new M(o),u.push(r)),r.textNodes.push(e),e===a&&(f=r.textNodes[0],p=f.length),e===l&&(c=r.textNodes[0],d=r.getLength())):r=null});var h=X(l,!s);if(h&&(r||(r=new M(l),u.push(r)),r.textNodes.push(h)),u.length){for(i=0,len=u.length;len>i;++i)u[i].doMerge(n);t.setStartAndEnd(f,p,c,d)}},createContainer:function(e){var t,n=$.getDocument(e),s=k&&!$.isHtmlNamespace(e)&&(t=e.namespaceURI)?n.createElementNS(e.namespaceURI,this.elementTagName):n.createElement(this.elementTagName);return this.copyPropertiesToElement(this.elementProperties,s,!1),this.copyAttributesToElement(this.elementAttributes,s),a(s,this.className),this.onElementCreate&&this.onElementCreate(s,this),s},elementHasProperties:function(e,t){var s=this;return n(t,function(t,n){if("className"==t)return p(e,n);if("object"==typeof n){if(!s.elementHasProperties(e[t],n))return!1}else if(e[t]!==n)return!1})},elementHasAttributes:function(e,t){return n(t,function(t,n){return e.getAttribute(t)!==n?!1:void 0})},applyToTextNode:function(e){if(d(e)){var t=e.parentNode;if(1==t.childNodes.length&&this.useExistingElements&&this.appliesToElement(t)&&this.elementHasProperties(t,this.elementProperties)&&this.elementHasAttributes(t,this.elementAttributes))a(t,this.className);else{var n=e.parentNode,s=this.createContainer(n);n.insertBefore(s,e),s.appendChild(e)}}},isRemovable:function(e){return e.tagName.toLowerCase()==this.elementTagName&&f(e)==this.elementSortedClassName&&this.elementHasProperties(e,this.elementProperties)&&!b(e,this.attrExceptions)&&this.elementHasAttributes(e,this.elementAttributes)&&this.isModifiable(e)},isEmptyContainer:function(e){var t=e.childNodes.length;return 1==e.nodeType&&this.isRemovable(e)&&(0==t||1==t&&this.isEmptyContainer(e.firstChild))},removeEmptyContainers:function(e){var t=this,n=e.getNodes([1],function(e){return t.isEmptyContainer(e)}),s=[e],r=P(s);U(n,function(e){N(e,r)}),w(s,r)},undoToTextNode:function(e,t,n,s){if(!t.containsNode(n)){var r=t.cloneRange();r.selectNode(n),r.isPointInRange(t.endContainer,t.endOffset)&&(I(n,t.endContainer,t.endOffset,s),t.setEndAfter(n)),r.isPointInRange(t.startContainer,t.startOffset)&&(n=I(n,t.startContainer,t.startOffset,s))}this.isRemovable(n)?y(n,s):q(n,this.className)},splitAncestorWithClass:function(e,t,n){var s=this.getSelfOrAncestorWithClass(e);s&&I(s,e,t,n)},undoToAncestor:function(e,t){this.isRemovable(e)?y(e,t):q(e,this.className)},applyToRange:function(e,t){var n=this;t=t||[];var s=P(t||[]);e.splitBoundariesPreservingPositions(s),n.removeEmptyElements&&n.removeEmptyContainers(e);var r=T(e);if(r.length){U(r,function(e){n.isIgnorableWhiteSpaceNode(e)||n.getSelfOrAncestorWithClass(e)||!n.isModifiable(e)||n.applyToTextNode(e,s)});var i=r[r.length-1];e.setStartAndEnd(r[0],0,i,i.length),n.normalize&&n.postApply(r,e,s,!1),w(t,s)}var o=n.getEmptyElements(e);U(o,function(e){a(e,n.className)})},applyToRanges:function(e){for(var t=e.length;t--;)this.applyToRange(e[t],e);return e},applyToSelection:function(t){var n=e.getSelection(t);n.setRanges(this.applyToRanges(n.getAllRanges()))},undoToRange:function(e,t){var n=this;t=t||[];var s=P(t);e.splitBoundariesPreservingPositions(s),n.removeEmptyElements&&n.removeEmptyContainers(e,s);var r,i,o=T(e),a=o[o.length-1];if(o.length){n.splitAncestorWithClass(e.endContainer,e.endOffset,s),n.splitAncestorWithClass(e.startContainer,e.startOffset,s);for(var l=0,u=o.length;u>l;++l)r=o[l],i=n.getSelfOrAncestorWithClass(r),i&&n.isModifiable(r)&&n.undoToAncestor(i,s);e.setStartAndEnd(o[0],0,a,a.length),n.normalize&&n.postApply(o,e,s,!0),w(t,s)}var f=n.getEmptyElements(e);U(f,function(e){q(e,n.className)})},undoToRanges:function(e){for(var t=e.length;t--;)this.undoToRange(e[t],e);return e},undoToSelection:function(t){var n=e.getSelection(t),s=e.getSelection(t).getAllRanges();this.undoToRanges(s),n.setRanges(s)},isAppliedToRange:function(e){if(e.collapsed||""==e.toString())return!!this.getSelfOrAncestorWithClass(e.commonAncestorContainer);var t=e.getNodes([3]);if(t.length)for(var n,s=0;n=t[s++];)if(!this.isIgnorableWhiteSpaceNode(n)&&C(e,n)&&this.isModifiable(n)&&!this.getSelfOrAncestorWithClass(n))return!1;return!0},isAppliedToRanges:function(e){var t=e.length;if(0==t)return!1;for(;t--;)if(!this.isAppliedToRange(e[t]))return!1;return!0},isAppliedToSelection:function(t){var n=e.getSelection(t);return this.isAppliedToRanges(n.getAllRanges())},toggleRange:function(e){this.isAppliedToRange(e)?this.undoToRange(e):this.applyToRange(e)},toggleSelection:function(e){this.isAppliedToSelection(e)?this.undoToSelection(e):this.applyToSelection(e)},getElementsWithClassIntersectingRange:function(e){var t=[],n=this;return e.getNodes([3],function(e){var s=n.getSelfOrAncestorWithClass(e);s&&!B(t,s)&&t.push(s)}),t},detach:function(){}},H.util={hasClass:o,addClass:a,removeClass:q,getClass:l,hasSameClasses:c,hasAllClasses:p,replaceWithOwnChildren:y,elementsHaveSameNonClassAttributes:E,elementHasNonClassAttributes:b,splitNodeAt:I,isEditableElement:G,isEditingHost:A,isEditable:S},e.CssClassApplier=e.ClassApplier=H,e.createClassApplier=j,D.createAliasForDeprecatedMethod(e,"createCssClassApplier","createClassApplier",t)}),e},this); \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/rangy-core.js b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-core.js
new file mode 100755
index 000000000..ea65b24d0
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-core.js
@@ -0,0 +1,11 @@
+/**
+ * Rangy, a cross-browser JavaScript range and selection library
+ * https://github.com/timdown/rangy
+ *
+ * Copyright 2015, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.3.0
+ * Build date: 10 May 2015
+ */
+!function(e,t){"function"==typeof define&&define.amd?define(e):"undefined"!=typeof module&&"object"==typeof exports?module.exports=e():t.rangy=e()}(function(){function e(e,t){var n=typeof e[t];return n==N||!(n!=C||!e[t])||"unknown"==n}function t(e,t){return!(typeof e[t]!=C||!e[t])}function n(e,t){return typeof e[t]!=E}function r(e){return function(t,n){for(var r=n.length;r--;)if(!e(t,n[r]))return!1;return!0}}function o(e){return e&&O(e,T)&&D(e,w)}function i(e){return t(e,"body")?e.body:e.getElementsByTagName("body")[0]}function a(t){typeof console!=E&&e(console,"log")&&console.log(t)}function s(e,t){b&&t?alert(e):a(e)}function c(e){I.initialized=!0,I.supported=!1,s("Rangy is not supported in this environment. Reason: "+e,I.config.alertOnFail)}function d(e){s("Rangy warning: "+e,I.config.alertOnWarn)}function f(e){return e.message||e.description||String(e)}function u(){if(b&&!I.initialized){var t,n=!1,r=!1;e(document,"createRange")&&(t=document.createRange(),O(t,y)&&D(t,S)&&(n=!0));var s=i(document);if(!s||"body"!=s.nodeName.toLowerCase())return void c("No body element found");if(s&&e(s,"createTextRange")&&(t=s.createTextRange(),o(t)&&(r=!0)),!n&&!r)return void c("Neither Range nor TextRange are available");I.initialized=!0,I.features={implementsDomRange:n,implementsTextRange:r};var d,u;for(var l in x)(d=x[l])instanceof p&&d.init(d,I);for(var h=0,g=M.length;g>h;++h)try{M[h](I)}catch(m){u="Rangy init listener threw an exception. Continuing. Detail: "+f(m),a(u)}}}function l(e,t,n){n&&(e+=" in module "+n.name),I.warn("DEPRECATED: "+e+" is deprecated. Please use "+t+" instead.")}function h(e,t,n,r){e[t]=function(){return l(t,n,r),e[n].apply(e,P.toArray(arguments))}}function g(e){e=e||window,u();for(var t=0,n=k.length;n>t;++t)k[t](e)}function p(e,t,n){this.name=e,this.dependencies=t,this.initialized=!1,this.supported=!1,this.initializer=n}function m(e,t,n){var r=new p(e,t,function(t){if(!t.initialized){t.initialized=!0;try{n(I,t),t.supported=!0}catch(r){var o="Module '"+e+"' failed to load: "+f(r);a(o),r.stack&&a(r.stack)}}});return x[e]=r,r}function R(){}function v(){}var C="object",N="function",E="undefined",S=["startContainer","startOffset","endContainer","endOffset","collapsed","commonAncestorContainer"],y=["setStart","setStartBefore","setStartAfter","setEnd","setEndBefore","setEndAfter","collapse","selectNode","selectNodeContents","compareBoundaryPoints","deleteContents","extractContents","cloneContents","insertNode","surroundContents","cloneRange","toString","detach"],w=["boundingHeight","boundingLeft","boundingTop","boundingWidth","htmlText","text"],T=["collapse","compareEndPoints","duplicate","moveToElementText","parentElement","select","setEndPoint","getBoundingClientRect"],O=r(e),_=r(t),D=r(n),A=[].forEach?function(e,t){e.forEach(t)}:function(e,t){for(var n=0,r=e.length;r>n;++n)t(e[n],n)},x={},b=typeof window!=E&&typeof document!=E,P={isHostMethod:e,isHostObject:t,isHostProperty:n,areHostMethods:O,areHostObjects:_,areHostProperties:D,isTextRange:o,getBody:i,forEach:A},I={version:"1.3.0",initialized:!1,isBrowser:b,supported:!0,util:P,features:{},modules:x,config:{alertOnFail:!1,alertOnWarn:!1,preferTextRange:!1,autoInitialize:typeof rangyAutoInitialize==E?!0:rangyAutoInitialize}};I.fail=c,I.warn=d;var B;({}).hasOwnProperty?(P.extend=B=function(e,t,n){var r,o;for(var i in t)t.hasOwnProperty(i)&&(r=e[i],o=t[i],n&&null!==r&&"object"==typeof r&&null!==o&&"object"==typeof o&&B(r,o,!0),e[i]=o);return t.hasOwnProperty("toString")&&(e.toString=t.toString),e},P.createOptions=function(e,t){var n={};return B(n,t),e&&B(n,e),n}):c("hasOwnProperty not supported"),b||c("Rangy can only run in a browser"),function(){var e;if(b){var t=document.createElement("div");t.appendChild(document.createElement("span"));var n=[].slice;try{1==n.call(t.childNodes,0)[0].nodeType&&(e=function(e){return n.call(e,0)})}catch(r){}}e||(e=function(e){for(var t=[],n=0,r=e.length;r>n;++n)t[n]=e[n];return t}),P.toArray=e}();var H;b&&(e(document,"addEventListener")?H=function(e,t,n){e.addEventListener(t,n,!1)}:e(document,"attachEvent")?H=function(e,t,n){e.attachEvent("on"+t,n)}:c("Document does not have required addEventListener or attachEvent method"),P.addListener=H);var M=[];P.deprecationNotice=l,P.createAliasForDeprecatedMethod=h,I.init=u,I.addInitListener=function(e){I.initialized?e(I):M.push(e)};var k=[];I.addShimListener=function(e){k.push(e)},b&&(I.shim=I.createMissingNativeApi=g,h(I,"createMissingNativeApi","shim")),p.prototype={init:function(){for(var e,t,n=this.dependencies||[],r=0,o=n.length;o>r;++r){if(t=n[r],e=x[t],!(e&&e instanceof p))throw new Error("required module '"+t+"' not found");if(e.init(),!e.supported)throw new Error("required module '"+t+"' not supported")}this.initializer(this)},fail:function(e){throw this.initialized=!0,this.supported=!1,new Error(e)},warn:function(e){I.warn("Module "+this.name+": "+e)},deprecationNotice:function(e,t){I.warn("DEPRECATED: "+e+" in module "+this.name+" is deprecated. Please use "+t+" instead")},createError:function(e){return new Error("Error in Rangy "+this.name+" module: "+e)}},I.createModule=function(e){var t,n;2==arguments.length?(t=arguments[1],n=[]):(t=arguments[2],n=arguments[1]);var r=m(e,n,t);I.initialized&&I.supported&&r.init()},I.createCoreModule=function(e,t,n){m(e,t,n)},I.RangePrototype=R,I.rangePrototype=new R,I.selectionPrototype=new v,I.createCoreModule("DomUtil",[],function(e,t){function n(e){var t;return typeof e.namespaceURI==b||null===(t=e.namespaceURI)||"http://www.w3.org/1999/xhtml"==t}function r(e){var t=e.parentNode;return 1==t.nodeType?t:null}function o(e){for(var t=0;e=e.previousSibling;)++t;return t}function i(e){switch(e.nodeType){case 7:case 10:return 0;case 3:case 8:return e.length;default:return e.childNodes.length}}function a(e,t){var n,r=[];for(n=e;n;n=n.parentNode)r.push(n);for(n=t;n;n=n.parentNode)if(M(r,n))return n;return null}function s(e,t,n){for(var r=n?t:t.parentNode;r;){if(r===e)return!0;r=r.parentNode}return!1}function c(e,t){return s(e,t,!0)}function d(e,t,n){for(var r,o=n?e:e.parentNode;o;){if(r=o.parentNode,r===t)return o;o=r}return null}function f(e){var t=e.nodeType;return 3==t||4==t||8==t}function u(e){if(!e)return!1;var t=e.nodeType;return 3==t||8==t}function l(e,t){var n=t.nextSibling,r=t.parentNode;return n?r.insertBefore(e,n):r.appendChild(e),e}function h(e,t,n){var r=e.cloneNode(!1);if(r.deleteData(0,t),e.deleteData(t,e.length-t),l(r,e),n)for(var i,a=0;i=n[a++];)i.node==e&&i.offset>t?(i.node=r,i.offset-=t):i.node==e.parentNode&&i.offset>o(e)&&++i.offset;return r}function g(e){if(9==e.nodeType)return e;if(typeof e.ownerDocument!=b)return e.ownerDocument;if(typeof e.document!=b)return e.document;if(e.parentNode)return g(e.parentNode);throw t.createError("getDocument: no document found for node")}function p(e){var n=g(e);if(typeof n.defaultView!=b)return n.defaultView;if(typeof n.parentWindow!=b)return n.parentWindow;throw t.createError("Cannot get a window object for node")}function m(e){if(typeof e.contentDocument!=b)return e.contentDocument;if(typeof e.contentWindow!=b)return e.contentWindow.document;throw t.createError("getIframeDocument: No Document object found for iframe element")}function R(e){if(typeof e.contentWindow!=b)return e.contentWindow;if(typeof e.contentDocument!=b)return e.contentDocument.defaultView;throw t.createError("getIframeWindow: No Window object found for iframe element")}function v(e){return e&&P.isHostMethod(e,"setTimeout")&&P.isHostObject(e,"document")}function C(e,t,n){var r;if(e?P.isHostProperty(e,"nodeType")?r=1==e.nodeType&&"iframe"==e.tagName.toLowerCase()?m(e):g(e):v(e)&&(r=e.document):r=document,!r)throw t.createError(n+"(): Parameter must be a Window object or DOM node");return r}function N(e){for(var t;t=e.parentNode;)e=t;return e}function E(e,n,r,i){var s,c,f,u,l;if(e==r)return n===i?0:i>n?-1:1;if(s=d(r,e,!0))return n<=o(s)?-1:1;if(s=d(e,r,!0))return o(s)<i?-1:1;if(c=a(e,r),!c)throw new Error("comparePoints error: nodes have no common ancestor");if(f=e===c?c:d(e,c,!0),u=r===c?c:d(r,c,!0),f===u)throw t.createError("comparePoints got to case 4 and childA and childB are the same!");for(l=c.firstChild;l;){if(l===f)return-1;if(l===u)return 1;l=l.nextSibling}}function S(e){var t;try{return t=e.parentNode,!1}catch(n){return!0}}function y(e){if(!e)return"[No node]";if(k&&S(e))return"[Broken node]";if(f(e))return'"'+e.data+'"';if(1==e.nodeType){var t=e.id?' id="'+e.id+'"':"";return"<"+e.nodeName+t+">[index:"+o(e)+",length:"+e.childNodes.length+"]["+(e.innerHTML||"[innerHTML not supported]").slice(0,25)+"]"}return e.nodeName}function w(e){for(var t,n=g(e).createDocumentFragment();t=e.firstChild;)n.appendChild(t);return n}function T(e,t,n){var r=I(e),o=e.createElement("div");o.contentEditable=""+!!n,t&&(o.innerHTML=t);var i=r.firstChild;return i?r.insertBefore(o,i):r.appendChild(o),o}function O(e){return e.parentNode.removeChild(e)}function _(e){this.root=e,this._next=e}function D(e){return new _(e)}function A(e,t){this.node=e,this.offset=t}function x(e){this.code=this[e],this.codeName=e,this.message="DOMException: "+this.codeName}var b="undefined",P=e.util,I=P.getBody;P.areHostMethods(document,["createDocumentFragment","createElement","createTextNode"])||t.fail("document missing a Node creation method"),P.isHostMethod(document,"getElementsByTagName")||t.fail("document missing getElementsByTagName method");var B=document.createElement("div");P.areHostMethods(B,["insertBefore","appendChild","cloneNode"]||!P.areHostObjects(B,["previousSibling","nextSibling","childNodes","parentNode"]))||t.fail("Incomplete Element implementation"),P.isHostProperty(B,"innerHTML")||t.fail("Element is missing innerHTML property");var H=document.createTextNode("test");P.areHostMethods(H,["splitText","deleteData","insertData","appendData","cloneNode"]||!P.areHostObjects(B,["previousSibling","nextSibling","childNodes","parentNode"])||!P.areHostProperties(H,["data"]))||t.fail("Incomplete Text Node implementation");var M=function(e,t){for(var n=e.length;n--;)if(e[n]===t)return!0;return!1},k=!1;!function(){var t=document.createElement("b");t.innerHTML="1";var n=t.firstChild;t.innerHTML="<br />",k=S(n),e.features.crashyTextNodes=k}();var L;typeof window.getComputedStyle!=b?L=function(e,t){return p(e).getComputedStyle(e,null)[t]}:typeof document.documentElement.currentStyle!=b?L=function(e,t){return e.currentStyle?e.currentStyle[t]:""}:t.fail("No means of obtaining computed style properties found"),_.prototype={_current:null,hasNext:function(){return!!this._next},next:function(){var e,t,n=this._current=this._next;if(this._current)if(e=n.firstChild)this._next=e;else{for(t=null;n!==this.root&&!(t=n.nextSibling);)n=n.parentNode;this._next=t}return this._current},detach:function(){this._current=this._next=this.root=null}},A.prototype={equals:function(e){return!!e&&this.node===e.node&&this.offset==e.offset},inspect:function(){return"[DomPosition("+y(this.node)+":"+this.offset+")]"},toString:function(){return this.inspect()}},x.prototype={INDEX_SIZE_ERR:1,HIERARCHY_REQUEST_ERR:3,WRONG_DOCUMENT_ERR:4,NO_MODIFICATION_ALLOWED_ERR:7,NOT_FOUND_ERR:8,NOT_SUPPORTED_ERR:9,INVALID_STATE_ERR:11,INVALID_NODE_TYPE_ERR:24},x.prototype.toString=function(){return this.message},e.dom={arrayContains:M,isHtmlNamespace:n,parentElement:r,getNodeIndex:o,getNodeLength:i,getCommonAncestor:a,isAncestorOf:s,isOrIsAncestorOf:c,getClosestAncestorIn:d,isCharacterDataNode:f,isTextOrCommentNode:u,insertAfter:l,splitDataNode:h,getDocument:g,getWindow:p,getIframeWindow:R,getIframeDocument:m,getBody:I,isWindow:v,getContentDocument:C,getRootContainer:N,comparePoints:E,isBrokenNode:S,inspectNode:y,getComputedStyleProperty:L,createTestElement:T,removeNode:O,fragmentFromNodeChildren:w,createIterator:D,DomPosition:A},e.DOMException=x}),I.createCoreModule("DomRange",["DomUtil"],function(e){function t(e,t){return 3!=e.nodeType&&(F(e,t.startContainer)||F(e,t.endContainer))}function n(e){return e.document||j(e.startContainer)}function r(e){return Q(e.startContainer)}function o(e){return new M(e.parentNode,W(e))}function i(e){return new M(e.parentNode,W(e)+1)}function a(e,t,n){var r=11==e.nodeType?e.firstChild:e;return L(t)?n==t.length?B.insertAfter(e,t):t.parentNode.insertBefore(e,0==n?t:U(t,n)):n>=t.childNodes.length?t.appendChild(e):t.insertBefore(e,t.childNodes[n]),r}function s(e,t,r){if(w(e),w(t),n(t)!=n(e))throw new k("WRONG_DOCUMENT_ERR");var o=z(e.startContainer,e.startOffset,t.endContainer,t.endOffset),i=z(e.endContainer,e.endOffset,t.startContainer,t.startOffset);return r?0>=o&&i>=0:0>o&&i>0}function c(e){for(var t,r,o,i=n(e.range).createDocumentFragment();r=e.next();){if(t=e.isPartiallySelectedSubtree(),r=r.cloneNode(!t),t&&(o=e.getSubtreeIterator(),r.appendChild(c(o)),o.detach()),10==r.nodeType)throw new k("HIERARCHY_REQUEST_ERR");i.appendChild(r)}return i}function d(e,t,n){var r,o;n=n||{stop:!1};for(var i,a;i=e.next();)if(e.isPartiallySelectedSubtree()){if(t(i)===!1)return void(n.stop=!0);if(a=e.getSubtreeIterator(),d(a,t,n),a.detach(),n.stop)return}else for(r=B.createIterator(i);o=r.next();)if(t(o)===!1)return void(n.stop=!0)}function f(e){for(var t;e.next();)e.isPartiallySelectedSubtree()?(t=e.getSubtreeIterator(),f(t),t.detach()):e.remove()}function u(e){for(var t,r,o=n(e.range).createDocumentFragment();t=e.next();){if(e.isPartiallySelectedSubtree()?(t=t.cloneNode(!1),r=e.getSubtreeIterator(),t.appendChild(u(r)),r.detach()):e.remove(),10==t.nodeType)throw new k("HIERARCHY_REQUEST_ERR");o.appendChild(t)}return o}function l(e,t,n){var r,o=!(!t||!t.length),i=!!n;o&&(r=new RegExp("^("+t.join("|")+")$"));var a=[];return d(new g(e,!1),function(t){if(!(o&&!r.test(t.nodeType)||i&&!n(t))){var s=e.startContainer;if(t!=s||!L(s)||e.startOffset!=s.length){var c=e.endContainer;t==c&&L(c)&&0==e.endOffset||a.push(t)}}}),a}function h(e){var t="undefined"==typeof e.getName?"Range":e.getName();return"["+t+"("+B.inspectNode(e.startContainer)+":"+e.startOffset+", "+B.inspectNode(e.endContainer)+":"+e.endOffset+")]"}function g(e,t){if(this.range=e,this.clonePartiallySelectedTextNodes=t,!e.collapsed){this.sc=e.startContainer,this.so=e.startOffset,this.ec=e.endContainer,this.eo=e.endOffset;var n=e.commonAncestorContainer;this.sc===this.ec&&L(this.sc)?(this.isSingleCharacterDataNode=!0,this._first=this._last=this._next=this.sc):(this._first=this._next=this.sc!==n||L(this.sc)?V(this.sc,n,!0):this.sc.childNodes[this.so],this._last=this.ec!==n||L(this.ec)?V(this.ec,n,!0):this.ec.childNodes[this.eo-1])}}function p(e){return function(t,n){for(var r,o=n?t:t.parentNode;o;){if(r=o.nodeType,Y(e,r))return o;o=o.parentNode}return null}}function m(e,t){if(rt(e,t))throw new k("INVALID_NODE_TYPE_ERR")}function R(e,t){if(!Y(t,e.nodeType))throw new k("INVALID_NODE_TYPE_ERR")}function v(e,t){if(0>t||t>(L(e)?e.length:e.childNodes.length))throw new k("INDEX_SIZE_ERR")}function C(e,t){if(tt(e,!0)!==tt(t,!0))throw new k("WRONG_DOCUMENT_ERR")}function N(e){if(nt(e,!0))throw new k("NO_MODIFICATION_ALLOWED_ERR")}function E(e,t){if(!e)throw new k(t)}function S(e,t){return t<=(L(e)?e.length:e.childNodes.length)}function y(e){return!!e.startContainer&&!!e.endContainer&&!(G&&(B.isBrokenNode(e.startContainer)||B.isBrokenNode(e.endContainer)))&&Q(e.startContainer)==Q(e.endContainer)&&S(e.startContainer,e.startOffset)&&S(e.endContainer,e.endOffset)}function w(e){if(!y(e))throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: ("+e.inspect()+")")}function T(e,t){w(e);var n=e.startContainer,r=e.startOffset,o=e.endContainer,i=e.endOffset,a=n===o;L(o)&&i>0&&i<o.length&&U(o,i,t),L(n)&&r>0&&r<n.length&&(n=U(n,r,t),a?(i-=r,o=n):o==n.parentNode&&i>=W(n)&&i++,r=0),e.setStartAndEnd(n,r,o,i)}function O(e){w(e);var t=e.commonAncestorContainer.parentNode.cloneNode(!1);return t.appendChild(e.cloneContents()),t.innerHTML}function _(e){e.START_TO_START=dt,e.START_TO_END=ft,e.END_TO_END=ut,e.END_TO_START=lt,e.NODE_BEFORE=ht,e.NODE_AFTER=gt,e.NODE_BEFORE_AND_AFTER=pt,e.NODE_INSIDE=mt}function D(e){_(e),_(e.prototype)}function A(e,t){return function(){w(this);var n,r,o=this.startContainer,a=this.startOffset,s=this.commonAncestorContainer,c=new g(this,!0);o!==s&&(n=V(o,s,!0),r=i(n),o=r.node,a=r.offset),d(c,N),c.reset();var f=e(c);return c.detach(),t(this,o,a,o,a),f}}function x(n,r){function a(e,t){return function(n){R(n,Z),R(Q(n),$);var r=(e?o:i)(n);(t?s:c)(this,r.node,r.offset)}}function s(e,t,n){var o=e.endContainer,i=e.endOffset;(t!==e.startContainer||n!==e.startOffset)&&((Q(t)!=Q(o)||1==z(t,n,o,i))&&(o=t,i=n),r(e,t,n,o,i))}function c(e,t,n){var o=e.startContainer,i=e.startOffset;(t!==e.endContainer||n!==e.endOffset)&&((Q(t)!=Q(o)||-1==z(t,n,o,i))&&(o=t,i=n),r(e,o,i,t,n))}var d=function(){};d.prototype=e.rangePrototype,n.prototype=new d,H.extend(n.prototype,{setStart:function(e,t){m(e,!0),v(e,t),s(this,e,t)},setEnd:function(e,t){m(e,!0),v(e,t),c(this,e,t)},setStartAndEnd:function(){var e=arguments,t=e[0],n=e[1],o=t,i=n;switch(e.length){case 3:i=e[2];break;case 4:o=e[2],i=e[3]}r(this,t,n,o,i)},setBoundary:function(e,t,n){this["set"+(n?"Start":"End")](e,t)},setStartBefore:a(!0,!0),setStartAfter:a(!1,!0),setEndBefore:a(!0,!1),setEndAfter:a(!1,!1),collapse:function(e){w(this),e?r(this,this.startContainer,this.startOffset,this.startContainer,this.startOffset):r(this,this.endContainer,this.endOffset,this.endContainer,this.endOffset)},selectNodeContents:function(e){m(e,!0),r(this,e,0,e,q(e))},selectNode:function(e){m(e,!1),R(e,Z);var t=o(e),n=i(e);r(this,t.node,t.offset,n.node,n.offset)},extractContents:A(u,r),deleteContents:A(f,r),canSurroundContents:function(){w(this),N(this.startContainer),N(this.endContainer);var e=new g(this,!0),n=e._first&&t(e._first,this)||e._last&&t(e._last,this);return e.detach(),!n},splitBoundaries:function(){T(this)},splitBoundariesPreservingPositions:function(e){T(this,e)},normalizeBoundaries:function(){w(this);var e,t=this.startContainer,n=this.startOffset,o=this.endContainer,i=this.endOffset,a=function(e){var t=e.nextSibling;t&&t.nodeType==e.nodeType&&(o=e,i=e.length,e.appendData(t.data),X(t))},s=function(e){var r=e.previousSibling;if(r&&r.nodeType==e.nodeType){t=e;var a=e.length;if(n=r.length,e.insertData(0,r.data),X(r),t==o)i+=n,o=t;else if(o==e.parentNode){var s=W(e);i==s?(o=e,i=a):i>s&&i--}}},c=!0;if(L(o))i==o.length?a(o):0==i&&(e=o.previousSibling,e&&e.nodeType==o.nodeType&&(i=e.length,t==o&&(c=!1),e.appendData(o.data),X(o),o=e));else{if(i>0){var d=o.childNodes[i-1];d&&L(d)&&a(d)}c=!this.collapsed}if(c){if(L(t))0==n?s(t):n==t.length&&(e=t.nextSibling,e&&e.nodeType==t.nodeType&&(o==e&&(o=t,i+=t.length),t.appendData(e.data),X(e)));else if(n<t.childNodes.length){var f=t.childNodes[n];f&&L(f)&&s(f)}}else t=o,n=i;r(this,t,n,o,i)},collapseToPoint:function(e,t){m(e,!0),v(e,t),this.setStartAndEnd(e,t)}}),D(n)}function b(e){e.collapsed=e.startContainer===e.endContainer&&e.startOffset===e.endOffset,e.commonAncestorContainer=e.collapsed?e.startContainer:B.getCommonAncestor(e.startContainer,e.endContainer)}function P(e,t,n,r,o){e.startContainer=t,e.startOffset=n,e.endContainer=r,e.endOffset=o,e.document=B.getDocument(t),b(e)}function I(e){this.startContainer=e,this.startOffset=0,this.endContainer=e,this.endOffset=0,this.document=e,b(this)}var B=e.dom,H=e.util,M=B.DomPosition,k=e.DOMException,L=B.isCharacterDataNode,W=B.getNodeIndex,F=B.isOrIsAncestorOf,j=B.getDocument,z=B.comparePoints,U=B.splitDataNode,V=B.getClosestAncestorIn,q=B.getNodeLength,Y=B.arrayContains,Q=B.getRootContainer,G=e.features.crashyTextNodes,X=B.removeNode;g.prototype={_current:null,_next:null,_first:null,_last:null,isSingleCharacterDataNode:!1,reset:function(){this._current=null,this._next=this._first},hasNext:function(){return!!this._next},next:function(){var e=this._current=this._next;return e&&(this._next=e!==this._last?e.nextSibling:null,L(e)&&this.clonePartiallySelectedTextNodes&&(e===this.ec&&(e=e.cloneNode(!0)).deleteData(this.eo,e.length-this.eo),this._current===this.sc&&(e=e.cloneNode(!0)).deleteData(0,this.so))),e},remove:function(){var e,t,n=this._current;!L(n)||n!==this.sc&&n!==this.ec?n.parentNode&&X(n):(e=n===this.sc?this.so:0,t=n===this.ec?this.eo:n.length,e!=t&&n.deleteData(e,t-e))},isPartiallySelectedSubtree:function(){var e=this._current;return t(e,this.range)},getSubtreeIterator:function(){var e;if(this.isSingleCharacterDataNode)e=this.range.cloneRange(),e.collapse(!1);else{e=new I(n(this.range));var t=this._current,r=t,o=0,i=t,a=q(t);F(t,this.sc)&&(r=this.sc,o=this.so),F(t,this.ec)&&(i=this.ec,a=this.eo),P(e,r,o,i,a)}return new g(e,this.clonePartiallySelectedTextNodes)},detach:function(){this.range=this._current=this._next=this._first=this._last=this.sc=this.so=this.ec=this.eo=null}};var Z=[1,3,4,5,7,8,10],$=[2,9,11],J=[5,6,10,12],K=[1,3,4,5,7,8,10,11],et=[1,3,4,5,7,8],tt=p([9,11]),nt=p(J),rt=p([6,10,12]),ot=document.createElement("style"),it=!1;try{ot.innerHTML="<b>x</b>",it=3==ot.firstChild.nodeType}catch(at){}e.features.htmlParsingConforms=it;var st=it?function(e){var t=this.startContainer,n=j(t);if(!t)throw new k("INVALID_STATE_ERR");var r=null;return 1==t.nodeType?r=t:L(t)&&(r=B.parentElement(t)),r=null===r||"HTML"==r.nodeName&&B.isHtmlNamespace(j(r).documentElement)&&B.isHtmlNamespace(r)?n.createElement("body"):r.cloneNode(!1),r.innerHTML=e,B.fragmentFromNodeChildren(r)}:function(e){var t=n(this),r=t.createElement("body");return r.innerHTML=e,B.fragmentFromNodeChildren(r)},ct=["startContainer","startOffset","endContainer","endOffset","collapsed","commonAncestorContainer"],dt=0,ft=1,ut=2,lt=3,ht=0,gt=1,pt=2,mt=3;H.extend(e.rangePrototype,{compareBoundaryPoints:function(e,t){w(this),C(this.startContainer,t.startContainer);var n,r,o,i,a=e==lt||e==dt?"start":"end",s=e==ft||e==dt?"start":"end";return n=this[a+"Container"],r=this[a+"Offset"],o=t[s+"Container"],i=t[s+"Offset"],z(n,r,o,i)},insertNode:function(e){if(w(this),R(e,K),N(this.startContainer),F(e,this.startContainer))throw new k("HIERARCHY_REQUEST_ERR");var t=a(e,this.startContainer,this.startOffset);this.setStartBefore(t)},cloneContents:function(){w(this);var e,t;if(this.collapsed)return n(this).createDocumentFragment();if(this.startContainer===this.endContainer&&L(this.startContainer))return e=this.startContainer.cloneNode(!0),e.data=e.data.slice(this.startOffset,this.endOffset),t=n(this).createDocumentFragment(),t.appendChild(e),t;var r=new g(this,!0);return e=c(r),r.detach(),e},canSurroundContents:function(){w(this),N(this.startContainer),N(this.endContainer);var e=new g(this,!0),n=e._first&&t(e._first,this)||e._last&&t(e._last,this);return e.detach(),!n},surroundContents:function(e){if(R(e,et),!this.canSurroundContents())throw new k("INVALID_STATE_ERR");var t=this.extractContents();if(e.hasChildNodes())for(;e.lastChild;)e.removeChild(e.lastChild);a(e,this.startContainer,this.startOffset),e.appendChild(t),this.selectNode(e)},cloneRange:function(){w(this);for(var e,t=new I(n(this)),r=ct.length;r--;)e=ct[r],t[e]=this[e];return t},toString:function(){w(this);var e=this.startContainer;if(e===this.endContainer&&L(e))return 3==e.nodeType||4==e.nodeType?e.data.slice(this.startOffset,this.endOffset):"";var t=[],n=new g(this,!0);return d(n,function(e){(3==e.nodeType||4==e.nodeType)&&t.push(e.data)}),n.detach(),t.join("")},compareNode:function(e){w(this);var t=e.parentNode,n=W(e);if(!t)throw new k("NOT_FOUND_ERR");var r=this.comparePoint(t,n),o=this.comparePoint(t,n+1);return 0>r?o>0?pt:ht:o>0?gt:mt},comparePoint:function(e,t){return w(this),E(e,"HIERARCHY_REQUEST_ERR"),C(e,this.startContainer),z(e,t,this.startContainer,this.startOffset)<0?-1:z(e,t,this.endContainer,this.endOffset)>0?1:0},createContextualFragment:st,toHtml:function(){return O(this)},intersectsNode:function(e,t){if(w(this),Q(e)!=r(this))return!1;var n=e.parentNode,o=W(e);if(!n)return!0;var i=z(n,o,this.endContainer,this.endOffset),a=z(n,o+1,this.startContainer,this.startOffset);return t?0>=i&&a>=0:0>i&&a>0},isPointInRange:function(e,t){return w(this),E(e,"HIERARCHY_REQUEST_ERR"),C(e,this.startContainer),z(e,t,this.startContainer,this.startOffset)>=0&&z(e,t,this.endContainer,this.endOffset)<=0},intersectsRange:function(e){return s(this,e,!1)},intersectsOrTouchesRange:function(e){return s(this,e,!0)},intersection:function(e){if(this.intersectsRange(e)){var t=z(this.startContainer,this.startOffset,e.startContainer,e.startOffset),n=z(this.endContainer,this.endOffset,e.endContainer,e.endOffset),r=this.cloneRange();return-1==t&&r.setStart(e.startContainer,e.startOffset),1==n&&r.setEnd(e.endContainer,e.endOffset),r}return null},union:function(e){if(this.intersectsOrTouchesRange(e)){var t=this.cloneRange();return-1==z(e.startContainer,e.startOffset,this.startContainer,this.startOffset)&&t.setStart(e.startContainer,e.startOffset),1==z(e.endContainer,e.endOffset,this.endContainer,this.endOffset)&&t.setEnd(e.endContainer,e.endOffset),t}throw new k("Ranges do not intersect")},containsNode:function(e,t){return t?this.intersectsNode(e,!1):this.compareNode(e)==mt},containsNodeContents:function(e){return this.comparePoint(e,0)>=0&&this.comparePoint(e,q(e))<=0},containsRange:function(e){var t=this.intersection(e);return null!==t&&e.equals(t)},containsNodeText:function(e){var t=this.cloneRange();t.selectNode(e);var n=t.getNodes([3]);if(n.length>0){t.setStart(n[0],0);var r=n.pop();return t.setEnd(r,r.length),this.containsRange(t)}return this.containsNodeContents(e)},getNodes:function(e,t){return w(this),l(this,e,t)},getDocument:function(){return n(this)},collapseBefore:function(e){this.setEndBefore(e),this.collapse(!1)},collapseAfter:function(e){this.setStartAfter(e),this.collapse(!0)},getBookmark:function(t){var r=n(this),o=e.createRange(r);t=t||B.getBody(r),o.selectNodeContents(t);var i=this.intersection(o),a=0,s=0;return i&&(o.setEnd(i.startContainer,i.startOffset),a=o.toString().length,s=a+i.toString().length),{start:a,end:s,containerNode:t}},moveToBookmark:function(e){var t=e.containerNode,n=0;this.setStart(t,0),this.collapse(!0);for(var r,o,i,a,s=[t],c=!1,d=!1;!d&&(r=s.pop());)if(3==r.nodeType)o=n+r.length,!c&&e.start>=n&&e.start<=o&&(this.setStart(r,e.start-n),c=!0),c&&e.end>=n&&e.end<=o&&(this.setEnd(r,e.end-n),d=!0),n=o;else for(a=r.childNodes,i=a.length;i--;)s.push(a[i])},getName:function(){return"DomRange"},equals:function(e){return I.rangesEqual(this,e)},isValid:function(){return y(this)},inspect:function(){return h(this)},detach:function(){}}),x(I,P),H.extend(I,{rangeProperties:ct,RangeIterator:g,copyComparisonConstants:D,createPrototypeRange:x,inspect:h,toHtml:O,getRangeDocument:n,rangesEqual:function(e,t){return e.startContainer===t.startContainer&&e.startOffset===t.startOffset&&e.endContainer===t.endContainer&&e.endOffset===t.endOffset}}),e.DomRange=I}),I.createCoreModule("WrappedRange",["DomRange"],function(e,t){var n,r,o=e.dom,i=e.util,a=o.DomPosition,s=e.DomRange,c=o.getBody,d=o.getContentDocument,f=o.isCharacterDataNode;if(e.features.implementsDomRange&&!function(){function r(e){for(var t,n=l.length;n--;)t=l[n],e[t]=e.nativeRange[t];e.collapsed=e.startContainer===e.endContainer&&e.startOffset===e.endOffset}function a(e,t,n,r,o){var i=e.startContainer!==t||e.startOffset!=n,a=e.endContainer!==r||e.endOffset!=o,s=!e.equals(e.nativeRange);(i||a||s)&&(e.setEnd(r,o),e.setStart(t,n))}var f,u,l=s.rangeProperties;n=function(e){if(!e)throw t.createError("WrappedRange: Range must be specified");this.nativeRange=e,r(this)},s.createPrototypeRange(n,a),f=n.prototype,f.selectNode=function(e){this.nativeRange.selectNode(e),r(this)},f.cloneContents=function(){return this.nativeRange.cloneContents()},f.surroundContents=function(e){this.nativeRange.surroundContents(e),r(this)},f.collapse=function(e){this.nativeRange.collapse(e),r(this)},f.cloneRange=function(){return new n(this.nativeRange.cloneRange())},f.refresh=function(){r(this)},f.toString=function(){return this.nativeRange.toString()};var h=document.createTextNode("test");c(document).appendChild(h);var g=document.createRange();g.setStart(h,0),g.setEnd(h,0);try{g.setStart(h,1),f.setStart=function(e,t){this.nativeRange.setStart(e,t),r(this)},f.setEnd=function(e,t){this.nativeRange.setEnd(e,t),r(this)},u=function(e){return function(t){this.nativeRange[e](t),r(this)}}}catch(p){f.setStart=function(e,t){try{this.nativeRange.setStart(e,t)}catch(n){this.nativeRange.setEnd(e,t),this.nativeRange.setStart(e,t)}r(this)},f.setEnd=function(e,t){try{this.nativeRange.setEnd(e,t)}catch(n){this.nativeRange.setStart(e,t),this.nativeRange.setEnd(e,t)}r(this)},u=function(e,t){return function(n){try{this.nativeRange[e](n)}catch(o){this.nativeRange[t](n),this.nativeRange[e](n)}r(this)}}}f.setStartBefore=u("setStartBefore","setEndBefore"),f.setStartAfter=u("setStartAfter","setEndAfter"),f.setEndBefore=u("setEndBefore","setStartBefore"),f.setEndAfter=u("setEndAfter","setStartAfter"),f.selectNodeContents=function(e){this.setStartAndEnd(e,0,o.getNodeLength(e))},g.selectNodeContents(h),g.setEnd(h,3);var m=document.createRange();m.selectNodeContents(h),m.setEnd(h,4),m.setStart(h,2),f.compareBoundaryPoints=-1==g.compareBoundaryPoints(g.START_TO_END,m)&&1==g.compareBoundaryPoints(g.END_TO_START,m)?function(e,t){return t=t.nativeRange||t,e==t.START_TO_END?e=t.END_TO_START:e==t.END_TO_START&&(e=t.START_TO_END),this.nativeRange.compareBoundaryPoints(e,t)}:function(e,t){return this.nativeRange.compareBoundaryPoints(e,t.nativeRange||t)};var R=document.createElement("div");R.innerHTML="123";var v=R.firstChild,C=c(document);C.appendChild(R),g.setStart(v,1),g.setEnd(v,2),g.deleteContents(),"13"==v.data&&(f.deleteContents=function(){this.nativeRange.deleteContents(),r(this)},f.extractContents=function(){var e=this.nativeRange.extractContents();return r(this),e}),C.removeChild(R),C=null,i.isHostMethod(g,"createContextualFragment")&&(f.createContextualFragment=function(e){return this.nativeRange.createContextualFragment(e)}),c(document).removeChild(h),f.getName=function(){return"WrappedRange"},e.WrappedRange=n,e.createNativeRange=function(e){return e=d(e,t,"createNativeRange"),e.createRange()}}(),e.features.implementsTextRange){var u=function(e){var t=e.parentElement(),n=e.duplicate();n.collapse(!0);var r=n.parentElement();n=e.duplicate(),n.collapse(!1);var i=n.parentElement(),a=r==i?r:o.getCommonAncestor(r,i);return a==t?a:o.getCommonAncestor(t,a)},l=function(e){return 0==e.compareEndPoints("StartToEnd",e)},h=function(e,t,n,r,i){var s=e.duplicate();s.collapse(n);var c=s.parentElement();if(o.isOrIsAncestorOf(t,c)||(c=t),!c.canHaveHTML){var d=new a(c.parentNode,o.getNodeIndex(c));return{boundaryPosition:d,nodeInfo:{nodeIndex:d.offset,containerElement:d.node}}}var u=o.getDocument(c).createElement("span");u.parentNode&&o.removeNode(u);for(var l,h,g,p,m,R=n?"StartToStart":"StartToEnd",v=i&&i.containerElement==c?i.nodeIndex:0,C=c.childNodes.length,N=C,E=N;;){if(E==C?c.appendChild(u):c.insertBefore(u,c.childNodes[E]),s.moveToElementText(u),l=s.compareEndPoints(R,e),0==l||v==N)break;if(-1==l){if(N==v+1)break;v=E}else N=N==v+1?v:E;E=Math.floor((v+N)/2),c.removeChild(u)}if(m=u.nextSibling,-1==l&&m&&f(m)){s.setEndPoint(n?"EndToStart":"EndToEnd",e);var S;if(/[\r\n]/.test(m.data)){var y=s.duplicate(),w=y.text.replace(/\r\n/g,"\r").length;for(S=y.moveStart("character",w);-1==(l=y.compareEndPoints("StartToEnd",y));)S++,y.moveStart("character",1)}else S=s.text.length;p=new a(m,S)}else h=(r||!n)&&u.previousSibling,g=(r||n)&&u.nextSibling,p=g&&f(g)?new a(g,0):h&&f(h)?new a(h,h.data.length):new a(c,o.getNodeIndex(u));return o.removeNode(u),{boundaryPosition:p,nodeInfo:{nodeIndex:E,containerElement:c}}},g=function(e,t){var n,r,i,a,s=e.offset,d=o.getDocument(e.node),u=c(d).createTextRange(),l=f(e.node);return l?(n=e.node,r=n.parentNode):(a=e.node.childNodes,n=s<a.length?a[s]:null,r=e.node),i=d.createElement("span"),i.innerHTML="&#feff;",n?r.insertBefore(i,n):r.appendChild(i),u.moveToElementText(i),u.collapse(!t),r.removeChild(i),l&&u[t?"moveStart":"moveEnd"]("character",s),u};r=function(e){this.textRange=e,this.refresh()},r.prototype=new s(document),r.prototype.refresh=function(){var e,t,n,r=u(this.textRange);
+l(this.textRange)?t=e=h(this.textRange,r,!0,!0).boundaryPosition:(n=h(this.textRange,r,!0,!1),e=n.boundaryPosition,t=h(this.textRange,r,!1,!1,n.nodeInfo).boundaryPosition),this.setStart(e.node,e.offset),this.setEnd(t.node,t.offset)},r.prototype.getName=function(){return"WrappedTextRange"},s.copyComparisonConstants(r);var p=function(e){if(e.collapsed)return g(new a(e.startContainer,e.startOffset),!0);var t=g(new a(e.startContainer,e.startOffset),!0),n=g(new a(e.endContainer,e.endOffset),!1),r=c(s.getRangeDocument(e)).createTextRange();return r.setEndPoint("StartToStart",t),r.setEndPoint("EndToEnd",n),r};if(r.rangeToTextRange=p,r.prototype.toTextRange=function(){return p(this)},e.WrappedTextRange=r,!e.features.implementsDomRange||e.config.preferTextRange){var m=function(e){return e("return this;")()}(Function);"undefined"==typeof m.Range&&(m.Range=r),e.createNativeRange=function(e){return e=d(e,t,"createNativeRange"),c(e).createTextRange()},e.WrappedRange=r}}e.createRange=function(n){return n=d(n,t,"createRange"),new e.WrappedRange(e.createNativeRange(n))},e.createRangyRange=function(e){return e=d(e,t,"createRangyRange"),new s(e)},i.createAliasForDeprecatedMethod(e,"createIframeRange","createRange"),i.createAliasForDeprecatedMethod(e,"createIframeRangyRange","createRangyRange"),e.addShimListener(function(t){var n=t.document;"undefined"==typeof n.createRange&&(n.createRange=function(){return e.createRange(n)}),n=t=null})}),I.createCoreModule("WrappedSelection",["DomRange","WrappedRange"],function(e,t){function n(e){return"string"==typeof e?/^backward(s)?$/i.test(e):!!e}function r(e,n){if(e){if(D.isWindow(e))return e;if(e instanceof R)return e.win;var r=D.getContentDocument(e,t,n);return D.getWindow(r)}return window}function o(e){return r(e,"getWinSelection").getSelection()}function i(e){return r(e,"getDocSelection").document.selection}function a(e){var t=!1;return e.anchorNode&&(t=1==D.comparePoints(e.anchorNode,e.anchorOffset,e.focusNode,e.focusOffset)),t}function s(e,t,n){var r=n?"end":"start",o=n?"start":"end";e.anchorNode=t[r+"Container"],e.anchorOffset=t[r+"Offset"],e.focusNode=t[o+"Container"],e.focusOffset=t[o+"Offset"]}function c(e){var t=e.nativeSelection;e.anchorNode=t.anchorNode,e.anchorOffset=t.anchorOffset,e.focusNode=t.focusNode,e.focusOffset=t.focusOffset}function d(e){e.anchorNode=e.focusNode=null,e.anchorOffset=e.focusOffset=0,e.rangeCount=0,e.isCollapsed=!0,e._ranges.length=0}function f(t){var n;return t instanceof b?(n=e.createNativeRange(t.getDocument()),n.setEnd(t.endContainer,t.endOffset),n.setStart(t.startContainer,t.startOffset)):t instanceof P?n=t.nativeRange:H.implementsDomRange&&t instanceof D.getWindow(t.startContainer).Range&&(n=t),n}function u(e){if(!e.length||1!=e[0].nodeType)return!1;for(var t=1,n=e.length;n>t;++t)if(!D.isAncestorOf(e[0],e[t]))return!1;return!0}function l(e){var n=e.getNodes();if(!u(n))throw t.createError("getSingleElementFromRange: range "+e.inspect()+" did not consist of a single element");return n[0]}function h(e){return!!e&&"undefined"!=typeof e.text}function g(e,t){var n=new P(t);e._ranges=[n],s(e,n,!1),e.rangeCount=1,e.isCollapsed=n.collapsed}function p(t){if(t._ranges.length=0,"None"==t.docSelection.type)d(t);else{var n=t.docSelection.createRange();if(h(n))g(t,n);else{t.rangeCount=n.length;for(var r,o=k(n.item(0)),i=0;i<t.rangeCount;++i)r=e.createRange(o),r.selectNode(n.item(i)),t._ranges.push(r);t.isCollapsed=1==t.rangeCount&&t._ranges[0].collapsed,s(t,t._ranges[t.rangeCount-1],!1)}}}function m(e,n){for(var r=e.docSelection.createRange(),o=l(n),i=k(r.item(0)),a=L(i).createControlRange(),s=0,c=r.length;c>s;++s)a.add(r.item(s));try{a.add(o)}catch(d){throw t.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)")}a.select(),p(e)}function R(e,t,n){this.nativeSelection=e,this.docSelection=t,this._ranges=[],this.win=n,this.refresh()}function v(e){e.win=e.anchorNode=e.focusNode=e._ranges=null,e.rangeCount=e.anchorOffset=e.focusOffset=0,e.detached=!0}function C(e,t){for(var n,r,o=tt.length;o--;)if(n=tt[o],r=n.selection,"deleteAll"==t)v(r);else if(n.win==e)return"delete"==t?(tt.splice(o,1),!0):r;return"deleteAll"==t&&(tt.length=0),null}function N(e,n){for(var r,o=k(n[0].startContainer),i=L(o).createControlRange(),a=0,s=n.length;s>a;++a){r=l(n[a]);try{i.add(r)}catch(c){throw t.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)")}}i.select(),p(e)}function E(e,t){if(e.win.document!=k(t))throw new I("WRONG_DOCUMENT_ERR")}function S(t){return function(n,r){var o;this.rangeCount?(o=this.getRangeAt(0),o["set"+(t?"Start":"End")](n,r)):(o=e.createRange(this.win.document),o.setStartAndEnd(n,r)),this.setSingleRange(o,this.isBackward())}}function y(e){var t=[],n=new B(e.anchorNode,e.anchorOffset),r=new B(e.focusNode,e.focusOffset),o="function"==typeof e.getName?e.getName():"Selection";if("undefined"!=typeof e.rangeCount)for(var i=0,a=e.rangeCount;a>i;++i)t[i]=b.inspect(e.getRangeAt(i));return"["+o+"(Ranges: "+t.join(", ")+")(anchor: "+n.inspect()+", focus: "+r.inspect()+"]"}e.config.checkSelectionRanges=!0;var w,T,O="boolean",_="number",D=e.dom,A=e.util,x=A.isHostMethod,b=e.DomRange,P=e.WrappedRange,I=e.DOMException,B=D.DomPosition,H=e.features,M="Control",k=D.getDocument,L=D.getBody,W=b.rangesEqual,F=x(window,"getSelection"),j=A.isHostObject(document,"selection");H.implementsWinGetSelection=F,H.implementsDocSelection=j;var z=j&&(!F||e.config.preferTextRange);if(z)w=i,e.isSelectionValid=function(e){var t=r(e,"isSelectionValid").document,n=t.selection;return"None"!=n.type||k(n.createRange().parentElement())==t};else{if(!F)return t.fail("Neither document.selection or window.getSelection() detected."),!1;w=o,e.isSelectionValid=function(){return!0}}e.getNativeSelection=w;var U=w();if(!U)return t.fail("Native selection was null (possibly issue 138?)"),!1;var V=e.createNativeRange(document),q=L(document),Y=A.areHostProperties(U,["anchorNode","focusNode","anchorOffset","focusOffset"]);H.selectionHasAnchorAndFocus=Y;var Q=x(U,"extend");H.selectionHasExtend=Q;var G=typeof U.rangeCount==_;H.selectionHasRangeCount=G;var X=!1,Z=!0,$=Q?function(t,n){var r=b.getRangeDocument(n),o=e.createRange(r);o.collapseToPoint(n.endContainer,n.endOffset),t.addRange(f(o)),t.extend(n.startContainer,n.startOffset)}:null;A.areHostMethods(U,["addRange","getRangeAt","removeAllRanges"])&&typeof U.rangeCount==_&&H.implementsDomRange&&!function(){var t=window.getSelection();if(t){for(var n=t.rangeCount,r=n>1,o=[],i=a(t),s=0;n>s;++s)o[s]=t.getRangeAt(s);var c=D.createTestElement(document,"",!1),d=c.appendChild(document.createTextNode("   ")),f=document.createRange();if(f.setStart(d,1),f.collapse(!0),t.removeAllRanges(),t.addRange(f),Z=1==t.rangeCount,t.removeAllRanges(),!r){var u=window.navigator.appVersion.match(/Chrome\/(.*?) /);if(u&&parseInt(u[1])>=36)X=!1;else{var l=f.cloneRange();f.setStart(d,0),l.setEnd(d,3),l.setStart(d,2),t.addRange(f),t.addRange(l),X=2==t.rangeCount}}for(D.removeNode(c),t.removeAllRanges(),s=0;n>s;++s)0==s&&i?$?$(t,o[s]):(e.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend"),t.addRange(o[s])):t.addRange(o[s])}}(),H.selectionSupportsMultipleRanges=X,H.collapsedNonEditableSelectionsSupported=Z;var J,K=!1;q&&x(q,"createControlRange")&&(J=q.createControlRange(),A.areHostProperties(J,["item","add"])&&(K=!0)),H.implementsControlRange=K,T=Y?function(e){return e.anchorNode===e.focusNode&&e.anchorOffset===e.focusOffset}:function(e){return e.rangeCount?e.getRangeAt(e.rangeCount-1).collapsed:!1};var et;x(U,"getRangeAt")?et=function(e,t){try{return e.getRangeAt(t)}catch(n){return null}}:Y&&(et=function(t){var n=k(t.anchorNode),r=e.createRange(n);return r.setStartAndEnd(t.anchorNode,t.anchorOffset,t.focusNode,t.focusOffset),r.collapsed!==this.isCollapsed&&r.setStartAndEnd(t.focusNode,t.focusOffset,t.anchorNode,t.anchorOffset),r}),R.prototype=e.selectionPrototype;var tt=[],nt=function(e){if(e&&e instanceof R)return e.refresh(),e;e=r(e,"getNativeSelection");var t=C(e),n=w(e),o=j?i(e):null;return t?(t.nativeSelection=n,t.docSelection=o,t.refresh()):(t=new R(n,o,e),tt.push({win:e,selection:t})),t};e.getSelection=nt,A.createAliasForDeprecatedMethod(e,"getIframeSelection","getSelection");var rt=R.prototype;if(!z&&Y&&A.areHostMethods(U,["removeAllRanges","addRange"])){rt.removeAllRanges=function(){this.nativeSelection.removeAllRanges(),d(this)};var ot=function(e,t){$(e.nativeSelection,t),e.refresh()};rt.addRange=G?function(t,r){if(K&&j&&this.docSelection.type==M)m(this,t);else if(n(r)&&Q)ot(this,t);else{var o;X?o=this.rangeCount:(this.removeAllRanges(),o=0);var i=f(t).cloneRange();try{this.nativeSelection.addRange(i)}catch(a){}if(this.rangeCount=this.nativeSelection.rangeCount,this.rangeCount==o+1){if(e.config.checkSelectionRanges){var c=et(this.nativeSelection,this.rangeCount-1);c&&!W(c,t)&&(t=new P(c))}this._ranges[this.rangeCount-1]=t,s(this,t,st(this.nativeSelection)),this.isCollapsed=T(this)}else this.refresh()}}:function(e,t){n(t)&&Q?ot(this,e):(this.nativeSelection.addRange(f(e)),this.refresh())},rt.setRanges=function(e){if(K&&j&&e.length>1)N(this,e);else{this.removeAllRanges();for(var t=0,n=e.length;n>t;++t)this.addRange(e[t])}}}else{if(!(x(U,"empty")&&x(V,"select")&&K&&z))return t.fail("No means of selecting a Range or TextRange was found"),!1;rt.removeAllRanges=function(){try{if(this.docSelection.empty(),"None"!=this.docSelection.type){var e;if(this.anchorNode)e=k(this.anchorNode);else if(this.docSelection.type==M){var t=this.docSelection.createRange();t.length&&(e=k(t.item(0)))}if(e){var n=L(e).createTextRange();n.select(),this.docSelection.empty()}}}catch(r){}d(this)},rt.addRange=function(t){this.docSelection.type==M?m(this,t):(e.WrappedTextRange.rangeToTextRange(t).select(),this._ranges[0]=t,this.rangeCount=1,this.isCollapsed=this._ranges[0].collapsed,s(this,t,!1))},rt.setRanges=function(e){this.removeAllRanges();var t=e.length;t>1?N(this,e):t&&this.addRange(e[0])}}rt.getRangeAt=function(e){if(0>e||e>=this.rangeCount)throw new I("INDEX_SIZE_ERR");return this._ranges[e].cloneRange()};var it;if(z)it=function(t){var n;e.isSelectionValid(t.win)?n=t.docSelection.createRange():(n=L(t.win.document).createTextRange(),n.collapse(!0)),t.docSelection.type==M?p(t):h(n)?g(t,n):d(t)};else if(x(U,"getRangeAt")&&typeof U.rangeCount==_)it=function(t){if(K&&j&&t.docSelection.type==M)p(t);else if(t._ranges.length=t.rangeCount=t.nativeSelection.rangeCount,t.rangeCount){for(var n=0,r=t.rangeCount;r>n;++n)t._ranges[n]=new e.WrappedRange(t.nativeSelection.getRangeAt(n));s(t,t._ranges[t.rangeCount-1],st(t.nativeSelection)),t.isCollapsed=T(t)}else d(t)};else{if(!Y||typeof U.isCollapsed!=O||typeof V.collapsed!=O||!H.implementsDomRange)return t.fail("No means of obtaining a Range or TextRange from the user's selection was found"),!1;it=function(e){var t,n=e.nativeSelection;n.anchorNode?(t=et(n,0),e._ranges=[t],e.rangeCount=1,c(e),e.isCollapsed=T(e)):d(e)}}rt.refresh=function(e){var t=e?this._ranges.slice(0):null,n=this.anchorNode,r=this.anchorOffset;if(it(this),e){var o=t.length;if(o!=this._ranges.length)return!0;if(this.anchorNode!=n||this.anchorOffset!=r)return!0;for(;o--;)if(!W(t[o],this._ranges[o]))return!0;return!1}};var at=function(e,t){var n=e.getAllRanges();e.removeAllRanges();for(var r=0,o=n.length;o>r;++r)W(t,n[r])||e.addRange(n[r]);e.rangeCount||d(e)};rt.removeRange=K&&j?function(e){if(this.docSelection.type==M){for(var t,n=this.docSelection.createRange(),r=l(e),o=k(n.item(0)),i=L(o).createControlRange(),a=!1,s=0,c=n.length;c>s;++s)t=n.item(s),t!==r||a?i.add(n.item(s)):a=!0;i.select(),p(this)}else at(this,e)}:function(e){at(this,e)};var st;!z&&Y&&H.implementsDomRange?(st=a,rt.isBackward=function(){return st(this)}):st=rt.isBackward=function(){return!1},rt.isBackwards=rt.isBackward,rt.toString=function(){for(var e=[],t=0,n=this.rangeCount;n>t;++t)e[t]=""+this._ranges[t];return e.join("")},rt.collapse=function(t,n){E(this,t);var r=e.createRange(t);r.collapseToPoint(t,n),this.setSingleRange(r),this.isCollapsed=!0},rt.collapseToStart=function(){if(!this.rangeCount)throw new I("INVALID_STATE_ERR");var e=this._ranges[0];this.collapse(e.startContainer,e.startOffset)},rt.collapseToEnd=function(){if(!this.rangeCount)throw new I("INVALID_STATE_ERR");var e=this._ranges[this.rangeCount-1];this.collapse(e.endContainer,e.endOffset)},rt.selectAllChildren=function(t){E(this,t);var n=e.createRange(t);n.selectNodeContents(t),this.setSingleRange(n)},rt.deleteFromDocument=function(){if(K&&j&&this.docSelection.type==M){for(var e,t=this.docSelection.createRange();t.length;)e=t.item(0),t.remove(e),D.removeNode(e);this.refresh()}else if(this.rangeCount){var n=this.getAllRanges();if(n.length){this.removeAllRanges();for(var r=0,o=n.length;o>r;++r)n[r].deleteContents();this.addRange(n[o-1])}}},rt.eachRange=function(e,t){for(var n=0,r=this._ranges.length;r>n;++n)if(e(this.getRangeAt(n)))return t},rt.getAllRanges=function(){var e=[];return this.eachRange(function(t){e.push(t)}),e},rt.setSingleRange=function(e,t){this.removeAllRanges(),this.addRange(e,t)},rt.callMethodOnEachRange=function(e,t){var n=[];return this.eachRange(function(r){n.push(r[e].apply(r,t||[]))}),n},rt.setStart=S(!0),rt.setEnd=S(!1),e.rangePrototype.select=function(e){nt(this.getDocument()).setSingleRange(this,e)},rt.changeEachRange=function(e){var t=[],n=this.isBackward();this.eachRange(function(n){e(n),t.push(n)}),this.removeAllRanges(),n&&1==t.length?this.addRange(t[0],"backward"):this.setRanges(t)},rt.containsNode=function(e,t){return this.eachRange(function(n){return n.containsNode(e,t)},!0)||!1},rt.getBookmark=function(e){return{backward:this.isBackward(),rangeBookmarks:this.callMethodOnEachRange("getBookmark",[e])}},rt.moveToBookmark=function(t){for(var n,r,o=[],i=0;n=t.rangeBookmarks[i++];)r=e.createRange(this.win),r.moveToBookmark(n),o.push(r);t.backward?this.setSingleRange(o[0],"backward"):this.setRanges(o)},rt.saveRanges=function(){return{backward:this.isBackward(),ranges:this.callMethodOnEachRange("cloneRange")}},rt.restoreRanges=function(e){this.removeAllRanges();for(var t,n=0;t=e.ranges[n];++n)this.addRange(t,e.backward&&0==n)},rt.toHtml=function(){var e=[];return this.eachRange(function(t){e.push(b.toHtml(t))}),e.join("")},H.implementsTextRange&&(rt.getNativeTextRange=function(){var n;if(n=this.docSelection){var r=n.createRange();if(h(r))return r;throw t.createError("getNativeTextRange: selection is a control selection")}if(this.rangeCount>0)return e.WrappedTextRange.rangeToTextRange(this.getRangeAt(0));throw t.createError("getNativeTextRange: selection contains no range")}),rt.getName=function(){return"WrappedSelection"},rt.inspect=function(){return y(this)},rt.detach=function(){C(this.win,"delete"),v(this)},R.detachAll=function(){C(null,"deleteAll")},R.inspect=y,R.isDirectionBackward=n,e.Selection=R,e.selectionPrototype=rt,e.addShimListener(function(e){"undefined"==typeof e.getSelection&&(e.getSelection=function(){return nt(e)}),e=null})});var L=!1,W=function(){L||(L=!0,!I.initialized&&I.config.autoInitialize&&u())};return b&&("complete"==document.readyState?W():(e(document,"addEventListener")&&document.addEventListener("DOMContentLoaded",W,!1),H(window,"load",W))),I},this); \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/rangy-cssclassapplier.js b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-cssclassapplier.js
new file mode 100644
index 000000000..60064a334
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-cssclassapplier.js
@@ -0,0 +1,32 @@
+/*
+ CSS Class Applier module for Rangy.
+ Adds, removes and toggles CSS classes on Ranges and Selections
+
+ Part of Rangy, a cross-browser JavaScript range and selection library
+ http://code.google.com/p/rangy/
+
+ Depends on Rangy core.
+
+ Copyright 2012, Tim Down
+ Licensed under the MIT license.
+ Version: 1.2.3
+ Build date: 26 February 2012
+*/
+rangy.createModule("CssClassApplier",function(i,v){function r(a,b){return a.className&&RegExp("(?:^|\\s)"+b+"(?:\\s|$)").test(a.className)}function s(a,b){if(a.className)r(a,b)||(a.className+=" "+b);else a.className=b}function o(a){return a.split(/\s+/).sort().join(" ")}function w(a,b){return o(a.className)==o(b.className)}function x(a){for(var b=a.parentNode;a.hasChildNodes();)b.insertBefore(a.firstChild,a);b.removeChild(a)}function y(a,b){var c=a.cloneRange();c.selectNodeContents(b);var d=c.intersection(a);
+d=d?d.toString():"";c.detach();return d!=""}function z(a){return a.getNodes([3],function(b){return y(a,b)})}function A(a,b){if(a.attributes.length!=b.attributes.length)return false;for(var c=0,d=a.attributes.length,e,f;c<d;++c){e=a.attributes[c];f=e.name;if(f!="class"){f=b.attributes.getNamedItem(f);if(e.specified!=f.specified)return false;if(e.specified&&e.nodeValue!==f.nodeValue)return false}}return true}function B(a,b){for(var c=0,d=a.attributes.length,e;c<d;++c){e=a.attributes[c].name;if(!(b&&
+h.arrayContains(b,e))&&a.attributes[c].specified&&e!="class")return true}return false}function C(a){var b;return a&&a.nodeType==1&&((b=a.parentNode)&&b.nodeType==9&&b.designMode=="on"||k(a)&&!k(a.parentNode))}function D(a){return(k(a)||a.nodeType!=1&&k(a.parentNode))&&!C(a)}function E(a){return a&&a.nodeType==1&&!M.test(p(a,"display"))}function N(a){if(a.data.length==0)return true;if(O.test(a.data))return false;switch(p(a.parentNode,"whiteSpace")){case "pre":case "pre-wrap":case "-moz-pre-wrap":return false;
+case "pre-line":if(/[\r\n]/.test(a.data))return false}return E(a.previousSibling)||E(a.nextSibling)}function m(a,b,c,d){var e,f=c==0;if(h.isAncestorOf(b,a))return a;if(h.isCharacterDataNode(b))if(c==0){c=h.getNodeIndex(b);b=b.parentNode}else if(c==b.length){c=h.getNodeIndex(b)+1;b=b.parentNode}else throw v.createError("splitNodeAt should not be called with offset in the middle of a data node ("+c+" in "+b.data);var g;g=b;var j=c;g=h.isCharacterDataNode(g)?j==0?!!g.previousSibling:j==g.length?!!g.nextSibling:
+true:j>0&&j<g.childNodes.length;if(g){if(!e){e=b.cloneNode(false);for(e.id&&e.removeAttribute("id");f=b.childNodes[c];)e.appendChild(f);h.insertAfter(e,b)}return b==a?e:m(a,e.parentNode,h.getNodeIndex(e),d)}else if(a!=b){e=b.parentNode;b=h.getNodeIndex(b);f||b++;return m(a,e,b,d)}return a}function F(a){var b=a?"nextSibling":"previousSibling";return function(c,d){var e=c.parentNode,f=c[b];if(f){if(f&&f.nodeType==3)return f}else if(d)if((f=e[b])&&f.nodeType==1&&e.tagName==f.tagName&&w(e,f)&&A(e,f))return f[a?
+"firstChild":"lastChild"];return null}}function t(a){this.firstTextNode=(this.isElementMerge=a.nodeType==1)?a.lastChild:a;this.textNodes=[this.firstTextNode]}function q(a,b,c){this.cssClass=a;var d,e,f=null;if(typeof b=="object"&&b!==null){c=b.tagNames;f=b.elementProperties;for(d=0;e=P[d++];)if(b.hasOwnProperty(e))this[e]=b[e];d=b.normalize}else d=b;this.normalize=typeof d=="undefined"?true:d;this.attrExceptions=[];d=document.createElement(this.elementTagName);this.elementProperties={};for(var g in f)if(f.hasOwnProperty(g)){if(G.hasOwnProperty(g))g=
+G[g];d[g]=f[g];this.elementProperties[g]=d[g];this.attrExceptions.push(g)}this.elementSortedClassName=this.elementProperties.hasOwnProperty("className")?o(this.elementProperties.className+" "+a):a;this.applyToAnyTagName=false;a=typeof c;if(a=="string")if(c=="*")this.applyToAnyTagName=true;else this.tagNames=c.toLowerCase().replace(/^\s\s*/,"").replace(/\s\s*$/,"").split(/\s*,\s*/);else if(a=="object"&&typeof c.length=="number"){this.tagNames=[];d=0;for(a=c.length;d<a;++d)if(c[d]=="*")this.applyToAnyTagName=
+true;else this.tagNames.push(c[d].toLowerCase())}else this.tagNames=[this.elementTagName]}i.requireModules(["WrappedSelection","WrappedRange"]);var h=i.dom,H=function(){function a(b,c,d){return c&&d?" ":""}return function(b,c){if(b.className)b.className=b.className.replace(RegExp("(?:^|\\s)"+c+"(?:\\s|$)"),a)}}(),p;if(typeof window.getComputedStyle!="undefined")p=function(a,b){return h.getWindow(a).getComputedStyle(a,null)[b]};else if(typeof document.documentElement.currentStyle!="undefined")p=function(a,
+b){return a.currentStyle[b]};else v.fail("No means of obtaining computed style properties found");var k;(function(){k=typeof document.createElement("div").isContentEditable=="boolean"?function(a){return a&&a.nodeType==1&&a.isContentEditable}:function(a){if(!a||a.nodeType!=1||a.contentEditable=="false")return false;return a.contentEditable=="true"||k(a.parentNode)}})();var M=/^inline(-block|-table)?$/i,O=/[^\r\n\t\f \u200B]/,Q=F(false),R=F(true);t.prototype={doMerge:function(){for(var a=[],b,c,d=0,
+e=this.textNodes.length;d<e;++d){b=this.textNodes[d];c=b.parentNode;a[d]=b.data;if(d){c.removeChild(b);c.hasChildNodes()||c.parentNode.removeChild(c)}}return this.firstTextNode.data=a=a.join("")},getLength:function(){for(var a=this.textNodes.length,b=0;a--;)b+=this.textNodes[a].length;return b},toString:function(){for(var a=[],b=0,c=this.textNodes.length;b<c;++b)a[b]="'"+this.textNodes[b].data+"'";return"[Merge("+a.join(",")+")]"}};var P=["elementTagName","ignoreWhiteSpace","applyToEditableOnly"],
+G={"class":"className"};q.prototype={elementTagName:"span",elementProperties:{},ignoreWhiteSpace:true,applyToEditableOnly:false,hasClass:function(a){return a.nodeType==1&&h.arrayContains(this.tagNames,a.tagName.toLowerCase())&&r(a,this.cssClass)},getSelfOrAncestorWithClass:function(a){for(;a;){if(this.hasClass(a,this.cssClass))return a;a=a.parentNode}return null},isModifiable:function(a){return!this.applyToEditableOnly||D(a)},isIgnorableWhiteSpaceNode:function(a){return this.ignoreWhiteSpace&&a&&
+a.nodeType==3&&N(a)},postApply:function(a,b,c){for(var d=a[0],e=a[a.length-1],f=[],g,j=d,I=e,J=0,K=e.length,n,L,l=0,u=a.length;l<u;++l){n=a[l];if(L=Q(n,!c)){if(!g){g=new t(L);f.push(g)}g.textNodes.push(n);if(n===d){j=g.firstTextNode;J=j.length}if(n===e){I=g.firstTextNode;K=g.getLength()}}else g=null}if(a=R(e,!c)){if(!g){g=new t(e);f.push(g)}g.textNodes.push(a)}if(f.length){l=0;for(u=f.length;l<u;++l)f[l].doMerge();b.setStart(j,J);b.setEnd(I,K)}},createContainer:function(a){a=a.createElement(this.elementTagName);
+i.util.extend(a,this.elementProperties);s(a,this.cssClass);return a},applyToTextNode:function(a){var b=a.parentNode;if(b.childNodes.length==1&&h.arrayContains(this.tagNames,b.tagName.toLowerCase()))s(b,this.cssClass);else{b=this.createContainer(h.getDocument(a));a.parentNode.insertBefore(b,a);b.appendChild(a)}},isRemovable:function(a){var b;if(b=a.tagName.toLowerCase()==this.elementTagName){if(b=o(a.className)==this.elementSortedClassName){var c;a:{b=this.elementProperties;for(c in b)if(b.hasOwnProperty(c)&&
+a[c]!==b[c]){c=false;break a}c=true}b=c&&!B(a,this.attrExceptions)&&this.isModifiable(a)}b=b}return b},undoToTextNode:function(a,b,c){if(!b.containsNode(c)){a=b.cloneRange();a.selectNode(c);if(a.isPointInRange(b.endContainer,b.endOffset)){m(c,b.endContainer,b.endOffset,[b]);b.setEndAfter(c)}if(a.isPointInRange(b.startContainer,b.startOffset))c=m(c,b.startContainer,b.startOffset,[b])}this.isRemovable(c)?x(c):H(c,this.cssClass)},applyToRange:function(a){a.splitBoundaries();var b=z(a);if(b.length){for(var c,
+d=0,e=b.length;d<e;++d){c=b[d];!this.isIgnorableWhiteSpaceNode(c)&&!this.getSelfOrAncestorWithClass(c)&&this.isModifiable(c)&&this.applyToTextNode(c)}a.setStart(b[0],0);c=b[b.length-1];a.setEnd(c,c.length);this.normalize&&this.postApply(b,a,false)}},applyToSelection:function(a){a=a||window;a=i.getSelection(a);var b,c=a.getAllRanges();a.removeAllRanges();for(var d=c.length;d--;){b=c[d];this.applyToRange(b);a.addRange(b)}},undoToRange:function(a){a.splitBoundaries();var b=z(a),c,d,e=b[b.length-1];if(b.length){for(var f=
+0,g=b.length;f<g;++f){c=b[f];(d=this.getSelfOrAncestorWithClass(c))&&this.isModifiable(c)&&this.undoToTextNode(c,a,d);a.setStart(b[0],0);a.setEnd(e,e.length)}this.normalize&&this.postApply(b,a,true)}},undoToSelection:function(a){a=a||window;a=i.getSelection(a);var b=a.getAllRanges(),c;a.removeAllRanges();for(var d=0,e=b.length;d<e;++d){c=b[d];this.undoToRange(c);a.addRange(c)}},getTextSelectedByRange:function(a,b){var c=b.cloneRange();c.selectNodeContents(a);var d=c.intersection(b);d=d?d.toString():
+"";c.detach();return d},isAppliedToRange:function(a){if(a.collapsed)return!!this.getSelfOrAncestorWithClass(a.commonAncestorContainer);else{for(var b=a.getNodes([3]),c=0,d;d=b[c++];)if(!this.isIgnorableWhiteSpaceNode(d)&&y(a,d)&&this.isModifiable(d)&&!this.getSelfOrAncestorWithClass(d))return false;return true}},isAppliedToSelection:function(a){a=a||window;a=i.getSelection(a).getAllRanges();for(var b=a.length;b--;)if(!this.isAppliedToRange(a[b]))return false;return true},toggleRange:function(a){this.isAppliedToRange(a)?
+this.undoToRange(a):this.applyToRange(a)},toggleSelection:function(a){this.isAppliedToSelection(a)?this.undoToSelection(a):this.applyToSelection(a)},detach:function(){}};q.util={hasClass:r,addClass:s,removeClass:H,hasSameClasses:w,replaceWithOwnChildren:x,elementsHaveSameNonClassAttributes:A,elementHasNonClassAttributes:B,splitNodeAt:m,isEditableElement:k,isEditingHost:C,isEditable:D};i.CssClassApplier=q;i.createCssClassApplier=function(a,b,c){return new q(a,b,c)}}); \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/rangy-highlighter.js b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-highlighter.js
new file mode 100755
index 000000000..31fd4f3dc
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-highlighter.js
@@ -0,0 +1,12 @@
+/**
+ * Highlighter module for Rangy, a cross-browser JavaScript range and selection library
+ * https://github.com/timdown/rangy
+ *
+ * Depends on Rangy core, ClassApplier and optionally TextRange modules.
+ *
+ * Copyright 2015, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.3.0
+ * Build date: 10 May 2015
+ */
+!function(e,t){"function"==typeof define&&define.amd?define(["./rangy-core"],e):"undefined"!=typeof module&&"object"==typeof exports?module.exports=e(require("rangy")):e(t.rangy)}(function(e){return e.createModule("Highlighter",["ClassApplier"],function(e){function t(e,t){return e.characterRange.start-t.characterRange.start}function n(e,t){return t?e.getElementById(t):l(e)}function r(e,t){this.type=e,this.converterCreator=t}function i(e,t){f[e]=new r(e,t)}function a(e){var t=f[e];if(t instanceof r)return t.create();throw new Error("Highlighter type '"+e+"' is not valid")}function s(e,t){this.start=e,this.end=t}function h(e,t,n,r,i,a){i?(this.id=i,d=Math.max(d,i+1)):this.id=d++,this.characterRange=t,this.doc=e,this.classApplier=n,this.converter=r,this.containerElementId=a||null,this.applied=!1}function o(e,t){t=t||"textContent",this.doc=e||document,this.classAppliers={},this.highlights=[],this.converter=a(t)}var c=e.dom,g=c.arrayContains,l=c.getBody,u=e.util.createOptions,p=e.util.forEach,d=1,f={};r.prototype.create=function(){var e=this.converterCreator();return e.type=this.type,e},e.registerHighlighterType=i,s.prototype={intersects:function(e){return this.start<e.end&&this.end>e.start},isContiguousWith:function(e){return this.start==e.end||this.end==e.start},union:function(e){return new s(Math.min(this.start,e.start),Math.max(this.end,e.end))},intersection:function(e){return new s(Math.max(this.start,e.start),Math.min(this.end,e.end))},getComplements:function(e){var t=[];if(this.start>=e.start){if(this.end<=e.end)return[];t.push(new s(e.end,this.end))}else t.push(new s(this.start,Math.min(this.end,e.start))),this.end>e.end&&t.push(new s(e.end,this.end));return t},toString:function(){return"[CharacterRange("+this.start+", "+this.end+")]"}},s.fromCharacterRange=function(e){return new s(e.start,e.end)};var R={rangeToCharacterRange:function(e,t){var n=e.getBookmark(t);return new s(n.start,n.end)},characterRangeToRange:function(t,n,r){var i=e.createRange(t);return i.moveToBookmark({start:n.start,end:n.end,containerNode:r}),i},serializeSelection:function(e,t){for(var n=e.getAllRanges(),r=n.length,i=[],a=1==r&&e.isBackward(),s=0,h=n.length;h>s;++s)i[s]={characterRange:this.rangeToCharacterRange(n[s],t),backward:a};return i},restoreSelection:function(e,t,n){e.removeAllRanges();for(var r,i,a,s=e.win.document,h=0,o=t.length;o>h;++h)i=t[h],a=i.characterRange,r=this.characterRangeToRange(s,i.characterRange,n),e.addRange(r,i.backward)}};i("textContent",function(){return R}),i("TextRange",function(){var t;return function(){if(!t){var n=e.modules.TextRange;if(!n)throw new Error("TextRange module is missing.");if(!n.supported)throw new Error("TextRange module is present but not supported.");t={rangeToCharacterRange:function(e,t){return s.fromCharacterRange(e.toCharacterRange(t))},characterRangeToRange:function(t,n,r){var i=e.createRange(t);return i.selectCharacters(r,n.start,n.end),i},serializeSelection:function(e,t){return e.saveCharacterRanges(t)},restoreSelection:function(e,t,n){e.restoreCharacterRanges(n,t)}}}return t}}()),h.prototype={getContainerElement:function(){return n(this.doc,this.containerElementId)},getRange:function(){return this.converter.characterRangeToRange(this.doc,this.characterRange,this.getContainerElement())},fromRange:function(e){this.characterRange=this.converter.rangeToCharacterRange(e,this.getContainerElement())},getText:function(){return this.getRange().toString()},containsElement:function(e){return this.getRange().containsNodeContents(e.firstChild)},unapply:function(){this.classApplier.undoToRange(this.getRange()),this.applied=!1},apply:function(){this.classApplier.applyToRange(this.getRange()),this.applied=!0},getHighlightElements:function(){return this.classApplier.getElementsWithClassIntersectingRange(this.getRange())},toString:function(){return"[Highlight(ID: "+this.id+", class: "+this.classApplier.className+", character range: "+this.characterRange.start+" - "+this.characterRange.end+")]"}},o.prototype={addClassApplier:function(e){this.classAppliers[e.className]=e},getHighlightForElement:function(e){for(var t=this.highlights,n=0,r=t.length;r>n;++n)if(t[n].containsElement(e))return t[n];return null},removeHighlights:function(e){for(var t,n=0,r=this.highlights.length;r>n;++n)t=this.highlights[n],g(e,t)&&(t.unapply(),this.highlights.splice(n--,1))},removeAllHighlights:function(){this.removeHighlights(this.highlights)},getIntersectingHighlights:function(e){var t=[],n=this.highlights;return p(e,function(e){p(n,function(n){e.intersectsRange(n.getRange())&&!g(t,n)&&t.push(n)})}),t},highlightCharacterRanges:function(t,n,r){var i,a,o,c=this.highlights,g=this.converter,l=this.doc,d=[],f=t?this.classAppliers[t]:null;r=u(r,{containerElementId:null,exclusive:!0});var R,v,m,C=r.containerElementId,w=r.exclusive;C&&(R=this.doc.getElementById(C),R&&(v=e.createRange(this.doc),v.selectNodeContents(R),m=new s(0,v.toString().length)));var y,E,T,x,A,H;for(i=0,a=n.length;a>i;++i)if(y=n[i],A=[],m&&(y=y.intersection(m)),y.start!=y.end){for(o=0;o<c.length;++o)T=!1,C==c[o].containerElementId&&(E=c[o].characterRange,x=f==c[o].classApplier,H=!x&&w,(E.intersects(y)||E.isContiguousWith(y))&&(x||H)&&(H&&p(E.getComplements(y),function(e){A.push(new h(l,e,c[o].classApplier,g,null,C))}),T=!0,x&&(y=E.union(y)))),T?(d.push(c[o]),c[o]=new h(l,E.union(y),f,g,null,C)):A.push(c[o]);f&&A.push(new h(l,y,f,g,null,C)),this.highlights=c=A}p(d,function(e){e.unapply()});var I=[];return p(c,function(e){e.applied||(e.apply(),I.push(e))}),I},highlightRanges:function(t,n,r){var i=[],a=this.converter;r=u(r,{containerElement:null,exclusive:!0});var s,h=r.containerElement,o=h?h.id:null;return h&&(s=e.createRange(h),s.selectNodeContents(h)),p(n,function(e){var t=h?s.intersection(e):e;i.push(a.rangeToCharacterRange(t,h||l(e.getDocument())))}),this.highlightCharacterRanges(t,i,{containerElementId:o,exclusive:r.exclusive})},highlightSelection:function(t,r){var i=this.converter,a=t?this.classAppliers[t]:!1;r=u(r,{containerElementId:null,selection:e.getSelection(this.doc),exclusive:!0});var h=r.containerElementId,o=r.exclusive,c=r.selection,g=c.win.document,l=n(g,h);if(!a&&t!==!1)throw new Error("No class applier found for class '"+t+"'");var d=i.serializeSelection(c,l),f=[];p(d,function(e){f.push(s.fromCharacterRange(e.characterRange))});var R=this.highlightCharacterRanges(t,f,{containerElementId:h,exclusive:o});return i.restoreSelection(c,d,l),R},unhighlightSelection:function(t){t=t||e.getSelection(this.doc);var n=this.getIntersectingHighlights(t.getAllRanges());return this.removeHighlights(n),t.removeAllRanges(),n},getHighlightsInSelection:function(t){return t=t||e.getSelection(this.doc),this.getIntersectingHighlights(t.getAllRanges())},selectionOverlapsHighlight:function(e){return this.getHighlightsInSelection(e).length>0},serialize:function(e){var n,r,i,s,h=this,o=h.highlights;return o.sort(t),e=u(e,{serializeHighlightText:!1,type:h.converter.type}),n=e.type,i=n!=h.converter.type,i&&(s=a(n)),r=["type:"+n],p(o,function(t){var n,a=t.characterRange;i&&(n=t.getContainerElement(),a=s.rangeToCharacterRange(h.converter.characterRangeToRange(h.doc,a,n),n));var o=[a.start,a.end,t.id,t.classApplier.className,t.containerElementId];e.serializeHighlightText&&o.push(t.getText()),r.push(o.join("$"))}),r.join("|")},deserialize:function(e){var t,r,i,o=e.split("|"),c=[],g=o[0],l=!1;if(!g||!(t=/^type:(\w+)$/.exec(g)))throw new Error("Serialized highlights are invalid.");r=t[1],r!=this.converter.type&&(i=a(r),l=!0),o.shift();for(var u,p,d,f,R,v,m=o.length;m-->0;){if(v=o[m].split("$"),d=new s(+v[0],+v[1]),f=v[4]||null,l&&(R=n(this.doc,f),d=this.converter.rangeToCharacterRange(i.characterRangeToRange(this.doc,d,R),R)),u=this.classAppliers[v[3]],!u)throw new Error("No class applier found for class '"+v[3]+"'");p=new h(this.doc,d,u,this.converter,parseInt(v[2]),f),p.apply(),c.push(p)}this.highlights=c}},e.Highlighter=o,e.createHighlighter=function(e,t){return new o(e,t)}}),e},this); \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/rangy-selectionsaverestore.js b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-selectionsaverestore.js
new file mode 100755
index 000000000..4029e43d1
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-selectionsaverestore.js
@@ -0,0 +1,15 @@
+/**
+ * Selection save and restore module for Rangy.
+ * Saves and restores user selections using marker invisible elements in the DOM.
+ *
+ * Part of Rangy, a cross-browser JavaScript range and selection library
+ * https://github.com/timdown/rangy
+ *
+ * Depends on Rangy core.
+ *
+ * Copyright 2015, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.3.0
+ * Build date: 10 May 2015
+ */
+!function(e,n){"function"==typeof define&&define.amd?define(["./rangy-core"],e):"undefined"!=typeof module&&"object"==typeof exports?module.exports=e(require("rangy")):e(n.rangy)}(function(e){return e.createModule("SaveRestore",["WrappedRange"],function(e,n){function r(e,n){return(n||document).getElementById(e)}function t(e,n){var r,t="selectionBoundary_"+ +new Date+"_"+(""+Math.random()).slice(2),a=m.getDocument(e.startContainer),o=e.cloneRange();return o.collapse(n),r=a.createElement("span"),r.id=t,r.style.lineHeight="0",r.style.display="none",r.className="rangySelectionBoundary",r.appendChild(a.createTextNode(k)),o.insertNode(r),r}function a(e,t,a,o){var s=r(a,e);s?(t[o?"setStartBefore":"setEndBefore"](s),p(s)):n.warn("Marker element has been removed. Cannot restore selection.")}function o(e,n){return n.compareBoundaryPoints(e.START_TO_START,e)}function s(n,r){var a,o,s=e.DomRange.getRangeDocument(n),i=n.toString(),d=v(r);return n.collapsed?(o=t(n,!1),{document:s,markerId:o.id,collapsed:!0}):(o=t(n,!1),a=t(n,!0),{document:s,startMarkerId:a.id,endMarkerId:o.id,collapsed:!1,backward:d,toString:function(){return"original text: '"+i+"', new text: '"+n.toString()+"'"}})}function i(t,o){var s=t.document;"undefined"==typeof o&&(o=!0);var i=e.createRange(s);if(t.collapsed){var d=r(t.markerId,s);if(d){d.style.display="inline";var l=d.previousSibling;l&&3==l.nodeType?(p(d),i.collapseToPoint(l,l.length)):(i.collapseBefore(d),p(d))}else n.warn("Marker element has been removed. Cannot restore selection.")}else a(s,i,t.startMarkerId,!0),a(s,i,t.endMarkerId,!1);return o&&i.normalizeBoundaries(),i}function d(n,t){var a,i,d=[],l=v(t);n=n.slice(0),n.sort(o);for(var c=0,u=n.length;u>c;++c)d[c]=s(n[c],l);for(c=u-1;c>=0;--c)a=n[c],i=e.DomRange.getRangeDocument(a),a.collapsed?a.collapseAfter(r(d[c].markerId,i)):(a.setEndBefore(r(d[c].endMarkerId,i)),a.setStartAfter(r(d[c].startMarkerId,i)));return d}function l(r){if(!e.isSelectionValid(r))return n.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus."),null;var t=e.getSelection(r),a=t.getAllRanges(),o=1==a.length&&t.isBackward(),s=d(a,o);return o?t.setSingleRange(a[0],o):t.setRanges(a),{win:r,rangeInfos:s,restored:!1}}function c(e){for(var n=[],r=e.length,t=r-1;t>=0;t--)n[t]=i(e[t],!0);return n}function u(n,r){if(!n.restored){var t=n.rangeInfos,a=e.getSelection(n.win),o=c(t),s=t.length;1==s&&r&&e.features.selectionHasExtend&&t[0].backward?(a.removeAllRanges(),a.addRange(o[0],!0)):a.setRanges(o),n.restored=!0}}function f(e,n){var t=r(n,e);t&&p(t)}function g(e){for(var n,r=e.rangeInfos,t=0,a=r.length;a>t;++t)n=r[t],n.collapsed?f(e.doc,n.markerId):(f(e.doc,n.startMarkerId),f(e.doc,n.endMarkerId))}var m=e.dom,p=m.removeNode,v=e.Selection.isDirectionBackward,k="";e.util.extend(e,{saveRange:s,restoreRange:i,saveRanges:d,restoreRanges:c,saveSelection:l,restoreSelection:u,removeMarkerElement:f,removeMarkers:g})}),e},this); \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/rangy-serializer.js b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-serializer.js
new file mode 100755
index 000000000..68780587d
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-serializer.js
@@ -0,0 +1,16 @@
+/**
+ * Serializer module for Rangy.
+ * Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a
+ * cookie or local storage and restore it on the user's next visit to the same page.
+ *
+ * Part of Rangy, a cross-browser JavaScript range and selection library
+ * https://github.com/timdown/rangy
+ *
+ * Depends on Rangy core.
+ *
+ * Copyright 2015, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.3.0
+ * Build date: 10 May 2015
+ */
+!function(e,n){"function"==typeof define&&define.amd?define(["./rangy-core"],e):"undefined"!=typeof module&&"object"==typeof exports?module.exports=e(require("rangy")):e(n.rangy)}(function(e){return e.createModule("Serializer",["WrappedSelection"],function(e,n){function t(e){return e.replace(/</g,"&lt;").replace(/>/g,"&gt;")}function o(e,n){n=n||[];var r=e.nodeType,i=e.childNodes,c=i.length,a=[r,e.nodeName,c].join(":"),d="",u="";switch(r){case 3:d=t(e.nodeValue);break;case 8:d="<!--"+t(e.nodeValue)+"-->";break;default:d="<"+a+">",u="</>"}d&&n.push(d);for(var s=0;c>s;++s)o(i[s],n);return u&&n.push(u),n}function r(e){var n=o(e).join("");return w(n).toString(16)}function i(e,n,t){var o=[],r=e;for(t=t||R.getDocument(e).documentElement;r&&r!=t;)o.push(R.getNodeIndex(r,!0)),r=r.parentNode;return o.join("/")+":"+n}function c(e,t,o){t||(t=(o||document).documentElement);for(var r,i=e.split(":"),c=t,a=i[0]?i[0].split("/"):[],d=a.length;d--;){if(r=parseInt(a[d],10),!(r<c.childNodes.length))throw n.createError("deserializePosition() failed: node "+R.inspectNode(c)+" has no child with index "+r+", "+d);c=c.childNodes[r]}return new R.DomPosition(c,parseInt(i[1],10))}function a(t,o,c){if(c=c||e.DomRange.getRangeDocument(t).documentElement,!R.isOrIsAncestorOf(c,t.commonAncestorContainer))throw n.createError("serializeRange(): range "+t.inspect()+" is not wholly contained within specified root node "+R.inspectNode(c));var a=i(t.startContainer,t.startOffset,c)+","+i(t.endContainer,t.endOffset,c);return o||(a+="{"+r(c)+"}"),a}function d(t,o,i){o?i=i||R.getDocument(o):(i=i||document,o=i.documentElement);var a=S.exec(t),d=a[4];if(d){var u=r(o);if(d!==u)throw n.createError("deserializeRange(): checksums of serialized range root node ("+d+") and target root node ("+u+") do not match")}var s=c(a[1],o,i),l=c(a[2],o,i),f=e.createRange(i);return f.setStartAndEnd(s.node,s.offset,l.node,l.offset),f}function u(e,n,t){n||(n=(t||document).documentElement);var o=S.exec(e),i=o[3];return!i||i===r(n)}function s(n,t,o){n=e.getSelection(n);for(var r=n.getAllRanges(),i=[],c=0,d=r.length;d>c;++c)i[c]=a(r[c],t,o);return i.join("|")}function l(n,t,o){t?o=o||R.getWindow(t):(o=o||window,t=o.document.documentElement);for(var r=n.split("|"),i=e.getSelection(o),c=[],a=0,u=r.length;u>a;++a)c[a]=d(r[a],t,o.document);return i.setRanges(c),i}function f(e,n,t){var o;n?o=t?t.document:R.getDocument(n):(t=t||window,n=t.document.documentElement);for(var r=e.split("|"),i=0,c=r.length;c>i;++i)if(!u(r[i],n,o))return!1;return!0}function m(e){for(var n,t,o=e.split(/[;,]/),r=0,i=o.length;i>r;++r)if(n=o[r].split("="),n[0].replace(/^\s+/,"")==C&&(t=n[1]))return decodeURIComponent(t.replace(/\s+$/,""));return null}function p(e){e=e||window;var n=m(e.document.cookie);n&&l(n,e.doc)}function g(n,t){n=n||window,t="object"==typeof t?t:{};var o=t.expires?";expires="+t.expires.toUTCString():"",r=t.path?";path="+t.path:"",i=t.domain?";domain="+t.domain:"",c=t.secure?";secure":"",a=s(e.getSelection(n));n.document.cookie=encodeURIComponent(C)+"="+encodeURIComponent(a)+o+r+i+c}var h="undefined",v=e.util;(typeof encodeURIComponent==h||typeof decodeURIComponent==h)&&n.fail("encodeURIComponent and/or decodeURIComponent method is missing");var w=function(){function e(e){for(var n,t=[],o=0,r=e.length;r>o;++o)n=e.charCodeAt(o),128>n?t.push(n):2048>n?t.push(n>>6|192,63&n|128):t.push(n>>12|224,n>>6&63|128,63&n|128);return t}function n(){for(var e,n,t=[],o=0;256>o;++o){for(n=o,e=8;e--;)1==(1&n)?n=n>>>1^3988292384:n>>>=1;t[o]=n>>>0}return t}function t(){return o||(o=n()),o}var o=null;return function(n){for(var o,r=e(n),i=-1,c=t(),a=0,d=r.length;d>a;++a)o=255&(i^r[a]),i=i>>>8^c[o];return(-1^i)>>>0}}(),R=e.dom,S=/^([^,]+),([^,\{]+)(\{([^}]+)\})?$/,C="rangySerializedSelection";v.extend(e,{serializePosition:i,deserializePosition:c,serializeRange:a,deserializeRange:d,canDeserializeRange:u,serializeSelection:s,deserializeSelection:l,canDeserializeSelection:f,restoreSelectionFromCookie:p,saveSelectionCookie:g,getElementChecksum:r,nodeToInfoString:o}),v.crc32=w}),e},this); \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/rangy-textrange.js b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-textrange.js
new file mode 100755
index 000000000..c79811ad4
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/rangy-textrange.js
@@ -0,0 +1,32 @@
+/**
+ * Text range module for Rangy.
+ * Text-based manipulation and searching of ranges and selections.
+ *
+ * Features
+ *
+ * - Ability to move range boundaries by character or word offsets
+ * - Customizable word tokenizer
+ * - Ignores text nodes inside <script> or <style> elements or those hidden by CSS display and visibility properties
+ * - Range findText method to search for text or regex within the page or within a range. Flags for whole words and case
+ * sensitivity
+ * - Selection and range save/restore as text offsets within a node
+ * - Methods to return visible text within a range or selection
+ * - innerText method for elements
+ *
+ * References
+ *
+ * https://www.w3.org/Bugs/Public/show_bug.cgi?id=13145
+ * http://aryeh.name/spec/innertext/innertext.html
+ * http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html
+ *
+ * Part of Rangy, a cross-browser JavaScript range and selection library
+ * https://github.com/timdown/rangy
+ *
+ * Depends on Rangy core.
+ *
+ * Copyright 2015, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.3.0
+ * Build date: 10 May 2015
+ */
+!function(e,t){"function"==typeof define&&define.amd?define(["./rangy-core"],e):"undefined"!=typeof module&&"object"==typeof exports?module.exports=e(require("rangy")):e(t.rangy)}(function(e){return e.createModule("TextRange",["WrappedSelection"],function(e,t){function n(e,t){function n(e,t,n){s.push({start:e,end:t,isWord:n})}for(var r,i,o,a=e.join(""),s=[],c=0;r=t.wordRegex.exec(a);){if(i=r.index,o=i+r[0].length,i>c&&n(c,i,!1),t.includeTrailingSpace)for(;X.test(e[o]);)++o;n(i,o,!0),c=o}return c<e.length&&n(c,e.length,!1),s}function r(e,t){for(var n=e.slice(t.start,t.end),r={isWord:t.isWord,chars:n,toString:function(){return n.join("")}},i=0,o=n.length;o>i;++i)n[i].token=r;return r}function i(e,t,n){for(var i,o=n(e,t),a=[],s=0;i=o[s++];)a.push(r(e,i));return a}function o(e){var t=e||"",n="string"==typeof t?t.split(""):t;return n.sort(function(e,t){return e.charCodeAt(0)-t.charCodeAt(0)}),n.join("").replace(/(.)\1+/g,"$1")}function a(e){var t,n;return e?(t=e.language||Z,n={},K(n,ct[t]||ct[Z]),K(n,e),n):ct[Z]}function s(e,t){var n=z(e,t);return t.hasOwnProperty("wordOptions")&&(n.wordOptions=a(n.wordOptions)),t.hasOwnProperty("characterOptions")&&(n.characterOptions=z(n.characterOptions,at)),n}function c(e,t){var n=pt(e,"display",t),r=e.tagName.toLowerCase();return"block"==n&&ot&&ft.hasOwnProperty(r)?ft[r]:n}function u(e){for(var t=f(e),n=0,r=t.length;r>n;++n)if(1==t[n].nodeType&&"none"==c(t[n]))return!0;return!1}function d(e){var t;return 3==e.nodeType&&(t=e.parentNode)&&"hidden"==pt(t,"visibility")}function l(e){return e&&(1==e.nodeType&&!/^(inline(-block|-table)?|none)$/.test(c(e))||9==e.nodeType||11==e.nodeType)}function h(e){return U.isCharacterDataNode(e)||!/^(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)$/i.test(e.nodeName)}function p(e){for(var t=[];e.parentNode;)t.unshift(e.parentNode),e=e.parentNode;return t}function f(e){return p(e).concat([e])}function g(e){for(;e&&!e.nextSibling;)e=e.parentNode;return e?e.nextSibling:null}function v(e,t){return!t&&e.hasChildNodes()?e.firstChild:g(e)}function S(e){var t=e.previousSibling;if(t){for(e=t;e.hasChildNodes();)e=e.lastChild;return e}var n=e.parentNode;return n&&1==n.nodeType?n:null}function C(e){if(!e||3!=e.nodeType)return!1;var t=e.data;if(""===t)return!0;var n=e.parentNode;if(!n||1!=n.nodeType)return!1;var r=pt(e.parentNode,"whiteSpace");return/^[\t\n\r ]+$/.test(t)&&/^(normal|nowrap)$/.test(r)||/^[\t\r ]+$/.test(t)&&"pre-line"==r}function N(e){if(""===e.data)return!0;if(!C(e))return!1;var t=e.parentNode;return t?u(e)?!0:!1:!0}function y(e){var t=e.nodeType;return 7==t||8==t||u(e)||/^(script|style)$/i.test(e.nodeName)||d(e)||N(e)}function m(e,t){var n=e.nodeType;return 7==n||8==n||1==n&&"none"==c(e,t)}function x(){this.store={}}function T(e,t,n){return function(r){var i=this.cache;if(i.hasOwnProperty(e))return gt++,i[e];vt++;var o=t.call(this,n?this[n]:this,r);return i[e]=o,o}}function P(e,t){this.node=e,this.session=t,this.cache=new x,this.positions=new x}function b(e,t){this.offset=t,this.nodeWrapper=e,this.node=e.node,this.session=e.session,this.cache=new x}function w(){return"[Position("+U.inspectNode(this.node)+":"+this.offset+")]"}function R(){return B(),Bt=new Ot}function E(){return Bt||R()}function B(){Bt&&Bt.detach(),Bt=null}function O(e,n,r,i){function o(){var e=null;return n?(e=s,c||(s=s.previousVisible(),c=!s||r&&s.equals(r))):c||(e=s=s.nextVisible(),c=!s||r&&s.equals(r)),c&&(s=null),e}r&&(n?y(r.node)&&(r=e.previousVisible()):y(r.node)&&(r=r.nextVisible()));var a,s=e,c=!1,u=!1;return{next:function(){if(u)return u=!1,a;for(var e,t;e=o();)if(t=e.getCharacter(i))return a=e,e;return null},rewind:function(){if(!a)throw t.createError("createCharacterIterator: cannot rewind. Only one position can be rewound.");u=!0},dispose:function(){e=r=null}}}function k(e,t,n){function r(e){for(var t,n,r=[],i=e?o:a,s=!1,c=!1;t=i.next();){if(n=t.character,Q.test(n))c&&(c=!1,s=!0);else{if(s){i.rewind();break}c=!0}r.push(t)}return r}var o=O(e,!1,null,t),a=O(e,!0,null,t),s=n.tokenizer,c=r(!0),u=r(!1).reverse(),d=i(u.concat(c),n,s),l=c.length?d.slice(kt(d,c[0].token)):[],h=u.length?d.slice(0,kt(d,u.pop().token)+1):[];return{nextEndToken:function(){for(var e,t;1==l.length&&!(e=l[0]).isWord&&(t=r(!0)).length>0;)l=i(e.chars.concat(t),n,s);return l.shift()},previousStartToken:function(){for(var e,t;1==h.length&&!(e=h[0]).isWord&&(t=r(!1)).length>0;)h=i(t.reverse().concat(e.chars),n,s);return h.pop()},dispose:function(){o.dispose(),a.dispose(),l=h=null}}}function L(e,t,n,r,i){var o,a,s,c,u=0,d=e,l=Math.abs(n);if(0!==n){var h=0>n;switch(t){case M:for(a=O(e,h,null,r);(o=a.next())&&l>u;)++u,d=o;s=o,a.dispose();break;case j:for(var p=k(e,r,i),f=h?p.previousStartToken:p.nextEndToken;(c=f())&&l>u;)c.isWord&&(++u,d=h?c.chars[0]:c.chars[c.chars.length-1]);break;default:throw new Error("movePositionBy: unit '"+t+"' not implemented")}h?(d=d.previousVisible(),u=-u):d&&d.isLeadingSpace&&!d.isTrailingSpace&&(t==j&&(a=O(e,!1,null,r),s=a.next(),a.dispose()),s&&(d=s.previousVisible()))}return{position:d,unitsMoved:u}}function I(e,t,n,r){var i=e.getRangeBoundaryPosition(t,!0),o=e.getRangeBoundaryPosition(t,!1),a=r?o:i,s=r?i:o;return O(a,!!r,s,n)}function A(e,t,n){for(var r,i=[],o=I(e,t,n);r=o.next();)i.push(r);return o.dispose(),i}function W(t,n,r){var i=e.createRange(t.node);return i.setStartAndEnd(t.node,t.offset,n.node,n.offset),!i.expand("word",{wordOptions:r})}function _(e,t,n,r,i){function o(e,t){var n=g[e].previousVisible(),r=g[t-1],o=!i.wholeWordsOnly||W(n,r,i.wordOptions);return{startPos:n,endPos:r,valid:o}}for(var a,s,c,u,d,l,h=et(i.direction),p=O(e,h,e.session.getRangeBoundaryPosition(r,h),i.characterOptions),f="",g=[],v=null;a=p.next();)if(s=a.character,n||i.caseSensitive||(s=s.toLowerCase()),h?(g.unshift(a),f=s+f):(g.push(a),f+=s),n){if(d=t.exec(f))if(c=d.index,u=c+d[0].length,l){if(!h&&u<f.length||h&&c>0){v=o(c,u);break}}else l=!0}else if(-1!=(c=f.indexOf(t))){v=o(c,c+t.length);break}return l&&(v=o(c,u)),p.dispose(),v}function D(e){return function(){var t=!!Bt,n=E(),r=[n].concat(G.toArray(arguments)),i=e.apply(this,r);return t||B(),i}}function F(e,t){return D(function(n,r,i,o){typeof i==q&&(i=r,r=M),o=s(o,dt);var a=e;t&&(a=i>=0,this.collapse(!a));var c=L(n.getRangeBoundaryPosition(this,a),r,i,o.characterOptions,o.wordOptions),u=c.position;return this[a?"setStart":"setEnd"](u.node,u.offset),c.unitsMoved})}function V(e){return D(function(t,n){n=z(n,at);for(var r,i=I(t,this,n,!e),o=0;(r=i.next())&&Q.test(r.character);)++o;i.dispose();var a=o>0;return a&&this[e?"moveStart":"moveEnd"]("character",e?o:-o,{characterOptions:n}),a})}function $(e){return D(function(t,n){var r=!1;return this.changeEachRange(function(t){r=t[e](n)||r}),r})}var q="undefined",M="character",j="word",U=e.dom,G=e.util,K=G.extend,z=G.createOptions,H=U.getBody,Y=/^[ \t\f\r\n]+$/,J=/^[ \t\f\r]+$/,Q=/^[\t-\r \u0085\u00A0\u1680\u180E\u2000-\u200B\u2028\u2029\u202F\u205F\u3000]+$/,X=/^[\t \u00A0\u1680\u180E\u2000-\u200B\u202F\u205F\u3000]+$/,Z="en",et=e.Selection.isDirectionBackward,tt=!1,nt=!1,rt=!1,it=!0;!function(){var t=U.createTestElement(document,"<p>1 </p><p></p>",!0),n=t.firstChild,r=e.getSelection();r.collapse(n.lastChild,2),r.setStart(n.firstChild,0),tt=1==(""+r).length,t.innerHTML="1 <br />",r.collapse(t,2),r.setStart(t.firstChild,0),nt=1==(""+r).length,t.innerHTML="1 <p>1</p>",r.collapse(t,2),r.setStart(t.firstChild,0),rt=1==(""+r).length,U.removeNode(t),r.removeAllRanges()}();var ot,at={includeBlockContentTrailingSpace:!0,includeSpaceBeforeBr:!0,includeSpaceBeforeBlock:!0,includePreLineTrailingSpace:!0,ignoreCharacters:""},st={includeBlockContentTrailingSpace:!it,includeSpaceBeforeBr:!nt,includeSpaceBeforeBlock:!rt,includePreLineTrailingSpace:!0},ct={en:{wordRegex:/[a-z0-9]+('[a-z0-9]+)*/gi,includeTrailingSpace:!1,tokenizer:n}},ut={caseSensitive:!1,withinRange:null,wholeWordsOnly:!1,wrap:!1,direction:"forward",wordOptions:null,characterOptions:null},dt={wordOptions:null,characterOptions:null},lt={wordOptions:null,characterOptions:null,trim:!1,trimStart:!0,trimEnd:!0},ht={wordOptions:null,characterOptions:null,direction:"forward"},pt=U.getComputedStyleProperty;!function(){var e=document.createElement("table"),t=H(document);t.appendChild(e),ot="block"==pt(e,"display"),t.removeChild(e)}();var ft={table:"table",caption:"table-caption",colgroup:"table-column-group",col:"table-column",thead:"table-header-group",tbody:"table-row-group",tfoot:"table-footer-group",tr:"table-row",td:"table-cell",th:"table-cell"};x.prototype={get:function(e){return this.store.hasOwnProperty(e)?this.store[e]:null},set:function(e,t){return this.store[e]=t}};var gt=0,vt=0,St={getPosition:function(e){var t=this.positions;return t.get(e)||t.set(e,new b(this,e))},toString:function(){return"[NodeWrapper("+U.inspectNode(this.node)+")]"}};P.prototype=St;var Ct="EMPTY",Nt="NON_SPACE",yt="UNCOLLAPSIBLE_SPACE",mt="COLLAPSIBLE_SPACE",xt="TRAILING_SPACE_BEFORE_BLOCK",Tt="TRAILING_SPACE_IN_BLOCK",Pt="TRAILING_SPACE_BEFORE_BR",bt="PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK",wt="TRAILING_LINE_BREAK_AFTER_BR",Rt="INCLUDED_TRAILING_LINE_BREAK_AFTER_BR";K(St,{isCharacterDataNode:T("isCharacterDataNode",U.isCharacterDataNode,"node"),getNodeIndex:T("nodeIndex",U.getNodeIndex,"node"),getLength:T("nodeLength",U.getNodeLength,"node"),containsPositions:T("containsPositions",h,"node"),isWhitespace:T("isWhitespace",C,"node"),isCollapsedWhitespace:T("isCollapsedWhitespace",N,"node"),getComputedDisplay:T("computedDisplay",c,"node"),isCollapsed:T("collapsed",y,"node"),isIgnored:T("ignored",m,"node"),next:T("nextPos",v,"node"),previous:T("previous",S,"node"),getTextNodeInfo:T("textNodeInfo",function(e){var t=null,n=!1,r=pt(e.parentNode,"whiteSpace"),i="pre-line"==r;return i?(t=J,n=!0):("normal"==r||"nowrap"==r)&&(t=Y,n=!0),{node:e,text:e.data,spaceRegex:t,collapseSpaces:n,preLine:i}},"node"),hasInnerText:T("hasInnerText",function(e,t){for(var n=this.session,r=n.getPosition(e.parentNode,this.getNodeIndex()+1),i=n.getPosition(e,0),o=t?r:i,a=t?i:r;o!==a;){if(o.prepopulateChar(),o.isDefinitelyNonEmpty())return!0;o=t?o.previousVisible():o.nextVisible()}return!1},"node"),isRenderedBlock:T("isRenderedBlock",function(e){for(var t=e.getElementsByTagName("br"),n=0,r=t.length;r>n;++n)if(!y(t[n]))return!0;return this.hasInnerText()},"node"),getTrailingSpace:T("trailingSpace",function(e){if("br"==e.tagName.toLowerCase())return"";switch(this.getComputedDisplay()){case"inline":for(var t=e.lastChild;t;){if(!m(t))return 1==t.nodeType?this.session.getNodeWrapper(t).getTrailingSpace():"";t=t.previousSibling}break;case"inline-block":case"inline-table":case"none":case"table-column":case"table-column-group":break;case"table-cell":return" ";default:return this.isRenderedBlock(!0)?"\n":""}return""},"node"),getLeadingSpace:T("leadingSpace",function(){switch(this.getComputedDisplay()){case"inline":case"inline-block":case"inline-table":case"none":case"table-column":case"table-column-group":case"table-cell":break;default:return this.isRenderedBlock(!1)?"\n":""}return""},"node")});var Et={character:"",characterType:Ct,isBr:!1,prepopulateChar:function(){var e=this;if(!e.prepopulatedChar){var t=e.node,n=e.offset,r="",i=Ct,o=!1;if(n>0)if(3==t.nodeType){var a=t.data,s=a.charAt(n-1),c=e.nodeWrapper.getTextNodeInfo(),u=c.spaceRegex;c.collapseSpaces?u.test(s)?n>1&&u.test(a.charAt(n-2))||(c.preLine&&"\n"===a.charAt(n)?(r=" ",i=bt):(r=" ",i=mt)):(r=s,i=Nt,o=!0):(r=s,i=yt,o=!0)}else{var d=t.childNodes[n-1];if(d&&1==d.nodeType&&!y(d)&&("br"==d.tagName.toLowerCase()?(r="\n",e.isBr=!0,i=mt,o=!1):e.checkForTrailingSpace=!0),!r){var l=t.childNodes[n];l&&1==l.nodeType&&!y(l)&&(e.checkForLeadingSpace=!0)}}e.prepopulatedChar=!0,e.character=r,e.characterType=i,e.isCharInvariant=o}},isDefinitelyNonEmpty:function(){var e=this.characterType;return e==Nt||e==yt},resolveLeadingAndTrailingSpaces:function(){if(this.prepopulatedChar||this.prepopulateChar(),this.checkForTrailingSpace){var e=this.session.getNodeWrapper(this.node.childNodes[this.offset-1]).getTrailingSpace();e&&(this.isTrailingSpace=!0,this.character=e,this.characterType=mt),this.checkForTrailingSpace=!1}if(this.checkForLeadingSpace){var t=this.session.getNodeWrapper(this.node.childNodes[this.offset]).getLeadingSpace();t&&(this.isLeadingSpace=!0,this.character=t,this.characterType=mt),this.checkForLeadingSpace=!1}},getPrecedingUncollapsedPosition:function(e){for(var t,n=this;n=n.previousVisible();)if(t=n.getCharacter(e),""!==t)return n;return null},getCharacter:function(e){function t(){return p||(d=f.getPrecedingUncollapsedPosition(e),p=!0),d}this.resolveLeadingAndTrailingSpaces();var n,r=this.character,i=o(e.ignoreCharacters),a=""!==r&&i.indexOf(r)>-1;if(this.isCharInvariant)return n=a?"":r;var s=["character",e.includeSpaceBeforeBr,e.includeBlockContentTrailingSpace,e.includePreLineTrailingSpace,i].join("_"),c=this.cache.get(s);if(null!==c)return c;var u,d,l="",h=this.characterType==mt,p=!1,f=this;return h&&(this.type==Rt?l="\n":" "==r&&(!t()||d.isTrailingSpace||"\n"==d.character||" "==d.character&&d.characterType==mt)||("\n"==r&&this.isLeadingSpace?t()&&"\n"!=d.character&&(l="\n"):(u=this.nextUncollapsed(),u&&(u.isBr?this.type=Pt:u.isTrailingSpace&&"\n"==u.character?this.type=Tt:u.isLeadingSpace&&"\n"==u.character&&(this.type=xt),"\n"==u.character?(this.type!=Pt||e.includeSpaceBeforeBr)&&(this.type!=xt||e.includeSpaceBeforeBlock)&&(this.type==Tt&&u.isTrailingSpace&&!e.includeBlockContentTrailingSpace||(this.type!=bt||u.type!=Nt||e.includePreLineTrailingSpace)&&("\n"==r?u.isTrailingSpace?this.isTrailingSpace||this.isBr&&(u.type=wt,t()&&d.isLeadingSpace&&!d.isTrailingSpace&&"\n"==d.character?u.character="":u.type=Rt):l="\n":" "==r&&(l=" "))):l=r)))),i.indexOf(l)>-1&&(l=""),this.cache.set(s,l),l},equals:function(e){return!!e&&this.node===e.node&&this.offset===e.offset},inspect:w,toString:function(){return this.character}};b.prototype=Et,K(Et,{next:T("nextPos",function(e){var t=e.nodeWrapper,n=e.node,r=e.offset,i=t.session;if(!n)return null;var o,a,s;return r==t.getLength()?(o=n.parentNode,a=o?t.getNodeIndex()+1:0):t.isCharacterDataNode()?(o=n,a=r+1):(s=n.childNodes[r],i.getNodeWrapper(s).containsPositions()?(o=s,a=0):(o=n,a=r+1)),o?i.getPosition(o,a):null}),previous:T("previous",function(e){var t,n,r,i=e.nodeWrapper,o=e.node,a=e.offset,s=i.session;return 0==a?(t=o.parentNode,n=t?i.getNodeIndex():0):i.isCharacterDataNode()?(t=o,n=a-1):(r=o.childNodes[a-1],s.getNodeWrapper(r).containsPositions()?(t=r,n=U.getNodeLength(r)):(t=o,n=a-1)),t?s.getPosition(t,n):null}),nextVisible:T("nextVisible",function(e){var t=e.next();if(!t)return null;var n=t.nodeWrapper,r=t.node,i=t;return n.isCollapsed()&&(i=n.session.getPosition(r.parentNode,n.getNodeIndex()+1)),i}),nextUncollapsed:T("nextUncollapsed",function(e){for(var t=e;t=t.nextVisible();)if(t.resolveLeadingAndTrailingSpaces(),""!==t.character)return t;return null}),previousVisible:T("previousVisible",function(e){var t=e.previous();if(!t)return null;var n=t.nodeWrapper,r=t.node,i=t;return n.isCollapsed()&&(i=n.session.getPosition(r.parentNode,n.getNodeIndex())),i})});var Bt=null,Ot=function(){function e(e){var t=new x;return{get:function(n){var r=t.get(n[e]);if(r)for(var i,o=0;i=r[o++];)if(i.node===n)return i;return null},set:function(n){var r=n.node[e],i=t.get(r)||t.set(r,[]);i.push(n)}}}function t(){this.initCaches()}var n=G.isHostProperty(document.documentElement,"uniqueID");return t.prototype={initCaches:function(){this.elementCache=n?function(){var e=new x;return{get:function(t){return e.get(t.uniqueID)},set:function(t){e.set(t.node.uniqueID,t)}}}():e("tagName"),this.textNodeCache=e("data"),this.otherNodeCache=e("nodeName")},getNodeWrapper:function(e){var t;switch(e.nodeType){case 1:t=this.elementCache;break;case 3:t=this.textNodeCache;break;default:t=this.otherNodeCache}var n=t.get(e);return n||(n=new P(e,this),t.set(n)),n},getPosition:function(e,t){return this.getNodeWrapper(e).getPosition(t)},getRangeBoundaryPosition:function(e,t){var n=t?"start":"end";return this.getPosition(e[n+"Container"],e[n+"Offset"])},detach:function(){this.elementCache=this.textNodeCache=this.otherNodeCache=null}},t}();K(U,{nextNode:v,previousNode:S});var kt=Array.prototype.indexOf?function(e,t){return e.indexOf(t)}:function(e,t){for(var n=0,r=e.length;r>n;++n)if(e[n]===t)return n;return-1};K(e.rangePrototype,{moveStart:F(!0,!1),moveEnd:F(!1,!1),move:F(!0,!0),trimStart:V(!0),trimEnd:V(!1),trim:D(function(e,t){var n=this.trimStart(t),r=this.trimEnd(t);return n||r}),expand:D(function(e,t,n){var r=!1;n=s(n,lt);var i=n.characterOptions;if(t||(t=M),t==j){var o,a,c=n.wordOptions,u=e.getRangeBoundaryPosition(this,!0),d=e.getRangeBoundaryPosition(this,!1),l=k(u,i,c),h=l.nextEndToken(),p=h.chars[0].previousVisible();if(this.collapsed)o=h;else{var f=k(d,i,c);o=f.previousStartToken()}return a=o.chars[o.chars.length-1],p.equals(u)||(this.setStart(p.node,p.offset),r=!0),a&&!a.equals(d)&&(this.setEnd(a.node,a.offset),r=!0),n.trim&&(n.trimStart&&(r=this.trimStart(i)||r),n.trimEnd&&(r=this.trimEnd(i)||r)),r}return this.moveEnd(M,1,n)}),text:D(function(e,t){return this.collapsed?"":A(e,this,z(t,at)).join("")}),selectCharacters:D(function(e,t,n,r,i){var o={characterOptions:i};t||(t=H(this.getDocument())),this.selectNodeContents(t),this.collapse(!0),this.moveStart("character",n,o),this.collapse(!0),this.moveEnd("character",r-n,o)}),toCharacterRange:D(function(e,t,n){t||(t=H(this.getDocument()));var r,i,o=t.parentNode,a=U.getNodeIndex(t),s=-1==U.comparePoints(this.startContainer,this.endContainer,o,a),c=this.cloneRange();return s?(c.setStartAndEnd(this.startContainer,this.startOffset,o,a),r=-c.text(n).length):(c.setStartAndEnd(o,a,this.startContainer,this.startOffset),r=c.text(n).length),i=r+this.text(n).length,{start:r,end:i}}),findText:D(function(t,n,r){r=s(r,ut),r.wholeWordsOnly&&(r.wordOptions.includeTrailingSpace=!1);var i=et(r.direction),o=r.withinRange;o||(o=e.createRange(),o.selectNodeContents(this.getDocument()));var a=n,c=!1;"string"==typeof a?r.caseSensitive||(a=a.toLowerCase()):c=!0;var u=t.getRangeBoundaryPosition(this,!i),d=o.comparePoint(u.node,u.offset);-1===d?u=t.getRangeBoundaryPosition(o,!0):1===d&&(u=t.getRangeBoundaryPosition(o,!1));for(var l,h=u,p=!1;;)if(l=_(h,a,c,o,r)){if(l.valid)return this.setStartAndEnd(l.startPos.node,l.startPos.offset,l.endPos.node,l.endPos.offset),!0;h=i?l.startPos:l.endPos}else{if(!r.wrap||p)return!1;o=o.cloneRange(),h=t.getRangeBoundaryPosition(o,!i),o.setBoundary(u.node,u.offset,i),p=!0}}),pasteHtml:function(e){if(this.deleteContents(),e){var t=this.createContextualFragment(e),n=t.lastChild;this.insertNode(t),this.collapseAfter(n)}}}),K(e.selectionPrototype,{expand:D(function(e,t,n){this.changeEachRange(function(e){e.expand(t,n)})}),move:D(function(e,t,n,r){var i=0;if(this.focusNode){this.collapse(this.focusNode,this.focusOffset);var o=this.getRangeAt(0);r||(r={}),r.characterOptions=z(r.characterOptions,st),i=o.move(t,n,r),this.setSingleRange(o)}return i}),trimStart:$("trimStart"),trimEnd:$("trimEnd"),trim:$("trim"),selectCharacters:D(function(t,n,r,i,o,a){var s=e.createRange(n);s.selectCharacters(n,r,i,a),this.setSingleRange(s,o)}),saveCharacterRanges:D(function(e,t,n){for(var r=this.getAllRanges(),i=r.length,o=[],a=1==i&&this.isBackward(),s=0,c=r.length;c>s;++s)o[s]={characterRange:r[s].toCharacterRange(t,n),backward:a,characterOptions:n};return o}),restoreCharacterRanges:D(function(t,n,r){this.removeAllRanges();for(var i,o,a,s=0,c=r.length;c>s;++s)o=r[s],a=o.characterRange,i=e.createRange(n),i.selectCharacters(n,a.start,a.end,o.characterOptions),this.addRange(i,o.backward)}),text:D(function(e,t){for(var n=[],r=0,i=this.rangeCount;i>r;++r)n[r]=this.getRangeAt(r).text(t);return n.join("")})}),e.innerText=function(t,n){var r=e.createRange(t);r.selectNodeContents(t);var i=r.text(n);return i},e.createWordIterator=function(e,t,n){var r=E();n=s(n,ht);var i=r.getPosition(e,t),o=k(i,n.characterOptions,n.wordOptions),a=et(n.direction);return{next:function(){return a?o.previousStartToken():o.nextEndToken()},dispose:function(){o.dispose(),this.next=function(){}}}},e.noMutation=function(e){var t=E();e(t),B()},e.noMutation.createEntryPointFunction=D,e.textRange={isBlockNode:l,isCollapsedWhitespaceNode:N,createPosition:D(function(e,t,n){return e.getPosition(t,n)})}}),e},this); \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/shortcode.js b/libs/editor/WordPressEditor/src/main/assets/libs/shortcode.js
new file mode 100644
index 000000000..acdcdb4c8
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/shortcode.js
@@ -0,0 +1,358 @@
+// WordPress/wp-includes/js/shortcode.js
+
+// Utility functions for parsing and handling shortcodes in Javascript.
+
+// Ensure the global `wp` object exists.
+window.wp = window.wp || {};
+
+(function(){
+ wp.shortcode = {
+ // ### Find the next matching shortcode
+ //
+ // Given a shortcode `tag`, a block of `text`, and an optional starting
+ // `index`, returns the next matching shortcode or `undefined`.
+ //
+ // Shortcodes are formatted as an object that contains the match
+ // `content`, the matching `index`, and the parsed `shortcode` object.
+ next: function( tag, text, index ) {
+ var re = wp.shortcode.regexp( tag ),
+ match, result;
+
+ re.lastIndex = index || 0;
+ match = re.exec( text );
+
+ if ( ! match ) {
+ return;
+ }
+
+ // If we matched an escaped shortcode, try again.
+ if ( '[' === match[1] && ']' === match[7] ) {
+ return wp.shortcode.next( tag, text, re.lastIndex );
+ }
+
+ result = {
+ index: match.index,
+ content: match[0],
+ shortcode: wp.shortcode.fromMatch( match )
+ };
+
+ // If we matched a leading `[`, strip it from the match
+ // and increment the index accordingly.
+ if ( match[1] ) {
+ result.content = result.content.slice( 1 );
+ result.index++;
+ }
+
+ // If we matched a trailing `]`, strip it from the match.
+ if ( match[7] ) {
+ result.content = result.content.slice( 0, -1 );
+ }
+
+ return result;
+ },
+
+ // ### Replace matching shortcodes in a block of text
+ //
+ // Accepts a shortcode `tag`, content `text` to scan, and a `callback`
+ // to process the shortcode matches and return a replacement string.
+ // Returns the `text` with all shortcodes replaced.
+ //
+ // Shortcode matches are objects that contain the shortcode `tag`,
+ // a shortcode `attrs` object, the `content` between shortcode tags,
+ // and a boolean flag to indicate if the match was a `single` tag.
+ replace: function( tag, text, callback ) {
+ return text.replace( wp.shortcode.regexp( tag ), function( match, left, tag, attrs, slash, content, closing, right ) {
+ // If both extra brackets exist, the shortcode has been
+ // properly escaped.
+ if ( left === '[' && right === ']' ) {
+ return match;
+ }
+
+ // Create the match object and pass it through the callback.
+ var result = callback( wp.shortcode.fromMatch( arguments ) );
+
+ // Make sure to return any of the extra brackets if they
+ // weren't used to escape the shortcode.
+ return result ? left + result + right : match;
+ });
+ },
+
+ // ### Generate a string from shortcode parameters
+ //
+ // Creates a `wp.shortcode` instance and returns a string.
+ //
+ // Accepts the same `options` as the `wp.shortcode()` constructor,
+ // containing a `tag` string, a string or object of `attrs`, a boolean
+ // indicating whether to format the shortcode using a `single` tag, and a
+ // `content` string.
+ string: function( options ) {
+ return new wp.shortcode( options ).string();
+ },
+
+ // ### Generate a RegExp to identify a shortcode
+ //
+ // The base regex is functionally equivalent to the one found in
+ // `get_shortcode_regex()` in `wp-includes/shortcodes.php`.
+ //
+ // Capture groups:
+ //
+ // 1. An extra `[` to allow for escaping shortcodes with double `[[]]`
+ // 2. The shortcode name
+ // 3. The shortcode argument list
+ // 4. The self closing `/`
+ // 5. The content of a shortcode when it wraps some content.
+ // 6. The closing tag.
+ // 7. An extra `]` to allow for escaping shortcodes with double `[[]]`
+ regexp: _.memoize( function( tag ) {
+ return new RegExp( '\\[(\\[?)(' + tag + ')(?![\\w-])([^\\]\\/]*(?:\\/(?!\\])[^\\]\\/]*)*?)(?:(\\/)\\]|\\](?:([^\\[]*(?:\\[(?!\\/\\2\\])[^\\[]*)*)(\\[\\/\\2\\]))?)(\\]?)', 'g' );
+ }),
+
+
+ // ### Parse shortcode attributes
+ //
+ // Shortcodes accept many types of attributes. These can chiefly be
+ // divided into named and numeric attributes:
+ //
+ // Named attributes are assigned on a key/value basis, while numeric
+ // attributes are treated as an array.
+ //
+ // Named attributes can be formatted as either `name="value"`,
+ // `name='value'`, or `name=value`. Numeric attributes can be formatted
+ // as `"value"` or just `value`.
+ attrs: _.memoize( function( text ) {
+ var named = {},
+ numeric = [],
+ pattern, match;
+
+ // This regular expression is reused from `shortcode_parse_atts()`
+ // in `wp-includes/shortcodes.php`.
+ //
+ // Capture groups:
+ //
+ // 1. An attribute name, that corresponds to...
+ // 2. a value in double quotes.
+ // 3. An attribute name, that corresponds to...
+ // 4. a value in single quotes.
+ // 5. An attribute name, that corresponds to...
+ // 6. an unquoted value.
+ // 7. A numeric attribute in double quotes.
+ // 8. An unquoted numeric attribute.
+ pattern = /(\w+)\s*=\s*"([^"]*)"(?:\s|$)|(\w+)\s*=\s*\'([^\']*)\'(?:\s|$)|(\w+)\s*=\s*([^\s\'"]+)(?:\s|$)|"([^"]*)"(?:\s|$)|(\S+)(?:\s|$)/g;
+
+ // Map zero-width spaces to actual spaces.
+ text = text.replace( /[\u00a0\u200b]/g, ' ' );
+
+ // Match and normalize attributes.
+ while ( (match = pattern.exec( text )) ) {
+ if ( match[1] ) {
+ named[ match[1].toLowerCase() ] = match[2];
+ } else if ( match[3] ) {
+ named[ match[3].toLowerCase() ] = match[4];
+ } else if ( match[5] ) {
+ named[ match[5].toLowerCase() ] = match[6];
+ } else if ( match[7] ) {
+ numeric.push( match[7] );
+ } else if ( match[8] ) {
+ numeric.push( match[8] );
+ }
+ }
+
+ return {
+ named: named,
+ numeric: numeric
+ };
+ }),
+
+ // ### Generate a Shortcode Object from a RegExp match
+ // Accepts a `match` object from calling `regexp.exec()` on a `RegExp`
+ // generated by `wp.shortcode.regexp()`. `match` can also be set to the
+ // `arguments` from a callback passed to `regexp.replace()`.
+ fromMatch: function( match ) {
+ var type;
+
+ if ( match[4] ) {
+ type = 'self-closing';
+ } else if ( match[6] ) {
+ type = 'closed';
+ } else {
+ type = 'single';
+ }
+
+ return new wp.shortcode({
+ tag: match[2],
+ attrs: match[3],
+ type: type,
+ content: match[5]
+ });
+ }
+ };
+
+
+ // Shortcode Objects
+ // -----------------
+ //
+ // Shortcode objects are generated automatically when using the main
+ // `wp.shortcode` methods: `next()`, `replace()`, and `string()`.
+ //
+ // To access a raw representation of a shortcode, pass an `options` object,
+ // containing a `tag` string, a string or object of `attrs`, a string
+ // indicating the `type` of the shortcode ('single', 'self-closing', or
+ // 'closed'), and a `content` string.
+ wp.shortcode = _.extend( function( options ) {
+ _.extend( this, _.pick( options || {}, 'tag', 'attrs', 'type', 'content' ) );
+
+ var attrs = this.attrs;
+
+ // Ensure we have a correctly formatted `attrs` object.
+ this.attrs = {
+ named: {},
+ numeric: []
+ };
+
+ if ( ! attrs ) {
+ return;
+ }
+
+ // Parse a string of attributes.
+ if ( _.isString( attrs ) ) {
+ this.attrs = wp.shortcode.attrs( attrs );
+
+ // Identify a correctly formatted `attrs` object.
+ } else if ( _.isEqual( _.keys( attrs ), [ 'named', 'numeric' ] ) ) {
+ this.attrs = attrs;
+
+ // Handle a flat object of attributes.
+ } else {
+ _.each( options.attrs, function( value, key ) {
+ this.set( key, value );
+ }, this );
+ }
+ }, wp.shortcode );
+
+ _.extend( wp.shortcode.prototype, {
+ // ### Get a shortcode attribute
+ //
+ // Automatically detects whether `attr` is named or numeric and routes
+ // it accordingly.
+ get: function( attr ) {
+ return this.attrs[ _.isNumber( attr ) ? 'numeric' : 'named' ][ attr ];
+ },
+
+ // ### Set a shortcode attribute
+ //
+ // Automatically detects whether `attr` is named or numeric and routes
+ // it accordingly.
+ set: function( attr, value ) {
+ this.attrs[ _.isNumber( attr ) ? 'numeric' : 'named' ][ attr ] = value;
+ return this;
+ },
+
+ // ### Transform the shortcode match into a string
+ string: function() {
+ var text = '[' + this.tag;
+
+ _.each( this.attrs.numeric, function( value ) {
+ if ( /\s/.test( value ) ) {
+ text += ' "' + value + '"';
+ } else {
+ text += ' ' + value;
+ }
+ });
+
+ _.each( this.attrs.named, function( value, name ) {
+ text += ' ' + name + '="' + value + '"';
+ });
+
+ // If the tag is marked as `single` or `self-closing`, close the
+ // tag and ignore any additional content.
+ if ( 'single' === this.type ) {
+ return text + ']';
+ } else if ( 'self-closing' === this.type ) {
+ return text + ' /]';
+ }
+
+ // Complete the opening tag.
+ text += ']';
+
+ if ( this.content ) {
+ text += this.content;
+ }
+
+ // Add the closing tag.
+ return text + '[/' + this.tag + ']';
+ }
+ });
+}());
+
+// HTML utility functions
+// ----------------------
+//
+// Experimental. These functions may change or be removed in the future.
+(function(){
+ wp.html = _.extend( wp.html || {}, {
+ // ### Parse HTML attributes.
+ //
+ // Converts `content` to a set of parsed HTML attributes.
+ // Utilizes `wp.shortcode.attrs( content )`, which is a valid superset of
+ // the HTML attribute specification. Reformats the attributes into an
+ // object that contains the `attrs` with `key:value` mapping, and a record
+ // of the attributes that were entered using `empty` attribute syntax (i.e.
+ // with no value).
+ attrs: function( content ) {
+ var result, attrs;
+
+ // If `content` ends in a slash, strip it.
+ if ( '/' === content[ content.length - 1 ] ) {
+ content = content.slice( 0, -1 );
+ }
+
+ result = wp.shortcode.attrs( content );
+ attrs = result.named;
+
+ _.each( result.numeric, function( key ) {
+ if ( /\s/.test( key ) ) {
+ return;
+ }
+
+ attrs[ key ] = '';
+ });
+
+ return attrs;
+ },
+
+ // ### Convert an HTML-representation of an object to a string.
+ string: function( options ) {
+ var text = '<' + options.tag,
+ content = options.content || '';
+
+ _.each( options.attrs, function( value, attr ) {
+ text += ' ' + attr;
+
+ // Use empty attribute notation where possible.
+ if ( '' === value ) {
+ return;
+ }
+
+ // Convert boolean values to strings.
+ if ( _.isBoolean( value ) ) {
+ value = value ? 'true' : 'false';
+ }
+
+ text += '="' + value + '"';
+ });
+
+ // Return the result if it is a self-closing tag.
+ if ( options.single ) {
+ return text + ' />';
+ }
+
+ // Complete the opening tag.
+ text += '>';
+
+ // If `content` is an object, recursively call this function.
+ text += _.isObject( content ) ? wp.html.string( content ) : content;
+
+ return text + '</' + options.tag + '>';
+ }
+ });
+}());
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/underscore-min.js b/libs/editor/WordPressEditor/src/main/assets/libs/underscore-min.js
new file mode 100644
index 000000000..ed15a637c
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/underscore-min.js
@@ -0,0 +1,6 @@
+// Underscore.js 1.7.0
+// http://underscorejs.org
+// (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+// Underscore may be freely distributed under the MIT license.
+(function(){var n=this,t=n._,r=Array.prototype,e=Object.prototype,u=Function.prototype,i=r.push,a=r.slice,o=r.concat,l=e.toString,c=e.hasOwnProperty,f=Array.isArray,s=Object.keys,p=u.bind,h=function(n){return n instanceof h?n:this instanceof h?void(this._wrapped=n):new h(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=h),exports._=h):n._=h,h.VERSION="1.7.0";var g=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}};h.iteratee=function(n,t,r){return null==n?h.identity:h.isFunction(n)?g(n,t,r):h.isObject(n)?h.matches(n):h.property(n)},h.each=h.forEach=function(n,t,r){if(null==n)return n;t=g(t,r);var e,u=n.length;if(u===+u)for(e=0;u>e;e++)t(n[e],e,n);else{var i=h.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},h.map=h.collect=function(n,t,r){if(null==n)return[];t=h.iteratee(t,r);for(var e,u=n.length!==+n.length&&h.keys(n),i=(u||n).length,a=Array(i),o=0;i>o;o++)e=u?u[o]:o,a[o]=t(n[e],e,n);return a};var v="Reduce of empty array with no initial value";h.reduce=h.foldl=h.inject=function(n,t,r,e){null==n&&(n=[]),t=g(t,e,4);var u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length,o=0;if(arguments.length<3){if(!a)throw new TypeError(v);r=n[i?i[o++]:o++]}for(;a>o;o++)u=i?i[o]:o,r=t(r,n[u],u,n);return r},h.reduceRight=h.foldr=function(n,t,r,e){null==n&&(n=[]),t=g(t,e,4);var u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;if(arguments.length<3){if(!a)throw new TypeError(v);r=n[i?i[--a]:--a]}for(;a--;)u=i?i[a]:a,r=t(r,n[u],u,n);return r},h.find=h.detect=function(n,t,r){var e;return t=h.iteratee(t,r),h.some(n,function(n,r,u){return t(n,r,u)?(e=n,!0):void 0}),e},h.filter=h.select=function(n,t,r){var e=[];return null==n?e:(t=h.iteratee(t,r),h.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e)},h.reject=function(n,t,r){return h.filter(n,h.negate(h.iteratee(t)),r)},h.every=h.all=function(n,t,r){if(null==n)return!0;t=h.iteratee(t,r);var e,u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;for(e=0;a>e;e++)if(u=i?i[e]:e,!t(n[u],u,n))return!1;return!0},h.some=h.any=function(n,t,r){if(null==n)return!1;t=h.iteratee(t,r);var e,u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;for(e=0;a>e;e++)if(u=i?i[e]:e,t(n[u],u,n))return!0;return!1},h.contains=h.include=function(n,t){return null==n?!1:(n.length!==+n.length&&(n=h.values(n)),h.indexOf(n,t)>=0)},h.invoke=function(n,t){var r=a.call(arguments,2),e=h.isFunction(t);return h.map(n,function(n){return(e?t:n[t]).apply(n,r)})},h.pluck=function(n,t){return h.map(n,h.property(t))},h.where=function(n,t){return h.filter(n,h.matches(t))},h.findWhere=function(n,t){return h.find(n,h.matches(t))},h.max=function(n,t,r){var e,u,i=-1/0,a=-1/0;if(null==t&&null!=n){n=n.length===+n.length?n:h.values(n);for(var o=0,l=n.length;l>o;o++)e=n[o],e>i&&(i=e)}else t=h.iteratee(t,r),h.each(n,function(n,r,e){u=t(n,r,e),(u>a||u===-1/0&&i===-1/0)&&(i=n,a=u)});return i},h.min=function(n,t,r){var e,u,i=1/0,a=1/0;if(null==t&&null!=n){n=n.length===+n.length?n:h.values(n);for(var o=0,l=n.length;l>o;o++)e=n[o],i>e&&(i=e)}else t=h.iteratee(t,r),h.each(n,function(n,r,e){u=t(n,r,e),(a>u||1/0===u&&1/0===i)&&(i=n,a=u)});return i},h.shuffle=function(n){for(var t,r=n&&n.length===+n.length?n:h.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=h.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},h.sample=function(n,t,r){return null==t||r?(n.length!==+n.length&&(n=h.values(n)),n[h.random(n.length-1)]):h.shuffle(n).slice(0,Math.max(0,t))},h.sortBy=function(n,t,r){return t=h.iteratee(t,r),h.pluck(h.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var m=function(n){return function(t,r,e){var u={};return r=h.iteratee(r,e),h.each(t,function(e,i){var a=r(e,i,t);n(u,e,a)}),u}};h.groupBy=m(function(n,t,r){h.has(n,r)?n[r].push(t):n[r]=[t]}),h.indexBy=m(function(n,t,r){n[r]=t}),h.countBy=m(function(n,t,r){h.has(n,r)?n[r]++:n[r]=1}),h.sortedIndex=function(n,t,r,e){r=h.iteratee(r,e,1);for(var u=r(t),i=0,a=n.length;a>i;){var o=i+a>>>1;r(n[o])<u?i=o+1:a=o}return i},h.toArray=function(n){return n?h.isArray(n)?a.call(n):n.length===+n.length?h.map(n,h.identity):h.values(n):[]},h.size=function(n){return null==n?0:n.length===+n.length?n.length:h.keys(n).length},h.partition=function(n,t,r){t=h.iteratee(t,r);var e=[],u=[];return h.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},h.first=h.head=h.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:0>t?[]:a.call(n,0,t)},h.initial=function(n,t,r){return a.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},h.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:a.call(n,Math.max(n.length-t,0))},h.rest=h.tail=h.drop=function(n,t,r){return a.call(n,null==t||r?1:t)},h.compact=function(n){return h.filter(n,h.identity)};var y=function(n,t,r,e){if(t&&h.every(n,h.isArray))return o.apply(e,n);for(var u=0,a=n.length;a>u;u++){var l=n[u];h.isArray(l)||h.isArguments(l)?t?i.apply(e,l):y(l,t,r,e):r||e.push(l)}return e};h.flatten=function(n,t){return y(n,t,!1,[])},h.without=function(n){return h.difference(n,a.call(arguments,1))},h.uniq=h.unique=function(n,t,r,e){if(null==n)return[];h.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=h.iteratee(r,e));for(var u=[],i=[],a=0,o=n.length;o>a;a++){var l=n[a];if(t)a&&i===l||u.push(l),i=l;else if(r){var c=r(l,a,n);h.indexOf(i,c)<0&&(i.push(c),u.push(l))}else h.indexOf(u,l)<0&&u.push(l)}return u},h.union=function(){return h.uniq(y(arguments,!0,!0,[]))},h.intersection=function(n){if(null==n)return[];for(var t=[],r=arguments.length,e=0,u=n.length;u>e;e++){var i=n[e];if(!h.contains(t,i)){for(var a=1;r>a&&h.contains(arguments[a],i);a++);a===r&&t.push(i)}}return t},h.difference=function(n){var t=y(a.call(arguments,1),!0,!0,[]);return h.filter(n,function(n){return!h.contains(t,n)})},h.zip=function(n){if(null==n)return[];for(var t=h.max(arguments,"length").length,r=Array(t),e=0;t>e;e++)r[e]=h.pluck(arguments,e);return r},h.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},h.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=h.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}for(;u>e;e++)if(n[e]===t)return e;return-1},h.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=n.length;for("number"==typeof r&&(e=0>r?e+r+1:Math.min(e,r+1));--e>=0;)if(n[e]===t)return e;return-1},h.range=function(n,t,r){arguments.length<=1&&(t=n||0,n=0),r=r||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=Array(e),i=0;e>i;i++,n+=r)u[i]=n;return u};var d=function(){};h.bind=function(n,t){var r,e;if(p&&n.bind===p)return p.apply(n,a.call(arguments,1));if(!h.isFunction(n))throw new TypeError("Bind must be called on a function");return r=a.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(a.call(arguments)));d.prototype=n.prototype;var u=new d;d.prototype=null;var i=n.apply(u,r.concat(a.call(arguments)));return h.isObject(i)?i:u}},h.partial=function(n){var t=a.call(arguments,1);return function(){for(var r=0,e=t.slice(),u=0,i=e.length;i>u;u++)e[u]===h&&(e[u]=arguments[r++]);for(;r<arguments.length;)e.push(arguments[r++]);return n.apply(this,e)}},h.bindAll=function(n){var t,r,e=arguments.length;if(1>=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=h.bind(n[r],n);return n},h.memoize=function(n,t){var r=function(e){var u=r.cache,i=t?t.apply(this,arguments):e;return h.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},h.delay=function(n,t){var r=a.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},h.defer=function(n){return h.delay.apply(h,[n,1].concat(a.call(arguments,1)))},h.throttle=function(n,t,r){var e,u,i,a=null,o=0;r||(r={});var l=function(){o=r.leading===!1?0:h.now(),a=null,i=n.apply(e,u),a||(e=u=null)};return function(){var c=h.now();o||r.leading!==!1||(o=c);var f=t-(c-o);return e=this,u=arguments,0>=f||f>t?(clearTimeout(a),a=null,o=c,i=n.apply(e,u),a||(e=u=null)):a||r.trailing===!1||(a=setTimeout(l,f)),i}},h.debounce=function(n,t,r){var e,u,i,a,o,l=function(){var c=h.now()-a;t>c&&c>0?e=setTimeout(l,t-c):(e=null,r||(o=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,a=h.now();var c=r&&!e;return e||(e=setTimeout(l,t)),c&&(o=n.apply(i,u),i=u=null),o}},h.wrap=function(n,t){return h.partial(t,n)},h.negate=function(n){return function(){return!n.apply(this,arguments)}},h.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},h.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},h.before=function(n,t){var r;return function(){return--n>0?r=t.apply(this,arguments):t=null,r}},h.once=h.partial(h.before,2),h.keys=function(n){if(!h.isObject(n))return[];if(s)return s(n);var t=[];for(var r in n)h.has(n,r)&&t.push(r);return t},h.values=function(n){for(var t=h.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},h.pairs=function(n){for(var t=h.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},h.invert=function(n){for(var t={},r=h.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},h.functions=h.methods=function(n){var t=[];for(var r in n)h.isFunction(n[r])&&t.push(r);return t.sort()},h.extend=function(n){if(!h.isObject(n))return n;for(var t,r,e=1,u=arguments.length;u>e;e++){t=arguments[e];for(r in t)c.call(t,r)&&(n[r]=t[r])}return n},h.pick=function(n,t,r){var e,u={};if(null==n)return u;if(h.isFunction(t)){t=g(t,r);for(e in n){var i=n[e];t(i,e,n)&&(u[e]=i)}}else{var l=o.apply([],a.call(arguments,1));n=new Object(n);for(var c=0,f=l.length;f>c;c++)e=l[c],e in n&&(u[e]=n[e])}return u},h.omit=function(n,t,r){if(h.isFunction(t))t=h.negate(t);else{var e=h.map(o.apply([],a.call(arguments,1)),String);t=function(n,t){return!h.contains(e,t)}}return h.pick(n,t,r)},h.defaults=function(n){if(!h.isObject(n))return n;for(var t=1,r=arguments.length;r>t;t++){var e=arguments[t];for(var u in e)n[u]===void 0&&(n[u]=e[u])}return n},h.clone=function(n){return h.isObject(n)?h.isArray(n)?n.slice():h.extend({},n):n},h.tap=function(n,t){return t(n),n};var b=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof h&&(n=n._wrapped),t instanceof h&&(t=t._wrapped);var u=l.call(n);if(u!==l.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]===n)return e[i]===t;var a=n.constructor,o=t.constructor;if(a!==o&&"constructor"in n&&"constructor"in t&&!(h.isFunction(a)&&a instanceof a&&h.isFunction(o)&&o instanceof o))return!1;r.push(n),e.push(t);var c,f;if("[object Array]"===u){if(c=n.length,f=c===t.length)for(;c--&&(f=b(n[c],t[c],r,e)););}else{var s,p=h.keys(n);if(c=p.length,f=h.keys(t).length===c)for(;c--&&(s=p[c],f=h.has(t,s)&&b(n[s],t[s],r,e)););}return r.pop(),e.pop(),f};h.isEqual=function(n,t){return b(n,t,[],[])},h.isEmpty=function(n){if(null==n)return!0;if(h.isArray(n)||h.isString(n)||h.isArguments(n))return 0===n.length;for(var t in n)if(h.has(n,t))return!1;return!0},h.isElement=function(n){return!(!n||1!==n.nodeType)},h.isArray=f||function(n){return"[object Array]"===l.call(n)},h.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},h.each(["Arguments","Function","String","Number","Date","RegExp"],function(n){h["is"+n]=function(t){return l.call(t)==="[object "+n+"]"}}),h.isArguments(arguments)||(h.isArguments=function(n){return h.has(n,"callee")}),"function"!=typeof/./&&(h.isFunction=function(n){return"function"==typeof n||!1}),h.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},h.isNaN=function(n){return h.isNumber(n)&&n!==+n},h.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===l.call(n)},h.isNull=function(n){return null===n},h.isUndefined=function(n){return n===void 0},h.has=function(n,t){return null!=n&&c.call(n,t)},h.noConflict=function(){return n._=t,this},h.identity=function(n){return n},h.constant=function(n){return function(){return n}},h.noop=function(){},h.property=function(n){return function(t){return t[n]}},h.matches=function(n){var t=h.pairs(n),r=t.length;return function(n){if(null==n)return!r;n=new Object(n);for(var e=0;r>e;e++){var u=t[e],i=u[0];if(u[1]!==n[i]||!(i in n))return!1}return!0}},h.times=function(n,t,r){var e=Array(Math.max(0,n));t=g(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},h.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},h.now=Date.now||function(){return(new Date).getTime()};var _={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;","`":"&#x60;"},w=h.invert(_),j=function(n){var t=function(t){return n[t]},r="(?:"+h.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};h.escape=j(_),h.unescape=j(w),h.result=function(n,t){if(null==n)return void 0;var r=n[t];return h.isFunction(r)?n[t]():r};var x=0;h.uniqueId=function(n){var t=++x+"";return n?n+t:t},h.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var A=/(.)^/,k={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},O=/\\|'|\r|\n|\u2028|\u2029/g,F=function(n){return"\\"+k[n]};h.template=function(n,t,r){!t&&r&&(t=r),t=h.defaults({},t,h.templateSettings);var e=RegExp([(t.escape||A).source,(t.interpolate||A).source,(t.evaluate||A).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,a,o){return i+=n.slice(u,o).replace(O,F),u=o+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":a&&(i+="';\n"+a+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var a=new Function(t.variable||"obj","_",i)}catch(o){throw o.source=i,o}var l=function(n){return a.call(this,n,h)},c=t.variable||"obj";return l.source="function("+c+"){\n"+i+"}",l},h.chain=function(n){var t=h(n);return t._chain=!0,t};var E=function(n){return this._chain?h(n).chain():n};h.mixin=function(n){h.each(h.functions(n),function(t){var r=h[t]=n[t];h.prototype[t]=function(){var n=[this._wrapped];return i.apply(n,arguments),E.call(this,r.apply(h,n))}})},h.mixin(h),h.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=r[n];h.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],E.call(this,r)}}),h.each(["concat","join","slice"],function(n){var t=r[n];h.prototype[n]=function(){return E.call(this,t.apply(this._wrapped,arguments))}}),h.prototype.value=function(){return this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return h})}).call(this);
+ //# sourceMappingURL=underscore-min.map \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/wpload.js b/libs/editor/WordPressEditor/src/main/assets/libs/wpload.js
new file mode 100644
index 000000000..1fc74b28c
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/wpload.js
@@ -0,0 +1,108 @@
+/**
+* The code of this function comes from the WordPress source code,
+* from file `wp-admin/js/editor.js`, so we can get 100% consistent results
+* with wp-admin.
+*/
+
+// Ensure the global `wp` object exists.
+window.wp = window.wp || {};
+
+(function(){
+
+ /**
+ * @brief Performs multiple transformations to the post source code when
+ * loading, replacing newlines with pargraph elements.
+ *
+ * @param pee The string to transform
+ *
+ * @return Returns the transformed string
+ */
+ wp.loadText = function( pee ) {
+
+ if ( pee == null || !pee.trim() ) {
+ // Just whitespace, null, or undefined
+ return '';
+ }
+
+ var preserve_linebreaks = false,
+ preserve_br = false,
+ blocklist = 'table|thead|tfoot|caption|col|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre' +
+ '|form|map|area|blockquote|address|math|style|p|h[1-6]|hr|fieldset|legend|section' +
+ '|article|aside|hgroup|header|footer|nav|figure|details|menu|summary';
+
+ if ( pee.indexOf( '<object' ) !== -1 ) {
+ pee = pee.replace( /<object[\s\S]+?<\/object>/g, function( a ) {
+ return a.replace( /[\r\n]+/g, '' );
+ });
+ }
+
+ pee = pee.replace( /<[^<>]+>/g, function( a ){
+ return a.replace( /[\r\n]+/g, ' ' );
+ });
+
+ // Protect pre|script tags
+ if ( pee.indexOf( '<pre' ) !== -1 || pee.indexOf( '<script' ) !== -1 ) {
+ preserve_linebreaks = true;
+ pee = pee.replace( /<(pre|script)[^>]*>[\s\S]+?<\/\1>/g, function( a ) {
+ return a.replace( /(\r\n|\n)/g, '<wp-line-break>' );
+ });
+ }
+
+ // keep <br> tags inside captions and convert line breaks
+ if ( pee.indexOf( '[caption' ) !== -1 ) {
+ preserve_br = true;
+ pee = pee.replace( /\[caption[\s\S]+?\[\/caption\]/g, function( a ) {
+ // keep existing <br>
+ a = a.replace( /<br([^>]*)>/g, '<wp-temp-br$1>' );
+ // no line breaks inside HTML tags
+ a = a.replace( /<[a-zA-Z0-9]+( [^<>]+)?>/g, function( b ) {
+ return b.replace( /[\r\n\t]+/, ' ' );
+ });
+ // convert remaining line breaks to <br>
+ return a.replace( /\s*\n\s*/g, '<wp-temp-br />' );
+ });
+ }
+
+ pee = pee + '\n\n';
+ pee = pee.replace( /<br \/>\s*<br \/>/gi, '\n\n' );
+ pee = pee.replace( new RegExp( '(<(?:' + blocklist + ')(?: [^>]*)?>)', 'gi' ), '\n$1' );
+ pee = pee.replace( new RegExp( '(</(?:' + blocklist + ')>)', 'gi' ), '$1\n\n' );
+ pee = pee.replace( /<hr( [^>]*)?>/gi, '<hr$1>\n\n' ); // hr is self closing block element
+ pee = pee.replace( /\s*<option/gi, '<option' ); // No <p> or <br> around <option>
+ pee = pee.replace( /<\/option>\s*/gi, '</option>' );
+ pee = pee.replace( /\r\n|\r/g, '\n' );
+ pee = pee.replace( /\n\s*\n+/g, '\n\n' );
+ pee = pee.replace( /([\s\S]+?)\n\n/g, '<p>$1</p>\n' );
+ pee = pee.replace( /<p>\s*?<\/p>/gi, '');
+ pee = pee.replace( new RegExp( '<p>\\s*(</?(?:' + blocklist + ')(?: [^>]*)?>)\\s*</p>', 'gi' ), '$1' );
+ pee = pee.replace( /<p>(<li.+?)<\/p>/gi, '$1');
+ pee = pee.replace( /<p>\s*<blockquote([^>]*)>/gi, '<blockquote$1><p>');
+ pee = pee.replace( /<\/blockquote>\s*<\/p>/gi, '</p></blockquote>');
+ pee = pee.replace( new RegExp( '<p>\\s*(</?(?:' + blocklist + ')(?: [^>]*)?>)', 'gi' ), '$1' );
+ pee = pee.replace( new RegExp( '(</?(?:' + blocklist + ')(?: [^>]*)?>)\\s*</p>', 'gi' ), '$1' );
+ pee = pee.replace( /\s*\n/gi, '<br />\n');
+ pee = pee.replace( new RegExp( '(</?(?:' + blocklist + ')[^>]*>)\\s*<br />', 'gi' ), '$1' );
+ pee = pee.replace( /<br \/>(\s*<\/?(?:p|li|div|dl|dd|dt|th|pre|td|ul|ol)>)/gi, '$1' );
+ pee = pee.replace( /(?:<p>|<br ?\/?>)*\s*\[caption([^\[]+)\[\/caption\]\s*(?:<\/p>|<br ?\/?>)*/gi, '[caption$1[/caption]' );
+
+ pee = pee.replace( /(<(?:div|th|td|form|fieldset|dd)[^>]*>)(.*?)<\/p>/g, function( a, b, c ) {
+ if ( c.match( /<p( [^>]*)?>/ ) ) {
+ return a;
+ }
+
+ return b + '<p>' + c + '</p>';
+ });
+
+ // put back the line breaks in pre|script
+ if ( preserve_linebreaks ) {
+ pee = pee.replace( /<wp-line-break>/g, '\n' );
+ }
+
+ if ( preserve_br ) {
+ pee = pee.replace( /<wp-temp-br([^>]*)>/g, '<br$1>' );
+ }
+
+ return pee;
+ }
+
+}());
diff --git a/libs/editor/WordPressEditor/src/main/assets/libs/wpsave.js b/libs/editor/WordPressEditor/src/main/assets/libs/wpsave.js
new file mode 100644
index 000000000..798673d98
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/libs/wpsave.js
@@ -0,0 +1,113 @@
+/**
+* The code of this function comes from the WordPress source code,
+* from file `wp-admin/js/editor.js`, so we can get 100% consistent results
+* with wp-admin.
+*/
+
+// Ensure the global `wp` object exists.
+window.wp = window.wp || {};
+
+(function(){
+
+ /**
+ * @brief Performs multiple transformations to the post source code when
+ * saving, removing paragraphs whenever possible.
+ *
+ * @param content The string to transform
+ *
+ * @return Returns the transformed string
+ */
+ wp.saveText = function( content ) {
+
+ if ( content == null || !content.trim() ) {
+ // Just whitespace, null, or undefined
+ return '';
+ }
+
+ var blocklist1, blocklist2,
+ preserve_linebreaks = false,
+ preserve_br = false;
+
+ // Protect pre|script tags
+ if ( content.indexOf( '<pre' ) !== -1 || content.indexOf( '<script' ) !== -1 ) {
+ preserve_linebreaks = true;
+ content = content.replace( /<(pre|script)[^>]*>[\s\S]+?<\/\1>/g, function( a ) {
+ a = a.replace( /<br ?\/?>(\r\n|\n)?/g, '<wp-line-break>' );
+ a = a.replace( /<\/?p( [^>]*)?>(\r\n|\n)?/g, '<wp-line-break>' );
+ return a.replace( /\r?\n/g, '<wp-line-break>' );
+ });
+ }
+
+ // keep <br> tags inside captions and remove line breaks
+ if ( content.indexOf( '[caption' ) !== -1 ) {
+ preserve_br = true;
+ content = content.replace( /\[caption[\s\S]+?\[\/caption\]/g, function( a ) {
+ return a.replace( /<br([^>]*)>/g, '<wp-temp-br$1>' ).replace( /[\r\n\t]+/, '' );
+ });
+ }
+
+ // Pretty it up for the source editor
+ blocklist1 = 'blockquote|ul|ol|li|table|thead|tbody|tfoot|tr|th|td|div|h[1-6]|p|fieldset';
+ content = content.replace( new RegExp( '\\s*</(' + blocklist1 + ')>\\s*', 'g' ), '</$1>\n' );
+ content = content.replace( new RegExp( '\\s*<((?:' + blocklist1 + ')(?: [^>]*)?)>', 'g' ), '\n<$1>' );
+
+ // Mark </p> if it has any attributes.
+ content = content.replace( /(<p [^>]+>.*?)<\/p>/g, '$1</p#>' );
+
+ // Separate <div> containing <p>
+ content = content.replace( /<div( [^>]*)?>\s*<p>/gi, '<div$1>\n\n' );
+
+ // Remove <p> and <br />
+ content = content.replace( /\s*<p>/gi, '' );
+ content = content.replace( /\s*<\/p>\s*/gi, '\n\n' );
+ content = content.replace( /\n[\s\u00a0]+\n/g, '\n\n' );
+ content = content.replace( /\s*<br ?\/?>\s*/gi, '\n' );
+
+ // Fix some block element newline issues
+ content = content.replace( /\s*<div/g, '\n<div' );
+ content = content.replace( /<\/div>\s*/g, '</div>\n' );
+ content = content.replace( /\s*\[caption([^\[]+)\[\/caption\]\s*/gi, '\n\n[caption$1[/caption]\n\n' );
+ content = content.replace( /caption\]\n\n+\[caption/g, 'caption]\n\n[caption' );
+
+ blocklist2 = 'blockquote|ul|ol|li|table|thead|tbody|tfoot|tr|th|td|h[1-6]|pre|fieldset';
+ content = content.replace( new RegExp('\\s*<((?:' + blocklist2 + ')(?: [^>]*)?)\\s*>', 'g' ), '\n<$1>' );
+ content = content.replace( new RegExp('\\s*</(' + blocklist2 + ')>\\s*', 'g' ), '</$1>\n' );
+ content = content.replace( /<li([^>]*)>/g, '\t<li$1>' );
+
+ if ( content.indexOf( '<option' ) !== -1 ) {
+ content = content.replace( /\s*<option/g, '\n<option' );
+ content = content.replace( /\s*<\/select>/g, '\n</select>' );
+ }
+
+ if ( content.indexOf( '<hr' ) !== -1 ) {
+ content = content.replace( /\s*<hr( [^>]*)?>\s*/g, '\n\n<hr$1>\n\n' );
+ }
+
+ if ( content.indexOf( '<object' ) !== -1 ) {
+ content = content.replace( /<object[\s\S]+?<\/object>/g, function( a ) {
+ return a.replace( /[\r\n]+/g, '' );
+ });
+ }
+
+ // Unmark special paragraph closing tags
+ content = content.replace( /<\/p#>/g, '</p>\n' );
+ content = content.replace( /\s*(<p [^>]+>[\s\S]*?<\/p>)/g, '\n$1' );
+
+ // Trim whitespace
+ content = content.replace( /^\s+/, '' );
+ content = content.replace( /[\s\u00a0]+$/, '' );
+
+ // put back the line breaks in pre|script
+ if ( preserve_linebreaks ) {
+ content = content.replace( /<wp-line-break>/g, '\n' );
+ }
+
+ // and the <br> tags in captions
+ if ( preserve_br ) {
+ content = content.replace( /<wp-temp-br([^>]*)>/g, '<br$1>' );
+ }
+
+ return content;
+ }
+
+}());
diff --git a/libs/editor/WordPressEditor/src/main/assets/svg/delete-image.svg b/libs/editor/WordPressEditor/src/main/assets/svg/delete-image.svg
new file mode 100644
index 000000000..4d961ebbd
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/svg/delete-image.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
+ <title>delete-image</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="gridicons-cross" fill="#FFFFFF">
+ <polygon id="Shape" points="17.705 7.705 16.295 6.295 12 10.59 7.705 6.295 6.295 7.705 10.59 12 6.295 16.295 7.705 17.705 12 13.41 16.295 17.705 17.705 16.295 13.41 12"></polygon>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/svg/edit-image.svg b/libs/editor/WordPressEditor/src/main/assets/svg/edit-image.svg
new file mode 100644
index 000000000..d60fa4965
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/svg/edit-image.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
+ <title>gridicons-pencil</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="gridicons-pencil" fill="#FFFFFF">
+ <path d="M17.3333333,8 L24,14.6666667 L11.324,27.3426667 C10.4093333,26.428 10.404,24.9506667 11.308,24.0293333 L11.3053333,24.0253333 C10.3853333,24.9266667 8.90533333,24.9226667 7.992,24.008 C7.08933333,23.1053333 7.07733333,21.6586667 7.944,20.7346667 L7.93333333,20.724 C7.008,21.5906667 5.56,21.5773333 4.65866667,20.676 L17.3333333,8 L17.3333333,8 Z M27.448,7.448 L24.552,4.552 C23.512,3.512 21.8226667,3.512 20.7813333,4.552 L18.6666667,6.66666667 L25.3333333,13.3333333 L27.448,11.2186667 C28.488,10.1786667 28.488,8.48933333 27.448,7.448 L27.448,7.448 Z M4,24 L4,28 L8,28 C8,25.7906667 6.20933333,24 4,24 L4,24 Z" id="Shape"></path>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/assets/svg/more-2x.png b/libs/editor/WordPressEditor/src/main/assets/svg/more-2x.png
new file mode 100644
index 000000000..c976e506d
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/svg/more-2x.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/assets/svg/pagebreak-2x.png b/libs/editor/WordPressEditor/src/main/assets/svg/pagebreak-2x.png
new file mode 100644
index 000000000..e5cb33bb4
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/svg/pagebreak-2x.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/assets/svg/retry-image-large.svg b/libs/editor/WordPressEditor/src/main/assets/svg/retry-image-large.svg
new file mode 100644
index 000000000..332e5bbda
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/svg/retry-image-large.svg
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+ width="72px" height="72px" viewBox="0 0 24 24" xml:space="preserve">
+<g id="refresh">
+ <path d="M17.91,14c-0.478,2.833-2.943,5-5.91,5c-3.308,0-6-2.692-6-6s2.692-6,6-6h2.172l-2.086,2.086L13.5,10.5L18,6l-4.5-4.5
+ l-1.414,1.414L14.172,5H12c-4.418,0-8,3.582-8,8s3.582,8,8,8c4.079,0,7.438-3.055,7.931-7H17.91z" fill="#FFFFFF"/>
+</g>
+<g id="Layer_1">
+</g>
+</svg>
diff --git a/libs/editor/WordPressEditor/src/main/assets/svg/retry-image.svg b/libs/editor/WordPressEditor/src/main/assets/svg/retry-image.svg
new file mode 100644
index 000000000..b3cb9533b
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/svg/retry-image.svg
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+ width="32px" height="32px" viewBox="0 0 24 24" xml:space="preserve">
+<g id="refresh">
+ <path d="M17.91,14c-0.478,2.833-2.943,5-5.91,5c-3.308,0-6-2.692-6-6s2.692-6,6-6h2.172l-2.086,2.086L13.5,10.5L18,6l-4.5-4.5
+ l-1.414,1.414L14.172,5H12c-4.418,0-8,3.582-8,8s3.582,8,8,8c4.079,0,7.438-3.055,7.931-7H17.91z" fill="#FFFFFF"/>
+</g>
+<g id="Layer_1">
+</g>
+</svg>
diff --git a/libs/editor/WordPressEditor/src/main/assets/svg/wpposter.svg b/libs/editor/WordPressEditor/src/main/assets/svg/wpposter.svg
new file mode 100644
index 000000000..b15c55159
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/assets/svg/wpposter.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1280" height="720" viewBox="0 0 1280 720" style="background:#2e4453">
+<svg x="512" y="232" width="256" height="256" viewBox="0 0 24 24">
+ <path fill="#FFFFFF" d="M20,4v2h-2V4H6v2H4V4C2.9,4,2,4.9,2,6v12c0,1.1,0.9,2,2,2v-2h2v2h12v-2h2v2c1.1,0,2-0.9,2-2V6
+ C22,4.9,21.1,4,20,4z M6,16H4v-3h2V16z M6,11H4V8h2V11z M10,15V9l4.5,3L10,15z M20,16h-2v-3h2V16z M20,11h-2V8h2V11z"/>
+</svg>
+</svg> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java
new file mode 100755
index 000000000..9ff8df7d8
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java
@@ -0,0 +1,1659 @@
+package org.wordpress.android.editor;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.app.AppCompatActivity;
+import android.text.Editable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.view.DragEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.webkit.URLUtil;
+import android.webkit.WebView;
+import android.widget.RelativeLayout.LayoutParams;
+import android.widget.ToggleButton;
+
+import com.android.volley.toolbox.ImageLoader;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.editor.EditorWebViewAbstract.ErrorListener;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.ProfilingUtils;
+import org.wordpress.android.util.ShortcodeUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.util.helpers.MediaFile;
+import org.wordpress.android.util.helpers.MediaGallery;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class EditorFragment extends EditorFragmentAbstract implements View.OnClickListener, View.OnTouchListener,
+ OnJsEditorStateChangedListener, OnImeBackListener, EditorWebViewAbstract.AuthHeaderRequestListener,
+ EditorMediaUploadListener {
+ private static final String ARG_PARAM_TITLE = "param_title";
+ private static final String ARG_PARAM_CONTENT = "param_content";
+
+ private static final String JS_CALLBACK_HANDLER = "nativeCallbackHandler";
+
+ private static final String KEY_TITLE = "title";
+ private static final String KEY_CONTENT = "content";
+
+ private static final String TAG_FORMAT_BAR_BUTTON_MEDIA = "media";
+ private static final String TAG_FORMAT_BAR_BUTTON_LINK = "link";
+
+ private static final float TOOLBAR_ALPHA_ENABLED = 1;
+ private static final float TOOLBAR_ALPHA_DISABLED = 0.5f;
+
+ private static final List<String> DRAGNDROP_SUPPORTED_MIMETYPES_TEXT = Arrays.asList(ClipDescription.MIMETYPE_TEXT_PLAIN,
+ ClipDescription.MIMETYPE_TEXT_HTML);
+ private static final List<String> DRAGNDROP_SUPPORTED_MIMETYPES_IMAGE = Arrays.asList("image/jpeg", "image/png");
+
+ public static final int MAX_ACTION_TIME_MS = 2000;
+
+ private String mTitle = "";
+ private String mContentHtml = "";
+
+ private EditorWebViewAbstract mWebView;
+ private View mSourceView;
+ private SourceViewEditText mSourceViewTitle;
+ private SourceViewEditText mSourceViewContent;
+
+ private int mSelectionStart;
+ private int mSelectionEnd;
+
+ private String mFocusedFieldId;
+
+ private String mTitlePlaceholder = "";
+ private String mContentPlaceholder = "";
+
+ private boolean mDomHasLoaded = false;
+ private boolean mIsKeyboardOpen = false;
+ private boolean mEditorWasPaused = false;
+ private boolean mHideActionBarOnSoftKeyboardUp = false;
+ private boolean mIsFormatBarDisabled = false;
+
+ private ConcurrentHashMap<String, MediaFile> mWaitingMediaFiles;
+ private Set<MediaGallery> mWaitingGalleries;
+ private Map<String, MediaType> mUploadingMedia;
+ private Set<String> mFailedMediaIds;
+ private MediaGallery mUploadingMediaGallery;
+
+ private String mJavaScriptResult = "";
+
+ private CountDownLatch mGetTitleCountDownLatch;
+ private CountDownLatch mGetContentCountDownLatch;
+ private CountDownLatch mGetSelectedTextCountDownLatch;
+
+ private final Map<String, ToggleButton> mTagToggleButtonMap = new HashMap<>();
+
+ private long mActionStartedAt = -1;
+
+ private final View.OnDragListener mOnDragListener = new View.OnDragListener() {
+ private long lastSetCoordsTimestamp;
+
+ private boolean isSupported(ClipDescription clipDescription, List<String> mimeTypesToCheck) {
+ if (clipDescription == null) {
+ return false;
+ }
+
+ for (String supportedMimeType : mimeTypesToCheck) {
+ if (clipDescription.hasMimeType(supportedMimeType)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onDrag(View view, DragEvent dragEvent) {
+ switch (dragEvent.getAction()) {
+ case DragEvent.ACTION_DRAG_STARTED:
+ return isSupported(dragEvent.getClipDescription(), DRAGNDROP_SUPPORTED_MIMETYPES_TEXT) ||
+ isSupported(dragEvent.getClipDescription(), DRAGNDROP_SUPPORTED_MIMETYPES_IMAGE);
+ case DragEvent.ACTION_DRAG_ENTERED:
+ // would be nice to start marking the place the item will drop
+ break;
+ case DragEvent.ACTION_DRAG_LOCATION:
+ int x = DisplayUtils.pxToDp(getActivity(), (int) dragEvent.getX());
+ int y = DisplayUtils.pxToDp(getActivity(), (int) dragEvent.getY());
+
+ // don't call into JS too often
+ long currentTimestamp = SystemClock.uptimeMillis();
+ if ((currentTimestamp - lastSetCoordsTimestamp) > 150) {
+ lastSetCoordsTimestamp = currentTimestamp;
+
+ mWebView.execJavaScriptFromString("ZSSEditor.moveCaretToCoords(" + x + ", " + y + ");");
+ }
+ break;
+ case DragEvent.ACTION_DRAG_EXITED:
+ // clear any drop marking maybe
+ break;
+ case DragEvent.ACTION_DROP:
+ if (mSourceView.getVisibility() == View.VISIBLE) {
+ if (isSupported(dragEvent.getClipDescription(), DRAGNDROP_SUPPORTED_MIMETYPES_IMAGE)) {
+ // don't allow dropping images into the HTML source
+ ToastUtils.showToast(getActivity(), R.string.editor_dropped_html_images_not_allowed,
+ ToastUtils.Duration.LONG);
+ return true;
+ } else {
+ // let the system handle the text drop
+ return false;
+ }
+ }
+
+ if (isSupported(dragEvent.getClipDescription(), DRAGNDROP_SUPPORTED_MIMETYPES_IMAGE) &&
+ ("zss_field_title".equals(mFocusedFieldId))) {
+ // don't allow dropping images into the title field
+ ToastUtils.showToast(getActivity(), R.string.editor_dropped_title_images_not_allowed,
+ ToastUtils.Duration.LONG);
+ return true;
+ }
+
+ if (isAdded()) {
+ mEditorDragAndDropListener.onRequestDragAndDropPermissions(dragEvent);
+ }
+
+ ClipDescription clipDescription = dragEvent.getClipDescription();
+ if (clipDescription.getMimeTypeCount() < 1) {
+ break;
+ }
+
+ ContentResolver contentResolver = getActivity().getContentResolver();
+ ArrayList<Uri> uris = new ArrayList<>();
+ boolean unsupportedDropsFound = false;
+
+ for (int i = 0; i < dragEvent.getClipData().getItemCount(); i++) {
+ ClipData.Item item = dragEvent.getClipData().getItemAt(i);
+ Uri uri = item.getUri();
+
+ final String uriType = uri != null ? contentResolver.getType(uri) : null;
+ if (uriType != null && DRAGNDROP_SUPPORTED_MIMETYPES_IMAGE.contains(uriType)) {
+ uris.add(uri);
+ continue;
+ } else if (item.getText() != null) {
+ insertTextToEditor(item.getText().toString());
+ continue;
+ } else if (item.getHtmlText() != null) {
+ insertTextToEditor(item.getHtmlText());
+ continue;
+ }
+
+ // any other drop types are not supported, including web URLs. We cannot proactively
+ // determine their mime type for filtering
+ unsupportedDropsFound = true;
+ }
+
+ if (unsupportedDropsFound) {
+ ToastUtils.showToast(getActivity(), R.string.editor_dropped_unsupported_files, ToastUtils
+ .Duration.LONG);
+ }
+
+ if (uris.size() > 0) {
+ mEditorDragAndDropListener.onMediaDropped(uris);
+ }
+
+ break;
+ case DragEvent.ACTION_DRAG_ENDED:
+ // clear any drop marking maybe
+ default:
+ break;
+ }
+ return true;
+ }
+
+ private void insertTextToEditor(String text) {
+ if (text != null) {
+ mWebView.execJavaScriptFromString("ZSSEditor.insertText('" + Utils.escapeHtml(text) + "', true);");
+ } else {
+ ToastUtils.showToast(getActivity(), R.string.editor_dropped_text_error, ToastUtils.Duration.SHORT);
+ AppLog.d(T.EDITOR, "Dropped text was null!");
+ }
+ }
+ };
+
+ public static EditorFragment newInstance(String title, String content) {
+ EditorFragment fragment = new EditorFragment();
+ Bundle args = new Bundle();
+ args.putString(ARG_PARAM_TITLE, title);
+ args.putString(ARG_PARAM_CONTENT, content);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ public EditorFragment() {
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ ProfilingUtils.start("Visual Editor Startup");
+ ProfilingUtils.split("EditorFragment.onCreate");
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_editor, container, false);
+
+ // Setup hiding the action bar when the soft keyboard is displayed for narrow viewports
+ if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE
+ && !getResources().getBoolean(R.bool.is_large_tablet_landscape)) {
+ mHideActionBarOnSoftKeyboardUp = true;
+ }
+
+ mWaitingMediaFiles = new ConcurrentHashMap<>();
+ mWaitingGalleries = Collections.newSetFromMap(new ConcurrentHashMap<MediaGallery, Boolean>());
+ mUploadingMedia = new HashMap<>();
+ mFailedMediaIds = new HashSet<>();
+
+ // -- WebView configuration
+
+ mWebView = (EditorWebViewAbstract) view.findViewById(R.id.webview);
+
+ // Revert to compatibility WebView for custom ROMs using a 4.3 WebView in Android 4.4
+ if (mWebView.shouldSwitchToCompatibilityMode()) {
+ ViewGroup parent = (ViewGroup) mWebView.getParent();
+ int index = parent.indexOfChild(mWebView);
+ parent.removeView(mWebView);
+ mWebView = new EditorWebViewCompatibility(getActivity(), null);
+ mWebView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+ parent.addView(mWebView, index);
+ }
+
+ mWebView.setOnTouchListener(this);
+ mWebView.setOnImeBackListener(this);
+ mWebView.setAuthHeaderRequestListener(this);
+
+ mWebView.setOnDragListener(mOnDragListener);
+
+ if (mCustomHttpHeaders != null && mCustomHttpHeaders.size() > 0) {
+ for (Map.Entry<String, String> entry : mCustomHttpHeaders.entrySet()) {
+ mWebView.setCustomHeader(entry.getKey(), entry.getValue());
+ }
+ }
+
+ // Ensure that the content field is always filling the remaining screen space
+ mWebView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ mWebView.post(new Runnable() {
+ @Override
+ public void run() {
+ mWebView.execJavaScriptFromString("try {ZSSEditor.refreshVisibleViewportSize();} catch (e) " +
+ "{console.log(e)}");
+ }
+ });
+ }
+ });
+
+ mEditorFragmentListener.onEditorFragmentInitialized();
+
+ initJsEditor();
+
+ if (savedInstanceState != null) {
+ setTitle(savedInstanceState.getCharSequence(KEY_TITLE));
+ setContent(savedInstanceState.getCharSequence(KEY_CONTENT));
+ }
+
+ // -- HTML mode configuration
+
+ mSourceView = view.findViewById(R.id.sourceview);
+ mSourceViewTitle = (SourceViewEditText) view.findViewById(R.id.sourceview_title);
+ mSourceViewContent = (SourceViewEditText) view.findViewById(R.id.sourceview_content);
+
+ // Toggle format bar on/off as user changes focus between title and content in HTML mode
+ mSourceViewTitle.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ updateFormatBarEnabledState(!hasFocus);
+ }
+ });
+
+ mSourceViewTitle.setOnTouchListener(this);
+ mSourceViewContent.setOnTouchListener(this);
+
+ mSourceViewTitle.setOnImeBackListener(this);
+ mSourceViewContent.setOnImeBackListener(this);
+
+ mSourceViewContent.addTextChangedListener(new HtmlStyleTextWatcher());
+
+ mSourceViewTitle.setHint(mTitlePlaceholder);
+ mSourceViewContent.setHint("<p>" + mContentPlaceholder + "</p>");
+
+ // attach drag-and-drop handler
+ mSourceViewTitle.setOnDragListener(mOnDragListener);
+ mSourceViewContent.setOnDragListener(mOnDragListener);
+
+ // -- Format bar configuration
+
+ setupFormatBarButtonMap(view);
+
+ return view;
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mEditorWasPaused = true;
+ mIsKeyboardOpen = false;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ // If the editor was previously paused and the current orientation is landscape,
+ // hide the actionbar because the keyboard is going to appear (even if it was hidden
+ // prior to being paused).
+ if (mEditorWasPaused
+ && (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE)
+ && !getResources().getBoolean(R.bool.is_large_tablet_landscape)) {
+ mIsKeyboardOpen = true;
+ mHideActionBarOnSoftKeyboardUp = true;
+ hideActionBarIfNeeded();
+ }
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ try {
+ mEditorDragAndDropListener = (EditorDragAndDropListener) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString() + " must implement EditorDragAndDropListener");
+ }
+ }
+
+ @Override
+ public void onDetach() {
+ // Soft cancel (delete flag off) all media uploads currently in progress
+ for (String mediaId : mUploadingMedia.keySet()) {
+ mEditorFragmentListener.onMediaUploadCancelClicked(mediaId, false);
+ }
+ super.onDetach();
+ }
+
+ @Override
+ public void setUserVisibleHint(boolean isVisibleToUser) {
+ if (mDomHasLoaded) {
+ mWebView.notifyVisibilityChanged(isVisibleToUser);
+ }
+ super.setUserVisibleHint(isVisibleToUser);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ outState.putCharSequence(KEY_TITLE, getTitle());
+ outState.putCharSequence(KEY_CONTENT, getContent());
+ }
+
+ private ActionBar getActionBar() {
+ if (!isAdded()) {
+ return null;
+ }
+
+ if (getActivity() instanceof AppCompatActivity) {
+ return ((AppCompatActivity) getActivity()).getSupportActionBar();
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+
+ if (getView() != null) {
+ // Reload the format bar to make sure the correct one for the new screen width is being used
+ View formatBar = getView().findViewById(R.id.format_bar);
+
+ if (formatBar != null) {
+ // Remember the currently active format bar buttons so they can be re-activated after the reload
+ ArrayList<String> activeTags = new ArrayList<>();
+ for (Map.Entry<String, ToggleButton> entry : mTagToggleButtonMap.entrySet()) {
+ if (entry.getValue().isChecked()) {
+ activeTags.add(entry.getKey());
+ }
+ }
+
+ ViewGroup parent = (ViewGroup) formatBar.getParent();
+ parent.removeView(formatBar);
+
+ formatBar = getActivity().getLayoutInflater().inflate(R.layout.format_bar, parent, false);
+ formatBar.setId(R.id.format_bar);
+ parent.addView(formatBar);
+
+ setupFormatBarButtonMap(formatBar);
+
+ if (mIsFormatBarDisabled) {
+ updateFormatBarEnabledState(false);
+ }
+
+ // Restore the active format bar buttons
+ for (String tag : activeTags) {
+ mTagToggleButtonMap.get(tag).setChecked(true);
+ }
+
+ if (mSourceView.getVisibility() == View.VISIBLE) {
+ ToggleButton htmlButton = (ToggleButton) formatBar.findViewById(R.id.format_bar_button_html);
+ htmlButton.setChecked(true);
+ }
+ }
+
+ // Reload HTML mode margins
+ View sourceViewTitle = getView().findViewById(R.id.sourceview_title);
+ View sourceViewContent = getView().findViewById(R.id.sourceview_content);
+
+ if (sourceViewTitle != null && sourceViewContent != null) {
+ int sideMargin = (int) getActivity().getResources().getDimension(R.dimen.sourceview_side_margin);
+
+ ViewGroup.MarginLayoutParams titleParams =
+ (ViewGroup.MarginLayoutParams) sourceViewTitle.getLayoutParams();
+ ViewGroup.MarginLayoutParams contentParams =
+ (ViewGroup.MarginLayoutParams) sourceViewContent.getLayoutParams();
+
+ titleParams.setMargins(sideMargin, titleParams.topMargin, sideMargin, titleParams.bottomMargin);
+ contentParams.setMargins(sideMargin, contentParams.topMargin, sideMargin, contentParams.bottomMargin);
+ }
+ }
+
+ // Toggle action bar auto-hiding for the new orientation
+ if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE
+ && !getResources().getBoolean(R.bool.is_large_tablet_landscape)) {
+ mHideActionBarOnSoftKeyboardUp = true;
+ hideActionBarIfNeeded();
+ } else {
+ mHideActionBarOnSoftKeyboardUp = false;
+ showActionBarIfNeeded();
+ }
+ }
+
+ private void setupFormatBarButtonMap(View view) {
+ ToggleButton boldButton = (ToggleButton) view.findViewById(R.id.format_bar_button_bold);
+ mTagToggleButtonMap.put(getString(R.string.format_bar_tag_bold), boldButton);
+
+ ToggleButton italicButton = (ToggleButton) view.findViewById(R.id.format_bar_button_italic);
+ mTagToggleButtonMap.put(getString(R.string.format_bar_tag_italic), italicButton);
+
+ ToggleButton quoteButton = (ToggleButton) view.findViewById(R.id.format_bar_button_quote);
+ mTagToggleButtonMap.put(getString(R.string.format_bar_tag_blockquote), quoteButton);
+
+ ToggleButton ulButton = (ToggleButton) view.findViewById(R.id.format_bar_button_ul);
+ mTagToggleButtonMap.put(getString(R.string.format_bar_tag_unorderedList), ulButton);
+
+ ToggleButton olButton = (ToggleButton) view.findViewById(R.id.format_bar_button_ol);
+ mTagToggleButtonMap.put(getString(R.string.format_bar_tag_orderedList), olButton);
+
+ // Tablet-only
+ ToggleButton strikethroughButton = (ToggleButton) view.findViewById(R.id.format_bar_button_strikethrough);
+ if (strikethroughButton != null) {
+ mTagToggleButtonMap.put(getString(R.string.format_bar_tag_strikethrough), strikethroughButton);
+ }
+
+ ToggleButton mediaButton = (ToggleButton) view.findViewById(R.id.format_bar_button_media);
+ mTagToggleButtonMap.put(TAG_FORMAT_BAR_BUTTON_MEDIA, mediaButton);
+
+ registerForContextMenu(mediaButton);
+
+ ToggleButton linkButton = (ToggleButton) view.findViewById(R.id.format_bar_button_link);
+ mTagToggleButtonMap.put(TAG_FORMAT_BAR_BUTTON_LINK, linkButton);
+
+ ToggleButton htmlButton = (ToggleButton) view.findViewById(R.id.format_bar_button_html);
+ htmlButton.setOnClickListener(this);
+
+ for (ToggleButton button : mTagToggleButtonMap.values()) {
+ button.setOnClickListener(this);
+ }
+ }
+
+ protected void initJsEditor() {
+ if (!isAdded()) {
+ return;
+ }
+
+ ProfilingUtils.split("EditorFragment.initJsEditor");
+
+ String htmlEditor = Utils.getHtmlFromFile(getActivity(), "android-editor.html");
+ if (htmlEditor != null) {
+ htmlEditor = htmlEditor.replace("%%TITLE%%", 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 = '" + getString(R.string.edit) + "';\n" +
+ "nativeState.localizedStringUploading = '" + getString(R.string.uploading) + "';\n" +
+ "nativeState.localizedStringUploadingGallery = '" + getString(R.string.uploading_gallery_placeholder) + "';\n");
+ }
+
+ // To avoid reflection security issues with JavascriptInterface on API<17, we use an iframe to make URL requests
+ // for callbacks from JS instead. These are received by WebViewClient.shouldOverrideUrlLoading() and then
+ // passed on to the JsCallbackReceiver
+ if (Build.VERSION.SDK_INT < 17) {
+ mWebView.setJsCallbackReceiver(new JsCallbackReceiver(this));
+ } else {
+ mWebView.addJavascriptInterface(new JsCallbackReceiver(this), JS_CALLBACK_HANDLER);
+ }
+
+ mWebView.loadDataWithBaseURL("file:///android_asset/", htmlEditor, "text/html", "utf-8", "");
+
+ if (mDebugModeEnabled) {
+ enableWebDebugging(true);
+ }
+ }
+
+ public void checkForFailedUploadAndSwitchToHtmlMode(final ToggleButton toggleButton) {
+ if (!isAdded()) {
+ return;
+ }
+
+ // Show an Alert Dialog asking the user if he wants to remove all failed media before upload
+ if (hasFailedMediaUploads()) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setMessage(R.string.editor_failed_uploads_switch_html)
+ .setPositiveButton(R.string.editor_remove_failed_uploads, new OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ // Clear failed uploads and switch to HTML mode
+ removeAllFailedMediaUploads();
+ toggleHtmlMode(toggleButton);
+ }
+ }).setNegativeButton(android.R.string.cancel, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ toggleButton.setChecked(false);
+ }
+ });
+ builder.create().show();
+ } else {
+ toggleHtmlMode(toggleButton);
+ }
+ }
+
+ public boolean isActionInProgress() {
+ return System.currentTimeMillis() - mActionStartedAt < MAX_ACTION_TIME_MS;
+ }
+
+ private void toggleHtmlMode(final ToggleButton toggleButton) {
+ if (!isAdded()) {
+ return;
+ }
+
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.HTML_BUTTON_TAPPED);
+
+ // Don't switch to HTML mode if currently uploading media
+ if (!mUploadingMedia.isEmpty() || isActionInProgress()) {
+ toggleButton.setChecked(false);
+ ToastUtils.showToast(getActivity(), R.string.alert_action_while_uploading, ToastUtils.Duration.LONG);
+ return;
+ }
+
+ clearFormatBarButtons();
+ updateFormatBarEnabledState(true);
+
+ if (toggleButton.isChecked()) {
+ Thread thread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ if (!isAdded()) {
+ return;
+ }
+
+ // Update mTitle and mContentHtml with the latest state from the ZSSEditor
+ getTitle();
+ getContent();
+
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // Set HTML mode state
+ mSourceViewTitle.setText(mTitle);
+
+ SpannableString spannableContent = new SpannableString(mContentHtml);
+ HtmlStyleUtils.styleHtmlForDisplay(spannableContent);
+ mSourceViewContent.setText(spannableContent);
+
+ mWebView.setVisibility(View.GONE);
+ mSourceView.setVisibility(View.VISIBLE);
+
+ mSourceViewContent.requestFocus();
+ mSourceViewContent.setSelection(0);
+
+ InputMethodManager imm = ((InputMethodManager) getActivity()
+ .getSystemService(Context.INPUT_METHOD_SERVICE));
+ imm.showSoftInput(mSourceViewContent, InputMethodManager.SHOW_IMPLICIT);
+ }
+ });
+ }
+ });
+
+ thread.start();
+
+ } else {
+ mWebView.setVisibility(View.VISIBLE);
+ mSourceView.setVisibility(View.GONE);
+
+ mTitle = mSourceViewTitle.getText().toString();
+ mContentHtml = mSourceViewContent.getText().toString();
+ updateVisualEditorFields();
+
+ // 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');");
+ }
+ }
+
+ private void displayLinkDialog() {
+ final LinkDialogFragment linkDialogFragment = new LinkDialogFragment();
+ linkDialogFragment.setTargetFragment(this, LinkDialogFragment.LINK_DIALOG_REQUEST_CODE_ADD);
+
+ final Bundle dialogBundle = new Bundle();
+
+ // Pass potential URL from user clipboard
+ String clipboardUri = Utils.getUrlFromClipboard(getActivity());
+ if (clipboardUri != null) {
+ dialogBundle.putString(LinkDialogFragment.LINK_DIALOG_ARG_URL, clipboardUri);
+ }
+
+ // Pass selected text to dialog
+ if (mSourceView.getVisibility() == View.VISIBLE) {
+ // HTML mode
+ mSelectionStart = mSourceViewContent.getSelectionStart();
+ mSelectionEnd = mSourceViewContent.getSelectionEnd();
+
+ String selectedText = mSourceViewContent.getText().toString().substring(mSelectionStart, mSelectionEnd);
+ dialogBundle.putString(LinkDialogFragment.LINK_DIALOG_ARG_TEXT, selectedText);
+
+ linkDialogFragment.setArguments(dialogBundle);
+ linkDialogFragment.show(getFragmentManager(), LinkDialogFragment.class.getSimpleName());
+ } else {
+ // Visual mode
+ Thread thread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ if (!isAdded()) {
+ return;
+ }
+
+ mGetSelectedTextCountDownLatch = new CountDownLatch(1);
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mWebView.execJavaScriptFromString(
+ "ZSSEditor.execFunctionForResult('getSelectedTextToLinkify');");
+ }
+ });
+
+ try {
+ if (mGetSelectedTextCountDownLatch.await(1, TimeUnit.SECONDS)) {
+ dialogBundle.putString(LinkDialogFragment.LINK_DIALOG_ARG_TEXT, mJavaScriptResult);
+ }
+ } catch (InterruptedException e) {
+ AppLog.d(T.EDITOR, "Failed to obtain selected text from JS editor.");
+ }
+
+ linkDialogFragment.setArguments(dialogBundle);
+ linkDialogFragment.show(getFragmentManager(), LinkDialogFragment.class.getSimpleName());
+ }
+ });
+
+ thread.start();
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (!isAdded()) {
+ return;
+ }
+
+ int id = v.getId();
+ if (id == R.id.format_bar_button_html) {
+ checkForFailedUploadAndSwitchToHtmlMode((ToggleButton) v);
+ } else if (id == R.id.format_bar_button_media) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.MEDIA_BUTTON_TAPPED);
+ ((ToggleButton) v).setChecked(false);
+
+ if (isActionInProgress()) {
+ ToastUtils.showToast(getActivity(), R.string.alert_action_while_uploading, ToastUtils.Duration.LONG);
+ return;
+ }
+
+ if (mSourceView.getVisibility() == View.VISIBLE) {
+ ToastUtils.showToast(getActivity(), R.string.alert_insert_image_html_mode, ToastUtils.Duration.LONG);
+ } else {
+ mEditorFragmentListener.onAddMediaClicked();
+ getActivity().openContextMenu(mTagToggleButtonMap.get(TAG_FORMAT_BAR_BUTTON_MEDIA));
+ }
+ } else if (id == R.id.format_bar_button_link) {
+ if (!((ToggleButton) v).isChecked()) {
+ // The link button was checked when it was pressed; remove the current link
+ mWebView.execJavaScriptFromString("ZSSEditor.unlink();");
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.UNLINK_BUTTON_TAPPED);
+ return;
+ }
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.LINK_BUTTON_TAPPED);
+
+ ((ToggleButton) v).setChecked(false);
+
+ displayLinkDialog();
+ } else {
+ if (v instanceof ToggleButton) {
+ onFormattingButtonClicked((ToggleButton) v);
+ }
+ }
+ }
+
+ @Override
+ public boolean onTouch(View view, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ // If the WebView or EditText has received a touch event, the keyboard will be displayed and the action bar
+ // should hide
+ mIsKeyboardOpen = true;
+ hideActionBarIfNeeded();
+ }
+ return false;
+ }
+
+ /**
+ * Intercept back button press while soft keyboard is visible.
+ */
+ @Override
+ public void onImeBack() {
+ mIsKeyboardOpen = false;
+ showActionBarIfNeeded();
+ }
+
+ @Override
+ public String onAuthHeaderRequested(String url) {
+ return mEditorFragmentListener.onAuthHeaderRequested(url);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if ((requestCode == LinkDialogFragment.LINK_DIALOG_REQUEST_CODE_ADD ||
+ requestCode == LinkDialogFragment.LINK_DIALOG_REQUEST_CODE_UPDATE)) {
+
+ if (resultCode == LinkDialogFragment.LINK_DIALOG_REQUEST_CODE_DELETE) {
+ mWebView.execJavaScriptFromString("ZSSEditor.unlink();");
+ return;
+ }
+
+ if (data == null) {
+ return;
+ }
+
+ Bundle extras = data.getExtras();
+ if (extras == null) {
+ return;
+ }
+
+ String linkUrl = extras.getString(LinkDialogFragment.LINK_DIALOG_ARG_URL);
+ String linkText = extras.getString(LinkDialogFragment.LINK_DIALOG_ARG_TEXT);
+
+ if (linkText == null || linkText.equals("")) {
+ linkText = linkUrl;
+ }
+
+ if (TextUtils.isEmpty(Uri.parse(linkUrl).getScheme())) linkUrl = "http://" + linkUrl;
+
+ if (mSourceView.getVisibility() == View.VISIBLE) {
+ Editable content = mSourceViewContent.getText();
+ if (content == null) {
+ return;
+ }
+
+ if (mSelectionStart < mSelectionEnd) {
+ content.delete(mSelectionStart, mSelectionEnd);
+ }
+
+ String urlHtml = "<a href=\"" + linkUrl + "\">" + linkText + "</a>";
+
+ content.insert(mSelectionStart, urlHtml);
+ mSourceViewContent.setSelection(mSelectionStart + urlHtml.length());
+ } else {
+ String jsMethod;
+ if (requestCode == LinkDialogFragment.LINK_DIALOG_REQUEST_CODE_ADD) {
+ jsMethod = "ZSSEditor.insertLink";
+ } else {
+ jsMethod = "ZSSEditor.updateLink";
+ }
+ mWebView.execJavaScriptFromString(jsMethod + "('" + Utils.escapeHtml(linkUrl) + "', '" +
+ Utils.escapeHtml(linkText) + "');");
+ }
+ } else if (requestCode == ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_REQUEST_CODE) {
+ if (data == null) {
+ mWebView.execJavaScriptFromString("ZSSEditor.clearCurrentEditingImage();");
+ return;
+ }
+
+ Bundle extras = data.getExtras();
+ if (extras == null) {
+ return;
+ }
+
+ final String imageMeta = Utils.escapeQuotes(StringUtils.notNullStr(extras.getString("imageMeta")));
+ final int imageRemoteId = extras.getInt("imageRemoteId");
+ final boolean isFeaturedImage = extras.getBoolean("isFeatured");
+
+ mWebView.post(new Runnable() {
+ @Override
+ public void run() {
+ mWebView.execJavaScriptFromString("ZSSEditor.updateCurrentImageMeta('" + imageMeta + "');");
+ }
+ });
+
+ if (imageRemoteId != 0) {
+ if (isFeaturedImage) {
+ mFeaturedImageId = imageRemoteId;
+ mEditorFragmentListener.onFeaturedImageChanged(mFeaturedImageId);
+ } else {
+ // If this image was unset as featured, clear the featured image id
+ if (mFeaturedImageId == imageRemoteId) {
+ mFeaturedImageId = 0;
+ mEditorFragmentListener.onFeaturedImageChanged(mFeaturedImageId);
+ }
+ }
+ }
+ }
+ }
+
+ @SuppressLint("NewApi")
+ private void enableWebDebugging(boolean enable) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ AppLog.i(T.EDITOR, "Enabling web debugging");
+ WebView.setWebContentsDebuggingEnabled(enable);
+ }
+ mWebView.setDebugModeEnabled(mDebugModeEnabled);
+ }
+
+ @Override
+ public void setTitle(CharSequence text) {
+ mTitle = text.toString();
+ }
+
+ @Override
+ public void setContent(CharSequence text) {
+ mContentHtml = text.toString();
+ }
+
+ /**
+ * Returns the contents of the title field from the JavaScript editor. Should be called from a background thread
+ * where possible.
+ */
+ @Override
+ public CharSequence getTitle() {
+ if (!isAdded()) {
+ return "";
+ }
+
+ if (mSourceView != null && mSourceView.getVisibility() == View.VISIBLE) {
+ mTitle = mSourceViewTitle.getText().toString();
+ return StringUtils.notNullStr(mTitle);
+ }
+
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ AppLog.d(T.EDITOR, "getTitle() called from UI thread");
+ }
+
+ mGetTitleCountDownLatch = new CountDownLatch(1);
+
+ // All WebView methods must be called from the UI thread
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_title').getHTMLForCallback();");
+ }
+ });
+
+ try {
+ mGetTitleCountDownLatch.await(1, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ AppLog.e(T.EDITOR, e);
+ Thread.currentThread().interrupt();
+ }
+
+ return StringUtils.notNullStr(mTitle.replaceAll("&nbsp;$", ""));
+ }
+
+ /**
+ * Returns the contents of the content field from the JavaScript editor. Should be called from a background thread
+ * where possible.
+ */
+ @Override
+ public CharSequence getContent() {
+ if (!isAdded()) {
+ return "";
+ }
+
+ if (mSourceView != null && mSourceView.getVisibility() == View.VISIBLE) {
+ mContentHtml = mSourceViewContent.getText().toString();
+ return StringUtils.notNullStr(mContentHtml);
+ }
+
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ AppLog.d(T.EDITOR, "getContent() called from UI thread");
+ }
+
+ mGetContentCountDownLatch = new CountDownLatch(1);
+
+ // All WebView methods must be called from the UI thread
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').getHTMLForCallback();");
+ }
+ });
+
+ try {
+ mGetContentCountDownLatch.await(1, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ AppLog.e(T.EDITOR, e);
+ Thread.currentThread().interrupt();
+ }
+
+ return StringUtils.notNullStr(mContentHtml);
+ }
+
+ @Override
+ public void appendMediaFile(final MediaFile mediaFile, final String mediaUrl, ImageLoader imageLoader) {
+ if (!mDomHasLoaded) {
+ // If the DOM hasn't loaded yet, we won't be able to add media to the ZSSEditor
+ // Place them in a queue to be handled when the DOM loaded callback is received
+ mWaitingMediaFiles.put(mediaUrl, mediaFile);
+ return;
+ }
+
+ final String safeMediaUrl = Utils.escapeQuotes(mediaUrl);
+
+ mWebView.post(new Runnable() {
+ @Override
+ public void run() {
+ if (URLUtil.isNetworkUrl(mediaUrl)) {
+ String mediaId = mediaFile.getMediaId();
+ if (mediaFile.isVideo()) {
+ String posterUrl = Utils.escapeQuotes(StringUtils.notNullStr(mediaFile.getThumbnailURL()));
+ String videoPressId = ShortcodeUtils.getVideoPressIdFromShortCode(
+ mediaFile.getVideoPressShortCode());
+
+ mWebView.execJavaScriptFromString("ZSSEditor.insertVideo('" + safeMediaUrl + "', '" +
+ posterUrl + "', '" + videoPressId + "');");
+ } else {
+ mWebView.execJavaScriptFromString("ZSSEditor.insertImage('" + safeMediaUrl + "', '" + mediaId +
+ "');");
+ }
+ mActionStartedAt = System.currentTimeMillis();
+ } else {
+ String id = mediaFile.getMediaId();
+ if (mediaFile.isVideo()) {
+ String posterUrl = Utils.escapeQuotes(StringUtils.notNullStr(mediaFile.getThumbnailURL()));
+ mWebView.execJavaScriptFromString("ZSSEditor.insertLocalVideo(" + id + ", '" + posterUrl +
+ "');");
+ mUploadingMedia.put(id, MediaType.VIDEO);
+ } else {
+ mWebView.execJavaScriptFromString("ZSSEditor.insertLocalImage(" + id + ", '" + safeMediaUrl +
+ "');");
+ mUploadingMedia.put(id, MediaType.IMAGE);
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public void appendGallery(MediaGallery mediaGallery) {
+ if (!mDomHasLoaded) {
+ // If the DOM hasn't loaded yet, we won't be able to add a gallery to the ZSSEditor
+ // Place it in a queue to be handled when the DOM loaded callback is received
+ mWaitingGalleries.add(mediaGallery);
+ return;
+ }
+
+ if (mediaGallery.getIds().isEmpty()) {
+ mUploadingMediaGallery = mediaGallery;
+ mWebView.execJavaScriptFromString("ZSSEditor.insertLocalGallery('" + mediaGallery.getUniqueId() + "');");
+ } else {
+ // Ensure that the content field is in focus (it may not be if we're adding a gallery to a new post by a
+ // share action and not via the format bar button)
+ mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').focus();");
+
+ mWebView.execJavaScriptFromString("ZSSEditor.insertGallery('" + mediaGallery.getIdsStr() + "', '" +
+ mediaGallery.getType() + "', " + mediaGallery.getNumColumns() + ");");
+ }
+ }
+
+ @Override
+ public void setUrlForVideoPressId(final String videoId, final String videoUrl, final String posterUrl) {
+ mWebView.post(new Runnable() {
+ @Override
+ public void run() {
+ mWebView.execJavaScriptFromString("ZSSEditor.setVideoPressLinks('" + videoId + "', '" +
+ Utils.escapeQuotes(videoUrl) + "', '" + Utils.escapeQuotes(posterUrl) + "');");
+ }
+ });
+ }
+
+ @Override
+ public boolean isUploadingMedia() {
+ return (mUploadingMedia.size() > 0);
+ }
+
+ @Override
+ public boolean hasFailedMediaUploads() {
+ return (mFailedMediaIds.size() > 0);
+ }
+
+ @Override
+ public void removeAllFailedMediaUploads() {
+ mWebView.execJavaScriptFromString("ZSSEditor.removeAllFailedMediaUploads();");
+ }
+
+ @Override
+ public Spanned getSpannedContent() {
+ return null;
+ }
+
+ @Override
+ public void setTitlePlaceholder(CharSequence placeholderText) {
+ mTitlePlaceholder = placeholderText.toString();
+ }
+
+ @Override
+ public void setContentPlaceholder(CharSequence placeholderText) {
+ mContentPlaceholder = placeholderText.toString();
+ }
+
+ @Override
+ public void onMediaUploadSucceeded(final String localMediaId, final MediaFile mediaFile) {
+ final MediaType mediaType = mUploadingMedia.get(localMediaId);
+ if (mediaType != null) {
+ mWebView.post(new Runnable() {
+ @Override
+ public void run() {
+ String remoteUrl = Utils.escapeQuotes(mediaFile.getFileURL());
+ if (mediaType.equals(MediaType.IMAGE)) {
+ String remoteMediaId = mediaFile.getMediaId();
+ mWebView.execJavaScriptFromString("ZSSEditor.replaceLocalImageWithRemoteImage(" + localMediaId +
+ ", '" + remoteMediaId + "', '" + remoteUrl + "');");
+ } else if (mediaType.equals(MediaType.VIDEO)) {
+ String posterUrl = Utils.escapeQuotes(StringUtils.notNullStr(mediaFile.getThumbnailURL()));
+ String videoPressId = ShortcodeUtils.getVideoPressIdFromShortCode(
+ mediaFile.getVideoPressShortCode());
+ mWebView.execJavaScriptFromString("ZSSEditor.replaceLocalVideoWithRemoteVideo(" + localMediaId +
+ ", '" + remoteUrl + "', '" + posterUrl + "', '" + videoPressId + "');");
+ }
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onMediaUploadProgress(final String mediaId, final float progress) {
+ final MediaType mediaType = mUploadingMedia.get(mediaId);
+ if (mediaType != null) {
+ mWebView.post(new Runnable() {
+ @Override
+ public void run() {
+ String progressString = String.format(Locale.US, "%.1f", progress);
+ mWebView.execJavaScriptFromString("ZSSEditor.setProgressOnMedia(" + mediaId + ", " +
+ progressString + ");");
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onMediaUploadFailed(final String mediaId, final String errorMessage) {
+ mWebView.post(new Runnable() {
+ @Override
+ public void run() {
+ MediaType mediaType = mUploadingMedia.get(mediaId);
+ if (mediaType != null) {
+ switch (mediaType) {
+ case IMAGE:
+ mWebView.execJavaScriptFromString("ZSSEditor.markImageUploadFailed(" + mediaId + ", '"
+ + Utils.escapeQuotes(errorMessage) + "');");
+ break;
+ case VIDEO:
+ mWebView.execJavaScriptFromString("ZSSEditor.markVideoUploadFailed(" + mediaId + ", '"
+ + Utils.escapeQuotes(errorMessage) + "');");
+ }
+ mFailedMediaIds.add(mediaId);
+ mUploadingMedia.remove(mediaId);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onGalleryMediaUploadSucceeded(final long galleryId, String remoteMediaId, int remaining) {
+ if (galleryId == mUploadingMediaGallery.getUniqueId()) {
+ ArrayList<String> mediaIds = mUploadingMediaGallery.getIds();
+ mediaIds.add(remoteMediaId);
+ mUploadingMediaGallery.setIds(mediaIds);
+
+ if (remaining == 0) {
+ mWebView.post(new Runnable() {
+ @Override
+ public void run() {
+ mWebView.execJavaScriptFromString("ZSSEditor.replacePlaceholderGallery('" + galleryId + "', '" +
+ mUploadingMediaGallery.getIdsStr() + "', '" +
+ mUploadingMediaGallery.getType() + "', " +
+ mUploadingMediaGallery.getNumColumns() + ");");
+ }
+ });
+ }
+ }
+ }
+
+ public void onDomLoaded() {
+ ProfilingUtils.split("EditorFragment.onDomLoaded");
+
+ mWebView.post(new Runnable() {
+ public void run() {
+ if (!isAdded()) {
+ return;
+ }
+
+ mDomHasLoaded = true;
+
+ mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').setMultiline('true');");
+
+ // Set title and content placeholder text
+ mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_title').setPlaceholderText('" +
+ Utils.escapeQuotes(mTitlePlaceholder) + "');");
+ mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').setPlaceholderText('" +
+ Utils.escapeQuotes(mContentPlaceholder) + "');");
+
+ // Load title and content into ZSSEditor
+ updateVisualEditorFields();
+
+ // If there are images that are still in progress (because the editor exited before they completed),
+ // set them to failed, so the user can restart them (otherwise they will stay stuck in 'uploading' mode)
+ mWebView.execJavaScriptFromString("ZSSEditor.markAllUploadingMediaAsFailed('"
+ + Utils.escapeQuotes(getString(R.string.tap_to_try_again)) + "');");
+
+ // Update the list of failed media uploads
+ mWebView.execJavaScriptFromString("ZSSEditor.getFailedMedia();");
+
+ hideActionBarIfNeeded();
+
+ // Reset all format bar buttons (in case they remained active through activity re-creation)
+ ToggleButton htmlButton = (ToggleButton) getActivity().findViewById(R.id.format_bar_button_html);
+ htmlButton.setChecked(false);
+ for (ToggleButton button : mTagToggleButtonMap.values()) {
+ 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<String, MediaFile> entry : mWaitingMediaFiles.entrySet()) {
+ appendMediaFile(entry.getValue(), entry.getKey(), null);
+ }
+ mWaitingMediaFiles.clear();
+ }
+
+ // Add any galleries that were placed in a queue due to the DOM not having loaded yet
+ if (mWaitingGalleries.size() > 0) {
+ // 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);
+ }
+
+ 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();
+ }
+ });
+ }
+
+ public void onSelectionStyleChanged(final Map<String, Boolean> changeMap) {
+ mWebView.post(new Runnable() {
+ public void run() {
+ for (Map.Entry<String, Boolean> entry : changeMap.entrySet()) {
+ // Handle toggling format bar style buttons
+ ToggleButton button = mTagToggleButtonMap.get(entry.getKey());
+ if (button != null) {
+ button.setChecked(entry.getValue());
+ }
+ }
+ }
+ });
+ }
+
+ public void onSelectionChanged(final Map<String, String> selectionArgs) {
+ mFocusedFieldId = selectionArgs.get("id"); // The field now in focus
+ mWebView.post(new Runnable() {
+ @Override
+ public void run() {
+ if (!mFocusedFieldId.isEmpty()) {
+ switch (mFocusedFieldId) {
+ case "zss_field_title":
+ updateFormatBarEnabledState(false);
+ break;
+ case "zss_field_content":
+ updateFormatBarEnabledState(true);
+ break;
+ }
+ }
+ }
+ });
+ }
+
+ public void onMediaTapped(final String mediaId, final MediaType mediaType, final JSONObject meta, String uploadStatus) {
+ if (mediaType == null || !isAdded()) {
+ return;
+ }
+
+ switch (uploadStatus) {
+ case "uploading":
+ // Display 'cancel upload' dialog
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(getString(R.string.stop_upload_dialog_title));
+ builder.setPositiveButton(R.string.stop_upload_button, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ mEditorFragmentListener.onMediaUploadCancelClicked(mediaId, true);
+
+ mWebView.post(new Runnable() {
+ @Override
+ public void run() {
+ switch (mediaType) {
+ case IMAGE:
+ mWebView.execJavaScriptFromString("ZSSEditor.removeImage(" + mediaId + ");");
+ break;
+ case VIDEO:
+ mWebView.execJavaScriptFromString("ZSSEditor.removeVideo(" + mediaId + ");");
+ }
+ mUploadingMedia.remove(mediaId);
+ }
+ });
+ dialog.dismiss();
+ }
+ });
+
+ builder.setNegativeButton(getString(R.string.cancel), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.dismiss();
+ }
+ });
+
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ break;
+ case "failed":
+ // Retry media upload
+ mEditorFragmentListener.onMediaRetryClicked(mediaId);
+
+ mWebView.post(new Runnable() {
+ @Override
+ public void run() {
+ switch (mediaType) {
+ case IMAGE:
+ mWebView.execJavaScriptFromString("ZSSEditor.unmarkImageUploadFailed(" + mediaId
+ + ");");
+ break;
+ case VIDEO:
+ mWebView.execJavaScriptFromString("ZSSEditor.unmarkVideoUploadFailed(" + mediaId
+ + ");");
+ }
+ mFailedMediaIds.remove(mediaId);
+ mUploadingMedia.put(mediaId, mediaType);
+ }
+ });
+ break;
+ default:
+ if (!mediaType.equals(MediaType.IMAGE)) {
+ return;
+ }
+
+ // Only show image options fragment for image taps
+ FragmentManager fragmentManager = getFragmentManager();
+
+ if (fragmentManager.findFragmentByTag(ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_TAG) != null) {
+ return;
+ }
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.IMAGE_EDITED);
+ ImageSettingsDialogFragment imageSettingsDialogFragment = new ImageSettingsDialogFragment();
+ imageSettingsDialogFragment.setTargetFragment(this,
+ ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_REQUEST_CODE);
+
+ Bundle dialogBundle = new Bundle();
+
+ dialogBundle.putString("maxWidth", mBlogSettingMaxImageWidth);
+ dialogBundle.putBoolean("featuredImageSupported", mFeaturedImageSupported);
+
+ // Request and add an authorization header for HTTPS images
+ // Use https:// when requesting the auth header, in case the image is incorrectly using http://.
+ // If an auth header is returned, force https:// for the actual HTTP request.
+ HashMap<String, String> headerMap = new HashMap<>();
+ if (mCustomHttpHeaders != null) {
+ headerMap.putAll(mCustomHttpHeaders);
+ }
+
+ try {
+ final String imageSrc = meta.getString("src");
+ String authHeader = mEditorFragmentListener.onAuthHeaderRequested(UrlUtils.makeHttps(imageSrc));
+ if (authHeader.length() > 0) {
+ meta.put("src", UrlUtils.makeHttps(imageSrc));
+ headerMap.put("Authorization", authHeader);
+ }
+ } catch (JSONException e) {
+ AppLog.e(T.EDITOR, "Could not retrieve image url from JSON metadata");
+ }
+ dialogBundle.putSerializable("headerMap", headerMap);
+
+ dialogBundle.putString("imageMeta", meta.toString());
+
+ String imageId = JSONUtils.getString(meta, "attachment_id");
+ if (!imageId.isEmpty()) {
+ dialogBundle.putBoolean("isFeatured", mFeaturedImageId == Integer.parseInt(imageId));
+ }
+
+ imageSettingsDialogFragment.setArguments(dialogBundle);
+
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+ fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
+
+ fragmentTransaction.add(android.R.id.content, imageSettingsDialogFragment,
+ ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_TAG)
+ .addToBackStack(null)
+ .commit();
+
+ mWebView.notifyVisibilityChanged(false);
+ break;
+ }
+ }
+
+ public void onLinkTapped(String url, String title) {
+ LinkDialogFragment linkDialogFragment = new LinkDialogFragment();
+ linkDialogFragment.setTargetFragment(this, LinkDialogFragment.LINK_DIALOG_REQUEST_CODE_UPDATE);
+
+ Bundle dialogBundle = new Bundle();
+
+ dialogBundle.putString(LinkDialogFragment.LINK_DIALOG_ARG_URL, url);
+ dialogBundle.putString(LinkDialogFragment.LINK_DIALOG_ARG_TEXT, title);
+
+ linkDialogFragment.setArguments(dialogBundle);
+ 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);
+ }
+
+ @Override
+ public void onVideoPressInfoRequested(final String videoId) {
+ mEditorFragmentListener.onVideoPressInfoRequested(videoId);
+ }
+
+ public void onGetHtmlResponse(Map<String, String> inputArgs) {
+ String functionId = inputArgs.get("function");
+
+ if (functionId.isEmpty()) {
+ return;
+ }
+
+ switch (functionId) {
+ case "getHTMLForCallback":
+ String fieldId = inputArgs.get("id");
+ String fieldContents = inputArgs.get("contents");
+ if (!fieldId.isEmpty()) {
+ switch (fieldId) {
+ case "zss_field_title":
+ mTitle = fieldContents;
+ mGetTitleCountDownLatch.countDown();
+ break;
+ case "zss_field_content":
+ mContentHtml = fieldContents;
+ mGetContentCountDownLatch.countDown();
+ break;
+ }
+ }
+ break;
+ case "getSelectedTextToLinkify":
+ mJavaScriptResult = inputArgs.get("result");
+ mGetSelectedTextCountDownLatch.countDown();
+ break;
+ case "getFailedMedia":
+ String[] mediaIds = inputArgs.get("ids").split(",");
+ for (String mediaId : mediaIds) {
+ if (!mediaId.equals("")) {
+ mFailedMediaIds.add(mediaId);
+ }
+ }
+ }
+ }
+
+ public void setWebViewErrorListener(ErrorListener errorListener) {
+ mWebView.setErrorListener(errorListener);
+ }
+
+ private void updateVisualEditorFields() {
+ mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_title').setPlainText('" +
+ Utils.escapeHtml(mTitle) + "');");
+ mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').setHTML('" +
+ Utils.escapeHtml(mContentHtml) + "');");
+ }
+
+ /**
+ * Hide the action bar if needed.
+ */
+ private void hideActionBarIfNeeded() {
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null
+ && !isHardwareKeyboardPresent()
+ && mHideActionBarOnSoftKeyboardUp
+ && mIsKeyboardOpen
+ && actionBar.isShowing()) {
+ getActionBar().hide();
+ }
+ }
+
+ /**
+ * Show the action bar if needed.
+ */
+ private void showActionBarIfNeeded() {
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null && !actionBar.isShowing()) {
+ actionBar.show();
+ }
+ }
+
+ /**
+ * Returns true if a hardware keyboard is detected, otherwise false.
+ */
+ private boolean isHardwareKeyboardPresent() {
+ Configuration config = getResources().getConfiguration();
+ boolean returnValue = false;
+ if (config.keyboard != Configuration.KEYBOARD_NOKEYS) {
+ returnValue = true;
+ }
+ return returnValue;
+ }
+
+ void updateFormatBarEnabledState(boolean enabled) {
+ float alpha = (enabled ? TOOLBAR_ALPHA_ENABLED : TOOLBAR_ALPHA_DISABLED);
+ for(ToggleButton button : mTagToggleButtonMap.values()) {
+ button.setEnabled(enabled);
+ button.setAlpha(alpha);
+ }
+
+ mIsFormatBarDisabled = !enabled;
+ }
+
+ private void clearFormatBarButtons() {
+ for (ToggleButton button : mTagToggleButtonMap.values()) {
+ if (button != null) {
+ button.setChecked(false);
+ }
+ }
+ }
+
+ private void onFormattingButtonClicked(ToggleButton toggleButton) {
+ String tag = toggleButton.getTag().toString();
+ buttonTappedListener(toggleButton);
+ if (mWebView.getVisibility() == View.VISIBLE) {
+ mWebView.execJavaScriptFromString("ZSSEditor.set" + StringUtils.capitalize(tag) + "();");
+ } else {
+ applyFormattingHtmlMode(toggleButton, tag);
+ }
+ }
+
+ private void buttonTappedListener(ToggleButton toggleButton) {
+ int id = toggleButton.getId();
+ if (id == R.id.format_bar_button_bold) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.BOLD_BUTTON_TAPPED);
+ } else if (id == R.id.format_bar_button_italic) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.ITALIC_BUTTON_TAPPED);
+ } else if (id == R.id.format_bar_button_ol) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.OL_BUTTON_TAPPED);
+ } else if (id == R.id.format_bar_button_ul) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.UL_BUTTON_TAPPED);
+ } else if (id == R.id.format_bar_button_quote) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.BLOCKQUOTE_BUTTON_TAPPED);
+ } else if (id == R.id.format_bar_button_strikethrough) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.STRIKETHROUGH_BUTTON_TAPPED);
+ }
+ }
+
+ /**
+ * In HTML mode, applies formatting to selected text, or inserts formatting tag at current cursor position
+ * @param toggleButton format bar button which was clicked
+ * @param tag identifier tag
+ */
+ private void applyFormattingHtmlMode(ToggleButton toggleButton, String tag) {
+ if (mSourceViewContent == null) {
+ return;
+ }
+
+ // Replace style tags with their proper HTML tags
+ String htmlTag;
+ if (tag.equals(getString(R.string.format_bar_tag_bold))) {
+ htmlTag = "b";
+ } else if (tag.equals(getString(R.string.format_bar_tag_italic))) {
+ htmlTag = "i";
+ } else if (tag.equals(getString(R.string.format_bar_tag_strikethrough))) {
+ htmlTag = "del";
+ } else if (tag.equals(getString(R.string.format_bar_tag_unorderedList))) {
+ htmlTag = "ul";
+ } else if (tag.equals(getString(R.string.format_bar_tag_orderedList))) {
+ htmlTag = "ol";
+ } else {
+ htmlTag = tag;
+ }
+
+ int selectionStart = mSourceViewContent.getSelectionStart();
+ int selectionEnd = mSourceViewContent.getSelectionEnd();
+
+ if (selectionStart > selectionEnd) {
+ int temp = selectionEnd;
+ selectionEnd = selectionStart;
+ selectionStart = temp;
+ }
+
+ boolean textIsSelected = selectionEnd > selectionStart;
+
+ String startTag = "<" + htmlTag + ">";
+ String endTag = "</" + htmlTag + ">";
+
+ // Add li tags together with ul and ol tags
+ if (htmlTag.equals("ul") || htmlTag.equals("ol")) {
+ startTag = startTag + "\n\t<li>";
+ endTag = "</li>\n" + endTag;
+ }
+
+ Editable content = mSourceViewContent.getText();
+ if (textIsSelected) {
+ // Surround selected text with opening and closing tags
+ content.insert(selectionStart, startTag);
+ content.insert(selectionEnd + startTag.length(), endTag);
+ toggleButton.setChecked(false);
+ mSourceViewContent.setSelection(selectionEnd + startTag.length() + endTag.length());
+ } else if (toggleButton.isChecked()) {
+ // Insert opening tag
+ content.insert(selectionStart, startTag);
+ mSourceViewContent.setSelection(selectionEnd + startTag.length());
+ } else {
+ // Insert closing tag
+ content.insert(selectionEnd, endTag);
+ mSourceViewContent.setSelection(selectionEnd + endTag.length());
+ }
+ }
+
+ @Override
+ public void onActionFinished() {
+ mActionStartedAt = -1;
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java
new file mode 100644
index 000000000..ba15d036a
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java
@@ -0,0 +1,180 @@
+package org.wordpress.android.editor;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.Spanned;
+import android.view.DragEvent;
+
+import com.android.volley.toolbox.ImageLoader;
+
+import org.wordpress.android.util.helpers.MediaFile;
+import org.wordpress.android.util.helpers.MediaGallery;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public abstract class EditorFragmentAbstract extends Fragment {
+ public abstract void setTitle(CharSequence text);
+ public abstract void setContent(CharSequence text);
+ public abstract CharSequence getTitle();
+ public abstract CharSequence getContent();
+ public abstract void appendMediaFile(MediaFile mediaFile, String imageUrl, ImageLoader imageLoader);
+ public abstract void appendGallery(MediaGallery mediaGallery);
+ public abstract void setUrlForVideoPressId(String videoPressId, String url, String posterUrl);
+ public abstract boolean isUploadingMedia();
+ public abstract boolean isActionInProgress();
+ public abstract boolean hasFailedMediaUploads();
+ public abstract void removeAllFailedMediaUploads();
+ public abstract void setTitlePlaceholder(CharSequence text);
+ public abstract void setContentPlaceholder(CharSequence text);
+
+ // TODO: remove this as soon as we can (we'll need to drop the legacy editor or fix html2spanned translation)
+ public abstract Spanned getSpannedContent();
+
+ public enum MediaType {
+ IMAGE, VIDEO;
+
+ public static MediaType fromString(String value) {
+ if (value != null) {
+ for (MediaType mediaType : MediaType.values()) {
+ if (value.equalsIgnoreCase(mediaType.toString())) {
+ return mediaType;
+ }
+ }
+ }
+ return null;
+ }
+ }
+
+ private static final String FEATURED_IMAGE_SUPPORT_KEY = "featured-image-supported";
+ private static final String FEATURED_IMAGE_WIDTH_KEY = "featured-image-width";
+
+ protected EditorFragmentListener mEditorFragmentListener;
+ protected EditorDragAndDropListener mEditorDragAndDropListener;
+ protected boolean mFeaturedImageSupported;
+ protected long mFeaturedImageId;
+ protected String mBlogSettingMaxImageWidth;
+ protected ImageLoader mImageLoader;
+ protected boolean mDebugModeEnabled;
+
+ protected HashMap<String, String> mCustomHttpHeaders;
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ try {
+ mEditorFragmentListener = (EditorFragmentListener) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString() + " must implement EditorFragmentListener");
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ outState.putBoolean(FEATURED_IMAGE_SUPPORT_KEY, mFeaturedImageSupported);
+ outState.putString(FEATURED_IMAGE_WIDTH_KEY, mBlogSettingMaxImageWidth);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (savedInstanceState != null) {
+ if (savedInstanceState.containsKey(FEATURED_IMAGE_SUPPORT_KEY)) {
+ mFeaturedImageSupported = savedInstanceState.getBoolean(FEATURED_IMAGE_SUPPORT_KEY);
+ }
+ if (savedInstanceState.containsKey(FEATURED_IMAGE_WIDTH_KEY)) {
+ mBlogSettingMaxImageWidth = savedInstanceState.getString(FEATURED_IMAGE_WIDTH_KEY);
+ }
+ }
+ }
+
+ public void setImageLoader(ImageLoader imageLoader) {
+ mImageLoader = imageLoader;
+ }
+
+ public void setFeaturedImageSupported(boolean featuredImageSupported) {
+ mFeaturedImageSupported = featuredImageSupported;
+ }
+
+ public void setBlogSettingMaxImageWidth(String blogSettingMaxImageWidth) {
+ mBlogSettingMaxImageWidth = blogSettingMaxImageWidth;
+ }
+
+ public void setFeaturedImageId(long featuredImageId) {
+ mFeaturedImageId = featuredImageId;
+ }
+
+ public void setCustomHttpHeader(String name, String value) {
+ if (mCustomHttpHeaders == null) {
+ mCustomHttpHeaders = new HashMap<>();
+ }
+
+ mCustomHttpHeaders.put(name, value);
+ }
+
+ public void setDebugModeEnabled(boolean debugModeEnabled) {
+ mDebugModeEnabled = debugModeEnabled;
+ }
+
+ /**
+ * Called by the activity when back button is pressed.
+ */
+ public boolean onBackPressed() {
+ return false;
+ }
+
+ /**
+ * The editor may need to differentiate local draft and published articles
+ *
+ * @param isLocalDraft edited post is a local draft
+ */
+ public void setLocalDraft(boolean isLocalDraft) {
+ // Not unused in the new editor
+ }
+
+ /**
+ * Callbacks used to communicate with the parent Activity
+ */
+ public interface EditorFragmentListener {
+ void onEditorFragmentInitialized();
+ void onSettingsClicked();
+ void onAddMediaClicked();
+ void onMediaRetryClicked(String mediaId);
+ void onMediaUploadCancelClicked(String mediaId, boolean delete);
+ void onFeaturedImageChanged(long mediaId);
+ void onVideoPressInfoRequested(String videoId);
+ String onAuthHeaderRequested(String url);
+ // TODO: remove saveMediaFile, it's currently needed for the legacy editor
+ void saveMediaFile(MediaFile mediaFile);
+ void onTrackableEvent(TrackableEvent event);
+ }
+
+ /**
+ * Callbacks for drag and drop support
+ */
+ public interface EditorDragAndDropListener {
+ void onMediaDropped(ArrayList<Uri> mediaUri);
+ void onRequestDragAndDropPermissions(DragEvent dragEvent);
+ }
+
+ public enum TrackableEvent {
+ HTML_BUTTON_TAPPED,
+ UNLINK_BUTTON_TAPPED,
+ LINK_BUTTON_TAPPED,
+ MEDIA_BUTTON_TAPPED,
+ IMAGE_EDITED,
+ BOLD_BUTTON_TAPPED,
+ ITALIC_BUTTON_TAPPED,
+ OL_BUTTON_TAPPED,
+ UL_BUTTON_TAPPED,
+ BLOCKQUOTE_BUTTON_TAPPED,
+ STRIKETHROUGH_BUTTON_TAPPED,
+ UNDERLINE_BUTTON_TAPPED,
+ MORE_BUTTON_TAPPED
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorMediaUploadListener.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorMediaUploadListener.java
new file mode 100644
index 000000000..26ba44150
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorMediaUploadListener.java
@@ -0,0 +1,10 @@
+package org.wordpress.android.editor;
+
+import org.wordpress.android.util.helpers.MediaFile;
+
+public interface EditorMediaUploadListener {
+ void onMediaUploadSucceeded(String localId, MediaFile mediaFile);
+ void onMediaUploadProgress(String localId, float progress);
+ void onMediaUploadFailed(String localId, String errorMessage);
+ void onGalleryMediaUploadSucceeded(long galleryId, String remoteId, int remaining);
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebView.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebView.java
new file mode 100644
index 000000000..f613eb5d5
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebView.java
@@ -0,0 +1,35 @@
+package org.wordpress.android.editor;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Build;
+import android.util.AttributeSet;
+
+import org.wordpress.android.util.AppLog;
+
+public class EditorWebView extends EditorWebViewAbstract {
+
+ public EditorWebView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @SuppressLint("NewApi")
+ public void execJavaScriptFromString(String javaScript) {
+ this.evaluateJavascript(javaScript, null);
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public boolean shouldSwitchToCompatibilityMode() {
+ if (Build.VERSION.SDK_INT <= 19) {
+ try {
+ this.evaluateJavascript("", null);
+ } catch (NoSuchMethodError | IllegalStateException e) {
+ AppLog.d(AppLog.T.EDITOR,
+ "Detected 4.4 ROM using classic WebView, reverting to compatibility EditorWebView.");
+ return true;
+ }
+ }
+ return false;
+ }
+} \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewAbstract.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewAbstract.java
new file mode 100644
index 000000000..64ded937d
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewAbstract.java
@@ -0,0 +1,256 @@
+package org.wordpress.android.editor;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.View;
+import android.webkit.ConsoleMessage;
+import android.webkit.ConsoleMessage.MessageLevel;
+import android.webkit.JsResult;
+import android.webkit.URLUtil;
+import android.webkit.WebChromeClient;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import org.wordpress.android.util.AppLog;
+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;
+import java.net.HttpURLConnection;
+import java.net.URLDecoder;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A text editor WebView with support for JavaScript execution.
+ */
+public abstract class EditorWebViewAbstract extends WebView {
+ public abstract void execJavaScriptFromString(String javaScript);
+
+ private OnImeBackListener mOnImeBackListener;
+ private AuthHeaderRequestListener mAuthHeaderRequestListener;
+ private ErrorListener mErrorListener;
+ private JsCallbackReceiver mJsCallbackReceiver;
+ private boolean mDebugModeEnabled;
+
+ private Map<String, String> mHeaderMap = new HashMap<>();
+
+ public EditorWebViewAbstract(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ configureWebView();
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ private void configureWebView() {
+ WebSettings webSettings = this.getSettings();
+ webSettings.setJavaScriptEnabled(true);
+ webSettings.setDefaultTextEncodingName("utf-8");
+
+ this.setWebViewClient(new WebViewClient() {
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ if (url != null && url.startsWith("callback") && mJsCallbackReceiver != null) {
+ String data = URLDecoder.decode(url);
+ String[] split = data.split(":", 2);
+ String callbackId = split[0];
+ String params = (split.length > 1 ? split[1] : "");
+ mJsCallbackReceiver.executeCallback(callbackId, params);
+ }
+ return true;
+ }
+
+ @Override
+ public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
+ AppLog.e(T.EDITOR, description);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @Override
+ public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
+ String url = request.getUrl().toString();
+
+ if (!URLUtil.isNetworkUrl(url)) {
+ return super.shouldInterceptRequest(view, request);
+ }
+
+ // Request and add an authorization header for HTTPS resource requests.
+ // Use https:// when requesting the auth header, in case the resource is incorrectly using http://.
+ // If an auth header is returned, force https:// for the actual HTTP request.
+ String authHeader = mAuthHeaderRequestListener.onAuthHeaderRequested(UrlUtils.makeHttps(url));
+ if (StringUtils.notNullStr(authHeader).length() > 0) {
+ try {
+ url = UrlUtils.makeHttps(url);
+
+ // Keep any existing request headers from the WebResourceRequest
+ Map<String, String> headerMap = request.getRequestHeaders();
+ for (Map.Entry<String, String> entry : mHeaderMap.entrySet()) {
+ headerMap.put(entry.getKey(), entry.getValue());
+ }
+ headerMap.put("Authorization", authHeader);
+
+ HttpURLConnection conn = HTTPUtils.setupUrlConnection(url, headerMap);
+ return new WebResourceResponse(conn.getContentType(), conn.getContentEncoding(),
+ conn.getInputStream());
+ } catch (IOException e) {
+ AppLog.e(T.EDITOR, e);
+ }
+ }
+
+ return super.shouldInterceptRequest(view, request);
+ }
+
+ /**
+ * Compatibility method for API < 21
+ */
+ @SuppressWarnings("deprecation")
+ @Override
+ public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
+ if (!URLUtil.isNetworkUrl(url)) {
+ return super.shouldInterceptRequest(view, url);
+ }
+
+ // Request and add an authorization header for HTTPS resource requests.
+ // Use https:// when requesting the auth header, in case the resource is incorrectly using http://.
+ // If an auth header is returned, force https:// for the actual HTTP request.
+ String authHeader = mAuthHeaderRequestListener.onAuthHeaderRequested(UrlUtils.makeHttps(url));
+ if (StringUtils.notNullStr(authHeader).length() > 0) {
+ try {
+ url = UrlUtils.makeHttps(url);
+
+ Map<String, String> headerMap = new HashMap<>(mHeaderMap);
+ headerMap.put("Authorization", authHeader);
+
+ HttpURLConnection conn = HTTPUtils.setupUrlConnection(url, headerMap);
+ return new WebResourceResponse(conn.getContentType(), conn.getContentEncoding(),
+ conn.getInputStream());
+ } catch (IOException e) {
+ AppLog.e(T.EDITOR, e);
+ }
+ }
+
+ return super.shouldInterceptRequest(view, url);
+ }
+ });
+
+ this.setWebChromeClient(new WebChromeClient() {
+ @Override
+ public boolean onConsoleMessage(@NonNull ConsoleMessage cm) {
+ if (cm.messageLevel() == MessageLevel.ERROR) {
+ if (mErrorListener != null) {
+ mErrorListener.onJavaScriptError(cm.sourceId(), cm.lineNumber(), cm.message());
+ }
+ AppLog.e(T.EDITOR, cm.message() + " -- From line " + cm.lineNumber() + " of " + cm.sourceId());
+ } else {
+ AppLog.d(T.EDITOR, cm.message() + " -- From line " + cm.lineNumber() + " of " + cm.sourceId());
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
+ AppLog.d(T.EDITOR, message);
+ if (mErrorListener != null) {
+ mErrorListener.onJavaScriptAlert(url, message);
+ }
+ return true;
+ }
+ });
+ }
+
+ @Override
+ public boolean onCheckIsTextEditor() {
+ return true;
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ notifyVisibilityChanged(visibility == View.VISIBLE);
+ super.setVisibility(visibility);
+ }
+
+
+ public boolean shouldSwitchToCompatibilityMode() {
+ return false;
+ }
+
+ public void setDebugModeEnabled(boolean enabled) {
+ mDebugModeEnabled = enabled;
+ }
+
+ /**
+ * Handles events that should be triggered when the WebView is hidden or is shown to the user
+ *
+ * @param visible the new visibility status of the WebView
+ */
+ public void notifyVisibilityChanged(boolean visible) {
+ if (!visible) {
+ this.post(new Runnable() {
+ @Override
+ public void run() {
+ execJavaScriptFromString("ZSSEditor.pauseAllVideos();");
+ }
+ });
+ }
+ }
+
+ public void setOnImeBackListener(OnImeBackListener listener) {
+ mOnImeBackListener = listener;
+ }
+
+ public void setAuthHeaderRequestListener(AuthHeaderRequestListener listener) {
+ mAuthHeaderRequestListener = listener;
+ }
+
+ /**
+ * Used on API<17 to handle callbacks as a safe alternative to JavascriptInterface (which has security risks
+ * at those API levels).
+ */
+ public void setJsCallbackReceiver(JsCallbackReceiver jsCallbackReceiver) {
+ mJsCallbackReceiver = jsCallbackReceiver;
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
+ if (mOnImeBackListener != null) {
+ 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);
+ }
+
+ public void setCustomHeader(String name, String value) {
+ mHeaderMap.put(name, value);
+ }
+
+ public void setErrorListener(ErrorListener errorListener) {
+ mErrorListener = errorListener;
+ }
+
+ public interface AuthHeaderRequestListener {
+ String onAuthHeaderRequested(String url);
+ }
+
+ public interface ErrorListener {
+ void onJavaScriptError(String sourceFile, int lineNumber, String message);
+ void onJavaScriptAlert(String url, String message);
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewCompatibility.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewCompatibility.java
new file mode 100644
index 000000000..72d431a21
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewCompatibility.java
@@ -0,0 +1,130 @@
+package org.wordpress.android.editor;
+
+import android.content.Context;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.webkit.WebView;
+
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * <p>Compatibility <code>EditorWebView</code> for pre-Chromium WebView (API<19). Provides a custom method for executing
+ * JavaScript, {@link #loadJavaScript(String)}, instead of {@link WebView#loadUrl(String)}. This is needed because
+ * <code>WebView#loadUrl(String)</code> on API<19 eventually calls <code>WebViewClassic#hideSoftKeyboard()</code>,
+ * hiding the keyboard whenever JavaScript is executed.</p>
+ * <p/>
+ * <p>This class uses reflection to access the normally inaccessible <code>WebViewCore#sendMessage(Message)</code>
+ * and use it to execute JavaScript, sidestepping <code>WebView#loadUrl(String)</code> and the keyboard issue.</p>
+ */
+@SuppressWarnings("TryWithIdenticalCatches")
+public class EditorWebViewCompatibility extends EditorWebViewAbstract {
+ public interface ReflectionFailureListener {
+ void onReflectionFailure(ReflectionException e);
+ }
+
+ public class ReflectionException extends Exception {
+ public ReflectionException(Throwable cause) {
+ super(cause);
+ }
+ }
+
+ private static final int EXECUTE_JS = 194; // WebViewCore internal JS message code
+
+ private Object mWebViewCore;
+ private Method mSendMessageMethod;
+
+ // Dirty static listener, but it's impossible to set the listener during the construction if we want to keep
+ // the xml layout
+ private static ReflectionFailureListener mReflectionFailureListener;
+ private boolean mReflectionSucceed = true;
+
+ public static void setReflectionFailureListener(ReflectionFailureListener reflectionFailureListener) {
+ mReflectionFailureListener = reflectionFailureListener;
+ }
+
+ public EditorWebViewCompatibility(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ try {
+ this.initReflection();
+ } catch (ReflectionException e) {
+ AppLog.e(T.EDITOR, e);
+ handleReflectionFailure(e);
+ }
+ }
+
+ private void initReflection() throws ReflectionException {
+ if (!mReflectionSucceed) {
+ // Reflection failed once already, it won't succeed on a second try
+ return;
+ }
+ Object webViewProvider;
+ try {
+ // On API >= 16, the WebViewCore instance is not defined inside WebView itself but inside a
+ // WebViewClassic (implementation of WebViewProvider), referenced from the WebView as mProvider
+
+ // Access WebViewClassic object
+ Field webViewProviderField = WebView.class.getDeclaredField("mProvider");
+ webViewProviderField.setAccessible(true);
+ webViewProvider = webViewProviderField.get(this);
+
+ // Access WebViewCore object
+ Field webViewCoreField = webViewProvider.getClass().getDeclaredField("mWebViewCore");
+ webViewCoreField.setAccessible(true);
+ mWebViewCore = webViewCoreField.get(webViewProvider);
+
+ // Access WebViewCore#sendMessage(Message) method
+ if (mWebViewCore != null) {
+ mSendMessageMethod = mWebViewCore.getClass().getDeclaredMethod("sendMessage", Message.class);
+ mSendMessageMethod.setAccessible(true);
+ }
+ } catch (NoSuchFieldException e) {
+ throw new ReflectionException(e);
+ } catch (NoSuchMethodException e) {
+ throw new ReflectionException(e);
+ } catch (IllegalAccessException e) {
+ throw new ReflectionException(e);
+ }
+ }
+
+ private void loadJavaScript(final String javaScript) throws ReflectionException {
+ if (mSendMessageMethod == null) {
+ initReflection();
+ } else {
+ Message jsMessage = Message.obtain(null, EXECUTE_JS, javaScript);
+ try {
+ mSendMessageMethod.invoke(mWebViewCore, jsMessage);
+ } catch (InvocationTargetException e) {
+ throw new ReflectionException(e);
+ } catch (IllegalAccessException e) {
+ throw new ReflectionException(e);
+ }
+ }
+ }
+
+ public void execJavaScriptFromString(String javaScript) {
+ try {
+ loadJavaScript(javaScript);
+ } catch (ReflectionException e) {
+ AppLog.e(T.EDITOR, e);
+ handleReflectionFailure(e);
+ }
+ }
+
+ private void handleReflectionFailure(ReflectionException e) {
+ if (mReflectionFailureListener != null) {
+ mReflectionFailureListener.onReflectionFailure(e);
+ }
+ mReflectionSucceed = false;
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mReflectionFailureListener = null;
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/HtmlStyleTextWatcher.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/HtmlStyleTextWatcher.java
new file mode 100644
index 000000000..90ffc7fe0
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/HtmlStyleTextWatcher.java
@@ -0,0 +1,245 @@
+package org.wordpress.android.editor;
+
+import android.text.Editable;
+import android.text.Spannable;
+import android.text.TextWatcher;
+
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+public class HtmlStyleTextWatcher implements TextWatcher {
+ private enum Operation {
+ INSERT, DELETE, REPLACE, NONE
+ }
+
+ private int mOffset;
+ private CharSequence mModifiedText;
+ private Operation mLastOperation;
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ if (s == null) {
+ return;
+ }
+
+ int lastCharacterLocation = start + count - 1;
+ if (s.length() > lastCharacterLocation && lastCharacterLocation >= 0) {
+ if (after < count) {
+ if (after > 0) {
+ // Text was deleted and replaced by some other text
+ mLastOperation = Operation.REPLACE;
+ } else {
+ // Text was deleted only
+ mLastOperation = Operation.DELETE;
+ }
+
+ mOffset = start;
+ mModifiedText = s.subSequence(start + after, start + count);
+ }
+ }
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ if (s == null) {
+ return;
+ }
+
+ int lastCharacterLocation = start + count - 1;
+ if (s.length() > lastCharacterLocation) {
+ if (count > 0) {
+ if (before > 0) {
+ // Text was added, replacing some existing text
+ mLastOperation = Operation.REPLACE;
+ mModifiedText = s.subSequence(start, start + count);
+ } else {
+ // Text was added only
+ mLastOperation = Operation.INSERT;
+ mOffset = start;
+ mModifiedText = s.subSequence(start + before, start + count);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mModifiedText == null || s == null) {
+ return;
+ }
+
+ SpanRange spanRange;
+
+ // If the modified text included a tag or entity symbol ("<", ">", "&" or ";"), find its match and restyle
+ if (mModifiedText.toString().contains("<")) {
+ spanRange = getRespanRangeForChangedOpeningSymbol(s, "<");
+ } else if (mModifiedText.toString().contains(">")) {
+ spanRange = getRespanRangeForChangedClosingSymbol(s, ">");
+ } else if (mModifiedText.toString().contains("&")) {
+ spanRange = getRespanRangeForChangedOpeningSymbol(s, "&");
+ } else if (mModifiedText.toString().contains(";")) {
+ spanRange = getRespanRangeForChangedClosingSymbol(s, ";");
+ } else {
+ // If the modified text didn't include any tag or entity symbols, restyle if the modified text is inside
+ // a tag or entity
+ spanRange = getRespanRangeForNormalText(s, "<");
+ if (spanRange == null) {
+ spanRange = getRespanRangeForNormalText(s, "&");
+ }
+ }
+
+ if (spanRange != null) {
+ updateSpans(s, spanRange);
+ }
+
+ mModifiedText = null;
+ mLastOperation = Operation.NONE;
+ }
+
+ /**
+ * For changes made which contain at least one opening symbol (e.g. '<' or '&'), whether added or deleted, returns
+ * the range of text which should have its style reapplied.
+ * @param content the content after modification
+ * @param openingSymbol the opening symbol recognized (e.g. '<' or '&')
+ * @return the range of characters to re-apply spans to
+ */
+ protected SpanRange getRespanRangeForChangedOpeningSymbol(Editable content, String openingSymbol) {
+ // For simplicity, re-parse the document if text was replaced
+ if (mLastOperation == Operation.REPLACE) {
+ return new SpanRange(0, content.length());
+ }
+
+ String closingSymbol = getMatchingSymbol(openingSymbol);
+
+ int firstOpeningTagLoc = mOffset + mModifiedText.toString().indexOf(openingSymbol);
+ int closingTagLoc;
+ if (mLastOperation == Operation.INSERT) {
+ // Apply span from the first added opening symbol until the closing symbol in the content matching the
+ // last added opening symbol
+ // e.g. pasting "<b><" before "/b>" - we want the span to be applied to all of "<b></b>"
+ int lastOpeningTagLoc = mOffset + mModifiedText.toString().lastIndexOf(openingSymbol);
+ closingTagLoc = content.toString().indexOf(closingSymbol, lastOpeningTagLoc);
+ } else {
+ // Apply span until the first closing tag that appears after the deleted text
+ closingTagLoc = content.toString().indexOf(closingSymbol, mOffset);
+ }
+
+ if (closingTagLoc > 0) {
+ return new SpanRange(firstOpeningTagLoc, closingTagLoc + 1);
+ }
+ return null;
+ }
+
+ /**
+ * For changes made which contain at least one closing symbol (e.g. '>' or ';') and no opening symbols, whether
+ * added or deleted, returns the range of text which should have its style reapplied.
+ * @param content the content after modification
+ * @param closingSymbol the closing symbol recognized (e.g. '>' or ';')
+ * @return the range of characters to re-apply spans to
+ */
+ protected SpanRange getRespanRangeForChangedClosingSymbol(Editable content, String closingSymbol) {
+ // For simplicity, re-parse the document if text was replaced
+ if (mLastOperation == Operation.REPLACE) {
+ return new SpanRange(0, content.length());
+ }
+
+ String openingSymbol = getMatchingSymbol(closingSymbol);
+
+ int firstClosingTagInModLoc = mOffset + mModifiedText.toString().indexOf(closingSymbol);
+ int firstClosingTagAfterModLoc = content.toString().indexOf(closingSymbol, mOffset + mModifiedText.length());
+
+ int openingTagLoc = content.toString().lastIndexOf(openingSymbol, firstClosingTagInModLoc - 1);
+ if (openingTagLoc >= 0) {
+ if (firstClosingTagAfterModLoc >= 0) {
+ return new SpanRange(openingTagLoc, firstClosingTagAfterModLoc + 1);
+ } else {
+ return new SpanRange(openingTagLoc, content.length());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * For changes made which contain no opening or closing symbols, checks whether the changed text is inside a tag,
+ * and if so returns the range of text which should have its style reapplied.
+ * @param content the content after modification
+ * @param openingSymbol the opening symbol of the tag to check for (e.g. '<' or '&')
+ * @return the range of characters to re-apply spans to
+ */
+ protected SpanRange getRespanRangeForNormalText(Editable content, String openingSymbol) {
+ String closingSymbol = getMatchingSymbol(openingSymbol);
+
+ int openingTagLoc = content.toString().lastIndexOf(openingSymbol, mOffset);
+ if (openingTagLoc >= 0) {
+ int closingTagLoc = content.toString().indexOf(closingSymbol, openingTagLoc);
+ if (closingTagLoc >= mOffset) {
+ return new SpanRange(openingTagLoc, closingTagLoc + 1);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Clears and re-applies spans to {@code content} within range {@code spanRange} according to rules in
+ * {@link HtmlStyleUtils}.
+ * @param content the content to re-style
+ * @param spanRange the range within {@code content} to be re-styled
+ */
+ protected void updateSpans(Spannable content, SpanRange spanRange) {
+ int spanStart = spanRange.getOpeningTagLoc();
+ int spanEnd = spanRange.getClosingTagLoc();
+
+ if (spanStart > content.length() || spanEnd > content.length()) {
+ AppLog.d(T.EDITOR, "The specified span range was beyond the Spannable's length");
+ return;
+ } else if (spanStart >= spanEnd) {
+ // If the span start is after the end position (probably due to a multi-line deletion), selective
+ // re-styling won't work
+ // Instead, do a clean re-styling of the whole document
+ spanStart = 0;
+ spanEnd = content.length();
+ }
+
+ HtmlStyleUtils.clearSpans(content, spanStart, spanEnd);
+ HtmlStyleUtils.styleHtmlForDisplay(content, spanStart, spanEnd);
+ }
+
+ /**
+ * Returns the closing/opening symbol corresponding to the given opening/closing symbol.
+ */
+ private String getMatchingSymbol(String symbol) {
+ switch(symbol) {
+ case "<":
+ return ">";
+ case ">":
+ return "<";
+ case "&":
+ return ";";
+ case ";":
+ return "&";
+ default:
+ return "";
+ }
+ }
+
+ /**
+ * Stores a pair of integers describing a range of values.
+ */
+ protected static class SpanRange {
+ private final int mOpeningTagLoc;
+ private final int mClosingTagLoc;
+
+ public SpanRange(int openingTagLoc, int closingTagLoc) {
+ mOpeningTagLoc = openingTagLoc;
+ mClosingTagLoc = closingTagLoc;
+ }
+
+ public int getOpeningTagLoc() {
+ return mOpeningTagLoc;
+ }
+
+ public int getClosingTagLoc() {
+ return mClosingTagLoc;
+ }
+ }
+} \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/HtmlStyleUtils.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/HtmlStyleUtils.java
new file mode 100644
index 000000000..912781f1f
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/HtmlStyleUtils.java
@@ -0,0 +1,150 @@
+package org.wordpress.android.editor;
+
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.text.Spannable;
+import android.text.style.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StyleSpan;
+
+import org.wordpress.android.util.AppLog;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class HtmlStyleUtils {
+ public static final int TAG_COLOR = Color.rgb(0, 80, 130);
+ public static final int ATTRIBUTE_COLOR = Color.rgb(158, 158, 158);
+
+ public static final String REGEX_HTML_TAGS = "(<\\/?[a-z][^<>]*>)";
+ public static final String REGEX_HTML_ATTRIBUTES = "(?<==)('|\")(.*?\\1)(?=.*?>)";
+ public static final String REGEX_HTML_COMMENTS = "(<!--.*?-->)";
+ public static final String REGEX_HTML_ENTITIES = "(&#34;|&#38;|&#39;|&#60;|&#62;|&#160;|&#161;|&#162;|&#163;" +
+ "|&#164;|&#165;|&#166;|&#167;|&#168;|&#169;|&#170;|&#171;|&#172;|&#173;|&#174;|&#175;|&#176;|&#177;" +
+ "|&#178;|&#179;|&#180;|&#181;|&#182;|&#183;|&#184;|&#185;|&#186;|&#187;|&#188;|&#189;|&#190;|&#191;" +
+ "|&#192;|&#193;|&#194;|&#195;|&#196;|&#197;|&#198;|&#199;|&#200;|&#201;|&#202;|&#203;|&#204;|&#205;" +
+ "|&#206;|&#207;|&#208;|&#209;|&#210;|&#211;|&#212;|&#213;|&#214;|&#215;|&#216;|&#217;|&#218;|&#219;" +
+ "|&#220;|&#221;|&#222;|&#223;|&#224;|&#225;|&#226;|&#227;|&#228;|&#229;|&#230;|&#231;|&#232;|&#233;" +
+ "|&#234;|&#235;|&#236;|&#237;|&#238;|&#239;|&#240;|&#241;|&#242;|&#243;|&#244;|&#245;|&#246;|&#247;" +
+ "|&#248;|&#249;|&#250;|&#251;|&#252;|&#253;|&#254;|&#255;|&#338;|&#339;|&#352;|&#353;|&#376;|&#402;" +
+ "|&#710;|&#732;|&#913;|&#914;|&#915;|&#916;|&#917;|&#918;|&#919;|&#920;|&#921;|&#922;|&#923;|&#924;" +
+ "|&#925;|&#926;|&#927;|&#928;|&#929;|&#931;|&#932;|&#933;|&#934;|&#935;|&#936;|&#937;|&#945;|&#946;" +
+ "|&#947;|&#948;|&#949;|&#950;|&#951;|&#952;|&#953;|&#954;|&#955;|&#956;|&#957;|&#958;|&#959;|&#960;" +
+ "|&#961;|&#962;|&#963;|&#964;|&#965;|&#966;|&#967;|&#968;|&#969;|&#977;|&#978;|&#982;|&#8194;|&#8195;" +
+ "|&#8201;|&#8204;|&#8205;|&#8206;|&#8207;|&#8211;|&#8212;|&#8216;|&#8217;|&#8218;|&#8220;|&#8221;|&#8222;" +
+ "|&#8224;|&#8225;|&#8226;|&#8230;|&#8240;|&#8242;|&#8243;|&#8249;|&#8250;|&#8254;|&#8260;|&#8364;|&#8465;" +
+ "|&#8472;|&#8476;|&#8482;|&#8501;|&#8592;|&#8593;|&#8594;|&#8595;|&#8596;|&#8629;|&#8656;|&#8657;|&#8658;" +
+ "|&#8659;|&#8660;|&#8704;|&#8706;|&#8707;|&#8709;|&#8711;|&#8712;|&#8713;|&#8715;|&#8719;|&#8721;|&#8722;" +
+ "|&#8727;|&#8730;|&#8733;|&#8734;|&#8736;|&#8743;|&#8744;|&#8745;|&#8746;|&#8747;|&#8756;|&#8764;|&#8773;" +
+ "|&#8776;|&#8800;|&#8801;|&#8804;|&#8805;|&#8834;|&#8835;|&#8836;|&#8838;|&#8839;|&#8853;|&#8855;|&#8869;" +
+ "|&#8901;|&#8968;|&#8969;|&#8970;|&#8971;|&#9001;|&#9002;|&#9674;|&#9824;|&#9827;|&#9829;|&#9830;|&quot;" +
+ "|&amp;|&apos;|&lt;|&gt;|&nbsp;|&iexcl;|&cent;|&pound;|&curren;|&yen;|&brvbar;|&sect;|&uml;|&copy;|&ordf;" +
+ "|&laquo;|&not;|&shy;|&reg;|&macr;|&deg;|&plusmn;|&sup2;|&sup3;|&acute;|&micro;|&para;|&middot;|&cedil;" +
+ "|&sup1;|&ordm;|&raquo;|&frac14;|&frac12;|&frac34;|&iquest;|&Agrave;|&Aacute;|&Acirc;|&Atilde;|&Auml;" +
+ "|&Aring;|&AElig;|&Ccedil;|&Egrave;|&Eacute;|&Ecirc;|&Euml;|&Igrave;|&Iacute;|&Icirc;|&Iuml;|&ETH;" +
+ "|&Ntilde;|&Ograve;|&Oacute;|&Ocirc;|&Otilde;|&Ouml;|&times;|&Oslash;|&Ugrave;|&Uacute;|&Ucirc;|&Uuml;" +
+ "|&Yacute;|&THORN;|&szlig;|&agrave;|&aacute;|&acirc;|&atilde;|&auml;|&aring;|&aelig;|&ccedil;|&egrave;" +
+ "|&eacute;|&ecirc;|&euml;|&igrave;|&iacute;|&icirc;|&iuml;|&eth;|&ntilde;|&ograve;|&oacute;|&ocirc;" +
+ "|&otilde;|&ouml;|&divide;|&oslash;|&Ugrave;|&Uacute;|&Ucirc;|&Uuml;|&yacute;|&thorn;|&yuml;|&OElig;" +
+ "|&oelig;|&Scaron;|&scaron;|&Yuml;|&fnof;|&circ;|&tilde;|&Alpha;|&Beta;|&Gamma;|&Delta;|&Epsilon;|&Zeta;" +
+ "|&Eta;|&Theta;|&Iota;|&Kappa;|&Lambda;|&Mu;|&Nu;|&Xi;|&Omicron;|&Pi;|&Rho;|&Sigma;|&Tau;|&Upsilon;|&Phi;" +
+ "|&Chi;|&Psi;|&Omega;|&alpha;|&beta;|&gamma;|&delta;|&epsilon;|&zeta;|&eta;|&theta;|&iota;|&kappa;" +
+ "|&lambda;|&mu;|&nu;|&xi;|&omicron;|&pi;|&rho;|&sigmaf;|&sigma;|&tau;|&upsilon;|&phi;|&chi;|&psi;|&omega;" +
+ "|&thetasym;|&Upsih;|&piv;|&ensp;|&emsp;|&thinsp;|&zwnj;|&zwj;|&lrm;|&rlm;|&ndash;|&mdash;|&lsquo;" +
+ "|&rsquo;|&sbquo;|&ldquo;|&rdquo;|&bdquo;|&dagger;|&Dagger;|&bull;|&hellip;|&permil;|&prime;|&Prime;" +
+ "|&lsaquo;|&rsaquo;|&oline;|&frasl;|&euro;|&image;|&weierp;|&real;|&trade;|&alefsym;|&larr;|&uarr;|&rarr;" +
+ "|&darr;|&harr;|&crarr;|&lArr;|&UArr;|&rArr;|&dArr;|&hArr;|&forall;|&part;|&exist;|&empty;|&nabla;|&isin;" +
+ "|&notin;|&ni;|&prod;|&sum;|&minus;|&lowast;|&radic;|&prop;|&infin;|&ang;|&and;|&or;|&cap;|&cup;|&int;" +
+ "|&there4;|&sim;|&cong;|&asymp;|&ne;|&equiv;|&le;|&ge;|&sub;|&sup;|&nsub;|&sube;|&supe;|&oplus;|&otimes;" +
+ "|&perp;|&sdot;|&lceil;|&rceil;|&lfloor;|&rfloor;|&lang;|&rang;|&loz;|&spades;|&clubs;|&hearts;|&diams;)";
+
+ public static final int SPANNABLE_FLAGS = Spannable.SPAN_EXCLUSIVE_EXCLUSIVE;
+
+ /**
+ * Apply styling rules to {@code content}.
+ */
+ public static void styleHtmlForDisplay(@NonNull Spannable content) {
+ styleHtmlForDisplay(content, 0, content.length());
+ }
+
+ /**
+ * Apply styling rules to {@code content} inside the range from {@code start} to {@code end}.
+ *
+ * @param content the Spannable to apply style rules to
+ * @param start the index in {@code content} to start styling from
+ * @param end the index in {@code content} to style until
+ */
+ public static void styleHtmlForDisplay(@NonNull Spannable content, int start, int end) {
+ if (Build.VERSION.RELEASE.equals("4.1") || Build.VERSION.RELEASE.equals("4.1.1")) {
+ // Avoids crashing bug in Android 4.1 and 4.1.1 triggered when spanned text is line-wrapped
+ // AOSP issue: https://code.google.com/p/android/issues/detail?id=35466
+ return;
+ }
+
+ applySpansByRegex(content, start, end, REGEX_HTML_TAGS);
+ applySpansByRegex(content, start, end, REGEX_HTML_ATTRIBUTES);
+ applySpansByRegex(content, start, end, REGEX_HTML_COMMENTS);
+ applySpansByRegex(content, start, end, REGEX_HTML_ENTITIES);
+ }
+
+ /**
+ * Applies styles to {@code content} from {@code start} to {@code end}, based on rule {@code regex}.
+ * @param content the Spannable to apply style rules to
+ * @param start the index in {@code content} to start styling from
+ * @param end the index in {@code content} to style until
+ * @param regex the pattern to match for styling
+ */
+ private static void applySpansByRegex(Spannable content, int start, int end, String regex) {
+ if (content == null || start < 0 || end < 0 || start > content.length() || end > content.length() ||
+ start >= end) {
+ AppLog.d(AppLog.T.EDITOR, "applySpansByRegex() received invalid input");
+ return;
+ }
+
+ Pattern pattern = Pattern.compile(regex);
+ Matcher matcher = pattern.matcher(content.subSequence(start, end));
+
+ while (matcher.find()) {
+ int matchStart = matcher.start() + start;
+ int matchEnd = matcher.end() + start;
+ switch(regex) {
+ case REGEX_HTML_TAGS:
+ content.setSpan(new ForegroundColorSpan(TAG_COLOR), matchStart, matchEnd, SPANNABLE_FLAGS);
+ break;
+ case REGEX_HTML_ATTRIBUTES:
+ content.setSpan(new ForegroundColorSpan(ATTRIBUTE_COLOR), matchStart, matchEnd, SPANNABLE_FLAGS);
+ break;
+ case REGEX_HTML_COMMENTS:
+ content.setSpan(new ForegroundColorSpan(ATTRIBUTE_COLOR), matchStart, matchEnd, SPANNABLE_FLAGS);
+ content.setSpan(new StyleSpan(Typeface.ITALIC), matchStart, matchEnd, SPANNABLE_FLAGS);
+ content.setSpan(new RelativeSizeSpan(0.75f), matchStart, matchEnd, SPANNABLE_FLAGS);
+ break;
+ case REGEX_HTML_ENTITIES:
+ content.setSpan(new ForegroundColorSpan(TAG_COLOR), matchStart, matchEnd, SPANNABLE_FLAGS);
+ content.setSpan(new StyleSpan(Typeface.BOLD), matchStart, matchEnd, SPANNABLE_FLAGS);
+ content.setSpan(new RelativeSizeSpan(0.75f), matchStart, matchEnd, SPANNABLE_FLAGS);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Clears all relevant spans in {@code content} from {@code start} to {@code end}. Relevant spans are the subclasses
+ * of {@link CharacterStyle} applied by {@link HtmlStyleUtils#applySpansByRegex(Spannable, int, int, String)}.
+ * @param content the Spannable to clear styles from
+ * @param spanStart the index in {@code content} to start clearing styles from
+ * @param spanEnd the index in {@code content} to clear styles until
+ */
+ public static void clearSpans(Spannable content, int spanStart, int spanEnd) {
+ CharacterStyle[] spans = content.getSpans(spanStart, spanEnd, CharacterStyle.class);
+
+ for (CharacterStyle span : spans) {
+ if (span instanceof ForegroundColorSpan || span instanceof StyleSpan || span instanceof RelativeSizeSpan) {
+ content.removeSpan(span);
+ }
+ }
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/ImageSettingsDialogFragment.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/ImageSettingsDialogFragment.java
new file mode 100644
index 000000000..70a995b01
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/ImageSettingsDialogFragment.java
@@ -0,0 +1,431 @@
+package org.wordpress.android.editor;
+
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.app.AppCompatActivity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.SeekBar;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.MediaUtils;
+import org.wordpress.android.util.ToastUtils;
+
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * A full-screen DialogFragment with image settings.
+ *
+ * Modifies the action bar - host activity must call {@link ImageSettingsDialogFragment#dismissFragment()}
+ * when the fragment is dismissed to restore it.
+ */
+public class ImageSettingsDialogFragment extends DialogFragment {
+ public static final int IMAGE_SETTINGS_DIALOG_REQUEST_CODE = 5;
+ public static final String IMAGE_SETTINGS_DIALOG_TAG = "image-settings";
+
+ private JSONObject mImageMeta;
+ private int mMaxImageWidth;
+
+ private EditText mTitleText;
+ private EditText mCaptionText;
+ private EditText mAltText;
+ private Spinner mAlignmentSpinner;
+ private String[] mAlignmentKeyArray;
+ private EditText mLinkTo;
+ private EditText mWidthText;
+ private CheckBox mFeaturedCheckBox;
+
+ private boolean mIsFeatured;
+
+ private Map<String, String> mHttpHeaders;
+
+ private CharSequence mPreviousActionBarTitle;
+ private boolean mPreviousHomeAsUpEnabled;
+ private View mPreviousCustomView;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar == null) {
+ return;
+ }
+
+ actionBar.show();
+
+ mPreviousActionBarTitle = actionBar.getTitle();
+ mPreviousCustomView = actionBar.getCustomView();
+
+ final int displayOptions = actionBar.getDisplayOptions();
+ mPreviousHomeAsUpEnabled = (displayOptions & ActionBar.DISPLAY_HOME_AS_UP) != 0;
+
+ actionBar.setTitle(R.string.image_settings);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ if (getResources().getBoolean(R.bool.show_extra_side_padding)) {
+ actionBar.setHomeAsUpIndicator(R.drawable.ic_close_padded);
+ } else {
+ actionBar.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp);
+ }
+
+ // Show custom view with padded Save button
+ actionBar.setDisplayShowCustomEnabled(true);
+ actionBar.setCustomView(R.layout.image_settings_formatbar);
+
+ actionBar.getCustomView().findViewById(R.id.menu_save).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mImageMeta = extractMetaDataFromFields(mImageMeta);
+
+ String imageRemoteId = "";
+ try {
+ imageRemoteId = mImageMeta.getString("attachment_id");
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.EDITOR, "Unable to retrieve featured image id from meta data");
+ }
+
+ Intent intent = new Intent();
+ intent.putExtra("imageMeta", mImageMeta.toString());
+
+ mIsFeatured = mFeaturedCheckBox.isChecked();
+ intent.putExtra("isFeatured", mIsFeatured);
+
+ if (!imageRemoteId.isEmpty()) {
+ intent.putExtra("imageRemoteId", Integer.parseInt(imageRemoteId));
+ }
+
+ getTargetFragment().onActivityResult(getTargetRequestCode(), getTargetRequestCode(), intent);
+
+ restorePreviousActionBar();
+ getFragmentManager().popBackStack();
+ ToastUtils.showToast(getActivity(), R.string.image_settings_save_toast);
+ }
+ });
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.dialog_image_options, container, false);
+
+ ImageView thumbnailImage = (ImageView) view.findViewById(R.id.image_thumbnail);
+ TextView filenameLabel = (TextView) view.findViewById(R.id.image_filename);
+ mTitleText = (EditText) view.findViewById(R.id.image_title);
+ mCaptionText = (EditText) view.findViewById(R.id.image_caption);
+ mAltText = (EditText) view.findViewById(R.id.image_alt_text);
+ mAlignmentSpinner = (Spinner) view.findViewById(R.id.alignment_spinner);
+ mLinkTo = (EditText) view.findViewById(R.id.image_link_to);
+ SeekBar widthSeekBar = (SeekBar) view.findViewById(R.id.image_width_seekbar);
+ mWidthText = (EditText) view.findViewById(R.id.image_width_text);
+ mFeaturedCheckBox = (CheckBox) view.findViewById(R.id.featuredImage);
+
+ // Populate the dialog with existing values
+ Bundle bundle = getArguments();
+ if (bundle != null) {
+ try {
+ mImageMeta = new JSONObject(bundle.getString("imageMeta"));
+
+ mHttpHeaders = (Map) bundle.getSerializable("headerMap");
+
+ final String imageSrc = mImageMeta.getString("src");
+ final String imageFilename = imageSrc.substring(imageSrc.lastIndexOf("/") + 1);
+
+ loadThumbnail(imageSrc, thumbnailImage);
+ filenameLabel.setText(imageFilename);
+
+ mTitleText.setText(mImageMeta.getString("title"));
+ mCaptionText.setText(mImageMeta.getString("caption"));
+ mAltText.setText(mImageMeta.getString("alt"));
+
+ String alignment = mImageMeta.getString("align");
+ mAlignmentKeyArray = getResources().getStringArray(R.array.alignment_key_array);
+ int alignmentIndex = Arrays.asList(mAlignmentKeyArray).indexOf(alignment);
+ mAlignmentSpinner.setSelection(alignmentIndex == -1 ? 0 : alignmentIndex);
+
+ mLinkTo.setText(mImageMeta.getString("linkUrl"));
+
+ mMaxImageWidth = MediaUtils.getMaximumImageWidth(mImageMeta.getInt("naturalWidth"),
+ bundle.getString("maxWidth"));
+
+ setupWidthSeekBar(widthSeekBar, mWidthText, mImageMeta.getInt("width"));
+
+ boolean featuredImageSupported = bundle.getBoolean("featuredImageSupported");
+ if (featuredImageSupported) {
+ mFeaturedCheckBox.setVisibility(View.VISIBLE);
+ mIsFeatured = bundle.getBoolean("isFeatured", false);
+ mFeaturedCheckBox.setChecked(mIsFeatured);
+ }
+ } catch (JSONException e1) {
+ AppLog.d(AppLog.T.EDITOR, "Missing JSON properties");
+ }
+ }
+
+ mTitleText.requestFocus();
+
+ return view;
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.show();
+ }
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ if (menu != null) {
+ menu.clear();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+
+ if (id == android.R.id.home) {
+ dismissFragment();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private ActionBar getActionBar() {
+ if (getActivity() instanceof AppCompatActivity) {
+ return ((AppCompatActivity) getActivity()).getSupportActionBar();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * To be called when the fragment is being dismissed, either by ActionBar navigation or by pressing back in the
+ * navigation bar.
+ * Displays a confirmation dialog if there are unsaved changes, otherwise undoes the fragment's modifications to
+ * the ActionBar and restores the last visible fragment.
+ */
+ public void dismissFragment() {
+ try {
+ JSONObject newImageMeta = extractMetaDataFromFields(new JSONObject());
+
+ for (int i = 0; i < newImageMeta.names().length(); i++) {
+ String name = newImageMeta.names().getString(i);
+ if (!newImageMeta.getString(name).equals(mImageMeta.getString(name))) {
+ showDiscardChangesDialog();
+ return;
+ }
+ }
+
+ if (mFeaturedCheckBox.isChecked() != mIsFeatured) {
+ // Featured image status has changed
+ showDiscardChangesDialog();
+ return;
+ }
+ } catch (JSONException e) {
+ AppLog.d(AppLog.T.EDITOR, "Unable to update JSON array");
+ }
+
+ getTargetFragment().onActivityResult(getTargetRequestCode(), getTargetRequestCode(), null);
+ restorePreviousActionBar();
+ getFragmentManager().popBackStack();
+ }
+
+ private void restorePreviousActionBar() {
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle(mPreviousActionBarTitle);
+ actionBar.setHomeAsUpIndicator(null);
+ actionBar.setDisplayHomeAsUpEnabled(mPreviousHomeAsUpEnabled);
+
+ actionBar.setCustomView(mPreviousCustomView);
+ if (mPreviousCustomView == null) {
+ actionBar.setDisplayShowCustomEnabled(false);
+ }
+ }
+ }
+
+ /**
+ * Displays a dialog asking the user to confirm that they want to exit, discarding unsaved changes.
+ */
+ private void showDiscardChangesDialog() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(getString(R.string.image_settings_dismiss_dialog_title));
+ builder.setPositiveButton(getString(R.string.discard), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ getTargetFragment().onActivityResult(getTargetRequestCode(), getTargetRequestCode(), null);
+ restorePreviousActionBar();
+ getFragmentManager().popBackStack();
+ }
+ });
+
+ builder.setNegativeButton(getString(R.string.cancel), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.dismiss();
+ }
+ });
+
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ /**
+ * Extracts the meta data from the dialog fields and updates the entries in the given JSONObject.
+ */
+ private JSONObject extractMetaDataFromFields(JSONObject metaData) {
+ try {
+ metaData.put("title", mTitleText.getText().toString());
+ metaData.put("caption", mCaptionText.getText().toString());
+ metaData.put("alt", mAltText.getText().toString());
+ if (mAlignmentSpinner.getSelectedItemPosition() < mAlignmentKeyArray.length) {
+ metaData.put("align", mAlignmentKeyArray[mAlignmentSpinner.getSelectedItemPosition()]);
+ }
+ metaData.put("linkUrl", mLinkTo.getText().toString());
+
+ int newWidth = getEditTextIntegerClamped(mWidthText, 10, mMaxImageWidth);
+ metaData.put("width", newWidth);
+ metaData.put("height", getRelativeHeightFromWidth(newWidth));
+ } catch (JSONException e) {
+ AppLog.d(AppLog.T.EDITOR, "Unable to build JSON object from new meta data");
+ }
+
+ return metaData;
+ }
+
+ private void loadThumbnail(final String src, final ImageView thumbnailImage) {
+ Thread thread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ if (isAdded()) {
+ final Uri localUri = Utils.downloadExternalMedia(getActivity(), Uri.parse(src), mHttpHeaders);
+
+ if (getActivity() != null) {
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ thumbnailImage.setImageURI(localUri);
+ }
+ });
+ }
+ }
+ }
+ });
+
+ thread.start();
+ }
+
+ /**
+ * Initialize the image width SeekBar and accompanying EditText
+ */
+ private void setupWidthSeekBar(final SeekBar widthSeekBar, final EditText widthText, int imageWidth) {
+ widthSeekBar.setMax(mMaxImageWidth / 10);
+
+ if (imageWidth != 0) {
+ widthSeekBar.setProgress(imageWidth / 10);
+ widthText.setText(String.valueOf(imageWidth) + "px");
+ }
+
+ widthSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (progress == 0) {
+ progress = 1;
+ }
+ widthText.setText(progress * 10 + "px");
+ }
+ });
+
+ widthText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (hasFocus) {
+ widthText.setText("");
+ }
+ }
+ });
+
+ widthText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ int width = getEditTextIntegerClamped(widthText, 10, mMaxImageWidth);
+ widthSeekBar.setProgress(width / 10);
+ widthText.setSelection((String.valueOf(width).length()));
+
+ InputMethodManager imm = (InputMethodManager) getActivity()
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(widthText.getWindowToken(),
+ InputMethodManager.RESULT_UNCHANGED_SHOWN);
+
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Return the integer value of the width EditText, adjusted to be within the given min and max, and stripped of the
+ * 'px' units
+ */
+ private int getEditTextIntegerClamped(EditText editText, int minWidth, int maxWidth) {
+ int width = 10;
+
+ try {
+ if (editText.getText() != null)
+ width = Integer.parseInt(editText.getText().toString().replace("px", ""));
+ } catch (NumberFormatException e) {
+ AppLog.e(AppLog.T.EDITOR, e);
+ }
+
+ width = Math.min(maxWidth, Math.max(width, minWidth));
+
+ return width;
+ }
+
+ /**
+ * Given the new width, return the proportionally adjusted height, given the dimensions of the original image
+ */
+ private int getRelativeHeightFromWidth(int width) {
+ int height = 0;
+
+ try {
+ int naturalHeight = mImageMeta.getInt("naturalHeight");
+ int naturalWidth = mImageMeta.getInt("naturalWidth");
+
+ float ratio = (float) naturalHeight / naturalWidth;
+ height = (int) (ratio * width);
+ } catch (JSONException e) {
+ AppLog.d(AppLog.T.EDITOR, "JSON object missing naturalHeight or naturalWidth property");
+ }
+
+ return height;
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/JsCallbackReceiver.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/JsCallbackReceiver.java
new file mode 100755
index 000000000..b8212fcef
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/JsCallbackReceiver.java
@@ -0,0 +1,236 @@
+package org.wordpress.android.editor;
+
+import android.webkit.JavascriptInterface;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.JSONUtils;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.wordpress.android.editor.EditorFragmentAbstract.MediaType;
+
+public class JsCallbackReceiver {
+ private static final String JS_CALLBACK_DELIMITER = "~";
+
+ private static final String CALLBACK_DOM_LOADED = "callback-dom-loaded";
+ private static final String CALLBACK_NEW_FIELD = "callback-new-field";
+
+ private static final String CALLBACK_INPUT = "callback-input";
+ private static final String CALLBACK_SELECTION_CHANGED = "callback-selection-changed";
+ private static final String CALLBACK_SELECTION_STYLE = "callback-selection-style";
+
+ private static final String CALLBACK_FOCUS_IN = "callback-focus-in";
+ private static final String CALLBACK_FOCUS_OUT = "callback-focus-out";
+
+ private static final String CALLBACK_IMAGE_REPLACED = "callback-image-replaced";
+ 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";
+
+ private static final String CALLBACK_LOG = "callback-log";
+
+ private static final String CALLBACK_RESPONSE_STRING = "callback-response-string";
+
+ private static final String CALLBACK_ACTION_FINISHED = "callback-action-finished";
+
+ private final OnJsEditorStateChangedListener mListener;
+
+ private Set<String> mPreviousStyleSet = new HashSet<>();
+
+ public JsCallbackReceiver(EditorFragmentAbstract editorFragmentAbstract) {
+ mListener = (OnJsEditorStateChangedListener) editorFragmentAbstract;
+ }
+
+ @JavascriptInterface
+ public void executeCallback(String callbackId, String params) {
+ switch (callbackId) {
+ case CALLBACK_DOM_LOADED:
+ mListener.onDomLoaded();
+ break;
+ case CALLBACK_SELECTION_STYLE:
+ // Compare the new styles to the previous ones, and notify the JsCallbackListener of the changeset
+ Set<String> rawStyleSet = Utils.splitDelimitedString(params, JS_CALLBACK_DELIMITER);
+
+ // Strip link details from active style set
+ Set<String> newStyleSet = new HashSet<>();
+ for (String element : rawStyleSet) {
+ if (element.matches("link:(.*)")) {
+ newStyleSet.add("link");
+ } else if (!element.matches("link-title:(.*)")) {
+ newStyleSet.add(element);
+ }
+ }
+
+ mListener.onSelectionStyleChanged(Utils.getChangeMapFromSets(mPreviousStyleSet, newStyleSet));
+ mPreviousStyleSet = newStyleSet;
+ break;
+ case CALLBACK_SELECTION_CHANGED:
+ // Called for changes to the field in current focus and for changes made to selection
+ // (includes moving the caret without selecting text)
+ // TODO: Possibly needed for handling WebView scrolling when caret moves (from iOS)
+ Set<String> selectionKeyValueSet = Utils.splitDelimitedString(params, JS_CALLBACK_DELIMITER);
+ mListener.onSelectionChanged(Utils.buildMapFromKeyValuePairs(selectionKeyValueSet));
+ break;
+ case CALLBACK_INPUT:
+ // Called on key press
+ // TODO: Possibly needed for handling WebView scrolling when caret moves (from iOS)
+ break;
+ case CALLBACK_FOCUS_IN:
+ // TODO: Needed to handle displaying/graying the format bar when focus changes between the title and content
+ AppLog.d(AppLog.T.EDITOR, "Focus in callback received");
+ break;
+ case CALLBACK_FOCUS_OUT:
+ // TODO: Needed to handle displaying/graying the format bar when focus changes between the title and content
+ AppLog.d(AppLog.T.EDITOR, "Focus out callback received");
+ break;
+ case CALLBACK_NEW_FIELD:
+ // TODO: Used for logging/testing purposes on iOS
+ AppLog.d(AppLog.T.EDITOR, "New field created, " + params);
+ break;
+ case CALLBACK_IMAGE_REPLACED:
+ AppLog.d(AppLog.T.EDITOR, "Image replaced, " + params);
+
+ // Extract the local media id from the callback string (stripping the 'id=' part)
+ if (params.length() > 3) {
+ mListener.onMediaReplaced(params.substring(3));
+ }
+ break;
+ case CALLBACK_VIDEO_REPLACED:
+ AppLog.d(AppLog.T.EDITOR, "Video replaced, " + params);
+
+ // Extract the local media id from the callback string (stripping the 'id=' part)
+ if (params.length() > 3) {
+ mListener.onMediaReplaced(params.substring(3));
+ }
+ break;
+ case CALLBACK_IMAGE_TAP:
+ AppLog.d(AppLog.T.EDITOR, "Image tapped, " + params);
+
+ String uploadStatus = "";
+
+ List<String> mediaIds = new ArrayList<>();
+ mediaIds.add("id");
+ mediaIds.add("url");
+ mediaIds.add("meta");
+ mediaIds.add("type");
+
+ Set<String> mediaDataSet = Utils.splitValuePairDelimitedString(params, JS_CALLBACK_DELIMITER, mediaIds);
+ Map<String, String> mediaDataMap = Utils.buildMapFromKeyValuePairs(mediaDataSet);
+
+ String mediaId = mediaDataMap.get("id");
+
+ String mediaUrl = mediaDataMap.get("url");
+ if (mediaUrl != null) {
+ mediaUrl = Utils.decodeHtml(mediaUrl);
+ }
+
+ MediaType mediaType = MediaType.fromString(mediaDataMap.get("type"));
+
+ String mediaMeta = mediaDataMap.get("meta");
+ JSONObject mediaMetaJson = new JSONObject();
+
+ if (mediaMeta != null) {
+ mediaMeta = Utils.decodeHtml(mediaMeta);
+
+ try {
+ mediaMetaJson = new JSONObject(mediaMeta);
+ String classes = JSONUtils.getString(mediaMetaJson, "classes");
+ Set<String> classesSet = Utils.splitDelimitedString(classes, ", ");
+
+ if (classesSet.contains("uploading")) {
+ uploadStatus = "uploading";
+ } else if (classesSet.contains("failed")) {
+ uploadStatus = "failed";
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ AppLog.d(AppLog.T.EDITOR, "Media meta data from callback-image-tap was not JSON-formatted");
+ }
+ }
+
+ mListener.onMediaTapped(mediaId, mediaType, mediaMetaJson, uploadStatus);
+ break;
+ case CALLBACK_LINK_TAP:
+ // Extract and HTML-decode the link data from the callback params
+ AppLog.d(AppLog.T.EDITOR, "Link tapped, " + params);
+
+ List<String> linkIds = new ArrayList<>();
+ linkIds.add("url");
+ linkIds.add("title");
+
+ Set<String> linkDataSet = Utils.splitValuePairDelimitedString(params, JS_CALLBACK_DELIMITER, linkIds);
+ Map<String, String> linkDataMap = Utils.buildMapFromKeyValuePairs(linkDataSet);
+
+ String url = linkDataMap.get("url");
+ if (url != null) {
+ url = Utils.decodeHtml(url);
+ }
+
+ String title = linkDataMap.get("title");
+ if (title != null) {
+ title = Utils.decodeHtml(title);
+ }
+
+ 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) {
+ mListener.onVideoPressInfoRequested(params.substring(3));
+ }
+ break;
+ case CALLBACK_LOG:
+ // Strip 'msg=' from beginning of string
+ if (params.length() > 4) {
+ AppLog.d(AppLog.T.EDITOR, callbackId + ": " + params.substring(4));
+ }
+ break;
+ case CALLBACK_RESPONSE_STRING:
+ AppLog.d(AppLog.T.EDITOR, callbackId + ": " + params);
+ Set<String> responseDataSet;
+ if (params.startsWith("function=") && params.contains(JS_CALLBACK_DELIMITER)) {
+ String functionName = params.substring("function=".length(), params.indexOf(JS_CALLBACK_DELIMITER));
+
+ List<String> responseIds = new ArrayList<>();
+ switch (functionName) {
+ case "getHTMLForCallback":
+ responseIds.add("id");
+ responseIds.add("contents");
+ break;
+ case "getSelectedTextToLinkify":
+ responseIds.add("result");
+ break;
+ case "getFailedMedia":
+ responseIds.add("ids");
+ }
+
+ responseDataSet = Utils.splitValuePairDelimitedString(params, JS_CALLBACK_DELIMITER, responseIds);
+ } else {
+ responseDataSet = Utils.splitDelimitedString(params, JS_CALLBACK_DELIMITER);
+ }
+ mListener.onGetHtmlResponse(Utils.buildMapFromKeyValuePairs(responseDataSet));
+ break;
+ case CALLBACK_ACTION_FINISHED:
+ mListener.onActionFinished();
+ break;
+ default:
+ AppLog.d(AppLog.T.EDITOR, "Unhandled callback: " + callbackId + ":" + params);
+ }
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/LegacyEditorFragment.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/LegacyEditorFragment.java
new file mode 100644
index 000000000..75febb3fa
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/LegacyEditorFragment.java
@@ -0,0 +1,1194 @@
+package org.wordpress.android.editor;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Typeface;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.text.Editable;
+import android.text.Layout;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.text.method.ArrowKeyMovementMethod;
+import android.text.style.AlignmentSpan;
+import android.text.style.QuoteSpan;
+import android.text.style.StrikethroughSpan;
+import android.text.style.StyleSpan;
+import android.text.style.URLSpan;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.WindowManager;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.webkit.URLUtil;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.ToggleButton;
+
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.ImageLoader;
+
+import org.wordpress.android.editor.legacy.EditLinkActivity;
+import org.wordpress.android.editor.legacy.WPEditImageSpan;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.ImageUtils;
+import org.wordpress.android.util.MediaUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.ToastUtils.Duration;
+import org.wordpress.android.util.helpers.MediaFile;
+import org.wordpress.android.util.helpers.MediaGallery;
+import org.wordpress.android.util.helpers.MediaGalleryImageSpan;
+import org.wordpress.android.util.helpers.WPImageSpan;
+import org.wordpress.android.util.helpers.WPUnderlineSpan;
+import org.wordpress.android.util.widgets.WPEditText;
+
+import java.util.Locale;
+
+public class LegacyEditorFragment extends EditorFragmentAbstract implements TextWatcher,
+ WPEditText.OnSelectionChangedListener, View.OnTouchListener {
+ public static final int ACTIVITY_REQUEST_CODE_CREATE_LINK = 4;
+ public static final String ACTION_MEDIA_GALLERY_TOUCHED = "MEDIA_GALLERY_TOUCHED";
+ public static final String EXTRA_MEDIA_GALLERY = "EXTRA_MEDIA_GALLERY";
+
+ private static final int MIN_THUMBNAIL_WIDTH = 200;
+ private static final int CONTENT_ANIMATION_DURATION = 250;
+ private static final String KEY_IMAGE_SPANS = "image-spans";
+ private static final String KEY_START = "start";
+ private static final String KEY_END = "end";
+ private static final String KEY_CONTENT = "content";
+ private static final String TAG_FORMAT_BAR_BUTTON_STRONG = "strong";
+ private static final String TAG_FORMAT_BAR_BUTTON_EM = "em";
+ private static final String TAG_FORMAT_BAR_BUTTON_UNDERLINE = "u";
+ private static final String TAG_FORMAT_BAR_BUTTON_STRIKE = "strike";
+ private static final String TAG_FORMAT_BAR_BUTTON_QUOTE = "blockquote";
+
+ private View mRootView;
+ private WPEditText mContentEditText;
+ private EditText mTitleEditText;
+ private ToggleButton mBoldToggleButton, mEmToggleButton, mBquoteToggleButton;
+ private ToggleButton mUnderlineToggleButton, mStrikeToggleButton;
+ private LinearLayout mFormatBar, mPostContentLinearLayout, mPostSettingsLinearLayout;
+ private Button mAddPictureButton;
+ private boolean mIsBackspace;
+ private boolean mScrollDetected;
+ private boolean mIsLocalDraft;
+
+ private int mStyleStart, mSelectionStart, mSelectionEnd, mFullViewBottom;
+ private int mLastPosition = -1;
+ private CharSequence mTitle;
+ private CharSequence mContent;
+
+ private float mLastYPos = 0;
+
+ @Override
+ public boolean onBackPressed() {
+ // leave full screen mode back button is pressed
+ if (getActionBar() != null && !getActionBar().isShowing()) {
+ setContentEditingModeVisible(false);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public CharSequence getTitle() {
+ if (mTitleEditText != null) {
+ return mTitleEditText.getText().toString();
+ }
+ return mTitle;
+ }
+
+ @Override
+ public CharSequence getContent() {
+ if (mContentEditText != null) {
+ return mContentEditText.getText().toString();
+ }
+ return mContent;
+ }
+
+ @Override
+ public void setTitle(CharSequence text) {
+ mTitle = text;
+ if (mTitleEditText != null) {
+ mTitleEditText.setText(text);
+ } else {
+ // TODO
+ }
+ }
+
+ @Override
+ public void setContent(CharSequence text) {
+ mContent = text;
+ if (mContentEditText != null) {
+ mContentEditText.setText(text);
+ mContentEditText.setSelection(mSelectionStart, mSelectionEnd);
+ } else {
+ // TODO
+ }
+ }
+
+ @Override
+ public Spanned getSpannedContent() {
+ return mContentEditText.getText();
+ }
+
+ public void setLocalDraft(boolean isLocalDraft) {
+ mIsLocalDraft = isLocalDraft;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.fragment_edit_post_content, container, false);
+
+ mFormatBar = (LinearLayout) rootView.findViewById(R.id.format_bar);
+ mTitleEditText = (EditText) rootView.findViewById(R.id.post_title);
+ mTitleEditText.setText(mTitle);
+ mTitleEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ // Go to full screen editor when 'next' button is tapped on soft keyboard
+ ActionBar actionBar = getActionBar();
+ if (actionId == EditorInfo.IME_ACTION_NEXT && actionBar != null && actionBar.isShowing()) {
+ setContentEditingModeVisible(true);
+ }
+ return false;
+ }
+ });
+
+ mContentEditText = (WPEditText) rootView.findViewById(R.id.post_content);
+ mContentEditText.setText(mContent);
+
+ mPostContentLinearLayout = (LinearLayout) rootView.findViewById(R.id.post_content_wrapper);
+ mPostSettingsLinearLayout = (LinearLayout) rootView.findViewById(R.id.post_settings_wrapper);
+ Button postSettingsButton = (Button) rootView.findViewById(R.id.post_settings_button);
+ postSettingsButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mEditorFragmentListener.onSettingsClicked();
+ }
+ });
+ mBoldToggleButton = (ToggleButton) rootView.findViewById(R.id.bold);
+ mEmToggleButton = (ToggleButton) rootView.findViewById(R.id.em);
+ mBquoteToggleButton = (ToggleButton) rootView.findViewById(R.id.bquote);
+ mUnderlineToggleButton = (ToggleButton) rootView.findViewById(R.id.underline);
+ mStrikeToggleButton = (ToggleButton) rootView.findViewById(R.id.strike);
+ mAddPictureButton = (Button) rootView.findViewById(R.id.addPictureButton);
+ Button linkButton = (Button) rootView.findViewById(R.id.link);
+ Button moreButton = (Button) rootView.findViewById(R.id.more);
+
+ registerForContextMenu(mAddPictureButton);
+ mContentEditText = (WPEditText) rootView.findViewById(R.id.post_content);
+ mContentEditText.setOnSelectionChangedListener(this);
+ mContentEditText.setOnTouchListener(this);
+ mContentEditText.addTextChangedListener(this);
+ mContentEditText.setOnEditTextImeBackListener(new WPEditText.EditTextImeBackListener() {
+ @Override
+ public void onImeBack(WPEditText ctrl, String text) {
+ // Go back to regular editor if IME keyboard is dismissed
+ // Bottom comparison is there to ensure that the keyboard is actually showing
+ ActionBar actionBar = getActionBar();
+ if (mRootView.getBottom() < mFullViewBottom && actionBar != null && !actionBar.isShowing()) {
+ setContentEditingModeVisible(false);
+ }
+ }
+ });
+ mAddPictureButton.setOnClickListener(mFormatBarButtonClickListener);
+ mBoldToggleButton.setOnClickListener(mFormatBarButtonClickListener);
+ linkButton.setOnClickListener(mFormatBarButtonClickListener);
+ mEmToggleButton.setOnClickListener(mFormatBarButtonClickListener);
+ mUnderlineToggleButton.setOnClickListener(mFormatBarButtonClickListener);
+ mStrikeToggleButton.setOnClickListener(mFormatBarButtonClickListener);
+ mBquoteToggleButton.setOnClickListener(mFormatBarButtonClickListener);
+ moreButton.setOnClickListener(mFormatBarButtonClickListener);
+ mEditorFragmentListener.onEditorFragmentInitialized();
+
+ if (savedInstanceState != null) {
+ Parcelable[] spans = savedInstanceState.getParcelableArray(KEY_IMAGE_SPANS);
+
+ mContent = savedInstanceState.getString(KEY_CONTENT, "");
+ mContentEditText.setText(mContent);
+ mContentEditText.setSelection(savedInstanceState.getInt(KEY_START, 0),
+ savedInstanceState.getInt(KEY_END, 0));
+
+ if (spans != null && spans.length > 0) {
+ for (Parcelable s : spans) {
+ WPImageSpan editSpan = (WPImageSpan)s;
+ addMediaFile(editSpan.getMediaFile(), editSpan.getMediaFile().getFilePath(),
+ mImageLoader, editSpan.getStartPosition(), editSpan.getEndPosition());
+ }
+ }
+ }
+
+ return rootView;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ mRootView = view;
+ mRootView.getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener);
+ }
+
+ private ViewTreeObserver.OnGlobalLayoutListener mGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
+ public void onGlobalLayout() {
+ mRootView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ mFullViewBottom = mRootView.getBottom();
+ }
+ };
+
+ private ActionBar getActionBar() {
+ if (!isAdded()) {
+ return null;
+ }
+ if (getActivity() instanceof AppCompatActivity) {
+ return ((AppCompatActivity) getActivity()).getSupportActionBar();
+ } else {
+ return null;
+ }
+ }
+
+ public void setContentEditingModeVisible(boolean isVisible) {
+ if (!isAdded()) {
+ return;
+ }
+ ActionBar actionBar = getActionBar();
+ if (isVisible) {
+ Animation fadeAnimation = new AlphaAnimation(1, 0);
+ fadeAnimation.setDuration(CONTENT_ANIMATION_DURATION);
+ fadeAnimation.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ mTitleEditText.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ mPostSettingsLinearLayout.setVisibility(View.GONE);
+ mFormatBar.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+ });
+
+ mPostContentLinearLayout.startAnimation(fadeAnimation);
+ if (actionBar != null) {
+ actionBar.hide();
+ }
+ } else {
+ mTitleEditText.setVisibility(View.VISIBLE);
+ mFormatBar.setVisibility(View.GONE);
+ Animation fadeAnimation = new AlphaAnimation(0, 1);
+ fadeAnimation.setDuration(CONTENT_ANIMATION_DURATION);
+ fadeAnimation.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ mPostSettingsLinearLayout.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+ });
+ mPostContentLinearLayout.startAnimation(fadeAnimation);
+ getActivity().invalidateOptionsMenu();
+ if (actionBar != null) {
+ actionBar.show();
+ }
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (requestCode == LegacyEditorFragment.ACTIVITY_REQUEST_CODE_CREATE_LINK && data != null) {
+ Bundle extras = data.getExtras();
+ if (extras == null) {
+ return;
+ }
+ String linkURL = extras.getString("linkURL");
+ String linkText = extras.getString("linkText");
+ createLinkFromSelection(linkURL, linkText);
+ }
+ }
+
+ public boolean hasEmptyContentFields() {
+ return TextUtils.isEmpty(mTitleEditText.getText()) && TextUtils.isEmpty(mContentEditText.getText());
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mFullViewBottom = mRootView.getBottom();
+ }
+
+ private void createLinkFromSelection(String linkURL, String linkText) {
+ try {
+ if (linkURL != null && !linkURL.equals("http://") && !linkURL.equals("")) {
+ if (mSelectionStart > mSelectionEnd) {
+ int temp = mSelectionEnd;
+ mSelectionEnd = mSelectionStart;
+ mSelectionStart = temp;
+ }
+ Editable editable = mContentEditText.getText();
+ if (editable == null) {
+ return;
+ }
+ if (mIsLocalDraft) {
+ if (linkText == null) {
+ if (mSelectionStart < mSelectionEnd) {
+ editable.delete(mSelectionStart, mSelectionEnd);
+ }
+ editable.insert(mSelectionStart, linkURL);
+ editable.setSpan(new URLSpan(linkURL), mSelectionStart, mSelectionStart + linkURL.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ mContentEditText.setSelection(mSelectionStart + linkURL.length());
+ } else {
+ if (mSelectionStart < mSelectionEnd) {
+ editable.delete(mSelectionStart, mSelectionEnd);
+ }
+ editable.insert(mSelectionStart, linkText);
+ editable.setSpan(new URLSpan(linkURL), mSelectionStart, mSelectionStart + linkText.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ mContentEditText.setSelection(mSelectionStart + linkText.length());
+ }
+ } else {
+ if (linkText == null) {
+ if (mSelectionStart < mSelectionEnd) {
+ editable.delete(mSelectionStart, mSelectionEnd);
+ }
+ String urlHTML = "<a href=\"" + linkURL + "\">" + linkURL + "</a>";
+ editable.insert(mSelectionStart, urlHTML);
+ mContentEditText.setSelection(mSelectionStart + urlHTML.length());
+ } else {
+ if (mSelectionStart < mSelectionEnd) {
+ editable.delete(mSelectionStart, mSelectionEnd);
+ }
+ String urlHTML = "<a href=\"" + linkURL + "\">" + linkText + "</a>";
+ editable.insert(mSelectionStart, urlHTML);
+ mContentEditText.setSelection(mSelectionStart + urlHTML.length());
+ }
+ }
+ }
+ } catch (RuntimeException e) {
+ AppLog.e(T.POSTS, e);
+ }
+ }
+
+ /**
+ * Formatting bar
+ */
+ private View.OnClickListener mFormatBarButtonClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int id = v.getId();
+ if (id == R.id.bold) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.BOLD_BUTTON_TAPPED);
+ onFormatButtonClick(mBoldToggleButton, TAG_FORMAT_BAR_BUTTON_STRONG);
+ } else if (id == R.id.em) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.ITALIC_BUTTON_TAPPED);
+ onFormatButtonClick(mEmToggleButton, TAG_FORMAT_BAR_BUTTON_EM);
+ } else if (id == R.id.underline) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.UNDERLINE_BUTTON_TAPPED);
+ onFormatButtonClick(mUnderlineToggleButton, TAG_FORMAT_BAR_BUTTON_UNDERLINE);
+ } else if (id == R.id.strike) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.STRIKETHROUGH_BUTTON_TAPPED);
+ onFormatButtonClick(mStrikeToggleButton, TAG_FORMAT_BAR_BUTTON_STRIKE);
+ } else if (id == R.id.bquote) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.BLOCKQUOTE_BUTTON_TAPPED);
+ onFormatButtonClick(mBquoteToggleButton, TAG_FORMAT_BAR_BUTTON_QUOTE);
+ } else if (id == R.id.more) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.MORE_BUTTON_TAPPED);
+ mSelectionEnd = mContentEditText.getSelectionEnd();
+ Editable str = mContentEditText.getText();
+ if (str != null) {
+ if (mSelectionEnd > str.length())
+ mSelectionEnd = str.length();
+ str.insert(mSelectionEnd, "\n<!--more-->\n");
+ }
+ } else if (id == R.id.link) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.LINK_BUTTON_TAPPED);
+ mSelectionStart = mContentEditText.getSelectionStart();
+ mStyleStart = mSelectionStart;
+ mSelectionEnd = mContentEditText.getSelectionEnd();
+ if (mSelectionStart > mSelectionEnd) {
+ int temp = mSelectionEnd;
+ mSelectionEnd = mSelectionStart;
+ mSelectionStart = temp;
+ }
+ Intent i = new Intent(getActivity(), EditLinkActivity.class);
+ if (mSelectionEnd > mSelectionStart) {
+ if (mContentEditText.getText() != null) {
+ String selectedText = mContentEditText.getText().subSequence(mSelectionStart, mSelectionEnd).toString();
+ i.putExtra("selectedText", selectedText);
+ }
+ }
+ startActivityForResult(i, ACTIVITY_REQUEST_CODE_CREATE_LINK);
+ } else if (id == R.id.addPictureButton) {
+ mEditorFragmentListener.onTrackableEvent(TrackableEvent.MEDIA_BUTTON_TAPPED);
+ mEditorFragmentListener.onAddMediaClicked();
+ if (isAdded()) {
+ getActivity().openContextMenu(mAddPictureButton);
+ }
+ }
+ }
+ };
+
+ private WPEditImageSpan createWPEditImageSpanLocal(Context context, MediaFile mediaFile) {
+ if (context == null || mediaFile == null || mediaFile.getFilePath() == null) {
+ return null;
+ }
+ Uri imageUri = Uri.parse(mediaFile.getFilePath());
+ Bitmap thumbnailBitmap;
+ if (MediaUtils.isVideo(imageUri.toString())) {
+ thumbnailBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.media_movieclip);
+ } else {
+ thumbnailBitmap = ImageUtils.getWPImageSpanThumbnailFromFilePath(context, imageUri.getEncodedPath(),
+ ImageUtils.getMaximumThumbnailWidthForEditor(context));
+ if (thumbnailBitmap == null) {
+ // Use a placeholder in case thumbnail can't be decoded (OOM for instance)
+ thumbnailBitmap = BitmapFactory.decodeResource(context.getResources(),
+ R.drawable.legacy_dashicon_format_image_big_grey);
+ }
+ }
+ WPEditImageSpan imageSpan = new WPEditImageSpan(context, thumbnailBitmap, imageUri);
+ mediaFile.setWidth(MediaUtils.getMaximumImageWidth(context, imageUri, mBlogSettingMaxImageWidth));
+ imageSpan.setMediaFile(mediaFile);
+ return imageSpan;
+ }
+
+ private WPEditImageSpan createWPEditImageSpanRemote(Context context, MediaFile mediaFile) {
+ if (context == null || mediaFile == null || mediaFile.getFileURL() == null) {
+ return null;
+ }
+ int drawable = mediaFile.isVideo() ? R.drawable.media_movieclip : R.drawable.legacy_dashicon_format_image_big_grey;
+ Uri uri = Uri.parse(mediaFile.getFileURL());
+ WPEditImageSpan imageSpan = new WPEditImageSpan(context, drawable, uri);
+ imageSpan.setMediaFile(mediaFile);
+ return imageSpan;
+ }
+
+ private WPEditImageSpan createWPEditImageSpan(Context context, MediaFile mediaFile) {
+ if (!URLUtil.isNetworkUrl(mediaFile.getFileURL())) {
+ return createWPEditImageSpanLocal(context, mediaFile);
+ } else {
+ return createWPEditImageSpanRemote(context, mediaFile);
+ }
+ }
+
+ /**
+ * Applies formatting to selected text, or marks the entry for a new text style
+ * at the current cursor position
+ * @param toggleButton button from formatting bar
+ * @param tag HTML tag name for text style
+ */
+ private void onFormatButtonClick(ToggleButton toggleButton, String tag) {
+ Spannable s = mContentEditText.getText();
+ if (s == null)
+ return;
+ int selectionStart = mContentEditText.getSelectionStart();
+ mStyleStart = selectionStart;
+ int selectionEnd = mContentEditText.getSelectionEnd();
+
+ if (selectionStart > selectionEnd) {
+ int temp = selectionEnd;
+ selectionEnd = selectionStart;
+ selectionStart = temp;
+ }
+
+ Class styleClass = null;
+ if (tag.equals(TAG_FORMAT_BAR_BUTTON_STRONG) || tag.equals(TAG_FORMAT_BAR_BUTTON_EM))
+ styleClass = StyleSpan.class;
+ else if (tag.equals(TAG_FORMAT_BAR_BUTTON_UNDERLINE))
+ styleClass = WPUnderlineSpan.class;
+ else if (tag.equals(TAG_FORMAT_BAR_BUTTON_STRIKE))
+ styleClass = StrikethroughSpan.class;
+ else if (tag.equals(TAG_FORMAT_BAR_BUTTON_QUOTE))
+ styleClass = QuoteSpan.class;
+
+ if (styleClass == null)
+ return;
+
+ Object[] allSpans = s.getSpans(selectionStart, selectionEnd, styleClass);
+ boolean textIsSelected = selectionEnd > selectionStart;
+ if (mIsLocalDraft) {
+ // Local drafts can use the rich text editor. Yay!
+ boolean shouldAddSpan = true;
+ for (Object span : allSpans) {
+ if (span instanceof StyleSpan) {
+ StyleSpan styleSpan = (StyleSpan)span;
+ if ((styleSpan.getStyle() == Typeface.BOLD && !tag.equals(TAG_FORMAT_BAR_BUTTON_STRONG))
+ || (styleSpan.getStyle() == Typeface.ITALIC && !tag.equals(TAG_FORMAT_BAR_BUTTON_EM))) {
+ continue;
+ }
+ }
+ if (!toggleButton.isChecked() && textIsSelected) {
+ // If span exists and text is selected, remove the span
+ s.removeSpan(span);
+ shouldAddSpan = false;
+ break;
+ } else if (!toggleButton.isChecked()) {
+ // Remove span at cursor point if button isn't checked
+ Object[] spans = s.getSpans(mStyleStart - 1, mStyleStart, styleClass);
+ for (Object removeSpan : spans) {
+ selectionStart = s.getSpanStart(removeSpan);
+ selectionEnd = s.getSpanEnd(removeSpan);
+ s.removeSpan(removeSpan);
+ }
+ }
+ }
+
+ if (shouldAddSpan) {
+ if (tag.equals(TAG_FORMAT_BAR_BUTTON_STRONG)) {
+ s.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), selectionStart, selectionEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } else if (tag.equals(TAG_FORMAT_BAR_BUTTON_EM)) {
+ s.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), selectionStart, selectionEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } else {
+ try {
+ s.setSpan(styleClass.newInstance(), selectionStart, selectionEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } catch (java.lang.InstantiationException e) {
+ AppLog.e(T.POSTS, e);
+ } catch (IllegalAccessException e) {
+ AppLog.e(T.POSTS, e);
+ }
+ }
+ }
+ } else {
+ // Add HTML tags when editing an existing post
+ String startTag = "<" + tag + ">";
+ String endTag = "</" + tag + ">";
+ Editable content = mContentEditText.getText();
+ if (textIsSelected) {
+ content.insert(selectionStart, startTag);
+ content.insert(selectionEnd + startTag.length(), endTag);
+ toggleButton.setChecked(false);
+ mContentEditText.setSelection(selectionEnd + startTag.length() + endTag.length());
+ } else if (toggleButton.isChecked()) {
+ content.insert(selectionStart, startTag);
+ mContentEditText.setSelection(selectionEnd + startTag.length());
+ } else if (!toggleButton.isChecked()) {
+ content.insert(selectionEnd, endTag);
+ mContentEditText.setSelection(selectionEnd + endTag.length());
+ }
+ }
+ }
+
+ /**
+ * Rich Text Editor
+ */
+ public void showImageSettings(final View alertView, final EditText titleText,
+ final EditText caption, final EditText imageWidthText,
+ final CheckBox featuredCheckBox, final CheckBox featuredInPostCheckBox,
+ final int maxWidth, final Spinner alignmentSpinner, final WPImageSpan imageSpan) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(getString(R.string.image_settings));
+ builder.setView(alertView);
+ builder.setPositiveButton(getString(android.R.string.ok), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ String title = (titleText.getText() != null) ? titleText.getText().toString() : "";
+ MediaFile mediaFile = imageSpan.getMediaFile();
+ if (mediaFile == null) {
+ return;
+ }
+ mediaFile.setTitle(title);
+ mediaFile.setHorizontalAlignment(alignmentSpinner.getSelectedItemPosition());
+ mediaFile.setWidth(getEditTextIntegerClamped(imageWidthText, 10, maxWidth));
+ String captionText = (caption.getText() != null) ? caption.getText().toString() : "";
+ mediaFile.setCaption(captionText);
+ mediaFile.setFeatured(featuredCheckBox.isChecked());
+ if (featuredCheckBox.isChecked()) {
+ // remove featured flag from all other images
+ Spannable contentSpannable = mContentEditText.getText();
+ WPImageSpan[] imageSpans =
+ contentSpannable.getSpans(0, contentSpannable.length(), WPImageSpan.class);
+ if (imageSpans.length > 1) {
+ for (WPImageSpan postImageSpan : imageSpans) {
+ if (postImageSpan != imageSpan) {
+ MediaFile postMediaFile = postImageSpan.getMediaFile();
+ postMediaFile.setFeatured(false);
+ postMediaFile.setFeaturedInPost(false);
+ // TODO: remove this
+ mEditorFragmentListener.saveMediaFile(postMediaFile);
+ }
+ }
+ }
+ }
+ mediaFile.setFeaturedInPost(featuredInPostCheckBox.isChecked());
+ // TODO: remove this
+ mEditorFragmentListener.saveMediaFile(mediaFile);
+ }
+ });
+ builder.setNegativeButton(getString(android.R.string.cancel), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ dialog.dismiss();
+ }
+ });
+ AlertDialog alertDialog = builder.create();
+ alertDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
+ alertDialog.show();
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ float pos = event.getY();
+
+ if (event.getAction() == 0)
+ mLastYPos = pos;
+
+ if (event.getAction() > 1) {
+ int scrollThreshold = DisplayUtils.dpToPx(getActivity(), 2);
+ if (((mLastYPos - pos) > scrollThreshold) || ((pos - mLastYPos) > scrollThreshold))
+ mScrollDetected = true;
+ }
+
+ mLastYPos = pos;
+
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null && actionBar.isShowing()) {
+ setContentEditingModeVisible(true);
+ return false;
+ }
+ }
+
+ if (event.getAction() == MotionEvent.ACTION_UP && !mScrollDetected) {
+ Layout layout = ((TextView) v).getLayout();
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ x += v.getScrollX();
+ y += v.getScrollY();
+ if (layout != null) {
+ int line = layout.getLineForVertical(y);
+ int charPosition = layout.getOffsetForHorizontal(line, x);
+
+ Spannable spannable = mContentEditText.getText();
+ if (spannable == null) {
+ return false;
+ }
+ // check if image span was tapped
+ WPImageSpan[] imageSpans = spannable.getSpans(charPosition, charPosition, WPImageSpan.class);
+
+ if (imageSpans.length != 0) {
+ final WPImageSpan imageSpan = imageSpans[0];
+ MediaFile mediaFile = imageSpan.getMediaFile();
+ if (mediaFile == null)
+ return false;
+ if (!mediaFile.isVideo()) {
+ LayoutInflater factory = LayoutInflater.from(getActivity());
+ final View alertView = factory.inflate(R.layout.alert_image_options, null);
+ if (alertView == null)
+ return false;
+ final EditText imageWidthText = (EditText) alertView.findViewById(R.id.imageWidthText);
+ final EditText titleText = (EditText) alertView.findViewById(R.id.title);
+ final EditText caption = (EditText) alertView.findViewById(R.id.caption);
+ final CheckBox featuredCheckBox = (CheckBox) alertView.findViewById(R.id.featuredImage);
+ final CheckBox featuredInPostCheckBox = (CheckBox) alertView.findViewById(R.id.featuredInPost);
+
+ // show featured image checkboxes if supported
+ if (mFeaturedImageSupported) {
+ featuredCheckBox.setVisibility(View.VISIBLE);
+ featuredInPostCheckBox.setVisibility(View.VISIBLE);
+ }
+
+ featuredCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (isChecked) {
+ featuredInPostCheckBox.setVisibility(View.VISIBLE);
+ } else {
+ featuredInPostCheckBox.setVisibility(View.GONE);
+ }
+
+ }
+ });
+
+ final SeekBar seekBar = (SeekBar) alertView.findViewById(R.id.imageWidth);
+ final Spinner alignmentSpinner = (Spinner) alertView.findViewById(R.id.alignment_spinner);
+ ArrayAdapter<CharSequence> adapter =
+ ArrayAdapter.createFromResource(getActivity(), R.array.alignment_array,
+ android.R.layout.simple_spinner_item);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ alignmentSpinner.setAdapter(adapter);
+
+ seekBar.setProgress(mediaFile.getWidth());
+ titleText.setText(mediaFile.getTitle());
+ caption.setText(mediaFile.getCaption());
+ featuredCheckBox.setChecked(mediaFile.isFeatured());
+
+ if (mediaFile.isFeatured()) {
+ featuredInPostCheckBox.setVisibility(View.VISIBLE);
+ } else {
+ featuredInPostCheckBox.setVisibility(View.GONE);
+ }
+
+ featuredInPostCheckBox.setChecked(mediaFile.isFeaturedInPost());
+
+ alignmentSpinner.setSelection(mediaFile.getHorizontalAlignment(), true);
+
+ final int maxWidth = MediaUtils.getMaximumImageWidth(getActivity(),
+ imageSpan.getImageSource(), mBlogSettingMaxImageWidth);
+ seekBar.setMax(maxWidth / 10);
+ imageWidthText.setText(String.format(Locale.US, "%dpx", maxWidth));
+ if (mediaFile.getWidth() != 0) {
+ seekBar.setProgress(mediaFile.getWidth() / 10);
+ }
+ seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (progress == 0) {
+ progress = 1;
+ }
+ imageWidthText.setText(String.format(Locale.US, "%dpx", progress * 10));
+ }
+ });
+
+ imageWidthText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (hasFocus) {
+ imageWidthText.setText("");
+ }
+ }
+ });
+
+ imageWidthText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ int width = getEditTextIntegerClamped(imageWidthText, 10, maxWidth);
+ seekBar.setProgress(width / 10);
+ imageWidthText.setSelection((String.valueOf(width).length()));
+
+ InputMethodManager imm = (InputMethodManager) getActivity()
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(imageWidthText.getWindowToken(),
+ InputMethodManager.RESULT_UNCHANGED_SHOWN);
+
+ return true;
+ }
+ });
+
+ showImageSettings(alertView, titleText, caption, imageWidthText, featuredCheckBox,
+ featuredInPostCheckBox, maxWidth, alignmentSpinner, imageSpan);
+ mScrollDetected = false;
+ return true;
+ }
+
+ } else {
+ mContentEditText.setMovementMethod(ArrowKeyMovementMethod.getInstance());
+ int selectionStart = mContentEditText.getSelectionStart();
+ if (selectionStart >= 0 && mContentEditText.getSelectionEnd() >= selectionStart)
+ mContentEditText.setSelection(selectionStart, mContentEditText.getSelectionEnd());
+ }
+
+ // get media gallery spans
+ MediaGalleryImageSpan[] gallerySpans = spannable.getSpans(charPosition, charPosition, MediaGalleryImageSpan.class);
+ if (gallerySpans.length > 0) {
+ final MediaGalleryImageSpan gallerySpan = gallerySpans[0];
+ Intent intent = new Intent(ACTION_MEDIA_GALLERY_TOUCHED);
+ intent.putExtra(EXTRA_MEDIA_GALLERY, gallerySpan.getMediaGallery());
+ getActivity().sendBroadcast(intent);
+ }
+ }
+ } else if (event.getAction() == 1) {
+ mScrollDetected = false;
+ }
+ return false;
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ int position = Selection.getSelectionStart(mContentEditText.getText());
+ if ((mIsBackspace && position != 1) || mLastPosition == position || !mIsLocalDraft)
+ return;
+
+ if (position < 0) {
+ position = 0;
+ }
+ mLastPosition = position;
+ if (position > 0) {
+ if (mStyleStart > position) {
+ mStyleStart = position - 1;
+ }
+
+ boolean shouldBold = mBoldToggleButton.isChecked();
+ boolean shouldEm = mEmToggleButton.isChecked();
+ boolean shouldUnderline = mUnderlineToggleButton.isChecked();
+ boolean shouldStrike = mStrikeToggleButton.isChecked();
+ boolean shouldQuote = mBquoteToggleButton.isChecked();
+
+ Object[] allSpans = s.getSpans(mStyleStart, position, Object.class);
+ for (Object span : allSpans) {
+ if (span instanceof StyleSpan) {
+ StyleSpan styleSpan = (StyleSpan) span;
+ if (styleSpan.getStyle() == Typeface.BOLD)
+ shouldBold = false;
+ else if (styleSpan.getStyle() == Typeface.ITALIC)
+ shouldEm = false;
+ } else if (span instanceof WPUnderlineSpan) {
+ shouldUnderline = false;
+ } else if (span instanceof StrikethroughSpan) {
+ shouldStrike = false;
+ } else if (span instanceof QuoteSpan) {
+ shouldQuote = false;
+ }
+ }
+
+ if (shouldBold)
+ s.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), mStyleStart, position, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+ if (shouldEm)
+ s.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), mStyleStart, position, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+ if (shouldUnderline)
+ s.setSpan(new WPUnderlineSpan(), mStyleStart, position, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+ if (shouldStrike)
+ s.setSpan(new StrikethroughSpan(), mStyleStart, position, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+ if (shouldQuote)
+ s.setSpan(new QuoteSpan(), mStyleStart, position, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ mIsBackspace = (count - after == 1) || (s.length() == 0);
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ @Override
+ public void onSelectionChanged() {
+ if (!mIsLocalDraft) {
+ return;
+ }
+
+ final Spannable s = mContentEditText.getText();
+ if (s == null)
+ return;
+ // set toggle buttons if cursor is inside of a matching span
+ mStyleStart = mContentEditText.getSelectionStart();
+ Object[] spans = s.getSpans(mContentEditText.getSelectionStart(), mContentEditText.getSelectionStart(), Object.class);
+
+ mBoldToggleButton.setChecked(false);
+ mEmToggleButton.setChecked(false);
+ mBquoteToggleButton.setChecked(false);
+ mUnderlineToggleButton.setChecked(false);
+ mStrikeToggleButton.setChecked(false);
+ for (Object span : spans) {
+ if (span instanceof StyleSpan) {
+ StyleSpan ss = (StyleSpan) span;
+ if (ss.getStyle() == android.graphics.Typeface.BOLD) {
+ mBoldToggleButton.setChecked(true);
+ }
+ if (ss.getStyle() == android.graphics.Typeface.ITALIC) {
+ mEmToggleButton.setChecked(true);
+ }
+ }
+ if (span instanceof QuoteSpan) {
+ mBquoteToggleButton.setChecked(true);
+ }
+ if (span instanceof WPUnderlineSpan) {
+ mUnderlineToggleButton.setChecked(true);
+ }
+ if (span instanceof StrikethroughSpan) {
+ mStrikeToggleButton.setChecked(true);
+ }
+ }
+ }
+
+ private int getEditTextIntegerClamped(EditText editText, int min, int max) {
+ int width = 10;
+ try {
+ if (editText.getText() != null)
+ width = Integer.parseInt(editText.getText().toString().replace("px", ""));
+ } catch (NumberFormatException e) {
+ AppLog.e(T.POSTS, e);
+ }
+ width = Math.min(max, Math.max(width, min));
+ return width;
+ }
+
+ private void loadWPImageSpanThumbnail(MediaFile mediaFile, String imageURL, ImageLoader imageLoader) {
+ if (mediaFile == null || imageURL == null) {
+ return;
+ }
+ final String mediaId = mediaFile.getMediaId();
+ if (mediaId == null) {
+ return;
+ }
+
+ final int maxThumbWidth = ImageUtils.getMaximumThumbnailWidthForEditor(getActivity());
+
+ imageLoader.get(imageURL, new ImageLoader.ImageListener() {
+ @Override
+ public void onErrorResponse(VolleyError arg0) {
+ }
+
+ @Override
+ public void onResponse(ImageLoader.ImageContainer container, boolean arg1) {
+ Bitmap downloadedBitmap = container.getBitmap();
+ if (downloadedBitmap == null) {
+ // no bitmap downloaded from the server.
+ return;
+ }
+
+ if (downloadedBitmap.getWidth() < MIN_THUMBNAIL_WIDTH) {
+ // Picture is too small. Show the placeholder in this case.
+ return;
+ }
+
+ Bitmap resizedBitmap;
+ // resize the downloaded bitmap
+ resizedBitmap = ImageUtils.getScaledBitmapAtLongestSide(downloadedBitmap, maxThumbWidth);
+
+ if (resizedBitmap == null) {
+ return;
+ }
+
+ final EditText editText = mContentEditText;
+ Editable s = editText.getText();
+ if (s == null) {
+ return;
+ }
+ WPImageSpan[] spans = s.getSpans(0, s.length(), WPImageSpan.class);
+ if (spans.length != 0 && getActivity() != null) {
+ for (WPImageSpan is : spans) {
+ MediaFile mediaFile = is.getMediaFile();
+ if (mediaFile == null) {
+ continue;
+ }
+ if (mediaId.equals(mediaFile.getMediaId()) && !is.isNetworkImageLoaded()) {
+ // replace the existing span with a new one with the correct image, re-add
+ // it to the same position.
+ int spanStart = is.getStartPosition();
+ int spanEnd = is.getEndPosition();
+ WPEditImageSpan imageSpan = new WPEditImageSpan(getActivity(), resizedBitmap,
+ is.getImageSource());
+ imageSpan.setMediaFile(is.getMediaFile());
+ imageSpan.setNetworkImageLoaded(true);
+ imageSpan.setPosition(spanStart, spanEnd);
+ s.removeSpan(is);
+ s.setSpan(imageSpan, spanStart, spanEnd + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ }
+ }
+ }
+ }
+ }, 0, 0);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ WPImageSpan[] spans = mContentEditText.getText().getSpans(0, mContentEditText.getText().length(), WPEditImageSpan.class);
+
+ if (spans != null && spans.length > 0) {
+ outState.putParcelableArray(KEY_IMAGE_SPANS, spans);
+ }
+
+ outState.putInt(KEY_START, mContentEditText.getSelectionStart());
+ outState.putInt(KEY_END, mContentEditText.getSelectionEnd());
+ outState.putString(KEY_CONTENT, mContentEditText.getText().toString());
+ }
+
+ private class AddMediaFileTask extends AsyncTask<Void, Void, WPEditImageSpan> {
+ private MediaFile mMediaFile;
+ private String mImageUrl;
+ private ImageLoader mImageLoader;
+ private int mStart;
+ private int mEnd;
+
+ public AddMediaFileTask(MediaFile mediaFile, String imageUrl, ImageLoader imageLoader, int start, int end) {
+ mMediaFile = mediaFile;
+ mImageUrl = imageUrl;
+ mImageLoader = imageLoader;
+ mStart = start;
+ mEnd = end;
+ }
+
+ protected WPEditImageSpan doInBackground(Void... voids) {
+ mMediaFile.setFileURL(mImageUrl);
+ mMediaFile.setFilePath(mImageUrl);
+ WPEditImageSpan imageSpan = createWPEditImageSpan(getActivity(), mMediaFile);
+ mEditorFragmentListener.saveMediaFile(mMediaFile);
+ return imageSpan;
+ }
+
+ protected void onPostExecute(WPEditImageSpan imageSpan) {
+ if (imageSpan == null) {
+ if (isAdded()) {
+ ToastUtils.showToast(getActivity(), R.string.alert_error_adding_media, Duration.LONG);
+ }
+ return ;
+ }
+ // Insert the WPImageSpan in the content field
+ int selectionStart = mStart;
+ int selectionEnd = mEnd;
+
+ if (selectionStart > selectionEnd) {
+ int temp = selectionEnd;
+ selectionEnd = selectionStart;
+ selectionStart = temp;
+ }
+
+ imageSpan.setPosition(selectionStart, selectionEnd);
+
+ int line, column = 0;
+ if (mContentEditText.getLayout() != null) {
+ line = mContentEditText.getLayout().getLineForOffset(selectionStart);
+ column = selectionStart - mContentEditText.getLayout().getLineStart(line);
+ }
+
+ Editable s = mContentEditText.getText();
+ if (s == null) {
+ return;
+ }
+
+ WPImageSpan[] imageSpans = s.getSpans(selectionStart, selectionEnd, WPImageSpan.class);
+ if (imageSpans.length != 0) {
+ // insert a few line breaks if the cursor is already on an image
+ s.insert(selectionEnd, "\n\n");
+ selectionStart = selectionStart + 2;
+ selectionEnd = selectionEnd + 2;
+ } else if (column != 0) {
+ // insert one line break if the cursor is not at the first column
+ s.insert(selectionEnd, "\n");
+ selectionStart = selectionStart + 1;
+ selectionEnd = selectionEnd + 1;
+ }
+
+ s.insert(selectionStart, " ");
+ s.setSpan(imageSpan, selectionStart, selectionEnd + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ AlignmentSpan.Standard as = new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER);
+ s.setSpan(as, selectionStart, selectionEnd + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ s.insert(selectionEnd + 1, "\n\n");
+
+ // Fetch and replace the WPImageSpan if it's a remote media
+ if (mImageLoader != null && URLUtil.isNetworkUrl(mImageUrl)) {
+ loadWPImageSpanThumbnail(mMediaFile, mImageUrl, mImageLoader);
+ }
+ }
+ }
+
+ public void addMediaFile(final MediaFile mediaFile, final String imageUrl, final ImageLoader imageLoader,
+ final int start, final int end) {
+ AddMediaFileTask addMediaFileTask = new AddMediaFileTask(mediaFile, imageUrl, imageLoader, start, end);
+ addMediaFileTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ @Override
+ public void appendMediaFile(final MediaFile mediaFile, final String imageUrl, final ImageLoader imageLoader) {
+ addMediaFile(mediaFile, imageUrl, imageLoader, mContentEditText.getSelectionStart(), mContentEditText.getSelectionEnd());
+ }
+
+ @Override
+ public void appendGallery(MediaGallery mediaGallery) {
+ Editable editableText = mContentEditText.getText();
+ if (editableText == null) {
+ return;
+ }
+
+ int selectionStart = mContentEditText.getSelectionStart();
+ int selectionEnd = mContentEditText.getSelectionEnd();
+
+ if (selectionStart > selectionEnd) {
+ int temp = selectionEnd;
+ selectionEnd = selectionStart;
+ selectionStart = temp;
+ }
+
+ int line, column = 0;
+ if (mContentEditText.getLayout() != null) {
+ line = mContentEditText.getLayout().getLineForOffset(selectionStart);
+ column = mContentEditText.getSelectionStart() - mContentEditText.getLayout().getLineStart(line);
+ }
+
+ if (column != 0) {
+ // insert one line break if the cursor is not at the first column
+ editableText.insert(selectionEnd, "\n");
+ selectionStart = selectionStart + 1;
+ selectionEnd = selectionEnd + 1;
+ }
+
+ editableText.insert(selectionStart, " ");
+ MediaGalleryImageSpan is = new MediaGalleryImageSpan(getActivity(), mediaGallery,
+ R.drawable.legacy_icon_mediagallery_placeholder);
+ editableText.setSpan(is, selectionStart, selectionEnd + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ AlignmentSpan.Standard as = new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER);
+ editableText.setSpan(as, selectionStart, selectionEnd + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ editableText.insert(selectionEnd + 1, "\n\n");
+ }
+
+ @Override
+ public void setUrlForVideoPressId(String videoPressId, String url, String posterUrl) {
+
+ }
+
+ @Override
+ public boolean isUploadingMedia() {
+ return false;
+ }
+
+ @Override
+ public boolean hasFailedMediaUploads() {
+ return false;
+ }
+
+ @Override
+ public void removeAllFailedMediaUploads() {}
+
+ @Override
+ public void setTitlePlaceholder(CharSequence text) {
+ }
+
+ @Override
+ public void setContentPlaceholder(CharSequence text) {
+ }
+
+ @Override
+ public boolean isActionInProgress() {
+ return false;
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/LinkDialogFragment.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/LinkDialogFragment.java
new file mode 100644
index 000000000..9be36cb33
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/LinkDialogFragment.java
@@ -0,0 +1,76 @@
+package org.wordpress.android.editor;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.EditText;
+
+public class LinkDialogFragment extends DialogFragment {
+
+ public static final int LINK_DIALOG_REQUEST_CODE_ADD = 1;
+ public static final int LINK_DIALOG_REQUEST_CODE_UPDATE = 2;
+ public static final int LINK_DIALOG_REQUEST_CODE_DELETE = 3;
+
+ public static final String LINK_DIALOG_ARG_URL = "linkURL";
+ public static final String LINK_DIALOG_ARG_TEXT = "linkText";
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ LayoutInflater inflater = getActivity().getLayoutInflater();
+
+ View view = inflater.inflate(R.layout.dialog_link, null);
+
+ final EditText urlEditText = (EditText) view.findViewById(R.id.linkURL);
+ final EditText linkEditText = (EditText) view.findViewById(R.id.linkText);
+
+ builder.setView(view)
+ .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ Intent intent = new Intent();
+ intent.putExtra(LINK_DIALOG_ARG_URL, urlEditText.getText().toString());
+ intent.putExtra(LINK_DIALOG_ARG_TEXT, linkEditText.getText().toString());
+ getTargetFragment().onActivityResult(getTargetRequestCode(), getTargetRequestCode(), intent);
+ }
+ })
+ .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ LinkDialogFragment.this.getDialog().cancel();
+ }
+ });
+
+ // If updating an existing link, add a 'Delete' button
+ if (getTargetRequestCode() == LINK_DIALOG_REQUEST_CODE_UPDATE) {
+ builder.setNeutralButton(R.string.delete, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ getTargetFragment().onActivityResult(getTargetRequestCode(), LINK_DIALOG_REQUEST_CODE_DELETE, null);
+ }
+ });
+ }
+
+ // Prepare initial state of EditTexts
+ Bundle bundle = getArguments();
+ if (bundle != null) {
+ linkEditText.setText(bundle.getString(LINK_DIALOG_ARG_TEXT));
+
+ String url = bundle.getString(LINK_DIALOG_ARG_URL);
+ if (url != null) {
+ urlEditText.setText(url);
+ }
+ urlEditText.selectAll();
+ }
+
+ AlertDialog dialog = builder.create();
+ dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
+
+ return dialog;
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/OnImeBackListener.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/OnImeBackListener.java
new file mode 100644
index 000000000..ed7ee0995
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/OnImeBackListener.java
@@ -0,0 +1,5 @@
+package org.wordpress.android.editor;
+
+public interface OnImeBackListener {
+ void onImeBack();
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/OnJsEditorStateChangedListener.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/OnJsEditorStateChangedListener.java
new file mode 100755
index 000000000..ca8cbf514
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/OnJsEditorStateChangedListener.java
@@ -0,0 +1,20 @@
+package org.wordpress.android.editor;
+
+import org.json.JSONObject;
+
+import java.util.Map;
+
+import static org.wordpress.android.editor.EditorFragmentAbstract.MediaType;
+
+public interface OnJsEditorStateChangedListener {
+ void onDomLoaded();
+ void onSelectionChanged(Map<String, String> selectionArgs);
+ void onSelectionStyleChanged(Map<String, Boolean> 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<String, String> responseArgs);
+ void onActionFinished();
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/RippleToggleButton.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/RippleToggleButton.java
new file mode 100644
index 000000000..fd2ac6110
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/RippleToggleButton.java
@@ -0,0 +1,95 @@
+package org.wordpress.android.editor;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.ToggleButton;
+
+public class RippleToggleButton extends ToggleButton {
+ private static final int FRAME_RATE = 10;
+ private static final int DURATION = 250;
+ private static final int FILL_INITIAL_OPACITY = 200;
+ private static final int STROKE_INITIAL_OPACITY = 255;
+
+ private float mHalfWidth;
+ private boolean mAnimationIsRunning = false;
+ private int mTimer = 0;
+ private Paint mFillPaint;
+ private Paint mStrokePaint;
+
+ public RippleToggleButton(Context context) {
+ this(context, null);
+ }
+
+ public RippleToggleButton(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public RippleToggleButton(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ private void init() {
+ if (isInEditMode()) {
+ return;
+ }
+
+ int rippleColor = getResources().getColor(R.color.format_bar_ripple_animation);
+
+ mFillPaint = new Paint();
+ mFillPaint.setAntiAlias(true);
+ mFillPaint.setColor(rippleColor);
+ mFillPaint.setStyle(Paint.Style.FILL);
+ mFillPaint.setAlpha(FILL_INITIAL_OPACITY);
+
+ mStrokePaint = new Paint();
+ mStrokePaint.setAntiAlias(true);
+ mStrokePaint.setColor(rippleColor);
+ mStrokePaint.setStyle(Paint.Style.STROKE);
+ mStrokePaint.setStrokeWidth(2);
+ mStrokePaint.setAlpha(STROKE_INITIAL_OPACITY);
+
+ setWillNotDraw(false);
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ super.draw(canvas);
+ if (mAnimationIsRunning) {
+ if (DURATION <= mTimer * FRAME_RATE) {
+ mAnimationIsRunning = false;
+ mTimer = 0;
+ } else {
+ float progressFraction = ((float) mTimer * FRAME_RATE) / DURATION;
+
+ mFillPaint.setAlpha((int) (FILL_INITIAL_OPACITY * (1 - progressFraction)));
+ mStrokePaint.setAlpha((int) (STROKE_INITIAL_OPACITY * (1 - progressFraction)));
+
+ canvas.drawCircle(mHalfWidth, mHalfWidth, mHalfWidth * progressFraction, mFillPaint);
+ canvas.drawCircle(mHalfWidth, mHalfWidth, mHalfWidth * progressFraction, mStrokePaint);
+
+ mTimer++;
+ }
+
+ invalidate();
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(@NonNull MotionEvent event) {
+ startRippleAnimation();
+ return super.onTouchEvent(event);
+ }
+
+ private void startRippleAnimation() {
+ if (this.isEnabled() && !mAnimationIsRunning) {
+ mHalfWidth = getMeasuredWidth() / 2;
+ mAnimationIsRunning = true;
+ invalidate();
+ }
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/SourceViewEditText.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/SourceViewEditText.java
new file mode 100644
index 000000000..12caa31d0
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/SourceViewEditText.java
@@ -0,0 +1,60 @@
+package org.wordpress.android.editor;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Typeface;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.widget.EditText;
+
+import org.wordpress.android.util.AppLog;
+
+/**
+ * An EditText with support for {@link org.wordpress.android.editor.OnImeBackListener} and typeface setting
+ * using a custom XML attribute.
+ */
+public class SourceViewEditText extends EditText {
+
+ private OnImeBackListener mOnImeBackListener;
+
+ public SourceViewEditText(Context context) {
+ super(context);
+ }
+
+ public SourceViewEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setCustomTypeface(attrs);
+ }
+
+ public SourceViewEditText(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ setCustomTypeface(attrs);
+ }
+
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ if(event.getKeyCode() == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
+ if (this.mOnImeBackListener != null) {
+ this.mOnImeBackListener.onImeBack();
+ }
+ }
+ return super.onKeyPreIme(keyCode, event);
+ }
+
+ public void setOnImeBackListener(OnImeBackListener listener) {
+ this.mOnImeBackListener = listener;
+ }
+
+ private void setCustomTypeface(AttributeSet attrs) {
+ TypedArray values = getContext().obtainStyledAttributes(attrs, R.styleable.SourceViewEditText);
+ String typefaceName = values.getString(R.styleable.SourceViewEditText_fontFile);
+ if (typefaceName != null) {
+ try {
+ Typeface typeface = Typeface.createFromAsset(getContext().getAssets(), "fonts/" + typefaceName);
+ this.setTypeface(typeface);
+ } catch (RuntimeException e) {
+ AppLog.e(AppLog.T.EDITOR, "Could not load typeface " + typefaceName);
+ }
+ }
+ values.recycle();
+ }
+} \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/Utils.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/Utils.java
new file mode 100644
index 000000000..476656320
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/Utils.java
@@ -0,0 +1,247 @@
+package org.wordpress.android.editor;
+
+import android.app.Activity;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.net.Uri;
+import android.util.Patterns;
+
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.HTTPUtils;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.URLDecoder;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+public class Utils {
+ public static String getHtmlFromFile(Activity activity, String filename) {
+ try {
+ AssetManager assetManager = activity.getAssets();
+ InputStream in = assetManager.open(filename);
+ return getStringFromInputStream(in);
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.EDITOR, "Unable to load editor HTML (is the assets symlink working?): " + e.getMessage());
+ return null;
+ }
+ }
+
+ public static String getStringFromInputStream(InputStream inputStream) throws IOException {
+ InputStreamReader is = new InputStreamReader(inputStream);
+ StringBuilder sb = new StringBuilder();
+ BufferedReader br = new BufferedReader(is);
+ String read = br.readLine();
+ while (read != null) {
+ sb.append(read);
+ sb.append('\n');
+ read = br.readLine();
+ }
+ return sb.toString();
+ }
+
+ public static String escapeHtml(String html) {
+ if (html != null) {
+ html = html.replace("\\", "\\\\");
+ html = html.replace("\"", "\\\"");
+ html = html.replace("'", "\\'");
+ html = html.replace("\r", "\\r");
+ html = html.replace("\n", "\\n");
+
+ // Escape invisible line separator (U+2028) and paragraph separator (U+2029) characters
+ // https://github.com/wordpress-mobile/WordPress-Editor-Android/issues/405
+ html = html.replace("\u2028", "\\u2028");
+ html = html.replace("\u2029", "\\u2029");
+ }
+ return html;
+ }
+
+ public static String decodeHtml(String html) {
+ if (html != null) {
+ try {
+ html = URLDecoder.decode(html, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ AppLog.e(AppLog.T.EDITOR, "Unsupported encoding exception while decoding HTML.");
+ }
+ }
+ return html;
+ }
+
+ public static String escapeQuotes(String text) {
+ if (text != null) {
+ text = text.replace("'", "\\'").replace("\"", "\\\"");
+ }
+ return text;
+ }
+
+ /**
+ * Splits a delimited string into a set of strings.
+ * @param string the delimited string to split
+ * @param delimiter the string delimiter
+ */
+ public static Set<String> splitDelimitedString(String string, String delimiter) {
+ Set<String> splitString = new HashSet<>();
+
+ StringTokenizer stringTokenizer = new StringTokenizer(string, delimiter);
+ while (stringTokenizer.hasMoreTokens()) {
+ splitString.add(stringTokenizer.nextToken());
+ }
+
+ return splitString;
+ }
+
+ /**
+ * Splits a delimited string of value pairs (of the form identifier=value) into a set of strings.
+ * @param string the delimited string to split
+ * @param delimiter the string delimiter
+ * @param identifiers the identifiers to match for in the string
+ */
+ public static Set<String> splitValuePairDelimitedString(String string, String delimiter, List<String> identifiers) {
+ String identifierSegment = "";
+ for (String identifier : identifiers) {
+ if (identifierSegment.length() != 0) {
+ identifierSegment += "|";
+ }
+ identifierSegment += identifier;
+ }
+
+ String regex = delimiter + "(?=(" + identifierSegment + ")=)";
+
+ return new HashSet<>(Arrays.asList(string.split(regex)));
+ }
+
+ /**
+ * Accepts a set of strings, each string being a key-value pair (<code>id=5</code>,
+ * <code>name=content-filed</code>). Returns a map of all the key-value pairs in the set.
+ * @param keyValueSet the set of key-value pair strings
+ */
+ public static Map<String, String> buildMapFromKeyValuePairs(Set<String> keyValueSet) {
+ Map<String, String> selectionArgs = new HashMap<>();
+ for (String pair : keyValueSet) {
+ int delimLoc = pair.indexOf("=");
+ if (delimLoc != -1) {
+ selectionArgs.put(pair.substring(0, delimLoc), pair.substring(delimLoc + 1));
+ }
+ }
+ return selectionArgs;
+ }
+
+ /**
+ * Compares two <code>Sets</code> and returns a <code>Map</code> of elements not contained in both
+ * <code>Sets</code>. Elements contained in <code>oldSet</code> but not in <code>newSet</code> will be marked
+ * <code>false</code> in the returned map; the converse will be marked <code>true</code>.
+ * @param oldSet the older of the two <code>Sets</code>
+ * @param newSet the newer of the two <code>Sets</code>
+ * @param <E> type of element stored in the <code>Sets</code>
+ * @return a <code>Map</code> containing the difference between <code>oldSet</code> and <code>newSet</code>, and whether the
+ * element was added (<code>true</code>) or removed (<code>false</code>) in <code>newSet</code>
+ */
+ public static <E> Map<E, Boolean> getChangeMapFromSets(Set<E> oldSet, Set<E> newSet) {
+ Map<E, Boolean> changeMap = new HashMap<>();
+
+ Set<E> additions = new HashSet<>(newSet);
+ additions.removeAll(oldSet);
+
+ Set<E> removals = new HashSet<>(oldSet);
+ removals.removeAll(newSet);
+
+ for (E s : additions) {
+ changeMap.put(s, true);
+ }
+
+ for (E s : removals) {
+ changeMap.put(s, false);
+ }
+
+ return changeMap;
+ }
+
+ public static Uri downloadExternalMedia(Context context, Uri imageUri, Map<String, String> headers) {
+ if(context != null && imageUri != null) {
+ File cacheDir = null;
+
+ if (context.getApplicationContext() != null) {
+ cacheDir = context.getCacheDir();
+ }
+
+ try {
+ InputStream inputStream;
+ if (imageUri.toString().startsWith("content://")) {
+ inputStream = context.getContentResolver().openInputStream(imageUri);
+ if (inputStream == null) {
+ AppLog.e(AppLog.T.UTILS, "openInputStream returned null");
+ return null;
+ }
+ } else {
+ if (headers == null) {
+ headers = Collections.emptyMap();
+ }
+
+ HttpURLConnection conn = HTTPUtils.setupUrlConnection(imageUri.toString(), headers);
+
+ // If the HTTP response is a redirect, follow it
+ int responseCode = conn.getResponseCode();
+ if (responseCode != HttpURLConnection.HTTP_OK) {
+ if (responseCode == HttpURLConnection.HTTP_MOVED_PERM
+ || responseCode == HttpURLConnection.HTTP_MOVED_TEMP
+ || responseCode == HttpURLConnection.HTTP_SEE_OTHER) {
+ conn = HTTPUtils.setupUrlConnection(conn.getHeaderField("Location"), headers);
+ }
+ }
+
+ inputStream = conn.getInputStream();
+ }
+
+ String fileName = "thumb-" + System.currentTimeMillis();
+
+ File f = new File(cacheDir, fileName);
+ FileOutputStream output = new FileOutputStream(f);
+ byte[] data = new byte[1024];
+
+ int count;
+ while ((count = inputStream.read(data)) != -1) {
+ output.write(data, 0, count);
+ }
+
+ output.flush();
+ output.close();
+ inputStream.close();
+ return Uri.fromFile(f);
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ }
+
+ return null;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Checks the Clipboard for text that matches the {@link Patterns#WEB_URL} pattern.
+ *
+ * @return the URL text in the clipboard, if it exists; otherwise null
+ */
+ public static String getUrlFromClipboard(Context context) {
+ if (context == null) return null;
+ ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData data = clipboard != null ? clipboard.getPrimaryClip() : null;
+ if (data == null || data.getItemCount() <= 0) return null;
+ String clipText = String.valueOf(data.getItemAt(0).getText());
+ return Patterns.WEB_URL.matcher(clipText).matches() ? clipText : null;
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/legacy/EditLinkActivity.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/legacy/EditLinkActivity.java
new file mode 100644
index 000000000..e02f98eb2
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/legacy/EditLinkActivity.java
@@ -0,0 +1,76 @@
+package org.wordpress.android.editor.legacy;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+
+import org.wordpress.android.editor.R;
+
+public class EditLinkActivity extends AppCompatActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.alert_create_link);
+
+ Bundle extras = getIntent().getExtras();
+ if (extras != null) {
+ String selectedText = extras.getString("selectedText");
+ if (selectedText != null) {
+ EditText linkTextET = (EditText) findViewById(R.id.linkText);
+ linkTextET.setText(selectedText);
+ }
+ }
+
+ final Button cancelButton = (Button) findViewById(R.id.cancel);
+ final Button okButton = (Button) findViewById(R.id.ok);
+
+ final EditText urlEditText = (EditText) findViewById(R.id.linkURL);
+ urlEditText.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (urlEditText.getText().toString().equals("")) {
+ urlEditText.setText("http://");
+ urlEditText.setSelection(7);
+ }
+ }
+
+ });
+
+ okButton.setOnClickListener(new Button.OnClickListener() {
+ public void onClick(View v) {
+ EditText linkURLET = (EditText) findViewById(R.id.linkURL);
+ String linkURL = linkURLET.getText().toString();
+
+ EditText linkTextET = (EditText) findViewById(R.id.linkText);
+ String linkText = linkTextET.getText().toString();
+
+ Bundle bundle = new Bundle();
+ bundle.putString("linkURL", linkURL);
+ if (!linkText.equals("")) {
+ bundle.putString("linkText", linkText);
+ }
+
+ Intent mIntent = new Intent();
+ mIntent.putExtras(bundle);
+ setResult(RESULT_OK, mIntent);
+ finish();
+ }
+ });
+
+ cancelButton.setOnClickListener(new Button.OnClickListener() {
+ public void onClick(View v) {
+ Intent mIntent = new Intent();
+ setResult(RESULT_CANCELED, mIntent);
+ finish();
+ }
+ });
+
+ // select end of url
+ urlEditText.performClick();
+ }
+}
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/legacy/WPEditImageSpan.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/legacy/WPEditImageSpan.java
new file mode 100644
index 000000000..25bc33894
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/legacy/WPEditImageSpan.java
@@ -0,0 +1,74 @@
+package org.wordpress.android.editor.legacy;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.wordpress.android.editor.R;
+import org.wordpress.android.util.helpers.MediaFile;
+import org.wordpress.android.util.helpers.WPImageSpan;
+
+public class WPEditImageSpan extends WPImageSpan {
+ private Bitmap mEditIconBitmap;
+
+ protected WPEditImageSpan() {
+ super();
+ }
+
+ public WPEditImageSpan(Context context, Bitmap b, Uri src) {
+ super(context, b, src);
+ init(context);
+ }
+
+ public WPEditImageSpan(Context context, int resId, Uri src) {
+ super(context, resId, src);
+ init(context);
+ }
+
+ private void init(Context context) {
+ if (context != null) {
+ mEditIconBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.ab_icon_edit);
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom,
+ Paint paint) {
+ super.draw(canvas, text, start, end, x, top, y, bottom, paint);
+
+ if (mEditIconBitmap != null && !mMediaFile.isVideo()) {
+ // Add 'edit' icon at bottom right of image
+ int width = getSize(paint, text, start, end, paint.getFontMetricsInt());
+ float editIconXPosition = (x + width) - mEditIconBitmap.getWidth();
+ float editIconYPosition = bottom - mEditIconBitmap.getHeight();
+
+ // Add a black background with a bit of alpha
+ Paint bgPaint = new Paint();
+ bgPaint.setColor(Color.argb(200, 0, 0, 0));
+ canvas.drawRect(editIconXPosition, editIconYPosition, editIconXPosition + mEditIconBitmap.getWidth(),
+ editIconYPosition + mEditIconBitmap.getHeight(), bgPaint);
+
+ // Add the icon to the canvas
+ canvas.drawBitmap(mEditIconBitmap, editIconXPosition, editIconYPosition, paint);
+ }
+ }
+
+ public static final Parcelable.Creator<WPEditImageSpan> CREATOR = new Parcelable.Creator<WPEditImageSpan>() {
+ public WPEditImageSpan createFromParcel(Parcel in) {
+ WPEditImageSpan editSpan = new WPEditImageSpan();
+ editSpan.setupFromParcel(in);
+
+ return editSpan;
+ }
+
+ public WPEditImageSpan[] newArray(int size) {
+ return new WPEditImageSpan[size];
+ }
+ };
+}
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ab_icon_edit.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ab_icon_edit.png
new file mode 100644
index 000000000..4b8f443e1
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ab_icon_edit.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/format_bar_chevron.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/format_bar_chevron.png
new file mode 100644
index 000000000..d47a4f1ce
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/format_bar_chevron.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ic_close_white_24dp.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ic_close_white_24dp.png
new file mode 100644
index 000000000..0fd15563a
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ic_close_white_24dp.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ic_post_settings.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ic_post_settings.png
new file mode 100644
index 000000000..4e4959dcd
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/ic_post_settings.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_admin_links.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_admin_links.png
new file mode 100644
index 000000000..55fb8739a
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_admin_links.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_admin_links_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_admin_links_grey.png
new file mode 100644
index 000000000..5b66e714c
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_admin_links_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_bold.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_bold.png
new file mode 100644
index 000000000..0ba4b95e8
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_bold.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_bold_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_bold_grey.png
new file mode 100644
index 000000000..fc896135d
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_bold_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_insertmore.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_insertmore.png
new file mode 100644
index 000000000..444f735ab
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_insertmore.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_insertmore_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_insertmore_grey.png
new file mode 100644
index 000000000..32411efc7
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_insertmore_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_italic.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_italic.png
new file mode 100644
index 000000000..48db52143
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_italic.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_italic_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_italic_grey.png
new file mode 100644
index 000000000..eea6add02
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_italic_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_strikethrough.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_strikethrough.png
new file mode 100644
index 000000000..7cb2dcd30
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_strikethrough.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_strikethrough_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_strikethrough_grey.png
new file mode 100644
index 000000000..90354d05d
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_strikethrough_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_underline.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_underline.png
new file mode 100644
index 000000000..7e14c49a5
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_underline.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_underline_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_underline_grey.png
new file mode 100644
index 000000000..978f97519
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_editor_underline_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_image_big_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_image_big_grey.png
new file mode 100644
index 000000000..a61883996
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_image_big_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_quote.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_quote.png
new file mode 100644
index 000000000..7f7c5181e
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_quote.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_quote_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_quote_grey.png
new file mode 100644
index 000000000..4dc307fc7
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_dashicon_format_quote_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_icon_mediagallery_placeholder.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_icon_mediagallery_placeholder.png
new file mode 100644
index 000000000..9e2b6e412
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/legacy_icon_mediagallery_placeholder.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/list_focused_wordpress.9.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/list_focused_wordpress.9.png
new file mode 100644
index 000000000..130a206d3
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/list_focused_wordpress.9.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/media_icon_32dp.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/media_icon_32dp.png
new file mode 100644
index 000000000..046432898
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/media_icon_32dp.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/media_movieclip.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/media_movieclip.png
new file mode 100644
index 000000000..bb49bcdc2
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/media_movieclip.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/noticon_picture.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/noticon_picture.png
new file mode 100644
index 000000000..7900b9283
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/noticon_picture.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/noticon_picture_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/noticon_picture_grey.png
new file mode 100644
index 000000000..c4ca1c13c
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-hdpi/noticon_picture_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ab_icon_edit.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ab_icon_edit.png
new file mode 100644
index 000000000..46d12a96d
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ab_icon_edit.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/format_bar_chevron.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/format_bar_chevron.png
new file mode 100644
index 000000000..03b57a17c
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/format_bar_chevron.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ic_close_white_24dp.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ic_close_white_24dp.png
new file mode 100644
index 000000000..76e07f097
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ic_close_white_24dp.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ic_post_settings.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ic_post_settings.png
new file mode 100644
index 000000000..3e2020016
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/ic_post_settings.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_admin_links.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_admin_links.png
new file mode 100644
index 000000000..eef4505a5
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_admin_links.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_admin_links_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_admin_links_grey.png
new file mode 100644
index 000000000..f545cd651
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_admin_links_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_bold.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_bold.png
new file mode 100644
index 000000000..4519f6c04
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_bold.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_bold_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_bold_grey.png
new file mode 100644
index 000000000..b85e41382
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_bold_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_insertmore.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_insertmore.png
new file mode 100644
index 000000000..61ae586af
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_insertmore.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_insertmore_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_insertmore_grey.png
new file mode 100644
index 000000000..912987466
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_insertmore_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_italic.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_italic.png
new file mode 100644
index 000000000..dbcea3513
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_italic.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_italic_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_italic_grey.png
new file mode 100644
index 000000000..6a776b669
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_italic_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_strikethrough.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_strikethrough.png
new file mode 100644
index 000000000..006acefc1
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_strikethrough.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_strikethrough_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_strikethrough_grey.png
new file mode 100644
index 000000000..c18266381
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_strikethrough_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_underline.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_underline.png
new file mode 100644
index 000000000..c3c76dbc8
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_underline.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_underline_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_underline_grey.png
new file mode 100644
index 000000000..f728bd9cf
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_editor_underline_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_image_big_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_image_big_grey.png
new file mode 100644
index 000000000..95dbdd936
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_image_big_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_quote.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_quote.png
new file mode 100644
index 000000000..164881f53
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_quote.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_quote_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_quote_grey.png
new file mode 100644
index 000000000..797c88e8a
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_dashicon_format_quote_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_icon_mediagallery_placeholder.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_icon_mediagallery_placeholder.png
new file mode 100644
index 000000000..f3fe14ad3
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/legacy_icon_mediagallery_placeholder.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/list_focused_wordpress.9.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/list_focused_wordpress.9.png
new file mode 100644
index 000000000..3dbd3937d
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/list_focused_wordpress.9.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/media_icon_32dp.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/media_icon_32dp.png
new file mode 100644
index 000000000..8c39cc8aa
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/media_icon_32dp.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/media_movieclip.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/media_movieclip.png
new file mode 100644
index 000000000..d1dfae9ea
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/media_movieclip.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/noticon_picture.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/noticon_picture.png
new file mode 100644
index 000000000..95bffba31
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/noticon_picture.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/noticon_picture_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/noticon_picture_grey.png
new file mode 100644
index 000000000..d6aeb0e3d
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xhdpi/noticon_picture_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ab_icon_edit.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ab_icon_edit.png
new file mode 100644
index 000000000..8c5d06a79
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ab_icon_edit.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/format_bar_chevron.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/format_bar_chevron.png
new file mode 100644
index 000000000..110b8ba5a
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/format_bar_chevron.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png
new file mode 100644
index 000000000..0eb9d8b08
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ic_post_settings.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ic_post_settings.png
new file mode 100644
index 000000000..14c9186ef
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/ic_post_settings.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_admin_links.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_admin_links.png
new file mode 100644
index 000000000..724dea05e
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_admin_links.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_admin_links_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_admin_links_grey.png
new file mode 100644
index 000000000..a6746556a
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_admin_links_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_bold.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_bold.png
new file mode 100644
index 000000000..2fc4e42a1
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_bold.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_bold_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_bold_grey.png
new file mode 100644
index 000000000..0a48139a0
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_bold_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_insertmore.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_insertmore.png
new file mode 100644
index 000000000..05437d16b
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_insertmore.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_insertmore_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_insertmore_grey.png
new file mode 100644
index 000000000..91541b529
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_insertmore_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_italic.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_italic.png
new file mode 100644
index 000000000..f1d118b74
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_italic.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_italic_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_italic_grey.png
new file mode 100644
index 000000000..3a684e978
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_italic_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_strikethrough.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_strikethrough.png
new file mode 100644
index 000000000..35d6579b5
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_strikethrough.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_strikethrough_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_strikethrough_grey.png
new file mode 100644
index 000000000..2e9e30f5c
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_strikethrough_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_underline.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_underline.png
new file mode 100644
index 000000000..fb8558945
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_underline.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_underline_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_underline_grey.png
new file mode 100644
index 000000000..9f1b612d6
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_editor_underline_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_image_big_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_image_big_grey.png
new file mode 100644
index 000000000..3a608ed32
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_image_big_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_quote.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_quote.png
new file mode 100644
index 000000000..daa89ba17
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_quote.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_quote_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_quote_grey.png
new file mode 100644
index 000000000..240493fb3
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_dashicon_format_quote_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_icon_mediagallery_placeholder.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_icon_mediagallery_placeholder.png
new file mode 100644
index 000000000..40976b210
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/legacy_icon_mediagallery_placeholder.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/media_icon_32dp.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/media_icon_32dp.png
new file mode 100644
index 000000000..7ec4e9b25
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/media_icon_32dp.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/media_movieclip.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/media_movieclip.png
new file mode 100644
index 000000000..90e9b768a
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/media_movieclip.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/noticon_picture.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/noticon_picture.png
new file mode 100644
index 000000000..a132b6da3
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/noticon_picture.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/noticon_picture_grey.png b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/noticon_picture_grey.png
new file mode 100644
index 000000000..7b8ff9608
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable-xxhdpi/noticon_picture_grey.png
Binary files differ
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold.xml
new file mode 100755
index 000000000..82788b1fb
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#87A6BC"
+ android:pathData="M17,15.009h4.547c2.126,0,3.671,0.303,4.632,0.907c0.962,0.605,1.442,1.567,1.442,2.887
+c0,0.896-0.211,1.63-0.631,2.205c-0.421,0.574-0.98,0.919-1.678,1.036v0.103c0.951,0.212,1.637,0.608,2.057,1.189
+C27.79,23.916,28,24.688,28,25.652c0,1.367-0.494,2.434-1.482,3.199C25.529,29.617,24.186,30,22.491,30H17V15.009z
+M20,20.946
+h2.027c0.862,0,1.486-0.133,1.872-0.4c0.387-0.267,0.579-0.708,0.579-1.323c0-0.574-0.21-0.986-0.63-1.236
+c-0.421-0.249-1.087-0.374-1.996-0.374H20V20.946z
+M20,23.469v3.906h2.253c0.876,0,1.521-0.167,1.939-0.502
+s0.626-0.848,0.626-1.539c0-1.244-0.889-1.865-2.668-1.865H20z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_disabled.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_disabled.xml
new file mode 100755
index 000000000..1a6fc6313
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_disabled.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#E8EEF2"
+ android:pathData="M17,15.009h4.547c2.126,0,3.671,0.303,4.632,0.907c0.962,0.605,1.442,1.567,1.442,2.887
+c0,0.896-0.211,1.63-0.631,2.205c-0.421,0.574-0.98,0.919-1.678,1.036v0.103c0.951,0.212,1.637,0.608,2.057,1.189
+C27.79,23.916,28,24.688,28,25.652c0,1.367-0.494,2.434-1.482,3.199C25.529,29.617,24.186,30,22.491,30H17V15.009z
+M20,20.946
+h2.027c0.862,0,1.486-0.133,1.872-0.4c0.387-0.267,0.579-0.708,0.579-1.323c0-0.574-0.21-0.986-0.63-1.236
+c-0.421-0.249-1.087-0.374-1.996-0.374H20V20.946z
+M20,23.469v3.906h2.253c0.876,0,1.521-0.167,1.939-0.502
+s0.626-0.848,0.626-1.539c0-1.244-0.889-1.865-2.668-1.865H20z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_highlighted.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_highlighted.xml
new file mode 100755
index 000000000..cdb305cc5
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_highlighted.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#0084BC"
+ android:pathData="M17,15.009h4.547c2.126,0,3.671,0.303,4.632,0.907c0.962,0.605,1.442,1.567,1.442,2.887
+c0,0.896-0.211,1.63-0.631,2.205c-0.421,0.574-0.98,0.919-1.678,1.036v0.103c0.951,0.212,1.637,0.608,2.057,1.189
+C27.79,23.916,28,24.688,28,25.652c0,1.367-0.494,2.434-1.482,3.199C25.529,29.617,24.186,30,22.491,30H17V15.009z
+M20,20.946
+h2.027c0.862,0,1.486-0.133,1.872-0.4c0.387-0.267,0.579-0.708,0.579-1.323c0-0.574-0.21-0.986-0.63-1.236
+c-0.421-0.249-1.087-0.374-1.996-0.374H20V20.946z
+M20,23.469v3.906h2.253c0.876,0,1.521-0.167,1.939-0.502
+s0.626-0.848,0.626-1.539c0-1.244-0.889-1.865-2.668-1.865H20z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_selector.xml
new file mode 100755
index 000000000..e35075d61
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_bold_selector.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:drawable="@drawable/format_bar_button_bold_highlighted"/>
+ <item android:state_pressed="true" android:drawable="@drawable/format_bar_button_bold_highlighted"/>
+ <item android:state_enabled="false" android:drawable="@drawable/format_bar_button_bold_disabled"/>
+ <item android:drawable="@drawable/format_bar_button_bold"/>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html.xml
new file mode 100755
index 000000000..96b241307
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html.xml
@@ -0,0 +1,18 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44.0"
+ android:viewportHeight="44.0">
+ <path
+ android:pathData="M13.93,26H12.79v-4.16H9.17V26H8.04v-9h1.13v3.87h3.62V17h1.14V26z"
+ android:fillColor="#A6BCCC"/>
+ <path
+ android:pathData="M20.97,17.97H18.6V26h-1.13v-8.03h-2.36V17h5.86V17.97z"
+ android:fillColor="#A6BCCC"/>
+ <path
+ android:pathData="M23.75,17l2.35,7.34L28.45,17h1.46v9h-1.13v-3.51l0.1,-3.51L26.53,26h-0.87l-2.34,-6.99l0.1,3.49V26h-1.13v-9H23.75z"
+ android:fillColor="#A6BCCC"/>
+ <path
+ android:pathData="M33,25.03h3.53V26h-4.67v-9h1.14V25.03z"
+ android:fillColor="#A6BCCC"/>
+</vector>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_disabled.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_disabled.xml
new file mode 100755
index 000000000..6e5b8074e
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_disabled.xml
@@ -0,0 +1,18 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44.0"
+ android:viewportHeight="44.0">
+ <path
+ android:pathData="M13.93,26H12.79v-4.16H9.17V26H8.04v-9h1.13v3.87h3.62V17h1.14V26z"
+ android:fillColor="#E8EEF2"/>
+ <path
+ android:pathData="M20.97,17.97H18.6V26h-1.13v-8.03h-2.36V17h5.86V17.97z"
+ android:fillColor="#E8EEF2"/>
+ <path
+ android:pathData="M23.75,17l2.35,7.34L28.45,17h1.46v9h-1.13v-3.51l0.1,-3.51L26.53,26h-0.87l-2.34,-6.99l0.1,3.49V26h-1.13v-9H23.75z"
+ android:fillColor="#E8EEF2"/>
+ <path
+ android:pathData="M33,25.03h3.53V26h-4.67v-9h1.14V25.03z"
+ android:fillColor="#E8EEF2"/>
+</vector>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_highlighted.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_highlighted.xml
new file mode 100755
index 000000000..331d9a8f9
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_highlighted.xml
@@ -0,0 +1,19 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44.0"
+ android:viewportHeight="44.0">
+ <path android:fillColor="#E1EBF1" android:pathData="M0,0h44v44h-44z"/>
+ <path
+ android:pathData="M13.93,26H12.79v-4.16H9.17V26H8.04v-9h1.13v3.87h3.62V17h1.14V26z"
+ android:fillColor="#A6BCCC"/>
+ <path
+ android:pathData="M20.97,17.97H18.6V26h-1.13v-8.03h-2.36V17h5.86V17.97z"
+ android:fillColor="#A6BCCC"/>
+ <path
+ android:pathData="M23.75,17l2.35,7.34L28.45,17h1.46v9h-1.13v-3.51l0.1,-3.51L26.53,26h-0.87l-2.34,-6.99l0.1,3.49V26h-1.13v-9H23.75z"
+ android:fillColor="#A6BCCC"/>
+ <path
+ android:pathData="M33,25.03h3.53V26h-4.67v-9h1.14V25.03z"
+ android:fillColor="#A6BCCC"/>
+</vector>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_selector.xml
new file mode 100755
index 000000000..767d6dd6b
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_html_selector.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:drawable="@drawable/format_bar_button_html_highlighted"/>
+ <item android:state_pressed="true" android:drawable="@drawable/format_bar_button_html_highlighted"/>
+ <item android:state_enabled="false" android:drawable="@drawable/format_bar_button_html_disabled"/>
+ <item android:drawable="@drawable/format_bar_button_html"/>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic.xml
new file mode 100755
index 000000000..bf4a9ae27
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#87A6BC"
+ android:pathData="M 20.536 15 L 20.109 17 L 21.609 17 L 19.263 28 L 17.763 28 L 17.336 30 L 23.464 30 L 23.89 28 L 22.39 28 L 24.737 17 L 26.237 17 L 26.664 15 Z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_disabled.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_disabled.xml
new file mode 100755
index 000000000..acb50bd33
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_disabled.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#E8EEF2"
+ android:pathData="M 20.536 15 L 20.109 17 L 21.609 17 L 19.263 28 L 17.763 28 L 17.336 30 L 23.464 30 L 23.89 28 L 22.39 28 L 24.737 17 L 26.237 17 L 26.664 15 Z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_highlighted.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_highlighted.xml
new file mode 100755
index 000000000..7c8e4de79
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_highlighted.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#0084BC"
+ android:pathData="M 20.536 15 L 20.109 17 L 21.609 17 L 19.263 28 L 17.763 28 L 17.336 30 L 23.464 30 L 23.89 28 L 22.39 28 L 24.737 17 L 26.237 17 L 26.664 15 Z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_selector.xml
new file mode 100755
index 000000000..6136a6805
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_italic_selector.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:drawable="@drawable/format_bar_button_italic_highlighted"/>
+ <item android:state_pressed="true" android:drawable="@drawable/format_bar_button_italic_highlighted"/>
+ <item android:state_enabled="false" android:drawable="@drawable/format_bar_button_italic_disabled"/>
+ <item android:drawable="@drawable/format_bar_button_italic"/>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link.xml
new file mode 100755
index 000000000..9ca70c1b8
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#87A6BC"
+ android:pathData="M27,23H17v-2h10V23z
+M28,17h-1c-1.631,0-3.065,0.792-3.977,2H27h1c1.103,0,2,0.897,2,2v2
+c0,1.103-0.897,2-2,2h-1h-3.977c0.913,1.208,2.347,2,3.977,2h1c2.209,0,4-1.791,4-4v-2C32,18.791,30.209,17,28,17z
+M12,21v2
+c0,2.209,1.791,4,4,4h1c1.63,0,3.065-0.792,3.977-2H17h-1c-1.103,0-2-0.897-2-2v-2c0-1.103,0.897-2,2-2h1h3.977
+c-0.913-1.208-2.347-2-3.977-2h-1C13.791,17,12,18.791,12,21z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_disabled.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_disabled.xml
new file mode 100755
index 000000000..d1b4d83f2
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_disabled.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#E8EEF2"
+ android:pathData="M27,23H17v-2h10V23z
+M28,17h-1c-1.631,0-3.065,0.792-3.977,2H27h1c1.103,0,2,0.897,2,2v2
+c0,1.103-0.897,2-2,2h-1h-3.977c0.913,1.208,2.347,2,3.977,2h1c2.209,0,4-1.791,4-4v-2C32,18.791,30.209,17,28,17z
+M12,21v2
+c0,2.209,1.791,4,4,4h1c1.63,0,3.065-0.792,3.977-2H17h-1c-1.103,0-2-0.897-2-2v-2c0-1.103,0.897-2,2-2h1h3.977
+c-0.913-1.208-2.347-2-3.977-2h-1C13.791,17,12,18.791,12,21z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_highlighted.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_highlighted.xml
new file mode 100755
index 000000000..6ce750b27
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_highlighted.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#0084BC"
+ android:pathData="M27,23H17v-2h10V23z
+M28,17h-1c-1.631,0-3.065,0.792-3.977,2H27h1c1.103,0,2,0.897,2,2v2
+c0,1.103-0.897,2-2,2h-1h-3.977c0.913,1.208,2.347,2,3.977,2h1c2.209,0,4-1.791,4-4v-2C32,18.791,30.209,17,28,17z
+M12,21v2
+c0,2.209,1.791,4,4,4h1c1.63,0,3.065-0.792,3.977-2H17h-1c-1.103,0-2-0.897-2-2v-2c0-1.103,0.897-2,2-2h1h3.977
+c-0.913-1.208-2.347-2-3.977-2h-1C13.791,17,12,18.791,12,21z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_selector.xml
new file mode 100755
index 000000000..eaad54d27
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_link_selector.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:drawable="@drawable/format_bar_button_link_highlighted"/>
+ <item android:state_pressed="true" android:drawable="@drawable/format_bar_button_link_highlighted"/>
+ <item android:state_enabled="false" android:drawable="@drawable/format_bar_button_link_disabled"/>
+ <item android:drawable="@drawable/format_bar_button_link"/>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media.xml
new file mode 100755
index 000000000..231c3dce9
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#87A6BC"
+ android:pathData="M33,14v2h-3v3h-2v-3h-3v-2h3v-3h2v3H33z
+M24.5,21c0.828,0,1.5-0.672,1.5-1.5S25.328,18,24.5,18
+S23,18.672,23,19.5S23.672,21,24.5,21z
+M28,24.234l-0.513-0.57c-0.794-0.885-2.181-0.885-2.976,0l-0.656,0.731L19,19l-3,3.333V16h7
+v-2h-7c-1.105,0-2,0.895-2,2v12c0,1.105,0.895,2,2,2h12c1.105,0,2-0.895,2-2v-7h-2V24.234z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_disabled.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_disabled.xml
new file mode 100755
index 000000000..d06f52747
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_disabled.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#E8EEF2"
+ android:pathData="M33,14v2h-3v3h-2v-3h-3v-2h3v-3h2v3H33z
+M24.5,21c0.828,0,1.5-0.672,1.5-1.5S25.328,18,24.5,18
+S23,18.672,23,19.5S23.672,21,24.5,21z
+M28,24.234l-0.513-0.57c-0.794-0.885-2.181-0.885-2.976,0l-0.656,0.731L19,19l-3,3.333V16h7
+v-2h-7c-1.105,0-2,0.895-2,2v12c0,1.105,0.895,2,2,2h12c1.105,0,2-0.895,2-2v-7h-2V24.234z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_highlighted.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_highlighted.xml
new file mode 100755
index 000000000..058e338f4
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_highlighted.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#0084BC"
+ android:pathData="M33,14v2h-3v3h-2v-3h-3v-2h3v-3h2v3H33z
+M24.5,21c0.828,0,1.5-0.672,1.5-1.5S25.328,18,24.5,18
+S23,18.672,23,19.5S23.672,21,24.5,21z
+M28,24.234l-0.513-0.57c-0.794-0.885-2.181-0.885-2.976,0l-0.656,0.731L19,19l-3,3.333V16h7
+v-2h-7c-1.105,0-2,0.895-2,2v12c0,1.105,0.895,2,2,2h12c1.105,0,2-0.895,2-2v-7h-2V24.234z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_selector.xml
new file mode 100755
index 000000000..d0c31e294
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_media_selector.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:drawable="@drawable/format_bar_button_media_highlighted"/>
+ <item android:state_pressed="true" android:drawable="@drawable/format_bar_button_media_highlighted"/>
+ <item android:state_enabled="false" android:drawable="@drawable/format_bar_button_media_disabled"/>
+ <item android:drawable="@drawable/format_bar_button_media"/>
+</selector>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more.xml
new file mode 100755
index 000000000..38d270bd7
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#87A6BC"
+ android:pathData="M13,15h18v4H13V15z M13,29h18v-4H13V29z M19,23h6v-2h-6V23z M13,23h4v-2h-4V23z
+M27,23h4v-2h-4V23z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more_disabled.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more_disabled.xml
new file mode 100755
index 000000000..614bbad28
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more_disabled.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#E8EEF2"
+ android:pathData="M13,15h18v4H13V15z M13,29h18v-4H13V29z M19,23h6v-2h-6V23z M13,23h4v-2h-4V23z
+M27,23h4v-2h-4V23z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more_highlighted.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more_highlighted.xml
new file mode 100755
index 000000000..aa3eff018
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_more_highlighted.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#0084BC"
+ android:pathData="M13,15h18v4H13V15z M13,29h18v-4H13V29z M19,23h6v-2h-6V23z M13,23h4v-2h-4V23z
+M27,23h4v-2h-4V23z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol.xml
new file mode 100755
index 000000000..a7160f486
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#87A6BC"
+ android:pathData="M18,29h13v-2H18V29z M18,23h13v-2H18V23z M18,15v2h13v-2H18z M13.575,15.252
+c0.107-0.096,0.197-0.188,0.269-0.275c-0.012,0.228-0.018,0.48-0.018,0.756V18h1.175v-4.283h-1.042l-1.471,1.198l0.601,0.738
+L13.575,15.252z
+M13.909,23.016c0.475-0.426,0.785-0.715,0.93-0.867c0.146-0.152,0.262-0.297,0.35-0.435
+c0.088-0.138,0.153-0.278,0.195-0.42c0.042-0.143,0.063-0.298,0.063-0.466c0-0.225-0.06-0.427-0.18-0.608s-0.289-0.32-0.507-0.417
+c-0.218-0.099-0.465-0.148-0.742-0.148c-0.221,0-0.419,0.022-0.596,0.067s-0.34,0.11-0.491,0.195
+c-0.15,0.085-0.336,0.226-0.557,0.423l0.636,0.744c0.174-0.15,0.33-0.264,0.467-0.341c0.138-0.077,0.274-0.116,0.409-0.116
+c0.131,0,0.233,0.032,0.305,0.097c0.072,0.064,0.108,0.152,0.108,0.264c0,0.09-0.018,0.176-0.054,0.258
+c-0.036,0.082-0.1,0.18-0.192,0.294c-0.092,0.114-0.287,0.328-0.586,0.64l-1.046,1.058V24h3.108v-0.955h-1.62V23.016L13.909,23.016
+z
+M14.439,27.762v-0.018c0.307-0.086,0.541-0.225,0.703-0.414c0.162-0.191,0.243-0.419,0.243-0.685
+c0-0.309-0.126-0.551-0.378-0.727c-0.252-0.176-0.6-0.264-1.043-0.264c-0.307,0-0.579,0.033-0.816,0.1
+c-0.237,0.067-0.469,0.178-0.696,0.334l0.48,0.773c0.293-0.184,0.576-0.275,0.85-0.275c0.147,0,0.263,0.027,0.35,0.082
+s0.13,0.139,0.13,0.252c0,0.301-0.294,0.451-0.882,0.451h-0.27v0.87h0.264c0.217,0,0.393,0.016,0.527,0.049
+c0.135,0.031,0.232,0.08,0.293,0.143c0.061,0.064,0.091,0.154,0.091,0.271c0,0.152-0.058,0.264-0.174,0.336
+c-0.116,0.07-0.301,0.106-0.555,0.106c-0.164,0-0.343-0.022-0.538-0.069c-0.194-0.045-0.385-0.116-0.573-0.212v0.961
+c0.228,0.088,0.441,0.148,0.637,0.182c0.196,0.033,0.41,0.05,0.64,0.05c0.561,0,0.998-0.114,1.314-0.343
+c0.315-0.228,0.473-0.542,0.473-0.94C15.512,28.19,15.154,27.852,14.439,27.762z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_disabled.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_disabled.xml
new file mode 100755
index 000000000..f396c5737
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_disabled.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#E8EEF2"
+ android:pathData="M18,29h13v-2H18V29z M18,23h13v-2H18V23z M18,15v2h13v-2H18z M13.575,15.252
+c0.107-0.096,0.197-0.188,0.269-0.275c-0.012,0.228-0.018,0.48-0.018,0.756V18h1.175v-4.283h-1.042l-1.471,1.198l0.601,0.738
+L13.575,15.252z
+M13.909,23.016c0.475-0.426,0.785-0.715,0.93-0.867c0.146-0.152,0.262-0.297,0.35-0.435
+c0.088-0.138,0.153-0.278,0.195-0.42c0.042-0.143,0.063-0.298,0.063-0.466c0-0.225-0.06-0.427-0.18-0.608s-0.289-0.32-0.507-0.417
+c-0.218-0.099-0.465-0.148-0.742-0.148c-0.221,0-0.419,0.022-0.596,0.067s-0.34,0.11-0.491,0.195
+c-0.15,0.085-0.336,0.226-0.557,0.423l0.636,0.744c0.174-0.15,0.33-0.264,0.467-0.341c0.138-0.077,0.274-0.116,0.409-0.116
+c0.131,0,0.233,0.032,0.305,0.097c0.072,0.064,0.108,0.152,0.108,0.264c0,0.09-0.018,0.176-0.054,0.258
+c-0.036,0.082-0.1,0.18-0.192,0.294c-0.092,0.114-0.287,0.328-0.586,0.64l-1.046,1.058V24h3.108v-0.955h-1.62V23.016L13.909,23.016
+z
+M14.439,27.762v-0.018c0.307-0.086,0.541-0.225,0.703-0.414c0.162-0.191,0.243-0.419,0.243-0.685
+c0-0.309-0.126-0.551-0.378-0.727c-0.252-0.176-0.6-0.264-1.043-0.264c-0.307,0-0.579,0.033-0.816,0.1
+c-0.237,0.067-0.469,0.178-0.696,0.334l0.48,0.773c0.293-0.184,0.576-0.275,0.85-0.275c0.147,0,0.263,0.027,0.35,0.082
+s0.13,0.139,0.13,0.252c0,0.301-0.294,0.451-0.882,0.451h-0.27v0.87h0.264c0.217,0,0.393,0.016,0.527,0.049
+c0.135,0.031,0.232,0.08,0.293,0.143c0.061,0.064,0.091,0.154,0.091,0.271c0,0.152-0.058,0.264-0.174,0.336
+c-0.116,0.07-0.301,0.106-0.555,0.106c-0.164,0-0.343-0.022-0.538-0.069c-0.194-0.045-0.385-0.116-0.573-0.212v0.961
+c0.228,0.088,0.441,0.148,0.637,0.182c0.196,0.033,0.41,0.05,0.64,0.05c0.561,0,0.998-0.114,1.314-0.343
+c0.315-0.228,0.473-0.542,0.473-0.94C15.512,28.19,15.154,27.852,14.439,27.762z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_highlighted.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_highlighted.xml
new file mode 100755
index 000000000..915e6c8bf
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_highlighted.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#0084BC"
+ android:pathData="M18,29h13v-2H18V29z M18,23h13v-2H18V23z M18,15v2h13v-2H18z M13.575,15.252
+c0.107-0.096,0.197-0.188,0.269-0.275c-0.012,0.228-0.018,0.48-0.018,0.756V18h1.175v-4.283h-1.042l-1.471,1.198l0.601,0.738
+L13.575,15.252z
+M13.909,23.016c0.475-0.426,0.785-0.715,0.93-0.867c0.146-0.152,0.262-0.297,0.35-0.435
+c0.088-0.138,0.153-0.278,0.195-0.42c0.042-0.143,0.063-0.298,0.063-0.466c0-0.225-0.06-0.427-0.18-0.608s-0.289-0.32-0.507-0.417
+c-0.218-0.099-0.465-0.148-0.742-0.148c-0.221,0-0.419,0.022-0.596,0.067s-0.34,0.11-0.491,0.195
+c-0.15,0.085-0.336,0.226-0.557,0.423l0.636,0.744c0.174-0.15,0.33-0.264,0.467-0.341c0.138-0.077,0.274-0.116,0.409-0.116
+c0.131,0,0.233,0.032,0.305,0.097c0.072,0.064,0.108,0.152,0.108,0.264c0,0.09-0.018,0.176-0.054,0.258
+c-0.036,0.082-0.1,0.18-0.192,0.294c-0.092,0.114-0.287,0.328-0.586,0.64l-1.046,1.058V24h3.108v-0.955h-1.62V23.016L13.909,23.016
+z
+M14.439,27.762v-0.018c0.307-0.086,0.541-0.225,0.703-0.414c0.162-0.191,0.243-0.419,0.243-0.685
+c0-0.309-0.126-0.551-0.378-0.727c-0.252-0.176-0.6-0.264-1.043-0.264c-0.307,0-0.579,0.033-0.816,0.1
+c-0.237,0.067-0.469,0.178-0.696,0.334l0.48,0.773c0.293-0.184,0.576-0.275,0.85-0.275c0.147,0,0.263,0.027,0.35,0.082
+s0.13,0.139,0.13,0.252c0,0.301-0.294,0.451-0.882,0.451h-0.27v0.87h0.264c0.217,0,0.393,0.016,0.527,0.049
+c0.135,0.031,0.232,0.08,0.293,0.143c0.061,0.064,0.091,0.154,0.091,0.271c0,0.152-0.058,0.264-0.174,0.336
+c-0.116,0.07-0.301,0.106-0.555,0.106c-0.164,0-0.343-0.022-0.538-0.069c-0.194-0.045-0.385-0.116-0.573-0.212v0.961
+c0.228,0.088,0.441,0.148,0.637,0.182c0.196,0.033,0.41,0.05,0.64,0.05c0.561,0,0.998-0.114,1.314-0.343
+c0.315-0.228,0.473-0.542,0.473-0.94C15.512,28.19,15.154,27.852,14.439,27.762z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_selector.xml
new file mode 100755
index 000000000..74b099634
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ol_selector.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:drawable="@drawable/format_bar_button_ol_highlighted"/>
+ <item android:state_pressed="true" android:drawable="@drawable/format_bar_button_ol_highlighted"/>
+ <item android:state_enabled="false" android:drawable="@drawable/format_bar_button_ol_disabled"/>
+ <item android:drawable="@drawable/format_bar_button_ol"/>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote.xml
new file mode 100755
index 000000000..9bcf8198c
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#87A6BC"
+ android:pathData="M21.192,25.757c0-0.88-0.23-1.618-0.69-2.217c-0.326-0.412-0.768-0.683-1.327-0.812
+c-0.55-0.128-1.07-0.137-1.54-0.028c-0.16-0.95,0.1-1.956,0.76-3.022c0.66-1.065,1.515-1.867,2.558-2.403L19.372,15
+c-0.8,0.396-1.56,0.898-2.26,1.505c-0.71,0.607-1.34,1.305-1.9,2.094c-0.56,0.789-0.98,1.68-1.25,2.69
+c-0.27,1.01-0.345,2.04-0.216,3.1c0.168,1.4,0.62,2.52,1.356,3.35C15.837,28.58,16.754,29,17.85,29c0.965,0,1.766-0.29,2.4-0.878
+c0.628-0.576,0.94-1.365,0.94-2.368L21.192,25.757z
+M30.316,25.757c0-0.88-0.23-1.618-0.69-2.217
+c-0.326-0.42-0.77-0.692-1.327-0.817c-0.56-0.124-1.073-0.13-1.54-0.022c-0.16-0.94,0.09-1.95,0.752-3.02
+c0.66-1.06,1.513-1.86,2.556-2.4L28.49,15c-0.8,0.396-1.555,0.898-2.26,1.505c-0.708,0.607-1.34,1.305-1.894,2.094
+c-0.556,0.79-0.97,1.68-1.24,2.69c-0.273,1.002-0.345,2.04-0.217,3.1c0.166,1.4,0.616,2.52,1.35,3.35
+c0.733,0.834,1.647,1.252,2.743,1.252c0.967,0,1.768-0.29,2.402-0.877c0.627-0.576,0.942-1.365,0.942-2.368L30.316,25.757z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_disabled.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_disabled.xml
new file mode 100755
index 000000000..dd03e26d6
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_disabled.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#E8EEF2"
+ android:pathData="M21.192,25.757c0-0.88-0.23-1.618-0.69-2.217c-0.326-0.412-0.768-0.683-1.327-0.812
+c-0.55-0.128-1.07-0.137-1.54-0.028c-0.16-0.95,0.1-1.956,0.76-3.022c0.66-1.065,1.515-1.867,2.558-2.403L19.372,15
+c-0.8,0.396-1.56,0.898-2.26,1.505c-0.71,0.607-1.34,1.305-1.9,2.094c-0.56,0.789-0.98,1.68-1.25,2.69
+c-0.27,1.01-0.345,2.04-0.216,3.1c0.168,1.4,0.62,2.52,1.356,3.35C15.837,28.58,16.754,29,17.85,29c0.965,0,1.766-0.29,2.4-0.878
+c0.628-0.576,0.94-1.365,0.94-2.368L21.192,25.757z
+M30.316,25.757c0-0.88-0.23-1.618-0.69-2.217
+c-0.326-0.42-0.77-0.692-1.327-0.817c-0.56-0.124-1.073-0.13-1.54-0.022c-0.16-0.94,0.09-1.95,0.752-3.02
+c0.66-1.06,1.513-1.86,2.556-2.4L28.49,15c-0.8,0.396-1.555,0.898-2.26,1.505c-0.708,0.607-1.34,1.305-1.894,2.094
+c-0.556,0.79-0.97,1.68-1.24,2.69c-0.273,1.002-0.345,2.04-0.217,3.1c0.166,1.4,0.616,2.52,1.35,3.35
+c0.733,0.834,1.647,1.252,2.743,1.252c0.967,0,1.768-0.29,2.402-0.877c0.627-0.576,0.942-1.365,0.942-2.368L30.316,25.757z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_highlighted.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_highlighted.xml
new file mode 100755
index 000000000..51b1f7d3e
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_highlighted.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#0084BC"
+ android:pathData="M21.192,25.757c0-0.88-0.23-1.618-0.69-2.217c-0.326-0.412-0.768-0.683-1.327-0.812
+c-0.55-0.128-1.07-0.137-1.54-0.028c-0.16-0.95,0.1-1.956,0.76-3.022c0.66-1.065,1.515-1.867,2.558-2.403L19.372,15
+c-0.8,0.396-1.56,0.898-2.26,1.505c-0.71,0.607-1.34,1.305-1.9,2.094c-0.56,0.789-0.98,1.68-1.25,2.69
+c-0.27,1.01-0.345,2.04-0.216,3.1c0.168,1.4,0.62,2.52,1.356,3.35C15.837,28.58,16.754,29,17.85,29c0.965,0,1.766-0.29,2.4-0.878
+c0.628-0.576,0.94-1.365,0.94-2.368L21.192,25.757z
+M30.316,25.757c0-0.88-0.23-1.618-0.69-2.217
+c-0.326-0.42-0.77-0.692-1.327-0.817c-0.56-0.124-1.073-0.13-1.54-0.022c-0.16-0.94,0.09-1.95,0.752-3.02
+c0.66-1.06,1.513-1.86,2.556-2.4L28.49,15c-0.8,0.396-1.555,0.898-2.26,1.505c-0.708,0.607-1.34,1.305-1.894,2.094
+c-0.556,0.79-0.97,1.68-1.24,2.69c-0.273,1.002-0.345,2.04-0.217,3.1c0.166,1.4,0.616,2.52,1.35,3.35
+c0.733,0.834,1.647,1.252,2.743,1.252c0.967,0,1.768-0.29,2.402-0.877c0.627-0.576,0.942-1.365,0.942-2.368L30.316,25.757z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_selector.xml
new file mode 100755
index 000000000..42daf634c
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_quote_selector.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:drawable="@drawable/format_bar_button_quote_highlighted"/>
+ <item android:state_pressed="true" android:drawable="@drawable/format_bar_button_quote_highlighted"/>
+ <item android:state_enabled="false" android:drawable="@drawable/format_bar_button_quote_disabled"/>
+ <item android:drawable="@drawable/format_bar_button_quote"/>
+</selector>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough.xml
new file mode 100755
index 000000000..5b11a2448
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#87A6BC"
+ android:pathData="M24.348,22H31v2h-4.613c0.239,0.515,0.368,1.094,0.368,1.748c0,1.317-0.474,2.355-1.423,3.114
+C24.385,29.621,23.066,30,21.376,30c-1.557,0-2.934-0.293-4.132-0.878v-2.874c0.985,0.439,1.818,0.749,2.5,0.928
+c0.682,0.181,1.306,0.27,1.872,0.27c0.679,0,1.2-0.129,1.562-0.39c0.363-0.259,0.545-0.644,0.545-1.158
+c0-0.285-0.08-0.54-0.24-0.763c-0.16-0.222-0.394-0.437-0.704-0.643c-0.18-0.12-0.482-0.287-0.88-0.491H13v-2h5.597H24.348z
+M20.82,20c-0.073-0.077-0.143-0.155-0.193-0.235c-0.126-0.202-0.189-0.441-0.189-0.713c0-0.439,0.156-0.795,0.469-1.068
+c0.313-0.273,0.762-0.409,1.348-0.409c0.492,0,0.993,0.063,1.502,0.19c0.509,0.126,1.153,0.349,1.931,0.669l0.998-2.405
+c-0.752-0.326-1.472-0.579-2.16-0.758C23.836,15.09,23.113,15,22.354,15c-1.544,0-2.753,0.369-3.628,1.108
+c-0.874,0.739-1.312,1.753-1.312,3.044c0,0.302,0.036,0.58,0.088,0.848H20.82z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_disabled.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_disabled.xml
new file mode 100755
index 000000000..bb763c150
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_disabled.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#E8EEF2"
+ android:pathData="M24.348,22H31v2h-4.613c0.239,0.515,0.368,1.094,0.368,1.748c0,1.317-0.474,2.355-1.423,3.114
+C24.385,29.621,23.066,30,21.376,30c-1.557,0-2.934-0.293-4.132-0.878v-2.874c0.985,0.439,1.818,0.749,2.5,0.928
+c0.682,0.181,1.306,0.27,1.872,0.27c0.679,0,1.2-0.129,1.562-0.39c0.363-0.259,0.545-0.644,0.545-1.158
+c0-0.285-0.08-0.54-0.24-0.763c-0.16-0.222-0.394-0.437-0.704-0.643c-0.18-0.12-0.482-0.287-0.88-0.491H13v-2h5.597H24.348z
+M20.82,20c-0.073-0.077-0.143-0.155-0.193-0.235c-0.126-0.202-0.189-0.441-0.189-0.713c0-0.439,0.156-0.795,0.469-1.068
+c0.313-0.273,0.762-0.409,1.348-0.409c0.492,0,0.993,0.063,1.502,0.19c0.509,0.126,1.153,0.349,1.931,0.669l0.998-2.405
+c-0.752-0.326-1.472-0.579-2.16-0.758C23.836,15.09,23.113,15,22.354,15c-1.544,0-2.753,0.369-3.628,1.108
+c-0.874,0.739-1.312,1.753-1.312,3.044c0,0.302,0.036,0.58,0.088,0.848H20.82z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_highlighted.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_highlighted.xml
new file mode 100755
index 000000000..bd8401f92
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_highlighted.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#0084BC"
+ android:pathData="M24.348,22H31v2h-4.613c0.239,0.515,0.368,1.094,0.368,1.748c0,1.317-0.474,2.355-1.423,3.114
+C24.385,29.621,23.066,30,21.376,30c-1.557,0-2.934-0.293-4.132-0.878v-2.874c0.985,0.439,1.818,0.749,2.5,0.928
+c0.682,0.181,1.306,0.27,1.872,0.27c0.679,0,1.2-0.129,1.562-0.39c0.363-0.259,0.545-0.644,0.545-1.158
+c0-0.285-0.08-0.54-0.24-0.763c-0.16-0.222-0.394-0.437-0.704-0.643c-0.18-0.12-0.482-0.287-0.88-0.491H13v-2h5.597H24.348z
+M20.82,20c-0.073-0.077-0.143-0.155-0.193-0.235c-0.126-0.202-0.189-0.441-0.189-0.713c0-0.439,0.156-0.795,0.469-1.068
+c0.313-0.273,0.762-0.409,1.348-0.409c0.492,0,0.993,0.063,1.502,0.19c0.509,0.126,1.153,0.349,1.931,0.669l0.998-2.405
+c-0.752-0.326-1.472-0.579-2.16-0.758C23.836,15.09,23.113,15,22.354,15c-1.544,0-2.753,0.369-3.628,1.108
+c-0.874,0.739-1.312,1.753-1.312,3.044c0,0.302,0.036,0.58,0.088,0.848H20.82z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_selector.xml
new file mode 100644
index 000000000..573755e91
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_strikethrough_selector.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:drawable="@drawable/format_bar_button_strikethrough_highlighted"/>
+ <item android:state_pressed="true" android:drawable="@drawable/format_bar_button_strikethrough_highlighted"/>
+ <item android:state_enabled="false" android:drawable="@drawable/format_bar_button_strikethrough_disabled"/>
+ <item android:drawable="@drawable/format_bar_button_strikethrough"/>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul.xml
new file mode 100755
index 000000000..8e23d2154
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#87A6BC"
+ android:pathData="M19,29h12v-2H19V29z M19,23h12v-2H19V23z M19,15v2h12v-2H19z
+M15,14.5c-0.828,0-1.5,0.672-1.5,1.5
+c0,0.828,0.672,1.5,1.5,1.5s1.5-0.672,1.5-1.5C16.5,15.172,15.828,14.5,15,14.5z
+M15,20.5c-0.828,0-1.5,0.672-1.5,1.5
+s0.672,1.5,1.5,1.5s1.5-0.672,1.5-1.5S15.828,20.5,15,20.5z
+M15,26.5c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5
+s1.5-0.672,1.5-1.5S15.828,26.5,15,26.5z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_disabled.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_disabled.xml
new file mode 100755
index 000000000..c1047a0f7
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_disabled.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#E8EEF2"
+ android:pathData="M19,29h12v-2H19V29z M19,23h12v-2H19V23z M19,15v2h12v-2H19z
+M15,14.5c-0.828,0-1.5,0.672-1.5,1.5
+c0,0.828,0.672,1.5,1.5,1.5s1.5-0.672,1.5-1.5C16.5,15.172,15.828,14.5,15,14.5z
+M15,20.5c-0.828,0-1.5,0.672-1.5,1.5
+s0.672,1.5,1.5,1.5s1.5-0.672,1.5-1.5S15.828,20.5,15,20.5z
+M15,26.5c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5
+s1.5-0.672,1.5-1.5S15.828,26.5,15,26.5z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_highlighted.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_highlighted.xml
new file mode 100755
index 000000000..de0c4958b
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_highlighted.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="44"
+ android:viewportHeight="44">
+
+ <path
+ android:fillColor="#0084BC"
+ android:pathData="M19,29h12v-2H19V29z M19,23h12v-2H19V23z M19,15v2h12v-2H19z
+M15,14.5c-0.828,0-1.5,0.672-1.5,1.5
+c0,0.828,0.672,1.5,1.5,1.5s1.5-0.672,1.5-1.5C16.5,15.172,15.828,14.5,15,14.5z
+M15,20.5c-0.828,0-1.5,0.672-1.5,1.5
+s0.672,1.5,1.5,1.5s1.5-0.672,1.5-1.5S15.828,20.5,15,20.5z
+M15,26.5c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5
+s1.5-0.672,1.5-1.5S15.828,26.5,15,26.5z" />
+</vector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_selector.xml
new file mode 100755
index 000000000..98cffc7a7
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/format_bar_button_ul_selector.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:drawable="@drawable/format_bar_button_ul_highlighted"/>
+ <item android:state_pressed="true" android:drawable="@drawable/format_bar_button_ul_highlighted"/>
+ <item android:state_enabled="false" android:drawable="@drawable/format_bar_button_ul_disabled"/>
+ <item android:drawable="@drawable/format_bar_button_ul"/>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/ic_close_padded.xml b/libs/editor/WordPressEditor/src/main/res/drawable/ic_close_padded.xml
new file mode 100644
index 000000000..fa9c56ac4
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/ic_close_padded.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:drawable="@drawable/ic_close_white_24dp"
+ android:left="4dp"
+ android:right="56dp"/>
+</layer-list> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_bold_selected_state.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_bold_selected_state.xml
new file mode 100644
index 000000000..5e7ca3ef8
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_bold_selected_state.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <solid android:color="@color/legacy_format_bar_button_selected"/>
+ </shape>
+ </item>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_editor_bold" android:gravity="center"/>
+ </item>
+</layer-list>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_bold_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_bold_selector.xml
new file mode 100644
index 000000000..66d8b631f
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_bold_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:state_checked="true" android:drawable="@drawable/legacy_format_bar_button_bold_selected_state"/>
+ <item android:state_pressed="true" android:drawable="@drawable/legacy_format_bar_button_bold_selected_state"/>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_editor_bold_grey" android:gravity="center"/>
+ </item>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_italic_selected_state.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_italic_selected_state.xml
new file mode 100644
index 000000000..13689b19a
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_italic_selected_state.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <solid android:color="@color/legacy_format_bar_button_selected"/>
+ </shape>
+ </item>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_editor_italic" android:gravity="center"/>
+ </item>
+</layer-list>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_italic_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_italic_selector.xml
new file mode 100644
index 000000000..f0fd90497
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_italic_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:state_checked="true" android:drawable="@drawable/legacy_format_bar_button_italic_selected_state"/>
+ <item android:state_pressed="true" android:drawable="@drawable/legacy_format_bar_button_italic_selected_state"/>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_editor_italic_grey" android:gravity="center"/>
+ </item>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_link_selected_state.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_link_selected_state.xml
new file mode 100644
index 000000000..d376ceef0
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_link_selected_state.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <solid android:color="@color/legacy_format_bar_button_selected"/>
+ </shape>
+ </item>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_admin_links" android:gravity="center"/>
+ </item>
+</layer-list>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_link_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_link_selector.xml
new file mode 100644
index 000000000..4c488f471
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_link_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:state_checked="true" android:drawable="@drawable/legacy_format_bar_button_link_selected_state"/>
+ <item android:state_pressed="true" android:drawable="@drawable/legacy_format_bar_button_link_selected_state"/>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_admin_links_grey" android:gravity="center"/>
+ </item>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_media_selected_state.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_media_selected_state.xml
new file mode 100644
index 000000000..9d4cbf83e
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_media_selected_state.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <solid android:color="@color/legacy_format_bar_button_selected"/>
+ </shape>
+ </item>
+ <item>
+ <bitmap android:src="@drawable/noticon_picture" android:gravity="center"/>
+ </item>
+</layer-list>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_media_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_media_selector.xml
new file mode 100644
index 000000000..ece70b5c4
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_media_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:state_checked="true" android:drawable="@drawable/legacy_format_bar_button_media_selected_state"/>
+ <item android:state_pressed="true" android:drawable="@drawable/legacy_format_bar_button_media_selected_state"/>
+ <item>
+ <bitmap android:src="@drawable/noticon_picture_grey" android:gravity="center"/>
+ </item>
+</selector>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_more_selected_state.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_more_selected_state.xml
new file mode 100644
index 000000000..0c7870ff5
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_more_selected_state.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <solid android:color="@color/legacy_format_bar_button_selected"/>
+ </shape>
+ </item>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_editor_insertmore" android:gravity="center"/>
+ </item>
+</layer-list>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_more_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_more_selector.xml
new file mode 100644
index 000000000..f28a013ce
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_more_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:state_checked="true" android:drawable="@drawable/legacy_format_bar_button_more_selected_state"/>
+ <item android:state_pressed="true" android:drawable="@drawable/legacy_format_bar_button_more_selected_state"/>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_editor_insertmore_grey" android:gravity="center"/>
+ </item>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_quote_selected_state.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_quote_selected_state.xml
new file mode 100644
index 000000000..b848cd831
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_quote_selected_state.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <solid android:color="@color/legacy_format_bar_button_selected"/>
+ </shape>
+ </item>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_format_quote" android:gravity="center"/>
+ </item>
+</layer-list>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_quote_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_quote_selector.xml
new file mode 100644
index 000000000..ad0a2e684
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_quote_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:state_checked="true" android:drawable="@drawable/legacy_format_bar_button_quote_selected_state"/>
+ <item android:state_pressed="true" android:drawable="@drawable/legacy_format_bar_button_quote_selected_state"/>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_format_quote_grey" android:gravity="center"/>
+ </item>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_strike_selected_state.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_strike_selected_state.xml
new file mode 100644
index 000000000..43762ab9e
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_strike_selected_state.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <solid android:color="@color/legacy_format_bar_button_selected"/>
+ </shape>
+ </item>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_editor_strikethrough" android:gravity="center"/>
+ </item>
+</layer-list>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_strike_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_strike_selector.xml
new file mode 100644
index 000000000..d96797dc7
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_strike_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:state_checked="true" android:drawable="@drawable/legacy_format_bar_button_strike_selected_state"/>
+ <item android:state_pressed="true" android:drawable="@drawable/legacy_format_bar_button_strike_selected_state"/>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_editor_strikethrough_grey" android:gravity="center"/>
+ </item>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_underline_selected_state.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_underline_selected_state.xml
new file mode 100644
index 000000000..786fe8d49
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_underline_selected_state.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <solid android:color="@color/legacy_format_bar_button_selected"/>
+ </shape>
+ </item>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_editor_underline" android:gravity="center"/>
+ </item>
+</layer-list>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_underline_selector.xml b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_underline_selector.xml
new file mode 100644
index 000000000..38ea66806
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/legacy_format_bar_button_underline_selector.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:state_checked="true" android:drawable="@drawable/legacy_format_bar_button_underline_selected_state"/>
+ <item android:state_pressed="true" android:drawable="@drawable/legacy_format_bar_button_underline_selected_state"/>
+ <item>
+ <bitmap android:src="@drawable/legacy_dashicon_editor_underline_grey" android:gravity="center"/>
+ </item>
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/list_divider.xml b/libs/editor/WordPressEditor/src/main/res/drawable/list_divider.xml
new file mode 100644
index 000000000..74267734f
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/list_divider.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ android:insetLeft="@dimen/margin_extra_large"
+ android:insetRight="@dimen/margin_extra_large">
+ <shape android:shape="rectangle">
+
+ <solid android:color="@color/legacy_format_bar_background" />
+
+ </shape>
+</inset>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/pressed_background_wordpress.xml b/libs/editor/WordPressEditor/src/main/res/drawable/pressed_background_wordpress.xml
new file mode 100644
index 000000000..4475720c2
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/pressed_background_wordpress.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- File created by the Android Action Bar Style Generator
+
+ Copyright (C) 2011 The Android Open Source Project
+ Copyright (C) 2012 readyState Software Ltd
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+ <solid android:color="@color/legacy_pressed_wordpress" />
+</shape>
diff --git a/libs/editor/WordPressEditor/src/main/res/drawable/selectable_background_wordpress.xml b/libs/editor/WordPressEditor/src/main/res/drawable/selectable_background_wordpress.xml
new file mode 100644
index 000000000..fbf846f5c
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/drawable/selectable_background_wordpress.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- File created by the Android Action Bar Style Generator
+
+ Copyright (C) 2011 The Android Open Source Project
+ Copyright (C) 2012 readyState Software Ltd
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:exitFadeDuration="@android:integer/config_mediumAnimTime" >
+ <item android:state_pressed="false" android:state_focused="true" android:drawable="@drawable/list_focused_wordpress" />
+ <item android:state_pressed="true" android:drawable="@drawable/pressed_background_wordpress" />
+ <item android:drawable="@android:color/transparent" />
+</selector> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/layout-v19/editor_webview.xml b/libs/editor/WordPressEditor/src/main/res/layout-v19/editor_webview.xml
new file mode 100644
index 000000000..fb826c2ff
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout-v19/editor_webview.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.wordpress.android.editor.EditorWebView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ /> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/layout-w360dp/format_bar.xml b/libs/editor/WordPressEditor/src/main/res/layout-w360dp/format_bar.xml
new file mode 100644
index 000000000..fc2eb283e
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout-w360dp/format_bar.xml
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/format_bar_height"
+ android:layout_gravity="bottom"
+ android:background="@color/format_bar_background"
+ android:layout_alignParentBottom="true"
+ android:orientation="vertical">
+
+ <View
+ android:id="@+id/format_bar_horizontal_divider"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/format_bar_horizontal_divider_height"
+ style="@style/Divider"/>
+
+ <LinearLayout
+ android:id="@+id/format_bar_buttons"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/format_bar_height"
+ android:layout_gravity="bottom"
+ android:layout_marginRight="@dimen/format_bar_right_margin"
+ android:orientation="horizontal"
+ tools:ignore="RtlHardcoded">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="@dimen/format_bar_left_margin"
+ android:orientation="horizontal"
+ tools:ignore="RtlHardcoded">
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_media"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_media"
+ android:background="@drawable/format_bar_button_media_selector"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_bold"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_bold"
+ android:background="@drawable/format_bar_button_bold_selector"
+ android:tag="@string/format_bar_tag_bold"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_italic"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_italic"
+ android:background="@drawable/format_bar_button_italic_selector"
+ android:tag="@string/format_bar_tag_italic"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_quote"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_quote"
+ android:background="@drawable/format_bar_button_quote_selector"
+ android:tag="@string/format_bar_tag_blockquote"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_ul"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_ul"
+ android:background="@drawable/format_bar_button_ul_selector"
+ android:tag="@string/format_bar_tag_unorderedList"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_ol"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_ol"
+ android:background="@drawable/format_bar_button_ol_selector"
+ android:tag="@string/format_bar_tag_orderedList"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_link"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_link"
+ android:background="@drawable/format_bar_button_link_selector"
+ android:tag="@string/format_bar_tag_link"/>
+ </LinearLayout>
+
+ <View
+ android:id="@+id/format_bar_vertical_divider"
+ android:layout_width="@dimen/format_bar_vertical_divider_width"
+ android:layout_height="@dimen/format_bar_vertical_divider_height"
+ android:layout_gravity="center"
+ style="@style/Divider"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_html"
+ style="@style/FormatBarHtmlButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_html"
+ android:background="@drawable/format_bar_button_html_selector"/>
+ </LinearLayout>
+</LinearLayout>
diff --git a/libs/editor/WordPressEditor/src/main/res/layout-w380dp/format_bar.xml b/libs/editor/WordPressEditor/src/main/res/layout-w380dp/format_bar.xml
new file mode 100644
index 000000000..3ee179ba2
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout-w380dp/format_bar.xml
@@ -0,0 +1,149 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/format_bar"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/format_bar_height"
+ android:layout_gravity="bottom"
+ android:background="@color/format_bar_background"
+ android:layout_alignParentBottom="true"
+ android:orientation="vertical">
+
+ <View
+ android:id="@+id/format_bar_horizontal_divider"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/format_bar_horizontal_divider_height"
+ style="@style/Divider"/>
+
+ <LinearLayout
+ android:id="@+id/format_bar_buttons"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/format_bar_height"
+ android:layout_gravity="bottom"
+ android:layout_marginRight="@dimen/format_bar_right_margin"
+ android:orientation="horizontal"
+ tools:ignore="RtlHardcoded">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="center">
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_media"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/format_bar_description_media"
+ android:background="@drawable/format_bar_button_media_selector"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="center">
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_bold"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/format_bar_description_bold"
+ android:background="@drawable/format_bar_button_bold_selector"
+ android:tag="@string/format_bar_tag_bold"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="center">
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_italic"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/format_bar_description_italic"
+ android:background="@drawable/format_bar_button_italic_selector"
+ android:tag="@string/format_bar_tag_italic"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="center">
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_quote"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/format_bar_description_quote"
+ android:background="@drawable/format_bar_button_quote_selector"
+ android:tag="@string/format_bar_tag_blockquote"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="center">
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_ul"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/format_bar_description_ul"
+ android:background="@drawable/format_bar_button_ul_selector"
+ android:tag="@string/format_bar_tag_unorderedList"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="center">
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_ol"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/format_bar_description_ol"
+ android:background="@drawable/format_bar_button_ol_selector"
+ android:tag="@string/format_bar_tag_orderedList"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="center">
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_link"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/format_bar_description_link"
+ android:background="@drawable/format_bar_button_link_selector"
+ android:tag="@string/format_bar_tag_link"/>
+ </LinearLayout>
+
+ <View
+ android:id="@+id/format_bar_vertical_divider"
+ android:layout_width="@dimen/format_bar_vertical_divider_width"
+ android:layout_height="@dimen/format_bar_vertical_divider_height"
+ android:layout_gravity="center"
+ style="@style/Divider"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_html"
+ style="@style/FormatBarHtmlButton"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/format_bar_description_html"
+ android:background="@drawable/format_bar_button_html_selector"/>
+
+
+ </LinearLayout>
+</LinearLayout>
diff --git a/libs/editor/WordPressEditor/src/main/res/layout-w600dp/format_bar.xml b/libs/editor/WordPressEditor/src/main/res/layout-w600dp/format_bar.xml
new file mode 100644
index 000000000..f4a64666b
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout-w600dp/format_bar.xml
@@ -0,0 +1,140 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/format_bar_height_tablet"
+ android:layout_gravity="bottom"
+ android:background="@color/format_bar_background"
+ android:layout_alignParentBottom="true"
+ android:orientation="vertical">
+
+ <View
+ android:id="@+id/format_bar_horizontal_divider"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/format_bar_horizontal_divider_height"
+ style="@style/Divider"/>
+
+ <LinearLayout
+ android:id="@+id/format_bar_buttons"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/format_bar_height_tablet"
+ android:layout_gravity="bottom"
+ android:gravity="center"
+ android:orientation="horizontal"
+ tools:ignore="RtlHardcoded">
+
+ <LinearLayout
+ style="@style/FormatBarTabletGroup"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:gravity="center">
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_media"
+ style="@style/FormatBarButtonTablet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/format_bar_description_media"
+ android:background="@drawable/format_bar_button_media_selector"/>
+ </LinearLayout>
+
+ <LinearLayout
+ style="@style/FormatBarTabletGroup"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:gravity="center">
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_bold"
+ style="@style/FormatBarButtonTablet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/format_bar_description_bold"
+ android:background="@drawable/format_bar_button_bold_selector"
+ android:tag="@string/format_bar_tag_bold"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_italic"
+ style="@style/FormatBarButtonTablet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/format_bar_description_italic"
+ android:background="@drawable/format_bar_button_italic_selector"
+ android:tag="@string/format_bar_tag_italic"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_strikethrough"
+ style="@style/FormatBarButtonTablet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/format_bar_button_strikethrough_selector"
+ android:tag="@string/format_bar_tag_strikethrough"/>
+ </LinearLayout>
+
+ <LinearLayout
+ style="@style/FormatBarTabletGroup"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:gravity="center">
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_link"
+ style="@style/FormatBarButtonTablet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/format_bar_description_link"
+ android:background="@drawable/format_bar_button_link_selector"
+ android:tag="@string/format_bar_tag_link"/>
+ </LinearLayout>
+
+ <LinearLayout
+ style="@style/FormatBarTabletGroup"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:gravity="center">
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_ul"
+ style="@style/FormatBarButtonTablet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/format_bar_description_ul"
+ android:background="@drawable/format_bar_button_ul_selector"
+ android:tag="@string/format_bar_tag_unorderedList"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_ol"
+ style="@style/FormatBarButtonTablet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/format_bar_description_ol"
+ android:background="@drawable/format_bar_button_ol_selector"
+ android:tag="@string/format_bar_tag_orderedList"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_quote"
+ style="@style/FormatBarButtonTablet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/format_bar_description_quote"
+ android:background="@drawable/format_bar_button_quote_selector"
+ android:tag="@string/format_bar_tag_blockquote"/>
+ </LinearLayout>
+
+ <LinearLayout
+ style="@style/FormatBarTabletGroup"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:gravity="center">
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_html"
+ style="@style/FormatBarButtonTablet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/format_bar_description_html"
+ android:background="@drawable/format_bar_button_html_selector"/>
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout>
diff --git a/libs/editor/WordPressEditor/src/main/res/layout/alert_create_link.xml b/libs/editor/WordPressEditor/src/main/res/layout/alert_create_link.xml
new file mode 100644
index 000000000..840917184
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout/alert_create_link.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/margin_medium"
+ android:layout_gravity="center_vertical">
+
+ <EditText
+ android:id="@+id/linkURL"
+ android:inputType="textUri"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/link_enter_url"
+ android:imeOptions="actionNext" />
+
+ <EditText
+ android:id="@+id/linkText"
+ android:inputType="text"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/linkURL"
+ android:hint="@string/link_enter_url_text" />
+
+ <Button
+ android:id="@+id/ok"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/linkText"
+ android:layout_alignParentRight="true"
+ android:layout_marginLeft="@dimen/margin_medium"
+ android:text="@android:string/ok" />
+
+ <Button
+ android:id="@+id/cancel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignTop="@id/ok"
+ android:layout_toLeftOf="@id/ok"
+ android:text="@android:string/cancel" />
+
+</RelativeLayout>
diff --git a/libs/editor/WordPressEditor/src/main/res/layout/alert_image_options.xml b/libs/editor/WordPressEditor/src/main/res/layout/alert_image_options.xml
new file mode 100644
index 000000000..b2d4fdb9c
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout/alert_image_options.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="10dp"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center_horizontal">
+
+ <EditText
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/title"
+ android:singleLine="true"/>
+
+ <EditText
+ android:id="@+id/caption"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/caption"
+ android:singleLine="true"
+ android:inputType="textCapSentences"/>
+
+ <TextView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/horizontal_alignment"
+ android:textStyle="bold"
+ android:textColor="@color/image_options_label"/>
+
+ <Spinner
+ android:id="@+id/alignment_spinner"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:prompt="@string/image_alignment"/>
+
+ <TextView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/width"
+ android:textStyle="bold"
+ android:textColor="@color/image_options_label"/>
+
+ <SeekBar
+ android:layout_height="wrap_content"
+ android:id="@+id/imageWidth"
+ android:layout_width="match_parent"/>
+
+ <EditText
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text=""
+ android:imeOptions="actionDone"
+ android:id="@+id/imageWidthText"
+ android:layout_gravity="left|center_vertical"
+ android:singleLine="true"
+ android:inputType="number"/>
+
+ <CheckBox
+ android:layout_gravity="left"
+ android:text="@string/featured"
+ android:id="@+id/featuredImage"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"/>
+
+ <CheckBox
+ android:layout_gravity="left"
+ android:text="@string/featured_in_post"
+ android:id="@+id/featuredInPost"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"/>
+ </LinearLayout>
+</ScrollView> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/layout/dialog_image_options.xml b/libs/editor/WordPressEditor/src/main/res/layout/dialog_image_options.xml
new file mode 100644
index 000000000..087bb1ba7
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout/dialog_image_options.xml
@@ -0,0 +1,154 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/white">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="@dimen/image_settings_dialog_extra_margin"
+ android:layout_marginLeft="@dimen/image_settings_dialog_extra_margin"
+ android:orientation="vertical"
+ android:padding="@dimen/image_settings_dialog_padding">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_marginBottom="@dimen/image_settings_dialog_thumbnail_container_margin_bottom">
+
+ <ImageView
+ android:id="@+id/image_thumbnail"
+ android:contentDescription="@string/image_thumbnail"
+ android:layout_width="@dimen/image_settings_dialog_thumbnail_size"
+ android:layout_height="@dimen/image_settings_dialog_thumbnail_size"
+ android:layout_marginLeft="@dimen/image_settings_dialog_thumbnail_left_margin"
+ android:layout_marginRight="@dimen/image_settings_dialog_thumbnail_right_margin"/>
+
+ <TextView
+ android:id="@+id/image_filename"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/image_settings_dialog_filename_margin_left"
+ android:layout_gravity="center_vertical"/>
+ </LinearLayout>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/title"
+ style="@style/ImageOptionsDialogLabel"/>
+
+ <EditText
+ android:id="@+id/image_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textCapSentences"
+ android:singleLine="true"
+ style="@style/ImageOptionsDialogInput"/>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/image_caption"
+ style="@style/ImageOptionsDialogLabel"/>
+
+ <EditText
+ android:id="@+id/image_caption"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textCapSentences"
+ android:singleLine="true"
+ style="@style/ImageOptionsDialogInput"/>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/image_alt_text"
+ style="@style/ImageOptionsDialogLabel"/>
+
+ <EditText
+ android:id="@+id/image_alt_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textCapSentences"
+ android:singleLine="true"
+ style="@style/ImageOptionsDialogInput"/>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/image_alignment"
+ style="@style/ImageOptionsDialogLabel"/>
+
+ <Spinner
+ android:id="@+id/alignment_spinner"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:entries="@array/alignment_array"
+ android:layout_marginStart="@dimen/image_settings_dialog_input_field_start_margin"
+ android:layout_marginLeft="@dimen/image_settings_dialog_input_field_start_margin"
+ android:prompt="@string/image_alignment"
+ style="@style/ImageOptionsDialogInput"/>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/image_link_to"
+ style="@style/ImageOptionsDialogLabel"/>
+
+ <EditText
+ android:id="@+id/image_link_to"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textNoSuggestions"
+ android:singleLine="true"
+ style="@style/ImageOptionsDialogInput"/>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/image_width"
+ style="@style/ImageOptionsDialogLabel"/>
+
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/image_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ app:srcCompat="@drawable/media_icon_32dp"/>
+
+ <SeekBar
+ android:id="@+id/image_width_seekbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_toRightOf="@+id/image_icon"/>
+ </RelativeLayout>
+
+ <EditText
+ android:id="@+id/image_width_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text=""
+ android:imeOptions="actionDone"
+ android:singleLine="true"
+ android:inputType="number"
+ style="@style/ImageOptionsDialogInput"/>
+
+ <CheckBox
+ android:id="@+id/featuredImage"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/featured"
+ android:layout_marginStart="@dimen/image_settings_dialog_input_field_start_margin"
+ android:layout_marginLeft="@dimen/image_settings_dialog_input_field_start_margin"
+ android:visibility="gone"/>
+ </LinearLayout>
+</ScrollView>
diff --git a/libs/editor/WordPressEditor/src/main/res/layout/dialog_link.xml b/libs/editor/WordPressEditor/src/main/res/layout/dialog_link.xml
new file mode 100644
index 000000000..c303163d0
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout/dialog_link.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <android.support.design.widget.TextInputLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/link_dialog_margin_inner"
+ android:layout_marginLeft="@dimen/link_dialog_margin_outer"
+ android:layout_marginRight="@dimen/link_dialog_margin_outer"
+ android:layout_marginTop="@dimen/link_dialog_margin_outer">
+
+ <EditText
+ android:id="@+id/linkURL"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/link_enter_url"
+ android:imeOptions="actionNext"
+ android:inputType="textUri"
+ tools:ignore="HardcodedText" />
+ </android.support.design.widget.TextInputLayout>
+
+ <android.support.design.widget.TextInputLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/link_dialog_margin_outer"
+ android:layout_marginLeft="@dimen/link_dialog_margin_outer"
+ android:layout_marginRight="@dimen/link_dialog_margin_outer"
+ android:layout_marginTop="@dimen/link_dialog_margin_inner">
+
+ <EditText
+ android:id="@+id/linkText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/link_enter_url_text"
+ android:inputType="text" />
+ </android.support.design.widget.TextInputLayout>
+</LinearLayout>
diff --git a/libs/editor/WordPressEditor/src/main/res/layout/editor_webview.xml b/libs/editor/WordPressEditor/src/main/res/layout/editor_webview.xml
new file mode 100644
index 000000000..b02cf21da
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout/editor_webview.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.wordpress.android.editor.EditorWebViewCompatibility
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ /> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/layout/format_bar.xml b/libs/editor/WordPressEditor/src/main/res/layout/format_bar.xml
new file mode 100644
index 000000000..1fa988beb
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout/format_bar.xml
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/format_bar_height"
+ android:layout_gravity="bottom"
+ android:layout_alignParentBottom="true"
+ android:background="@color/format_bar_background"
+ android:orientation="vertical">
+
+ <View
+ android:id="@+id/format_bar_horizontal_divider"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/format_bar_horizontal_divider_height"
+ style="@style/Divider"/>
+
+ <LinearLayout
+ android:id="@+id/format_bar_buttons"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/format_bar_height"
+ android:layout_gravity="bottom"
+ android:layout_marginRight="@dimen/format_bar_right_margin"
+ android:orientation="horizontal"
+ tools:ignore="RtlHardcoded">
+
+ <HorizontalScrollView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginRight="@dimen/format_bar_scroll_right_margin">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/format_bar_left_margin"
+ android:orientation="horizontal"
+ tools:ignore="RtlHardcoded">
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_media"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_media"
+ android:background="@drawable/format_bar_button_media_selector"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_bold"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_bold"
+ android:background="@drawable/format_bar_button_bold_selector"
+ android:tag="@string/format_bar_tag_bold"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_italic"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_italic"
+ android:background="@drawable/format_bar_button_italic_selector"
+ android:tag="@string/format_bar_tag_italic"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_quote"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_quote"
+ android:background="@drawable/format_bar_button_quote_selector"
+ android:tag="@string/format_bar_tag_blockquote"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_ul"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_ul"
+ android:background="@drawable/format_bar_button_ul_selector"
+ android:tag="@string/format_bar_tag_unorderedList"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_ol"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_ol"
+ android:background="@drawable/format_bar_button_ol_selector"
+ android:tag="@string/format_bar_tag_orderedList"/>
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_link"
+ style="@style/FormatBarButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_link"
+ android:background="@drawable/format_bar_button_link_selector"
+ android:tag="@string/format_bar_tag_link"/>
+ </LinearLayout>
+ </HorizontalScrollView>
+
+ <ImageView
+ app:srcCompat="@drawable/format_bar_chevron"
+ android:background="@android:color/transparent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/imageView" />
+
+ <org.wordpress.android.editor.RippleToggleButton
+ android:id="@+id/format_bar_button_html"
+ style="@style/FormatBarHtmlButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_html"
+ android:background="@drawable/format_bar_button_html_selector"/>
+ </LinearLayout>
+</LinearLayout>
diff --git a/libs/editor/WordPressEditor/src/main/res/layout/fragment_edit_post_content.xml b/libs/editor/WordPressEditor/src/main/res/layout/fragment_edit_post_content.xml
new file mode 100644
index 000000000..c94a08e5e
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout/fragment_edit_post_content.xml
@@ -0,0 +1,165 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:background="@android:color/white"
+ android:focusableInTouchMode="true">
+
+ <ScrollView
+ android:layout_width="fill_parent"
+ android:layout_height="0dp"
+ android:orientation="vertical"
+ android:fillViewport="true"
+ android:layout_weight="1">
+
+ <LinearLayout
+ android:id="@+id/post_content_wrapper"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <EditText
+ android:id="@+id/post_title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/margin_extra_large"
+ android:layout_marginRight="@dimen/margin_extra_large"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:textSize="22sp"
+ android:hint="@string/post_title"
+ android:inputType="textCapSentences|textAutoCorrect" />
+
+ <org.wordpress.android.util.widgets.WPEditText
+ android:id="@+id/post_content"
+ android:layout_width="fill_parent"
+ android:layout_height="0dp"
+ android:maxLength="10000000"
+ android:layout_marginLeft="@dimen/post_editor_content_side_margin"
+ android:layout_marginRight="@dimen/post_editor_content_side_margin"
+ android:layout_marginTop="8dp"
+ android:gravity="top"
+ android:layout_weight="1"
+ android:hint="@string/post_content"
+ android:background="@null"
+ android:lineSpacingExtra="4dp"
+ android:textSize="18sp"
+ android:inputType="textMultiLine|textCapSentences|textAutoCorrect"
+ android:imeOptions="flagNoExtractUi"
+ android:textColorLink="@color/legacy_placeholder_content_text" />
+ </LinearLayout>
+ </ScrollView>
+
+ <LinearLayout
+ android:id="@+id/post_settings_wrapper"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1dp"
+ android:background="@drawable/list_divider" />
+
+ <Button
+ android:id="@+id/post_settings_button"
+ android:layout_width="fill_parent"
+ android:layout_height="48dp"
+ android:gravity="center_vertical"
+ android:paddingLeft="16dp"
+ android:textSize="18sp"
+ android:drawableLeft="@drawable/ic_post_settings"
+ android:background="@drawable/selectable_background_wordpress"
+ android:text="@string/post_settings"
+ android:drawablePadding="6dp"
+ android:layout_gravity="bottom"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/format_bar"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/legacy_format_bar_height"
+ android:layout_gravity="bottom"
+ android:background="@color/legacy_format_bar_background"
+ android:orientation="horizontal"
+ android:visibility="gone">
+
+ <HorizontalScrollView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <ToggleButton
+ android:id="@+id/bold"
+ style="@style/LegacyToggleButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_bold"
+ android:background="@drawable/legacy_format_bar_button_bold_selector" />
+
+ <ToggleButton
+ android:id="@+id/em"
+ style="@style/LegacyToggleButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_italic"
+ android:background="@drawable/legacy_format_bar_button_italic_selector" />
+
+ <ToggleButton
+ android:id="@+id/underline"
+ style="@style/LegacyToggleButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_underline"
+ android:background="@drawable/legacy_format_bar_button_underline_selector" />
+
+ <ToggleButton
+ android:id="@+id/strike"
+ style="@style/LegacyToggleButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_strike"
+ android:background="@drawable/legacy_format_bar_button_strike_selector" />
+
+ <ToggleButton
+ android:id="@+id/bquote"
+ style="@style/LegacyToggleButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:contentDescription="@string/format_bar_description_quote"
+ android:background="@drawable/legacy_format_bar_button_quote_selector" />
+
+ <Button
+ android:id="@+id/link"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:minWidth="@dimen/legacy_format_bar_height"
+ android:contentDescription="@string/format_bar_description_link"
+ android:background="@drawable/legacy_format_bar_button_link_selector" />
+
+ <Button
+ android:id="@+id/more"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:minWidth="@dimen/legacy_format_bar_height"
+ android:contentDescription="@string/format_bar_description_more"
+ android:background="@drawable/legacy_format_bar_button_more_selector" />
+ </LinearLayout>
+ </HorizontalScrollView>
+
+ <Button
+ android:id="@+id/addPictureButton"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:minWidth="@dimen/legacy_format_bar_height"
+ android:contentDescription="@string/format_bar_description_media"
+ android:background="@drawable/legacy_format_bar_button_media_selector" />
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/libs/editor/WordPressEditor/src/main/res/layout/fragment_editor.xml b/libs/editor/WordPressEditor/src/main/res/layout/fragment_editor.xml
new file mode 100755
index 000000000..ff0326f31
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout/fragment_editor.xml
@@ -0,0 +1,82 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:editor="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ tools:context="org.wordpress.android.editor.EditorFragment">
+
+ <include
+ layout="@layout/editor_webview"
+ android:id="@+id/webview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_above="@+id/format_bar"/>
+
+ <ScrollView
+ android:id="@+id/sourceview"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="@android:color/white"
+ android:orientation="vertical"
+ android:layout_above="@id/format_bar"
+ android:fillViewport="true"
+ android:visibility="gone">
+
+ <LinearLayout
+ android:id="@+id/post_content_wrapper"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <org.wordpress.android.editor.SourceViewEditText
+ android:id="@+id/sourceview_title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/sourceview_side_margin"
+ android:layout_marginRight="@dimen/sourceview_side_margin"
+ android:layout_marginTop="@dimen/sourceview_top_margin"
+ android:layout_marginBottom="@dimen/sourceview_title_bottom_margin"
+ android:background="@null"
+ android:textSize="24sp"
+ android:textColorHint="@color/sourceview_placeholder_text"
+ android:inputType="textCapSentences|textAutoCorrect"
+ android:imeOptions="flagNoExtractUi"
+ editor:fontFile="Merriweather-Bold.ttf"/>
+
+ <View
+ android:id="@+id/sourceview_horizontal_divider"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/format_bar_horizontal_divider_height"
+ android:layout_marginLeft="@dimen/sourceview_side_margin"
+ android:layout_marginRight="@dimen/sourceview_side_margin"
+ style="@style/DividerSourceView"/>
+
+ <org.wordpress.android.editor.SourceViewEditText
+ android:id="@+id/sourceview_content"
+ android:layout_width="fill_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:gravity="top"
+ android:layout_marginLeft="@dimen/sourceview_side_margin"
+ android:layout_marginRight="@dimen/sourceview_side_margin"
+ android:layout_marginTop="@dimen/sourceview_top_margin"
+ android:background="@null"
+ android:textSize="16sp"
+ android:maxLength="10000000"
+ android:textColorHint="@color/sourceview_placeholder_text"
+ android:inputType="textMultiLine|textCapSentences|textNoSuggestions"
+ android:lineSpacingExtra="4dp"
+ android:imeOptions="flagNoExtractUi"
+ android:typeface="monospace"/>
+ </LinearLayout>
+ </ScrollView>
+
+ <include
+ layout="@layout/format_bar"
+ android:id="@+id/format_bar"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"/>
+
+</RelativeLayout>
diff --git a/libs/editor/WordPressEditor/src/main/res/layout/image_settings_formatbar.xml b/libs/editor/WordPressEditor/src/main/res/layout/image_settings_formatbar.xml
new file mode 100644
index 000000000..ff5ea7df4
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout/image_settings_formatbar.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/menu_save"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:text="@string/save"
+ android:textAllCaps="true"
+ android:textColor="@android:color/white"/>
+</LinearLayout> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/layout/legacy_activity_editor.xml b/libs/editor/WordPressEditor/src/main/res/layout/legacy_activity_editor.xml
new file mode 100644
index 000000000..24756188d
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/layout/legacy_activity_editor.xml
@@ -0,0 +1,13 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".EditorActivity">
+
+ <fragment
+ android:id="@+id/postEditor"
+ android:name="org.wordpress.android.editor.LegacyEditorFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+</RelativeLayout>
diff --git a/libs/editor/WordPressEditor/src/main/res/values-w1280dp/dimens.xml b/libs/editor/WordPressEditor/src/main/res/values-w1280dp/dimens.xml
new file mode 100644
index 000000000..c68bd2b8f
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values-w1280dp/dimens.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="sourceview_side_margin">@dimen/sourceview_side_margin_extra_large</dimen>
+ <dimen name="format_bar_button_margin_tablet">@dimen/format_bar_button_margin_tablet_extra_large</dimen>
+ <dimen name="format_bar_button_group_tablet">@dimen/format_bar_button_group_tablet_extra_large</dimen>
+</resources> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/values-w1280dp/layouts.xml b/libs/editor/WordPressEditor/src/main/res/values-w1280dp/layouts.xml
new file mode 100644
index 000000000..17b7dd6be
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values-w1280dp/layouts.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <bool name="is_large_tablet_landscape">true</bool>
+</resources> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/values-w720dp/dimens.xml b/libs/editor/WordPressEditor/src/main/res/values-w720dp/dimens.xml
new file mode 100644
index 000000000..ad669947f
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values-w720dp/dimens.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="sourceview_side_margin">@dimen/sourceview_side_margin_large</dimen>
+ <dimen name="image_settings_dialog_extra_margin">@dimen/image_settings_dialog_extra_margin_tablet</dimen>
+</resources> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/values-w720dp/layouts.xml b/libs/editor/WordPressEditor/src/main/res/values-w720dp/layouts.xml
new file mode 100644
index 000000000..de5683d0f
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values-w720dp/layouts.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <bool name="show_extra_side_padding">true</bool>
+</resources> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/values-w800dp/dimens.xml b/libs/editor/WordPressEditor/src/main/res/values-w800dp/dimens.xml
new file mode 100644
index 000000000..1f110a98b
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values-w800dp/dimens.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="format_bar_button_margin_tablet">@dimen/format_bar_button_margin_tablet_large</dimen>
+ <dimen name="format_bar_button_group_tablet">@dimen/format_bar_button_group_tablet_large</dimen>
+</resources> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/values/attrs.xml b/libs/editor/WordPressEditor/src/main/res/values/attrs.xml
new file mode 100644
index 000000000..bfe9ab434
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values/attrs.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <declare-styleable name="SourceViewEditText">
+ <attr name="fontFile" format="string" />
+ </declare-styleable>
+</resources> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/values/colors.xml b/libs/editor/WordPressEditor/src/main/res/values/colors.xml
new file mode 100755
index 000000000..44ffdaf50
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values/colors.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="legacy_format_bar_button_selected">#00aadc</color>
+ <color name="legacy_placeholder_content_text">#21759B</color>
+ <color name="legacy_format_bar_background">#eeeeee</color>
+ <color name="legacy_pressed_wordpress">#CC78C8E6</color>
+
+ <color name="image_options_label">@color/wp_gray</color>
+
+ <color name="format_bar_button_normal_color">@color/wp_gray_lighten_10</color>
+ <color name="format_bar_button_highlighted_color">@color/wp_blue</color>
+ <color name="format_bar_background">#f9fbfc</color>
+ <color name="format_bar_ripple_animation">@color/wp_gray_lighten_10</color>
+
+ <color name="format_bar_divider">@color/wp_gray_lighten_10</color>
+ <color name="sourceview_separator">@color/wp_gray_lighten_30</color>
+ <color name="sourceview_placeholder_text">@color/wp_gray</color>
+</resources>
diff --git a/libs/editor/WordPressEditor/src/main/res/values/dimens.xml b/libs/editor/WordPressEditor/src/main/res/values/dimens.xml
new file mode 100644
index 000000000..3f76aa7db
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values/dimens.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="format_bar_height">44dp</dimen>
+ <dimen name="format_bar_height_tablet">49dp</dimen>
+ <dimen name="format_bar_left_margin">5dp</dimen>
+ <dimen name="format_bar_right_margin">2dp</dimen>
+ <dimen name="format_bar_html_button_left_margin">3dp</dimen>
+ <dimen name="format_bar_scroll_right_margin">-7dp</dimen>
+
+ <dimen name="format_bar_button_margin_tablet">@dimen/format_bar_button_margin_tablet_small</dimen>
+ <dimen name="format_bar_button_margin_tablet_small">0dp</dimen>
+ <dimen name="format_bar_button_margin_tablet_large">7dp</dimen>
+ <dimen name="format_bar_button_margin_tablet_extra_large">12dp</dimen>
+
+ <dimen name="format_bar_button_group_tablet">@dimen/format_bar_button_group_tablet_small</dimen>
+ <dimen name="format_bar_button_group_tablet_small">15dp</dimen>
+ <dimen name="format_bar_button_group_tablet_large">25dp</dimen>
+ <dimen name="format_bar_button_group_tablet_extra_large">35dp</dimen>
+
+ <dimen name="format_bar_horizontal_divider_height">1dp</dimen>
+ <dimen name="format_bar_vertical_divider_width">0.6dp</dimen>
+ <dimen name="format_bar_vertical_divider_height">28dp</dimen>
+
+ <dimen name="sourceview_side_margin">15dp</dimen>
+ <dimen name="sourceview_side_margin_large">90dp</dimen>
+ <dimen name="sourceview_side_margin_extra_large">170dp</dimen>
+ <dimen name="sourceview_top_margin">15dp</dimen>
+ <dimen name="sourceview_title_bottom_margin">15dp</dimen>
+
+ <dimen name="link_dialog_margin_inner">4dp</dimen>
+ <dimen name="link_dialog_margin_outer">16dp</dimen>
+
+ <dimen name="image_settings_dialog_padding">16dp</dimen>
+ <dimen name="image_settings_dialog_extra_margin">0dp</dimen>
+ <dimen name="image_settings_dialog_extra_margin_tablet">84dp</dimen>
+ <dimen name="image_settings_dialog_label_indent">4dp</dimen>
+ <dimen name="image_settings_dialog_label_margin_bottom">4dp</dimen>
+ <dimen name="image_settings_dialog_input_margin_bottom">16dp</dimen>
+ <dimen name="image_settings_dialog_input_field_start_margin">-4dp</dimen>
+ <dimen name="image_settings_dialog_top_separator_margin_top">16dp</dimen>
+ <dimen name="image_settings_dialog_thumbnail_container_margin_bottom">8dp</dimen>
+ <dimen name="image_settings_dialog_thumbnail_size">100dp</dimen>
+ <dimen name="image_settings_dialog_thumbnail_left_margin">4dp</dimen>
+ <dimen name="image_settings_dialog_thumbnail_right_margin">8dp</dimen>
+ <dimen name="image_settings_dialog_filename_margin_left">16dp</dimen>
+
+ <dimen name="post_editor_content_side_margin">20dp</dimen>
+
+ <dimen name="legacy_format_bar_height">40dp</dimen>
+ <dimen name="margin_extra_small">2dp</dimen>
+ <dimen name="margin_small">4dp</dimen>
+ <dimen name="margin_medium">8dp</dimen>
+ <dimen name="margin_large">12dp</dimen>
+ <dimen name="margin_extra_large">16dp</dimen>
+</resources>
diff --git a/libs/editor/WordPressEditor/src/main/res/values/layouts.xml b/libs/editor/WordPressEditor/src/main/res/values/layouts.xml
new file mode 100644
index 000000000..ae7684978
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values/layouts.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <bool name="is_large_tablet_landscape">false</bool>
+ <bool name="show_extra_side_padding">false</bool>
+</resources> \ No newline at end of file
diff --git a/libs/editor/WordPressEditor/src/main/res/values/strings.xml b/libs/editor/WordPressEditor/src/main/res/values/strings.xml
new file mode 100644
index 000000000..d4ba277be
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values/strings.xml
@@ -0,0 +1,97 @@
+<resources>
+ <string name="app_name">Editor</string>
+ <string name="post_settings">Settings</string>
+ <string name="post_content">Content</string>
+ <string name="post_title">Title</string>
+
+ <string name="ok">OK</string>
+ <string name="cancel">Cancel</string>
+ <string name="delete">Delete</string>
+ <string name="save">Save</string>
+ <string name="discard">Discard</string>
+ <string name="edit">Edit</string>
+ <string name="tap_to_try_again">Tap to try again!</string>
+ <string name="uploading">Uploading…</string>
+ <string name="uploading_gallery_placeholder">Uploading gallery…</string>
+
+ <string name="alert_insert_image_html_mode">Can\'t insert media directly in HTML mode. Please switch back to visual mode.</string>
+ <string name="alert_action_while_uploading">You are currently uploading media. Please wait until this completes.</string>
+ <string name="alert_error_adding_media">An error occurred while inserting media</string>
+
+ <string name="stop_upload_dialog_title">Stop uploading?</string>
+ <string name="stop_upload_button">Stop Upload</string>
+
+ <string name="format_bar_tag_bold" translatable="false">bold</string>
+ <string name="format_bar_tag_italic" translatable="false">italic</string>
+ <string name="format_bar_tag_blockquote" translatable="false">blockquote</string>
+ <string name="format_bar_tag_unorderedList" translatable="false">unorderedList</string>
+ <string name="format_bar_tag_orderedList" translatable="false">orderedList</string>
+ <string name="format_bar_tag_strikethrough" translatable="false">strikeThrough</string>
+ <string name="format_bar_tag_link" translatable="false">link</string>
+
+ <!-- link view -->
+ <string name="link_enter_url">URL</string>
+ <string name="link_enter_url_text">Link text (optional)</string>
+ <string name="create_a_link">Create a link</string>
+
+ <!-- image settings -->
+ <string name="image_settings">Image settings</string>
+ <string name="title">Title</string>
+ <string name="width">Width</string>
+ <string name="featured">Use as featured image</string>
+ <string name="featured_in_post">Include image in post content</string>
+ <string name="image_alignment">Alignment</string>
+ <string name="horizontal_alignment">Horizontal alignment</string>
+ <string name="caption">Caption (optional)</string>
+
+ <string name="image_settings_dismiss_dialog_title">Discard unsaved changes?</string>
+ <string name="image_settings_save_toast">Changes saved</string>
+
+ <string name="image_caption">Caption</string>
+ <string name="image_alt_text">Alt text</string>
+ <string name="image_link_to">Link to</string>
+ <string name="image_width">Width</string>
+
+ <string name="alignment_none">None</string>
+ <string name="alignment_left">Left</string>
+ <string name="alignment_center">Center</string>
+ <string name="alignment_right">Right</string>
+
+ <string-array name="alignment_key_array" translatable="false">
+ <item>none</item>
+ <item>left</item>
+ <item>center</item>
+ <item>right</item>
+ </string-array>
+
+ <string-array name="alignment_array" translatable="false">
+ <item>@string/alignment_none</item>
+ <item>@string/alignment_left</item>
+ <item>@string/alignment_center</item>
+ <item>@string/alignment_right</item>
+ </string-array>
+
+ <!-- Accessibility - format bar button descriptions -->
+ <string name="format_bar_description_bold">Bold</string>
+ <string name="format_bar_description_italic">Italic</string>
+ <string name="format_bar_description_underline">Underline</string>
+ <string name="format_bar_description_strike">Strikethrough</string>
+ <string name="format_bar_description_quote">Block quote</string>
+ <string name="format_bar_description_link">Insert link</string>
+ <string name="format_bar_description_more">Insert more</string>
+ <string name="format_bar_description_media">Insert media</string>
+ <string name="format_bar_description_ul">Unordered list</string>
+ <string name="format_bar_description_ol">Ordered list</string>
+ <string name="format_bar_description_html">HTML mode</string>
+ <string name="visual_editor">Visual editor</string>
+ <string name="image_thumbnail">Image thumbnail</string>
+
+ <string name="editor_failed_uploads_switch_html">Some media uploads have failed. You can\'t switch to HTML mode
+ in this state. Remove all failed uploads and continue?</string>
+ <string name="editor_remove_failed_uploads">Remove failed uploads</string>
+
+ <string name="editor_dropped_text_error">Error occurred while dropping text</string>
+ <string name="editor_dropped_title_images_not_allowed">Dropping images in the Title is not allowed</string>
+ <string name="editor_dropped_html_images_not_allowed">Dropping images while in HTML mode is not allowed</string>
+ <string name="editor_dropped_unsupported_files">Warning: not all dropped items are supported!</string>
+</resources>
diff --git a/libs/editor/WordPressEditor/src/main/res/values/styles.xml b/libs/editor/WordPressEditor/src/main/res/values/styles.xml
new file mode 100755
index 000000000..3e99e0b88
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values/styles.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <style name="LegacyToggleButton">
+ <item name="android:minWidth">@dimen/legacy_format_bar_height</item>
+ <item name="android:minHeight">@dimen/legacy_format_bar_height</item>
+ <item name="android:textOn">""</item>
+ <item name="android:textOff">""</item>
+ </style>
+
+ <style name="FormatBarButton">
+ <item name="android:minWidth">@dimen/format_bar_height</item>
+ <item name="android:minHeight">@dimen/format_bar_height</item>
+ <item name="android:textOn">""</item>
+ <item name="android:textOff">""</item>
+ </style>
+
+ <style name="FormatBarButtonTablet" parent="FormatBarButton">
+ <item name="android:layout_marginRight">@dimen/format_bar_button_margin_tablet</item>
+ <item name="android:layout_marginLeft">@dimen/format_bar_button_margin_tablet</item>
+ </style>
+
+ <style name="FormatBarHtmlButton" parent="FormatBarButton">
+ <item name="android:layout_marginLeft">@dimen/format_bar_html_button_left_margin</item>
+ </style>
+
+ <style name="Divider">
+ <item name="android:background">@color/format_bar_divider</item>
+ </style>
+
+ <style name="DividerSourceView">
+ <item name="android:background">@color/sourceview_separator</item>
+ </style>
+
+ <style name="FormatBarTabletGroup">
+ <item name="android:layout_marginLeft">@dimen/format_bar_button_group_tablet</item>
+ <item name="android:layout_marginRight">@dimen/format_bar_button_group_tablet</item>
+ </style>
+
+ <style name="ImageOptionsDialogLabel">
+ <item name="android:textColor">@color/image_options_label</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:paddingLeft">@dimen/image_settings_dialog_label_indent</item>
+ <item name="android:layout_marginBottom">@dimen/image_settings_dialog_label_margin_bottom</item>
+ </style>
+
+ <style name="ImageOptionsDialogInput">
+ <item name="android:layout_marginBottom">@dimen/image_settings_dialog_input_margin_bottom</item>
+ </style>
+</resources>
diff --git a/libs/editor/WordPressEditor/src/main/res/values/wp_colors.xml b/libs/editor/WordPressEditor/src/main/res/values/wp_colors.xml
new file mode 100644
index 000000000..15caebf9f
--- /dev/null
+++ b/libs/editor/WordPressEditor/src/main/res/values/wp_colors.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="wp_gray">#87a6bc</color>
+ <color name="wp_gray_lighten_10">#a8bece</color>
+ <color name="wp_gray_lighten_20">#c8d7e1</color>
+ <color name="wp_gray_lighten_30">#e9eff3</color>
+ <color name="wp_gray_light">#f3f6f8</color>
+
+ <color name="wp_blue">#0087be</color>
+ <color name="wp_blue_light">#78dcfa</color>
+ <color name="wp_blue_medium">#00aadc</color>
+ <color name="wp_blue_dark">#005082</color>
+</resources> \ No newline at end of file
diff --git a/libs/editor/build.gradle b/libs/editor/build.gradle
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/libs/editor/build.gradle
diff --git a/libs/editor/example/build.gradle b/libs/editor/example/build.gradle
new file mode 100644
index 000000000..d45489a35
--- /dev/null
+++ b/libs/editor/example/build.gradle
@@ -0,0 +1,56 @@
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.2.0'
+ }
+}
+
+repositories {
+ jcenter()
+}
+
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 24
+ buildToolsVersion "24.0.2"
+
+ defaultConfig {
+ applicationId "org.wordpress.editorexample"
+ minSdkVersion 16
+ targetSdkVersion 24
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ compile project(":WordPressEditor");
+
+ // Test libraries
+ testCompile 'junit:junit:4.11'
+ testCompile 'org.mockito:mockito-core:1.10.19'
+ testCompile 'org.robolectric:robolectric:3.0'
+
+ // Workaround for IDE bug
+ // http://stackoverflow.com/questions/22246183/android-studio-doesnt-recognize-espresso-classes
+ provided 'junit:junit:4.11'
+ provided 'org.mockito:mockito-core:1.10.19'
+}
+
+//
+// Testing
+//
+
+android.testOptions.unitTests.all {
+ include '**/*Test.class'
+ exclude '**/ApplicationTest.class'
+} \ No newline at end of file
diff --git a/libs/editor/example/proguard-rules.pro b/libs/editor/example/proguard-rules.pro
new file mode 100644
index 000000000..d8d549daa
--- /dev/null
+++ b/libs/editor/example/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/max/work/android-sdk-mac/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/libs/editor/example/src/main/AndroidManifest.xml b/libs/editor/example/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..d6b56a50a
--- /dev/null
+++ b/libs/editor/example/src/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.wordpress.android.editor.example" >
+
+ <application
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme" >
+ <activity
+ android:name=".MainExampleActivity"
+ android:label="@string/title_activity_example">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ <activity
+ android:name=".EditorExampleActivity"
+ android:configChanges="orientation|keyboardHidden|screenSize">
+ </activity>
+ </application>
+
+</manifest>
diff --git a/libs/editor/example/src/main/assets/example/cowboy-cat.jpg b/libs/editor/example/src/main/assets/example/cowboy-cat.jpg
new file mode 100644
index 000000000..77f930b99
--- /dev/null
+++ b/libs/editor/example/src/main/assets/example/cowboy-cat.jpg
Binary files differ
diff --git a/libs/editor/example/src/main/assets/example/example-content.html b/libs/editor/example/src/main/assets/example/example-content.html
new file mode 100644
index 000000000..5f35f4826
--- /dev/null
+++ b/libs/editor/example/src/main/assets/example/example-content.html
@@ -0,0 +1,58 @@
+<p>
+I'm a test post.
+
+<strong>Bold text</strong>
+
+<em>Italic text</em>
+
+<a href="http://www.wordpress.com">I'm a link!</a>
+
+<!--more c'est mort -->
+
+<del datetime="2014-05-19T20:55:58+00:00">Strikethrough</del>
+
+Moar Text.
+</p>
+<p>
+Code:
+<code>
+ 10 PRINT "HOWDY WORLD"
+ 20 GOTO 10
+</code>
+</p>
+<p>
+Unordered List:
+<ul>
+ <li>One</li>
+ <li>Two</li>
+ <li>Three</li>
+</ul>
+</p>
+<p>
+Ordered List:
+<ol>
+ <li>One</li>
+ <li>Two</li>
+ <li>Three</li>
+</ol>
+</p>
+<p>
+Master cleanse Intelligentsia butcher Brooklyn Tumblr. Etsy lo-fi Marfa bicycle rights. Intelligentsia Helvetica fanny pack, normcore Odd Future fixie brunch mustache aesthetic kitsch artisan cardigan mlkshk. Pour-over hashtag selfies pug Tumblr mlkshk. Food truck small batch McSweeney's trust fund, Intelligentsia bitters Brooklyn twee meh authentic. Normcore distillery American Apparel single-origin coffee artisan try-hard, stumptown XOXO tote bag fanny pack Blue Bottle Shoreditch food truck Banksy. Church-key American Apparel Blue Bottle, swag try-hard Odd Future mustache chia iPhone.
+</p>
+<p>
+Block Quote:
+<blockquote>Kale chips Schlitz forage irony, kogi Tumblr Carles chillwave Etsy pug photo booth YOLO biodiesel tote bag actually. PBR Portland yr pickled, bespoke meggings selvage letterpress kitsch plaid before they sold out put a bird on it you probably haven't heard of them. Yr master cleanse slow-carb crucifix, sustainable keytar Helvetica Tumblr High Life mumblecore narwhal cornhole deep v craft beer. Portland forage hashtag locavore, before they sold out put a bird on it irony hella. Godard kale chips street art tote bag cardigan. Church-key next level seitan keytar meggings Portland. Keffiyeh flexitarian post-ironic drinking vinegar wayfarers.</blockquote>
+</p>
+<p>
+Image:<br/><br/>
+
+<img src="example/cowboy-cat.jpg" alt="Cowboy Cat" />
+</p>
+<p>
+Fanny pack Odd Future Intelligentsia lo-fi semiotics whatever. Selvage keffiyeh mustache sustainable ethnic, chambray mumblecore McSweeney's biodiesel Pitchfork four loko disrupt post-ironic art party American Apparel. Kitsch umami beard salvia, Vice before they sold out vegan tousled lomo jean shorts pickled PBR&amp;B. Tousled Wes Anderson Shoreditch flannel, 90's XOXO quinoa whatever mumblecore cliche Truffaut stumptown. Photo booth crucifix plaid Brooklyn. Authentic letterpress PBR&amp;B, sustainable VHS master cleanse ethnic High Life. Messenger bag umami pug flannel.
+</p>
+
+<!--nextpage-->
+
+Wow, such sample, much text.
+
diff --git a/libs/editor/example/src/main/java/org/wordpress/android/editor/example/EditorExampleActivity.java b/libs/editor/example/src/main/java/org/wordpress/android/editor/example/EditorExampleActivity.java
new file mode 100644
index 000000000..8723f708e
--- /dev/null
+++ b/libs/editor/example/src/main/java/org/wordpress/android/editor/example/EditorExampleActivity.java
@@ -0,0 +1,348 @@
+package org.wordpress.android.editor.example;
+
+import android.app.Fragment;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.ContextMenu;
+import android.view.DragEvent;
+import android.view.MenuItem;
+import android.view.View;
+
+import org.wordpress.android.editor.EditorFragmentAbstract;
+import org.wordpress.android.editor.EditorFragmentAbstract.EditorDragAndDropListener;
+import org.wordpress.android.editor.EditorFragmentAbstract.EditorFragmentListener;
+import org.wordpress.android.editor.EditorFragmentAbstract.TrackableEvent;
+import org.wordpress.android.editor.EditorMediaUploadListener;
+import org.wordpress.android.editor.ImageSettingsDialogFragment;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.helpers.MediaFile;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+public class EditorExampleActivity extends AppCompatActivity implements EditorFragmentListener,
+ EditorDragAndDropListener {
+ public static final String EDITOR_PARAM = "EDITOR_PARAM";
+ public static final String TITLE_PARAM = "TITLE_PARAM";
+ public static final String CONTENT_PARAM = "CONTENT_PARAM";
+ public static final String DRAFT_PARAM = "DRAFT_PARAM";
+ public static final String TITLE_PLACEHOLDER_PARAM = "TITLE_PLACEHOLDER_PARAM";
+ public static final String CONTENT_PLACEHOLDER_PARAM = "CONTENT_PLACEHOLDER_PARAM";
+ public static final int USE_NEW_EDITOR = 1;
+ public static final int USE_LEGACY_EDITOR = 2;
+
+ public static final int ADD_MEDIA_ACTIVITY_REQUEST_CODE = 1111;
+ public static final int ADD_MEDIA_FAIL_ACTIVITY_REQUEST_CODE = 1112;
+ public static final int ADD_MEDIA_SLOW_NETWORK_REQUEST_CODE = 1113;
+
+ public static final String MEDIA_REMOTE_ID_SAMPLE = "123";
+
+ private static final int SELECT_IMAGE_MENU_POSITION = 0;
+ private static final int SELECT_IMAGE_FAIL_MENU_POSITION = 1;
+ private static final int SELECT_VIDEO_MENU_POSITION = 2;
+ private static final int SELECT_VIDEO_FAIL_MENU_POSITION = 3;
+ private static final int SELECT_IMAGE_SLOW_MENU_POSITION = 4;
+
+ private EditorFragmentAbstract mEditorFragment;
+
+ private Map<String, String> mFailedUploads;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (getIntent().getIntExtra(EDITOR_PARAM, USE_NEW_EDITOR) == USE_NEW_EDITOR) {
+ ToastUtils.showToast(this, R.string.starting_new_editor);
+ setContentView(R.layout.activity_new_editor);
+ } else {
+ ToastUtils.showToast(this, R.string.starting_legacy_editor);
+ setContentView(R.layout.activity_legacy_editor);
+ }
+
+ mFailedUploads = new HashMap<>();
+ }
+
+ @Override
+ public void onAttachFragment(Fragment fragment) {
+ super.onAttachFragment(fragment);
+ if (fragment instanceof EditorFragmentAbstract) {
+ mEditorFragment = (EditorFragmentAbstract) fragment;
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ Fragment fragment = getFragmentManager()
+ .findFragmentByTag(ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_TAG);
+ if (fragment != null && fragment.isVisible()) {
+ ((ImageSettingsDialogFragment) fragment).dismissFragment();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ menu.add(0, SELECT_IMAGE_MENU_POSITION, 0, getString(R.string.select_image));
+ menu.add(0, SELECT_IMAGE_FAIL_MENU_POSITION, 0, getString(R.string.select_image_fail));
+ menu.add(0, SELECT_VIDEO_MENU_POSITION, 0, getString(R.string.select_video));
+ menu.add(0, SELECT_VIDEO_FAIL_MENU_POSITION, 0, getString(R.string.select_video_fail));
+ menu.add(0, SELECT_IMAGE_SLOW_MENU_POSITION, 0, getString(R.string.select_image_slow_network));
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ Intent intent = new Intent(Intent.ACTION_PICK);
+
+ switch (item.getItemId()) {
+ case SELECT_IMAGE_MENU_POSITION:
+ intent.setType("image/*");
+ intent.setAction(Intent.ACTION_GET_CONTENT);
+ intent = Intent.createChooser(intent, getString(R.string.select_image));
+
+ startActivityForResult(intent, ADD_MEDIA_ACTIVITY_REQUEST_CODE);
+ return true;
+ case SELECT_IMAGE_FAIL_MENU_POSITION:
+ intent.setType("image/*");
+ intent.setAction(Intent.ACTION_GET_CONTENT);
+ intent = Intent.createChooser(intent, getString(R.string.select_image_fail));
+
+ startActivityForResult(intent, ADD_MEDIA_FAIL_ACTIVITY_REQUEST_CODE);
+ return true;
+ case SELECT_VIDEO_MENU_POSITION:
+ intent.setType("video/*");
+ intent.setAction(Intent.ACTION_GET_CONTENT);
+ intent = Intent.createChooser(intent, getString(R.string.select_video));
+
+ startActivityForResult(intent, ADD_MEDIA_ACTIVITY_REQUEST_CODE);
+ return true;
+ case SELECT_VIDEO_FAIL_MENU_POSITION:
+ intent.setType("video/*");
+ intent.setAction(Intent.ACTION_GET_CONTENT);
+ intent = Intent.createChooser(intent, getString(R.string.select_video_fail));
+
+ startActivityForResult(intent, ADD_MEDIA_FAIL_ACTIVITY_REQUEST_CODE);
+ return true;
+ case SELECT_IMAGE_SLOW_MENU_POSITION:
+ intent.setType("image/*");
+ intent.setAction(Intent.ACTION_GET_CONTENT);
+ intent = Intent.createChooser(intent, getString(R.string.select_image_slow_network));
+
+ startActivityForResult(intent, ADD_MEDIA_SLOW_NETWORK_REQUEST_CODE);
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (data == null) {
+ return;
+ }
+
+ Uri mediaUri = data.getData();
+
+ MediaFile mediaFile = new MediaFile();
+ String mediaId = String.valueOf(System.currentTimeMillis());
+ mediaFile.setMediaId(mediaId);
+ mediaFile.setVideo(mediaUri.toString().contains("video"));
+
+ switch (requestCode) {
+ case ADD_MEDIA_ACTIVITY_REQUEST_CODE:
+ mEditorFragment.appendMediaFile(mediaFile, mediaUri.toString(), null);
+
+ if (mEditorFragment instanceof EditorMediaUploadListener) {
+ simulateFileUpload(mediaId, mediaUri.toString());
+ }
+ break;
+ case ADD_MEDIA_FAIL_ACTIVITY_REQUEST_CODE:
+ mEditorFragment.appendMediaFile(mediaFile, mediaUri.toString(), null);
+
+ if (mEditorFragment instanceof EditorMediaUploadListener) {
+ simulateFileUploadFail(mediaId, mediaUri.toString());
+ }
+ break;
+ case ADD_MEDIA_SLOW_NETWORK_REQUEST_CODE:
+ mEditorFragment.appendMediaFile(mediaFile, mediaUri.toString(), null);
+
+ if (mEditorFragment instanceof EditorMediaUploadListener) {
+ simulateSlowFileUpload(mediaId, mediaUri.toString());
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void onSettingsClicked() {
+ // TODO
+ }
+
+ @Override
+ public void onAddMediaClicked() {
+ // TODO
+ }
+
+ @Override
+ public void onMediaRetryClicked(String mediaId) {
+ if (mFailedUploads.containsKey(mediaId)) {
+ simulateFileUpload(mediaId, mFailedUploads.get(mediaId));
+ }
+ }
+
+ @Override
+ public void onMediaUploadCancelClicked(String mediaId, boolean delete) {
+
+ }
+
+ @Override
+ public void onFeaturedImageChanged(long mediaId) {
+
+ }
+
+ @Override
+ public void onVideoPressInfoRequested(String videoId) {
+
+ }
+
+ @Override
+ public String onAuthHeaderRequested(String url) {
+ return "";
+ }
+
+ @Override
+ public void onEditorFragmentInitialized() {
+ // arbitrary setup
+ mEditorFragment.setFeaturedImageSupported(true);
+ mEditorFragment.setBlogSettingMaxImageWidth("600");
+ mEditorFragment.setDebugModeEnabled(true);
+
+ // get title and content and draft switch
+ String title = getIntent().getStringExtra(TITLE_PARAM);
+ String content = getIntent().getStringExtra(CONTENT_PARAM);
+ boolean isLocalDraft = getIntent().getBooleanExtra(DRAFT_PARAM, true);
+ mEditorFragment.setTitle(title);
+ mEditorFragment.setContent(content);
+ mEditorFragment.setTitlePlaceholder(getIntent().getStringExtra(TITLE_PLACEHOLDER_PARAM));
+ mEditorFragment.setContentPlaceholder(getIntent().getStringExtra(CONTENT_PLACEHOLDER_PARAM));
+ mEditorFragment.setLocalDraft(isLocalDraft);
+ }
+
+ @Override
+ public void saveMediaFile(MediaFile mediaFile) {
+ // TODO
+ }
+
+ @Override
+ public void onTrackableEvent(TrackableEvent event) {
+ AppLog.d(T.EDITOR, "Trackable event: " + event);
+ }
+
+ private void simulateFileUpload(final String mediaId, final String mediaUrl) {
+ Thread thread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ float count = (float) 0.1;
+ while (count < 1.1) {
+ sleep(500);
+
+ ((EditorMediaUploadListener) mEditorFragment).onMediaUploadProgress(mediaId, count);
+
+ count += 0.1;
+ }
+
+ MediaFile mediaFile = new MediaFile();
+ mediaFile.setMediaId(MEDIA_REMOTE_ID_SAMPLE);
+ mediaFile.setFileURL(mediaUrl);
+
+ ((EditorMediaUploadListener) mEditorFragment).onMediaUploadSucceeded(mediaId, mediaFile);
+
+ if (mFailedUploads.containsKey(mediaId)) {
+ mFailedUploads.remove(mediaId);
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ };
+
+ thread.start();
+ }
+
+ private void simulateFileUploadFail(final String mediaId, final String mediaUrl) {
+ Thread thread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ float count = (float) 0.1;
+ while (count < 0.6) {
+ sleep(500);
+
+ ((EditorMediaUploadListener) mEditorFragment).onMediaUploadProgress(mediaId, count);
+
+ count += 0.1;
+ }
+
+ ((EditorMediaUploadListener) mEditorFragment).onMediaUploadFailed(mediaId,
+ getString(R.string.tap_to_try_again));
+
+ mFailedUploads.put(mediaId, mediaUrl);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ };
+
+ thread.start();
+ }
+
+ private void simulateSlowFileUpload(final String mediaId, final String mediaUrl) {
+ Thread thread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ sleep(5000);
+ float count = (float) 0.1;
+ while (count < 1.1) {
+ sleep(2000);
+
+ ((EditorMediaUploadListener) mEditorFragment).onMediaUploadProgress(mediaId, count);
+
+ count += 0.1;
+ }
+
+ MediaFile mediaFile = new MediaFile();
+ mediaFile.setMediaId(MEDIA_REMOTE_ID_SAMPLE);
+ mediaFile.setFileURL(mediaUrl);
+
+ ((EditorMediaUploadListener) mEditorFragment).onMediaUploadSucceeded(mediaId, mediaFile);
+
+ if (mFailedUploads.containsKey(mediaId)) {
+ mFailedUploads.remove(mediaId);
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ };
+
+ thread.start();
+ }
+
+ @Override
+ public void onMediaDropped(ArrayList<Uri> mediaUri) {
+ // TODO
+ }
+
+ @Override
+ public void onRequestDragAndDropPermissions(DragEvent dragEvent) {
+ // TODO
+ }
+}
diff --git a/libs/editor/example/src/main/java/org/wordpress/android/editor/example/MainExampleActivity.java b/libs/editor/example/src/main/java/org/wordpress/android/editor/example/MainExampleActivity.java
new file mode 100644
index 000000000..52b522d38
--- /dev/null
+++ b/libs/editor/example/src/main/java/org/wordpress/android/editor/example/MainExampleActivity.java
@@ -0,0 +1,86 @@
+package org.wordpress.android.editor.example;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+
+import org.wordpress.android.editor.Utils;
+import org.wordpress.android.editor.example.EditorExampleActivity;
+
+public class MainExampleActivity extends AppCompatActivity {
+ private Activity mActivity;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mActivity = this;
+ setContentView(R.layout.activity_example);
+
+ Button newEditorPost1 = (Button) findViewById(R.id.new_editor_post_1);
+ newEditorPost1.setOnClickListener(new OnClickListener() {
+ @Override public void onClick(View v) {
+ Intent intent = new Intent(MainExampleActivity.this, EditorExampleActivity.class);
+ Bundle bundle = new Bundle();
+ bundle.putString(EditorExampleActivity.TITLE_PARAM, getString(R.string.example_post_visual_title));
+ bundle.putString(EditorExampleActivity.CONTENT_PARAM, Utils.getHtmlFromFile(mActivity,
+ "example/example-content.html"));
+ bundle.putString(EditorExampleActivity.TITLE_PLACEHOLDER_PARAM,
+ getString(R.string.example_post_title_placeholder));
+ bundle.putString(EditorExampleActivity.CONTENT_PLACEHOLDER_PARAM,
+ getString(R.string.example_post_content_placeholder));
+ bundle.putInt(EditorExampleActivity.EDITOR_PARAM, EditorExampleActivity.USE_NEW_EDITOR);
+ intent.putExtras(bundle);
+ startActivity(intent);
+ }
+ });
+
+ Button newEditorPostEmpty = (Button) findViewById(R.id.new_editor_post_empty);
+ newEditorPostEmpty.setOnClickListener(new OnClickListener() {
+ @Override public void onClick(View v) {
+ Intent intent = new Intent(MainExampleActivity.this, EditorExampleActivity.class);
+ Bundle bundle = new Bundle();
+ bundle.putString(EditorExampleActivity.TITLE_PARAM, "");
+ bundle.putString(EditorExampleActivity.CONTENT_PARAM, "");
+ bundle.putString(EditorExampleActivity.TITLE_PLACEHOLDER_PARAM,
+ getString(R.string.example_post_title_placeholder));
+ bundle.putString(EditorExampleActivity.CONTENT_PLACEHOLDER_PARAM,
+ getString(R.string.example_post_content_placeholder));
+ bundle.putInt(EditorExampleActivity.EDITOR_PARAM, EditorExampleActivity.USE_NEW_EDITOR);
+ intent.putExtras(bundle);
+ startActivity(intent);
+ }
+ });
+
+ Button legacyEditorPost1Local = (Button) findViewById(R.id.legacy_editor_post_1_local);
+ legacyEditorPost1Local.setOnClickListener(new OnClickListener() {
+ @Override public void onClick(View v) {
+ Intent intent = new Intent(MainExampleActivity.this, EditorExampleActivity.class);
+ Bundle bundle = new Bundle();
+ bundle.putString(EditorExampleActivity.TITLE_PARAM, getString(R.string.example_post_1_title));
+ bundle.putString(EditorExampleActivity.CONTENT_PARAM, getString(R.string.example_post_1_content));
+ bundle.putInt(EditorExampleActivity.EDITOR_PARAM, EditorExampleActivity.USE_LEGACY_EDITOR);
+ bundle.putBoolean(EditorExampleActivity.DRAFT_PARAM, true);
+ intent.putExtras(bundle);
+ startActivity(intent);
+ }
+ });
+
+ Button legacyEditorPost1Remote = (Button) findViewById(R.id.legacy_editor_post_1_remote);
+ legacyEditorPost1Remote.setOnClickListener(new OnClickListener() {
+ @Override public void onClick(View v) {
+ Intent intent = new Intent(MainExampleActivity.this, EditorExampleActivity.class);
+ Bundle bundle = new Bundle();
+ bundle.putString(EditorExampleActivity.TITLE_PARAM, getString(R.string.example_post_1_title));
+ bundle.putString(EditorExampleActivity.CONTENT_PARAM, getString(R.string.example_post_1_content));
+ bundle.putInt(EditorExampleActivity.EDITOR_PARAM, EditorExampleActivity.USE_LEGACY_EDITOR);
+ bundle.putBoolean(EditorExampleActivity.DRAFT_PARAM, false);
+ intent.putExtras(bundle);
+ startActivity(intent);
+ }
+ });
+ }
+}
diff --git a/libs/editor/example/src/main/res/layout/activity_example.xml b/libs/editor/example/src/main/res/layout/activity_example.xml
new file mode 100644
index 000000000..b94cea8ab
--- /dev/null
+++ b/libs/editor/example/src/main/res/layout/activity_example.xml
@@ -0,0 +1,44 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context="org.wordpress.android.editor.example.MainExampleActivity">
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="New Editor - Post 1"
+ android:id="@+id/new_editor_post_1"
+ android:layout_alignParentTop="true"
+ android:layout_marginTop="32dp"
+ android:layout_centerHorizontal="true"/>
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="New Editor - Empty Post"
+ android:id="@+id/new_editor_post_empty"
+ android:layout_marginTop="32dp"
+ android:layout_centerHorizontal="true"
+ android:layout_below="@id/new_editor_post_1"/>
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Legacy Editor - Post 1 - Local Draft"
+ android:id="@+id/legacy_editor_post_1_local"
+ android:layout_marginTop="32dp"
+ android:layout_centerHorizontal="true"
+ android:layout_below="@id/new_editor_post_empty"/>
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Legacy Editor - Post 1 - Remote"
+ android:id="@+id/legacy_editor_post_1_remote"
+ android:layout_marginTop="32dp"
+ android:layout_centerHorizontal="true"
+ android:layout_below="@id/legacy_editor_post_1_local"/>
+
+
+</RelativeLayout>
diff --git a/libs/editor/example/src/main/res/layout/activity_legacy_editor.xml b/libs/editor/example/src/main/res/layout/activity_legacy_editor.xml
new file mode 100644
index 000000000..24756188d
--- /dev/null
+++ b/libs/editor/example/src/main/res/layout/activity_legacy_editor.xml
@@ -0,0 +1,13 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".EditorActivity">
+
+ <fragment
+ android:id="@+id/postEditor"
+ android:name="org.wordpress.android.editor.LegacyEditorFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+</RelativeLayout>
diff --git a/libs/editor/example/src/main/res/layout/activity_new_editor.xml b/libs/editor/example/src/main/res/layout/activity_new_editor.xml
new file mode 100644
index 000000000..478e491e5
--- /dev/null
+++ b/libs/editor/example/src/main/res/layout/activity_new_editor.xml
@@ -0,0 +1,13 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".EditorActivity">
+
+ <fragment
+ android:id="@+id/postEditor"
+ android:name="org.wordpress.android.editor.EditorFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+</RelativeLayout>
diff --git a/libs/editor/example/src/main/res/mipmap-hdpi/ic_launcher.png b/libs/editor/example/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..cde69bccc
--- /dev/null
+++ b/libs/editor/example/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/libs/editor/example/src/main/res/mipmap-mdpi/ic_launcher.png b/libs/editor/example/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..c133a0cbd
--- /dev/null
+++ b/libs/editor/example/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/libs/editor/example/src/main/res/mipmap-xhdpi/ic_launcher.png b/libs/editor/example/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..bfa42f0e7
--- /dev/null
+++ b/libs/editor/example/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/libs/editor/example/src/main/res/mipmap-xxhdpi/ic_launcher.png b/libs/editor/example/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..324e72cdd
--- /dev/null
+++ b/libs/editor/example/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/libs/editor/example/src/main/res/values/colors.xml b/libs/editor/example/src/main/res/values/colors.xml
new file mode 100644
index 000000000..e8782a95d
--- /dev/null
+++ b/libs/editor/example/src/main/res/values/colors.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="color_control_activated">@color/wp_blue_light</color>
+</resources> \ No newline at end of file
diff --git a/libs/editor/example/src/main/res/values/strings.xml b/libs/editor/example/src/main/res/values/strings.xml
new file mode 100644
index 000000000..c8ac8b7dd
--- /dev/null
+++ b/libs/editor/example/src/main/res/values/strings.xml
@@ -0,0 +1,21 @@
+<resources>
+ <string name="app_name">Editor Example</string>
+ <string name="title_activity_example">Editor Example</string>
+ <string name="starting_legacy_editor">Starting legacy editor</string>
+ <string name="starting_new_editor">Starting new editor</string>
+ <string name="example_post_visual_title">I\'m editing a post!</string>
+ <string name="example_post_title_placeholder">Post title</string>
+ <string name="example_post_content_placeholder">Share your story here…</string>
+ <string name="example_post_1_title">Post 1</string>
+ <string name="example_post_1_content">Post 1 Content:\nBest post ever!</string>
+ <string name="example_post_2_title">Post 2</string>
+ <string name="example_post_2_content">
+ <![CDATA[<p>Post 2 Content</p><blockquote>Quoted text</blockquote><br/>]]>
+ </string>
+
+ <string name="select_image">Select an image</string>
+ <string name="select_image_fail">Select an image (failure demo)</string>
+ <string name="select_video">Select a video</string>
+ <string name="select_video_fail">Select a video (failure demo)</string>
+ <string name="select_image_slow_network">Select an image (slow network demo)</string>
+</resources>
diff --git a/libs/editor/example/src/main/res/values/styles.xml b/libs/editor/example/src/main/res/values/styles.xml
new file mode 100644
index 000000000..1ef1ba72e
--- /dev/null
+++ b/libs/editor/example/src/main/res/values/styles.xml
@@ -0,0 +1,9 @@
+<resources>
+
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+ <!-- Customize your theme here. -->
+ <item name="colorControlActivated">@color/color_control_activated</item>
+ </style>
+
+</resources>
diff --git a/libs/editor/example/src/test/java/org/wordpress/android/editor/ApplicationTest.java b/libs/editor/example/src/test/java/org/wordpress/android/editor/ApplicationTest.java
new file mode 100644
index 000000000..d2db16a8c
--- /dev/null
+++ b/libs/editor/example/src/test/java/org/wordpress/android/editor/ApplicationTest.java
@@ -0,0 +1,13 @@
+package org.wordpress.android.editor;
+
+import android.app.Application;
+import android.test.ApplicationTestCase;
+
+/**
+ * <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
+ */
+public class ApplicationTest extends ApplicationTestCase<Application> {
+ public ApplicationTest() {
+ super(Application.class);
+ }
+}
diff --git a/libs/editor/example/src/test/java/org/wordpress/android/editor/EditorFragmentAbstractTest.java b/libs/editor/example/src/test/java/org/wordpress/android/editor/EditorFragmentAbstractTest.java
new file mode 100644
index 000000000..d9e045b29
--- /dev/null
+++ b/libs/editor/example/src/test/java/org/wordpress/android/editor/EditorFragmentAbstractTest.java
@@ -0,0 +1,112 @@
+package org.wordpress.android.editor;
+
+import android.app.Activity;
+import android.text.Spanned;
+
+import com.android.volley.toolbox.ImageLoader;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.wordpress.android.util.helpers.MediaFile;
+import org.wordpress.android.util.helpers.MediaGallery;
+
+@Config(sdk = 18)
+@RunWith(RobolectricTestRunner.class)
+public class EditorFragmentAbstractTest {
+ @Test
+ public void testActivityMustImplementEditorFragmentListener() {
+ // Host Activity must implement EditorFragmentListener, exception expected if not
+ boolean didPassTest = false;
+ Activity hostActivity = Robolectric.buildActivity(Activity.class).create().get();
+ EditorFragmentAbstract testFragment = new DefaultEditorFragment();
+
+ try {
+ testFragment.onAttach(hostActivity);
+ } catch (ClassCastException classCastException) {
+ didPassTest = true;
+ }
+
+ Assert.assertTrue(didPassTest);
+ }
+
+ @Test
+ public void testOnBackPressReturnsFalseByDefault() {
+ // The default behavior of onBackPressed should return false
+ Assert.assertFalse(new DefaultEditorFragment().onBackPressed());
+ }
+
+ /**
+ * Used to test default behavior of non-abstract methods.
+ */
+ public static class DefaultEditorFragment extends EditorFragmentAbstract {
+ @Override
+ public void setTitle(CharSequence text) {
+ }
+
+ @Override
+ public void setContent(CharSequence text) {
+ }
+
+ @Override
+ public CharSequence getTitle() {
+ return null;
+ }
+
+ @Override
+ public CharSequence getContent() {
+ return null;
+ }
+
+ @Override
+ public void appendMediaFile(MediaFile mediaFile, String imageUrl, ImageLoader imageLoader) {
+ }
+
+ @Override
+ public void appendGallery(MediaGallery mediaGallery) {
+ }
+
+ @Override
+ public void setUrlForVideoPressId(String videoPressId, String url, String posterUrl) {
+
+ }
+
+ @Override
+ public boolean isUploadingMedia() {
+ return false;
+ }
+
+ @Override
+ public boolean isActionInProgress() {
+ return false;
+ }
+
+ @Override
+ public boolean hasFailedMediaUploads() {
+ return false;
+ }
+
+ @Override
+ public void removeAllFailedMediaUploads() {
+
+ }
+
+ @Override
+ public void setTitlePlaceholder(CharSequence text) {
+
+ }
+
+ @Override
+ public void setContentPlaceholder(CharSequence text) {
+
+ }
+
+ @Override
+ public Spanned getSpannedContent() {
+ return null;
+ }
+ }
+}
diff --git a/libs/editor/example/src/test/java/org/wordpress/android/editor/HtmlStyleTextWatcherTest.java b/libs/editor/example/src/test/java/org/wordpress/android/editor/HtmlStyleTextWatcherTest.java
new file mode 100644
index 000000000..beaabe31c
--- /dev/null
+++ b/libs/editor/example/src/test/java/org/wordpress/android/editor/HtmlStyleTextWatcherTest.java
@@ -0,0 +1,496 @@
+package org.wordpress.android.editor;
+
+import android.text.Editable;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StyleSpan;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import static org.junit.Assert.assertEquals;
+
+@Config(sdk = 18)
+@RunWith(RobolectricTestRunner.class)
+public class HtmlStyleTextWatcherTest {
+
+ private HtmlStyleTextWatcherForTests mWatcher;
+ private Editable mContent;
+ private boolean mUpdateSpansWasCalled;
+ private HtmlStyleTextWatcher.SpanRange mSpanRange;
+
+ @Before
+ public void setUp() {
+ mWatcher = new HtmlStyleTextWatcherForTests();
+ mUpdateSpansWasCalled = false;
+ }
+
+ @Test
+ public void testTypingNormalText() {
+ // -- Test typing in normal text (non-HTML) in an empty document
+ mContent = new SpannableStringBuilder("a");
+
+ mWatcher.onTextChanged(mContent, 0, 0, 1); // Typed "a"
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(false, mUpdateSpansWasCalled);
+
+ mContent = new SpannableStringBuilder("ab");
+
+ mWatcher.onTextChanged(mContent, 1, 0, 1); // Typed "b"
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(false, mUpdateSpansWasCalled);
+
+
+ // -- Test typing in normal text after exiting tags
+ mContent = new SpannableStringBuilder("text <b>bold</b> a");
+
+ mWatcher.onTextChanged(mContent, 17, 0, 1); // Typed "a"
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(false, mUpdateSpansWasCalled);
+
+
+ // -- Test typing in normal text before exiting tags
+ mContent = new SpannableStringBuilder("text a <b>bold</b>");
+
+ mWatcher.onTextChanged(mContent, 5, 0, 1); // Typed "a"
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(false, mUpdateSpansWasCalled);
+ }
+
+ @Test
+ public void testTypingInOpeningTag() {
+ // Test with several different cases of pre-existing text
+ String[] previousTextCases = new String[]{"", "plain text", "<i>",
+ "<blockquote>some existing content</blockquote> "};
+ for (String initialText : previousTextCases) {
+ int offset = initialText.length();
+ mUpdateSpansWasCalled = false;
+
+ // -- Test typing in an opening tag symbol
+ mContent = new SpannableStringBuilder(initialText + "<");
+
+ mWatcher.onTextChanged(mContent, offset, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ // No formatting should be applied/removed
+ assertEquals(false, mUpdateSpansWasCalled);
+
+
+ // -- Test typing in the tag name
+ mContent = new SpannableStringBuilder(initialText + "<b");
+
+ mWatcher.onTextChanged(mContent, offset + 1, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ // No formatting should be applied/removed
+ assertEquals(false, mUpdateSpansWasCalled);
+
+
+ // -- Test typing in a closing tag symbol
+ mContent = new SpannableStringBuilder(initialText + "<b>");
+
+ mWatcher.onTextChanged(mContent, offset + 2, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(offset, mSpanRange.getOpeningTagLoc());
+ assertEquals(offset + 3, mSpanRange.getClosingTagLoc());
+ }
+ }
+
+ @Test
+ public void testTypingInClosingTag() {
+ // Test with several different cases of pre-existing text
+ String[] previousTextCases = new String[]{"<b>stuff", "plain text <b>stuff", "<i><b>stuff",
+ "<blockquote>some existing content</blockquote> <b>stuff"};
+
+ for (String initialText : previousTextCases) {
+ int offset = initialText.length();
+ mUpdateSpansWasCalled = false;
+
+ // -- Test typing in an opening tag symbol
+ mContent = new SpannableStringBuilder(initialText + "<");
+
+ mWatcher.onTextChanged(mContent, offset, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ // No formatting should be applied/removed
+ assertEquals(false, mUpdateSpansWasCalled);
+
+
+ // -- Test typing in the closing tag slash
+ mContent = new SpannableStringBuilder(initialText + "</");
+
+ mWatcher.onTextChanged(mContent, offset + 1, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ // No formatting should be applied/removed
+ assertEquals(false, mUpdateSpansWasCalled);
+
+ // -- Test typing in the tag name
+ mContent = new SpannableStringBuilder(initialText + "</b");
+
+ mWatcher.onTextChanged(mContent, offset + 2, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ // No formatting should be applied/removed
+ assertEquals(false, mUpdateSpansWasCalled);
+
+
+ // -- Test typing in a closing tag symbol
+ mContent = new SpannableStringBuilder(initialText + "</b>");
+
+ mWatcher.onTextChanged(mContent, offset + 3, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(offset, mSpanRange.getOpeningTagLoc());
+ assertEquals(offset + 4, mSpanRange.getClosingTagLoc());
+ }
+ }
+
+ @Test
+ public void testTypingInTagWithSurroundingTags() {
+ // Spans in this case will be applied until the end of the next tag
+ // This fixes a pasting bug and might be refined later
+ // -- Test typing in the opening tag symbol
+ mContent = new SpannableStringBuilder("some <del>text</del> < <b>bold text</b>");
+
+ mWatcher.onTextChanged(mContent, 21, 0, 1); // Added lone "<"
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(21, mSpanRange.getOpeningTagLoc());
+ assertEquals(26, mSpanRange.getClosingTagLoc());
+
+
+ // -- Test typing in the tag name
+ mContent = new SpannableStringBuilder("some <del>text</del> <i <b>bold text</b>");
+
+ mWatcher.onTextChanged(mContent, 22, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(21, mSpanRange.getOpeningTagLoc());
+ assertEquals(27, mSpanRange.getClosingTagLoc());
+
+
+ // -- Test typing in the closing tag symbol
+ mContent = new SpannableStringBuilder("some <del>text</del> <i> <b>bold text</b>");
+
+ mWatcher.onTextChanged(mContent, 23, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(21, mSpanRange.getOpeningTagLoc());
+ assertEquals(28, mSpanRange.getClosingTagLoc());
+ }
+
+ @Test
+ public void testTypingInLoneClosingSymbol() {
+ // -- Test typing in an isolated closing tag symbol
+ mContent = new SpannableStringBuilder("some text >");
+
+ mWatcher.onTextChanged(mContent, 10, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ // No formatting should be applied/removed
+ assertEquals(false, mUpdateSpansWasCalled);
+
+
+ // -- Test typing in an isolated closing tag symbol with surrounding tags
+ mContent = new SpannableStringBuilder("some <b>tex>t</b>");
+
+ mWatcher.onTextChanged(mContent, 11, 0, 1); // Added lone ">"
+ mWatcher.afterTextChanged(mContent);
+
+ // The span in this case will be applied from the start of the previous tag to the end of the next tag
+ assertEquals(5, mSpanRange.getOpeningTagLoc());
+ assertEquals(17, mSpanRange.getClosingTagLoc());
+ }
+
+ @Test
+ public void testTypingInEntity() {
+ // Test with several different cases of pre-existing text
+ String[] previousTextCases = new String[]{"", "plain text", "&rho;",
+ "<blockquote>some existing content &dagger;</blockquote> "};
+ for (String initialText : previousTextCases) {
+ int offset = initialText.length();
+ mUpdateSpansWasCalled = false;
+
+ // -- Test typing in the entity's opening '&'
+ mContent = new SpannableStringBuilder(initialText + "&");
+
+ mWatcher.onTextChanged(mContent, offset, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ // No formatting should be applied/removed
+ assertEquals(false, mUpdateSpansWasCalled);
+
+
+ // -- Test typing in the entity's main text
+ mContent = new SpannableStringBuilder(initialText + "&amp");
+
+ mWatcher.onTextChanged(mContent, offset + 3, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ // No formatting should be applied/removed
+ assertEquals(false, mUpdateSpansWasCalled);
+
+
+ // -- Test typing in the entity's closing ';'
+ mContent = new SpannableStringBuilder(initialText + "&amp;");
+
+ mWatcher.onTextChanged(mContent, offset + 4, 0, 1);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(offset, mSpanRange.getOpeningTagLoc());
+ assertEquals(offset + 5, mSpanRange.getClosingTagLoc());
+ }
+ }
+
+ @Test
+ public void testAddingTagFromFormatBar() {
+ // -- Test adding a tag to an empty document
+ mContent = new SpannableStringBuilder("<b>");
+
+ mWatcher.onTextChanged(mContent, 0, 0, 3);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(0, mSpanRange.getOpeningTagLoc());
+ assertEquals(3, mSpanRange.getClosingTagLoc());
+
+
+ // -- Test adding a tag at the end of a document with text
+ mContent = new SpannableStringBuilder("stuff<b>");
+
+ mWatcher.onTextChanged(mContent, 5, 0, 3);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(5, mSpanRange.getOpeningTagLoc());
+ assertEquals(8, mSpanRange.getClosingTagLoc());
+
+
+ // -- Test adding a tag at the end of a document containing other html
+ mContent = new SpannableStringBuilder("some text <i>italics</i> <b>");
+
+ mWatcher.onTextChanged(mContent, 25, 0, 3); // Added "<b>"
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(25, mSpanRange.getOpeningTagLoc());
+ assertEquals(28, mSpanRange.getClosingTagLoc());
+
+
+ // -- Test adding a tag at the start of a document with text
+ mContent = new SpannableStringBuilder("<b>some text");
+
+ mWatcher.onTextChanged(mContent, 0, 0, 3);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(0, mSpanRange.getOpeningTagLoc());
+ assertEquals(3, mSpanRange.getClosingTagLoc());
+
+
+ // -- Test adding a tag at the start of a document containing other html
+ mContent = new SpannableStringBuilder("<b>some text <i>italics</i>");
+
+ mWatcher.onTextChanged(mContent, 0, 0, 3);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(0, mSpanRange.getOpeningTagLoc());
+ assertEquals(3, mSpanRange.getClosingTagLoc());
+
+
+ // -- Test adding a tag within another tag pair
+ mContent = new SpannableStringBuilder("<b>some <i>text</b>");
+
+ mWatcher.onTextChanged(mContent, 8, 0, 3); // Added <i>
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(8, mSpanRange.getOpeningTagLoc());
+ assertEquals(11, mSpanRange.getClosingTagLoc());
+
+
+ // -- Test adding a closing tag within another tag pair
+ mContent = new SpannableStringBuilder("<b>some <i>text</i></b>");
+
+ mWatcher.onTextChanged(mContent, 15, 0, 4); // Added "</i>"
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(15, mSpanRange.getOpeningTagLoc());
+ assertEquals(19, mSpanRange.getClosingTagLoc());
+ }
+
+ @Test
+ public void testAddingListTagsFromFormatBar() {
+ // -- Test adding a list tag to an empty document
+ mContent = new SpannableStringBuilder("<ul>\n\t<li>");
+
+ mWatcher.onTextChanged(mContent, 0, 0, 10);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(0, mSpanRange.getOpeningTagLoc());
+ assertEquals(10, mSpanRange.getClosingTagLoc());
+
+
+ // -- Test adding a closing list tag
+ mContent = new SpannableStringBuilder("<ul>\n" + //5
+ "\t<li>list item</li>\n" + //20
+ "\t<li>another list item</li>\n" + //22
+ "</ul>");
+
+ mWatcher.onTextChanged(mContent, 47, 0, 11); // Added "</li>\n</ul>"
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(47, mSpanRange.getOpeningTagLoc());
+ assertEquals(58, mSpanRange.getClosingTagLoc());
+ }
+
+ @Test
+ public void testDeletingPartsOfTag() {
+ // -- Test deleting different characters within a tag
+ mContent = new SpannableStringBuilder("<b>stuff</b>");
+
+ int deletedChar = 0;
+ mWatcher.beforeTextChanged(mContent, deletedChar, 1, 0);
+ // Deleted characters are removed from the string between beforeTextChanged() and onTextChanged()
+ mContent.delete(deletedChar, deletedChar + 1);
+ mWatcher.onTextChanged(mContent, deletedChar, 1, 0);
+ mWatcher.afterTextChanged(mContent);
+
+ // "b>" should be re-styled
+ assertEquals(0, mSpanRange.getOpeningTagLoc());
+ assertEquals(2, mSpanRange.getClosingTagLoc());
+
+ for (int i = 8; i < 12; i++) {
+ mContent = new SpannableStringBuilder("<b>stuff</b>");
+
+ mWatcher.beforeTextChanged(mContent, i, 1, 0);
+ mContent.delete(i, i + 1);
+ mWatcher.afterTextChanged(mContent);
+
+ // Style should be updated starting from the end of 'stuff'
+ assertEquals(8, mSpanRange.getOpeningTagLoc());
+ assertEquals(mContent.length(), mSpanRange.getClosingTagLoc());
+ }
+ }
+
+ @Test
+ public void testPasteTagPair() {
+ // -- Test pasting in a set of opening and closing tags at the end of the document
+ mContent = new SpannableStringBuilder("text <b></b>");
+
+ mWatcher.onTextChanged(mContent, 5, 0, 7);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(5, mSpanRange.getOpeningTagLoc());
+ assertEquals(12, mSpanRange.getClosingTagLoc());
+ }
+
+ @Test
+ public void testCutAndPasteTagPart() {
+ // -- Test cutting a tag and part of another tag from the document
+ mContent = new SpannableStringBuilder("test <b></b> <i>italics</i>");
+
+ mWatcher.beforeTextChanged(mContent, 5, 4, 0); // Deleted "<b><"
+ mContent.delete(5, 9);
+ mWatcher.onTextChanged(mContent, 5, 4, 0);
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(5, mSpanRange.getOpeningTagLoc());
+ assertEquals(8, mSpanRange.getClosingTagLoc());
+
+
+ // -- Test pasting the cut text back in
+ mContent = new SpannableStringBuilder("test <b></b> <i>italics</i>");
+ mWatcher.onTextChanged(mContent, 5, 0, 4); // Pasted "<b><" back in
+ mWatcher.afterTextChanged(mContent);
+
+ assertEquals(5, mSpanRange.getOpeningTagLoc());
+ assertEquals(12, mSpanRange.getClosingTagLoc());
+ }
+
+ @Test
+ public void testCutAndPasteTagPartReplacingText() {
+ // -- Test pasting cut text while text is selected
+ // Pasted "<b><", replacing "st " of "test "
+ mContent = new SpannableStringBuilder("test /b> <i>italics</i>");
+ mWatcher.beforeTextChanged(mContent, 2, 3, 4);
+ mContent = new SpannableStringBuilder("te<b></b> <i>italics</i>");
+ mWatcher.onTextChanged(mContent, 2, 3, 4);
+ mWatcher.afterTextChanged(mContent);
+
+ // Should re-style whole document
+ assertEquals(0, mSpanRange.getOpeningTagLoc());
+ assertEquals(mContent.length(), mSpanRange.getClosingTagLoc());
+
+
+ // -- Test pasting cut text while text is selected, case 2
+ // Pasted "i>", replacing "test "
+ mContent = new SpannableStringBuilder("<test italics</i>");
+ mWatcher.beforeTextChanged(mContent, 1, 5, 2);
+ mContent = new SpannableStringBuilder("<i>italics</i>");
+ mWatcher.onTextChanged(mContent, 1, 5, 2);
+ mWatcher.afterTextChanged(mContent);
+
+ // Should re-style whole document
+ assertEquals(0, mSpanRange.getOpeningTagLoc());
+ assertEquals(mContent.length(), mSpanRange.getClosingTagLoc());
+ }
+
+ @Test
+ public void testNoChange() {
+
+ mWatcher.beforeTextChanged("sample", 0, 0, 0);
+ mWatcher.onTextChanged("sample", 0, 0, 0);
+ mWatcher.afterTextChanged(null);
+
+ // No formatting should be applied/removed
+ assertEquals(false, mUpdateSpansWasCalled);
+ }
+
+ @Test
+ public void testUpdateSpans() {
+ // -- Test tag styling
+ HtmlStyleTextWatcher watcher = new HtmlStyleTextWatcher();
+ Spannable content = new SpannableStringBuilder("<b>stuff</b>");
+ watcher.updateSpans(content, new HtmlStyleTextWatcher.SpanRange(0, 3));
+
+ assertEquals(1, content.getSpans(0, 3, ForegroundColorSpan.class).length);
+
+ // -- Test entity styling
+ content = new SpannableStringBuilder("text &amp; more text");
+ watcher.updateSpans(content, new HtmlStyleTextWatcher.SpanRange(5, 10));
+
+ assertEquals(1, content.getSpans(5, 10, ForegroundColorSpan.class).length);
+ assertEquals(1, content.getSpans(5, 10, StyleSpan.class).length);
+ assertEquals(1, content.getSpans(5, 10, RelativeSizeSpan.class).length);
+
+ // -- Test comment styling
+ content = new SpannableStringBuilder("text <!--comment--> more text");
+ watcher.updateSpans(content, new HtmlStyleTextWatcher.SpanRange(5, 19));
+
+ assertEquals(1, content.getSpans(5, 19, ForegroundColorSpan.class).length);
+ assertEquals(1, content.getSpans(5, 19, StyleSpan.class).length);
+ assertEquals(1, content.getSpans(5, 19, RelativeSizeSpan.class).length);
+
+ content = new SpannableStringBuilder("<b>stuff</b>");
+ watcher.updateSpans(content, new HtmlStyleTextWatcher.SpanRange(0, 3));
+
+ watcher.updateSpans(content, new HtmlStyleTextWatcher.SpanRange(0, 42));
+ assertEquals(1, content.getSpans(0, 3, ForegroundColorSpan.class).length);
+
+ }
+
+ private class HtmlStyleTextWatcherForTests extends HtmlStyleTextWatcher {
+ @Override
+ protected void updateSpans(Spannable s, SpanRange spanRange) {
+ mSpanRange = spanRange;
+ mUpdateSpansWasCalled = true;
+ }
+ }
+}
diff --git a/libs/editor/example/src/test/java/org/wordpress/android/editor/HtmlStyleUtilsTest.java b/libs/editor/example/src/test/java/org/wordpress/android/editor/HtmlStyleUtilsTest.java
new file mode 100644
index 000000000..12e7793b1
--- /dev/null
+++ b/libs/editor/example/src/test/java/org/wordpress/android/editor/HtmlStyleUtilsTest.java
@@ -0,0 +1,92 @@
+package org.wordpress.android.editor;
+
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import static org.junit.Assert.assertEquals;
+
+@Config(sdk = 18)
+@RunWith(RobolectricTestRunner.class)
+public class HtmlStyleUtilsTest {
+
+ @Test
+ public void testBulkStyling() {
+ // -- Test bulk styling
+ Spannable content = new SpannableStringBuilder("text <b>bold</b> &amp; <!--a comment--> <a href=\"website\">link</a>");
+ HtmlStyleUtils.styleHtmlForDisplay(content);
+
+ assertEquals(0, content.getSpans(0, 5, CharacterStyle.class).length); // 'text '
+
+ assertEquals(1, content.getSpans(5, 8, ForegroundColorSpan.class).length); // '<b>'
+
+ assertEquals(1, content.getSpans(12, 16, ForegroundColorSpan.class).length); // '</b>'
+
+ assertEquals(1, content.getSpans(17, 22, ForegroundColorSpan.class).length); // '&amp;'
+ assertEquals(1, content.getSpans(17, 22, StyleSpan.class).length); // '&amp;'
+ assertEquals(1, content.getSpans(17, 22, RelativeSizeSpan.class).length); // '&amp;'
+
+ assertEquals(1, content.getSpans(23, 39, ForegroundColorSpan.class).length); // '<!--a comment-->'
+ assertEquals(1, content.getSpans(23, 39, StyleSpan.class).length); // '<!--a comment-->'
+ assertEquals(1, content.getSpans(23, 39, RelativeSizeSpan.class).length); // '<!--a comment-->'
+
+ assertEquals(2, content.getSpans(40, 58, ForegroundColorSpan.class).length); // '<a href="website">'
+ assertEquals(1, content.getSpans(40, 48, ForegroundColorSpan.class).length); // '<a href='
+ // Attribute span is applied on top of tag span, so there should be 2 ForegroundColorSpans present
+ assertEquals(2, content.getSpans(48, 57, ForegroundColorSpan.class).length); // '"website"'
+ assertEquals(1, content.getSpans(57, 58, ForegroundColorSpan.class).length); // '>'
+
+ assertEquals(0, content.getSpans(58, 62, CharacterStyle.class).length); // 'link'
+
+ assertEquals(1, content.getSpans(62, 66, ForegroundColorSpan.class).length); // '</a>'
+ }
+
+ @Test
+ public void testClearSpans() {
+ Spannable content = new SpannableStringBuilder("<b>text &amp;");
+
+ HtmlStyleUtils.styleHtmlForDisplay(content);
+
+ assertEquals(1, content.getSpans(0, 3, ForegroundColorSpan.class).length); // '<b>'
+
+ assertEquals(1, content.getSpans(9, 14, ForegroundColorSpan.class).length); // '&amp;'
+ assertEquals(1, content.getSpans(9, 14, StyleSpan.class).length); // '&amp;'
+ assertEquals(1, content.getSpans(9, 14, RelativeSizeSpan.class).length); // '&amp;'
+
+ HtmlStyleUtils.clearSpans(content, 9, 14);
+
+ assertEquals(1, content.getSpans(0, 3, ForegroundColorSpan.class).length);
+
+ assertEquals(0, content.getSpans(9, 14, ForegroundColorSpan.class).length);
+ assertEquals(0, content.getSpans(9, 14, StyleSpan.class).length);
+ assertEquals(0, content.getSpans(9, 14, RelativeSizeSpan.class).length);
+
+ HtmlStyleUtils.clearSpans(content, 0, 3);
+
+ assertEquals(0, content.getSpans(0, 3, ForegroundColorSpan.class).length);
+
+
+ }
+
+ @Test
+ public void testClearSpansShouldIgnoreUnderline() {
+ // clearSpans() should ignore UnderlineSpan as it's used by the system for spelling suggestions
+ Spannable content = new SpannableStringBuilder("test");
+
+ content.setSpan(new UnderlineSpan(), 0, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ HtmlStyleUtils.clearSpans(content, 0, 4);
+
+ assertEquals(1, content.getSpans(0, 4, UnderlineSpan.class).length);
+ }
+}
diff --git a/libs/editor/example/src/test/java/org/wordpress/android/editor/JsCallbackReceiverTest.java b/libs/editor/example/src/test/java/org/wordpress/android/editor/JsCallbackReceiverTest.java
new file mode 100644
index 000000000..7584824f7
--- /dev/null
+++ b/libs/editor/example/src/test/java/org/wordpress/android/editor/JsCallbackReceiverTest.java
@@ -0,0 +1,99 @@
+package org.wordpress.android.editor;
+
+import android.util.Log;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowLog;
+import org.wordpress.android.util.AppLog;
+
+import java.util.List;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static org.mockito.Mockito.mock;
+import static org.robolectric.shadows.ShadowLog.LogItem;
+
+@Config(sdk = 18)
+@RunWith(RobolectricTestRunner.class)
+public class JsCallbackReceiverTest {
+ private final static String EDITOR_LOG_TAG = "WordPress-" + AppLog.T.EDITOR.toString();
+
+ private JsCallbackReceiver mJsCallbackReceiver;
+
+ @Before
+ public void setUp() {
+ EditorFragment editorFragment = mock(EditorFragment.class);
+ mJsCallbackReceiver = new JsCallbackReceiver(editorFragment);
+ }
+
+ @Test
+ public void testCallbacksRecognized() {
+ mJsCallbackReceiver.executeCallback("callback-dom-loaded", "");
+ assertNotLogged("Unhandled callback");
+
+ mJsCallbackReceiver.executeCallback("callback-new-field", "field-name");
+ assertNotLogged("Unhandled callback");
+
+ mJsCallbackReceiver.executeCallback("callback-input", "arguments");
+ assertNotLogged("Unhandled callback");
+
+ mJsCallbackReceiver.executeCallback("callback-selection-changed", "arguments");
+ assertNotLogged("Unhandled callback");
+
+ mJsCallbackReceiver.executeCallback("callback-selection-style", "arguments");
+ assertNotLogged("Unhandled callback");
+
+ mJsCallbackReceiver.executeCallback("callback-focus-in", "");
+ assertNotLogged("Unhandled callback");
+
+ mJsCallbackReceiver.executeCallback("callback-focus-out", "");
+ assertNotLogged("Unhandled callback");
+
+ mJsCallbackReceiver.executeCallback("callback-image-replaced", "arguments");
+ assertNotLogged("Unhandled callback");
+
+ mJsCallbackReceiver.executeCallback("callback-image-tap", "arguments");
+ assertNotLogged("Unhandled callback");
+
+ mJsCallbackReceiver.executeCallback("callback-link-tap", "arguments");
+ assertNotLogged("Unhandled callback");
+
+ mJsCallbackReceiver.executeCallback("callback-log", "arguments");
+ assertNotLogged("Unhandled callback");
+
+ mJsCallbackReceiver.executeCallback("callback-response-string", "arguments");
+ assertNotLogged("Unhandled callback");
+ }
+
+ @Test
+ public void testUnknownCallbackShouldBeLogged() {
+ mJsCallbackReceiver.executeCallback("callback-does-not-exist", "content");
+ assertLogged(Log.DEBUG, EDITOR_LOG_TAG, "Unhandled callback: callback-does-not-exist:content", null);
+ }
+
+ @Test
+ public void testCallbackLog() {
+ mJsCallbackReceiver.executeCallback("callback-log", "msg=test-message");
+ assertLogged(Log.DEBUG, EDITOR_LOG_TAG, "callback-log: test-message", null);
+ }
+
+ private void assertLogged(int type, String tag, String msg, Throwable throwable) {
+ LogItem lastLog = ShadowLog.getLogs().get(0);
+ assertEquals(type, lastLog.type);
+ assertEquals(msg, lastLog.msg);
+ assertEquals(tag, lastLog.tag);
+ assertEquals(throwable, lastLog.throwable);
+ }
+
+ private void assertNotLogged(String msg) {
+ List<LogItem> logList = ShadowLog.getLogs();
+ if (!logList.isEmpty()) {
+ assertFalse(logList.get(0).msg.contains(msg));
+ ShadowLog.reset();
+ }
+ }
+}
diff --git a/libs/editor/example/src/test/java/org/wordpress/android/editor/UtilsTest.java b/libs/editor/example/src/test/java/org/wordpress/android/editor/UtilsTest.java
new file mode 100644
index 000000000..b7bc70fe8
--- /dev/null
+++ b/libs/editor/example/src/test/java/org/wordpress/android/editor/UtilsTest.java
@@ -0,0 +1,215 @@
+package org.wordpress.android.editor;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.wordpress.android.editor.Utils.buildMapFromKeyValuePairs;
+import static org.wordpress.android.editor.Utils.decodeHtml;
+import static org.wordpress.android.editor.Utils.escapeHtml;
+import static org.wordpress.android.editor.Utils.getChangeMapFromSets;
+import static org.wordpress.android.editor.Utils.splitDelimitedString;
+import static org.wordpress.android.editor.Utils.splitValuePairDelimitedString;
+import static org.wordpress.android.editor.Utils.getUrlFromClipboard;
+
+@Config(sdk = 18)
+@RunWith(RobolectricTestRunner.class)
+public class UtilsTest {
+
+ @Test
+ public void testEscapeHtml() {
+ // Test null
+ assertEquals(null, escapeHtml(null));
+ }
+
+ @Test
+ public void testDecodeHtml() {
+ // Test null
+ assertEquals(null, decodeHtml(null));
+
+ // Test normal usage
+ assertEquals("http://www.wordpress.com/", decodeHtml("http%3A%2F%2Fwww.wordpress.com%2F"));
+ }
+
+ @Test
+ public void testSplitDelimitedString() {
+ Set<String> splitString = new HashSet<>();
+
+ // Test normal usage
+ splitString.add("p");
+ splitString.add("bold");
+ splitString.add("justifyLeft");
+
+ assertEquals(splitString, splitDelimitedString("p~bold~justifyLeft", "~"));
+
+ // Test empty string
+ assertEquals(Collections.emptySet(), splitDelimitedString("", "~"));
+ }
+
+ @Test
+ public void testSplitValuePairDelimitedString() {
+ // Test usage with a URL containing the delimiter
+ Set<String> keyValueSet = new HashSet<>();
+ keyValueSet.add("url=http://www.wordpress.com/~user");
+ keyValueSet.add("title=I'm a link!");
+
+ List<String> identifiers = new ArrayList<>();
+ identifiers.add("url");
+ identifiers.add("title");
+
+ assertEquals(keyValueSet, splitValuePairDelimitedString(
+ "url=http://www.wordpress.com/~user~title=I'm a link!", "~", identifiers));
+
+ // Test usage with a matching identifier but no delimiters
+ keyValueSet.clear();
+ keyValueSet.add("url=http://www.wordpress.com/");
+
+ assertEquals(keyValueSet, splitValuePairDelimitedString("url=http://www.wordpress.com/", "~", identifiers));
+
+ // Test usage with no matching identifier and no delimiters
+ keyValueSet.clear();
+ keyValueSet.add("something=something else");
+
+ assertEquals(keyValueSet, splitValuePairDelimitedString("something=something else", "~", identifiers));
+ }
+
+ @Test
+ public void testBuildMapFromKeyValuePairs() {
+ Set<String> keyValueSet = new HashSet<>();
+ Map<String, String> expectedMap = new HashMap<>();
+
+ // Test normal usage
+ keyValueSet.add("id=test");
+ keyValueSet.add("name=example");
+
+ expectedMap.put("id", "test");
+ expectedMap.put("name", "example");
+
+ assertEquals(expectedMap, buildMapFromKeyValuePairs(keyValueSet));
+
+ // Test mixed valid and invalid entries
+ keyValueSet.clear();
+ keyValueSet.add("test");
+ keyValueSet.add("name=example");
+
+ expectedMap.clear();
+ expectedMap.put("name", "example");
+
+ assertEquals(expectedMap, buildMapFromKeyValuePairs(keyValueSet));
+
+ // Test multiple '=' (should split at the first `=` and treat the rest of them as part of the string)
+ keyValueSet.clear();
+ keyValueSet.add("id=test");
+ keyValueSet.add("contents=some text\n<a href=\"http://wordpress.com\">WordPress</a>");
+
+ expectedMap.clear();
+ expectedMap.put("id", "test");
+ expectedMap.put("contents", "some text\n<a href=\"http://wordpress.com\">WordPress</a>");
+
+ assertEquals(expectedMap, buildMapFromKeyValuePairs(keyValueSet));
+
+ // Test invalid entry
+ keyValueSet.clear();
+ keyValueSet.add("test");
+
+ assertEquals(Collections.emptyMap(), buildMapFromKeyValuePairs(keyValueSet));
+
+ // Test empty sets
+ assertEquals(Collections.emptyMap(), buildMapFromKeyValuePairs(Collections.<String>emptySet()));
+ }
+
+ @Test
+ public void testGetChangeMapFromSets() {
+ Set<String> oldSet = new HashSet<>();
+ Set<String> newSet = new HashSet<>();
+ Map<String, Boolean> expectedMap = new HashMap<>();
+
+ // Test normal usage
+ oldSet.add("p");
+ oldSet.add("bold");
+ oldSet.add("justifyLeft");
+
+ newSet.add("p");
+ newSet.add("justifyRight");
+
+ expectedMap.put("bold", false);
+ expectedMap.put("justifyLeft", false);
+ expectedMap.put("justifyRight", true);
+
+ assertEquals(expectedMap, getChangeMapFromSets(oldSet, newSet));
+
+ // Test no changes
+ oldSet.clear();
+ oldSet.add("p");
+ oldSet.add("bold");
+
+ newSet.clear();
+ newSet.add("p");
+ newSet.add("bold");
+
+ assertEquals(Collections.emptyMap(), getChangeMapFromSets(oldSet, newSet));
+
+ // Test empty sets
+ assertEquals(Collections.emptyMap(), getChangeMapFromSets(Collections.emptySet(), Collections.emptySet()));
+ }
+
+ @Test
+ public void testClipboardUrlWithNullContext() {
+ assertNull(getUrlFromClipboard(null));
+ }
+
+ @Test
+ public void testClipboardUrlWithNoClipData() {
+ assertNull(getClipboardUrlHelper(0, null));
+ }
+
+ @Test
+ public void testClipboardUrlWithNonUriData() {
+ assertNull(getClipboardUrlHelper(1, "not a URL"));
+ }
+
+ @Test
+ public void testClipboardUrlWithLocalUriData() {
+ assertNull(getClipboardUrlHelper(1, "file://test.png"));
+ }
+
+ @Test
+ public void testClipboardWithUrlData() {
+ String testUrl = "google.com";
+ assertEquals(testUrl, getClipboardUrlHelper(1, testUrl));
+ }
+
+ private String getClipboardUrlHelper(int itemCount, String clipText) {
+ ClipData.Item mockItem = mock(ClipData.Item.class);
+ when(mockItem.getText()).thenReturn(clipText);
+
+ ClipData mockPrimary = mock(ClipData.class);
+ when(mockPrimary.getItemCount()).thenReturn(itemCount);
+ when(mockPrimary.getItemAt(0)).thenReturn(mockItem);
+
+ ClipboardManager mockManager = mock(ClipboardManager.class);
+ when(mockManager.getPrimaryClip()).thenReturn(mockPrimary);
+
+ Context mockContext = mock(Context.class);
+ when(mockContext.getSystemService(Context.CLIPBOARD_SERVICE)).thenReturn(mockManager);
+
+ return getUrlFromClipboard(mockContext);
+ }
+}
diff --git a/libs/editor/example/src/test/js/test-formatter.js b/libs/editor/example/src/test/js/test-formatter.js
new file mode 100644
index 000000000..067d3c74e
--- /dev/null
+++ b/libs/editor/example/src/test/js/test-formatter.js
@@ -0,0 +1,135 @@
+var assert = require('chai').assert;
+var assetsDir = '../../../../WordPressEditor/src/main/assets';
+var underscore = require(assetsDir + '/libs/underscore-min.js');
+
+// Set up globals needed by shortcode, wpload, and wpsave
+global.window = {};
+global._ = underscore;
+global.wp = {};
+
+// wp-admin libraries
+var shortcode = require(assetsDir + '/libs/shortcode.js');
+var wpload = require(assetsDir + '/libs/wpload.js');
+var wpsave = require(assetsDir + '/libs/wpsave.js');
+
+var formatterlib = require(assetsDir + '/editor-utils-formatter.js');
+var formatter = formatterlib.Formatter;
+
+// Media strings
+
+// Image strings
+var imageSrc = 'content://com.android.providers.media.documents/document/image%3A12951';
+var plainImageHtml = '<img src="' + imageSrc + '" alt="" class="wp-image-123 size-full" width="172" height="244">';
+var imageWrappedInLinkHtml = '<a href="' + imageSrc + '">' + plainImageHtml + '</a>';
+
+// Captioned image strings
+var imageCaptionShortcode = '[caption width="600" align="alignnone"]' + imageSrc + 'Text[/caption]';
+var imageWithCaptionHtml = '<label class="wp-temp" data-wp-temp="caption" onclick="">' +
+ '<span class="wp-caption" style="width:600px; max-width:100% !important;" data-caption-width="600" ' +
+ 'data-caption-align="alignnone">' + imageSrc + 'Text</span></label>';
+var linkedImageCaptionShortcode = '[caption width="600" align="alignnone"]' + imageWrappedInLinkHtml + 'Text[/caption]';
+var linkedImageCaptionHtml = '<label class="wp-temp" data-wp-temp="caption" onclick="">' +
+ '<span class="wp-caption" style="width:600px; max-width:100% !important;" data-caption-width="600" ' +
+ 'data-caption-align="alignnone">' + imageWrappedInLinkHtml + 'Text</span></label>';
+
+// Video strings
+var videoSrc = 'content://com.android.providers.media.documents/document/video%3A12966';
+var videoShortcode = '[video src="' + videoSrc + '" poster=""][/video]';
+var videoHtml = '<span class="edit-container" contenteditable="false"><span class="delete-overlay"></span>' +
+ '<video webkit-playsinline src="' + videoSrc + '" poster="" preload="metadata" onclick="" controls="controls">' +
+ '</video></span>';
+
+// VideoPress video strings
+var vpVideoShortcode = '[wpvideo ABCD1234]';
+var vpVideoHtml = '<span class="edit-container" contenteditable="false"><span class="delete-overlay"></span>' +
+ '<video data-wpvideopress="ABCD1234" webkit-playsinline src="" preload="metadata" poster="svg/wpposter.svg" ' +
+ 'onclick="" onerror="ZSSEditor.sendVideoPressInfoRequest(\'ABCD1234\');"></video></span>';
+
+describe('HTML to Visual formatter should correctly convert', function () {
+ it('single-line HTML', function () {
+ assert.equal('<p>Some text</p>\n', formatter.htmlToVisual('Some text'));
+ });
+
+ it('multi-paragraph HTML', function () {
+ assert.equal('<p>Some text</p>\n<p>More text</p>\n', formatter.htmlToVisual('Some text\n\nMore text'));
+ });
+
+ testMediaParagraphWrapping('non-linked image', plainImageHtml, plainImageHtml);
+ testMediaParagraphWrapping('linked image', imageWrappedInLinkHtml, imageWrappedInLinkHtml);
+ testMediaParagraphWrapping('non-linked image, with caption', imageCaptionShortcode, imageWithCaptionHtml);
+ testMediaParagraphWrapping('linked image, with caption', linkedImageCaptionShortcode, linkedImageCaptionHtml);
+ testMediaParagraphWrapping('non-VideoPress video', videoShortcode, videoHtml);
+ testMediaParagraphWrapping('VideoPress video', vpVideoShortcode, vpVideoHtml);
+});
+
+function testMediaParagraphWrapping(mediaType, htmlModeMediaHtml, visualModeMediaHtml) {
+ describe(mediaType, function () {
+ it('alone in post', function () {
+ var visualFormattingApplied = formatter.htmlToVisual(htmlModeMediaHtml);
+ assert.equal('<p>' + visualModeMediaHtml + '</p>\n', visualFormattingApplied);
+
+ var convertedToDivs = formatter.convertPToDiv(visualFormattingApplied).replace(/\n/g, '');
+ assert.equal('<div>' + visualModeMediaHtml + '</div><div><br></div>', convertedToDivs);
+ });
+
+ it('with paragraphs above and below', function () {
+ var imageBetweenParagraphs = 'Line 1\n\n' + htmlModeMediaHtml + '\n\nLine 2';
+
+ var visualFormattingApplied = formatter.htmlToVisual(imageBetweenParagraphs);
+ assert.equal('<p>Line 1</p>\n<p>' + visualModeMediaHtml + '</p>\n<p>Line 2</p>\n', visualFormattingApplied);
+
+ var convertedToDivs = formatter.convertPToDiv(visualFormattingApplied).replace(/\n/g, '');
+ assert.equal('<div>Line 1</div><div>' + visualModeMediaHtml + '</div><div>Line 2</div>', convertedToDivs);
+ });
+
+ it('with line breaks above and below', function () {
+ var imageBetweenLineBreaks = 'Line 1\n' + htmlModeMediaHtml + '\nLine 2';
+
+ var visualFormattingApplied = formatter.htmlToVisual(imageBetweenLineBreaks);
+ assert.equal('<p>Line 1<br />\n' + visualModeMediaHtml + '<br />\nLine 2</p>\n', visualFormattingApplied);
+
+ var convertedToDivs = formatter.convertPToDiv(visualFormattingApplied).replace(/\n/g, '');
+ assert.equal('<div>Line 1</div><div>' + visualModeMediaHtml + '</div><div>Line 2</div>', convertedToDivs);
+ });
+
+ it('start of post, with paragraph underneath', function () {
+ var imageFollowedByParagraph = htmlModeMediaHtml + '\n\nLine 2';
+
+ var visualFormattingApplied = formatter.htmlToVisual(imageFollowedByParagraph);
+ assert.equal('<p>' + visualModeMediaHtml + '</p>\n<p>Line 2</p>\n', visualFormattingApplied);
+
+ var convertedToDivs = formatter.convertPToDiv(visualFormattingApplied).replace(/\n/g, '');
+ assert.equal('<div>' + visualModeMediaHtml + '</div><div>Line 2</div>', convertedToDivs);
+ });
+
+ it('start of post, with line break underneath', function () {
+ var imageFollowedByLineBreak = htmlModeMediaHtml + '\nLine 2';
+
+ var visualFormattingApplied = formatter.htmlToVisual(imageFollowedByLineBreak);
+ assert.equal('<p>' + visualModeMediaHtml + '<br \/>\nLine 2</p>\n', visualFormattingApplied);
+
+ var convertedToDivs = formatter.convertPToDiv(visualFormattingApplied).replace(/\n/g, '');
+ assert.equal('<div>' + visualModeMediaHtml + '</div><div>Line 2</div>', convertedToDivs);
+ });
+
+ it('end of post, with paragraph above', function () {
+ var imageUnderParagraph = 'Line 1\n\n' + htmlModeMediaHtml;
+
+ var visualFormattingApplied = formatter.htmlToVisual(imageUnderParagraph);
+ assert.equal('<p>Line 1</p>\n<p>' + visualModeMediaHtml + '</p>\n', visualFormattingApplied);
+
+ var convertedToDivs = formatter.convertPToDiv(visualFormattingApplied).replace(/\n/g, '');
+ assert.equal('<div>Line 1</div><div>' + visualModeMediaHtml + '</div><div><br></div>', convertedToDivs);
+ });
+
+ it('end of post, with line break above', function () {
+ var imageUnderLineBreak = 'Line 1\n' + htmlModeMediaHtml;
+
+ var visualFormattingApplied = formatter.htmlToVisual(imageUnderLineBreak);
+ assert.equal('<p>Line 1<br \/>\n' + visualModeMediaHtml + '</p>\n', visualFormattingApplied);
+
+ var convertedToDivs = formatter.convertPToDiv(visualFormattingApplied).replace(/\n/g, '');
+ assert.equal('<div>Line 1</div><div>' + visualModeMediaHtml + '</div><div><br></div>', convertedToDivs);
+ });
+ });
+}
diff --git a/libs/editor/gradle/wrapper/gradle-wrapper.jar b/libs/editor/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..8c0fb64a8
--- /dev/null
+++ b/libs/editor/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/libs/editor/gradle/wrapper/gradle-wrapper.properties b/libs/editor/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..fed7c8a3c
--- /dev/null
+++ b/libs/editor/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Aug 25 15:33:22 CEST 2016
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
diff --git a/libs/editor/gradlew b/libs/editor/gradlew
new file mode 100755
index 000000000..91a7e269e
--- /dev/null
+++ b/libs/editor/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/libs/editor/gradlew.bat b/libs/editor/gradlew.bat
new file mode 100644
index 000000000..8a0b282aa
--- /dev/null
+++ b/libs/editor/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/libs/editor/settings.gradle b/libs/editor/settings.gradle
new file mode 100644
index 000000000..af0d9296c
--- /dev/null
+++ b/libs/editor/settings.gradle
@@ -0,0 +1,2 @@
+include ':WordPressEditor'
+include ':example'