aboutsummaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
Diffstat (limited to 'libs')
-rw-r--r--libs/analytics/.gitignore25
-rw-r--r--libs/analytics/LICENSE22
-rw-r--r--libs/analytics/README.md6
-rw-r--r--libs/analytics/WordPressAnalytics/build.gradle89
-rw-r--r--libs/analytics/WordPressAnalytics/gradle.properties-example6
-rw-r--r--libs/analytics/WordPressAnalytics/src/main/AndroidManifest.xml5
-rw-r--r--libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsMetadata.java69
-rw-r--r--libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java271
-rw-r--r--libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanel.java1172
-rw-r--r--libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanelInstructionsForStat.java140
-rw-r--r--libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java589
-rw-r--r--libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/Tracker.java77
-rw-r--r--libs/analytics/build.gradle0
-rw-r--r--libs/analytics/gradle/wrapper/gradle-wrapper.jarbin0 -> 51348 bytes
-rw-r--r--libs/analytics/gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xlibs/analytics/gradlew164
-rw-r--r--libs/analytics/gradlew.bat90
-rw-r--r--libs/analytics/settings.gradle1
-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
-rw-r--r--libs/networking/.gitignore23
-rw-r--r--libs/networking/WordPressNetworking/build.gradle49
-rw-r--r--libs/networking/WordPressNetworking/gradle.properties-example2
-rw-r--r--libs/networking/WordPressNetworking/src/main/AndroidManifest.xml3
-rw-r--r--libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/Authenticator.java13
-rw-r--r--libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/AuthenticatorRequest.java96
-rw-r--r--libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactory.java19
-rw-r--r--libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryAbstract.java9
-rw-r--r--libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryDefault.java14
-rw-r--r--libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientUtils.java475
-rw-r--r--libs/networking/build.gradle0
-rw-r--r--libs/networking/gradle/wrapper/gradle-wrapper.jarbin0 -> 49896 bytes
-rw-r--r--libs/networking/gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xlibs/networking/gradlew164
-rw-r--r--libs/networking/gradlew.bat90
-rw-r--r--libs/networking/settings.gradle1
-rw-r--r--libs/utils/.gitignore25
-rw-r--r--libs/utils/README.md27
-rw-r--r--libs/utils/WordPressUtils/README.md1
-rw-r--r--libs/utils/WordPressUtils/build.gradle65
-rw-r--r--libs/utils/WordPressUtils/gradle.properties-example6
-rw-r--r--libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/JSONUtilsTest.java32
-rw-r--r--libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/ShortcodeUtilsTest.java25
-rw-r--r--libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java108
-rw-r--r--libs/utils/WordPressUtils/src/main/AndroidManifest.xml5
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ActivityUtils.java16
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtils.java100
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AppLog.java272
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/BlogUtils.java74
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DateTimeUtils.java246
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DeviceUtils.java94
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DisplayUtils.java91
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EditTextUtils.java75
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EmoticonsUtils.java106
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/FormatUtils.java35
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GeocoderUtils.java116
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GravatarUtils.java84
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HTTPUtils.java31
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HtmlUtils.java156
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ImageUtils.java649
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/JSONUtils.java251
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/LanguageUtils.java52
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MapUtils.java107
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MediaUtils.java334
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/NetworkUtils.java89
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PackageUtils.java45
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PermissionUtils.java97
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java104
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ProfilingUtils.java87
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ServiceUtils.java16
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ShortcodeUtils.java31
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SqlUtils.java142
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java327
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactory.java14
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryAbstract.java7
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryDefault.java9
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ToastUtils.java37
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UrlUtils.java257
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UserEmailUtils.java38
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/ListScrollPositionManager.java58
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/LocationHelper.java144
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaFile.java339
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGallery.java87
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGalleryImageSpan.java21
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/SwipeToRefreshHelper.java72
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/Version.java47
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPHtmlTagHandler.java59
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageGetter.java177
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageSpan.java140
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPQuoteSpan.java44
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPUnderlineSpan.java34
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPWebChromeClient.java45
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/AutoResizeTextView.java299
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/CustomSwipeRefreshLayout.java33
-rw-r--r--libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPEditText.java62
-rw-r--r--libs/utils/WordPressUtils/src/main/res/values/attrs.xml7
-rw-r--r--libs/utils/WordPressUtils/src/main/res/values/strings.xml5
-rw-r--r--libs/utils/build.gradle0
-rw-r--r--libs/utils/gradle/wrapper/gradle-wrapper.jarbin0 -> 51348 bytes
-rw-r--r--libs/utils/gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xlibs/utils/gradlew164
-rw-r--r--libs/utils/gradlew.bat90
-rw-r--r--libs/utils/settings.gradle1
357 files changed, 27605 insertions, 0 deletions
diff --git a/libs/analytics/.gitignore b/libs/analytics/.gitignore
new file mode 100644
index 000000000..8babf679a
--- /dev/null
+++ b/libs/analytics/.gitignore
@@ -0,0 +1,25 @@
+# generated files
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+tools/deploy-mvn-artifact.conf
+
+# Intellij project files
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# Gradle
+.gradle/
+gradle.properties
+
+# Idea
+.idea/workspace.xml
+*.iml
+
+# OS X
+.DS_Store
+
+# dependencies
diff --git a/libs/analytics/LICENSE b/libs/analytics/LICENSE
new file mode 100644
index 000000000..20efd1b3e
--- /dev/null
+++ b/libs/analytics/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015
+
+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.
+
diff --git a/libs/analytics/README.md b/libs/analytics/README.md
new file mode 100644
index 000000000..0ef374cf6
--- /dev/null
+++ b/libs/analytics/README.md
@@ -0,0 +1,6 @@
+# WordPressCom-Analytics-Android
+Library for handling Analytics tracking in WordPress Android.
+
+Part of the [WordPress-Android] project.
+
+[WordPress-Android]: https://github.com/wordpress-mobile/WordPress-Android
diff --git a/libs/analytics/WordPressAnalytics/build.gradle b/libs/analytics/WordPressAnalytics/build.gradle
new file mode 100644
index 000000000..6332955f4
--- /dev/null
+++ b/libs/analytics/WordPressAnalytics/build.gradle
@@ -0,0 +1,89 @@
+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()
+}
+
+dependencies {
+ compile 'com.automattic:tracks:1.1.0'
+ compile 'com.mixpanel.android:mixpanel-android:4.6.4'
+ compile 'org.wordpress:utils:1.11.0'
+}
+
+android {
+ publishNonDefault true
+
+ compileSdkVersion 24
+ buildToolsVersion "24.0.2"
+
+ defaultConfig {
+ versionName "1.2.0"
+ minSdkVersion 16
+ targetSdkVersion 24
+ }
+}
+
+version android.defaultConfig.versionName
+group = "org.wordpress"
+archivesBaseName = "analytics"
+
+signing {
+ required {
+ project.properties.containsKey("signing.keyId") && project.properties.containsKey("signing.secretKeyRingFile")
+ }
+ sign configurations.archives
+}
+
+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 'WordPressCom-Analytics-Android'
+ packaging 'aar'
+ description 'Analytics lib for WordPress-Android'
+ url 'https://github.com/wordpress-mobile/WordPressCom-Analytics-Android'
+ scm {
+ connection 'scm:git:https://github.com/wordpress-mobile/WordPressCom-Analytics-Android.git'
+ developerConnection 'scm:git:https://github.com/wordpress-mobile/WordPressCom-Analytics-Android.git'
+ url 'https://github.com/wordpress-mobile/WordPressCom-Analytics-Android'
+ }
+
+ 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/analytics/WordPressAnalytics/gradle.properties-example b/libs/analytics/WordPressAnalytics/gradle.properties-example
new file mode 100644
index 000000000..5281d935c
--- /dev/null
+++ b/libs/analytics/WordPressAnalytics/gradle.properties-example
@@ -0,0 +1,6 @@
+ossrhUsername=hello
+ossrhPassword=world
+
+signing.keyId=byebye
+signing.password=secret
+signing.secretKeyRingFile=/home/user/secret.gpg
diff --git a/libs/analytics/WordPressAnalytics/src/main/AndroidManifest.xml b/libs/analytics/WordPressAnalytics/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..d4cf8c539
--- /dev/null
+++ b/libs/analytics/WordPressAnalytics/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.wordpress.android.analytics">
+
+</manifest>
diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsMetadata.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsMetadata.java
new file mode 100644
index 000000000..a49629bc0
--- /dev/null
+++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsMetadata.java
@@ -0,0 +1,69 @@
+package org.wordpress.android.analytics;
+
+public class AnalyticsMetadata {
+ private boolean mIsUserConnected;
+ private boolean mIsWordPressComUser;
+ private boolean mIsJetpackUser;
+ private int mSessionCount;
+ private int mNumBlogs;
+ private String mUsername = "";
+ private String mEmail = "";
+
+ public AnalyticsMetadata() {}
+
+ public boolean isUserConnected() {
+ return mIsUserConnected;
+ }
+
+ public void setUserConnected(boolean isUserConnected) {
+ mIsUserConnected = isUserConnected;
+ }
+
+ public boolean isWordPressComUser() {
+ return mIsWordPressComUser;
+ }
+
+ public void setWordPressComUser(boolean isWordPressComUser) {
+ mIsWordPressComUser = isWordPressComUser;
+ }
+
+ public boolean isJetpackUser() {
+ return mIsJetpackUser;
+ }
+
+ public void setJetpackUser(boolean isJetpackUser) {
+ mIsJetpackUser = isJetpackUser;
+ }
+
+ public int getSessionCount() {
+ return mSessionCount;
+ }
+
+ public void setSessionCount(int sessionCount) {
+ this.mSessionCount = sessionCount;
+ }
+
+ public int getNumBlogs() {
+ return mNumBlogs;
+ }
+
+ public void setNumBlogs(int numBlogs) {
+ this.mNumBlogs = numBlogs;
+ }
+
+ public String getUsername() {
+ return mUsername;
+ }
+
+ public void setUsername(String username) {
+ this.mUsername = username;
+ }
+
+ public String getEmail() {
+ return mEmail;
+ }
+
+ public void setEmail(String email) {
+ this.mEmail = email;
+ }
+}
diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java
new file mode 100644
index 000000000..c160f47a6
--- /dev/null
+++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java
@@ -0,0 +1,271 @@
+package org.wordpress.android.analytics;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public final class AnalyticsTracker {
+ private static boolean mHasUserOptedOut;
+
+ public static final String READER_DETAIL_TYPE_KEY = "post_detail_type";
+ public static final String READER_DETAIL_TYPE_NORMAL = "normal";
+ public static final String READER_DETAIL_TYPE_BLOG_PREVIEW = "preview-blog";
+ public static final String READER_DETAIL_TYPE_TAG_PREVIEW = "preview-tag";
+
+ public enum Stat {
+ APPLICATION_OPENED,
+ APPLICATION_CLOSED,
+ APPLICATION_INSTALLED,
+ APPLICATION_UPGRADED,
+ READER_ACCESSED,
+ READER_ARTICLE_COMMENTED_ON,
+ READER_ARTICLE_LIKED,
+ READER_ARTICLE_OPENED,
+ READER_ARTICLE_UNLIKED,
+ READER_BLOG_BLOCKED,
+ READER_BLOG_FOLLOWED,
+ READER_BLOG_PREVIEWED,
+ READER_BLOG_UNFOLLOWED,
+ READER_DISCOVER_VIEWED,
+ READER_INFINITE_SCROLL,
+ READER_LIST_FOLLOWED,
+ READER_LIST_LOADED,
+ READER_LIST_PREVIEWED,
+ READER_LIST_UNFOLLOWED,
+ READER_TAG_FOLLOWED,
+ READER_TAG_LOADED,
+ READER_TAG_PREVIEWED,
+ READER_TAG_UNFOLLOWED,
+ READER_SEARCH_LOADED,
+ READER_SEARCH_PERFORMED,
+ READER_SEARCH_RESULT_TAPPED,
+ READER_RELATED_POST_CLICKED,
+ STATS_ACCESSED,
+ STATS_INSIGHTS_ACCESSED,
+ STATS_PERIOD_DAYS_ACCESSED,
+ STATS_PERIOD_WEEKS_ACCESSED,
+ STATS_PERIOD_MONTHS_ACCESSED,
+ STATS_PERIOD_YEARS_ACCESSED,
+ STATS_VIEW_ALL_ACCESSED,
+ STATS_SINGLE_POST_ACCESSED,
+ STATS_TAPPED_BAR_CHART,
+ STATS_SCROLLED_TO_BOTTOM,
+ STATS_WIDGET_ADDED,
+ STATS_WIDGET_REMOVED,
+ STATS_WIDGET_TAPPED,
+ EDITOR_CREATED_POST,
+ EDITOR_ADDED_PHOTO_VIA_LOCAL_LIBRARY,
+ EDITOR_ADDED_VIDEO_VIA_LOCAL_LIBRARY,
+ EDITOR_ADDED_PHOTO_VIA_WP_MEDIA_LIBRARY,
+ EDITOR_ADDED_VIDEO_VIA_WP_MEDIA_LIBRARY,
+ EDITOR_UPDATED_POST,
+ EDITOR_SCHEDULED_POST,
+ EDITOR_CLOSED,
+ EDITOR_PUBLISHED_POST,
+ EDITOR_SAVED_DRAFT,
+ EDITOR_DISCARDED_CHANGES,
+ EDITOR_EDITED_IMAGE, // Visual editor only
+ EDITOR_ENABLED_NEW_VERSION, // Visual editor only
+ EDITOR_TOGGLED_OFF, // Visual editor only
+ EDITOR_TOGGLED_ON, // Visual editor only
+ EDITOR_UPLOAD_MEDIA_FAILED, // Visual editor only
+ EDITOR_UPLOAD_MEDIA_RETRIED, // Visual editor only
+ EDITOR_TAPPED_BLOCKQUOTE,
+ EDITOR_TAPPED_BOLD,
+ EDITOR_TAPPED_HTML, // Visual editor only
+ EDITOR_TAPPED_IMAGE,
+ EDITOR_TAPPED_ITALIC,
+ EDITOR_TAPPED_LINK,
+ EDITOR_TAPPED_MORE,
+ EDITOR_TAPPED_STRIKETHROUGH,
+ EDITOR_TAPPED_UNDERLINE,
+ EDITOR_TAPPED_ORDERED_LIST, // Visual editor only
+ EDITOR_TAPPED_UNLINK, // Visual editor only
+ EDITOR_TAPPED_UNORDERED_LIST, // Visual editor only
+ ME_ACCESSED,
+ ME_GRAVATAR_TAPPED,
+ ME_GRAVATAR_TOOLTIP_TAPPED,
+ ME_GRAVATAR_PERMISSIONS_INTERRUPTED,
+ ME_GRAVATAR_PERMISSIONS_DENIED,
+ ME_GRAVATAR_PERMISSIONS_ACCEPTED,
+ ME_GRAVATAR_SHOT_NEW,
+ ME_GRAVATAR_GALLERY_PICKED,
+ ME_GRAVATAR_CROPPED,
+ ME_GRAVATAR_UPLOADED,
+ ME_GRAVATAR_UPLOAD_UNSUCCESSFUL,
+ ME_GRAVATAR_UPLOAD_EXCEPTION,
+ MY_SITE_ACCESSED,
+ NOTIFICATIONS_ACCESSED,
+ NOTIFICATIONS_OPENED_NOTIFICATION_DETAILS,
+ NOTIFICATION_REPLIED_TO,
+ NOTIFICATION_APPROVED,
+ NOTIFICATION_UNAPPROVED,
+ NOTIFICATION_LIKED,
+ NOTIFICATION_UNLIKED,
+ NOTIFICATION_TRASHED,
+ NOTIFICATION_FLAGGED_AS_SPAM,
+ OPENED_POSTS,
+ OPENED_PAGES,
+ OPENED_COMMENTS,
+ OPENED_VIEW_SITE,
+ OPENED_VIEW_ADMIN,
+ OPENED_MEDIA_LIBRARY,
+ OPENED_BLOG_SETTINGS,
+ OPENED_ACCOUNT_SETTINGS,
+ OPENED_APP_SETTINGS,
+ OPENED_MY_PROFILE,
+ OPENED_PEOPLE_MANAGEMENT,
+ OPENED_PERSON,
+ CREATED_ACCOUNT,
+ CREATED_SITE,
+ ACCOUNT_LOGOUT,
+ SHARED_ITEM,
+ ADDED_SELF_HOSTED_SITE,
+ SIGNED_IN,
+ SIGNED_INTO_JETPACK,
+ PERFORMED_JETPACK_SIGN_IN_FROM_STATS_SCREEN,
+ STATS_SELECTED_INSTALL_JETPACK,
+ STATS_SELECTED_CONNECT_JETPACK,
+ PUSH_NOTIFICATION_RECEIVED,
+ PUSH_NOTIFICATION_TAPPED, // Same of opened
+ SUPPORT_OPENED_HELPSHIFT_SCREEN,
+ SUPPORT_SENT_REPLY_TO_SUPPORT_MESSAGE,
+ LOGIN_MAGIC_LINK_EXITED,
+ LOGIN_MAGIC_LINK_FAILED,
+ LOGIN_MAGIC_LINK_OPENED,
+ LOGIN_MAGIC_LINK_REQUESTED,
+ LOGIN_MAGIC_LINK_SUCCEEDED,
+ LOGIN_FAILED,
+ LOGIN_FAILED_TO_GUESS_XMLRPC,
+ LOGIN_INSERTED_INVALID_URL,
+ LOGIN_AUTOFILL_CREDENTIALS_FILLED,
+ LOGIN_AUTOFILL_CREDENTIALS_UPDATED,
+ PERSON_REMOVED,
+ PERSON_UPDATED,
+ PUSH_AUTHENTICATION_APPROVED,
+ PUSH_AUTHENTICATION_EXPIRED,
+ PUSH_AUTHENTICATION_FAILED,
+ PUSH_AUTHENTICATION_IGNORED,
+ NOTIFICATION_SETTINGS_LIST_OPENED,
+ NOTIFICATION_SETTINGS_STREAMS_OPENED,
+ NOTIFICATION_SETTINGS_DETAILS_OPENED,
+ THEMES_ACCESSED_THEMES_BROWSER,
+ THEMES_ACCESSED_SEARCH,
+ THEMES_CHANGED_THEME,
+ THEMES_PREVIEWED_SITE,
+ THEMES_DEMO_ACCESSED,
+ THEMES_CUSTOMIZE_ACCESSED,
+ THEMES_SUPPORT_ACCESSED,
+ THEMES_DETAILS_ACCESSED,
+ ACCOUNT_SETTINGS_LANGUAGE_CHANGED,
+ SITE_SETTINGS_ACCESSED,
+ SITE_SETTINGS_ACCESSED_MORE_SETTINGS,
+ SITE_SETTINGS_LEARN_MORE_CLICKED,
+ SITE_SETTINGS_LEARN_MORE_LOADED,
+ SITE_SETTINGS_ADDED_LIST_ITEM,
+ SITE_SETTINGS_DELETED_LIST_ITEMS,
+ SITE_SETTINGS_SAVED_REMOTELY,
+ SITE_SETTINGS_HINT_TOAST_SHOWN,
+ SITE_SETTINGS_START_OVER_ACCESSED,
+ SITE_SETTINGS_START_OVER_CONTACT_SUPPORT_CLICKED,
+ SITE_SETTINGS_EXPORT_SITE_ACCESSED,
+ SITE_SETTINGS_EXPORT_SITE_REQUESTED,
+ SITE_SETTINGS_EXPORT_SITE_RESPONSE_OK,
+ SITE_SETTINGS_EXPORT_SITE_RESPONSE_ERROR,
+ SITE_SETTINGS_DELETE_SITE_ACCESSED,
+ SITE_SETTINGS_DELETE_SITE_PURCHASES_REQUESTED,
+ SITE_SETTINGS_DELETE_SITE_PURCHASES_SHOWN,
+ SITE_SETTINGS_DELETE_SITE_PURCHASES_SHOW_CLICKED,
+ SITE_SETTINGS_DELETE_SITE_REQUESTED,
+ SITE_SETTINGS_DELETE_SITE_RESPONSE_OK,
+ SITE_SETTINGS_DELETE_SITE_RESPONSE_ERROR,
+ ABTEST_START,
+ TRAIN_TRACKS_RENDER,
+ TRAIN_TRACKS_INTERACT
+ }
+
+ private static final List<Tracker> TRACKERS = new ArrayList<>();
+
+ private AnalyticsTracker() {
+ }
+
+ public static void init(Context context) {
+ loadPrefHasUserOptedOut(context);
+ }
+
+ public static void loadPrefHasUserOptedOut(Context context) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ boolean hasUserOptedOut = !prefs.getBoolean("wp_pref_send_usage_stats", true);
+ if (hasUserOptedOut != mHasUserOptedOut) {
+ mHasUserOptedOut = hasUserOptedOut;
+ }
+ }
+
+ public static void registerTracker(Tracker tracker) {
+ if (tracker != null) {
+ TRACKERS.add(tracker);
+ }
+ }
+
+ public static void track(Stat stat) {
+ if (mHasUserOptedOut) {
+ return;
+ }
+ for (Tracker tracker : TRACKERS) {
+ tracker.track(stat);
+ }
+ }
+
+ public static void track(Stat stat, Map<String, ?> properties) {
+ if (mHasUserOptedOut) {
+ return;
+ }
+ for (Tracker tracker : TRACKERS) {
+ tracker.track(stat, properties);
+ }
+ }
+
+
+ public static void flush() {
+ if (mHasUserOptedOut) {
+ return;
+ }
+ for (Tracker tracker : TRACKERS) {
+ tracker.flush();
+ }
+ }
+
+ public static void endSession(boolean force) {
+ if (mHasUserOptedOut && !force) {
+ return;
+ }
+ for (Tracker tracker : TRACKERS) {
+ tracker.endSession();
+ }
+ }
+
+ public static void registerPushNotificationToken(String regId) {
+ if (mHasUserOptedOut) {
+ return;
+ }
+ for (Tracker tracker : TRACKERS) {
+ tracker.registerPushNotificationToken(regId);
+ }
+ }
+
+ public static void clearAllData() {
+ for (Tracker tracker : TRACKERS) {
+ tracker.clearAllData();
+ }
+ }
+
+ public static void refreshMetadata(AnalyticsMetadata metadata) {
+ for (Tracker tracker : TRACKERS) {
+ tracker.refreshMetadata(metadata);
+ }
+ }
+}
diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanel.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanel.java
new file mode 100644
index 000000000..1e1b278f8
--- /dev/null
+++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanel.java
@@ -0,0 +1,1172 @@
+package org.wordpress.android.analytics;
+
+import android.annotation.SuppressLint;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import com.mixpanel.android.mpmetrics.MixpanelAPI;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+
+import java.util.EnumMap;
+import java.util.Map;
+
+public class AnalyticsTrackerMixpanel extends Tracker {
+ public static final String SESSION_COUNT = "sessionCount";
+
+ private MixpanelAPI mMixpanel;
+ private EnumMap<AnalyticsTracker.Stat, JSONObject> mAggregatedProperties;
+ private static final String MIXPANEL_PLATFORM = "platform";
+ private static final String MIXPANEL_SESSION_COUNT = "session_count";
+ private static final String DOTCOM_USER = "dotcom_user";
+ private static final String JETPACK_USER = "jetpack_user";
+ private static final String MIXPANEL_NUMBER_OF_BLOGS = "number_of_blogs";
+ private static final String APP_LOCALE = "app_locale";
+ private static final String MIXPANEL_ANON_ID = "mixpanel_user_anon_id";
+
+ public AnalyticsTrackerMixpanel(Context context, String token) throws IllegalArgumentException {
+ super(context);
+ mAggregatedProperties = new EnumMap<>(AnalyticsTracker.Stat.class);
+ mMixpanel = MixpanelAPI.getInstance(context, token);
+ }
+
+ @SuppressWarnings("deprecation")
+ @SuppressLint("NewApi")
+ public static void showNotification(Context context, PendingIntent intent, int notificationIcon, CharSequence title,
+ CharSequence message) {
+ final NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ final Notification.Builder builder = new Notification.Builder(context).setSmallIcon(notificationIcon)
+ .setTicker(message).setWhen(System.currentTimeMillis()).setContentTitle(title).setContentText(message)
+ .setContentIntent(intent);
+ Notification notification;
+ notification = builder.build();
+ notification.flags |= Notification.FLAG_AUTO_CANCEL;
+ nm.notify(0, notification);
+ }
+
+ String getAnonIdPrefKey() {
+ return MIXPANEL_ANON_ID;
+ }
+
+ @Override
+ public void track(AnalyticsTracker.Stat stat) {
+ track(stat, null);
+ }
+
+ @Override
+ public void track(AnalyticsTracker.Stat stat, Map<String, ?> properties) {
+ AnalyticsTrackerMixpanelInstructionsForStat instructions = instructionsForStat(stat);
+
+ if (instructions == null) {
+ return;
+ }
+
+ trackMixpanelDataForInstructions(instructions, properties);
+ }
+
+ private void trackMixpanelDataForInstructions(AnalyticsTrackerMixpanelInstructionsForStat instructions,
+ Map<String, ?> properties) {
+ if (instructions.getDisableForSelfHosted()) {
+ return;
+ }
+
+ // Just a security check we're tracking the correct user
+ if (getWordPressComUserName() == null && getAnonID() == null) {
+ this.clearAllData();
+ generateNewAnonID();
+ mMixpanel.identify(getAnonID());
+ }
+
+ trackMixpanelEventForInstructions(instructions, properties);
+ trackMixpanelPropertiesForInstructions(instructions);
+ }
+
+ private void trackMixpanelPropertiesForInstructions(AnalyticsTrackerMixpanelInstructionsForStat instructions) {
+ if (instructions.getPeoplePropertyToIncrement() != null && !instructions.getPeoplePropertyToIncrement()
+ .isEmpty()) {
+ incrementPeopleProperty(instructions.getPeoplePropertyToIncrement());
+ }
+
+ if (instructions.getSuperPropertyToIncrement() != null && !instructions.getSuperPropertyToIncrement()
+ .isEmpty()) {
+ incrementSuperProperty(instructions.getSuperPropertyToIncrement());
+ }
+
+ if (instructions.getPropertyToIncrement() != null && !instructions.getPropertyToIncrement().isEmpty()) {
+ incrementProperty(instructions.getPropertyToIncrement(), instructions.getStatToAttachProperty());
+ }
+
+ if (instructions.getSuperPropertiesToFlag() != null && instructions.getSuperPropertiesToFlag().size() > 0) {
+ for (String superPropertyToFlag : instructions.getSuperPropertiesToFlag()) {
+ flagSuperProperty(superPropertyToFlag);
+ }
+ }
+
+ if (instructions.getPeoplePropertiesToAssign() != null
+ && instructions.getPeoplePropertiesToAssign().size() > 0) {
+ for (Map.Entry<String, Object> entry: instructions.getPeoplePropertiesToAssign().entrySet()) {
+ setValueForPeopleProperty(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+
+ private void setValueForPeopleProperty(String peopleProperty, Object value) {
+ try {
+ mMixpanel.getPeople().set(peopleProperty, value);
+ } catch (OutOfMemoryError outOfMemoryError) {
+ // ignore exception
+ }
+ }
+
+ private void trackMixpanelEventForInstructions(AnalyticsTrackerMixpanelInstructionsForStat instructions,
+ Map<String, ?> properties) {
+ String eventName = instructions.getMixpanelEventName();
+ if (eventName != null && !eventName.isEmpty()) {
+ JSONObject savedPropertiesForStat = propertiesForStat(instructions.getStat());
+ if (savedPropertiesForStat == null) {
+ savedPropertiesForStat = new JSONObject();
+ }
+
+ // Retrieve properties user has already passed in and combine them with the saved properties
+ if (properties != null) {
+ for (Object o : properties.entrySet()) {
+ Map.Entry pairs = (Map.Entry) o;
+ String key = (String) pairs.getKey();
+ try {
+ Object value = pairs.getValue();
+ savedPropertiesForStat.put(key, value);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ }
+ }
+ }
+ mMixpanel.track(eventName, savedPropertiesForStat);
+ removePropertiesForStat(instructions.getStat());
+ }
+ }
+
+ @Override
+ public void registerPushNotificationToken(String regId) {
+ try {
+ mMixpanel.getPeople().setPushRegistrationId(regId);
+ } catch (OutOfMemoryError outOfMemoryError) {
+ // ignore exception
+ }
+ }
+
+ @Override
+ public void endSession() {
+ mAggregatedProperties.clear();
+ mMixpanel.flush();
+ }
+
+ @Override
+ public void flush() {
+ mMixpanel.flush();
+ }
+
+ @Override
+ public void refreshMetadata(AnalyticsMetadata metadata) {
+ // Register super properties
+ try {
+ JSONObject properties = new JSONObject();
+ properties.put(MIXPANEL_PLATFORM, "Android");
+ properties.put(MIXPANEL_SESSION_COUNT, metadata.getSessionCount());
+ properties.put(DOTCOM_USER, metadata.isUserConnected());
+ properties.put(JETPACK_USER, metadata.isJetpackUser());
+ properties.put(MIXPANEL_NUMBER_OF_BLOGS, metadata.getNumBlogs());
+ properties.put(APP_LOCALE, mContext.getResources().getConfiguration().locale.toString());
+ mMixpanel.registerSuperProperties(properties);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ }
+
+
+ if (metadata.isUserConnected() && metadata.isWordPressComUser()) {
+ setWordPressComUserName(metadata.getUsername());
+ // Re-unify the user
+ if (getAnonID() != null) {
+ mMixpanel.alias(getWordPressComUserName(), getAnonID());
+ clearAnonID();
+ } else {
+ mMixpanel.identify(metadata.getUsername());
+ }
+ } else {
+ // Not wpcom connected. Check if anonID is already present
+ setWordPressComUserName(null);
+ if (getAnonID() == null) {
+ generateNewAnonID();
+ }
+ mMixpanel.identify(getAnonID());
+ }
+
+ // Application opened and start.
+ if (metadata.isUserConnected()) {
+ try {
+ String userID = getWordPressComUserName() != null ? getWordPressComUserName() : getAnonID();
+ if (userID == null) {
+ // This should not be an option here
+ return;
+ }
+
+ mMixpanel.getPeople().identify(userID);
+ JSONObject jsonObj = new JSONObject();
+ jsonObj.put("$username", userID);
+ if (metadata.getEmail() != null) {
+ jsonObj.put("$email", metadata.getEmail());
+ }
+ jsonObj.put("$first_name", userID);
+ mMixpanel.getPeople().set(jsonObj);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ } catch (OutOfMemoryError outOfMemoryError) {
+ // ignore exception
+ }
+ }
+ }
+
+ @Override
+ public void clearAllData() {
+ super.clearAllData();
+ mMixpanel.clearSuperProperties();
+ try {
+ mMixpanel.getPeople().clearPushRegistrationId();
+ } catch (OutOfMemoryError outOfMemoryError) {
+ // ignore exception
+ }
+ }
+
+ private AnalyticsTrackerMixpanelInstructionsForStat instructionsForStat(
+ AnalyticsTracker.Stat stat) {
+ AnalyticsTrackerMixpanelInstructionsForStat instructions;
+ switch (stat) {
+ case APPLICATION_OPENED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Application Opened");
+ instructions.setSuperPropertyToIncrement("Application Opened");
+ incrementSessionCount();
+ break;
+ case APPLICATION_CLOSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Application Closed");
+ break;
+ case APPLICATION_INSTALLED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Application Installed");
+ break;
+ case APPLICATION_UPGRADED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Application Upgraded");
+ break;
+ case READER_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_reader");
+ instructions.setCurrentDateForPeopleProperty("last_time_accessed_reader");
+ break;
+ case READER_ARTICLE_COMMENTED_ON:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Commented on Article");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement(
+ "number_of_times_commented_on_reader_article");
+ instructions.setCurrentDateForPeopleProperty("last_time_commented_on_article");
+ break;
+ case READER_ARTICLE_LIKED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Liked Article");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_liked_article");
+ instructions.setCurrentDateForPeopleProperty("last_time_liked_reader_article");
+ break;
+ case READER_ARTICLE_OPENED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Opened Article");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_opened_article");
+ instructions.setCurrentDateForPeopleProperty("last_time_opened_reader_article");
+ break;
+ case READER_ARTICLE_UNLIKED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Unliked Article");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_unliked_article");
+ instructions.setCurrentDateForPeopleProperty("last_time_unliked_reader_article");
+ break;
+ case READER_BLOG_BLOCKED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Blocked Blog");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_blocked_a_blog");
+ instructions.setCurrentDateForPeopleProperty("last_time_blocked_a_blog");
+ break;
+ case READER_BLOG_FOLLOWED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Followed Site");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_followed_site");
+ instructions.setCurrentDateForPeopleProperty("last_time_followed_site");
+ break;
+ case READER_BLOG_PREVIEWED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Blog Preview");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_viewed_blog_preview");
+ instructions.setCurrentDateForPeopleProperty("last_time_viewed_blog_preview");
+ break;
+ case READER_BLOG_UNFOLLOWED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Unfollowed Site");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_unfollowed_site");
+ instructions.setCurrentDateForPeopleProperty("last_time_unfollowed_site");
+ break;
+ case READER_DISCOVER_VIEWED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Discover Content Viewed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement(
+ "number_of_times_discover_content_viewed");
+ instructions.setCurrentDateForPeopleProperty("last_time_discover_content_viewed");
+ break;
+ case READER_INFINITE_SCROLL:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Infinite Scroll");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement(
+ "number_of_times_reader_performed_infinite_scroll");
+ instructions.setCurrentDateForPeopleProperty("last_time_performed_reader_infinite_scroll");
+ break;
+ case READER_LIST_FOLLOWED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Followed List");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_followed_list");
+ instructions.setCurrentDateForPeopleProperty("last_time_followed_list");
+ break;
+ case READER_LIST_LOADED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Loaded List");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_loaded_list");
+ instructions.setCurrentDateForPeopleProperty("last_time_loaded_list");
+ break;
+ case READER_LIST_PREVIEWED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - List Preview");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_viewed_list_preview");
+ instructions.setCurrentDateForPeopleProperty("last_time_viewed_list_preview");
+ break;
+ case READER_LIST_UNFOLLOWED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Unfollowed List");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_unfollowed_list");
+ instructions.setCurrentDateForPeopleProperty("last_time_unfollowed_list");
+ break;
+ case READER_TAG_FOLLOWED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Followed Reader Tag");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_followed_reader_tag");
+ instructions.setCurrentDateForPeopleProperty("last_time_followed_reader_tag");
+ break;
+ case READER_TAG_LOADED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Loaded Tag");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_loaded_tag");
+ instructions.setCurrentDateForPeopleProperty("last_time_loaded_tag");
+ break;
+ case READER_TAG_PREVIEWED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Tag Preview");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_viewed_tag_preview");
+ instructions.setCurrentDateForPeopleProperty("last_time_viewed_tag_preview");
+ break;
+ case READER_TAG_UNFOLLOWED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Unfollowed Reader Tag");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_unfollowed_reader_tag");
+ instructions.setCurrentDateForPeopleProperty("last_time_unfollowed_reader_tag");
+ break;
+ case READER_SEARCH_LOADED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Loaded Search");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_reader_search_loaded");
+ instructions.setCurrentDateForPeopleProperty("last_time_reader_search_loaded");
+ break;
+ case READER_SEARCH_PERFORMED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Performed Search");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_reader_search_performed");
+ instructions.setCurrentDateForPeopleProperty("last_time_reader_search_performed");
+ break;
+ case READER_SEARCH_RESULT_TAPPED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Tapped Search Result");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_reader_search_result_tapped");
+ instructions.setCurrentDateForPeopleProperty("last_time_reader_search_result_tapped");
+ break;
+ case READER_RELATED_POST_CLICKED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Reader - Related Post Clicked");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_reader_related_post_clicked");
+ instructions.setCurrentDateForPeopleProperty("last_time_reader_related_post_clicked");
+ break;
+ case EDITOR_CREATED_POST:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Created Post");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_created_post");
+ instructions.setCurrentDateForPeopleProperty("last_time_created_post_in_editor");
+ break;
+ case EDITOR_SAVED_DRAFT:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Saved Draft");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_saved_draft");
+ instructions.setCurrentDateForPeopleProperty("last_time_saved_draft");
+ break;
+ case EDITOR_DISCARDED_CHANGES:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Discarded Changes");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_discarded_changes");
+ instructions.setCurrentDateForPeopleProperty("last_time_discarded_changes");
+ break;
+ case EDITOR_EDITED_IMAGE:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Edited Image");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_edited_image");
+ instructions.setCurrentDateForPeopleProperty("last_time_edited_image");
+ break;
+ case EDITOR_ENABLED_NEW_VERSION:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Enabled New Version");
+ instructions.addSuperPropertyToFlag("enabled_new_editor");
+ break;
+ case EDITOR_TOGGLED_ON:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Toggled New Editor On");
+ instructions.setPeoplePropertyToValue("enabled_new_editor", true);
+ break;
+ case EDITOR_TOGGLED_OFF:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Toggled New Editor Off");
+ instructions.setPeoplePropertyToValue("enabled_new_editor", false);
+ break;
+ case EDITOR_UPLOAD_MEDIA_FAILED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Upload Media Failed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_upload_media_failed");
+ instructions.setCurrentDateForPeopleProperty("last_time_editor_upload_media_failed");
+ break;
+ case EDITOR_UPLOAD_MEDIA_RETRIED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Retried Uploading Media");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_retried_uploading_media");
+ instructions.setCurrentDateForPeopleProperty("last_time_editor_retried_uploading_media");
+ break;
+ case EDITOR_CLOSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Closed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_closed");
+ break;
+ case EDITOR_ADDED_PHOTO_VIA_LOCAL_LIBRARY:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Added Photo via Local Library");
+ instructions.
+ setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_added_photo_via_local_library");
+ instructions.setCurrentDateForPeopleProperty("last_time_added_photo_via_local_library_to_post");
+ break;
+ case EDITOR_ADDED_PHOTO_VIA_WP_MEDIA_LIBRARY:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Added Photo via WP Media Library");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement(
+ "number_of_times_added_photo_via_wp_media_library");
+ instructions.setCurrentDateForPeopleProperty("last_time_added_photo_via_wp_media_library_to_post");
+ break;
+ case EDITOR_ADDED_VIDEO_VIA_LOCAL_LIBRARY:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Added Video via Local Library");
+ instructions.
+ setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_added_video_via_local_library");
+ instructions.setCurrentDateForPeopleProperty("last_time_added_video_via_local_library_to_post");
+ break;
+ case EDITOR_ADDED_VIDEO_VIA_WP_MEDIA_LIBRARY:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Added Video via WP Media Library");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement(
+ "number_of_times_added_video_via_wp_media_library");
+ instructions.setCurrentDateForPeopleProperty("last_time_added_video_via_wp_media_library_to_post");
+ break;
+ case EDITOR_PUBLISHED_POST:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Published Post");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_published_post");
+ instructions.setCurrentDateForPeopleProperty("last_time_published_post");
+ break;
+ case EDITOR_UPDATED_POST:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Updated Post");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_updated_post");
+ instructions.setCurrentDateForPeopleProperty("last_time_updated_post");
+ break;
+ case EDITOR_SCHEDULED_POST:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Scheduled Post");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_scheduled_post");
+ instructions.setCurrentDateForPeopleProperty("last_time_scheduled_post");
+ break;
+ case EDITOR_TAPPED_BLOCKQUOTE:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped Blockquote Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_blockquote");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_blockquote_in_editor");
+ break;
+ case EDITOR_TAPPED_BOLD:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped Bold Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_bold");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_bold_in_editor");
+ break;
+ case EDITOR_TAPPED_IMAGE:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped Image Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_image");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_image_in_editor");
+ break;
+ case EDITOR_TAPPED_ITALIC:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped Italics Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_italic");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_italic_in_editor");
+ break;
+ case EDITOR_TAPPED_LINK:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped Link Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_link");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_link_in_editor");
+ break;
+ case EDITOR_TAPPED_MORE:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped More Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_more");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_more_in_editor");
+ break;
+ case EDITOR_TAPPED_STRIKETHROUGH:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped Strikethrough Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_strikethrough");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_strikethrough_in_editor");
+ break;
+ case EDITOR_TAPPED_UNDERLINE:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped Underline Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_underline");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_underline_in_editor");
+ break;
+ case EDITOR_TAPPED_HTML:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped HTML Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_html");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_html_in_editor");
+ break;
+ case EDITOR_TAPPED_ORDERED_LIST:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped Ordered List Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_ordered_list");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_ordered_list_in_editor");
+ break;
+ case EDITOR_TAPPED_UNLINK:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped Unlink Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_unlink");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_unlink_in_editor");
+ break;
+ case EDITOR_TAPPED_UNORDERED_LIST:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Editor - Tapped Unordered List Button");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_editor_tapped_unordered_list");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_unordered_list_in_editor");
+ break;
+ case NOTIFICATIONS_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Notifications - Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_notifications");
+ instructions.setCurrentDateForPeopleProperty("last_time_accessed_notifications");
+ break;
+ case NOTIFICATIONS_OPENED_NOTIFICATION_DETAILS:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Notifications - Opened Notification Details");
+ instructions.
+ setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_opened_notification_details");
+ instructions.setCurrentDateForPeopleProperty("last_time_opened_notification_details");
+ break;
+ case NOTIFICATION_APPROVED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsWithSuperPropertyAndPeoplePropertyIncrementor(
+ "number_of_notifications_approved");
+ break;
+ case NOTIFICATION_UNAPPROVED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsWithSuperPropertyAndPeoplePropertyIncrementor(
+ "number_of_notifications_unapproved");
+ break;
+ case NOTIFICATION_REPLIED_TO:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsWithSuperPropertyAndPeoplePropertyIncrementor(
+ "number_of_notifications_replied_to");
+ break;
+ case NOTIFICATION_TRASHED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsWithSuperPropertyAndPeoplePropertyIncrementor(
+ "number_of_notifications_trashed");
+ break;
+ case NOTIFICATION_FLAGGED_AS_SPAM:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsWithSuperPropertyAndPeoplePropertyIncrementor(
+ "number_of_notifications_flagged_as_spam");
+ break;
+ case NOTIFICATION_LIKED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Notifications - Liked Comment");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_comment_likes_from_notification");
+ break;
+ case NOTIFICATION_UNLIKED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Notifications - Unliked Comment");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_comment_unlikes_from_notification");
+ break;
+ case OPENED_POSTS:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Site Menu - Opened Posts");
+ break;
+ case OPENED_PAGES:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Site Menu - Opened Pages");
+ break;
+ case OPENED_COMMENTS:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Site Menu - Opened Comments");
+ break;
+ case OPENED_VIEW_SITE:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Site Menu - Opened View Site");
+ break;
+ case OPENED_VIEW_ADMIN:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Site Menu - Opened View Admin");
+ break;
+ case OPENED_MEDIA_LIBRARY:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Site Menu - Opened Media Library");
+ break;
+ case OPENED_BLOG_SETTINGS:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Site Menu - Opened Site Settings");
+ break;
+ case OPENED_ACCOUNT_SETTINGS:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Opened Account Settings");
+ break;
+ case OPENED_APP_SETTINGS:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Opened App Settings");
+ break;
+ case OPENED_MY_PROFILE:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Opened My Profile");
+ break;
+ case OPENED_PEOPLE_MANAGEMENT:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("People Management - Accessed List");
+ break;
+ case OPENED_PERSON:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("People Management - Accessed Details");
+ break;
+ case CREATED_ACCOUNT:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Created Account");
+ instructions.setCurrentDateForPeopleProperty("$created");
+ instructions.addSuperPropertyToFlag("created_account_on_mobile");
+ break;
+ case CREATED_SITE:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Created Site");
+ break;
+ case SHARED_ITEM:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsWithSuperPropertyAndPeoplePropertyIncrementor("number_of_items_shared");
+ break;
+ case ADDED_SELF_HOSTED_SITE:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Added Self Hosted Site");
+ instructions.setCurrentDateForPeopleProperty("last_time_added_self_hosted_site");
+ break;
+ case SIGNED_IN:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Signed In");
+ break;
+ case SIGNED_INTO_JETPACK:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Signed into Jetpack");
+ instructions.addSuperPropertyToFlag("jetpack_user");
+ instructions.addSuperPropertyToFlag("dotcom_user");
+ break;
+ case ACCOUNT_LOGOUT:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Logged Out");
+ break;
+ case PERFORMED_JETPACK_SIGN_IN_FROM_STATS_SCREEN:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Signed into Jetpack from Stats Screen");
+ break;
+ case STATS_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_stats");
+ instructions.setCurrentDateForPeopleProperty("last_time_accessed_stats");
+ break;
+ case STATS_INSIGHTS_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Insights Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_insights_screen_stats");
+ instructions.setCurrentDateForPeopleProperty("last_time_accessed_insights_screen_stats");
+ break;
+ case STATS_PERIOD_DAYS_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Period Days Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_days_screen_stats");
+ instructions.setCurrentDateForPeopleProperty("last_time_accessed_days_screen_stats");
+ break;
+ case STATS_PERIOD_WEEKS_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Period Weeks Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_weeks_screen_stats");
+ instructions.setCurrentDateForPeopleProperty("last_time_accessed_weeks_screen_stats");
+ break;
+ case STATS_PERIOD_MONTHS_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Period Months Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_months_screen_stats");
+ instructions.setCurrentDateForPeopleProperty("last_time_accessed_months_screen_stats");
+ break;
+ case STATS_PERIOD_YEARS_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Period Years Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_years_screen_stats");
+ instructions.setCurrentDateForPeopleProperty("last_time_accessed_years_screen_stats");
+ break;
+ case STATS_VIEW_ALL_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - View All Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_view_all_screen_stats");
+ instructions.setCurrentDateForPeopleProperty("last_time_accessed_view_all_screen_stats");
+ break;
+ case STATS_SINGLE_POST_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Single Post Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_single_post_screen_stats");
+ instructions.setCurrentDateForPeopleProperty("last_time_accessed_single_post_screen_stats");
+ break;
+ case STATS_TAPPED_BAR_CHART:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Tapped Bar Chart");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_tapped_stats_bar_chart");
+ instructions.setCurrentDateForPeopleProperty("last_time_tapped_stats_bar_chart");
+ break;
+ case STATS_SCROLLED_TO_BOTTOM:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Scrolled to Bottom");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_scrolled_to_bottom_of_stats");
+ instructions.setCurrentDateForPeopleProperty("last_time_scrolled_to_bottom_of_stats");
+ break;
+ case STATS_SELECTED_INSTALL_JETPACK:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Selected Install Jetpack");
+ break;
+ case STATS_SELECTED_CONNECT_JETPACK:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Selected Connect Jetpack");
+ break;
+ case STATS_WIDGET_ADDED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Widget Added");
+ break;
+ case STATS_WIDGET_REMOVED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Widget Removed");
+ break;
+ case STATS_WIDGET_TAPPED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Stats - Widget Tapped");
+ break;
+ case PUSH_NOTIFICATION_RECEIVED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Push Notification - Received");
+ break;
+ case PUSH_NOTIFICATION_TAPPED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Push Notification - Alert Tapped");
+ break;
+ case SUPPORT_OPENED_HELPSHIFT_SCREEN:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Support - Opened Helpshift Screen");
+ instructions.addSuperPropertyToFlag("opened_helpshift_screen");
+ break;
+ case SUPPORT_SENT_REPLY_TO_SUPPORT_MESSAGE:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Support - Replied to Helpshift");
+ instructions.addSuperPropertyToFlag("support_replied_to_helpshift");
+ break;
+ case LOGIN_MAGIC_LINK_EXITED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Login - Magic Link exited");
+ break;
+ case LOGIN_MAGIC_LINK_FAILED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Login - Magic Link failed");
+ break;
+ case LOGIN_MAGIC_LINK_OPENED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Login - Magic Link opened");
+ break;
+ case LOGIN_MAGIC_LINK_REQUESTED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Login - Magic Link requested");
+ break;
+ case LOGIN_MAGIC_LINK_SUCCEEDED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Login - Magic Link succeeded");
+ break;
+ case LOGIN_FAILED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Login - Failed Login");
+ break;
+ case LOGIN_FAILED_TO_GUESS_XMLRPC:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Login - Failed To Guess XMLRPC");
+ break;
+ case LOGIN_INSERTED_INVALID_URL:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Login - Inserted Invalid URL");
+ break;
+ case LOGIN_AUTOFILL_CREDENTIALS_FILLED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Login - Auto Fill Credentials Filled");
+ break;
+ case LOGIN_AUTOFILL_CREDENTIALS_UPDATED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Login - Auto Fill Credentials Updated");
+ break;
+ case PERSON_REMOVED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("People Management - Removed Person");
+ break;
+ case PERSON_UPDATED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("People Management - Updated Person");
+ break;
+ case PUSH_AUTHENTICATION_APPROVED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Push Authentication - Approved");
+ break;
+ case PUSH_AUTHENTICATION_EXPIRED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Push Authentication - Expired");
+ break;
+ case PUSH_AUTHENTICATION_FAILED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Push Authentication - Failed");
+ break;
+ case PUSH_AUTHENTICATION_IGNORED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Push Authentication - Ignored");
+ break;
+ case NOTIFICATION_SETTINGS_LIST_OPENED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Notification Settings - Accessed List");
+ break;
+ case NOTIFICATION_SETTINGS_STREAMS_OPENED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Notification Settings - Accessed Stream");
+ break;
+ case NOTIFICATION_SETTINGS_DETAILS_OPENED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Notification Settings - Accessed Details");
+ break;
+ case ME_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me Tab - Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_me_tab");
+ break;
+ case ME_GRAVATAR_TAPPED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Tapped Gravatar");
+ break;
+ case ME_GRAVATAR_TOOLTIP_TAPPED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Tapped Gravatar Tooltip");
+ break;
+ case ME_GRAVATAR_PERMISSIONS_INTERRUPTED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Gravatar Permissions Interrupted");
+ break;
+ case ME_GRAVATAR_PERMISSIONS_DENIED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Gravatar Permissions Denied");
+ break;
+ case ME_GRAVATAR_PERMISSIONS_ACCEPTED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Gravatar Permissions Accepted");
+ break;
+ case ME_GRAVATAR_SHOT_NEW:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Gravatar Shot New Photo");
+ break;
+ case ME_GRAVATAR_GALLERY_PICKED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Gravatar Picked From Gallery");
+ break;
+ case ME_GRAVATAR_CROPPED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Gravatar Cropped");
+ break;
+ case ME_GRAVATAR_UPLOADED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Gravatar Uploaded");
+ break;
+ case ME_GRAVATAR_UPLOAD_UNSUCCESSFUL:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Gravatar Upload Unsuccessful");
+ break;
+ case ME_GRAVATAR_UPLOAD_EXCEPTION:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Me - Gravatar Upload Exception");
+ break;
+ case MY_SITE_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("My Site - Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_my_site");
+ break;
+ case THEMES_ACCESSED_THEMES_BROWSER:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Themes - Accessed Theme Browser");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_accessed_theme_browser");
+ instructions.setCurrentDateForPeopleProperty("last_time_accessed_theme_browser");
+ break;
+ case THEMES_ACCESSED_SEARCH:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Themes - Accessed Theme");
+ break;
+ case THEMES_CHANGED_THEME:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Themes - Changed Theme");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_changed_theme");
+ instructions.setCurrentDateForPeopleProperty("last_time_changed_theme");
+ break;
+ case THEMES_PREVIEWED_SITE:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Themes - Previewed Theme for Site");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_previewed_a_theme");
+ instructions.setCurrentDateForPeopleProperty("last_time_previewed_a_theme");
+ break;
+ case THEMES_DEMO_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Themes - Demo Accessed");
+ break;
+ case THEMES_CUSTOMIZE_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Themes - Customize Accessed");
+ break;
+ case THEMES_SUPPORT_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Themes - Support Accessed");
+ break;
+ case THEMES_DETAILS_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Themes - Details Accessed");
+ break;
+ case ACCOUNT_SETTINGS_LANGUAGE_CHANGED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Account Settings - Changed Language");
+ break;
+ case SITE_SETTINGS_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Settings - Site Settings Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_settings_accessed");
+ break;
+ case SITE_SETTINGS_ACCESSED_MORE_SETTINGS:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Settings - More Settings Accessed");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_more_settings_accessed");
+ break;
+ case SITE_SETTINGS_ADDED_LIST_ITEM:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Settings - Added List Item");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_list_items_added");
+ break;
+ case SITE_SETTINGS_DELETED_LIST_ITEMS:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Settings - Site Deleted List Items");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_list_items_were_deleted");
+ break;
+ case SITE_SETTINGS_HINT_TOAST_SHOWN:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Settings - Preference Hint Shown");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_preference_hints_viewed");
+ break;
+ case SITE_SETTINGS_LEARN_MORE_CLICKED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Settings - Learn More Clicked");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_learn_more_clicked");
+ break;
+ case SITE_SETTINGS_LEARN_MORE_LOADED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Settings - Learn More Loaded");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_learn_more_seen");
+ break;
+ case SITE_SETTINGS_SAVED_REMOTELY:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.
+ mixpanelInstructionsForEventName("Settings - Saved Remotely");
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_settings_updated_remotely");
+ break;
+ case SITE_SETTINGS_START_OVER_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Start Over Accessed");
+ break;
+ case SITE_SETTINGS_START_OVER_CONTACT_SUPPORT_CLICKED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Start Over Contact Support Clicked");
+ break;
+ case SITE_SETTINGS_EXPORT_SITE_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Export Site Accessed");
+ break;
+ case SITE_SETTINGS_EXPORT_SITE_REQUESTED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Export Site Requested");
+ break;
+ case SITE_SETTINGS_EXPORT_SITE_RESPONSE_OK:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Export Site Response OK");
+ break;
+ case SITE_SETTINGS_EXPORT_SITE_RESPONSE_ERROR:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Export Site Response Error");
+ break;
+ case SITE_SETTINGS_DELETE_SITE_ACCESSED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Delete Site Accessed");
+ break;
+ case SITE_SETTINGS_DELETE_SITE_PURCHASES_REQUESTED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Delete Site Purchases Requested");
+ break;
+ case SITE_SETTINGS_DELETE_SITE_PURCHASES_SHOWN:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Delete Site Purchases Shown");
+ break;
+ case SITE_SETTINGS_DELETE_SITE_PURCHASES_SHOW_CLICKED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Delete Site Show Purchases Clicked");
+ break;
+ case SITE_SETTINGS_DELETE_SITE_REQUESTED:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Delete Site Requested");
+ break;
+ case SITE_SETTINGS_DELETE_SITE_RESPONSE_OK:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Delete Site Response OK");
+ break;
+ case SITE_SETTINGS_DELETE_SITE_RESPONSE_ERROR:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("Settings - Delete Site Response Error");
+ break;
+ case ABTEST_START:
+ instructions = AnalyticsTrackerMixpanelInstructionsForStat.mixpanelInstructionsForEventName("AB Test - Started");
+ break;
+ case TRAIN_TRACKS_RENDER: case TRAIN_TRACKS_INTERACT:
+ // Do nothing. These events are just for Tracks.
+ instructions = null;
+ break;
+ default:
+ instructions = null;
+ break;
+ }
+ return instructions;
+ }
+
+ private void incrementPeopleProperty(String property) {
+ try {
+ mMixpanel.getPeople().increment(property, 1);
+ } catch (OutOfMemoryError outOfMemoryError) {
+ // ignore exception
+ }
+ }
+
+ @SuppressLint("CommitPrefEdits")
+ private void incrementSuperProperty(String property) {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
+ int propertyCount = preferences.getInt(property, 0);
+ propertyCount++;
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.putInt(property, propertyCount);
+ editor.commit();
+
+ try {
+ JSONObject superProperties = mMixpanel.getSuperProperties();
+ superProperties.put(property, propertyCount);
+ mMixpanel.registerSuperProperties(superProperties);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ }
+ }
+
+ private void flagSuperProperty(String property) {
+ try {
+ JSONObject superProperties = mMixpanel.getSuperProperties();
+ superProperties.put(property, true);
+ mMixpanel.registerSuperProperties(superProperties);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ }
+ }
+
+ private void savePropertyValueForStat(String property, Object value, AnalyticsTracker.Stat stat) {
+ JSONObject properties = mAggregatedProperties.get(stat);
+ if (properties == null) {
+ properties = new JSONObject();
+ mAggregatedProperties.put(stat, properties);
+ }
+
+ try {
+ properties.put(property, value);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ }
+ }
+
+ private JSONObject propertiesForStat(AnalyticsTracker.Stat stat) {
+ return mAggregatedProperties.get(stat);
+ }
+
+ private void removePropertiesForStat(AnalyticsTracker.Stat stat) {
+ mAggregatedProperties.remove(stat);
+ }
+
+ private Object propertyForStat(String property, AnalyticsTracker.Stat stat) {
+ JSONObject properties = mAggregatedProperties.get(stat);
+ if (properties == null) {
+ return null;
+ }
+
+ try {
+ return properties.get(property);
+ } catch (JSONException e) {
+ // We are okay with swallowing this exception as the next line will just return a null value
+ }
+
+ return null;
+ }
+
+ private void incrementProperty(String property, AnalyticsTracker.Stat stat) {
+ Object currentValueObj = propertyForStat(property, stat);
+ int currentValue = 1;
+ if (currentValueObj != null) {
+ currentValue = Integer.valueOf(currentValueObj.toString());
+ currentValue++;
+ }
+
+ savePropertyValueForStat(property, Integer.toString(currentValue), stat);
+ }
+
+
+ public void incrementSessionCount() {
+ // Tracking session count will help us isolate users who just installed the app
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
+ int sessionCount = preferences.getInt(SESSION_COUNT, 0);
+ sessionCount++;
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.putInt(SESSION_COUNT, sessionCount);
+ editor.apply();
+ }
+}
diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanelInstructionsForStat.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanelInstructionsForStat.java
new file mode 100644
index 000000000..38aefa988
--- /dev/null
+++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanelInstructionsForStat.java
@@ -0,0 +1,140 @@
+package org.wordpress.android.analytics;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+public class AnalyticsTrackerMixpanelInstructionsForStat {
+ private String mMixpanelEventName;
+ private String mSuperPropertyToIncrement;
+ private String mPeoplePropertyToIncrement;
+ private ArrayList<String> mSuperPropertiesToFlag;
+ private AnalyticsTracker.Stat mStatToAttachProperty;
+ private AnalyticsTracker.Stat mStat;
+ private String mPropertyToIncrement;
+ private boolean mDisableForSelfHosted;
+ private Map<String, Object> mPeoplePropertiesToAssign;
+
+ public AnalyticsTrackerMixpanelInstructionsForStat() {
+ mSuperPropertiesToFlag = new ArrayList<String>();
+ mPeoplePropertiesToAssign = new HashMap<String, Object>();
+ }
+
+ public static AnalyticsTrackerMixpanelInstructionsForStat mixpanelInstructionsForEventName(String eventName) {
+ AnalyticsTrackerMixpanelInstructionsForStat instructions = new AnalyticsTrackerMixpanelInstructionsForStat();
+ instructions.setMixpanelEventName(eventName);
+ return instructions;
+ }
+
+ public static AnalyticsTrackerMixpanelInstructionsForStat
+ mixpanelInstructionsWithSuperPropertyAndPeoplePropertyIncrementor(String property) {
+ AnalyticsTrackerMixpanelInstructionsForStat instructions = new AnalyticsTrackerMixpanelInstructionsForStat();
+ instructions.setSuperPropertyAndPeoplePropertyToIncrement(property);
+ return instructions;
+ }
+
+ public static AnalyticsTrackerMixpanelInstructionsForStat mixpanelInstructionsWithPropertyIncrementor(
+ String property, AnalyticsTracker.Stat stat) {
+ AnalyticsTrackerMixpanelInstructionsForStat instructions = new AnalyticsTrackerMixpanelInstructionsForStat();
+ instructions.setStatToAttachProperty(stat);
+ instructions.setPropertyToIncrement(property);
+ return instructions;
+ }
+
+ public String getMixpanelEventName() {
+ return mMixpanelEventName;
+ }
+
+ public void setMixpanelEventName(String mixpanelEventName) {
+ this.mMixpanelEventName = mixpanelEventName;
+ }
+
+ public String getSuperPropertyToIncrement() {
+ return mSuperPropertyToIncrement;
+ }
+
+ public void setSuperPropertyToIncrement(String superPropertyToIncrement) {
+ this.mSuperPropertyToIncrement = superPropertyToIncrement;
+ }
+
+ public String getPeoplePropertyToIncrement() {
+ return mPeoplePropertyToIncrement;
+ }
+
+ public void setPeoplePropertyToIncrement(String peoplePropertyToIncrement) {
+ this.mPeoplePropertyToIncrement = peoplePropertyToIncrement;
+ }
+
+ public void setSuperPropertyAndPeoplePropertyToIncrement(String property) {
+ setSuperPropertyToIncrement(property);
+ setPeoplePropertyToIncrement(property);
+ }
+
+ public AnalyticsTracker.Stat getStatToAttachProperty() {
+ return mStatToAttachProperty;
+ }
+
+ public void setStatToAttachProperty(AnalyticsTracker.Stat statToAttachProperty) {
+ this.mStatToAttachProperty = statToAttachProperty;
+ }
+
+ public String getPropertyToIncrement() {
+ return mPropertyToIncrement;
+ }
+
+ public void setPropertyToIncrement(String propertyToIncrement) {
+ this.mPropertyToIncrement = propertyToIncrement;
+ }
+
+ public boolean getDisableForSelfHosted() {
+ return mDisableForSelfHosted;
+ }
+
+ public void setDisableForSelfHosted(boolean disableForSelfHosted) {
+ this.mDisableForSelfHosted = disableForSelfHosted;
+ }
+
+ public AnalyticsTracker.Stat getStat() {
+ return mStat;
+ }
+
+ public void setStat(AnalyticsTracker.Stat stat) {
+ this.mStat = stat;
+ }
+
+ public ArrayList<String> getSuperPropertiesToFlag() {
+ return mSuperPropertiesToFlag;
+ }
+
+ public void addSuperPropertyToFlag(String superPropertyToFlag) {
+ if (!mSuperPropertiesToFlag.contains(superPropertyToFlag)) {
+ mSuperPropertiesToFlag.add(superPropertyToFlag);
+ }
+ }
+
+ private static final ThreadLocal<DateFormat> AnalyticsDateFormat = new ThreadLocal<DateFormat>() {
+ @Override
+ protected DateFormat initialValue() {
+ DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
+ format.setTimeZone(TimeZone.getTimeZone("UTC"));
+ return format;
+ }
+ };
+
+ public void setCurrentDateForPeopleProperty(String property) {
+ setPeoplePropertyToValue(property, AnalyticsDateFormat.get().format(new Date()));
+ }
+
+ public void setPeoplePropertyToValue(String property, Object value) {
+ mPeoplePropertiesToAssign.put(property, value);
+ }
+
+ public Map<String, Object> getPeoplePropertiesToAssign() {
+ return mPeoplePropertiesToAssign;
+ }
+}
diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java
new file mode 100644
index 000000000..76c181b9b
--- /dev/null
+++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java
@@ -0,0 +1,589 @@
+package org.wordpress.android.analytics;
+
+import android.content.Context;
+
+import com.automattic.android.tracks.TracksClient;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class AnalyticsTrackerNosara extends Tracker {
+
+ private static final String JETPACK_USER = "jetpack_user";
+ private static final String NUMBER_OF_BLOGS = "number_of_blogs";
+ private static final String TRACKS_ANON_ID = "nosara_tracks_anon_id";
+
+ private static final String EVENTS_PREFIX = "wpandroid_";
+
+ private TracksClient mNosaraClient;
+
+ public AnalyticsTrackerNosara(Context context) throws IllegalArgumentException {
+ super(context);
+ mNosaraClient = TracksClient.getClient(context);
+ }
+
+ String getAnonIdPrefKey() {
+ return TRACKS_ANON_ID;
+ }
+
+ @Override
+ public void track(AnalyticsTracker.Stat stat) {
+ track(stat, null);
+ }
+
+ @Override
+ public void track(AnalyticsTracker.Stat stat, Map<String, ?> properties) {
+ if (mNosaraClient == null) {
+ return;
+ }
+
+ String eventName = getEventNameForStat(stat);
+ if (eventName == null) {
+ AppLog.w(AppLog.T.STATS, "There is NO match for the event " + stat.name() + "stat");
+ return;
+ }
+
+ Map<String, Object> predefinedEventProperties = new HashMap<String, Object>();
+ switch (stat) {
+ case EDITOR_ADDED_PHOTO_VIA_LOCAL_LIBRARY:
+ predefinedEventProperties.put("via", "local_library");
+ break;
+ case EDITOR_ADDED_PHOTO_VIA_WP_MEDIA_LIBRARY:
+ predefinedEventProperties.put("via", "media_library");
+ break;
+ case EDITOR_ADDED_VIDEO_VIA_LOCAL_LIBRARY:
+ predefinedEventProperties.put("via", "local_library");
+ break;
+ case EDITOR_ADDED_VIDEO_VIA_WP_MEDIA_LIBRARY:
+ predefinedEventProperties.put("via", "media_library");
+ break;
+ case EDITOR_TAPPED_BLOCKQUOTE:
+ predefinedEventProperties.put("button", "blockquote");
+ break;
+ case EDITOR_TAPPED_BOLD:
+ predefinedEventProperties.put("button", "bold");
+ break;
+ case EDITOR_TAPPED_IMAGE:
+ predefinedEventProperties.put("button", "image");
+ break;
+ case EDITOR_TAPPED_ITALIC:
+ predefinedEventProperties.put("button", "italic");
+ break;
+ case EDITOR_TAPPED_LINK:
+ predefinedEventProperties.put("button", "link");
+ break;
+ case EDITOR_TAPPED_MORE:
+ predefinedEventProperties.put("button", "more");
+ break;
+ case EDITOR_TAPPED_STRIKETHROUGH:
+ predefinedEventProperties.put("button", "strikethrough");
+ break;
+ case EDITOR_TAPPED_UNDERLINE:
+ predefinedEventProperties.put("button", "underline");
+ break;
+ case EDITOR_TAPPED_HTML:
+ predefinedEventProperties.put("button", "html");
+ break;
+ case EDITOR_TAPPED_ORDERED_LIST:
+ predefinedEventProperties.put("button", "ordered_list");
+ break;
+ case EDITOR_TAPPED_UNLINK:
+ predefinedEventProperties.put("button", "unlink");
+ break;
+ case EDITOR_TAPPED_UNORDERED_LIST:
+ predefinedEventProperties.put("button", "unordered_list");
+ break;
+ case OPENED_POSTS:
+ predefinedEventProperties.put("menu_item", "posts");
+ break;
+ case OPENED_PAGES:
+ predefinedEventProperties.put("menu_item", "pages");
+ break;
+ case OPENED_COMMENTS:
+ predefinedEventProperties.put("menu_item", "comments");
+ break;
+ case OPENED_VIEW_SITE:
+ predefinedEventProperties.put("menu_item", "view_site");
+ break;
+ case OPENED_VIEW_ADMIN:
+ predefinedEventProperties.put("menu_item", "view_admin");
+ break;
+ case OPENED_MEDIA_LIBRARY:
+ predefinedEventProperties.put("menu_item", "media_library");
+ break;
+ case OPENED_BLOG_SETTINGS:
+ predefinedEventProperties.put("menu_item", "site_settings");
+ break;
+ case STATS_PERIOD_DAYS_ACCESSED:
+ predefinedEventProperties.put("period", "days");
+ break;
+ case STATS_PERIOD_WEEKS_ACCESSED:
+ predefinedEventProperties.put("period", "weeks");
+ break;
+ case STATS_PERIOD_MONTHS_ACCESSED:
+ predefinedEventProperties.put("period", "months");
+ break;
+ case STATS_PERIOD_YEARS_ACCESSED:
+ predefinedEventProperties.put("period", "years");
+ break;
+ }
+
+ final String user;
+ final TracksClient.NosaraUserType userType;
+ if (getWordPressComUserName() != null) {
+ user = getWordPressComUserName();
+ userType = TracksClient.NosaraUserType.WPCOM;
+ } else {
+ // This is just a security checks since the anonID is already available here.
+ // refresh metadata is called on login/logout/startup and it loads/generates the anonId when necessary.
+ if (getAnonID() == null) {
+ user = generateNewAnonID();
+ } else {
+ user = getAnonID();
+ }
+ userType = TracksClient.NosaraUserType.ANON;
+ }
+
+
+ // create the merged JSON Object of properties
+ // Properties defined by the user have precedence over the default ones pre-defined at "event level"
+ final JSONObject propertiesToJSON;
+ if (properties != null && properties.size() > 0) {
+ propertiesToJSON = new JSONObject(properties);
+ for (String key : predefinedEventProperties.keySet()) {
+ try {
+ if (propertiesToJSON.has(key)) {
+ AppLog.w(AppLog.T.STATS, "The user has defined a property named: '" + key + "' that will override" +
+ "the same property pre-defined at event level. This may generate unexpected behavior!!");
+ AppLog.w(AppLog.T.STATS, "User value: " + propertiesToJSON.get(key).toString() + " - pre-defined value: " +
+ predefinedEventProperties.get(key).toString());
+ } else {
+ propertiesToJSON.put(key, predefinedEventProperties.get(key));
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.STATS, "Error while merging user-defined properties with pre-defined properties", e);
+ }
+ }
+ } else{
+ propertiesToJSON = new JSONObject(predefinedEventProperties);
+ }
+
+ if (propertiesToJSON.length() > 0) {
+ mNosaraClient.track(EVENTS_PREFIX + eventName, propertiesToJSON, user, userType);
+ } else {
+ mNosaraClient.track(EVENTS_PREFIX + eventName, user, userType);
+ }
+ }
+
+
+
+ @Override
+ public void endSession() {
+ this.flush();
+ }
+
+ @Override
+ public void flush() {
+ if (mNosaraClient == null) {
+ return;
+ }
+ mNosaraClient.flush();
+ }
+
+ @Override
+ public void refreshMetadata(AnalyticsMetadata metadata) {
+ if (mNosaraClient == null) {
+ return;
+ }
+
+ try {
+ JSONObject properties = new JSONObject();
+ properties.put(JETPACK_USER, metadata.isJetpackUser());
+ properties.put(NUMBER_OF_BLOGS, metadata.getNumBlogs());
+ mNosaraClient.registerUserProperties(properties);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ }
+
+ if (metadata.isUserConnected() && metadata.isWordPressComUser()) {
+ setWordPressComUserName(metadata.getUsername());
+ // Re-unify the user
+ if (getAnonID() != null) {
+ mNosaraClient.trackAliasUser(getWordPressComUserName(), getAnonID(), TracksClient.NosaraUserType.WPCOM);
+ clearAnonID();
+ }
+ } else {
+ // Not wpcom connected. Check if anonID is already present
+ setWordPressComUserName(null);
+ if (getAnonID() == null) {
+ generateNewAnonID();
+ }
+ }
+
+
+ }
+
+
+ @Override
+ public void clearAllData() {
+ super.clearAllData();
+ if (mNosaraClient == null) {
+ return;
+ }
+ mNosaraClient.clearUserProperties();
+ }
+
+ @Override
+ public void registerPushNotificationToken(String regId) {
+ return;
+ }
+
+ public static String getEventNameForStat(AnalyticsTracker.Stat stat) {
+ switch (stat) {
+ case APPLICATION_OPENED:
+ return "application_opened";
+ case APPLICATION_CLOSED:
+ return "application_closed";
+ case APPLICATION_INSTALLED:
+ return "application_installed";
+ case APPLICATION_UPGRADED:
+ return "application_upgraded";
+ case READER_ACCESSED:
+ return "reader_accessed";
+ case READER_ARTICLE_COMMENTED_ON:
+ return "reader_article_commented_on";
+ case READER_ARTICLE_LIKED:
+ return "reader_article_liked";
+ case READER_ARTICLE_OPENED:
+ return "reader_article_opened";
+ case READER_ARTICLE_UNLIKED:
+ return "reader_article_unliked";
+ case READER_BLOG_BLOCKED:
+ return "reader_blog_blocked";
+ case READER_BLOG_FOLLOWED:
+ return "reader_site_followed";
+ case READER_BLOG_PREVIEWED:
+ return "reader_blog_previewed";
+ case READER_BLOG_UNFOLLOWED:
+ return "reader_site_unfollowed";
+ case READER_DISCOVER_VIEWED:
+ return "reader_discover_viewed";
+ case READER_INFINITE_SCROLL:
+ return "reader_infinite_scroll_performed";
+ case READER_LIST_FOLLOWED:
+ return "reader_list_followed";
+ case READER_LIST_LOADED:
+ return "reader_list_loaded";
+ case READER_LIST_PREVIEWED:
+ return "reader_list_previewed";
+ case READER_LIST_UNFOLLOWED:
+ return "reader_list_unfollowed";
+ case READER_TAG_FOLLOWED:
+ return "reader_reader_tag_followed";
+ case READER_TAG_LOADED:
+ return "reader_tag_loaded";
+ case READER_TAG_PREVIEWED:
+ return "reader_tag_previewed";
+ case READER_SEARCH_LOADED:
+ return "reader_search_loaded";
+ case READER_SEARCH_PERFORMED:
+ return "reader_search_performed";
+ case READER_SEARCH_RESULT_TAPPED:
+ return "reader_searchcard_clicked";
+ case READER_TAG_UNFOLLOWED:
+ return "reader_reader_tag_unfollowed";
+ case READER_RELATED_POST_CLICKED:
+ return "reader_related_post_clicked";
+ case EDITOR_CREATED_POST:
+ return "editor_post_created";
+ case EDITOR_SAVED_DRAFT:
+ return "editor_draft_saved";
+ case EDITOR_DISCARDED_CHANGES:
+ return "editor_discarded_changes";
+ case EDITOR_EDITED_IMAGE:
+ return "editor_image_edited";
+ case EDITOR_ENABLED_NEW_VERSION:
+ return "editor_enabled_new_version";
+ case EDITOR_TOGGLED_OFF:
+ return "editor_toggled_off";
+ case EDITOR_TOGGLED_ON:
+ return "editor_toggled_on";
+ case EDITOR_UPLOAD_MEDIA_FAILED:
+ return "editor_upload_media_failed";
+ case EDITOR_UPLOAD_MEDIA_RETRIED:
+ return "editor_upload_media_retried";
+ case EDITOR_CLOSED:
+ return "editor_closed";
+ case EDITOR_ADDED_PHOTO_VIA_LOCAL_LIBRARY:
+ return "editor_photo_added";
+ case EDITOR_ADDED_PHOTO_VIA_WP_MEDIA_LIBRARY:
+ return "editor_photo_added";
+ case EDITOR_ADDED_VIDEO_VIA_LOCAL_LIBRARY:
+ return "editor_video_added";
+ case EDITOR_ADDED_VIDEO_VIA_WP_MEDIA_LIBRARY:
+ return "editor_video_added";
+ case EDITOR_PUBLISHED_POST:
+ return "editor_post_published";
+ case EDITOR_UPDATED_POST:
+ return "editor_post_updated";
+ case EDITOR_SCHEDULED_POST:
+ return "editor_post_scheduled";
+ case EDITOR_TAPPED_BLOCKQUOTE:
+ return "editor_button_tapped";
+ case EDITOR_TAPPED_BOLD:
+ return "editor_button_tapped";
+ case EDITOR_TAPPED_IMAGE:
+ return "editor_button_tapped";
+ case EDITOR_TAPPED_ITALIC:
+ return "editor_button_tapped";
+ case EDITOR_TAPPED_LINK:
+ return "editor_button_tapped";
+ case EDITOR_TAPPED_MORE:
+ return "editor_button_tapped";
+ case EDITOR_TAPPED_STRIKETHROUGH:
+ return "editor_button_tapped";
+ case EDITOR_TAPPED_UNDERLINE:
+ return "editor_button_tapped";
+ case EDITOR_TAPPED_HTML:
+ return "editor_button_tapped";
+ case EDITOR_TAPPED_ORDERED_LIST:
+ return "editor_button_tapped";
+ case EDITOR_TAPPED_UNLINK:
+ return "editor_button_tapped";
+ case EDITOR_TAPPED_UNORDERED_LIST:
+ return "editor_button_tapped";
+ case NOTIFICATIONS_ACCESSED:
+ return "notifications_accessed";
+ case NOTIFICATIONS_OPENED_NOTIFICATION_DETAILS:
+ return "notifications_notification_details_opened";
+ case NOTIFICATION_APPROVED:
+ return "notifications_approved";
+ case NOTIFICATION_UNAPPROVED:
+ return "notifications_unapproved";
+ case NOTIFICATION_REPLIED_TO:
+ return "notifications_replied_to";
+ case NOTIFICATION_TRASHED:
+ return "notifications_trashed";
+ case NOTIFICATION_FLAGGED_AS_SPAM:
+ return "notifications_flagged_as_spam";
+ case NOTIFICATION_LIKED:
+ return "notifications_comment_liked";
+ case NOTIFICATION_UNLIKED:
+ return "notifications_comment_unliked";
+ case OPENED_POSTS:
+ return "site_menu_opened";
+ case OPENED_PAGES:
+ return "site_menu_opened";
+ case OPENED_COMMENTS:
+ return "site_menu_opened";
+ case OPENED_VIEW_SITE:
+ return "site_menu_opened";
+ case OPENED_VIEW_ADMIN:
+ return "site_menu_opened";
+ case OPENED_MEDIA_LIBRARY:
+ return "site_menu_opened";
+ case OPENED_BLOG_SETTINGS:
+ return "site_menu_opened";
+ case OPENED_ACCOUNT_SETTINGS:
+ return "account_settings_opened";
+ case OPENED_APP_SETTINGS:
+ return "app_settings_opened";
+ case OPENED_MY_PROFILE:
+ return "my_profile_opened";
+ case OPENED_PEOPLE_MANAGEMENT:
+ return "people_management_list_opened";
+ case OPENED_PERSON:
+ return "people_management_details_opened";
+ case CREATED_ACCOUNT:
+ return "account_created";
+ case CREATED_SITE:
+ return "site_created";
+ case SHARED_ITEM:
+ return "item_shared";
+ case ADDED_SELF_HOSTED_SITE:
+ return "self_hosted_blog_added";
+ case SIGNED_IN:
+ return "signed_in";
+ case SIGNED_INTO_JETPACK:
+ return "signed_into_jetpack";
+ case ACCOUNT_LOGOUT:
+ return "account_logout";
+ case PERFORMED_JETPACK_SIGN_IN_FROM_STATS_SCREEN:
+ return "stats_screen_signed_into_jetpack";
+ case STATS_ACCESSED:
+ return "stats_accessed";
+ case STATS_INSIGHTS_ACCESSED:
+ return "stats_insights_accessed";
+ case STATS_PERIOD_DAYS_ACCESSED:
+ return "stats_period_accessed";
+ case STATS_PERIOD_WEEKS_ACCESSED:
+ return "stats_period_accessed";
+ case STATS_PERIOD_MONTHS_ACCESSED:
+ return "stats_period_accessed";
+ case STATS_PERIOD_YEARS_ACCESSED:
+ return "stats_period_accessed";
+ case STATS_VIEW_ALL_ACCESSED:
+ return "stats_view_all_accessed";
+ case STATS_SINGLE_POST_ACCESSED:
+ return "stats_single_post_accessed";
+ case STATS_TAPPED_BAR_CHART:
+ return "stats_bar_chart_tapped";
+ case STATS_SCROLLED_TO_BOTTOM:
+ return "stats_scrolled_to_bottom";
+ case STATS_SELECTED_INSTALL_JETPACK:
+ return "stats_install_jetpack_selected";
+ case STATS_SELECTED_CONNECT_JETPACK:
+ return "stats_connect_jetpack_selected";
+ case STATS_WIDGET_ADDED:
+ return "stats_widget_added";
+ case STATS_WIDGET_REMOVED:
+ return "stats_widget_removed";
+ case STATS_WIDGET_TAPPED:
+ return "stats_widget_tapped";
+ case PUSH_NOTIFICATION_RECEIVED:
+ return "push_notification_received";
+ case PUSH_NOTIFICATION_TAPPED:
+ return "push_notification_alert_tapped";
+ case SUPPORT_OPENED_HELPSHIFT_SCREEN:
+ return "support_helpshift_screen_opened";
+ case SUPPORT_SENT_REPLY_TO_SUPPORT_MESSAGE:
+ return "support_reply_to_support_message_sent";
+ case LOGIN_MAGIC_LINK_EXITED:
+ return "login_magic_link_exited";
+ case LOGIN_MAGIC_LINK_FAILED:
+ return "login_magic_link_failed";
+ case LOGIN_MAGIC_LINK_OPENED:
+ return "login_magic_link_opened";
+ case LOGIN_MAGIC_LINK_REQUESTED:
+ return "login_magic_link_requested";
+ case LOGIN_MAGIC_LINK_SUCCEEDED:
+ return "login_magic_link_succeeded";
+ case LOGIN_FAILED:
+ return "login_failed_to_login";
+ case LOGIN_FAILED_TO_GUESS_XMLRPC:
+ return "login_failed_to_guess_xmlrpc";
+ case LOGIN_INSERTED_INVALID_URL:
+ return "login_inserted_invalid_url";
+ case LOGIN_AUTOFILL_CREDENTIALS_FILLED:
+ return "login_autofill_credentials_filled";
+ case LOGIN_AUTOFILL_CREDENTIALS_UPDATED:
+ return "login_autofill_credentials_updated";
+ case PERSON_REMOVED:
+ return "people_management_person_removed";
+ case PERSON_UPDATED:
+ return "people_management_person_updated";
+ case PUSH_AUTHENTICATION_APPROVED:
+ return "push_authentication_approved";
+ case PUSH_AUTHENTICATION_EXPIRED:
+ return "push_authentication_expired";
+ case PUSH_AUTHENTICATION_FAILED:
+ return "push_authentication_failed";
+ case PUSH_AUTHENTICATION_IGNORED:
+ return "push_authentication_ignored";
+ case NOTIFICATION_SETTINGS_LIST_OPENED:
+ return "notification_settings_list_opened";
+ case NOTIFICATION_SETTINGS_STREAMS_OPENED:
+ return "notification_settings_streams_opened";
+ case NOTIFICATION_SETTINGS_DETAILS_OPENED:
+ return "notification_settings_details_opened";
+ case ME_ACCESSED:
+ return "me_tab_accessed";
+ case ME_GRAVATAR_TAPPED:
+ return "me_gravatar_tapped";
+ case ME_GRAVATAR_TOOLTIP_TAPPED:
+ return "me_gravatar_tooltip_tapped";
+ case ME_GRAVATAR_PERMISSIONS_INTERRUPTED:
+ return "me_gravatar_permissions_interrupted";
+ case ME_GRAVATAR_PERMISSIONS_DENIED:
+ return "me_gravatar_permissions_denied";
+ case ME_GRAVATAR_PERMISSIONS_ACCEPTED:
+ return "me_gravatar_permissions_accepted";
+ case ME_GRAVATAR_SHOT_NEW:
+ return "me_gravatar_shot_new";
+ case ME_GRAVATAR_GALLERY_PICKED:
+ return "me_gravatar_gallery_picked";
+ case ME_GRAVATAR_CROPPED:
+ return "me_gravatar_cropped";
+ case ME_GRAVATAR_UPLOADED:
+ return "me_gravatar_uploaded";
+ case ME_GRAVATAR_UPLOAD_UNSUCCESSFUL:
+ return "me_gravatar_upload_unsuccessful";
+ case ME_GRAVATAR_UPLOAD_EXCEPTION:
+ return "me_gravatar_upload_exception";
+ case MY_SITE_ACCESSED:
+ return "my_site_tab_accessed";
+ case THEMES_ACCESSED_THEMES_BROWSER:
+ return "themes_theme_browser_accessed";
+ case THEMES_ACCESSED_SEARCH:
+ return "themes_search_accessed";
+ case THEMES_CHANGED_THEME:
+ return "themes_theme_changed";
+ case THEMES_PREVIEWED_SITE:
+ return "themes_theme_for_site_previewed";
+ case THEMES_DEMO_ACCESSED:
+ return "themes_demo_accessed";
+ case THEMES_CUSTOMIZE_ACCESSED:
+ return "themes_customize_accessed";
+ case THEMES_SUPPORT_ACCESSED:
+ return "themes_support_accessed";
+ case THEMES_DETAILS_ACCESSED:
+ return "themes_details_accessed";
+ case ACCOUNT_SETTINGS_LANGUAGE_CHANGED:
+ return "account_settings_language_changed";
+ case SITE_SETTINGS_ACCESSED:
+ return "site_settings_accessed";
+ case SITE_SETTINGS_ACCESSED_MORE_SETTINGS:
+ return "site_settings_more_settings_accessed";
+ case SITE_SETTINGS_ADDED_LIST_ITEM:
+ return "site_settings_added_list_item";
+ case SITE_SETTINGS_DELETED_LIST_ITEMS:
+ return "site_settings_deleted_list_items";
+ case SITE_SETTINGS_HINT_TOAST_SHOWN:
+ return "site_settings_hint_toast_shown";
+ case SITE_SETTINGS_LEARN_MORE_CLICKED:
+ return "site_settings_learn_more_clicked";
+ case SITE_SETTINGS_LEARN_MORE_LOADED:
+ return "site_settings_learn_more_loaded";
+ case SITE_SETTINGS_SAVED_REMOTELY:
+ return "site_settings_saved_remotely";
+ case SITE_SETTINGS_START_OVER_ACCESSED:
+ return "site_settings_start_over_accessed";
+ case SITE_SETTINGS_START_OVER_CONTACT_SUPPORT_CLICKED:
+ return "site_settings_start_over_contact_support_clicked";
+ case SITE_SETTINGS_EXPORT_SITE_ACCESSED:
+ return "site_settings_export_site_accessed";
+ case SITE_SETTINGS_EXPORT_SITE_REQUESTED:
+ return "site_settings_export_site_requested";
+ case SITE_SETTINGS_EXPORT_SITE_RESPONSE_OK:
+ return "site_settings_export_site_response_ok";
+ case SITE_SETTINGS_EXPORT_SITE_RESPONSE_ERROR:
+ return "site_settings_export_site_response_error";
+ case SITE_SETTINGS_DELETE_SITE_ACCESSED:
+ return "site_settings_delete_site_accessed";
+ case SITE_SETTINGS_DELETE_SITE_PURCHASES_REQUESTED:
+ return "site_settings_delete_site_purchases_requested";
+ case SITE_SETTINGS_DELETE_SITE_PURCHASES_SHOWN:
+ return "site_settings_delete_site_purchases_shown";
+ case SITE_SETTINGS_DELETE_SITE_PURCHASES_SHOW_CLICKED:
+ return "site_settings_delete_site_purchases_show_clicked";
+ case SITE_SETTINGS_DELETE_SITE_REQUESTED:
+ return "site_settings_delete_site_requested";
+ case SITE_SETTINGS_DELETE_SITE_RESPONSE_OK:
+ return "site_settings_delete_site_response_ok";
+ case SITE_SETTINGS_DELETE_SITE_RESPONSE_ERROR:
+ return "site_settings_delete_site_response_error";
+ case ABTEST_START:
+ return "abtest_start";
+ case TRAIN_TRACKS_RENDER:
+ return "traintracks_render";
+ case TRAIN_TRACKS_INTERACT:
+ return "traintracks_interact";
+ default:
+ return null;
+ }
+ }
+}
diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/Tracker.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/Tracker.java
new file mode 100644
index 000000000..e1b35f125
--- /dev/null
+++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/Tracker.java
@@ -0,0 +1,77 @@
+package org.wordpress.android.analytics;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import java.util.Map;
+import java.util.UUID;
+
+import org.wordpress.android.analytics.AnalyticsTracker.Stat;
+import org.wordpress.android.util.AppLog;
+
+public abstract class Tracker {
+ abstract void track(Stat stat);
+ abstract void track(Stat stat, Map<String, ?> properties);
+ abstract void endSession();
+ abstract void flush();
+ abstract void refreshMetadata(AnalyticsMetadata metadata);
+ abstract void registerPushNotificationToken(String regId);
+ abstract String getAnonIdPrefKey();
+
+ private String mAnonID = null; // do not access this variable directly. Use methods.
+ private String mWpcomUserName = null;
+ Context mContext;
+
+ public Tracker(Context context) throws IllegalArgumentException {
+ if (null == context) {
+ throw new IllegalArgumentException("Tracker requires a not-null context");
+ }
+ mContext = context;
+ }
+
+ void clearAllData() {
+ // Reset the anon ID here
+ clearAnonID();
+ setWordPressComUserName(null);
+ }
+
+ void clearAnonID() {
+ mAnonID = null;
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
+ if (preferences.contains(getAnonIdPrefKey())) {
+ final SharedPreferences.Editor editor = preferences.edit();
+ editor.remove(getAnonIdPrefKey());
+ editor.commit();
+ }
+ }
+
+ String getAnonID() {
+ if (mAnonID == null) {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
+ mAnonID = preferences.getString(getAnonIdPrefKey(), null);
+ }
+ return mAnonID;
+ }
+
+ String generateNewAnonID() {
+ String uuid = UUID.randomUUID().toString().replace("-", "");
+ AppLog.d(AppLog.T.STATS, "New anonID generated in " + this.getClass().getSimpleName() + ": " + uuid);
+
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
+ final SharedPreferences.Editor editor = preferences.edit();
+ editor.putString(getAnonIdPrefKey(), uuid);
+ editor.commit();
+
+ mAnonID = uuid;
+ return uuid;
+ }
+
+ String getWordPressComUserName() {
+ return mWpcomUserName;
+ }
+
+ void setWordPressComUserName(String userName) {
+ mWpcomUserName = userName;
+ }
+}
diff --git a/libs/analytics/build.gradle b/libs/analytics/build.gradle
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/libs/analytics/build.gradle
diff --git a/libs/analytics/gradle/wrapper/gradle-wrapper.jar b/libs/analytics/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..0087cd3b1
--- /dev/null
+++ b/libs/analytics/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/libs/analytics/gradle/wrapper/gradle-wrapper.properties b/libs/analytics/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..26af108cc
--- /dev/null
+++ b/libs/analytics/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Jul 09 11:48:51 CEST 2014
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.14-all.zip
diff --git a/libs/analytics/gradlew b/libs/analytics/gradlew
new file mode 100755
index 000000000..91a7e269e
--- /dev/null
+++ b/libs/analytics/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/analytics/gradlew.bat b/libs/analytics/gradlew.bat
new file mode 100644
index 000000000..8a0b282aa
--- /dev/null
+++ b/libs/analytics/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/analytics/settings.gradle b/libs/analytics/settings.gradle
new file mode 100644
index 000000000..6ad7b7de9
--- /dev/null
+++ b/libs/analytics/settings.gradle
@@ -0,0 +1 @@
+include ':WordPressAnalytics'
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'
diff --git a/libs/networking/.gitignore b/libs/networking/.gitignore
new file mode 100644
index 000000000..df914c258
--- /dev/null
+++ b/libs/networking/.gitignore
@@ -0,0 +1,23 @@
+# generated files
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+tools/deploy-mvn-artifact.conf
+
+# Intellij project files
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# Gradle
+.gradle/
+gradle.properties
+
+# Idea
+.idea/workspace.xml
+*.iml
+
+# OS X
+.DS_Store
diff --git a/libs/networking/WordPressNetworking/build.gradle b/libs/networking/WordPressNetworking/build.gradle
new file mode 100644
index 000000000..c0e937773
--- /dev/null
+++ b/libs/networking/WordPressNetworking/build.gradle
@@ -0,0 +1,49 @@
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.2.0'
+ }
+}
+
+repositories {
+ jcenter()
+ maven { url 'http://wordpress-mobile.github.io/WordPress-Android' }
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'maven'
+
+android {
+ publishNonDefault true
+
+ compileSdkVersion 24
+ buildToolsVersion "24.0.2"
+
+ defaultConfig {
+ minSdkVersion 14
+ targetSdkVersion 24
+ versionName "1.0.0"
+ }
+}
+
+dependencies {
+ compile 'org.wordpress:utils:1.11.0'
+ compile 'com.automattic:rest:1.0.7'
+}
+
+uploadArchives {
+ repositories {
+ mavenDeployer {
+ def repo_url = ""
+ if (project.hasProperty("repository")) {
+ repo_url = project.repository
+ }
+ repository(url: repo_url)
+ pom.version = android.defaultConfig.versionName
+ pom.groupId = "org.wordpress"
+ pom.artifactId = "wordpress-networking"
+ }
+ }
+}
diff --git a/libs/networking/WordPressNetworking/gradle.properties-example b/libs/networking/WordPressNetworking/gradle.properties-example
new file mode 100644
index 000000000..5a17295c3
--- /dev/null
+++ b/libs/networking/WordPressNetworking/gradle.properties-example
@@ -0,0 +1,2 @@
+wp.db_secret = wordpress
+repository = file:///Users/max/work/automattic/WordPress-Android-gh-pages/
diff --git a/libs/networking/WordPressNetworking/src/main/AndroidManifest.xml b/libs/networking/WordPressNetworking/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..f19a8bd89
--- /dev/null
+++ b/libs/networking/WordPressNetworking/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.wordpress.android.networking">
+</manifest>
diff --git a/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/Authenticator.java b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/Authenticator.java
new file mode 100644
index 000000000..cf7bd78bb
--- /dev/null
+++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/Authenticator.java
@@ -0,0 +1,13 @@
+package org.wordpress.android.networking;
+
+/**
+ * Interface that provides a method that should perform the necessary task to make sure
+ * the provided AuthenticatorRequest will be authenticated.
+ *
+ * The Authenticator must call AuthenticatorRequest.send() when it has completed its operations. For
+ * convenience the AuthenticatorRequest class provides AuthenticatorRequest.setAccessToken so the Authenticator can
+ * easily update the access token.
+ */
+public interface Authenticator {
+ void authenticate(final AuthenticatorRequest authenticatorRequest);
+}
diff --git a/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/AuthenticatorRequest.java b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/AuthenticatorRequest.java
new file mode 100644
index 000000000..40d2af7f3
--- /dev/null
+++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/AuthenticatorRequest.java
@@ -0,0 +1,96 @@
+package org.wordpress.android.networking;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.Oauth;
+import com.wordpress.rest.RestClient;
+import com.wordpress.rest.RestRequest;
+import com.wordpress.rest.RestRequest.ErrorListener;
+
+/**
+ * Encapsulates the behaviour for asking the Authenticator for an access token. This
+ * allows the request maker to disregard the authentication state when making requests.
+ */
+public class AuthenticatorRequest {
+ private RestRequest mRequest;
+ private RestRequest.ErrorListener mListener;
+ private RestClient mRestClient;
+ private Authenticator mAuthenticator;
+
+ protected AuthenticatorRequest(RestRequest request, ErrorListener listener, RestClient restClient,
+ Authenticator authenticator) {
+ mRequest = request;
+ mListener = listener;
+ mRestClient = restClient;
+ mAuthenticator = authenticator;
+ }
+
+ public String getSiteId() {
+ return extractSiteIdFromUrl(mRestClient.getEndpointURL(), mRequest.getUrl());
+ }
+
+ /**
+ * Parse out the site ID from an URL.
+ * Note: For batch REST API calls, only the first siteID is returned
+ *
+ * @return The site ID
+ */
+ public static String extractSiteIdFromUrl(String restEndpointUrl, String url) {
+ if (url == null) {
+ return null;
+ }
+
+ final String sitePrefix = restEndpointUrl.endsWith("/") ? restEndpointUrl + "sites/" : restEndpointUrl + "/sites/";
+ final String batchCallPrefix = restEndpointUrl.endsWith("/") ? restEndpointUrl + "batch/?urls%5B%5D=%2Fsites%2F"
+ : restEndpointUrl + "/batch/?urls%5B%5D=%2Fsites%2F";
+
+ if (url.startsWith(sitePrefix) && !sitePrefix.equals(url)) {
+ int marker = sitePrefix.length();
+ if (url.indexOf("/", marker) < marker) {
+ return null;
+ }
+ return url.substring(marker, url.indexOf("/", marker));
+ } else if (url.startsWith(batchCallPrefix) && !batchCallPrefix.equals(url)) {
+ int marker = batchCallPrefix.length();
+ if (url.indexOf("%2F", marker) < marker) {
+ return null;
+ }
+ return url.substring(marker, url.indexOf("%2F", marker));
+ }
+
+ // not a sites/$siteId request or a batch request
+ return null;
+ }
+
+ /**
+ * Attempt to send the request, checks to see if we have an access token and if not
+ * asks the Authenticator to authenticate the request.
+ *
+ * If no Authenticator is provided the request is always sent.
+ */
+ protected void send(){
+ if (mAuthenticator == null) {
+ mRestClient.send(mRequest);
+ } else {
+ mAuthenticator.authenticate(this);
+ }
+ }
+
+ public void sendWithAccessToken(String token){
+ mRequest.setAccessToken(token);
+ mRestClient.send(mRequest);
+ }
+
+ public void sendWithAccessToken(Oauth.Token token){
+ sendWithAccessToken(token.toString());
+ }
+
+ /**
+ * If an access token cannot be obtained the request can be aborted and the
+ * handler's onFailure method is called
+ */
+ public void abort(VolleyError error){
+ if (mListener != null) {
+ mListener.onErrorResponse(error);
+ }
+ }
+}
diff --git a/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactory.java b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactory.java
new file mode 100644
index 000000000..492b2b99f
--- /dev/null
+++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactory.java
@@ -0,0 +1,19 @@
+package org.wordpress.android.networking;
+
+import com.android.volley.RequestQueue;
+import com.wordpress.rest.RestClient;
+
+public class RestClientFactory {
+ private static RestClientFactoryAbstract sFactory;
+
+ public static RestClient instantiate(RequestQueue queue) {
+ return instantiate(queue, RestClient.REST_CLIENT_VERSIONS.V1);
+ }
+
+ public static RestClient instantiate(RequestQueue queue, RestClient.REST_CLIENT_VERSIONS version) {
+ if (sFactory == null) {
+ sFactory = new RestClientFactoryDefault();
+ }
+ return sFactory.make(queue, version);
+ }
+}
diff --git a/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryAbstract.java b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryAbstract.java
new file mode 100644
index 000000000..799a5a8e8
--- /dev/null
+++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryAbstract.java
@@ -0,0 +1,9 @@
+package org.wordpress.android.networking;
+
+import com.android.volley.RequestQueue;
+import com.wordpress.rest.RestClient;
+
+public interface RestClientFactoryAbstract {
+ public RestClient make(RequestQueue queue);
+ public RestClient make(RequestQueue queue, RestClient.REST_CLIENT_VERSIONS version);
+}
diff --git a/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryDefault.java b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryDefault.java
new file mode 100644
index 000000000..b87d4b40f
--- /dev/null
+++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryDefault.java
@@ -0,0 +1,14 @@
+package org.wordpress.android.networking;
+
+import com.android.volley.RequestQueue;
+import com.wordpress.rest.RestClient;
+
+public class RestClientFactoryDefault implements RestClientFactoryAbstract {
+ public RestClient make(RequestQueue queue) {
+ return new RestClient(queue);
+ }
+
+ public RestClient make(RequestQueue queue, RestClient.REST_CLIENT_VERSIONS version) {
+ return new RestClient(queue, version);
+ }
+}
diff --git a/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientUtils.java b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientUtils.java
new file mode 100644
index 000000000..6e06cfeee
--- /dev/null
+++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientUtils.java
@@ -0,0 +1,475 @@
+/**
+ * Interface to the WordPress.com REST API.
+ */
+package org.wordpress.android.networking;
+
+import android.content.Context;
+import android.net.Uri;
+import android.text.TextUtils;
+
+import com.android.volley.DefaultRetryPolicy;
+import com.android.volley.Request;
+import com.android.volley.Request.Method;
+import com.android.volley.RequestQueue;
+import com.android.volley.RetryPolicy;
+import com.android.volley.toolbox.RequestFuture;
+import com.wordpress.rest.JsonRestRequest;
+import com.wordpress.rest.RestClient;
+import com.wordpress.rest.RestRequest;
+import com.wordpress.rest.RestRequest.ErrorListener;
+import com.wordpress.rest.RestRequest.Listener;
+
+import org.json.JSONObject;
+import org.wordpress.android.util.LanguageUtils;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+public class RestClientUtils {
+ private static final String NOTIFICATION_FIELDS = "id,type,unread,body,subject,timestamp,meta";
+ private static final String COMMENT_REPLY_CONTENT_FIELD = "content";
+ private static String sUserAgent = "WordPress Networking Android";
+
+ private RestClient mRestClient;
+ private Authenticator mAuthenticator;
+ private Context mContext;
+
+ /**
+ * Socket timeout in milliseconds for rest requests
+ */
+ public static final int REST_TIMEOUT_MS = 30000;
+
+ /**
+ * Default number of retries for POST rest requests
+ */
+ public static final int REST_MAX_RETRIES_POST = 0;
+
+ /**
+ * Default number of retries for GET rest requests
+ */
+ public static final int REST_MAX_RETRIES_GET = 3;
+
+ /**
+ * Default backoff multiplier for rest requests
+ */
+ public static final float REST_BACKOFF_MULT = 2f;
+
+ public static void setUserAgent(String userAgent) {
+ sUserAgent = userAgent;
+ }
+
+ public RestClientUtils(Context context, RequestQueue queue, Authenticator authenticator, RestRequest.OnAuthFailedListener onAuthFailedListener) {
+ this(context, queue, authenticator, onAuthFailedListener, RestClient.REST_CLIENT_VERSIONS.V1);
+ }
+
+ public RestClientUtils(Context context, RequestQueue queue, Authenticator authenticator, RestRequest.OnAuthFailedListener onAuthFailedListener, RestClient.REST_CLIENT_VERSIONS version) {
+ // load an existing access token from prefs if we have one
+ mContext = context;
+ mAuthenticator = authenticator;
+ mRestClient = RestClientFactory.instantiate(queue, version);
+ if (onAuthFailedListener != null) {
+ mRestClient.setOnAuthFailedListener(onAuthFailedListener);
+ }
+ mRestClient.setUserAgent(sUserAgent);
+ }
+
+ public Authenticator getAuthenticator() {
+ return mAuthenticator;
+ }
+
+ public RestClient getRestClient() {
+ return mRestClient;
+ }
+
+ public void getCategories(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/categories", siteId);
+ get(path, null, null, listener, errorListener);
+ }
+
+ /**
+ * get a list of recent comments
+ * <p/>
+ * https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/comments/
+ */
+ public void getComments(String siteId, Map<String, String> params, final Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/comments", siteId);
+ get(path, params, null, listener, errorListener);
+ }
+
+ /**
+ * Reply to a comment
+ * <p/>
+ * https://developer.wordpress.com/docs/api/1/post/sites/%24site/posts/%24post_ID/replies/new/
+ */
+ public void replyToComment(String reply, String path, Listener listener, ErrorListener errorListener) {
+ Map<String, String> params = new HashMap<String, String>();
+ params.put(COMMENT_REPLY_CONTENT_FIELD, reply);
+ post(path, params, null, listener, errorListener);
+ }
+
+ /**
+ * Reply to a comment.
+ * <p/>
+ * https://developer.wordpress.com/docs/api/1/post/sites/%24site/posts/%24post_ID/replies/new/
+ */
+ public void replyToComment(long siteId, long commentId, String content, Listener listener,
+ ErrorListener errorListener) {
+ Map<String, String> params = new HashMap<String, String>();
+ params.put(COMMENT_REPLY_CONTENT_FIELD, content);
+ String path = String.format("sites/%d/comments/%d/replies/new", siteId, commentId);
+ post(path, params, null, listener, errorListener);
+ }
+
+ /**
+ * Follow a site given an ID or domain
+ * <p/>
+ * https://developer.wordpress.com/docs/api/1/post/sites/%24site/follows/new/
+ */
+ public void followSite(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/follows/new", siteId);
+ post(path, listener, errorListener);
+ }
+
+ /**
+ * Unfollow a site given an ID or domain
+ * <p/>
+ * https://developer.wordpress.com/docs/api/1/post/sites/%24site/follows/mine/delete/
+ */
+ public void unfollowSite(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/follows/mine/delete", siteId);
+ post(path, listener, errorListener);
+ }
+
+ /**
+ * Get notifications with the provided params.
+ * <p/>
+ * https://developer.wordpress.com/docs/api/1/get/notifications/
+ */
+ public void getNotifications(Map<String, String> params, Listener listener, ErrorListener errorListener) {
+ params.put("number", "40");
+ params.put("num_note_items", "20");
+ params.put("fields", NOTIFICATION_FIELDS);
+ get("notifications", params, null, listener, errorListener);
+ }
+
+ /**
+ * Get notifications with default params.
+ * <p/>
+ * https://developer.wordpress.com/docs/api/1/get/notifications/
+ */
+ public void getNotifications(Listener listener, ErrorListener errorListener) {
+ getNotifications(new HashMap<String, String>(), listener, errorListener);
+ }
+
+ /**
+ * Update the seen timestamp.
+ * <p/>
+ * https://developer.wordpress.com/docs/api/1/post/notifications/seen
+ */
+ public void markNotificationsSeen(String timestamp, Listener listener, ErrorListener errorListener) {
+ Map<String, String> params = new HashMap<String, String>();
+ params.put("time", timestamp);
+ post("notifications/seen", params, null, listener, errorListener);
+ }
+
+ /**
+ * Moderate a comment.
+ * <p/>
+ * http://developer.wordpress.com/docs/api/1/sites/%24site/comments/%24comment_ID/
+ */
+ public void moderateComment(String siteId, String commentId, String status, Listener listener,
+ ErrorListener errorListener) {
+ Map<String, String> params = new HashMap<String, String>();
+ params.put("status", status);
+ String path = String.format("sites/%s/comments/%s/", siteId, commentId);
+ post(path, params, null, listener, errorListener);
+ }
+
+ /**
+ * Edit the content of a comment
+ */
+ public void editCommentContent(long siteId, long commentId, String content, Listener listener,
+ ErrorListener errorListener) {
+ Map<String, String> params = new HashMap<String, String>();
+ params.put("content", content);
+ String path = String.format("sites/%d/comments/%d/", siteId, commentId);
+ post(path, params, null, listener, errorListener);
+ }
+
+ /**
+ * Like or unlike a comment.
+ */
+ public void likeComment(String siteId, String commentId, boolean isLiked, Listener listener,
+ ErrorListener errorListener) {
+ Map<String, String> params = new HashMap<String, String>();
+ String path = String.format("sites/%s/comments/%s/likes/", siteId, commentId);
+
+ if (!isLiked) {
+ path += "mine/delete";
+ } else {
+ path += "new";
+ }
+
+ post(path, params, null, listener, errorListener);
+ }
+
+ public void getFreeSearchThemes(String siteId, int limit, int offset, String searchTerm, Listener listener, ErrorListener errorListener) {
+ getSearchThemes("free", siteId, limit, offset, searchTerm, listener, errorListener);
+ }
+
+ public void getSearchThemes(String tier, String siteId, int limit, int offset, String searchTerm, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/themes?tier=" + tier + "&number=%d&offset=%d&search=%s", siteId, limit, offset, searchTerm);
+ get(path, listener, errorListener);
+ }
+
+ public void getFreeThemes(String siteId, int limit, int offset, Listener listener, ErrorListener errorListener) {
+ getThemes("free", siteId, limit, offset, listener, errorListener);
+ }
+
+ public void getPurchasedThemes(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/themes/purchased", siteId);
+ get(path, listener, errorListener);
+ }
+
+ /**
+ * Get all a site's themes
+ */
+ public void getThemes(String tier, String siteId, int limit, int offset, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/themes/?tier=" + tier + "&number=%d&offset=%d", siteId, limit, offset);
+ get(path, listener, errorListener);
+ }
+
+ /**
+ * Set a site's theme
+ */
+ public void setTheme(String siteId, String themeId, Listener listener, ErrorListener errorListener) {
+ Map<String, String> params = new HashMap<>();
+ params.put("theme", themeId);
+ String path = String.format("sites/%s/themes/mine", siteId);
+ post(path, params, null, listener, errorListener);
+ }
+
+ /**
+ * Get a site's current theme
+ */
+ public void getCurrentTheme(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/themes/mine", siteId);
+ get(path, listener, errorListener);
+ }
+
+ public void getGeneralSettings(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/settings", siteId);
+ Map<String, String> params = new HashMap<String, String>();
+ get(path, params, null, listener, errorListener);
+ }
+
+ public void setGeneralSiteSettings(String siteId, Listener listener, ErrorListener errorListener,
+ Map<String, String> params) {
+ String path = String.format("sites/%s/settings", siteId);
+ post(path, params, null, listener, errorListener);
+ }
+
+ /**
+ * Delete a site
+ */
+ public void deleteSite(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/delete", siteId);
+ post(path, listener, errorListener);
+ }
+
+ public void getSitePurchases(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/purchases", siteId);
+ get(path, listener, errorListener);
+ }
+
+ public void exportContentAll(String siteId, Listener listener, ErrorListener errorListener) {
+ String path = String.format("sites/%s/exports/start", siteId);
+ post(path, listener, errorListener);
+ }
+
+ public void sendLoginEmail(Map<String, String> params, Listener listener, ErrorListener errorListener) {
+ post("auth/send-login-email", params, null, listener, errorListener);
+ }
+
+ public void isAvailable(String email, Listener listener, ErrorListener errorListener) {
+ String path = String.format("is-available/email?q=%s", email);
+ get(path, listener, errorListener);
+ }
+
+ /**
+ * Make GET request
+ */
+ public Request<JSONObject> get(String path, Listener listener, ErrorListener errorListener) {
+ return get(path, null, null, listener, errorListener);
+ }
+
+ /**
+ * Make GET request with params
+ */
+ public Request<JSONObject> get(String path, Map<String, String> params, RetryPolicy retryPolicy, Listener listener,
+ ErrorListener errorListener) {
+ // turn params into querystring
+ HashMap<String, String> paramsWithLocale = getRestLocaleParams(mContext);
+ if (params != null) {
+ paramsWithLocale.putAll(params);
+ }
+
+ String realPath = getSanitizedPath(path);
+ if (TextUtils.isEmpty(realPath)) {
+ realPath = path;
+ }
+ paramsWithLocale.putAll(getSanitizedParameters(path));
+
+ RestRequest request = mRestClient.makeRequest(Method.GET, mRestClient.getAbsoluteURL(realPath, paramsWithLocale), null,
+ listener, errorListener);
+
+ if (retryPolicy == null) {
+ retryPolicy = new DefaultRetryPolicy(REST_TIMEOUT_MS, REST_MAX_RETRIES_GET, REST_BACKOFF_MULT);
+ }
+ request.setRetryPolicy(retryPolicy);
+ AuthenticatorRequest authCheck = new AuthenticatorRequest(request, errorListener, mRestClient, mAuthenticator);
+ authCheck.send();
+ return request;
+ }
+
+ /**
+ * Make Synchronous GET request
+ *
+ * @throws TimeoutException
+ * @throws ExecutionException
+ * @throws InterruptedException
+ */
+ public JSONObject getSynchronous(String path) throws InterruptedException, ExecutionException, TimeoutException {
+ return getSynchronous(path, null, null);
+ }
+
+ /**
+ * Make Synchronous GET request with params
+ *
+ * @throws TimeoutException
+ * @throws ExecutionException
+ * @throws InterruptedException
+ */
+ public JSONObject getSynchronous(String path, Map<String, String> params, RetryPolicy retryPolicy)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ RequestFuture<JSONObject> future = RequestFuture.newFuture();
+
+ HashMap<String, String> paramsWithLocale = getRestLocaleParams(mContext);
+ if (params != null) {
+ paramsWithLocale.putAll(params);
+ }
+
+ String realPath = getSanitizedPath(path);
+ if (TextUtils.isEmpty(realPath)) {
+ realPath = path;
+ }
+ paramsWithLocale.putAll(getSanitizedParameters(path));
+
+ RestRequest request = mRestClient.makeRequest(Method.GET, mRestClient.getAbsoluteURL(realPath, paramsWithLocale), null, future, future);
+
+ if (retryPolicy == null) {
+ retryPolicy = new DefaultRetryPolicy(REST_TIMEOUT_MS, REST_MAX_RETRIES_GET, REST_BACKOFF_MULT);
+ }
+ request.setRetryPolicy(retryPolicy);
+
+ AuthenticatorRequest authCheck = new AuthenticatorRequest(request, null, mRestClient, mAuthenticator);
+ authCheck.send(); //this insert the request into the queue. //TODO: Verify that everything is OK on REST calls without a valid token
+ JSONObject response = future.get();
+ return response;
+ }
+
+ /**
+ * Make POST request
+ */
+ public void post(String path, Listener listener, ErrorListener errorListener) {
+ Map<String, String> params = null;
+ post(path, params, null, listener, errorListener);
+ }
+
+ /**
+ * Make POST request with params
+ */
+ public void post(final String path, Map<String, String> params, RetryPolicy retryPolicy, Listener listener,
+ ErrorListener errorListener) {
+ final RestRequest request = mRestClient.makeRequest(Method.POST, mRestClient.getAbsoluteURL(path, getRestLocaleParams(mContext)), params,
+ listener, errorListener);
+ if (retryPolicy == null) {
+ retryPolicy = new DefaultRetryPolicy(REST_TIMEOUT_MS, REST_MAX_RETRIES_POST,
+ REST_BACKOFF_MULT); //Do not retry on failure
+ }
+ request.setRetryPolicy(retryPolicy);
+ AuthenticatorRequest authCheck = new AuthenticatorRequest(request, errorListener, mRestClient, mAuthenticator);
+ authCheck.send();
+ }
+
+
+ /**
+ * Make a JSON POST request
+ */
+ public void post(final String path, JSONObject params, RetryPolicy retryPolicy, Listener listener,
+ ErrorListener errorListener) {
+ final JsonRestRequest request = mRestClient.makeRequest(mRestClient.getAbsoluteURL(path, getRestLocaleParams(mContext)), params,
+ listener, errorListener);
+ if (retryPolicy == null) {
+ retryPolicy = new DefaultRetryPolicy(REST_TIMEOUT_MS, REST_MAX_RETRIES_POST,
+ REST_BACKOFF_MULT); //Do not retry on failure
+ }
+ request.setRetryPolicy(retryPolicy);
+ AuthenticatorRequest authCheck = new AuthenticatorRequest(request, errorListener, mRestClient, mAuthenticator);
+ authCheck.send();
+ }
+
+ /**
+ * Takes a URL and returns the path within, or an empty string (not null)
+ */
+ public static String getSanitizedPath(String unsanitizedPath){
+ if (unsanitizedPath != null) {
+ int qmarkPos = unsanitizedPath.indexOf('?');
+ if (qmarkPos > -1) { //strip any querystring params off this to obtain the path
+ return unsanitizedPath.substring(0, qmarkPos+1);
+ } else {
+ // return the string as is, consider the whole string as the path
+ return unsanitizedPath;
+ }
+ }
+ return "";
+ }
+
+ /**
+ * Takes a URL with query strings and returns a Map of query string values.
+ */
+ public static HashMap<String, String> getSanitizedParameters(String unsanitizedPath){
+ HashMap<String, String> queryParams = new HashMap<>();
+
+ Uri uri = Uri.parse(unsanitizedPath);
+
+ if (uri.getQueryParameterNames() != null ) {
+ Iterator iter = uri.getQueryParameterNames().iterator();
+ while (iter.hasNext()) {
+ String name = (String)iter.next();
+ String value = uri.getQueryParameter(name);
+ queryParams.put(name, value);
+ }
+ }
+
+ return queryParams;
+ }
+
+ /**
+ * Returns locale parameter used in REST calls which require the response to be localized
+ */
+ public static HashMap<String, String> getRestLocaleParams(Context context) {
+ HashMap<String, String> params = new HashMap<>();
+ String deviceLanguageCode = LanguageUtils.getCurrentDeviceLanguageCode(context);
+ if (!TextUtils.isEmpty(deviceLanguageCode)) {
+ //patch locale if it's any of the deprecated codes as can be read in Locale.java source code:
+ deviceLanguageCode = LanguageUtils.patchDeviceLanguageCode(deviceLanguageCode);
+ params.put("locale", deviceLanguageCode);
+ }
+ return params;
+ }
+
+}
diff --git a/libs/networking/build.gradle b/libs/networking/build.gradle
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/libs/networking/build.gradle
diff --git a/libs/networking/gradle/wrapper/gradle-wrapper.jar b/libs/networking/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..8c0fb64a8
--- /dev/null
+++ b/libs/networking/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/libs/networking/gradle/wrapper/gradle-wrapper.properties b/libs/networking/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..4c74aab81
--- /dev/null
+++ b/libs/networking/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Apr 10 15:27:10 PDT 2013
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.14-all.zip
diff --git a/libs/networking/gradlew b/libs/networking/gradlew
new file mode 100755
index 000000000..91a7e269e
--- /dev/null
+++ b/libs/networking/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/networking/gradlew.bat b/libs/networking/gradlew.bat
new file mode 100644
index 000000000..8a0b282aa
--- /dev/null
+++ b/libs/networking/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/networking/settings.gradle b/libs/networking/settings.gradle
new file mode 100644
index 000000000..62b0b6897
--- /dev/null
+++ b/libs/networking/settings.gradle
@@ -0,0 +1 @@
+include ':WordPressNetworking'
diff --git a/libs/utils/.gitignore b/libs/utils/.gitignore
new file mode 100644
index 000000000..8babf679a
--- /dev/null
+++ b/libs/utils/.gitignore
@@ -0,0 +1,25 @@
+# generated files
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+tools/deploy-mvn-artifact.conf
+
+# Intellij project files
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# Gradle
+.gradle/
+gradle.properties
+
+# Idea
+.idea/workspace.xml
+*.iml
+
+# OS X
+.DS_Store
+
+# dependencies
diff --git a/libs/utils/README.md b/libs/utils/README.md
new file mode 100644
index 000000000..e18da3d92
--- /dev/null
+++ b/libs/utils/README.md
@@ -0,0 +1,27 @@
+# WordPress-Utils-Android
+
+Collection of utility methods for Android and WordPress.
+
+## Use the library
+
+* In your build.gradle:
+```groovy
+dependencies {
+ // use the latest 1.x version
+ compile 'org.wordpress:utils:1.+'
+}
+```
+
+## Publish it to bintray
+
+```shell
+$ ./gradlew assemble publishToMavenLocal bintrayUpload -PbintrayUser=FIXME -PbintrayKey=FIXME -PdryRun=false
+```
+
+## Apps that use this library
+- [WordPress for Android][1]
+
+## License
+Dual licensed under MIT, and GPL.
+
+[1]: https://github.com/wordpress-mobile/WordPress-Android
diff --git a/libs/utils/WordPressUtils/README.md b/libs/utils/WordPressUtils/README.md
new file mode 100644
index 000000000..62a759585
--- /dev/null
+++ b/libs/utils/WordPressUtils/README.md
@@ -0,0 +1 @@
+# org.wordpress.android.util \ No newline at end of file
diff --git a/libs/utils/WordPressUtils/build.gradle b/libs/utils/WordPressUtils/build.gradle
new file mode 100644
index 000000000..96191a5d0
--- /dev/null
+++ b/libs/utils/WordPressUtils/build.gradle
@@ -0,0 +1,65 @@
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.2.0'
+ classpath 'com.novoda:bintray-release:0.3.4'
+ }
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'com.novoda.bintray-release'
+
+repositories {
+ jcenter()
+}
+
+dependencies {
+ compile('commons-lang:commons-lang:2.6') {
+ exclude group: 'commons-logging'
+ }
+ compile 'com.mcxiaoke.volley:library:1.0.18'
+ compile 'com.android.support:support-v13:24.2.1'
+}
+
+android {
+ useLibrary 'org.apache.http.legacy'
+
+ publishNonDefault true
+
+ compileSdkVersion 24
+ buildToolsVersion "24.0.2"
+
+ defaultConfig {
+ versionName "1.14.0"
+ minSdkVersion 14
+ targetSdkVersion 24
+ }
+}
+
+android.libraryVariants.all { variant ->
+ task("generate${variant.name}Javadoc", type: Javadoc) {
+ description "Generates Javadoc for $variant.name."
+ source = variant.javaCompile.source
+ classpath = files(variant.javaCompile.classpath.files, android.getBootClasspath())
+
+ options {
+ links "http://docs.oracle.com/javase/7/docs/api/"
+ }
+ exclude '**/R.java'
+ }
+}
+
+publish {
+ artifactId = 'utils'
+ userOrg = 'wordpress-mobile'
+ groupId = 'org.wordpress'
+ uploadName = 'utils'
+ description = 'Utils library for Android'
+ publishVersion = android.defaultConfig.versionName
+ licences = ['MIT', 'GPL']
+ website = 'https://github.com/wordpress-mobile/WordPress-Utils-Android/'
+ dryRun = 'false'
+ autoPublish = 'true'
+}
diff --git a/libs/utils/WordPressUtils/gradle.properties-example b/libs/utils/WordPressUtils/gradle.properties-example
new file mode 100644
index 000000000..5281d935c
--- /dev/null
+++ b/libs/utils/WordPressUtils/gradle.properties-example
@@ -0,0 +1,6 @@
+ossrhUsername=hello
+ossrhPassword=world
+
+signing.keyId=byebye
+signing.password=secret
+signing.secretKeyRingFile=/home/user/secret.gpg
diff --git a/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/JSONUtilsTest.java b/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/JSONUtilsTest.java
new file mode 100644
index 000000000..f7c747ff7
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/JSONUtilsTest.java
@@ -0,0 +1,32 @@
+package org.wordpress.android.util;
+
+import android.test.InstrumentationTestCase;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+public class JSONUtilsTest extends InstrumentationTestCase {
+ public void testQueryJSONNullSource1() {
+ JSONUtils.queryJSON((JSONObject) null, "", "");
+ }
+
+ public void testQueryJSONNullSource2() {
+ JSONUtils.queryJSON((JSONArray) null, "", "");
+ }
+
+ public void testQueryJSONNullQuery1() {
+ JSONUtils.queryJSON(new JSONObject(), null, "");
+ }
+
+ public void testQueryJSONNullQuery2() {
+ JSONUtils.queryJSON(new JSONArray(), null, "");
+ }
+
+ public void testQueryJSONNullReturnValue1() {
+ JSONUtils.queryJSON(new JSONObject(), "", null);
+ }
+
+ public void testQueryJSONNullReturnValue2() {
+ JSONUtils.queryJSON(new JSONArray(), "", null);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/ShortcodeUtilsTest.java b/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/ShortcodeUtilsTest.java
new file mode 100644
index 000000000..c506a452e
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/ShortcodeUtilsTest.java
@@ -0,0 +1,25 @@
+package org.wordpress.android.util;
+
+import android.test.InstrumentationTestCase;
+
+public class ShortcodeUtilsTest extends InstrumentationTestCase {
+ public void testGetVideoPressShortcodeFromId() {
+ assertEquals("[wpvideo abcd1234]", ShortcodeUtils.getVideoPressShortcodeFromId("abcd1234"));
+ }
+
+ public void testGetVideoPressShortcodeFromNullId() {
+ assertEquals("", ShortcodeUtils.getVideoPressShortcodeFromId(null));
+ }
+
+ public void testGetVideoPressIdFromCorrectShortcode() {
+ assertEquals("abcd1234", ShortcodeUtils.getVideoPressIdFromShortCode("[wpvideo abcd1234]"));
+ }
+
+ public void testGetVideoPressIdFromInvalidShortcode() {
+ assertEquals("", ShortcodeUtils.getVideoPressIdFromShortCode("[other abcd1234]"));
+ }
+
+ public void testGetVideoPressIdFromNullShortcode() {
+ assertEquals("", ShortcodeUtils.getVideoPressIdFromShortCode(null));
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java b/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java
new file mode 100644
index 000000000..abf7a8fae
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java
@@ -0,0 +1,108 @@
+package org.wordpress.android.util;
+
+import android.test.InstrumentationTestCase;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+
+public class UrlUtilsTest extends InstrumentationTestCase {
+ public void testGetDomainFromUrlWithEmptyStringDoesNotReturnNull() {
+ assertNotNull(UrlUtils.getHost(""));
+ }
+
+ public void testGetDomainFromUrlWithNoHostDoesNotReturnNull() {
+ assertNotNull(UrlUtils.getHost("wordpress"));
+ }
+
+ public void testGetDomainFromUrlWithHostReturnsHost() {
+ String url = "http://www.wordpress.com";
+ String host = UrlUtils.getHost(url);
+
+ assertTrue(host.equals("www.wordpress.com"));
+ }
+
+ public void testAppendUrlParameter1() {
+ String url = UrlUtils.appendUrlParameter("http://wp.com/test", "preview", "true");
+ assertEquals("http://wp.com/test?preview=true", url);
+ }
+
+ public void testAppendUrlParameter2() {
+ String url = UrlUtils.appendUrlParameter("http://wp.com/test?q=pony", "preview", "true");
+ assertEquals("http://wp.com/test?q=pony&preview=true", url);
+ }
+
+ public void testAppendUrlParameter3() {
+ String url = UrlUtils.appendUrlParameter("http://wp.com/test?q=pony#unicorn", "preview", "true");
+ assertEquals("http://wp.com/test?q=pony&preview=true#unicorn", url);
+ }
+
+ public void testAppendUrlParameter4() {
+ String url = UrlUtils.appendUrlParameter("/relative/test", "preview", "true");
+ assertEquals("/relative/test?preview=true", url);
+ }
+
+ public void testAppendUrlParameter5() {
+ String url = UrlUtils.appendUrlParameter("/relative/", "preview", "true");
+ assertEquals("/relative/?preview=true", url);
+ }
+
+ public void testAppendUrlParameter6() {
+ String url = UrlUtils.appendUrlParameter("http://wp.com/test/", "preview", "true");
+ assertEquals("http://wp.com/test/?preview=true", url);
+ }
+
+ public void testAppendUrlParameter7() {
+ String url = UrlUtils.appendUrlParameter("http://wp.com/test/?q=pony", "preview", "true");
+ assertEquals("http://wp.com/test/?q=pony&preview=true", url);
+ }
+
+ public void testAppendUrlParameters1() {
+ Map<String, String> params = new HashMap<>();
+ params.put("w", "200");
+ params.put("h", "300");
+ String url = UrlUtils.appendUrlParameters("http://wp.com/test", params);
+ if (!url.equals("http://wp.com/test?h=300&w=200") && !url.equals("http://wp.com/test?w=200&h=300")) {
+ assertTrue("failed test on url: " + url, false);
+ }
+ }
+
+ public void testAppendUrlParameters2() {
+ Map<String, String> params = new HashMap<>();
+ params.put("h", "300");
+ params.put("w", "200");
+ String url = UrlUtils.appendUrlParameters("/relative/test", params);
+ if (!url.equals("/relative/test?h=300&w=200") && !url.equals("/relative/test?w=200&h=300")) {
+ assertTrue("failed test on url: " + url, false);
+ }
+ }
+
+ public void testHttps1() {
+ assertFalse(UrlUtils.isHttps(buildURL("http://wordpress.com/xmlrpc.php")));
+ }
+
+ public void testHttps2() {
+ assertFalse(UrlUtils.isHttps(buildURL("http://wordpress.com#.b.com/test")));
+ }
+
+ public void testHttps3() {
+ assertFalse(UrlUtils.isHttps(buildURL("http://wordpress.com/xmlrpc.php")));
+ }
+
+ public void testHttps4() {
+ assertTrue(UrlUtils.isHttps(buildURL("https://wordpress.com")));
+ }
+
+ public void testHttps5() {
+ assertTrue(UrlUtils.isHttps(buildURL("https://wordpress.com/test#test")));
+ }
+
+ private URL buildURL(String address) {
+ URL url = null;
+ try {
+ url = new URL(address);
+ } catch (MalformedURLException e) {}
+ return url;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/AndroidManifest.xml b/libs/utils/WordPressUtils/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..44b1dcddc
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.wordpress.android.util">
+ <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+</manifest>
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ActivityUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ActivityUtils.java
new file mode 100644
index 000000000..396e06c37
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ActivityUtils.java
@@ -0,0 +1,16 @@
+package org.wordpress.android.util;
+
+import android.app.Activity;
+import android.content.Context;
+import android.view.inputmethod.InputMethodManager;
+
+public class ActivityUtils {
+ public static void hideKeyboard(Activity activity) {
+ if (activity != null && activity.getCurrentFocus() != null) {
+ InputMethodManager inputManager = (InputMethodManager) activity.getSystemService(
+ Context.INPUT_METHOD_SERVICE);
+ inputManager.hideSoftInputFromWindow(activity.getCurrentFocus().getWindowToken(),
+ InputMethodManager.HIDE_NOT_ALWAYS);
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtils.java
new file mode 100644
index 000000000..79b2dbced
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtils.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2011 wordpress.org
+ *
+ * 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.
+ */
+
+package org.wordpress.android.util;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+
+public class AlertUtils {
+ /**
+ * Show Alert Dialog
+ * @param context
+ * @param titleId
+ * @param messageId
+ */
+ public static void showAlert(Context context, int titleId, int messageId) {
+ Dialog dlg = new AlertDialog.Builder(context)
+ .setTitle(titleId)
+ .setPositiveButton(android.R.string.ok, null)
+ .setMessage(messageId)
+ .create();
+
+ dlg.show();
+ }
+
+ /**
+ * Show Alert Dialog
+ * @param context
+ * @param titleId
+ * @param message
+ */
+ public static void showAlert(Context context, int titleId, String message) {
+ Dialog dlg = new AlertDialog.Builder(context)
+ .setTitle(titleId)
+ .setPositiveButton(android.R.string.ok, null)
+ .setMessage(message)
+ .create();
+
+ dlg.show();
+ }
+
+ /**
+ * Show Alert Dialog
+ * @param context
+ * @param titleId
+ * @param messageId
+ * @param positiveButtontxt
+ * @param positiveListener
+ * @param negativeButtontxt
+ * @param negativeListener
+ */
+ public static void showAlert(Context context, int titleId, int messageId,
+ CharSequence positiveButtontxt, DialogInterface.OnClickListener positiveListener,
+ CharSequence negativeButtontxt, DialogInterface.OnClickListener negativeListener) {
+ Dialog dlg = new AlertDialog.Builder(context)
+ .setTitle(titleId)
+ .setPositiveButton(positiveButtontxt, positiveListener)
+ .setNegativeButton(negativeButtontxt, negativeListener)
+ .setMessage(messageId)
+ .setCancelable(false)
+ .create();
+
+ dlg.show();
+ }
+
+ /**
+ * Show Alert Dialog
+ * @param context
+ * @param titleId
+ * @param message
+ * @param positiveButtontxt
+ * @param positiveListener
+ */
+ public static void showAlert(Context context, int titleId, String message,
+ CharSequence positiveButtontxt, DialogInterface.OnClickListener positiveListener) {
+ Dialog dlg = new AlertDialog.Builder(context)
+ .setTitle(titleId)
+ .setPositiveButton(positiveButtontxt, positiveListener)
+ .setMessage(message)
+ .setCancelable(false)
+ .create();
+
+ dlg.show();
+ }
+} \ No newline at end of file
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AppLog.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AppLog.java
new file mode 100644
index 000000000..be60e748e
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AppLog.java
@@ -0,0 +1,272 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * simple wrapper for Android log calls, enables recording and displaying log
+ */
+public class AppLog {
+ // T for Tag
+ public enum T {READER, EDITOR, MEDIA, NUX, API, STATS, UTILS, NOTIFS, DB, POSTS, COMMENTS, THEMES, TESTS, PROFILING,
+ SIMPERIUM, SUGGESTION, MAIN, SETTINGS, PLANS, PEOPLE}
+
+ public static final String TAG = "WordPress";
+ public static final int HEADER_LINE_COUNT = 2;
+
+ private static boolean mEnableRecording = false;
+
+ private AppLog() {
+ throw new AssertionError();
+ }
+
+ /**
+ * Capture log so it can be displayed by AppLogViewerActivity
+ * @param enable A boolean flag to capture log. Default is false, pass true to enable recording
+ */
+ public static void enableRecording(boolean enable) {
+ mEnableRecording = enable;
+ }
+
+ /**
+ * Sends a VERBOSE log message
+ * @param tag Used to identify the source of a log message.
+ * It usually identifies the class or activity where the log call occurs.
+ * @param message The message you would like logged.
+ */
+ public static void v(T tag, String message) {
+ message = StringUtils.notNullStr(message);
+ Log.v(TAG + "-" + tag.toString(), message);
+ addEntry(tag, LogLevel.v, message);
+ }
+
+ /**
+ * Sends a DEBUG log message
+ * @param tag Used to identify the source of a log message.
+ * It usually identifies the class or activity where the log call occurs.
+ * @param message The message you would like logged.
+ */
+ public static void d(T tag, String message) {
+ message = StringUtils.notNullStr(message);
+ Log.d(TAG + "-" + tag.toString(), message);
+ addEntry(tag, LogLevel.d, message);
+ }
+
+ /**
+ * Sends a INFO log message
+ * @param tag Used to identify the source of a log message.
+ * It usually identifies the class or activity where the log call occurs.
+ * @param message The message you would like logged.
+ */
+ public static void i(T tag, String message) {
+ message = StringUtils.notNullStr(message);
+ Log.i(TAG + "-" + tag.toString(), message);
+ addEntry(tag, LogLevel.i, message);
+ }
+
+ /**
+ * Sends a WARN log message
+ * @param tag Used to identify the source of a log message.
+ * It usually identifies the class or activity where the log call occurs.
+ * @param message The message you would like logged.
+ */
+ public static void w(T tag, String message) {
+ message = StringUtils.notNullStr(message);
+ Log.w(TAG + "-" + tag.toString(), message);
+ addEntry(tag, LogLevel.w, message);
+ }
+
+ /**
+ * Sends a ERROR log message
+ * @param tag Used to identify the source of a log message.
+ * It usually identifies the class or activity where the log call occurs.
+ * @param message The message you would like logged.
+ */
+ public static void e(T tag, String message) {
+ message = StringUtils.notNullStr(message);
+ Log.e(TAG + "-" + tag.toString(), message);
+ addEntry(tag, LogLevel.e, message);
+ }
+
+ /**
+ * Send a ERROR log message and log the exception.
+ * @param tag Used to identify the source of a log message.
+ * It usually identifies the class or activity where the log call occurs.
+ * @param message The message you would like logged.
+ * @param tr An exception to log
+ */
+ public static void e(T tag, String message, Throwable tr) {
+ message = StringUtils.notNullStr(message);
+ Log.e(TAG + "-" + tag.toString(), message, tr);
+ addEntry(tag, LogLevel.e, message + " - exception: " + tr.getMessage());
+ addEntry(tag, LogLevel.e, "StackTrace: " + getStringStackTrace(tr));
+ }
+
+ /**
+ * Sends a ERROR log message and the exception with StackTrace
+ * @param tag Used to identify the source of a log message. It usually identifies the class or activity where the log call occurs.
+ * @param tr An exception to log to get StackTrace
+ */
+ public static void e(T tag, Throwable tr) {
+ Log.e(TAG + "-" + tag.toString(), tr.getMessage(), tr);
+ addEntry(tag, LogLevel.e, tr.getMessage());
+ addEntry(tag, LogLevel.e, "StackTrace: " + getStringStackTrace(tr));
+ }
+
+ /**
+ * Sends a ERROR log message
+ * @param tag Used to identify the source of a log message. It usually identifies the class or activity where the log call occurs.
+ * @param volleyErrorMsg
+ * @param statusCode
+ */
+ public static void e(T tag, String volleyErrorMsg, int statusCode) {
+ if (TextUtils.isEmpty(volleyErrorMsg)) {
+ return;
+ }
+ String logText;
+ if (statusCode == -1) {
+ logText = volleyErrorMsg;
+ } else {
+ logText = volleyErrorMsg + ", status " + statusCode;
+ }
+ Log.e(TAG + "-" + tag.toString(), logText);
+ addEntry(tag, LogLevel.w, logText);
+ }
+
+ // --------------------------------------------------------------------------------------------------------
+
+ private static final int MAX_ENTRIES = 99;
+
+ private enum LogLevel {
+ v, d, i, w, e;
+ private String toHtmlColor() {
+ switch(this) {
+ case v:
+ return "grey";
+ case i:
+ return "black";
+ case w:
+ return "purple";
+ case e:
+ return "red";
+ case d:
+ default:
+ return "teal";
+ }
+ }
+ }
+
+ private static class LogEntry {
+ LogLevel mLogLevel;
+ String mLogText;
+ T mLogTag;
+
+ public LogEntry(LogLevel logLevel, String logText, T logTag) {
+ mLogLevel = logLevel;
+ mLogText = logText;
+ if (mLogText == null) {
+ mLogText = "null";
+ }
+ mLogTag = logTag;
+ }
+
+ private String toHtml() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("<font color=\"");
+ sb.append(mLogLevel.toHtmlColor());
+ sb.append("\">");
+ sb.append("[");
+ sb.append(mLogTag.name());
+ sb.append("] ");
+ sb.append(mLogLevel.name());
+ sb.append(": ");
+ sb.append(TextUtils.htmlEncode(mLogText).replace("\n", "<br />"));
+ sb.append("</font>");
+ return sb.toString();
+ }
+ }
+
+ private static class LogEntryList extends ArrayList<LogEntry> {
+ private synchronized boolean addEntry(LogEntry entry) {
+ if (size() >= MAX_ENTRIES)
+ removeFirstEntry();
+ return add(entry);
+ }
+ private void removeFirstEntry() {
+ Iterator<LogEntry> it = iterator();
+ if (!it.hasNext())
+ return;
+ try {
+ remove(it.next());
+ } catch (NoSuchElementException e) {
+ // ignore
+ }
+ }
+ }
+
+ private static LogEntryList mLogEntries = new LogEntryList();
+
+ private static void addEntry(T tag, LogLevel level, String text) {
+ // skip if recording is disabled (default)
+ if (!mEnableRecording) {
+ return;
+ }
+ LogEntry entry = new LogEntry(level, text, tag);
+ mLogEntries.addEntry(entry);
+ }
+
+ private static String getStringStackTrace(Throwable throwable) {
+ StringWriter errors = new StringWriter();
+ throwable.printStackTrace(new PrintWriter(errors));
+ return errors.toString();
+ }
+
+ /**
+ * Returns entire log as html for display (see AppLogViewerActivity)
+ * @param context
+ * @return Arraylist of Strings containing log messages
+ */
+ public static ArrayList<String> toHtmlList(Context context) {
+ ArrayList<String> items = new ArrayList<String>();
+
+ // add version & device info - be sure to change HEADER_LINE_COUNT if additional lines are added
+ items.add("<strong>WordPress Android version: " + PackageUtils.getVersionName(context) + "</strong>");
+ items.add("<strong>Android device name: " + DeviceUtils.getInstance().getDeviceName(context) + "</strong>");
+
+ Iterator<LogEntry> it = mLogEntries.iterator();
+ while (it.hasNext()) {
+ items.add(it.next().toHtml());
+ }
+ return items;
+ }
+
+ /**
+ * Converts the entire log to plain text
+ * @param context
+ * @return The log as plain text
+ */
+ public static String toPlainText(Context context) {
+ StringBuilder sb = new StringBuilder();
+
+ // add version & device info
+ sb.append("WordPress Android version: " + PackageUtils.getVersionName(context)).append("\n")
+ .append("Android device name: " + DeviceUtils.getInstance().getDeviceName(context)).append("\n\n");
+
+ Iterator<LogEntry> it = mLogEntries.iterator();
+ int lineNum = 1;
+ while (it.hasNext()) {
+ sb.append(String.format("%02d - ", lineNum))
+ .append(it.next().mLogText)
+ .append("\n");
+ lineNum++;
+ }
+ return sb.toString();
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/BlogUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/BlogUtils.java
new file mode 100644
index 000000000..c81ec64a5
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/BlogUtils.java
@@ -0,0 +1,74 @@
+package org.wordpress.android.util;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+public class BlogUtils {
+ public static Comparator<Object> BlogNameComparator = new Comparator<Object>() {
+ public int compare(Object blog1, Object blog2) {
+ Map<String, Object> blogMap1 = (Map<String, Object>) blog1;
+ Map<String, Object> blogMap2 = (Map<String, Object>) blog2;
+ String blogName1 = getBlogNameOrHomeURLFromAccountMap(blogMap1);
+ String blogName2 = getBlogNameOrHomeURLFromAccountMap(blogMap2);
+ return blogName1.compareToIgnoreCase(blogName2);
+ }
+ };
+
+ /**
+ * Return a blog name or blog home URL if trimmed name is an empty string
+ */
+ public static String getBlogNameOrHomeURLFromAccountMap(Map<String, Object> account) {
+ String blogName = getBlogNameFromAccountMap(account);
+ if (blogName.trim().length() == 0) {
+ blogName = BlogUtils.getHomeURLOrHostNameFromAccountMap(account);
+ }
+ return blogName;
+ }
+
+ /**
+ * Return a blog name or blog url (host part only) if trimmed name is an empty string
+ */
+ public static String getBlogNameFromAccountMap(Map<String, Object> account) {
+ return StringUtils.unescapeHTML(MapUtils.getMapStr(account, "blogName"));
+ }
+
+ /**
+ * Return the blog home URL setting or the host name if home URL is an empty string.
+ */
+ public static String getHomeURLOrHostNameFromAccountMap(Map<String, Object> account) {
+ String homeURL = UrlUtils.removeScheme(MapUtils.getMapStr(account, "homeURL"));
+ homeURL = StringUtils.removeTrailingSlash(homeURL);
+
+ if (homeURL.length() == 0) {
+ return UrlUtils.getHost(MapUtils.getMapStr(account, "url"));
+ }
+
+ return homeURL;
+ }
+
+ public static String[] getBlogNamesFromAccountMapList(List<Map<String, Object>> accounts) {
+ List<String> blogNames = new ArrayList<>();
+ for (Map<String, Object> account : accounts) {
+ blogNames.add(getBlogNameOrHomeURLFromAccountMap(account));
+ }
+ return blogNames.toArray(new String[blogNames.size()]);
+ }
+
+ public static String[] getHomeURLOrHostNamesFromAccountMapList(List<Map<String, Object>> accounts) {
+ List<String> urls = new ArrayList<>();
+ for (Map<String, Object> account : accounts) {
+ urls.add(getHomeURLOrHostNameFromAccountMap(account));
+ }
+ return urls.toArray(new String[urls.size()]);
+ }
+
+ public static String[] getBlogIdsFromAccountMapList(List<Map<String, Object>> accounts) {
+ List<String> ids = new ArrayList<>();
+ for (Map<String, Object> account : accounts) {
+ ids.add(MapUtils.getMapStr(account, "blogId"));
+ }
+ return ids.toArray(new String[ids.size()]);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DateTimeUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DateTimeUtils.java
new file mode 100644
index 000000000..2a796f3ee
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DateTimeUtils.java
@@ -0,0 +1,246 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.text.format.DateUtils;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public class DateTimeUtils {
+ private DateTimeUtils() {
+ throw new AssertionError();
+ }
+
+ // See http://drdobbs.com/java/184405382
+ private static final ThreadLocal<DateFormat> ISO8601_FORMAT = new ThreadLocal<DateFormat>() {
+ @Override
+ protected DateFormat initialValue() {
+ return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US);
+ }
+ };
+
+ /**
+ * Converts a date to a relative time span ("8h", "3d", etc.) - similar to
+ * DateUtils.getRelativeTimeSpanString but returns shorter result
+ */
+ public static String javaDateToTimeSpan(final Date date, Context context) {
+ if (date == null) {
+ return "";
+ }
+
+ long passedTime = date.getTime();
+ long currentTime = System.currentTimeMillis();
+
+ // return "now" if less than a minute has elapsed
+ long secondsSince = (currentTime - passedTime) / 1000;
+ if (secondsSince < 60) {
+ return context.getString(R.string.timespan_now);
+ }
+
+ // less than an hour (ex: 12m)
+ long minutesSince = secondsSince / 60;
+ if (minutesSince < 60) {
+ return Long.toString(minutesSince) + "m";
+ }
+
+ // less than a day (ex: 17h)
+ long hoursSince = minutesSince / 60;
+ if (hoursSince < 24) {
+ return Long.toString(hoursSince) + "h";
+ }
+
+ // less than a week (ex: 5d)
+ long daysSince = hoursSince / 24;
+ if (daysSince < 7) {
+ return Long.toString(daysSince) + "d";
+ }
+
+ // less than a year old, so return day/month without year (ex: Jan 30)
+ if (daysSince < 365) {
+ return DateUtils.formatDateTime(context, passedTime, DateUtils.FORMAT_NO_YEAR |
+ DateUtils.FORMAT_ABBREV_ALL);
+ }
+
+ // date is older, so include year (ex: Jan 30, 2013)
+ return DateUtils.formatDateTime(context, passedTime, DateUtils.FORMAT_ABBREV_ALL);
+ }
+
+ /**
+ * Given an ISO 8601-formatted date as a String, returns a {@link Date}.
+ */
+ public static Date dateFromIso8601(final String strDate) {
+ try {
+ DateFormat formatter = ISO8601_FORMAT.get();
+ return formatter.parse(strDate);
+ } catch (ParseException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Given an ISO 8601-formatted date as a String, returns a {@link Date} in UTC.
+ */
+ public static Date dateUTCFromIso8601(String iso8601date) {
+ try {
+ iso8601date = iso8601date.replace("Z", "+0000").replace("+00:00", "+0000");
+ DateFormat formatter = ISO8601_FORMAT.get();
+ formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
+ return formatter.parse(iso8601date);
+ } catch (ParseException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Given a {@link Date}, returns an ISO 8601-formatted String.
+ */
+ public static String iso8601FromDate(Date date) {
+ if (date == null) {
+ return "";
+ }
+ DateFormat formatter = ISO8601_FORMAT.get();
+ return formatter.format(date);
+ }
+
+ /**
+ * Given a {@link Date}, returns an ISO 8601-formatted String in UTC.
+ */
+ public static String iso8601UTCFromDate(Date date) {
+ if (date == null) {
+ return "";
+ }
+ TimeZone tz = TimeZone.getTimeZone("UTC");
+ DateFormat formatter = ISO8601_FORMAT.get();
+ formatter.setTimeZone(tz);
+
+ String iso8601date = formatter.format(date);
+
+ // Use "+00:00" notation rather than "+0000" to be consistent with the WP.COM API
+ return iso8601date.replace("+0000", "+00:00");
+ }
+
+ /**
+ * Returns the current UTC date
+ */
+ public static Date nowUTC() {
+ Date dateTimeNow = new Date();
+ return localDateToUTC(dateTimeNow);
+ }
+
+ public static Date localDateToUTC(Date dtLocal) {
+ if (dtLocal == null) {
+ return null;
+ }
+ TimeZone tz = TimeZone.getDefault();
+ int currentOffsetFromUTC = tz.getRawOffset() + (tz.inDaylightTime(dtLocal) ? tz.getDSTSavings() : 0);
+ return new Date(dtLocal.getTime() - currentOffsetFromUTC);
+ }
+
+ // Routines to return a diff between two dates - always return a positive number
+
+ public static int daysBetween(Date dt1, Date dt2) {
+ long hrDiff = hoursBetween(dt1, dt2);
+ if (hrDiff == 0) {
+ return 0;
+ }
+ return (int) (hrDiff / 24);
+ }
+
+ public static int hoursBetween(Date dt1, Date dt2) {
+ long minDiff = minutesBetween(dt1, dt2);
+ if (minDiff == 0) {
+ return 0;
+ }
+ return (int) (minDiff / 60);
+ }
+
+ public static int minutesBetween(Date dt1, Date dt2) {
+ long msDiff = millisecondsBetween(dt1, dt2);
+ if (msDiff == 0) {
+ return 0;
+ }
+ return (int) (msDiff / 60000);
+ }
+
+ public static int secondsBetween(Date dt1, Date dt2) {
+ long msDiff = millisecondsBetween(dt1, dt2);
+ if (msDiff == 0) {
+ return 0;
+ }
+ return (int) (msDiff / 1000);
+ }
+
+ public static long millisecondsBetween(Date dt1, Date dt2) {
+ if (dt1 == null || dt2 == null) {
+ return 0;
+ }
+ return Math.abs(dt1.getTime() - dt2.getTime());
+ }
+
+ public static boolean isSameYear(Date dt1, Date dt2) {
+ if (dt1 == null || dt2 == null) {
+ return false;
+ }
+ return dt1.getYear() == dt2.getYear();
+ }
+
+ public static boolean isSameMonthAndYear(Date dt1, Date dt2) {
+ if (dt1 == null || dt2 == null) {
+ return false;
+ }
+ return dt1.getYear() == dt2.getYear() && dt1.getMonth() == dt2.getMonth();
+ }
+
+ // Routines involving Unix timestamps (GMT assumed)
+
+ /**
+ * Given an ISO 8601-formatted date as a String, returns the corresponding UNIX timestamp.
+ */
+ public static long timestampFromIso8601(final String strDate) {
+ return (timestampFromIso8601Millis(strDate) / 1000);
+ }
+
+ /**
+ * Given an ISO 8601-formatted date as a String, returns the corresponding timestamp in milliseconds.
+ */
+ public static long timestampFromIso8601Millis(final String strDate) {
+ Date date = dateFromIso8601(strDate);
+ if (date == null) {
+ return 0;
+ }
+ return (date.getTime());
+ }
+
+ /**
+ * Given a UNIX timestamp, returns the corresponding {@link Date}.
+ */
+ public static Date dateFromTimestamp(long timestamp) {
+ return new java.util.Date(timestamp * 1000);
+ }
+
+ /**
+ * Given a UNIX timestamp, returns an ISO 8601-formatted date as a String.
+ */
+ public static String iso8601FromTimestamp(long timestamp) {
+ return iso8601FromDate(dateFromTimestamp(timestamp));
+ }
+
+ /**
+ * Given a UNIX timestamp, returns an ISO 8601-formatted date in UTC as a String.
+ */
+ public static String iso8601UTCFromTimestamp(long timestamp) {
+ return iso8601UTCFromDate(dateFromTimestamp(timestamp));
+ }
+
+ /**
+ * Given a UNIX timestamp, returns a relative time span ("8h", "3d", etc.).
+ */
+ public static String timeSpanFromTimestamp(long timestamp, Context context) {
+ Date dateGMT = dateFromTimestamp(timestamp);
+ return javaDateToTimeSpan(dateGMT, context);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DeviceUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DeviceUtils.java
new file mode 100644
index 000000000..639d5479c
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DeviceUtils.java
@@ -0,0 +1,94 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+
+import org.wordpress.android.util.AppLog.T;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+public class DeviceUtils {
+ private static DeviceUtils instance;
+ private boolean isKindleFire = false;
+
+ public boolean isKindleFire() {
+ return isKindleFire;
+ }
+
+ public static DeviceUtils getInstance() {
+ if (instance == null) {
+ instance = new DeviceUtils();
+ }
+ return instance;
+ }
+
+ private DeviceUtils() {
+ isKindleFire = android.os.Build.MODEL.equalsIgnoreCase("kindle fire") ? true: false;
+ }
+
+ /**
+ * Checks camera availability recursively based on API level.
+ *
+ * TODO: change "android.hardware.camera.front" and "android.hardware.camera.any" to
+ * {@link PackageManager#FEATURE_CAMERA_FRONT} and {@link PackageManager#FEATURE_CAMERA_ANY},
+ * respectively, once they become accessible or minSdk version is incremented.
+ *
+ * @param context The context.
+ * @return Whether camera is available.
+ */
+ public boolean hasCamera(Context context) {
+ final PackageManager pm = context.getPackageManager();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)
+ || pm.hasSystemFeature("android.hardware.camera.front");
+ }
+
+ return pm.hasSystemFeature("android.hardware.camera.any");
+ }
+
+ public String getDeviceName(Context context) {
+ String manufacturer = Build.MANUFACTURER;
+ String undecodedModel = Build.MODEL;
+ String model = null;
+
+ try {
+ Properties prop = new Properties();
+ InputStream fileStream;
+ // Read the device name from a precomplied list:
+ // see http://making.meetup.com/post/29648976176/human-readble-android-device-names
+ fileStream = context.getAssets().open("android_models.properties");
+ prop.load(fileStream);
+ fileStream.close();
+ String decodedModel = prop.getProperty(undecodedModel.replaceAll(" ", "_"));
+ if (decodedModel != null && !decodedModel.trim().equals("")) {
+ model = decodedModel;
+ }
+ } catch (IOException e) {
+ AppLog.e(T.UTILS, e.getMessage());
+ }
+
+ if (model == null) { //Device model not found in the list
+ if (undecodedModel.startsWith(manufacturer)) {
+ model = capitalize(undecodedModel);
+ } else {
+ model = capitalize(manufacturer) + " " + undecodedModel;
+ }
+ }
+ return model;
+ }
+
+ private String capitalize(String s) {
+ if (s == null || s.length() == 0) {
+ return "";
+ }
+ char first = s.charAt(0);
+ if (Character.isUpperCase(first)) {
+ return s;
+ } else {
+ return Character.toUpperCase(first) + s.substring(1);
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DisplayUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DisplayUtils.java
new file mode 100644
index 000000000..40a017e9a
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DisplayUtils.java
@@ -0,0 +1,91 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Point;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.Display;
+import android.view.Window;
+import android.view.WindowManager;
+
+public class DisplayUtils {
+ private DisplayUtils() {
+ throw new AssertionError();
+ }
+
+ public static boolean isLandscape(Context context) {
+ if (context == null)
+ return false;
+ return context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
+ }
+
+ public static Point getDisplayPixelSize(Context context) {
+ WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ Display display = wm.getDefaultDisplay();
+ Point size = new Point();
+ display.getSize(size);
+ return size;
+ }
+
+ public static int getDisplayPixelWidth(Context context) {
+ Point size = getDisplayPixelSize(context);
+ return (size.x);
+ }
+
+ public static int getDisplayPixelHeight(Context context) {
+ Point size = getDisplayPixelSize(context);
+ return (size.y);
+ }
+
+ public static float spToPx(Context context, float sp){
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ final float scale = displayMetrics.scaledDensity;
+ return sp * scale;
+ }
+
+ public static int dpToPx(Context context, int dp) {
+ float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
+ context.getResources().getDisplayMetrics());
+ return (int) px;
+ }
+
+ public static int pxToDp(Context context, int px) {
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ return (int) ((px/displayMetrics.density)+0.5);
+ }
+
+ public static boolean isXLarge(Context context) {
+ if ((context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK)
+ == Configuration.SCREENLAYOUT_SIZE_XLARGE) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * returns the height of the ActionBar if one is enabled - supports both the native ActionBar
+ * and ActionBarSherlock - http://stackoverflow.com/a/15476793/1673548
+ */
+ public static int getActionBarHeight(Context context) {
+ if (context == null) {
+ return 0;
+ }
+ TypedValue tv = new TypedValue();
+ if (context.getTheme() != null
+ && context.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
+ return TypedValue.complexToDimensionPixelSize(tv.data, context.getResources().getDisplayMetrics());
+ }
+
+ // if we get this far, it's because the device doesn't support an ActionBar,
+ // so return the standard ActionBar height (48dp)
+ return dpToPx(context, 48);
+ }
+
+ /**
+ * detect when FEATURE_ACTION_BAR_OVERLAY has been set
+ */
+ public static boolean hasActionBarOverlay(Window window) {
+ return window.hasFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EditTextUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EditTextUtils.java
new file mode 100644
index 000000000..66a0c77fd
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EditTextUtils.java
@@ -0,0 +1,75 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.TextView;
+
+/**
+ * EditText utils
+ */
+public class EditTextUtils {
+ private EditTextUtils() {
+ throw new AssertionError();
+ }
+
+ /**
+ * returns non-null text string from passed TextView
+ */
+ public static String getText(TextView textView) {
+ return (textView != null) ? textView.getText().toString() : "";
+ }
+
+ /**
+ * moves caret to end of text
+ */
+ public static void moveToEnd(EditText edit) {
+ if (edit.getText() == null) {
+ return;
+ }
+ edit.setSelection(edit.getText().toString().length());
+ }
+
+ /**
+ * returns true if nothing has been entered into passed editor
+ */
+ public static boolean isEmpty(EditText edit) {
+ return TextUtils.isEmpty(getText(edit));
+ }
+
+ /**
+ * hide the soft keyboard for the passed EditText
+ */
+ public static void hideSoftInput(EditText edit) {
+ if (edit == null) {
+ return;
+ }
+
+ InputMethodManager imm = getInputMethodManager(edit);
+ if (imm != null) {
+ imm.hideSoftInputFromWindow(edit.getWindowToken(), 0);
+ }
+ }
+
+ /**
+ * show the soft keyboard for the passed EditText
+ */
+ public static void showSoftInput(EditText edit) {
+ if (edit == null) {
+ return;
+ }
+
+ edit.requestFocus();
+
+ InputMethodManager imm = getInputMethodManager(edit);
+ if (imm != null) {
+ imm.showSoftInput(edit, InputMethodManager.SHOW_IMPLICIT);
+ }
+ }
+
+ private static InputMethodManager getInputMethodManager(EditText edit) {
+ Context context = edit.getContext();
+ return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EmoticonsUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EmoticonsUtils.java
new file mode 100644
index 000000000..45661d980
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/EmoticonsUtils.java
@@ -0,0 +1,106 @@
+package org.wordpress.android.util;
+
+import android.text.Html;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.ImageSpan;
+import android.util.SparseArray;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES;
+
+public class EmoticonsUtils {
+ public static final int EMOTICON_COLOR = 0xFF21759B;
+ private static final boolean HAS_EMOJI = SDK_INT >= VERSION_CODES.JELLY_BEAN;
+ private static final Map<String, String> wpSmilies;
+ public static final SparseArray<String> wpSmiliesCodePointToText;
+
+ static {
+ Map<String, String> smilies = new HashMap<String, String>();
+ smilies.put("icon_mrgreen.gif", HAS_EMOJI ? "\uD83D\uDE00" : ":mrgreen:" );
+ smilies.put("icon_neutral.gif", HAS_EMOJI ? "\uD83D\uDE14" : ":|" );
+ smilies.put("icon_twisted.gif", HAS_EMOJI ? "\uD83D\uDE16" : ":twisted:" );
+ smilies.put("icon_arrow.gif", HAS_EMOJI ? "\u27A1" : ":arrow:" );
+ smilies.put("icon_eek.gif", HAS_EMOJI ? "\uD83D\uDE32" : "8-O" );
+ smilies.put("icon_smile.gif", HAS_EMOJI ? "\uD83D\uDE0A" : ":)" );
+ smilies.put("icon_confused.gif", HAS_EMOJI ? "\uD83D\uDE15" : ":?" );
+ smilies.put("icon_cool.gif", HAS_EMOJI ? "\uD83D\uDE0A" : "8)" );
+ smilies.put("icon_evil.gif", HAS_EMOJI ? "\uD83D\uDE21" : ":evil:" );
+ smilies.put("icon_biggrin.gif", HAS_EMOJI ? "\uD83D\uDE03" : ":D" );
+ smilies.put("icon_idea.gif", HAS_EMOJI ? "\uD83D\uDCA1" : ":idea:" );
+ smilies.put("icon_redface.gif", HAS_EMOJI ? "\uD83D\uDE33" : ":oops:" );
+ smilies.put("icon_razz.gif", HAS_EMOJI ? "\uD83D\uDE1D" : ":P" );
+ smilies.put("icon_rolleyes.gif", HAS_EMOJI ? "\uD83D\uDE0F" : ":roll:" );
+ smilies.put("icon_wink.gif", HAS_EMOJI ? "\uD83D\uDE09" : ";)" );
+ smilies.put("icon_cry.gif", HAS_EMOJI ? "\uD83D\uDE22" : ":'(" );
+ smilies.put("icon_surprised.gif", HAS_EMOJI ? "\uD83D\uDE32" : ":o" );
+ smilies.put("icon_lol.gif", HAS_EMOJI ? "\uD83D\uDE03" : ":lol:" );
+ smilies.put("icon_mad.gif", HAS_EMOJI ? "\uD83D\uDE21" : ":x" );
+ smilies.put("icon_sad.gif", HAS_EMOJI ? "\uD83D\uDE1E" : ":(" );
+ smilies.put("icon_exclaim.gif", HAS_EMOJI ? "\u2757" : ":!:" );
+ smilies.put("icon_question.gif", HAS_EMOJI ? "\u2753" : ":?:" );
+
+ wpSmilies = Collections.unmodifiableMap(smilies);
+
+ wpSmiliesCodePointToText = new SparseArray<String>(20);
+ wpSmiliesCodePointToText.put(10145, ":arrow:");
+ wpSmiliesCodePointToText.put(128161, ":idea:");
+ wpSmiliesCodePointToText.put(128512, ":mrgreen:");
+ wpSmiliesCodePointToText.put(128515, ":D");
+ wpSmiliesCodePointToText.put(128522, ":)");
+ wpSmiliesCodePointToText.put(128521, ";)");
+ wpSmiliesCodePointToText.put(128532, ":|");
+ wpSmiliesCodePointToText.put(128533, ":?");
+ wpSmiliesCodePointToText.put(128534, ":twisted:");
+ wpSmiliesCodePointToText.put(128542, ":(");
+ wpSmiliesCodePointToText.put(128545, ":evil:");
+ wpSmiliesCodePointToText.put(128546, ":'(");
+ wpSmiliesCodePointToText.put(128562, ":o");
+ wpSmiliesCodePointToText.put(128563, ":oops:");
+ wpSmiliesCodePointToText.put(128527, ":roll:");
+ wpSmiliesCodePointToText.put(10071, ":!:");
+ wpSmiliesCodePointToText.put(10067, ":?:");
+ }
+
+ public static String lookupImageSmiley(String url){
+ return lookupImageSmiley(url, "");
+ }
+
+ public static String lookupImageSmiley(String url, String ifNone){
+ String file = url.substring(url.lastIndexOf("/") + 1);
+ if (wpSmilies.containsKey(file)) {
+ return wpSmilies.get(file);
+ }
+ return ifNone;
+ }
+
+ public static Spanned replaceEmoticonsWithEmoji(SpannableStringBuilder html){
+ ImageSpan imgs[] = html.getSpans(0, html.length(), ImageSpan.class);
+ for (ImageSpan img : imgs) {
+ String emoticon = EmoticonsUtils.lookupImageSmiley(img.getSource());
+ if (!emoticon.equals("")) {
+ int start = html.getSpanStart(img);
+ html.replace(start, html.getSpanEnd(img), emoticon);
+ html.setSpan(new ForegroundColorSpan(EMOTICON_COLOR), start,
+ start + emoticon.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ html.removeSpan(img);
+ }
+ }
+ return html;
+ }
+
+ public static String replaceEmoticonsWithEmoji(final String text) {
+ if (text != null && text.contains("icon_")) {
+ final SpannableStringBuilder html = (SpannableStringBuilder)replaceEmoticonsWithEmoji((SpannableStringBuilder) Html.fromHtml(text));
+ // Html.toHtml() is used here rather than toString() since the latter strips html
+ return Html.toHtml(html);
+ } else {
+ return text;
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/FormatUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/FormatUtils.java
new file mode 100644
index 000000000..28282ed5f
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/FormatUtils.java
@@ -0,0 +1,35 @@
+package org.wordpress.android.util;
+
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+
+public class FormatUtils {
+ /*
+ * NumberFormat isn't synchronized, so a separate instance must be created for each thread
+ * http://developer.android.com/reference/java/text/NumberFormat.html
+ */
+ private static final ThreadLocal<NumberFormat> IntegerInstance = new ThreadLocal<NumberFormat>() {
+ @Override
+ protected NumberFormat initialValue() {
+ return NumberFormat.getIntegerInstance();
+ }
+ };
+
+ private static final ThreadLocal<DecimalFormat> DecimalInstance = new ThreadLocal<DecimalFormat>() {
+ @Override
+ protected DecimalFormat initialValue() {
+ return (DecimalFormat) DecimalFormat.getInstance();
+ }
+ };
+
+ /*
+ * returns the passed integer formatted with thousands-separators based on the current locale
+ */
+ public static final String formatInt(int value) {
+ return IntegerInstance.get().format(value).toString();
+ }
+
+ public static final String formatDecimal(int value) {
+ return DecimalInstance.get().format(value).toString();
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GeocoderUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GeocoderUtils.java
new file mode 100644
index 000000000..372473e15
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GeocoderUtils.java
@@ -0,0 +1,116 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.location.Address;
+import android.location.Geocoder;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+
+public final class GeocoderUtils {
+ private GeocoderUtils() {
+ throw new AssertionError();
+ }
+
+ public static Geocoder getGeocoder(Context context) {
+ // first make sure a Geocoder service exists on this device (requires API 9)
+ if (!Geocoder.isPresent()) {
+ return null;
+ }
+
+ Geocoder gcd;
+
+ try {
+ gcd = new Geocoder(context, LanguageUtils.getCurrentDeviceLanguage(context));
+ } catch (NullPointerException cannotIstantiateEx) {
+ AppLog.e(AppLog.T.UTILS, "Cannot instantiate Geocoder", cannotIstantiateEx);
+ return null;
+ }
+
+ return gcd;
+ }
+
+ public static Address getAddressFromCoords(Context context, double latitude, double longitude) {
+ Address address = null;
+ List<Address> addresses = null;
+
+ Geocoder gcd = getGeocoder(context);
+
+ if (gcd == null) {
+ return null;
+ }
+
+ try {
+ addresses = gcd.getFromLocation(latitude, longitude, 1);
+ } catch (IOException e) {
+ // may get "Unable to parse response from server" IOException here if Geocoder
+ // service is hit too frequently
+ AppLog.e(AppLog.T.UTILS,
+ "Unable to parse response from server. Is Geocoder service hitting the server too frequently?",
+ e
+ );
+ }
+
+ // addresses may be null or empty if network isn't connected
+ if (addresses != null && addresses.size() > 0) {
+ address = addresses.get(0);
+ }
+
+ return address;
+ }
+
+ public static Address getAddressFromLocationName(Context context, String locationName) {
+ int maxResults = 1;
+ Address address = null;
+ List<Address> addresses = null;
+
+ Geocoder gcd = getGeocoder(context);
+
+ if (gcd == null) {
+ return null;
+ }
+
+ try {
+ addresses = gcd.getFromLocationName(locationName, maxResults);
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.UTILS, "Failed to get coordinates from location", e);
+ }
+
+ // addresses may be null or empty if network isn't connected
+ if (addresses != null && addresses.size() > 0) {
+ address = addresses.get(0);
+ }
+
+ return address;
+ }
+
+ public static String getLocationNameFromAddress(Address address) {
+ String locality = "", adminArea = "", country = "";
+ if (address.getLocality() != null) {
+ locality = address.getLocality();
+ }
+
+ if (address.getAdminArea() != null) {
+ adminArea = address.getAdminArea();
+ }
+
+ if (address.getCountryName() != null) {
+ country = address.getCountryName();
+ }
+
+ return ((locality.equals("")) ? locality : locality + ", ")
+ + ((adminArea.equals("")) ? adminArea : adminArea + " ") + country;
+ }
+
+ public static double[] getCoordsFromAddress(Address address) {
+ double[] coordinates = new double[2];
+
+ if (address.hasLatitude() && address.hasLongitude()) {
+ coordinates[0] = address.getLatitude();
+ coordinates[1] = address.getLongitude();
+ }
+
+ return coordinates;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GravatarUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GravatarUtils.java
new file mode 100644
index 000000000..1fbfb3e56
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/GravatarUtils.java
@@ -0,0 +1,84 @@
+package org.wordpress.android.util;
+
+import android.text.TextUtils;
+
+/**
+ * see https://en.gravatar.com/site/implement/images/
+ */
+public class GravatarUtils {
+
+ // by default tell gravatar to respond to non-existent images with a 404 - this means
+ // it's up to the caller to catch the 404 and provide a suitable default image
+ private static final DefaultImage DEFAULT_GRAVATAR = DefaultImage.STATUS_404;
+
+ public static enum DefaultImage {
+ MYSTERY_MAN,
+ STATUS_404,
+ IDENTICON,
+ MONSTER,
+ WAVATAR,
+ RETRO,
+ BLANK;
+
+ @Override
+ public String toString() {
+ switch (this) {
+ case MYSTERY_MAN:
+ return "mm";
+ case STATUS_404:
+ return "404";
+ case IDENTICON:
+ return "identicon";
+ case MONSTER:
+ return "monsterid";
+ case WAVATAR:
+ return "wavatar";
+ case RETRO:
+ return "retro";
+ default:
+ return "blank";
+ }
+ }
+ }
+
+ /*
+ * gravatars often contain the ?s= parameter which determines their size - detect this and
+ * replace it with a new ?s= parameter which requests the avatar at the exact size needed
+ */
+ public static String fixGravatarUrl(final String imageUrl, int avatarSz) {
+ return fixGravatarUrl(imageUrl, avatarSz, DEFAULT_GRAVATAR);
+ }
+ public static String fixGravatarUrl(final String imageUrl, int avatarSz, DefaultImage defaultImage) {
+ if (TextUtils.isEmpty(imageUrl)) {
+ return "";
+ }
+
+ // if this isn't a gravatar image, return as resized photon image url
+ if (!imageUrl.contains("gravatar.com")) {
+ return PhotonUtils.getPhotonImageUrl(imageUrl, avatarSz, avatarSz);
+ }
+
+ // remove all other params, then add query string for size and default image
+ return UrlUtils.removeQuery(imageUrl) + "?s=" + avatarSz + "&d=" + defaultImage.toString();
+ }
+
+ public static String gravatarFromEmail(final String email, int size) {
+ return gravatarFromEmail(email, size, DEFAULT_GRAVATAR);
+ }
+ public static String gravatarFromEmail(final String email, int size, DefaultImage defaultImage) {
+ return "http://gravatar.com/avatar/"
+ + StringUtils.getMd5Hash(StringUtils.notNullStr(email))
+ + "?d=" + defaultImage.toString()
+ + "&size=" + Integer.toString(size);
+ }
+
+ public static String blavatarFromUrl(final String url, int size) {
+ return blavatarFromUrl(url, size, DEFAULT_GRAVATAR);
+ }
+ public static String blavatarFromUrl(final String url, int size, DefaultImage defaultImage) {
+ return "http://gravatar.com/blavatar/"
+ + StringUtils.getMd5Hash(UrlUtils.getHost(url))
+ + "?d=" + defaultImage.toString()
+ + "&size=" + Integer.toString(size);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HTTPUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HTTPUtils.java
new file mode 100644
index 000000000..9773d45d7
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HTTPUtils.java
@@ -0,0 +1,31 @@
+package org.wordpress.android.util;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Map;
+
+public class HTTPUtils {
+ public static final int REQUEST_TIMEOUT_MS = 30000;
+
+ /**
+ * Builds an HttpURLConnection from a URL and header map. Will force HTTPS usage if given an Authorization header.
+ * @throws IOException
+ */
+ public static HttpURLConnection setupUrlConnection(String url, Map<String, String> headers) throws IOException {
+ // Force HTTPS usage if an authorization header was specified
+ if (headers.keySet().contains("Authorization")) {
+ url = UrlUtils.makeHttps(url);
+ }
+
+ HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
+ conn.setReadTimeout(REQUEST_TIMEOUT_MS);
+ conn.setConnectTimeout(REQUEST_TIMEOUT_MS);
+
+ for (Map.Entry<String, String> entry : headers.entrySet()) {
+ conn.setRequestProperty(entry.getKey(), entry.getValue());
+ }
+
+ return conn;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HtmlUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HtmlUtils.java
new file mode 100644
index 000000000..b5319372a
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/HtmlUtils.java
@@ -0,0 +1,156 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.Html;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.QuoteSpan;
+
+import org.apache.commons.lang.StringEscapeUtils;
+import org.wordpress.android.util.helpers.WPHtmlTagHandler;
+import org.wordpress.android.util.helpers.WPImageGetter;
+import org.wordpress.android.util.helpers.WPQuoteSpan;
+
+public class HtmlUtils {
+
+ /**
+ * Removes html from the passed string - relies on Html.fromHtml which handles invalid HTML,
+ * but it's very slow, so avoid using this where performance is important
+ * @param text String containing html
+ * @return String without HTML
+ */
+ public static String stripHtml(final String text) {
+ if (TextUtils.isEmpty(text)) {
+ return "";
+ }
+ return Html.fromHtml(text).toString().trim();
+ }
+
+ /**
+ * This is much faster than stripHtml() but should only be used when we know the html is valid
+ * since the regex will be unpredictable with invalid html
+ * @param str String containing only valid html
+ * @return String without HTML
+ */
+ public static String fastStripHtml(String str) {
+ if (TextUtils.isEmpty(str)) {
+ return str;
+ }
+
+ // insert a line break before P tags unless the only one is at the start
+ if (str.lastIndexOf("<p") > 0) {
+ str = str.replaceAll("<p(.|\n)*?>", "\n<p>");
+ }
+
+ // convert BR tags to line breaks
+ if (str.contains("<br")) {
+ str = str.replaceAll("<br(.|\n)*?>", "\n");
+ }
+
+ // use regex to strip tags, then convert entities in the result
+ return trimStart(fastUnescapeHtml(str.replaceAll("<(.|\n)*?>", "")));
+ }
+
+ /*
+ * Same as apache.commons.lang.StringUtils.stripStart() but also removes non-breaking
+ * space (160) chars
+ */
+ private static String trimStart(final String str) {
+ int strLen;
+ if (str == null || (strLen = str.length()) == 0) {
+ return "";
+ }
+ int start = 0;
+ while (start != strLen && (Character.isWhitespace(str.charAt(start)) || str.charAt(start) == 160)) {
+ start++;
+ }
+ return str.substring(start);
+ }
+
+ /**
+ * Convert html entities to actual Unicode characters - relies on commons apache lang
+ * @param text String to be decoded to Unicode
+ * @return String containing unicode characters
+ */
+ public static String fastUnescapeHtml(final String text) {
+ if (text == null || !text.contains("&")) {
+ return text;
+ }
+ return StringEscapeUtils.unescapeHtml(text);
+ }
+
+ /**
+ * Converts an R.color.xxx resource to an HTML hex color
+ * @param context Android Context
+ * @param resId Android R.color.xxx
+ * @return A String HTML hex color code
+ */
+ public static String colorResToHtmlColor(Context context, int resId) {
+ try {
+ return String.format("#%06X", 0xFFFFFF & context.getResources().getColor(resId));
+ } catch (Resources.NotFoundException e) {
+ return "#000000";
+ }
+ }
+
+ /**
+ * Remove {@code <script>..</script>} blocks from the passed string - added to project after noticing
+ * comments on posts that use the "Sociable" plugin ( http://wordpress.org/plugins/sociable/ )
+ * may have a script block which contains {@code <!--//-->} followed by a CDATA section followed by {@code <!]]>,}
+ * all of which will show up if we don't strip it here.
+ * @see <a href="http://wordpress.org/plugins/sociable/">Wordpress Sociable Plugin</a>
+ * @return String without {@code <script>..</script>}, {@code <!--//-->} blocks followed by a CDATA section followed by {@code <!]]>,}
+ * @param text String containing script tags
+ */
+ public static String stripScript(final String text) {
+ if (text == null) {
+ return null;
+ }
+
+ StringBuilder sb = new StringBuilder(text);
+ int start = sb.indexOf("<script");
+
+ while (start > -1) {
+ int end = sb.indexOf("</script>", start);
+ if (end == -1) {
+ return sb.toString();
+ }
+ sb.delete(start, end + 9);
+ start = sb.indexOf("<script", start);
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * An alternative to Html.fromHtml() supporting {@code <ul>}, {@code <ol>}, {@code <blockquote>}
+ * tags and replacing EmoticonsUtils with Emojis
+ * @param source
+ * @param wpImageGetter
+ */
+ public static SpannableStringBuilder fromHtml(String source, WPImageGetter wpImageGetter) {
+ SpannableStringBuilder html;
+ try {
+ html = (SpannableStringBuilder) Html.fromHtml(source, wpImageGetter, new WPHtmlTagHandler());
+ } catch (RuntimeException runtimeException) {
+ // In case our tag handler fails
+ html = (SpannableStringBuilder) Html.fromHtml(source, wpImageGetter, null);
+ }
+ EmoticonsUtils.replaceEmoticonsWithEmoji(html);
+ QuoteSpan spans[] = html.getSpans(0, html.length(), QuoteSpan.class);
+ for (QuoteSpan span : spans) {
+ html.setSpan(new WPQuoteSpan(), html.getSpanStart(span), html.getSpanEnd(span), html.getSpanFlags(span));
+ html.setSpan(new ForegroundColorSpan(0xFF666666), html.getSpanStart(span), html.getSpanEnd(span),
+ html.getSpanFlags(span));
+ html.removeSpan(span);
+ }
+ return html;
+ }
+
+ public static Spanned fromHtml(String source) {
+ return fromHtml(source, null);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ImageUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ImageUtils.java
new file mode 100644
index 000000000..2fd4449b8
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ImageUtils.java
@@ -0,0 +1,649 @@
+package org.wordpress.android.util;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+import android.widget.ImageView;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+
+public class ImageUtils {
+ public static int[] getImageSize(Uri uri, Context context){
+ String path = null;
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+
+ if (uri.toString().contains("content:")) {
+ String[] projection = new String[] { MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA };
+ Cursor cur = null;
+ try {
+ cur = context.getContentResolver().query(uri, projection, null, null, null);
+ if (cur != null && cur.moveToFirst()) {
+ int dataColumn = cur.getColumnIndex(MediaStore.Images.Media.DATA);
+ path = cur.getString(dataColumn);
+ }
+ } catch (IllegalStateException stateException) {
+ Log.d(ImageUtils.class.getName(), "IllegalStateException querying content:" + uri);
+ } finally {
+ SqlUtils.closeCursor(cur);
+ }
+ }
+
+ if (TextUtils.isEmpty(path)) {
+ //The file isn't ContentResolver, or it can't be access by ContentResolver. Try to access the file directly.
+ path = uri.toString().replace("content://media", "");
+ path = path.replace("file://", "");
+ }
+
+ BitmapFactory.decodeFile(path, options);
+ int imageHeight = options.outHeight;
+ int imageWidth = options.outWidth;
+ return new int[]{imageWidth, imageHeight};
+ }
+
+ // Read the orientation from ContentResolver. If it fails, read from EXIF.
+ public static int getImageOrientation(Context ctx, String filePath) {
+ Uri curStream;
+ int orientation = 0;
+
+ // Remove file protocol
+ filePath = filePath.replace("file://", "");
+
+ if (!filePath.contains("content://"))
+ curStream = Uri.parse("content://media" + filePath);
+ else
+ curStream = Uri.parse(filePath);
+
+ try {
+ Cursor cur = ctx.getContentResolver().query(curStream, new String[]{MediaStore.Images.Media.ORIENTATION}, null, null, null);
+ if (cur != null) {
+ if (cur.moveToFirst()) {
+ orientation = cur.getInt(cur.getColumnIndex(MediaStore.Images.Media.ORIENTATION));
+ }
+ cur.close();
+ }
+ } catch (Exception errReadingContentResolver) {
+ AppLog.e(AppLog.T.UTILS, errReadingContentResolver);
+ }
+
+ if (orientation == 0) {
+ orientation = getExifOrientation(filePath);
+ }
+
+ return orientation;
+ }
+
+
+ public static int getExifOrientation(String path) {
+ ExifInterface exif;
+ try {
+ exif = new ExifInterface(path);
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ return 0;
+ }
+
+ int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0);
+
+ switch (exifOrientation) {
+ case ExifInterface.ORIENTATION_NORMAL:
+ return 0;
+ case ExifInterface.ORIENTATION_ROTATE_90:
+ return 90;
+ case ExifInterface.ORIENTATION_ROTATE_180:
+ return 180;
+ case ExifInterface.ORIENTATION_ROTATE_270:
+ return 270;
+ default:
+ return 0;
+ }
+ }
+
+ public static Bitmap downloadBitmap(String url) {
+ final DefaultHttpClient client = new DefaultHttpClient();
+
+ final HttpGet getRequest = new HttpGet(url);
+
+ try {
+ HttpResponse response = client.execute(getRequest);
+ final int statusCode = response.getStatusLine().getStatusCode();
+ if (statusCode != HttpStatus.SC_OK) {
+ AppLog.w(AppLog.T.UTILS, "ImageDownloader Error " + statusCode
+ + " while retrieving bitmap from " + url);
+ return null;
+ }
+
+ final HttpEntity entity = response.getEntity();
+ if (entity != null) {
+ InputStream inputStream = null;
+ try {
+ inputStream = entity.getContent();
+ return BitmapFactory.decodeStream(inputStream);
+ } finally {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ entity.consumeContent();
+ }
+ }
+ } catch (Exception e) {
+ // Could provide a more explicit error message for IOException or
+ // IllegalStateException
+ getRequest.abort();
+ AppLog.w(AppLog.T.UTILS, "ImageDownloader Error while retrieving bitmap from " + url);
+ }
+ return null;
+ }
+
+ /** From http://developer.android.com/training/displaying-bitmaps/load-bitmap.html **/
+ public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
+ // Raw height and width of image
+ final int height = options.outHeight;
+ final int width = options.outWidth;
+ int inSampleSize = 1;
+
+ if (height > reqHeight || width > reqWidth) {
+ // Calculate ratios of height and width to requested height and width
+ final int heightRatio = Math.round((float) height / (float) reqHeight);
+ final int widthRatio = Math.round((float) width / (float) reqWidth);
+
+ // Choose the smallest ratio as inSampleSize value, this will guarantee
+ // a final image with both dimensions larger than or equal to the
+ // requested height and width.
+ inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
+ }
+
+ return inSampleSize;
+ }
+
+
+ public interface BitmapWorkerCallback {
+ public void onBitmapReady(String filePath, ImageView imageView, Bitmap bitmap);
+ }
+
+ public static class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> {
+ private final WeakReference<ImageView> imageViewReference;
+ private final BitmapWorkerCallback callback;
+ private int targetWidth;
+ private int targetHeight;
+ private String path;
+
+ public BitmapWorkerTask(ImageView imageView, int width, int height, BitmapWorkerCallback callback) {
+ // Use a WeakReference to ensure the ImageView can be garbage collected
+ imageViewReference = new WeakReference<ImageView>(imageView);
+ this.callback = callback;
+ targetWidth = width;
+ targetHeight = height;
+ }
+
+ // Decode image in background.
+ @Override
+ protected Bitmap doInBackground(String... params) {
+ path = params[0];
+
+ BitmapFactory.Options bfo = new BitmapFactory.Options();
+ bfo.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(path, bfo);
+
+ bfo.inSampleSize = calculateInSampleSize(bfo, targetWidth, targetHeight);
+ bfo.inJustDecodeBounds = false;
+
+ // get proper rotation
+ int bitmapWidth = 0;
+ int bitmapHeight = 0;
+ try {
+ File f = new File(path);
+ ExifInterface exif = new ExifInterface(f.getPath());
+ int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
+ int angle = 0;
+ if (orientation == ExifInterface.ORIENTATION_NORMAL) { // no need to rotate
+ return BitmapFactory.decodeFile(path, bfo);
+ } else if (orientation == ExifInterface.ORIENTATION_ROTATE_90) {
+ angle = 90;
+ } else if (orientation == ExifInterface.ORIENTATION_ROTATE_180) {
+ angle = 180;
+ } else if (orientation == ExifInterface.ORIENTATION_ROTATE_270) {
+ angle = 270;
+ }
+
+ Matrix mat = new Matrix();
+ mat.postRotate(angle);
+
+ try {
+ Bitmap bmp = BitmapFactory.decodeStream(new FileInputStream(f), null, bfo);
+ if (bmp == null) {
+ AppLog.e(AppLog.T.UTILS, "can't decode bitmap: " + f.getPath());
+ return null;
+ }
+ bitmapWidth = bmp.getWidth();
+ bitmapHeight = bmp.getHeight();
+ return Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), bmp.getHeight(), mat, true);
+ } catch (OutOfMemoryError oom) {
+ AppLog.e(AppLog.T.UTILS, "OutOfMemoryError Error in setting image: " + oom);
+ }
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.UTILS, "Error in setting image", e);
+ }
+
+ return null;
+ }
+
+ // Once complete, see if ImageView is still around and set bitmap.
+ @Override
+ protected void onPostExecute(Bitmap bitmap) {
+ if (imageViewReference == null || bitmap == null)
+ return;
+
+ final ImageView imageView = imageViewReference.get();
+
+ if (callback != null)
+ callback.onBitmapReady(path, imageView, bitmap);
+
+ }
+ }
+
+
+ public static String getTitleForWPImageSpan(Context ctx, String filePath) {
+ if (filePath == null)
+ return null;
+
+ Uri curStream;
+ String title;
+
+ if (!filePath.contains("content://"))
+ curStream = Uri.parse("content://media" + filePath);
+ else
+ curStream = Uri.parse(filePath);
+
+ if (filePath.contains("video")) {
+ return "Video";
+ } else {
+ String[] projection = new String[] { MediaStore.Images.Thumbnails.DATA };
+
+ Cursor cur;
+ try {
+ cur = ctx.getContentResolver().query(curStream, projection, null, null, null);
+ } catch (Exception e1) {
+ AppLog.e(AppLog.T.UTILS, e1);
+ return null;
+ }
+ File jpeg;
+ if (cur != null) {
+ String thumbData = "";
+ if (cur.moveToFirst()) {
+ int dataColumn = cur.getColumnIndex(MediaStore.Images.Media.DATA);
+ thumbData = cur.getString(dataColumn);
+ }
+ cur.close();
+ if (thumbData == null) {
+ return null;
+ }
+ jpeg = new File(thumbData);
+ } else {
+ String path = filePath.toString().replace("file://", "");
+ jpeg = new File(path);
+ }
+ title = jpeg.getName();
+ return title;
+ }
+ }
+
+ /**
+ * Resizes an image to be placed in the Post Content Editor
+ *
+ * @return resized bitmap
+ */
+ public static Bitmap getWPImageSpanThumbnailFromFilePath(Context context, String filePath, int targetWidth) {
+ if (filePath == null || context == null) {
+ return null;
+ }
+
+ Uri curUri;
+ if (!filePath.contains("content://")) {
+ curUri = Uri.parse("content://media" + filePath);
+ } else {
+ curUri = Uri.parse(filePath);
+ }
+
+ if (filePath.contains("video")) {
+ // Load the video thumbnail from the MediaStore
+ int videoId = 0;
+ try {
+ videoId = Integer.parseInt(curUri.getLastPathSegment());
+ } catch (NumberFormatException e) {
+ }
+ ContentResolver crThumb = context.getContentResolver();
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = 1;
+ Bitmap videoThumbnail = MediaStore.Video.Thumbnails.getThumbnail(crThumb, videoId, MediaStore.Video.Thumbnails.MINI_KIND,
+ options);
+ if (videoThumbnail != null) {
+ return getScaledBitmapAtLongestSide(videoThumbnail, targetWidth);
+ } else {
+ return null;
+ }
+ } else {
+ // Create resized bitmap
+ int rotation = getImageOrientation(context, filePath);
+ byte[] bytes = createThumbnailFromUri(context, curUri, targetWidth, null, rotation);
+
+ if (bytes != null && bytes.length > 0) {
+ try {
+ Bitmap resizedBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
+ if (resizedBitmap != null) {
+ return getScaledBitmapAtLongestSide(resizedBitmap, targetWidth);
+ }
+ } catch (OutOfMemoryError e) {
+ AppLog.e(AppLog.T.UTILS, "OutOfMemoryError Error in setting image: " + e);
+ return null;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /*
+ Resize a bitmap to the targetSize on its longest side.
+ */
+ public static Bitmap getScaledBitmapAtLongestSide(Bitmap bitmap, int targetSize) {
+ if (bitmap.getWidth() <= targetSize && bitmap.getHeight() <= targetSize) {
+ // Do not resize.
+ return bitmap;
+ }
+
+ int targetWidth, targetHeight;
+ if (bitmap.getHeight() > bitmap.getWidth()) {
+ // Resize portrait bitmap
+ targetHeight = targetSize;
+ float percentage = (float) targetSize / bitmap.getHeight();
+ targetWidth = (int)(bitmap.getWidth() * percentage);
+ } else {
+ // Resize landscape or square image
+ targetWidth = targetSize;
+ float percentage = (float) targetSize / bitmap.getWidth();
+ targetHeight = (int)(bitmap.getHeight() * percentage);
+ }
+
+ return Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, true);
+ }
+
+ /**
+ * Given the path to an image, resize the image down to within a maximum width
+ * @param path the path to the original image
+ * @param maxWidth the maximum allowed width
+ * @return the path to the resized image
+ */
+ public static String createResizedImageWithMaxWidth(Context context, String path, int maxWidth) {
+ File file = new File(path);
+ if (!file.exists()) {
+ return path;
+ }
+
+ String mimeType = MediaUtils.getMediaFileMimeType(file);
+ if (mimeType.equals("image/gif")) {
+ // Don't rescale gifs to maintain their quality
+ return path;
+ }
+
+ String fileName = MediaUtils.getMediaFileName(file, mimeType);
+ String fileExtension = MimeTypeMap.getFileExtensionFromUrl(fileName).toLowerCase();
+
+ int[] dimensions = getImageSize(Uri.fromFile(file), context);
+ int orientation = getImageOrientation(context, path);
+
+ if (dimensions[0] <= maxWidth) {
+ // Image width is within limits; don't resize
+ return path;
+ }
+
+ // Create resized image
+ byte[] bytes = ImageUtils.createThumbnailFromUri(context, Uri.parse(path), maxWidth, fileExtension, orientation);
+
+ if (bytes != null) {
+ try {
+ File resizedImageFile = File.createTempFile("wp-image-", fileExtension);
+ FileOutputStream out = new FileOutputStream(resizedImageFile);
+ out.write(bytes);
+ out.close();
+
+ String tempFilePath = resizedImageFile.getPath();
+
+ if (!TextUtils.isEmpty(tempFilePath)) {
+ return tempFilePath;
+ } else {
+ AppLog.e(AppLog.T.POSTS, "Failed to create resized image");
+ }
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.POSTS, "Failed to create image temp file");
+ }
+ }
+
+ return path;
+ }
+
+ /**
+ * nbradbury - 21-Feb-2014 - similar to createThumbnail but more efficient since it doesn't
+ * require passing the full-size image as an array of bytes[]
+ */
+ public static byte[] createThumbnailFromUri(Context context,
+ Uri imageUri,
+ int maxWidth,
+ String fileExtension,
+ int rotation) {
+ if (context == null || imageUri == null || maxWidth <= 0)
+ return null;
+
+ String filePath = null;
+ if (imageUri.toString().contains("content:")) {
+ String[] projection = new String[] { MediaStore.Images.Media.DATA };
+ Cursor cur = null;
+ try {
+ cur = context.getContentResolver().query(imageUri, projection, null, null, null);
+ if (cur != null && cur.moveToFirst()) {
+ int dataColumn = cur.getColumnIndex(MediaStore.Images.Media.DATA);
+ filePath = cur.getString(dataColumn);
+ }
+ } catch (IllegalStateException stateException) {
+ Log.d(ImageUtils.class.getName(), "IllegalStateException querying content:" + imageUri);
+ } finally {
+ SqlUtils.closeCursor(cur);
+ }
+ }
+
+ if (TextUtils.isEmpty(filePath)) {
+ //access the file directly
+ filePath = imageUri.toString().replace("content://media", "");
+ filePath = filePath.replace("file://", "");
+ }
+
+ // get just the image bounds
+ BitmapFactory.Options optBounds = new BitmapFactory.Options();
+ optBounds.inJustDecodeBounds = true;
+
+ try {
+ BitmapFactory.decodeFile(filePath, optBounds);
+ } catch (OutOfMemoryError e) {
+ AppLog.e(AppLog.T.UTILS, "OutOfMemoryError Error in setting image: " + e);
+ return null;
+ }
+
+ // determine correct scale value (should be power of 2)
+ // http://stackoverflow.com/questions/477572/android-strange-out-of-memory-issue/3549021#3549021
+ int scale = 1;
+ if (maxWidth > 0 && optBounds.outWidth > maxWidth) {
+ double d = Math.pow(2, (int) Math.round(Math.log(maxWidth / (double) optBounds.outWidth) / Math.log(0.5)));
+ scale = (int) d;
+ }
+
+ BitmapFactory.Options optActual = new BitmapFactory.Options();
+ optActual.inSampleSize = scale;
+
+ // Get the roughly resized bitmap
+ final Bitmap bmpResized;
+ try {
+ bmpResized = BitmapFactory.decodeFile(filePath, optActual);
+ } catch (OutOfMemoryError e) {
+ AppLog.e(AppLog.T.UTILS, "OutOfMemoryError Error in setting image: " + e);
+ return null;
+ }
+
+ if (bmpResized == null) {
+ return null;
+ }
+
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+
+ // Now calculate exact scale in order to resize accurately
+ float percentage = (float) maxWidth / bmpResized.getWidth();
+ float proportionateHeight = bmpResized.getHeight() * percentage;
+ int finalHeight = (int) Math.rint(proportionateHeight);
+
+ float scaleWidth = ((float) maxWidth) / bmpResized.getWidth();
+ float scaleHeight = ((float) finalHeight) / bmpResized.getHeight();
+
+ float scaleBy = Math.min(scaleWidth, scaleHeight);
+
+ // Resize the bitmap to exact size
+ Matrix matrix = new Matrix();
+ matrix.postScale(scaleBy, scaleBy);
+
+ // apply rotation
+ if (rotation != 0) {
+ matrix.setRotate(rotation);
+ }
+
+ Bitmap.CompressFormat fmt;
+ if (fileExtension != null && fileExtension.equalsIgnoreCase("png")) {
+ fmt = Bitmap.CompressFormat.PNG;
+ } else {
+ fmt = Bitmap.CompressFormat.JPEG;
+ }
+
+ final Bitmap bmpRotated;
+ try {
+ bmpRotated = Bitmap.createBitmap(bmpResized, 0, 0, bmpResized.getWidth(), bmpResized.getHeight(), matrix,
+ true);
+ } catch (OutOfMemoryError e) {
+ AppLog.e(AppLog.T.UTILS, "OutOfMemoryError Error in setting image: " + e);
+ return null;
+ } catch (NullPointerException e) {
+ // See: https://github.com/wordpress-mobile/WordPress-Android/issues/1844
+ AppLog.e(AppLog.T.UTILS, "Bitmap.createBitmap has thrown a NPE internally. This should never happen: " + e);
+ return null;
+ }
+
+ if (bmpRotated == null) {
+ // Fix an issue where bmpRotated is null even if the documentation doesn't say Bitmap.createBitmap can return null.
+ // See: https://github.com/wordpress-mobile/WordPress-Android/issues/1848
+ return null;
+ }
+
+ bmpRotated.compress(fmt, 100, stream);
+ bmpResized.recycle();
+ bmpRotated.recycle();
+
+ return stream.toByteArray();
+ }
+
+ public static Bitmap getCircularBitmap(final Bitmap bitmap) {
+ if (bitmap==null)
+ return null;
+
+ final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(output);
+ final Paint paint = new Paint();
+ final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
+ final RectF rectF = new RectF(rect);
+
+ paint.setAntiAlias(true);
+ canvas.drawARGB(0, 0, 0, 0);
+ paint.setColor(Color.RED);
+ canvas.drawOval(rectF, paint);
+
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
+ canvas.drawBitmap(bitmap, rect, rect, paint);
+
+ return output;
+ }
+
+ /**
+ * Returns the passed bitmap with rounded corners
+ * @param bitmap - the bitmap to modify
+ * @param radius - the radius of the corners
+ * @param borderColor - the border to apply (use Color.TRANSPARENT for none)
+ */
+ public static Bitmap getRoundedEdgeBitmap(final Bitmap bitmap, int radius, int borderColor) {
+ if (bitmap == null) {
+ return null;
+ }
+
+ final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(output);
+ final Paint paint = new Paint();
+ final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
+ final RectF rectF = new RectF(rect);
+
+ paint.setAntiAlias(true);
+ canvas.drawARGB(0, 0, 0, 0);
+ paint.setColor(Color.RED);
+ canvas.drawRoundRect(rectF, radius, radius, paint);
+
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
+ canvas.drawBitmap(bitmap, rect, rect, paint);
+
+ if (borderColor != Color.TRANSPARENT) {
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeWidth(1f);
+ paint.setColor(borderColor);
+ canvas.drawRoundRect(rectF, radius, radius, paint);
+ }
+
+ return output;
+ }
+
+ /**
+ * Get the maximum size a thumbnail can be to fit in either portrait or landscape orientations.
+ */
+ public static int getMaximumThumbnailWidthForEditor(Context context) {
+ int maximumThumbnailWidthForEditor;
+ Point size = DisplayUtils.getDisplayPixelSize(context);
+ int screenWidth = size.x;
+ int screenHeight = size.y;
+ maximumThumbnailWidthForEditor = (screenWidth > screenHeight) ? screenHeight : screenWidth;
+ // 48dp of padding on each side so you can still place the cursor next to the image.
+ int padding = DisplayUtils.dpToPx(context, 48) * 2;
+ maximumThumbnailWidthForEditor -= padding;
+ return maximumThumbnailWidthForEditor;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/JSONUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/JSONUtils.java
new file mode 100644
index 000000000..196a7b1f3
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/JSONUtils.java
@@ -0,0 +1,251 @@
+package org.wordpress.android.util;
+
+import android.text.TextUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog.T;
+
+import java.util.ArrayList;
+
+public class JSONUtils {
+ private static String QUERY_SEPERATOR = ".";
+ private static String QUERY_ARRAY_INDEX_START = "[";
+ private static String QUERY_ARRAY_INDEX_END = "]";
+ private static String QUERY_ARRAY_FIRST = "first";
+ private static String QUERY_ARRAY_LAST = "last";
+
+ private static final String JSON_NULL_STR = "null";
+ private static final String TAG = "JSONUtils";
+
+ /**
+ * Given a JSONObject and a key path (e.g property.child) and a default it will
+ * traverse the object graph and pull out the desired property
+ */
+ public static <U> U queryJSON(JSONObject source, String query, U defaultObject) {
+ if (source == null) {
+ AppLog.e(T.UTILS, "Parameter source is null, can't query a null object");
+ return defaultObject;
+ }
+ if (query == null) {
+ AppLog.e(T.UTILS, "Parameter query is null");
+ return defaultObject;
+ }
+ int nextSeperator = query.indexOf(QUERY_SEPERATOR);
+ int nextIndexStart = query.indexOf(QUERY_ARRAY_INDEX_START);
+ if (nextSeperator == -1 && nextIndexStart == -1) {
+ // last item let's get it
+ try {
+ if (!source.has(query)) {
+ return defaultObject;
+ }
+ Object result = source.get(query);
+ if (result.getClass().isAssignableFrom(defaultObject.getClass())) {
+ return (U) result;
+ } else {
+ AppLog.w(T.UTILS, String.format("The returned object type %s is not assignable to the type %s. Using default!",
+ result.getClass(),defaultObject.getClass()));
+ return defaultObject;
+ }
+ } catch (java.lang.ClassCastException e) {
+ AppLog.e(T.UTILS, "Unable to cast the object to " + defaultObject.getClass().getName(), e);
+ return defaultObject;
+ } catch (JSONException e) {
+ AppLog.e(T.UTILS, "Unable to get the Key from the input object. Key:" + query, e);
+ return defaultObject;
+ }
+ }
+ int endQuery;
+ if (nextSeperator == -1 || nextIndexStart == -1) {
+ endQuery = Math.max(nextSeperator, nextIndexStart);
+ } else {
+ endQuery = Math.min(nextSeperator, nextIndexStart);
+ }
+ String nextQuery = query.substring(endQuery);
+ String key = query.substring(0, endQuery);
+ try {
+ if (nextQuery.indexOf(QUERY_SEPERATOR) == 0) {
+ return queryJSON(source.getJSONObject(key), nextQuery.substring(1), defaultObject);
+ } else if (nextQuery.indexOf(QUERY_ARRAY_INDEX_START) == 0) {
+ return queryJSON(source.getJSONArray(key), nextQuery, defaultObject);
+ } else if (!nextQuery.equals("")) {
+ return defaultObject;
+ }
+ Object result = source.get(key);
+ if (result.getClass().isAssignableFrom(defaultObject.getClass())) {
+ return (U) result;
+ } else {
+ AppLog.w(T.UTILS, String.format("The returned object type %s is not assignable to the type %s. Using default!",
+ result.getClass(),defaultObject.getClass()));
+ return defaultObject;
+ }
+ } catch (java.lang.ClassCastException e) {
+ AppLog.e(T.UTILS, "Unable to cast the object to " + defaultObject.getClass().getName(), e);
+ return defaultObject;
+ } catch (JSONException e) {
+ return defaultObject;
+ }
+ }
+
+ /**
+ * Given a JSONArray and a query (e.g. [0].property) it will traverse the array and
+ * pull out the requested property.
+ *
+ * Acceptable indexes include negative numbers to reference items from the end of
+ * the list as well as "last" and "first" as more explicit references to "0" and "-1"
+ */
+ public static <U> U queryJSON(JSONArray source, String query, U defaultObject) {
+ if (source == null) {
+ AppLog.e(T.UTILS, "Parameter source is null, can't query a null object");
+ return defaultObject;
+ }
+ if (query == null) {
+ AppLog.e(T.UTILS, "Parameter query is null");
+ return defaultObject;
+ }
+ // query must start with [ have an index and then have ]
+ int indexStart = query.indexOf(QUERY_ARRAY_INDEX_START);
+ int indexEnd = query.indexOf(QUERY_ARRAY_INDEX_END);
+ if (indexStart == -1 || indexEnd == -1 || indexStart > indexEnd) {
+ return defaultObject;
+ }
+ // get "index" from "[index]"
+ String indexStr = query.substring(indexStart + 1, indexEnd);
+ int index;
+ if (indexStr.equals(QUERY_ARRAY_FIRST)) {
+ index = 0;
+ } else if (indexStr.equals(QUERY_ARRAY_LAST)) {
+ index = -1;
+ } else {
+ index = Integer.parseInt(indexStr);
+ }
+ if (index < 0) {
+ index = source.length() + index;
+ }
+ // copy remaining query
+ String remainingQuery = query.substring(indexEnd + 1);
+ try {
+ if (remainingQuery.indexOf(QUERY_ARRAY_INDEX_START) == 0) {
+ return queryJSON(source.getJSONArray(index), remainingQuery, defaultObject);
+ } else if (remainingQuery.indexOf(QUERY_SEPERATOR) == 0) {
+ return queryJSON(source.getJSONObject(index), remainingQuery.substring(1), defaultObject);
+ } else if (!remainingQuery.equals("")) {
+ // TODO throw an exception since the query isn't valid?
+ AppLog.w(T.UTILS, String.format("Incorrect query for next object %s", remainingQuery));
+ return defaultObject;
+ }
+ Object result = source.get(index);
+ if (result.getClass().isAssignableFrom(defaultObject.getClass())) {
+ return (U) result;
+ } else {
+ AppLog.w(T.UTILS, String.format("The returned object type %s is not assignable to the type %s. Using default!",
+ result.getClass(),defaultObject.getClass()));
+ return defaultObject;
+ }
+ } catch (java.lang.ClassCastException e) {
+ AppLog.e(T.UTILS, "Unable to cast the object to "+defaultObject.getClass().getName(), e);
+ return defaultObject;
+ } catch (JSONException e) {
+ return defaultObject;
+ }
+ }
+
+ /**
+ * Convert a JSONArray (expected to contain strings) in a string list
+ */
+ public static ArrayList<String> fromJSONArrayToStringList(JSONArray jsonArray) {
+ ArrayList<String> stringList = new ArrayList<String>();
+ for (int i = 0; i < jsonArray.length(); i++) {
+ try {
+ stringList.add(jsonArray.getString(i));
+ } catch (JSONException e) {
+ AppLog.e(T.UTILS, e);
+ }
+ }
+ return stringList;
+ }
+
+ /**
+ * Convert a string list in a JSONArray
+ */
+ public static JSONArray fromStringListToJSONArray(ArrayList<String> stringList) {
+ JSONArray jsonArray = new JSONArray();
+ if (stringList != null) {
+ for (int i = 0; i < stringList.size(); i++) {
+ jsonArray.put(stringList.get(i));
+ }
+ }
+ return jsonArray;
+ }
+
+ /*
+ * wrapper for JSONObject.optString() which handles "null" values
+ */
+ public static String getString(JSONObject json, String name) {
+ String value = json.optString(name);
+ // return empty string for "null"
+ if (JSON_NULL_STR.equals(value))
+ return "";
+ return value;
+ }
+
+ /*
+ * use with strings that contain HTML entities
+ */
+ public static String getStringDecoded(JSONObject json, String name) {
+ String value = getString(json, name);
+ return HtmlUtils.fastUnescapeHtml(value);
+ }
+
+ /*
+ * replacement for JSONObject.optBoolean() - optBoolean() only checks for "true" and "false",
+ * but our API sometimes uses "0" to denote false
+ */
+ public static boolean getBool(JSONObject json, String name) {
+ String value = getString(json, name);
+ if (TextUtils.isEmpty(value))
+ return false;
+ if (value.equals("0"))
+ return false;
+ if (value.equalsIgnoreCase("false"))
+ return false;
+ if (value.equalsIgnoreCase("no"))
+ return false;
+ return true;
+ }
+
+ /*
+ * returns the JSONObject child of the passed parent that matches the passed query
+ * this is basically an "optJSONObject" that supports nested queries, for example:
+ *
+ * getJSONChild("meta/data/site")
+ *
+ * would find this:
+ *
+ * "meta": {
+ * "data": {
+ * "site": {
+ * "ID": 3584907,
+ * "name": "WordPress.com News",
+ * }
+ * }
+ * }
+ */
+ public static JSONObject getJSONChild(final JSONObject jsonParent, final String query) {
+ if (jsonParent == null || TextUtils.isEmpty(query))
+ return null;
+ String[] names = query.split("/");
+ JSONObject jsonChild = null;
+ for (int i = 0; i < names.length; i++) {
+ if (jsonChild == null) {
+ jsonChild = jsonParent.optJSONObject(names[i]);
+ } else {
+ jsonChild = jsonChild.optJSONObject(names[i]);
+ }
+ if (jsonChild == null)
+ return null;
+ }
+ return jsonChild;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/LanguageUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/LanguageUtils.java
new file mode 100644
index 000000000..515b044b3
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/LanguageUtils.java
@@ -0,0 +1,52 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+
+import java.util.Locale;
+
+/**
+ * Methods for dealing with i18n messages
+ */
+public class LanguageUtils {
+
+ public static Locale getCurrentDeviceLanguage(Context context) {
+ //better use getConfiguration as it has the latest locale configuration change.
+ //Otherwise Locale.getDefault().getLanguage() gets
+ //the config upon application launch.
+ Locale deviceLocale = context != null ? context.getResources().getConfiguration().locale : Locale.getDefault();
+ return deviceLocale;
+ }
+
+ public static String getCurrentDeviceLanguageCode(Context context) {
+ String deviceLanguageCode = getCurrentDeviceLanguage(context).toString();
+ return deviceLanguageCode;
+ }
+
+ public static String getPatchedCurrentDeviceLanguage(Context context) {
+ return patchDeviceLanguageCode(getCurrentDeviceLanguageCode(context));
+ }
+
+ /**
+ * Patches a deviceLanguageCode if any of deprecated values iw, id, or yi
+ */
+ public static String patchDeviceLanguageCode(String deviceLanguageCode){
+ String patchedCode = deviceLanguageCode;
+ /*
+ <p>Note that Java uses several deprecated two-letter codes. The Hebrew ("he") language
+ * code is rewritten as "iw", Indonesian ("id") as "in", and Yiddish ("yi") as "ji". This
+ * rewriting happens even if you construct your own {@code Locale} object, not just for
+ * instances returned by the various lookup methods.
+ */
+ if (deviceLanguageCode != null) {
+ if (deviceLanguageCode.startsWith("iw"))
+ patchedCode = deviceLanguageCode.replace("iw", "he");
+ else if (deviceLanguageCode.startsWith("in"))
+ patchedCode = deviceLanguageCode.replace("in", "id");
+ else if (deviceLanguageCode.startsWith("ji"))
+ patchedCode = deviceLanguageCode.replace("ji", "yi");
+ }
+
+ return patchedCode;
+ }
+
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MapUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MapUtils.java
new file mode 100644
index 000000000..c6e72dc5b
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MapUtils.java
@@ -0,0 +1,107 @@
+package org.wordpress.android.util;
+
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * wrappers for extracting values from a Map object
+ */
+public class MapUtils {
+ /*
+ * returns a String value for the passed key in the passed map
+ * always returns "" instead of null
+ */
+ public static String getMapStr(final Map<?, ?> map, final String key) {
+ if (map == null || key == null || !map.containsKey(key) || map.get(key) == null) {
+ return "";
+ }
+ return map.get(key).toString();
+ }
+
+ /*
+ * returns an int value for the passed key in the passed map
+ * defaultValue is returned if key doesn't exist or isn't a number
+ */
+ public static int getMapInt(final Map<?, ?> map, final String key) {
+ return getMapInt(map, key, 0);
+ }
+ public static int getMapInt(final Map<?, ?> map, final String key, int defaultValue) {
+ try {
+ return Integer.parseInt(getMapStr(map, key));
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ /*
+ * long version of above
+ */
+ public static long getMapLong(final Map<?, ?> map, final String key) {
+ return getMapLong(map, key, 0);
+ }
+ public static long getMapLong(final Map<?, ?> map, final String key, long defaultValue) {
+ try {
+ return Long.parseLong(getMapStr(map, key));
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ /*
+ * float version of above
+ */
+ public static float getMapFloat(final Map<?, ?> map, final String key) {
+ return getMapFloat(map, key, 0);
+ }
+ public static float getMapFloat(final Map<?, ?> map, final String key, float defaultValue) {
+ try {
+ return Float.parseFloat(getMapStr(map, key));
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ /*
+ * double version of above
+ */
+ public static double getMapDouble(final Map<?, ?> map, final String key) {
+ return getMapDouble(map, key, 0);
+ }
+ public static double getMapDouble(final Map<?, ?> map, final String key, double defaultValue) {
+ try {
+ return Double.parseDouble(getMapStr(map, key));
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ /*
+ * returns a date object from the passed key in the passed map
+ * returns null if key doesn't exist or isn't a date
+ */
+ public static Date getMapDate(final Map<?, ?> map, final String key) {
+ if (map == null || key == null || !map.containsKey(key))
+ return null;
+ try {
+ return (Date) map.get(key);
+ } catch (ClassCastException e) {
+ return null;
+ }
+ }
+
+ /*
+ * returns a boolean value from the passed key in the passed map
+ * returns true unless key doesn't exist, or the value is "0" or "false"
+ */
+ public static boolean getMapBool(final Map<?, ?> map, final String key) {
+ String value = getMapStr(map, key);
+ if (value.isEmpty())
+ return false;
+ if (value.startsWith("0")) // handles "0" and "0.0"
+ return false;
+ if (value.equalsIgnoreCase("false"))
+ return false;
+ // all other values are assume to be true
+ return true;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MediaUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MediaUtils.java
new file mode 100644
index 000000000..a96dadc74
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/MediaUtils.java
@@ -0,0 +1,334 @@
+package org.wordpress.android.util;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.webkit.MimeTypeMap;
+
+import org.wordpress.android.util.AppLog.T;
+
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public class MediaUtils {
+ private static final int DEFAULT_MAX_IMAGE_WIDTH = 1024;
+
+ public static boolean isValidImage(String url) {
+ if (url == null) {
+ return false;
+ }
+ url = url.toLowerCase();
+ return url.endsWith(".png") || url.endsWith(".jpg") || url.endsWith(".jpeg") || url.endsWith(".gif");
+ }
+
+ public static boolean isDocument(String url) {
+ if (url == null) {
+ return false;
+ }
+ url = url.toLowerCase();
+ return url.endsWith(".doc") || url.endsWith(".docx") || url.endsWith(".odt") || url.endsWith(".pdf");
+ }
+
+ public static boolean isPowerpoint(String url) {
+ if (url == null) {
+ return false;
+ }
+ url = url.toLowerCase();
+ return url.endsWith(".ppt") || url.endsWith(".pptx") || url.endsWith(".pps") || url.endsWith(".ppsx") ||
+ url.endsWith(".key");
+ }
+
+ public static boolean isSpreadsheet(String url) {
+ if (url == null) {
+ return false;
+ }
+ url = url.toLowerCase();
+ return url.endsWith(".xls") || url.endsWith(".xlsx");
+ }
+
+ public static boolean isVideo(String url) {
+ if (url == null) {
+ return false;
+ }
+ url = url.toLowerCase();
+ return url.endsWith(".ogv") || url.endsWith(".mp4") || url.endsWith(".m4v") || url.endsWith(".mov") ||
+ url.endsWith(".wmv") || url.endsWith(".avi") || url.endsWith(".mpg") || url.endsWith(".3gp") ||
+ url.endsWith(".3g2") || url.contains("video");
+ }
+
+ public static boolean isAudio(String url) {
+ if (url == null) {
+ return false;
+ }
+ url = url.toLowerCase();
+ return url.endsWith(".mp3") || url.endsWith(".ogg") || url.endsWith(".wav") || url.endsWith(".wma") ||
+ url.endsWith(".aiff") || url.endsWith(".aif") || url.endsWith(".aac") || url.endsWith(".m4a");
+ }
+
+ /**
+ * E.g. Jul 2, 2013 @ 21:57
+ */
+ public static String getDate(long ms) {
+ Date date = new Date(ms);
+ SimpleDateFormat sdf = new SimpleDateFormat("MMM d, yyyy '@' HH:mm", Locale.ENGLISH);
+
+ // The timezone on the website is at GMT
+ sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
+
+ return sdf.format(date);
+ }
+
+ public static boolean isLocalFile(String state) {
+ if (state == null) {
+ return false;
+ }
+
+ return (state.equals("queued") || state.equals("uploading") || state.equals("retry")
+ || state.equals("failed"));
+ }
+
+ public static Uri getLastRecordedVideoUri(Activity activity) {
+ String[] proj = { MediaStore.Video.Media._ID };
+ Uri contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+ String sortOrder = MediaStore.Video.VideoColumns.DATE_TAKEN + " DESC";
+ CursorLoader loader = new CursorLoader(activity, contentUri, proj, null, null, sortOrder);
+ Cursor cursor = loader.loadInBackground();
+ cursor.moveToFirst();
+
+ return Uri.parse(contentUri.toString() + "/" + cursor.getLong(0));
+ }
+
+ /**
+ * Get image width setting from the image width site setting string. This string can be an int, in this case it's
+ * the maximum image width defined by the site.
+ * Examples:
+ * "1000" will return 1000
+ * "Original Size" will return Integer.MAX_VALUE
+ * "Largeur originale" will return Integer.MAX_VALUE
+ * null will return Integer.MAX_VALUE
+ * @param imageWidthSiteSettingString Image width site setting string
+ * @return Integer.MAX_VALUE if image width is not defined or invalid, maximum image width in other cases.
+ */
+ public static int getImageWidthSettingFromString(String imageWidthSiteSettingString) {
+ if (imageWidthSiteSettingString == null) {
+ return Integer.MAX_VALUE;
+ }
+ try {
+ return Integer.valueOf(imageWidthSiteSettingString);
+ } catch (NumberFormatException e) {
+ return Integer.MAX_VALUE;
+ }
+ }
+
+ /**
+ * Calculate and return the maximum allowed image width by comparing the width of the image at its full size with
+ * the maximum upload width set in the blog settings
+ * @param imageWidth the image's natural (full) width
+ * @param imageWidthSiteSettingString the maximum upload width set in the site settings
+ * @return maximum allowed image width
+ */
+ public static int getMaximumImageWidth(int imageWidth, String imageWidthSiteSettingString) {
+ int imageWidthBlogSetting = getImageWidthSettingFromString(imageWidthSiteSettingString);
+ int imageWidthPictureSetting = imageWidth == 0 ? Integer.MAX_VALUE : imageWidth;
+
+ if (Math.min(imageWidthPictureSetting, imageWidthBlogSetting) == Integer.MAX_VALUE) {
+ // Default value in case of errors reading the picture size or the blog settings is set to Original size
+ return DEFAULT_MAX_IMAGE_WIDTH;
+ } else {
+ return Math.min(imageWidthPictureSetting, imageWidthBlogSetting);
+ }
+ }
+
+ public static int getMaximumImageWidth(Context context, Uri curStream, String imageWidthBlogSettingString) {
+ int[] dimensions = ImageUtils.getImageSize(curStream, context);
+ return getMaximumImageWidth(dimensions[0], imageWidthBlogSettingString);
+ }
+
+ public static boolean isInMediaStore(Uri mediaUri) {
+ // Check if the image is externally hosted (Picasa/Google Photos for example)
+ if (mediaUri != null && mediaUri.toString().startsWith("content://media/")) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public static Uri downloadExternalMedia(Context context, Uri imageUri) {
+ if (context == null || imageUri == null) {
+ return null;
+ }
+ File cacheDir = null;
+
+ String mimeType = context.getContentResolver().getType(imageUri);
+ boolean isVideo = (mimeType != null && mimeType.contains("video"));
+
+ // If the device has an SD card
+ if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED)) {
+ String mediaFolder = isVideo ? "video" : "images";
+ cacheDir = new File(android.os.Environment.getExternalStorageDirectory() + "/WordPress/" + mediaFolder);
+ } else {
+ if (context.getApplicationContext() != null) {
+ cacheDir = context.getApplicationContext().getCacheDir();
+ }
+ }
+
+ if (cacheDir != null && !cacheDir.exists()) {
+ cacheDir.mkdirs();
+ }
+ try {
+ InputStream input;
+ // Download the file
+ if (imageUri.toString().startsWith("content://")) {
+ input = context.getContentResolver().openInputStream(imageUri);
+ if (input == null) {
+ AppLog.e(T.UTILS, "openInputStream returned null");
+ return null;
+ }
+ } else {
+ input = new URL(imageUri.toString()).openStream();
+ }
+
+ String fileName = "wp-" + System.currentTimeMillis();
+ if (isVideo) {
+ fileName += "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+ }
+
+ File f = new File(cacheDir, fileName);
+
+ OutputStream output = new FileOutputStream(f);
+
+ byte data[] = new byte[1024];
+ int count;
+ while ((count = input.read(data)) != -1) {
+ output.write(data, 0, count);
+ }
+
+ output.flush();
+ output.close();
+ input.close();
+
+ return Uri.fromFile(f);
+ } catch (FileNotFoundException e) {
+ AppLog.e(T.UTILS, e);
+ } catch (MalformedURLException e) {
+ AppLog.e(T.UTILS, e);
+ } catch (IOException e) {
+ AppLog.e(T.UTILS, e);
+ }
+
+ return null;
+ }
+
+ public static String getMimeTypeOfInputStream(InputStream stream) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(stream, null, options);
+ return options.outMimeType;
+ }
+
+ public static String getMediaFileMimeType(File mediaFile) {
+ String originalFileName = mediaFile.getName().toLowerCase();
+ String mimeType = UrlUtils.getUrlMimeType(originalFileName);
+
+ if (TextUtils.isEmpty(mimeType)) {
+ try {
+ String filePathForGuessingMime;
+ if (mediaFile.getPath().contains("://")) {
+ filePathForGuessingMime = Uri.encode(mediaFile.getPath(), ":/");
+ } else {
+ filePathForGuessingMime = "file://"+ Uri.encode(mediaFile.getPath(), "/");
+ }
+ URL urlForGuessingMime = new URL(filePathForGuessingMime);
+ URLConnection uc = urlForGuessingMime.openConnection();
+ String guessedContentType = uc.getContentType(); //internally calls guessContentTypeFromName(url.getFile()); and guessContentTypeFromStream(is);
+ // check if returned "content/unknown"
+ if (!TextUtils.isEmpty(guessedContentType) && !guessedContentType.equals("content/unknown")) {
+ mimeType = guessedContentType;
+ }
+ } catch (MalformedURLException e) {
+ AppLog.e(AppLog.T.API, "MalformedURLException while trying to guess the content type for the file here " + mediaFile.getPath() + " with URLConnection", e);
+ }
+ catch (IOException e) {
+ AppLog.e(AppLog.T.API, "Error while trying to guess the content type for the file here " + mediaFile.getPath() +" with URLConnection", e);
+ }
+ }
+
+ // No mimeType yet? Try to decode the image and get the mimeType from there
+ if (TextUtils.isEmpty(mimeType)) {
+ try {
+ DataInputStream inputStream = new DataInputStream(new FileInputStream(mediaFile));
+ String mimeTypeFromStream = getMimeTypeOfInputStream(inputStream);
+ if (!TextUtils.isEmpty(mimeTypeFromStream)) {
+ mimeType = mimeTypeFromStream;
+ }
+ inputStream.close();
+ } catch (FileNotFoundException e) {
+ AppLog.e(AppLog.T.API, "FileNotFoundException while trying to guess the content type for the file " + mediaFile.getPath(), e);
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.API, "IOException while trying to guess the content type for the file " + mediaFile.getPath(), e);
+ }
+ }
+
+ if (TextUtils.isEmpty(mimeType)) {
+ mimeType = "";
+ } else {
+ if (mimeType.equalsIgnoreCase("video/mp4v-es")) { //Fixes #533. See: http://tools.ietf.org/html/rfc3016
+ mimeType = "video/mp4";
+ }
+ }
+
+ return mimeType;
+ }
+
+ public static String getMediaFileName(File mediaFile, String mimeType) {
+ String originalFileName = mediaFile.getName().toLowerCase();
+ String extension = MimeTypeMap.getFileExtensionFromUrl(originalFileName);
+ if (!TextUtils.isEmpty(extension)) //File name already has the extension in it
+ return originalFileName;
+
+ if (!TextUtils.isEmpty(mimeType)) { //try to get the extension from mimeType
+ String fileExtension = getExtensionForMimeType(mimeType);
+ if (!TextUtils.isEmpty(fileExtension)) {
+ originalFileName += "." + fileExtension;
+ }
+ } else {
+ //No mimetype and no extension!!
+ AppLog.e(AppLog.T.API, "No mimetype and no extension for " + mediaFile.getPath());
+ }
+
+ return originalFileName;
+ }
+
+ public static String getExtensionForMimeType(String mimeType) {
+ if (TextUtils.isEmpty(mimeType))
+ return "";
+
+ MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
+ String fileExtensionFromMimeType = mimeTypeMap.getExtensionFromMimeType(mimeType);
+ if (TextUtils.isEmpty(fileExtensionFromMimeType)) {
+ // We're still without an extension - split the mime type and retrieve it
+ String[] split = mimeType.split("/");
+ fileExtensionFromMimeType = split.length > 1 ? split[1] : split[0];
+ }
+
+ return fileExtensionFromMimeType.toLowerCase();
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/NetworkUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/NetworkUtils.java
new file mode 100644
index 000000000..240c93f50
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/NetworkUtils.java
@@ -0,0 +1,89 @@
+package org.wordpress.android.util;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.provider.Settings;
+
+/**
+ * requires android.permission.ACCESS_NETWORK_STATE
+ */
+
+public class NetworkUtils {
+ public static final int TYPE_UNKNOWN = -1;
+
+ /**
+ * returns information on the active network connection
+ */
+ private static NetworkInfo getActiveNetworkInfo(Context context) {
+ if (context == null) {
+ return null;
+ }
+ ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (cm == null) {
+ return null;
+ }
+ // note that this may return null if no network is currently active
+ return cm.getActiveNetworkInfo();
+ }
+
+ /**
+ * returns the ConnectivityManager.TYPE_xxx if there's an active connection, otherwise
+ * returns TYPE_UNKNOWN
+ */
+ private static int getActiveNetworkType(Context context) {
+ NetworkInfo info = getActiveNetworkInfo(context);
+ if (info == null || !info.isConnected()) {
+ return TYPE_UNKNOWN;
+ }
+ return info.getType();
+ }
+
+ /**
+ * returns true if a network connection is available
+ */
+ public static boolean isNetworkAvailable(Context context) {
+ NetworkInfo info = getActiveNetworkInfo(context);
+ return (info != null && info.isConnected());
+ }
+
+ /**
+ * returns true if the user is connected to WiFi
+ */
+ public static boolean isWiFiConnected(Context context) {
+ return (getActiveNetworkType(context) == ConnectivityManager.TYPE_WIFI);
+ }
+
+ /**
+ * returns true if airplane mode has been enabled
+ */
+ @TargetApi(VERSION_CODES.JELLY_BEAN_MR1)
+ @SuppressWarnings("deprecation")
+ public static boolean isAirplaneModeOn(Context context) {
+ // prior to JellyBean 4.2 this was Settings.System.AIRPLANE_MODE_ON, JellyBean 4.2
+ // moved it to Settings.Global
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ return Settings.System.getInt(context.getContentResolver(), Settings.System.AIRPLANE_MODE_ON, 0) != 0;
+ } else {
+ return Settings.Global.getInt(context.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0) != 0;
+ }
+ }
+
+ /**
+ * returns true if there's an active network connection, otherwise displays a toast error
+ * and returns false
+ */
+ public static boolean checkConnection(Context context) {
+ if (context == null) {
+ return false;
+ }
+ if (isNetworkAvailable(context)) {
+ return true;
+ }
+ ToastUtils.showToast(context, R.string.no_network_message);
+ return false;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PackageUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PackageUtils.java
new file mode 100644
index 000000000..52900a0bf
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PackageUtils.java
@@ -0,0 +1,45 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+
+public class PackageUtils {
+ /**
+ * Return true if Debug build. false otherwise.
+ */
+ public static boolean isDebugBuild() {
+ return BuildConfig.DEBUG;
+ }
+
+ public static PackageInfo getPackageInfo(Context context) {
+ try {
+ PackageManager manager = context.getPackageManager();
+ return manager.getPackageInfo(context.getPackageName(), 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Return version code, or 0 if it can't be read
+ */
+ public static int getVersionCode(Context context) {
+ PackageInfo packageInfo = getPackageInfo(context);
+ if (packageInfo != null) {
+ return packageInfo.versionCode;
+ }
+ return 0;
+ }
+
+ /**
+ * Return version name, or the string "0" if it can't be read
+ */
+ public static String getVersionName(Context context) {
+ PackageInfo packageInfo = getPackageInfo(context);
+ if (packageInfo != null) {
+ return packageInfo.versionName;
+ }
+ return "0";
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PermissionUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PermissionUtils.java
new file mode 100644
index 000000000..bf30103d2
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PermissionUtils.java
@@ -0,0 +1,97 @@
+package org.wordpress.android.util;
+
+import android.Manifest.permission;
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.support.v13.app.FragmentCompat;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PermissionUtils {
+ /**
+ * Check for permissions, request them if they're not granted.
+ *
+ * @return true if permissions are already granted, else request them and return false.
+ */
+ private static boolean checkAndRequestPermissions(Activity activity, int requestCode, String[] permissionList) {
+ List<String> toRequest = new ArrayList<>();
+ for (String permission : permissionList) {
+ if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
+ toRequest.add(permission);
+ }
+ }
+ if (toRequest.size() > 0) {
+ String[] requestedPermissions = toRequest.toArray(new String[toRequest.size()]);
+ ActivityCompat.requestPermissions(activity, requestedPermissions, requestCode);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Check for permissions, request them if they're not granted.
+ *
+ * @return true if permissions are already granted, else request them and return false.
+ */
+ private static boolean checkAndRequestPermissions(Fragment fragment, int requestCode, String[] permissionList) {
+ List<String> toRequest = new ArrayList<>();
+ for (String permission : permissionList) {
+ Context context = fragment.getActivity();
+ if (context != null && ContextCompat.checkSelfPermission(context, permission) != PackageManager
+ .PERMISSION_GRANTED) {
+ toRequest.add(permission);
+ }
+ }
+ if (toRequest.size() > 0) {
+ String[] requestedPermissions = toRequest.toArray(new String[toRequest.size()]);
+ FragmentCompat.requestPermissions(fragment, requestedPermissions, requestCode);
+ return false;
+ }
+ return true;
+ }
+
+ public static boolean checkAndRequestCameraAndStoragePermissions(Activity activity, int requestCode) {
+ return checkAndRequestPermissions(activity, requestCode, new String[]{
+ permission.WRITE_EXTERNAL_STORAGE,
+ permission.CAMERA
+ });
+ }
+
+ public static boolean checkAndRequestCameraAndStoragePermissions(Fragment fragment, int requestCode) {
+ return checkAndRequestPermissions(fragment, requestCode, new String[]{
+ permission.WRITE_EXTERNAL_STORAGE,
+ permission.CAMERA
+ });
+ }
+
+ public static boolean checkAndRequestStoragePermission(Activity activity, int requestCode) {
+ return checkAndRequestPermissions(activity, requestCode, new String[]{
+ permission.WRITE_EXTERNAL_STORAGE
+ });
+ }
+
+ public static boolean checkAndRequestStoragePermission(Fragment fragment, int requestCode) {
+ return checkAndRequestPermissions(fragment, requestCode, new String[]{
+ permission.WRITE_EXTERNAL_STORAGE
+ });
+ }
+
+ public static boolean checkLocationPermissions(Activity activity, int requestCode) {
+ return checkAndRequestPermissions(activity, requestCode, new String[]{
+ permission.ACCESS_FINE_LOCATION,
+ permission.ACCESS_COARSE_LOCATION
+ });
+ }
+
+ public static boolean checkLocationPermissions(Fragment fragment, int requestCode) {
+ return checkAndRequestPermissions(fragment, requestCode, new String[]{
+ permission.ACCESS_FINE_LOCATION,
+ permission.ACCESS_COARSE_LOCATION
+ });
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java
new file mode 100644
index 000000000..85a2adc93
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java
@@ -0,0 +1,104 @@
+package org.wordpress.android.util;
+
+import android.text.TextUtils;
+
+/**
+ * routines related to the Photon API
+ * http://developer.wordpress.com/docs/photon/
+ */
+public class PhotonUtils {
+
+ private PhotonUtils() {
+ throw new AssertionError();
+ }
+
+ /*
+ * returns true if the passed url is an obvious "mshots" url
+ */
+ public static boolean isMshotsUrl(final String imageUrl) {
+ return (imageUrl != null && imageUrl.contains("/mshots/"));
+ }
+
+ /*
+ * returns a photon url for the passed image with the resize query set to the passed
+ * dimensions - note that the passed quality parameter will only affect JPEGs
+ */
+ public enum Quality {
+ HIGH,
+ MEDIUM,
+ LOW
+ }
+ public static String getPhotonImageUrl(String imageUrl, int width, int height) {
+ return getPhotonImageUrl(imageUrl, width, height, Quality.MEDIUM);
+ }
+ public static String getPhotonImageUrl(String imageUrl, int width, int height, Quality quality) {
+ if (TextUtils.isEmpty(imageUrl)) {
+ return "";
+ }
+
+ // make sure it's valid
+ int schemePos = imageUrl.indexOf("://");
+ if (schemePos == -1) {
+ return imageUrl;
+ }
+
+ // we have encountered some image urls that incorrectly have a # fragment part, which
+ // must be removed before removing the query string
+ int fragmentPos = imageUrl.indexOf("#");
+ if (fragmentPos > 0) {
+ imageUrl = imageUrl.substring(0, fragmentPos);
+ }
+
+ // remove existing query string since it may contain params that conflict with the passed ones
+ imageUrl = UrlUtils.removeQuery(imageUrl);
+
+ // if this is an "mshots" url, skip photon and return it with a query that sets the width/height
+ if (isMshotsUrl(imageUrl)) {
+ return imageUrl + "?w=" + width + "&h=" + height;
+ }
+
+ // strip=all removes EXIF and other non-visual data from JPEGs
+ String query = "?strip=all";
+
+ switch (quality) {
+ case HIGH:
+ query += "&quality=100";
+ break;
+ case LOW:
+ query += "&quality=35";
+ break;
+ default: // medium
+ query += "&quality=65";
+ break;
+ }
+
+ // if both width & height are passed use the "resize" param, use only "w" or "h" if just
+ // one of them is set
+ if (width > 0 && height > 0) {
+ query += "&resize=" + width + "," + height;
+ } else if (width > 0) {
+ query += "&w=" + width;
+ } else if (height > 0) {
+ query += "&h=" + height;
+ }
+
+ // return passed url+query if it's already a photon url
+ if (imageUrl.contains(".wp.com")) {
+ if (imageUrl.contains("i0.wp.com") || imageUrl.contains("i1.wp.com") || imageUrl.contains("i2.wp.com"))
+ return imageUrl + query;
+ }
+
+ // use wordpress.com as the host if image is on wordpress.com since it supports the same
+ // query params and, more importantly, can handle images in private blogs
+ if (imageUrl.contains("wordpress.com")) {
+ return imageUrl + query;
+ }
+
+ // must use https for https image urls
+ if (UrlUtils.isHttps(imageUrl)) {
+ return "https://i0.wp.com/" + imageUrl.substring(schemePos+3, imageUrl.length()) + query;
+ } else {
+ return "http://i0.wp.com/" + imageUrl.substring(schemePos+3, imageUrl.length()) + query;
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ProfilingUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ProfilingUtils.java
new file mode 100644
index 000000000..4660a3500
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ProfilingUtils.java
@@ -0,0 +1,87 @@
+package org.wordpress.android.util;
+
+import android.os.SystemClock;
+
+import org.wordpress.android.util.AppLog.T;
+
+import java.util.ArrayList;
+
+/**
+ * forked from android.util.TimingLogger to use AppLog instead of Log + new static interface.
+ */
+public class ProfilingUtils {
+ private static ProfilingUtils sInstance;
+
+ private String mLabel;
+ private ArrayList<Long> mSplits;
+ private ArrayList<String> mSplitLabels;
+
+ public static void start(String label) {
+ getInstance().reset(label);
+ }
+
+ public static void split(String splitLabel) {
+ getInstance().addSplit(splitLabel);
+ }
+
+ public static void dump() {
+ getInstance().dumpToLog();
+ }
+
+ public static void stop() {
+ getInstance().reset(null);
+ }
+
+ private static ProfilingUtils getInstance() {
+ if (sInstance == null) {
+ sInstance = new ProfilingUtils();
+ }
+ return sInstance;
+ }
+
+ public ProfilingUtils() {
+ reset("init");
+ }
+
+ public void reset(String label) {
+ mLabel = label;
+ reset();
+ }
+
+ public void reset() {
+ if (mSplits == null) {
+ mSplits = new ArrayList<Long>();
+ mSplitLabels = new ArrayList<String>();
+ } else {
+ mSplits.clear();
+ mSplitLabels.clear();
+ }
+ addSplit(null);
+ }
+
+ public void addSplit(String splitLabel) {
+ if (mLabel == null) {
+ return;
+ }
+ long now = SystemClock.elapsedRealtime();
+ mSplits.add(now);
+ mSplitLabels.add(splitLabel);
+ }
+
+ public void dumpToLog() {
+ if (mLabel == null) {
+ return;
+ }
+ AppLog.d(T.PROFILING, mLabel + ": begin");
+ final long first = mSplits.get(0);
+ long now = first;
+ for (int i = 1; i < mSplits.size(); i++) {
+ now = mSplits.get(i);
+ final String splitLabel = mSplitLabels.get(i);
+ final long prev = mSplits.get(i - 1);
+ AppLog.d(T.PROFILING, mLabel + ": " + (now - prev) + " ms, " + splitLabel);
+ }
+ AppLog.d(T.PROFILING, mLabel + ": end, " + (now - first) + " ms");
+ }
+}
+
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ServiceUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ServiceUtils.java
new file mode 100644
index 000000000..6bcfde06b
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ServiceUtils.java
@@ -0,0 +1,16 @@
+package org.wordpress.android.util;
+
+import android.app.ActivityManager;
+import android.content.Context;
+
+public class ServiceUtils {
+ public static boolean isServiceRunning(Context context, Class<?> serviceClass) {
+ ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
+ if (serviceClass.getName().equals(service.service.getClassName())) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ShortcodeUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ShortcodeUtils.java
new file mode 100644
index 000000000..09480f156
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ShortcodeUtils.java
@@ -0,0 +1,31 @@
+package org.wordpress.android.util;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ShortcodeUtils {
+ public static String getVideoPressShortcodeFromId(String videoPressId) {
+ if (videoPressId == null || videoPressId.isEmpty()) {
+ return "";
+ }
+
+ return "[wpvideo " + videoPressId + "]";
+ }
+
+ public static String getVideoPressIdFromShortCode(String shortcode) {
+ String videoPressId = "";
+
+ if (shortcode != null) {
+ String videoPressShortcodeRegex = "^\\[wpvideo (.*)]$";
+
+ Pattern pattern = Pattern.compile(videoPressShortcodeRegex);
+ Matcher matcher = pattern.matcher(shortcode);
+
+ if (matcher.find()) {
+ videoPressId = matcher.group(1);
+ }
+ }
+
+ return videoPressId;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SqlUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SqlUtils.java
new file mode 100644
index 000000000..38b4b74a9
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SqlUtils.java
@@ -0,0 +1,142 @@
+package org.wordpress.android.util;
+
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDoneException;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteStatement;
+
+import org.wordpress.android.util.AppLog.T;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SqlUtils {
+ private SqlUtils() {
+ throw new AssertionError();
+ }
+
+ /*
+ * SQLite doesn't have a boolean datatype, so booleans are stored as 0=false, 1=true
+ */
+ public static long boolToSql(boolean value) {
+ return (value ? 1 : 0);
+ }
+ public static boolean sqlToBool(int value) {
+ return (value != 0);
+ }
+
+ public static void closeStatement(SQLiteStatement stmt) {
+ if (stmt != null) {
+ stmt.close();
+ }
+ }
+
+ public static void closeCursor(Cursor c) {
+ if (c != null && !c.isClosed()) {
+ c.close();
+ }
+ }
+
+ /*
+ * wrapper for DatabaseUtils.longForQuery() which returns 0 if query returns no rows
+ */
+ public static long longForQuery(SQLiteDatabase db, String query, String[] selectionArgs) {
+ try {
+ return DatabaseUtils.longForQuery(db, query, selectionArgs);
+ } catch (SQLiteDoneException e) {
+ return 0;
+ }
+ }
+
+ public static int intForQuery(SQLiteDatabase db, String query, String[] selectionArgs) {
+ long value = longForQuery(db, query, selectionArgs);
+ return (int)value;
+ }
+
+ public static boolean boolForQuery(SQLiteDatabase db, String query, String[] selectionArgs) {
+ long value = longForQuery(db, query, selectionArgs);
+ return sqlToBool((int) value);
+ }
+
+ /*
+ * wrapper for DatabaseUtils.stringForQuery(), returns "" if query returns no rows
+ */
+ public static String stringForQuery(SQLiteDatabase db, String query, String[] selectionArgs) {
+ try {
+ return DatabaseUtils.stringForQuery(db, query, selectionArgs);
+ } catch (SQLiteDoneException e) {
+ return "";
+ }
+ }
+
+ /*
+ * returns the number of rows in the passed table
+ */
+ public static long getRowCount(SQLiteDatabase db, String tableName) {
+ return DatabaseUtils.queryNumEntries(db, tableName);
+ }
+
+ /*
+ * removes all rows from the passed table
+ */
+ public static void deleteAllRowsInTable(SQLiteDatabase db, String tableName) {
+ db.delete(tableName, null, null);
+ }
+
+ /*
+ * drop all tables from the passed SQLiteDatabase - make sure to pass a
+ * writable database
+ */
+ public static boolean dropAllTables(SQLiteDatabase db) throws SQLiteException {
+ if (db == null) {
+ return false;
+ }
+
+ if (db.isReadOnly()) {
+ throw new SQLiteException("can't drop tables from a read-only database");
+ }
+
+ List<String> tableNames = new ArrayList<String>();
+ Cursor cursor = db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'", null);
+ if (cursor.moveToFirst()) {
+ do {
+ String tableName = cursor.getString(0);
+ if (!tableName.equals("android_metadata") && !tableName.equals("sqlite_sequence")) {
+ tableNames.add(tableName);
+ }
+ } while (cursor.moveToNext());
+ }
+
+ db.beginTransaction();
+ try {
+ for (String tableName: tableNames) {
+ db.execSQL("DROP TABLE IF EXISTS " + tableName);
+ }
+ db.setTransactionSuccessful();
+ return true;
+ } finally {
+ db.endTransaction();
+ closeCursor(cursor);
+ }
+ }
+
+ /*
+ * Android's CursorWindow has a max size of 2MB per row which can be exceeded
+ * with a very large text column, causing an IllegalStateException when the
+ * row is read - prevent this by limiting the amount of text that's stored in
+ * the text column.
+ * https://github.com/android/platform_frameworks_base/blob/b77bc869241644a662f7e615b0b00ecb5aee373d/core/res/res/values/config.xml#L1268
+ * https://github.com/android/platform_frameworks_base/blob/3bdbf644d61f46b531838558fabbd5b990fc4913/core/java/android/database/CursorWindow.java#L103
+ */
+ // Max 512K characters (a UTF-8 char is 4 bytes max, so a 512K characters string is always < 2Mb)
+ private static final int MAX_TEXT_LEN = 1024 * 1024 / 2;
+ public static String maxSQLiteText(final String text) {
+ if (text.length() <= MAX_TEXT_LEN) {
+ return text;
+ }
+ AppLog.w(T.UTILS, "sqlite > max text exceeded, storing truncated text");
+ return text.substring(0, MAX_TEXT_LEN);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java
new file mode 100644
index 000000000..b15e8d824
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java
@@ -0,0 +1,327 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.support.annotation.StringRes;
+import android.text.Html;
+import android.text.TextUtils;
+
+import org.wordpress.android.util.AppLog.T;
+
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+public class StringUtils {
+ public static String[] mergeStringArrays(String array1[], String array2[]) {
+ if (array1 == null || array1.length == 0) {
+ return array2;
+ }
+ if (array2 == null || array2.length == 0) {
+ return array1;
+ }
+ List<String> array1List = Arrays.asList(array1);
+ List<String> array2List = Arrays.asList(array2);
+ List<String> result = new ArrayList<String>(array1List);
+ List<String> tmp = new ArrayList<String>(array1List);
+ tmp.retainAll(array2List);
+ result.addAll(array2List);
+ return ((String[]) result.toArray(new String[result.size()]));
+ }
+
+ public static String convertHTMLTagsForUpload(String source) {
+ // bold
+ source = source.replace("<b>", "<strong>");
+ source = source.replace("</b>", "</strong>");
+
+ // italics
+ source = source.replace("<i>", "<em>");
+ source = source.replace("</i>", "</em>");
+
+ return source;
+ }
+
+ public static String convertHTMLTagsForDisplay(String source) {
+ // bold
+ source = source.replace("<strong>", "<b>");
+ source = source.replace("</strong>", "</b>");
+
+ // italics
+ source = source.replace("<em>", "<i>");
+ source = source.replace("</em>", "</i>");
+
+ return source;
+ }
+
+ public static String addPTags(String source) {
+ String[] asploded = source.split("\n\n");
+
+ if (asploded.length > 0) {
+ StringBuilder wrappedHTML = new StringBuilder();
+ for (int i = 0; i < asploded.length; i++) {
+ String trimmed = asploded[i].trim();
+ if (trimmed.length() > 0) {
+ trimmed = trimmed.replace("<br />", "<br>").replace("<br/>", "<br>").replace("<br>\n", "<br>")
+ .replace("\n", "<br>");
+ wrappedHTML.append("<p>");
+ wrappedHTML.append(trimmed);
+ wrappedHTML.append("</p>");
+ }
+ }
+ return wrappedHTML.toString();
+ } else {
+ return source;
+ }
+ }
+
+ public static BigInteger getMd5IntHash(String input) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ byte[] messageDigest = md.digest(input.getBytes());
+ BigInteger number = new BigInteger(1, messageDigest);
+ return number;
+ } catch (NoSuchAlgorithmException e) {
+ AppLog.e(T.UTILS, e);
+ return null;
+ }
+ }
+
+ public static String getMd5Hash(String input) {
+ BigInteger number = getMd5IntHash(input);
+ String md5 = number.toString(16);
+ while (md5.length() < 32) {
+ md5 = "0" + md5;
+ }
+ return md5;
+ }
+
+ public static String unescapeHTML(String html) {
+ if (html != null) {
+ return Html.fromHtml(html).toString();
+ } else {
+ return "";
+ }
+ }
+
+ /*
+ * nbradbury - adapted from Html.escapeHtml(), which was added in API Level 16
+ * TODO: not thoroughly tested yet, so marked as private - not sure I like the way
+ * this replaces two spaces with "&nbsp;"
+ */
+ private static String escapeHtml(final String text) {
+ if (text == null) {
+ return "";
+ }
+
+ StringBuilder out = new StringBuilder();
+ int length = text.length();
+
+ for (int i = 0; i < length; i++) {
+ char c = text.charAt(i);
+
+ if (c == '<') {
+ out.append("&lt;");
+ } else if (c == '>') {
+ out.append("&gt;");
+ } else if (c == '&') {
+ out.append("&amp;");
+ } else if (c > 0x7E || c < ' ') {
+ out.append("&#").append((int) c).append(";");
+ } else if (c == ' ') {
+ while (i + 1 < length && text.charAt(i + 1) == ' ') {
+ out.append("&nbsp;");
+ i++;
+ }
+
+ out.append(' ');
+ } else {
+ out.append(c);
+ }
+ }
+
+ return out.toString();
+ }
+
+ /*
+ * returns empty string if passed string is null, otherwise returns passed string
+ */
+ public static String notNullStr(String s) {
+ if (s == null) {
+ return "";
+ }
+ return s;
+ }
+
+ /**
+ * returns true if two strings are equal or two strings are null
+ */
+ public static boolean equals(String s1, String s2) {
+ if (s1 == null) {
+ return s2 == null;
+ }
+ return s1.equals(s2);
+ }
+
+ /*
+ * capitalizes the first letter in the passed string - based on Apache commons/lang3/StringUtils
+ * http://svn.apache.org/viewvc/commons/proper/lang/trunk/src/main/java/org/apache/commons/lang3/StringUtils.java?revision=1497829&view=markup
+ */
+ public static String capitalize(final String str) {
+ int strLen;
+ if (str == null || (strLen = str.length()) == 0) {
+ return str;
+ }
+
+ char firstChar = str.charAt(0);
+ if (Character.isTitleCase(firstChar)) {
+ return str;
+ }
+
+ return new StringBuilder(strLen).append(Character.toTitleCase(firstChar)).append(str.substring(1)).toString();
+ }
+
+ public static String removeTrailingSlash(final String str) {
+ if (TextUtils.isEmpty(str) || !str.endsWith("/")) {
+ return str;
+ }
+
+ return str.substring(0, str.length() - 1);
+ }
+
+ /*
+ * Wrap an image URL in a photon URL
+ * Check out http://developer.wordpress.com/docs/photon/
+ */
+ public static String getPhotonUrl(String imageUrl, int size) {
+ imageUrl = imageUrl.replace("http://", "").replace("https://", "");
+ return "http://i0.wp.com/" + imageUrl + "?w=" + size;
+ }
+
+ public static String replaceUnicodeSurrogateBlocksWithHTMLEntities(final String inputString) {
+ final int length = inputString.length();
+ StringBuilder out = new StringBuilder(); // Used to hold the output.
+ for (int offset = 0; offset < length; ) {
+ final int codepoint = inputString.codePointAt(offset);
+ final char current = inputString.charAt(offset);
+ if (Character.isHighSurrogate(current) || Character.isLowSurrogate(current)) {
+ if (EmoticonsUtils.wpSmiliesCodePointToText.get(codepoint) != null) {
+ out.append(EmoticonsUtils.wpSmiliesCodePointToText.get(codepoint));
+ } else {
+ final String htmlEscapedChar = "&#x" + Integer.toHexString(codepoint) + ";";
+ out.append(htmlEscapedChar);
+ }
+ } else {
+ out.append(current);
+ }
+ offset += Character.charCount(codepoint);
+ }
+ return out.toString();
+ }
+
+ /**
+ * Used to convert a language code ([lc]_[rc] where lc is language code (en, fr, es, etc...)
+ * and rc is region code (zh-CN, zh-HK, zh-TW, etc...) to a displayable string with the languages
+ * name.
+ *
+ * The input string must be between 2 and 6 characters, inclusive. An empty string is returned
+ * if that is not the case.
+ *
+ * If the input string is recognized by {@link Locale} the result of this method is the given
+ *
+ * @return non-null
+ */
+ public static String getLanguageString(String languagueCode, Locale displayLocale) {
+ if (languagueCode == null || languagueCode.length() < 2 || languagueCode.length() > 6) {
+ return "";
+ }
+
+ Locale languageLocale = new Locale(languagueCode.substring(0, 2));
+ return languageLocale.getDisplayLanguage(displayLocale) + languagueCode.substring(2);
+ }
+
+ /**
+ * This method ensures that the output String has only
+ * valid XML unicode characters as specified by the
+ * XML 1.0 standard. For reference, please see
+ * <a href="http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char">the
+ * standard</a>. This method will return an empty
+ * String if the input is null or empty.
+ *
+ * @param in The String whose non-valid characters we want to remove.
+ * @return The in String, stripped of non-valid characters.
+ */
+ public static final String stripNonValidXMLCharacters(String in) {
+ StringBuilder out = new StringBuilder(); // Used to hold the output.
+ char current; // Used to reference the current character.
+
+ if (in == null || ("".equals(in))) {
+ return ""; // vacancy test.
+ }
+ for (int i = 0; i < in.length(); i++) {
+ current = in.charAt(i); // NOTE: No IndexOutOfBoundsException caught here; it should not happen.
+ if ((current == 0x9) ||
+ (current == 0xA) ||
+ (current == 0xD) ||
+ ((current >= 0x20) && (current <= 0xD7FF)) ||
+ ((current >= 0xE000) && (current <= 0xFFFD)) ||
+ ((current >= 0x10000) && (current <= 0x10FFFF))) {
+ out.append(current);
+ }
+ }
+ return out.toString();
+ }
+
+ /*
+ * simple wrapper for Integer.valueOf(string) so caller doesn't need to catch NumberFormatException
+ */
+ public static int stringToInt(String s) {
+ return stringToInt(s, 0);
+ }
+
+ public static int stringToInt(String s, int defaultValue) {
+ if (s == null)
+ return defaultValue;
+ try {
+ return Integer.valueOf(s);
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ public static long stringToLong(String s) {
+ return stringToLong(s, 0L);
+ }
+
+ public static long stringToLong(String s, long defaultValue) {
+ if (s == null)
+ return defaultValue;
+ try {
+ return Long.valueOf(s);
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Formats the string for the given quantity, using the given arguments.
+ * We need this because our translation platform doesn't support Android plurals.
+ *
+ * @param zero The desired string identifier to get when quantity is exactly 0
+ * @param one The desired string identifier to get when quantity is exactly 1
+ * @param other The desired string identifier to get when quantity is not (0 or 1)
+ * @param quantity The number used to get the correct string
+ */
+ public static String getQuantityString(Context context, @StringRes int zero, @StringRes int one,
+ @StringRes int other, int quantity) {
+ if (quantity == 0) {
+ return context.getString(zero);
+ }
+ if (quantity == 1) {
+ return context.getString(one);
+ }
+ return String.format(context.getString(other), quantity);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactory.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactory.java
new file mode 100644
index 000000000..e3fee7fc3
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactory.java
@@ -0,0 +1,14 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+
+public class SystemServiceFactory {
+ private static SystemServiceFactoryAbstract sFactory;
+
+ public static Object get(Context context, String name) {
+ if (sFactory == null) {
+ sFactory = new SystemServiceFactoryDefault();
+ }
+ return sFactory.get(context, name);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryAbstract.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryAbstract.java
new file mode 100644
index 000000000..a9d522db4
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryAbstract.java
@@ -0,0 +1,7 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+
+public interface SystemServiceFactoryAbstract {
+ public Object get(Context context, String name);
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryDefault.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryDefault.java
new file mode 100644
index 000000000..eb488dde9
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryDefault.java
@@ -0,0 +1,9 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+
+public class SystemServiceFactoryDefault implements SystemServiceFactoryAbstract {
+ public Object get(Context context, String name) {
+ return context.getSystemService(name);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ToastUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ToastUtils.java
new file mode 100644
index 000000000..9b99c6ea5
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/ToastUtils.java
@@ -0,0 +1,37 @@
+package org.wordpress.android.util;
+
+import android.content.Context;
+import android.view.Gravity;
+import android.widget.Toast;
+
+/**
+ * Provides a simplified way to show toast messages without having to create the toast, set the
+ * desired gravity, etc.
+ */
+public class ToastUtils {
+ public enum Duration {SHORT, LONG}
+
+ private ToastUtils() {
+ throw new AssertionError();
+ }
+
+ public static Toast showToast(Context context, int stringResId) {
+ return showToast(context, stringResId, Duration.SHORT);
+ }
+
+ public static Toast showToast(Context context, int stringResId, Duration duration) {
+ return showToast(context, context.getString(stringResId), duration);
+ }
+
+ public static Toast showToast(Context context, String text) {
+ return showToast(context, text, Duration.SHORT);
+ }
+
+ public static Toast showToast(Context context, String text, Duration duration) {
+ Toast toast = Toast.makeText(context, text,
+ (duration == Duration.SHORT ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG));
+ toast.setGravity(Gravity.CENTER, 0, 0);
+ toast.show();
+ return toast;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UrlUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UrlUtils.java
new file mode 100644
index 000000000..cad48ef8e
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UrlUtils.java
@@ -0,0 +1,257 @@
+package org.wordpress.android.util;
+
+import android.net.Uri;
+import android.text.TextUtils;
+import android.webkit.MimeTypeMap;
+import android.webkit.URLUtil;
+
+import org.wordpress.android.util.AppLog.T;
+
+import java.io.UnsupportedEncodingException;
+import java.net.IDN;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+
+public class UrlUtils {
+ public static String urlEncode(final String text) {
+ try {
+ return URLEncoder.encode(text, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ return text;
+ }
+ }
+
+ public static String urlDecode(final String text) {
+ try {
+ return URLDecoder.decode(text, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ return text;
+ }
+ }
+
+ /**
+ * @param urlString url to get host from
+ * @return host of uri if available. Empty string otherwise.
+ */
+ public static String getHost(final String urlString) {
+ if (urlString != null) {
+ Uri uri = Uri.parse(urlString);
+ if (uri.getHost() != null) {
+ return uri.getHost();
+ }
+ }
+ return "";
+ }
+
+ /**
+ * Convert IDN names to punycode if necessary
+ */
+ public static String convertUrlToPunycodeIfNeeded(String url) {
+ if (!Charset.forName("US-ASCII").newEncoder().canEncode(url)) {
+ if (url.toLowerCase().startsWith("http://")) {
+ url = "http://" + IDN.toASCII(url.substring(7));
+ } else if (url.toLowerCase().startsWith("https://")) {
+ url = "https://" + IDN.toASCII(url.substring(8));
+ } else {
+ url = IDN.toASCII(url);
+ }
+ }
+ return url;
+ }
+
+ /**
+ * Remove leading double slash, and inherit protocol scheme
+ */
+ public static String removeLeadingDoubleSlash(String url, String scheme) {
+ if (url != null && url.startsWith("//")) {
+ url = url.substring(2);
+ if (scheme != null) {
+ if (scheme.endsWith("://")){
+ url = scheme + url;
+ } else {
+ AppLog.e(T.UTILS, "Invalid scheme used: " + scheme);
+ }
+ }
+ }
+ return url;
+ }
+
+ /**
+ * Add scheme prefix to an URL. This method must be called on all user entered or server fetched URLs to ensure
+ * http client will work as expected.
+ *
+ * @param url url entered by the user or fetched from a server
+ * @param addHttps true and the url is not starting with http://, it will make the url starts with https://
+ * @return url prefixed by http:// or https://
+ */
+ public static String addUrlSchemeIfNeeded(String url, boolean addHttps) {
+ if (url == null) {
+ return null;
+ }
+
+ // Remove leading double slash (eg. //example.com), needed for some wporg instances configured to
+ // switch between http or https
+ url = removeLeadingDoubleSlash(url, (addHttps ? "https" : "http") + "://");
+
+ // If the URL is a valid http or https URL, we're good to go
+ if (URLUtil.isHttpUrl(url) || URLUtil.isHttpsUrl(url)) {
+ return url;
+ }
+
+ // Else, remove the old scheme and prefix it by https:// or http://
+ return (addHttps ? "https" : "http") + "://" + removeScheme(url);
+ }
+
+ /**
+ * normalizes a URL, primarily for comparison purposes, for example so that
+ * normalizeUrl("http://google.com/") = normalizeUrl("http://google.com")
+ */
+ public static String normalizeUrl(final String urlString) {
+ if (urlString == null) {
+ return null;
+ }
+
+ // this routine is called from some performance-critical code and creating a URI from a string
+ // is slow, so skip it when possible - if we know it's not a relative path (and 99.9% of the
+ // time it won't be for our purposes) then we can normalize it without java.net.URI.normalize()
+ if (urlString.startsWith("http") &&
+ !urlString.contains("build/intermediates/exploded-aar/org.wordpress/graphview/3.1.1")) {
+ // return without a trailing slash
+ if (urlString.endsWith("/")) {
+ return urlString.substring(0, urlString.length() - 1);
+ }
+ return urlString;
+ }
+
+ // url is relative, so fall back to using slower java.net.URI normalization
+ try {
+ URI uri = URI.create(urlString);
+ return uri.normalize().toString();
+ } catch (IllegalArgumentException e) {
+ return urlString;
+ }
+ }
+
+
+ /**
+ * returns the passed url without the scheme
+ */
+ public static String removeScheme(final String urlString) {
+ if (urlString == null) {
+ return null;
+ }
+
+ int doubleslash = urlString.indexOf("//");
+ if (doubleslash == -1) {
+ doubleslash = 0;
+ } else {
+ doubleslash += 2;
+ }
+
+ return urlString.substring(doubleslash, urlString.length());
+ }
+
+ /**
+ * returns the passed url without the query parameters
+ */
+ public static String removeQuery(final String urlString) {
+ if (urlString == null) {
+ return null;
+ }
+ return Uri.parse(urlString).buildUpon().clearQuery().toString();
+ }
+
+ /**
+ * returns true if passed url is https:
+ */
+ public static boolean isHttps(final String urlString) {
+ return (urlString != null && urlString.startsWith("https:"));
+ }
+
+ public static boolean isHttps(URL url) {
+ return url != null && "https".equals(url.getProtocol());
+ }
+
+ public static boolean isHttps(URI uri) {
+ if (uri == null) return false;
+
+ String protocol = uri.getScheme();
+ return protocol != null && protocol.equals("https");
+ }
+
+ /**
+ * returns https: version of passed http: url
+ */
+ public static String makeHttps(final String urlString) {
+ if (urlString == null || !urlString.startsWith("http:")) {
+ return urlString;
+ }
+ return "https:" + urlString.substring(5, urlString.length());
+ }
+
+ /**
+ * see http://stackoverflow.com/a/8591230/1673548
+ */
+ public static String getUrlMimeType(final String urlString) {
+ if (urlString == null) {
+ return null;
+ }
+
+ String extension = MimeTypeMap.getFileExtensionFromUrl(urlString);
+ if (extension == null) {
+ return null;
+ }
+
+ MimeTypeMap mime = MimeTypeMap.getSingleton();
+ String mimeType = mime.getMimeTypeFromExtension(extension);
+ if (mimeType == null) {
+ return null;
+ }
+
+ return mimeType;
+ }
+
+ /**
+ * returns false if the url is not valid or if the url host is null, else true
+ */
+ public static boolean isValidUrlAndHostNotNull(String url) {
+ try {
+ URI uri = URI.create(url);
+ if (uri.getHost() == null) {
+ return false;
+ }
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ return true;
+ }
+
+ // returns true if the passed url is for an image
+ public static boolean isImageUrl(String url) {
+ if (TextUtils.isEmpty(url)) return false;
+
+ String cleanedUrl = removeQuery(url.toLowerCase());
+
+ return cleanedUrl.endsWith("jpg") || cleanedUrl.endsWith("jpeg") ||
+ cleanedUrl.endsWith("gif") || cleanedUrl.endsWith("png");
+ }
+
+ public static String appendUrlParameter(String url, String paramName, String paramValue) {
+ Map<String, String> parameters = new HashMap<>();
+ parameters.put(paramName, paramValue);
+ return appendUrlParameters(url, parameters);
+ }
+
+ public static String appendUrlParameters(String url, Map<String, String> parameters) {
+ Uri.Builder uriBuilder = Uri.parse(url).buildUpon();
+ for (Map.Entry<String, String> parameter : parameters.entrySet()) {
+ uriBuilder.appendQueryParameter(parameter.getKey(), parameter.getValue());
+ }
+ return uriBuilder.build().toString();
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UserEmailUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UserEmailUtils.java
new file mode 100644
index 000000000..385960558
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/UserEmailUtils.java
@@ -0,0 +1,38 @@
+package org.wordpress.android.util;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.Context;
+import android.util.Patterns;
+
+import org.wordpress.android.util.AppLog.T;
+
+import java.util.regex.Pattern;
+
+public class UserEmailUtils {
+ /**
+ * Get primary account and return its name if it matches the email address pattern.
+ *
+ * @return primary account email address if it can be found or empty string else.
+ */
+ public static String getPrimaryEmail(Context context) {
+ try {
+ AccountManager accountManager = AccountManager.get(context);
+ if (accountManager == null)
+ return "";
+ Account[] accounts = accountManager.getAccounts();
+ Pattern emailPattern = Patterns.EMAIL_ADDRESS;
+ for (Account account : accounts) {
+ // make sure account.name is an email address before adding to the list
+ if (emailPattern.matcher(account.name).matches()) {
+ return account.name;
+ }
+ }
+ return "";
+ } catch (SecurityException e) {
+ // exception will occur if app doesn't have GET_ACCOUNTS permission
+ AppLog.e(T.UTILS, "SecurityException - missing GET_ACCOUNTS permission");
+ return "";
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/ListScrollPositionManager.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/ListScrollPositionManager.java
new file mode 100644
index 000000000..914373c8f
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/ListScrollPositionManager.java
@@ -0,0 +1,58 @@
+package org.wordpress.android.util.helpers;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.preference.PreferenceManager;
+import android.view.View;
+import android.widget.ListView;
+
+public class ListScrollPositionManager {
+ private int mSelectedPosition;
+ private int mListViewScrollStateIndex;
+ private int mListViewScrollStateOffset;
+ private ListView mListView;
+ private boolean mSetSelection;
+
+ public ListScrollPositionManager(ListView listView, boolean setSelection) {
+ mListView = listView;
+ mSetSelection = setSelection;
+ }
+
+ public void saveScrollOffset() {
+ mListViewScrollStateIndex = mListView.getFirstVisiblePosition();
+ View view = mListView.getChildAt(0);
+ mListViewScrollStateOffset = 0;
+ if (view != null) {
+ mListViewScrollStateOffset = view.getTop();
+ }
+ if (mSetSelection) {
+ mSelectedPosition = mListView.getCheckedItemPosition();
+ }
+ }
+
+ public void restoreScrollOffset() {
+ mListView.setSelectionFromTop(mListViewScrollStateIndex, mListViewScrollStateOffset);
+ if (mSetSelection) {
+ mListView.setItemChecked(mSelectedPosition, true);
+ }
+ }
+
+ public void saveToPreferences(Context context, String uniqueId) {
+ saveScrollOffset();
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
+ Editor editor = settings.edit();
+ editor.putInt("scroll-position-manager-index-" + uniqueId, mListViewScrollStateIndex);
+ editor.putInt("scroll-position-manager-offset-" + uniqueId, mListViewScrollStateOffset);
+ editor.putInt("scroll-position-manager-selected-position-" + uniqueId, mSelectedPosition);
+ editor.apply();
+ }
+
+ public void restoreFromPreferences(Context context, String uniqueId) {
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
+ mListViewScrollStateIndex = settings.getInt("scroll-position-manager-index-" + uniqueId, 0);
+ mListViewScrollStateOffset = settings.getInt("scroll-position-manager-offset-" + uniqueId, 0);
+ mSelectedPosition = settings.getInt("scroll-position-manager-selected-position-" + uniqueId, 0);
+ restoreScrollOffset();
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/LocationHelper.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/LocationHelper.java
new file mode 100644
index 000000000..ff472c2ed
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/LocationHelper.java
@@ -0,0 +1,144 @@
+//This Handy-Dandy class acquired and tweaked from http://stackoverflow.com/a/3145655/309558
+package org.wordpress.android.util.helpers;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.os.Bundle;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class LocationHelper {
+ Timer mTimer;
+ LocationManager mLocationManager;
+ LocationResult mLocationResult;
+ boolean mGpsEnabled = false;
+ boolean mNetworkEnabled = false;
+
+ @SuppressLint("MissingPermission")
+ public boolean getLocation(Activity activity, LocationResult result) {
+ mLocationResult = result;
+ if (mLocationManager == null) {
+ mLocationManager = (LocationManager) activity.getSystemService(Context.LOCATION_SERVICE);
+ }
+
+ // exceptions will be thrown if provider is not permitted.
+ try {
+ mGpsEnabled = mLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
+ } catch (Exception ex) {
+ }
+ try {
+ mNetworkEnabled = mLocationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
+ } catch (Exception ex) {
+ }
+
+ // don't start listeners if no provider is enabled
+ if (!mGpsEnabled && !mNetworkEnabled) {
+ return false;
+ }
+
+ if (mGpsEnabled) {
+ mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, locationListenerGps);
+ }
+
+ if (mNetworkEnabled) {
+ mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, locationListenerNetwork);
+ }
+
+ mTimer = new Timer();
+ mTimer.schedule(new GetLastLocation(), 30000);
+ return true;
+ }
+
+ LocationListener locationListenerGps = new LocationListener() {
+ @SuppressLint("MissingPermission")
+ public void onLocationChanged(Location location) {
+ mTimer.cancel();
+ mLocationResult.gotLocation(location);
+ mLocationManager.removeUpdates(this);
+ mLocationManager.removeUpdates(locationListenerNetwork);
+ }
+
+ public void onProviderDisabled(String provider) {
+ }
+
+ public void onProviderEnabled(String provider) {
+ }
+
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+ }
+ };
+
+ LocationListener locationListenerNetwork = new LocationListener() {
+ @SuppressLint("MissingPermission")
+ public void onLocationChanged(Location location) {
+ mTimer.cancel();
+ mLocationResult.gotLocation(location);
+ mLocationManager.removeUpdates(this);
+ mLocationManager.removeUpdates(locationListenerGps);
+ }
+
+ public void onProviderDisabled(String provider) {
+ }
+
+ public void onProviderEnabled(String provider) {
+ }
+
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+ }
+ };
+
+ class GetLastLocation extends TimerTask {
+ @Override
+ @SuppressLint("MissingPermission")
+ public void run() {
+ mLocationManager.removeUpdates(locationListenerGps);
+ mLocationManager.removeUpdates(locationListenerNetwork);
+
+ Location net_loc = null, gps_loc = null;
+ if (mGpsEnabled) {
+ gps_loc = mLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
+ }
+ if (mNetworkEnabled) {
+ net_loc = mLocationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
+ }
+
+ // if there are both values use the latest one
+ if (gps_loc != null && net_loc != null) {
+ if (gps_loc.getTime() > net_loc.getTime()) {
+ mLocationResult.gotLocation(gps_loc);
+ } else {
+ mLocationResult.gotLocation(net_loc);
+ }
+ return;
+ }
+
+ if (gps_loc != null) {
+ mLocationResult.gotLocation(gps_loc);
+ return;
+ }
+ if (net_loc != null) {
+ mLocationResult.gotLocation(net_loc);
+ return;
+ }
+ mLocationResult.gotLocation(null);
+ }
+ }
+
+ public static abstract class LocationResult {
+ public abstract void gotLocation(Location location);
+ }
+
+ @SuppressLint("MissingPermission")
+ public void cancelTimer() {
+ if (mTimer != null) {
+ mTimer.cancel();
+ mLocationManager.removeUpdates(locationListenerGps);
+ mLocationManager.removeUpdates(locationListenerNetwork);
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaFile.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaFile.java
new file mode 100644
index 000000000..b57ad0165
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaFile.java
@@ -0,0 +1,339 @@
+package org.wordpress.android.util.helpers;
+
+import android.text.TextUtils;
+import android.webkit.MimeTypeMap;
+
+import org.wordpress.android.util.MapUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.util.Date;
+import java.util.Map;
+
+public class MediaFile {
+ protected int id;
+ protected long postID;
+ protected String filePath = null; //path of the file into disk
+ protected String fileName = null; //name of the file into the server
+ protected String title = null;
+ protected String description = null;
+ protected String caption = null;
+ protected int horizontalAlignment; //0 = none, 1 = left, 2 = center, 3 = right
+ protected boolean verticalAligment = false; //false = bottom, true = top
+ protected int width = 500, height;
+ protected String mimeType = "";
+ protected String videoPressShortCode = null;
+ protected boolean featured = false;
+ protected boolean isVideo = false;
+ protected boolean featuredInPost;
+ protected String fileURL = null; // url of the file to download
+ protected String thumbnailURL = null; // url of the thumbnail to download
+ private String blogId;
+ private long dateCreatedGmt;
+ private String uploadState = null;
+ private String mediaId;
+
+ public static String VIDEOPRESS_SHORTCODE_ID = "videopress_shortcode";
+
+ public MediaFile(String blogId, Map<?, ?> resultMap, boolean isDotCom) {
+ setBlogId(blogId);
+ setMediaId(MapUtils.getMapStr(resultMap, "attachment_id"));
+ setPostID(MapUtils.getMapLong(resultMap, "parent"));
+ setTitle(MapUtils.getMapStr(resultMap, "title"));
+ setCaption(MapUtils.getMapStr(resultMap, "caption"));
+ setDescription(MapUtils.getMapStr(resultMap, "description"));
+ setVideoPressShortCode(MapUtils.getMapStr(resultMap, VIDEOPRESS_SHORTCODE_ID));
+
+ // get the file name from the link
+ String link = MapUtils.getMapStr(resultMap, "link");
+ setFileName(new String(link).replaceAll("^.*/([A-Za-z0-9_-]+)\\.\\w+$", "$1"));
+
+ String fileType = new String(link).replaceAll(".*\\.(\\w+)$", "$1").toLowerCase();
+ String fileMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileType);
+ setMimeType(fileMimeType);
+
+ // make the file urls be https://... so that we can get these images with oauth when the blogs are private
+ // assume no https for images in self-hosted blogs
+ String fileUrl = MapUtils.getMapStr(resultMap, "link");
+ if (isDotCom) {
+ fileUrl = fileUrl.replace("http:", "https:");
+ }
+ setFileURL(fileUrl);
+
+ String thumbnailURL = MapUtils.getMapStr(resultMap, "thumbnail");
+ if (thumbnailURL.startsWith("http")) {
+ if (isDotCom) {
+ thumbnailURL = thumbnailURL.replace("http:", "https:");
+ }
+ setThumbnailURL(thumbnailURL);
+ }
+
+ Date date = MapUtils.getMapDate(resultMap, "date_created_gmt");
+ if (date != null) {
+ setDateCreatedGMT(date.getTime());
+ }
+
+ Object meta = resultMap.get("metadata");
+ if (meta != null && meta instanceof Map) {
+ Map<?, ?> metadata = (Map<?, ?>) meta;
+ setWidth(MapUtils.getMapInt(metadata, "width"));
+ setHeight(MapUtils.getMapInt(metadata, "height"));
+ }
+ }
+
+ public MediaFile() {
+ // default constructor
+ }
+
+ public MediaFile(MediaFile mediaFile) {
+ this.id = mediaFile.id;
+ this.postID = mediaFile.postID;
+ this.filePath = mediaFile.filePath;
+ this.fileName = mediaFile.fileName;
+ this.title = mediaFile.title;
+ this.description = mediaFile.description;
+ this.caption = mediaFile.caption;
+ this.horizontalAlignment = mediaFile.horizontalAlignment;
+ this.verticalAligment = mediaFile.verticalAligment;
+ this.width = mediaFile.width;
+ this.height = mediaFile.height;
+ this.mimeType = mediaFile.mimeType;
+ this.videoPressShortCode = mediaFile.videoPressShortCode;
+ this.featured = mediaFile.featured;
+ this.isVideo = mediaFile.isVideo;
+ this.featuredInPost = mediaFile.featuredInPost;
+ this.fileURL = mediaFile.fileURL;
+ this.thumbnailURL = mediaFile.thumbnailURL;
+ this.blogId = mediaFile.blogId;
+ this.dateCreatedGmt = mediaFile.dateCreatedGmt;
+ this.uploadState = mediaFile.uploadState;
+ this.mediaId = mediaFile.mediaId;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getMediaId() {
+ return mediaId;
+ }
+
+ public void setMediaId(String id) {
+ mediaId = id;
+ }
+
+ public boolean isFeatured() {
+ return featured;
+ }
+
+ public void setFeatured(boolean featured) {
+ this.featured = featured;
+ }
+
+ public long getPostID() {
+ return postID;
+ }
+
+ public void setPostID(long postID) {
+ this.postID = postID;
+ }
+
+ public String getFilePath() {
+ return filePath;
+ }
+
+ public void setFilePath(String filePath) {
+ this.filePath = filePath;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getCaption() {
+ return caption;
+ }
+
+ public void setCaption(String caption) {
+ this.caption = caption;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getFileURL() {
+ return fileURL;
+ }
+
+ public void setFileURL(String fileURL) {
+ this.fileURL = fileURL;
+ }
+
+ public String getThumbnailURL() {
+ return thumbnailURL;
+ }
+
+ public void setThumbnailURL(String thumbnailURL) {
+ this.thumbnailURL = thumbnailURL;
+ }
+
+ public boolean isVerticalAlignmentOnTop() {
+ return verticalAligment;
+ }
+
+ public void setVerticalAlignmentOnTop(boolean verticalAligment) {
+ this.verticalAligment = verticalAligment;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public void setWidth(int width) {
+ this.width = width;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public void setHeight(int height) {
+ this.height = height;
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public void setFileName(String fileName) {
+ this.fileName = fileName;
+ }
+
+ public String getMimeType() {
+ return StringUtils.notNullStr(mimeType);
+ }
+
+ public void setMimeType(String type) {
+ mimeType = StringUtils.notNullStr(type);
+ }
+
+ public String getVideoPressShortCode() {
+ return videoPressShortCode;
+ }
+
+ public void setVideoPressShortCode(String videoPressShortCode) {
+ this.videoPressShortCode = videoPressShortCode;
+ }
+
+ public int getHorizontalAlignment() {
+ return horizontalAlignment;
+ }
+
+ public void setHorizontalAlignment(int horizontalAlignment) {
+ this.horizontalAlignment = horizontalAlignment;
+ }
+
+ public boolean isVideo() {
+ return isVideo;
+ }
+
+ public void setVideo(boolean isVideo) {
+ this.isVideo = isVideo;
+ }
+
+ public boolean isFeaturedInPost() {
+ return featuredInPost;
+ }
+
+ public void setFeaturedInPost(boolean featuredInPost) {
+ this.featuredInPost = featuredInPost;
+ }
+
+ public String getBlogId() {
+ return blogId;
+ }
+
+ public void setBlogId(String blogId) {
+ this.blogId = blogId;
+ }
+
+ public void setDateCreatedGMT(long date_created_gmt) {
+ this.dateCreatedGmt = date_created_gmt;
+ }
+
+ public long getDateCreatedGMT() {
+ return dateCreatedGmt;
+ }
+
+ public void setUploadState(String uploadState) {
+ this.uploadState = uploadState;
+ }
+
+ public String getUploadState() {
+ return uploadState;
+ }
+
+ /**
+ * Outputs the Html for an image
+ * If a fullSizeUrl exists, a link will be created to it from the resizedPictureUrl
+ */
+ public String getImageHtmlForUrls(String fullSizeUrl, String resizedPictureURL, boolean shouldAddImageWidthCSS) {
+ String alignment = "";
+ switch (getHorizontalAlignment()) {
+ case 0:
+ alignment = "alignnone";
+ break;
+ case 1:
+ alignment = "alignleft";
+ break;
+ case 2:
+ alignment = "aligncenter";
+ break;
+ case 3:
+ alignment = "alignright";
+ break;
+ }
+
+ String alignmentCSS = "class=\"" + alignment + " size-full\" ";
+
+ if (shouldAddImageWidthCSS) {
+ alignmentCSS += "style=\"max-width: " + getWidth() + "px\" ";
+ }
+
+ // Check if we uploaded a featured picture that is not added to the Post content (normal case)
+ if ((fullSizeUrl != null && fullSizeUrl.equalsIgnoreCase("")) ||
+ (resizedPictureURL != null && resizedPictureURL.equalsIgnoreCase(""))) {
+ return ""; // Not featured in Post. Do not add to the content.
+ }
+
+ if (fullSizeUrl == null && resizedPictureURL != null) {
+ fullSizeUrl = resizedPictureURL;
+ } else if (fullSizeUrl != null && resizedPictureURL == null) {
+ resizedPictureURL = fullSizeUrl;
+ }
+
+ String mediaTitle = StringUtils.notNullStr(getTitle());
+
+ String content = String.format("<a href=\"%s\"><img title=\"%s\" %s alt=\"image\" src=\"%s\" /></a>",
+ fullSizeUrl, mediaTitle, alignmentCSS, resizedPictureURL);
+
+ if (!TextUtils.isEmpty(getCaption())) {
+ content = String.format("[caption id=\"\" align=\"%s\" width=\"%d\"]%s%s[/caption]",
+ alignment, getWidth(), content, TextUtils.htmlEncode(getCaption()));
+ }
+
+ return content;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGallery.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGallery.java
new file mode 100644
index 000000000..ab7326a17
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGallery.java
@@ -0,0 +1,87 @@
+
+package org.wordpress.android.util.helpers;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+
+/**
+ * A model representing a Media Gallery.
+ * A unique id is not used on the website, but only in this app.
+ * It is used to uniquely determining the instance of the object, as it is
+ * passed between post and media gallery editor.
+ */
+public class MediaGallery implements Serializable {
+ private static final long serialVersionUID = 2359176987182027508L;
+
+ private long uniqueId;
+ private boolean isRandom;
+ private String type;
+ private int numColumns;
+ private ArrayList<String> ids;
+
+ public MediaGallery(boolean isRandom, String type, int numColumns, ArrayList<String> ids) {
+ this.isRandom = isRandom;
+ this.type = type;
+ this.numColumns = numColumns;
+ this.ids = ids;
+ this.uniqueId = System.currentTimeMillis();
+ }
+
+ public MediaGallery() {
+ isRandom = false;
+ type = "";
+ numColumns = 3;
+ ids = new ArrayList<String>();
+ this.uniqueId = System.currentTimeMillis();
+ }
+
+ public boolean isRandom() {
+ return isRandom;
+ }
+
+ public void setRandom(boolean isRandom) {
+ this.isRandom = isRandom;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public int getNumColumns() {
+ return numColumns;
+ }
+
+ public void setNumColumns(int numColumns) {
+ this.numColumns = numColumns;
+ }
+
+ public ArrayList<String> getIds() {
+ return ids;
+ }
+
+ public String getIdsStr() {
+ String ids_str = "";
+ if (ids.size() > 0) {
+ for (String id : ids) {
+ ids_str += id + ",";
+ }
+ ids_str = ids_str.substring(0, ids_str.length() - 1);
+ }
+ return ids_str;
+ }
+
+ public void setIds(ArrayList<String> ids) {
+ this.ids = ids;
+ }
+
+ /**
+ * An id to uniquely identify a media gallery object, so that the same object can be edited in the post editor
+ */
+ public long getUniqueId() {
+ return uniqueId;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGalleryImageSpan.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGalleryImageSpan.java
new file mode 100644
index 000000000..588b98141
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGalleryImageSpan.java
@@ -0,0 +1,21 @@
+package org.wordpress.android.util.helpers;
+
+import android.content.Context;
+import android.text.style.ImageSpan;
+
+public class MediaGalleryImageSpan extends ImageSpan {
+ private MediaGallery mMediaGallery;
+
+ public MediaGalleryImageSpan(Context context, MediaGallery mediaGallery, int placeHolder) {
+ super(context, placeHolder);
+ setMediaGallery(mediaGallery);
+ }
+
+ public MediaGallery getMediaGallery() {
+ return mMediaGallery;
+ }
+
+ public void setMediaGallery(MediaGallery mediaGallery) {
+ this.mMediaGallery = mediaGallery;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/SwipeToRefreshHelper.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/SwipeToRefreshHelper.java
new file mode 100644
index 000000000..e6f4bf323
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/SwipeToRefreshHelper.java
@@ -0,0 +1,72 @@
+package org.wordpress.android.util.helpers;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener;
+import android.util.TypedValue;
+
+import org.wordpress.android.util.R;
+import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout;
+
+public class SwipeToRefreshHelper implements OnRefreshListener {
+ private CustomSwipeRefreshLayout mSwipeRefreshLayout;
+ private RefreshListener mRefreshListener;
+ private boolean mRefreshing;
+
+ public interface RefreshListener {
+ public void onRefreshStarted();
+ }
+
+ public SwipeToRefreshHelper(Context context, CustomSwipeRefreshLayout swipeRefreshLayout, RefreshListener listener) {
+ init(context, swipeRefreshLayout, listener);
+ }
+
+ public void init(Context context, CustomSwipeRefreshLayout swipeRefreshLayout, RefreshListener listener) {
+ mRefreshListener = listener;
+ mSwipeRefreshLayout = swipeRefreshLayout;
+ mSwipeRefreshLayout.setOnRefreshListener(this);
+ final TypedArray styleAttrs = obtainStyledAttrsFromThemeAttr(context, R.attr.swipeToRefreshStyle,
+ R.styleable.RefreshIndicator);
+ int color = styleAttrs.getColor(R.styleable.RefreshIndicator_refreshIndicatorColor, context.getResources()
+ .getColor(android.R.color.holo_blue_dark));
+ mSwipeRefreshLayout.setColorSchemeColors(color, color, color, color);
+ }
+
+ public void setRefreshing(boolean refreshing) {
+ mRefreshing = refreshing;
+ // Delayed refresh, it fixes https://code.google.com/p/android/issues/detail?id=77712
+ // 50ms seems a good compromise (always worked during tests) and fast enough so user can't notice the delay
+ if (refreshing) {
+ mSwipeRefreshLayout.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ // use mRefreshing so if the refresh takes less than 50ms, loading indicator won't show up.
+ mSwipeRefreshLayout.setRefreshing(mRefreshing);
+ }
+ }, 50);
+ } else {
+ mSwipeRefreshLayout.setRefreshing(false);
+ }
+ }
+
+ public boolean isRefreshing() {
+ return mSwipeRefreshLayout.isRefreshing();
+ }
+
+ @Override
+ public void onRefresh() {
+ mRefreshListener.onRefreshStarted();
+ }
+
+ public void setEnabled(boolean enabled) {
+ mSwipeRefreshLayout.setEnabled(enabled);
+ }
+
+ public static TypedArray obtainStyledAttrsFromThemeAttr(Context context, int themeAttr, int[] styleAttrs) {
+ TypedValue outValue = new TypedValue();
+ context.getTheme().resolveAttribute(themeAttr, outValue, true);
+ int styleResId = outValue.resourceId;
+ return context.obtainStyledAttributes(styleResId, styleAttrs);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/Version.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/Version.java
new file mode 100644
index 000000000..b35f84757
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/Version.java
@@ -0,0 +1,47 @@
+package org.wordpress.android.util.helpers;
+
+//See: http://stackoverflow.com/a/11024200
+public class Version implements Comparable<Version> {
+ private String version;
+
+ public final String get() {
+ return this.version;
+ }
+
+ public Version(String version) {
+ if(version == null)
+ throw new IllegalArgumentException("Version can not be null");
+ if(!version.matches("[0-9]+(\\.[0-9]+)*"))
+ throw new IllegalArgumentException("Invalid version format");
+ this.version = version;
+ }
+
+ @Override public int compareTo(Version that) {
+ if(that == null)
+ return 1;
+ String[] thisParts = this.get().split("\\.");
+ String[] thatParts = that.get().split("\\.");
+ int length = Math.max(thisParts.length, thatParts.length);
+ for(int i = 0; i < length; i++) {
+ int thisPart = i < thisParts.length ?
+ Integer.parseInt(thisParts[i]) : 0;
+ int thatPart = i < thatParts.length ?
+ Integer.parseInt(thatParts[i]) : 0;
+ if(thisPart < thatPart)
+ return -1;
+ if(thisPart > thatPart)
+ return 1;
+ }
+ return 0;
+ }
+
+ @Override public boolean equals(Object that) {
+ if(this == that)
+ return true;
+ if(that == null)
+ return false;
+ if(this.getClass() != that.getClass())
+ return false;
+ return this.compareTo((Version) that) == 0;
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPHtmlTagHandler.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPHtmlTagHandler.java
new file mode 100644
index 000000000..da333b24e
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPHtmlTagHandler.java
@@ -0,0 +1,59 @@
+package org.wordpress.android.util.helpers;
+
+import android.text.Editable;
+import android.text.Html;
+import android.text.style.BulletSpan;
+import android.text.style.LeadingMarginSpan;
+
+import org.xml.sax.XMLReader;
+
+import java.util.Vector;
+
+/**
+ * Handle tags that the Html class doesn't understand
+ * Tweaked from source at http://stackoverflow.com/questions/4044509/android-how-to-use-the-html-taghandler
+ */
+public class WPHtmlTagHandler implements Html.TagHandler {
+ private int mListItemCount = 0;
+ private Vector<String> mListParents = new Vector<String>();
+
+ @Override
+ public void handleTag(final boolean opening, final String tag, Editable output,
+ final XMLReader xmlReader) {
+ if (tag.equals("ul") || tag.equals("ol") || tag.equals("dd")) {
+ if (opening) {
+ mListParents.add(tag);
+ } else {
+ mListParents.remove(tag);
+ }
+ mListItemCount = 0;
+ } else if (tag.equals("li") && !opening) {
+ handleListTag(output);
+ }
+ }
+
+ private void handleListTag(Editable output) {
+ if (mListParents.lastElement().equals("ul")) {
+ output.append("\n");
+ String[] split = output.toString().split("\n");
+ int start = 0;
+ if (split.length != 1) {
+ int lastIndex = split.length - 1;
+ start = output.length() - split[lastIndex].length() - 1;
+ }
+ output.setSpan(new BulletSpan(15 * mListParents.size()), start, output.length(), 0);
+ } else if (mListParents.lastElement().equals("ol")) {
+ mListItemCount++;
+ output.append("\n");
+ String[] split = output.toString().split("\n");
+ int start = 0;
+ if (split.length != 1) {
+ int lastIndex = split.length - 1;
+ start = output.length() - split[lastIndex].length() - 1;
+ }
+ output.insert(start, mListItemCount + ". ");
+ output.setSpan(new LeadingMarginSpan.Standard(15 * mListParents.size()), start,
+ output.length(), 0);
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageGetter.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageGetter.java
new file mode 100644
index 000000000..b03d74045
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageGetter.java
@@ -0,0 +1,177 @@
+package org.wordpress.android.util.helpers;
+
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.text.Html;
+import android.text.TextUtils;
+import android.widget.TextView;
+
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.ImageLoader;
+
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.PhotonUtils;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * ImageGetter for Html.fromHtml()
+ * adapted from existing ImageGetter code in NoteCommentFragment
+ */
+public class WPImageGetter implements Html.ImageGetter {
+ private final WeakReference<TextView> mWeakView;
+ private final int mMaxSize;
+ private ImageLoader mImageLoader;
+ private Drawable mLoadingDrawable;
+ private Drawable mFailedDrawable;
+
+ public WPImageGetter(TextView view) {
+ this(view, 0);
+ }
+
+ public WPImageGetter(TextView view, int maxSize) {
+ mWeakView = new WeakReference<TextView>(view);
+ mMaxSize = maxSize;
+ }
+
+ public WPImageGetter(TextView view, int maxSize, ImageLoader imageLoader, Drawable loadingDrawable,
+ Drawable failedDrawable) {
+ mWeakView = new WeakReference<TextView>(view);
+ mMaxSize = maxSize;
+ mImageLoader = imageLoader;
+ mLoadingDrawable = loadingDrawable;
+ mFailedDrawable = failedDrawable;
+ }
+
+ private TextView getView() {
+ return mWeakView.get();
+ }
+
+ @Override
+ public Drawable getDrawable(String source) {
+ if (mImageLoader == null || mLoadingDrawable == null || mFailedDrawable == null) {
+ throw new RuntimeException("Developer, you need to call setImageLoader, setLoadingDrawable and setFailedDrawable");
+ }
+
+ if (TextUtils.isEmpty(source)) {
+ return null;
+ }
+
+ // images in reader comments may skip "http:" (no idea why) so make sure to add protocol here
+ if (source.startsWith("//")) {
+ source = "http:" + source;
+ }
+
+ // use Photon if a max size is requested (otherwise the full-sized image will be downloaded
+ // and then resized)
+ if (mMaxSize > 0) {
+ source = PhotonUtils.getPhotonImageUrl(source, mMaxSize, 0);
+ }
+
+ final RemoteDrawable remote = new RemoteDrawable(mLoadingDrawable, mFailedDrawable);
+
+ mImageLoader.get(source, new ImageLoader.ImageListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ remote.displayFailed();
+ TextView view = getView();
+ if (view != null) {
+ view.invalidate();
+ }
+ }
+
+ @Override
+ public void onResponse(ImageLoader.ImageContainer response, boolean isImmediate) {
+ if (response.getBitmap() == null) {
+ AppLog.w(T.UTILS, "WPImageGetter null bitmap");
+ }
+
+ TextView view = getView();
+ if (view == null) {
+ AppLog.w(T.UTILS, "WPImageGetter view is invalid");
+ return;
+ }
+
+ int maxWidth = view.getWidth() - view.getPaddingLeft() - view.getPaddingRight();
+ if (mMaxSize > 0 && (maxWidth > mMaxSize || maxWidth == 0)) {
+ maxWidth = mMaxSize;
+ }
+
+ Drawable drawable = new BitmapDrawable(view.getContext().getResources(), response.getBitmap());
+ remote.setRemoteDrawable(drawable, maxWidth);
+
+ // force textView to resize correctly if image isn't cached by resetting the content
+ // to itself - this way the textView will use the cached image, and resizing to
+ // accommodate the image isn't necessary
+ if (!isImmediate) {
+ view.setText(view.getText());
+ }
+ }
+ });
+
+ return remote;
+ }
+
+ public static class RemoteDrawable extends BitmapDrawable {
+ Drawable mRemoteDrawable;
+ final Drawable mLoadingDrawable;
+ final Drawable mFailedDrawable;
+ private boolean mDidFail = false;
+
+ public RemoteDrawable(Drawable loadingDrawable, Drawable failedDrawable) {
+ mLoadingDrawable = loadingDrawable;
+ mFailedDrawable = failedDrawable;
+ setBounds(0, 0, mLoadingDrawable.getIntrinsicWidth(), mLoadingDrawable.getIntrinsicHeight());
+ }
+
+ public void displayFailed() {
+ mDidFail = true;
+ }
+
+ public void setBounds(int x, int y, int width, int height) {
+ super.setBounds(x, y, width, height);
+ if (mRemoteDrawable != null) {
+ mRemoteDrawable.setBounds(x, y, width, height);
+ return;
+ }
+ if (mLoadingDrawable != null) {
+ mLoadingDrawable.setBounds(x, y, width, height);
+ mFailedDrawable.setBounds(x, y, width, height);
+ }
+ }
+
+ public void setRemoteDrawable(Drawable remote, int maxWidth) {
+ // null sentinel for now
+ if (remote == null) {
+ // throw error
+ return;
+ }
+ mRemoteDrawable = remote;
+ // determine if we need to scale the image to fit in view
+ int imgWidth = remote.getIntrinsicWidth();
+ int imgHeight = remote.getIntrinsicHeight();
+ float xScale = (float) imgWidth / (float) maxWidth;
+ if (xScale > 1.0f) {
+ setBounds(0, 0, Math.round(imgWidth / xScale), Math.round(imgHeight / xScale));
+ } else {
+ setBounds(0, 0, imgWidth, imgHeight);
+ }
+ }
+
+ public boolean didFail() {
+ return mDidFail;
+ }
+
+ public void draw(Canvas canvas) {
+ if (mRemoteDrawable != null) {
+ mRemoteDrawable.draw(canvas);
+ } else if (didFail()) {
+ mFailedDrawable.draw(canvas);
+ } else {
+ mLoadingDrawable.draw(canvas);
+ }
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageSpan.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageSpan.java
new file mode 100644
index 000000000..fa0a0b4aa
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageSpan.java
@@ -0,0 +1,140 @@
+//Add WordPress image fields to ImageSpan object
+
+package org.wordpress.android.util.helpers;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.style.ImageSpan;
+
+public class WPImageSpan extends ImageSpan implements Parcelable {
+ protected Uri mImageSource = null;
+ protected boolean mNetworkImageLoaded = false;
+ protected MediaFile mMediaFile;
+ protected int mStartPosition, mEndPosition;
+
+ protected WPImageSpan() {
+ super((Bitmap) null);
+ }
+
+ public WPImageSpan(Context context, Bitmap b, Uri src) {
+ super(context, b);
+ this.mImageSource = src;
+ mMediaFile = new MediaFile();
+ }
+
+ public WPImageSpan(Context context, int resId, Uri src) {
+ super(context, resId);
+ this.mImageSource = src;
+ mMediaFile = new MediaFile();
+ }
+
+ public void setPosition(int start, int end) {
+ mStartPosition = start;
+ mEndPosition = end;
+ }
+
+ public int getStartPosition() {
+ return mStartPosition >= 0 ? mStartPosition : 0;
+ }
+
+ public int getEndPosition() {
+ return mEndPosition < getStartPosition() ? getStartPosition() : mEndPosition;
+ }
+
+ public MediaFile getMediaFile() {
+ return mMediaFile;
+ }
+
+ public void setMediaFile(MediaFile mMediaFile) {
+ this.mMediaFile = mMediaFile;
+ }
+
+ public void setImageSource(Uri mImageSource) {
+ this.mImageSource = mImageSource;
+ }
+
+ public Uri getImageSource() {
+ return mImageSource;
+ }
+
+ public boolean isNetworkImageLoaded() {
+ return mNetworkImageLoaded;
+ }
+
+ public void setNetworkImageLoaded(boolean networkImageLoaded) {
+ this.mNetworkImageLoaded = networkImageLoaded;
+ }
+
+ protected void setupFromParcel(Parcel in) {
+ MediaFile mediaFile = new MediaFile();
+
+ boolean[] booleans = new boolean[2];
+ in.readBooleanArray(booleans);
+ setNetworkImageLoaded(booleans[0]);
+ mediaFile.setVideo(booleans[1]);
+
+ setImageSource(Uri.parse(in.readString()));
+ mediaFile.setMediaId(in.readString());
+ mediaFile.setBlogId(in.readString());
+ mediaFile.setPostID(in.readLong());
+ mediaFile.setCaption(in.readString());
+ mediaFile.setDescription(in.readString());
+ mediaFile.setTitle(in.readString());
+ mediaFile.setMimeType(in.readString());
+ mediaFile.setFileName(in.readString());
+ mediaFile.setThumbnailURL(in.readString());
+ mediaFile.setVideoPressShortCode(in.readString());
+ mediaFile.setFileURL(in.readString());
+ mediaFile.setFilePath(in.readString());
+ mediaFile.setDateCreatedGMT(in.readLong());
+ mediaFile.setWidth(in.readInt());
+ mediaFile.setHeight(in.readInt());
+ setPosition(in.readInt(), in.readInt());
+
+ setMediaFile(mediaFile);
+ }
+
+ public static final Parcelable.Creator<WPImageSpan> CREATOR
+ = new Parcelable.Creator<WPImageSpan>() {
+ public WPImageSpan createFromParcel(Parcel in) {
+ WPImageSpan imageSpan = new WPImageSpan();
+ imageSpan.setupFromParcel(in);
+ return imageSpan;
+ }
+
+ public WPImageSpan[] newArray(int size) {
+ return new WPImageSpan[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ parcel.writeBooleanArray(new boolean[] {mNetworkImageLoaded, mMediaFile.isVideo()});
+ parcel.writeString(mImageSource.toString());
+ parcel.writeString(mMediaFile.getMediaId());
+ parcel.writeString(mMediaFile.getBlogId());
+ parcel.writeLong(mMediaFile.getPostID());
+ parcel.writeString(mMediaFile.getCaption());
+ parcel.writeString(mMediaFile.getDescription());
+ parcel.writeString(mMediaFile.getTitle());
+ parcel.writeString(mMediaFile.getMimeType());
+ parcel.writeString(mMediaFile.getFileName());
+ parcel.writeString(mMediaFile.getThumbnailURL());
+ parcel.writeString(mMediaFile.getVideoPressShortCode());
+ parcel.writeString(mMediaFile.getFileURL());
+ parcel.writeString(mMediaFile.getFilePath());
+ parcel.writeLong(mMediaFile.getDateCreatedGMT());
+ parcel.writeInt(mMediaFile.getWidth());
+ parcel.writeInt(mMediaFile.getHeight());
+ parcel.writeInt(getStartPosition());
+ parcel.writeInt(getEndPosition());
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPQuoteSpan.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPQuoteSpan.java
new file mode 100644
index 000000000..33cdc0093
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPQuoteSpan.java
@@ -0,0 +1,44 @@
+package org.wordpress.android.util.helpers;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.text.Layout;
+import android.text.style.QuoteSpan;
+
+/**
+ * Customzed QuoteSpan for use in SpannableString's
+ */
+public class WPQuoteSpan extends QuoteSpan {
+ public static final int STRIPE_COLOR = 0xFF21759B;
+ private static final int STRIPE_WIDTH = 5;
+ private static final int GAP_WIDTH = 20;
+
+ public WPQuoteSpan(){
+ super(STRIPE_COLOR);
+ }
+
+ @Override
+ public int getLeadingMargin(boolean first) {
+ int margin = GAP_WIDTH * 2 + STRIPE_WIDTH;
+ return margin;
+ }
+
+ /**
+ * Draw a nice thick gray bar if Ice Cream Sandwhich or newer. There's a
+ * bug on older devices that does not respect the increased margin.
+ */
+ @Override
+ public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom,
+ CharSequence text, int start, int end, boolean first, Layout layout) {
+ Paint.Style style = p.getStyle();
+ int color = p.getColor();
+
+ p.setStyle(Paint.Style.FILL);
+ p.setColor(STRIPE_COLOR);
+
+ c.drawRect(GAP_WIDTH + x, top, x + dir * STRIPE_WIDTH, bottom, p);
+
+ p.setStyle(style);
+ p.setColor(color);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPUnderlineSpan.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPUnderlineSpan.java
new file mode 100644
index 000000000..4b6805ccf
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPUnderlineSpan.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package org.wordpress.android.util.helpers;
+
+import android.os.Parcel;
+import android.text.style.UnderlineSpan;
+
+/**
+ * WPUnderlineSpan is used as an alternative class to UnderlineSpan. UnderlineSpan is used by EditText auto
+ * correct, so it can get mixed up with our formatting.
+ */
+public class WPUnderlineSpan extends UnderlineSpan {
+ public WPUnderlineSpan() {
+ super();
+ }
+
+ public WPUnderlineSpan(Parcel src) {
+ super(src);
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPWebChromeClient.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPWebChromeClient.java
new file mode 100644
index 000000000..1418e79ea
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPWebChromeClient.java
@@ -0,0 +1,45 @@
+package org.wordpress.android.util.helpers;
+
+import android.app.Activity;
+import android.text.TextUtils;
+import android.view.View;
+import android.webkit.WebChromeClient;
+import android.webkit.WebView;
+import android.widget.ProgressBar;
+
+public class WPWebChromeClient extends WebChromeClient {
+ private final ProgressBar mProgressBar;
+ private final Activity mActivity;
+ private final boolean mAutoUpdateActivityTitle;
+
+ public WPWebChromeClient(Activity activity, ProgressBar progressBar) {
+ mActivity = activity;
+ mProgressBar = progressBar;
+ mAutoUpdateActivityTitle = true;
+ }
+
+ public WPWebChromeClient(Activity activity,
+ ProgressBar progressBar,
+ boolean autoUpdateActivityTitle) {
+ mActivity = activity;
+ mProgressBar = progressBar;
+ mAutoUpdateActivityTitle = autoUpdateActivityTitle;
+ }
+
+ public void onProgressChanged(WebView webView, int progress) {
+ if (mActivity != null
+ && !mActivity.isFinishing()
+ && mAutoUpdateActivityTitle
+ && !TextUtils.isEmpty(webView.getTitle())) {
+ mActivity.setTitle(webView.getTitle());
+ }
+ if (mProgressBar != null) {
+ if (progress == 100) {
+ mProgressBar.setVisibility(View.GONE);
+ } else {
+ mProgressBar.setVisibility(View.VISIBLE);
+ mProgressBar.setProgress(progress);
+ }
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/AutoResizeTextView.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/AutoResizeTextView.java
new file mode 100644
index 000000000..b0b4dc017
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/AutoResizeTextView.java
@@ -0,0 +1,299 @@
+package org.wordpress.android.util.widgets;
+
+import android.content.Context;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.widget.TextView;
+
+/**
+ * Text view that auto adjusts text size to fit within the view.
+ * If the text size equals the minimum text size and still does not
+ * fit, append with an ellipsis.
+ *
+ * See http://stackoverflow.com/a/5535672
+ *
+ */
+public class AutoResizeTextView extends TextView {
+ // Minimum text size for this text view
+ private static final float MIN_TEXT_SIZE = 20;
+
+ // Interface for resize notifications
+ public interface OnTextResizeListener {
+ void onTextResize(TextView textView, float oldSize, float newSize);
+ }
+
+ // Our ellipse string - Unicode Character 'HORIZONTAL ELLIPSIS'
+ private static final String M_ELLIPSIS = "\u2026";
+
+ // Registered resize listener
+ private OnTextResizeListener mTextResizeListener;
+
+ // Flag for text and/or size changes to force a resize
+ private boolean mNeedsResize = false;
+
+ // Text size that is set from code. This acts as a starting point for resizing
+ private float mTextSize;
+
+ // Temporary upper bounds on the starting text size
+ private float mMaxTextSize = 0;
+
+ // Lower bounds for text size
+ private float mMinTextSize = MIN_TEXT_SIZE;
+
+ // Text view line spacing multiplier
+ private float mSpacingMult = 1.0f;
+
+ // Text view additional line spacing
+ private float mSpacingAdd = 0.0f;
+
+ // Add ellipsis to text that overflows at the smallest text size
+ private boolean mAddEllipsis = true;
+
+ // Default constructor override
+ public AutoResizeTextView(Context context) {
+ this(context, null);
+ }
+
+ // Default constructor when inflating from XML file
+ public AutoResizeTextView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ // Default constructor override
+ public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mTextSize = getTextSize();
+ }
+
+ /**
+ * When text changes, set the force resize flag to true and reset the text size.
+ */
+ @Override
+ protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) {
+ mNeedsResize = true;
+ // Since this view may be reused, it is good to reset the text size
+ resetTextSize();
+ }
+
+ /**
+ * If the text view size changed, set the force resize flag to true
+ */
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ if (w != oldw || h != oldh) {
+ mNeedsResize = true;
+ }
+ }
+
+ /**
+ * Register listener to receive resize notifications
+ * @param listener
+ */
+ public void setOnResizeListener(OnTextResizeListener listener) {
+ mTextResizeListener = listener;
+ }
+
+ /**
+ * Override the set text size to update our internal reference values
+ */
+ @Override
+ public void setTextSize(float size) {
+ super.setTextSize(size);
+ mTextSize = getTextSize();
+ }
+
+ /**
+ * Override the set text size to update our internal reference values
+ */
+ @Override
+ public void setTextSize(int unit, float size) {
+ super.setTextSize(unit, size);
+ mTextSize = getTextSize();
+ }
+
+ /**
+ * Override the set line spacing to update our internal reference values
+ */
+ @Override
+ public void setLineSpacing(float add, float mult) {
+ super.setLineSpacing(add, mult);
+ mSpacingMult = mult;
+ mSpacingAdd = add;
+ }
+
+ /**
+ * Set the upper text size limit and invalidate the view
+ * @param maxTextSize
+ */
+ public void setMaxTextSize(float maxTextSize) {
+ mMaxTextSize = maxTextSize;
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Return upper text size limit
+ * @return
+ */
+ public float getMaxTextSize() {
+ return mMaxTextSize;
+ }
+
+ /**
+ * Set the lower text size limit and invalidate the view
+ * @param minTextSize
+ */
+ public void setMinTextSize(float minTextSize) {
+ mMinTextSize = minTextSize;
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Return lower text size limit
+ * @return
+ */
+ public float getMinTextSize() {
+ return mMinTextSize;
+ }
+
+ /**
+ * Set flag to add ellipsis to text that overflows at the smallest text size
+ * @param addEllipsis
+ */
+ public void setAddEllipsis(boolean addEllipsis) {
+ mAddEllipsis = addEllipsis;
+ }
+
+ /**
+ * Return flag to add ellipsis to text that overflows at the smallest text size
+ * @return
+ */
+ public boolean getAddEllipsis() {
+ return mAddEllipsis;
+ }
+
+ /**
+ * Reset the text to the original size
+ */
+ private void resetTextSize() {
+ if (mTextSize > 0) {
+ super.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
+ mMaxTextSize = mTextSize;
+ }
+ }
+
+ /**
+ * Resize text after measuring
+ */
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ if (changed || mNeedsResize) {
+ int widthLimit = (right - left) - getCompoundPaddingLeft() - getCompoundPaddingRight();
+ int heightLimit = (bottom - top) - getCompoundPaddingBottom() - getCompoundPaddingTop();
+ resizeText(widthLimit, heightLimit);
+ }
+ super.onLayout(changed, left, top, right, bottom);
+ }
+
+ /**
+ * Resize the text size with default width and height
+ */
+ public void resizeText() {
+ int heightLimit = getHeight() - getPaddingBottom() - getPaddingTop();
+ int widthLimit = getWidth() - getPaddingLeft() - getPaddingRight();
+ resizeText(widthLimit, heightLimit);
+ }
+
+ /**
+ * Resize the text size with specified width and height
+ * @param width
+ * @param height
+ */
+ public void resizeText(int width, int height) {
+ CharSequence text = getText();
+ // Do not resize if the view does not have dimensions or there is no text
+ if (text == null || text.length() == 0 || height <= 0 || width <= 0 || mTextSize == 0) {
+ return;
+ }
+
+ // Get the text view's paint object
+ TextPaint textPaint = getPaint();
+
+ // Store the current text size
+ float oldTextSize = textPaint.getTextSize();
+ // If there is a max text size set, use the lesser of that and the default text size
+ float targetTextSize = mMaxTextSize > 0 ? Math.min(mTextSize, mMaxTextSize) : mTextSize;
+
+ // Get the required text height
+ int textHeight = getTextHeight(text, textPaint, width, targetTextSize);
+
+ // Until we either fit within our text view or we had reached our min text size, incrementally try smaller sizes
+ while (textHeight > height && targetTextSize > mMinTextSize) {
+ targetTextSize = Math.max(targetTextSize - 2, mMinTextSize);
+ textHeight = getTextHeight(text, textPaint, width, targetTextSize);
+ }
+
+ // If we had reached our minimum text size and still don't fit, append an ellipsis
+ if (mAddEllipsis && targetTextSize == mMinTextSize && textHeight > height) {
+ // Draw using a static layout
+ // modified: use a copy of TextPaint for measuring
+ TextPaint paint = new TextPaint(textPaint);
+ // Draw using a static layout
+ StaticLayout layout = new StaticLayout(text, paint, width, Layout.Alignment.ALIGN_NORMAL,
+ mSpacingMult, mSpacingAdd, false);
+ // Check that we have a least one line of rendered text
+ if (layout.getLineCount() > 0) {
+ // Since the line at the specific vertical position would be cut off,
+ // we must trim up to the previous line
+ int lastLine = layout.getLineForVertical(height) - 1;
+ // If the text would not even fit on a single line, clear it
+ if (lastLine < 0) {
+ setText("");
+ } else {
+ // Otherwise, trim to the previous line and add an ellipsis
+ int start = layout.getLineStart(lastLine);
+ int end = layout.getLineEnd(lastLine);
+ float lineWidth = layout.getLineWidth(lastLine);
+ float ellipseWidth = paint.measureText(M_ELLIPSIS);
+
+ // Trim characters off until we have enough room to draw the ellipsis
+ while (width < lineWidth + ellipseWidth) {
+ lineWidth = paint.measureText(text.subSequence(start, --end + 1).toString());
+ }
+ setText(text.subSequence(0, end) + M_ELLIPSIS);
+ }
+ }
+ }
+
+ // Some devices try to auto adjust line spacing, so force default line spacing
+ // and invalidate the layout as a side effect
+ setTextSize(TypedValue.COMPLEX_UNIT_PX, targetTextSize);
+ setLineSpacing(mSpacingAdd, mSpacingMult);
+
+ // Notify the listener if registered
+ if (mTextResizeListener != null) {
+ mTextResizeListener.onTextResize(this, oldTextSize, targetTextSize);
+ }
+
+ // Reset force resize flag
+ mNeedsResize = false;
+ }
+
+ // Set the text size of the text paint object and use a static layout to render text off screen before measuring
+ private int getTextHeight(CharSequence source, TextPaint paint, int width, float textSize) {
+ // modified: make a copy of the original TextPaint object for measuring
+ // (apparently the object gets modified while measuring, see also the
+ // docs for TextView.getPaint() (which states to access it read-only)
+ TextPaint paintCopy = new TextPaint(paint);
+ // Update the text paint object
+ paintCopy.setTextSize(textSize);
+ // Measure using a static layout
+ StaticLayout layout = new StaticLayout(source, paintCopy, width, Layout.Alignment.ALIGN_NORMAL,
+ mSpacingMult, mSpacingAdd, true);
+ return layout.getHeight();
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/CustomSwipeRefreshLayout.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/CustomSwipeRefreshLayout.java
new file mode 100644
index 000000000..356268922
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/CustomSwipeRefreshLayout.java
@@ -0,0 +1,33 @@
+package org.wordpress.android.util.widgets;
+
+import android.content.Context;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+public class CustomSwipeRefreshLayout extends SwipeRefreshLayout {
+ public CustomSwipeRefreshLayout(Context context) {
+ super(context);
+ }
+
+ public CustomSwipeRefreshLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ try{
+ return super.onTouchEvent(event);
+ } catch(IllegalArgumentException e) {
+ // Fix for https://github.com/wordpress-mobile/WordPress-Android/issues/2373
+ // Catch IllegalArgumentException which can be fired by the underlying SwipeRefreshLayout.onTouchEvent()
+ // method.
+ // When android support-v4 fixes it, we'll have to remove that custom layout completely.
+ AppLog.e(T.UTILS, e);
+ return true;
+ }
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPEditText.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPEditText.java
new file mode 100644
index 000000000..0468cf807
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPEditText.java
@@ -0,0 +1,62 @@
+package org.wordpress.android.util.widgets;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.widget.EditText;
+
+/*
+ * @deprecated This custom EditText is used solely by the "legacy" editor in WP Android.
+ * It will be removed when we drop the legacy editor and should not be used in new code.
+ */
+@Deprecated
+public class WPEditText extends EditText {
+ private EditTextImeBackListener mOnImeBack;
+ private OnSelectionChangedListener onSelectionChangedListener;
+
+ public WPEditText(Context context) {
+ super(context);
+ }
+
+ public WPEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public WPEditText(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onSelectionChanged(int selStart, int selEnd) {
+ if (onSelectionChangedListener != null) {
+ onSelectionChangedListener.onSelectionChanged();
+ }
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_BACK
+ && event.getAction() == KeyEvent.ACTION_UP) {
+ if (mOnImeBack != null)
+ mOnImeBack.onImeBack(this, this.getText().toString());
+ }
+
+ return super.onKeyPreIme(keyCode, event);
+ }
+
+ public void setOnEditTextImeBackListener(EditTextImeBackListener listener) {
+ mOnImeBack = listener;
+ }
+
+ public interface EditTextImeBackListener {
+ public abstract void onImeBack(WPEditText ctrl, String text);
+ }
+
+ public void setOnSelectionChangedListener(OnSelectionChangedListener listener) {
+ onSelectionChangedListener = listener;
+ }
+
+ public interface OnSelectionChangedListener {
+ public abstract void onSelectionChanged();
+ }
+}
diff --git a/libs/utils/WordPressUtils/src/main/res/values/attrs.xml b/libs/utils/WordPressUtils/src/main/res/values/attrs.xml
new file mode 100644
index 000000000..dd1fa5cbf
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/res/values/attrs.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <attr name="swipeToRefreshStyle" format="reference"/>
+ <declare-styleable name="RefreshIndicator">
+ <attr name="refreshIndicatorColor" format="reference|color"/>
+ </declare-styleable>
+</resources>
diff --git a/libs/utils/WordPressUtils/src/main/res/values/strings.xml b/libs/utils/WordPressUtils/src/main/res/values/strings.xml
new file mode 100644
index 000000000..34d25dada
--- /dev/null
+++ b/libs/utils/WordPressUtils/src/main/res/values/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="no_network_message">There is no network available</string>
+ <string name="timespan_now">Now</string>
+</resources>
diff --git a/libs/utils/build.gradle b/libs/utils/build.gradle
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/libs/utils/build.gradle
diff --git a/libs/utils/gradle/wrapper/gradle-wrapper.jar b/libs/utils/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..0087cd3b1
--- /dev/null
+++ b/libs/utils/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/libs/utils/gradle/wrapper/gradle-wrapper.properties b/libs/utils/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..5e7158a61
--- /dev/null
+++ b/libs/utils/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Sep 06 11:08:13 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/utils/gradlew b/libs/utils/gradlew
new file mode 100755
index 000000000..91a7e269e
--- /dev/null
+++ b/libs/utils/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/utils/gradlew.bat b/libs/utils/gradlew.bat
new file mode 100644
index 000000000..8a0b282aa
--- /dev/null
+++ b/libs/utils/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/utils/settings.gradle b/libs/utils/settings.gradle
new file mode 100644
index 000000000..3519745ed
--- /dev/null
+++ b/libs/utils/settings.gradle
@@ -0,0 +1 @@
+include ':WordPressUtils' \ No newline at end of file