diff --git a/.travis.yml b/.travis.yml
index 737689c3..e69de29b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,24 +0,0 @@
-language: android
-jdk: oraclejdk8
-
-android:
- components:
- - extra-android-m2repository
- - extra-android-support
- - platform-tools
- - tools
- - build-tools-25.0.1
- - android-25
-
-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/README.md b/README.md
index 75379491..7c66a06a 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,5 @@
# WordPress-Editor-Android #
-[](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.
diff --git a/WordPressEditor/build.gradle b/WordPressEditor/build.gradle
index 6ad0e825..fa188d40 100644
--- a/WordPressEditor/build.gradle
+++ b/WordPressEditor/build.gradle
@@ -1,31 +1,49 @@
buildscript {
repositories {
+ google()
jcenter()
}
+
+ ext {
+ aztecVersion = 'v1.3.44'
+ }
+
dependencies {
- classpath 'com.android.tools.build:gradle:2.2.3'
+ classpath 'com.android.tools.build:gradle:4.0.1'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
}
+repositories {
+ google()
+ jcenter()
+ maven { url "https://www.jitpack.io" }
+ maven { url "https://dl.bintray.com/wordpress-mobile/react-native-mirror/" }
+}
+
apply plugin: 'com.android.library'
apply plugin: 'maven'
apply plugin: 'signing'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
-repositories {
- jcenter()
-}
-android {
- publishNonDefault true
+// import the `submoduleGitHash()` function
+apply from: 'https://gist.githubusercontent.com/hypest/e06f6097065728b6db7b7c462f8fef1a/raw/3b91756fca76e4c2a9b573313e186c47842e1f7d/submoduleGitHash.gradle'
- compileSdkVersion 25
- buildToolsVersion "25.0.1"
+android {
+ compileSdkVersion 29
defaultConfig {
versionCode 13
versionName "1.3"
- minSdkVersion 16
- targetSdkVersion 25
+ minSdkVersion 21
+ targetSdkVersion 29
+ buildConfigField "boolean", "BUILD_GUTENBERG_FROM_SOURCE", (rootProject.ext.has("buildGutenbergFromSource") ? rootProject.ext.buildGutenbergFromSource : false).toString()
+ }
+ compileOptions {
+ sourceCompatibility 1.8
+ targetCompatibility 1.8
}
buildTypes {
release {
@@ -45,10 +63,35 @@ android {
}
dependencies {
- compile 'com.android.support:appcompat-v7:25.0.1'
- compile 'com.android.support:support-v4:25.0.1'
- compile 'com.android.support:design:25.0.1'
- compile 'org.wordpress:utils:1.11.0'
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'androidx.vectordrawable:vectordrawable-animated:1.0.0'
+ implementation 'androidx.gridlayout:gridlayout:1.0.0'
+ implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+ implementation 'com.google.android.material:material:1.1.0'
+ implementation 'org.apache.commons:commons-lang3:3.5'
+ implementation 'com.android.volley:volley:1.1.1'
+ implementation 'org.wordpress:utils:1.25'
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
+
+ api ("com.github.wordpress-mobile.WordPress-Aztec-Android:aztec:$aztecVersion") {
+ exclude group: "com.android.volley"
+ }
+ api ("com.github.wordpress-mobile.WordPress-Aztec-Android:wordpress-shortcodes:$aztecVersion")
+ api ("com.github.wordpress-mobile.WordPress-Aztec-Android:wordpress-comments:$aztecVersion")
+
+ implementation (project(':@wordpress_react-native-bridge')) {
+ exclude group: 'com.squareup.okhttp3'
+ exclude group: "com.android.volley"
+ exclude group: 'com.google.code.findbugs'
+ }
+
+ // Required Aztec dependencies (they should be included but Jitpack seems to be stripping these out)
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:1.1.4"
+ implementation "org.jsoup:jsoup:1.10.3"
+
+ implementation 'com.google.code.gson:gson:2.6.2'
+
+ lintChecks 'org.wordpress:lint:1.0.1'
}
signing {
diff --git a/WordPressEditor/lint.xml b/WordPressEditor/lint.xml
index d377d1d0..fff68c3f 100644
--- a/WordPressEditor/lint.xml
+++ b/WordPressEditor/lint.xml
@@ -7,4 +7,13 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/WordPressEditor/src/androidTest/AndroidManifest.xml b/WordPressEditor/src/androidTest/AndroidManifest.xml
index eeef377f..9cdaeb41 100644
--- a/WordPressEditor/src/androidTest/AndroidManifest.xml
+++ b/WordPressEditor/src/androidTest/AndroidManifest.xml
@@ -1,12 +1,4 @@
-
-
-
- >
diff --git a/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/EditorFragmentForTests.java b/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/EditorFragmentForTests.java
deleted file mode 100644
index 1babaa3d..00000000
--- a/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/EditorFragmentForTests.java
+++ /dev/null
@@ -1,41 +0,0 @@
-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 changeMap) {
- super.onSelectionStyleChanged(changeMap);
- mOnSelectionStyleChangedCalled = true;
- }
-}
diff --git a/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/EditorFragmentTest.java b/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/EditorFragmentTest.java
deleted file mode 100644
index 54c1f831..00000000
--- a/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/EditorFragmentTest.java
+++ /dev/null
@@ -1,183 +0,0 @@
-package org.wordpress.android.editor;
-
-import android.app.Activity;
-import android.test.ActivityInstrumentationTestCase2;
-import android.view.View;
-import android.widget.ToggleButton;
-
-import org.wordpress.android.editor.EditorFragment.IllegalEditorStateException;
-
-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 {
- 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 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, IllegalEditorStateException {
- 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 content");
-
- // Check that getTitle() and getContent() return latest version even in HTML mode
- try {
- assertEquals("new title", mFragment.getTitle());
- assertEquals("new content", mFragment.getContent());
- } catch (IllegalEditorStateException e) {
- throw new RuntimeException();
- }
-
- 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 content", 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"));
- }
- }
- }
-}
diff --git a/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockActivity.java b/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockActivity.java
deleted file mode 100644
index 580ea2de..00000000
--- a/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockActivity.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.wordpress.android.editor;
-
-import android.support.v7.app.AppCompatActivity;
-
-public class MockActivity extends AppCompatActivity {
-
-}
diff --git a/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockEditorActivity.java b/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockEditorActivity.java
deleted file mode 100644
index ecd51515..00000000
--- a/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockEditorActivity.java
+++ /dev/null
@@ -1,102 +0,0 @@
-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("
Example content
");
- }
-
- @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 mediaUri) {
-
- }
-
- @Override
- public void onRequestDragAndDropPermissions(DragEvent dragEvent) {
-
- }
-}
-
diff --git a/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/TestingUtils.java b/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/TestingUtils.java
deleted file mode 100644
index e51cba6b..00000000
--- a/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/TestingUtils.java
+++ /dev/null
@@ -1,14 +0,0 @@
-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/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/ZssEditorTest.java b/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/ZssEditorTest.java
deleted file mode 100644
index 9989cbd6..00000000
--- a/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/ZssEditorTest.java
+++ /dev/null
@@ -1,128 +0,0 @@
-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 ZSSEditor inside an EditorWebViewAbstract, with no UI.
- */
-public class ZssEditorTest extends ActivityInstrumentationTestCase2 {
- 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 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 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/WordPressEditor/src/main/AndroidManifest.xml b/WordPressEditor/src/main/AndroidManifest.xml
index fcade320..b42a780d 100644
--- a/WordPressEditor/src/main/AndroidManifest.xml
+++ b/WordPressEditor/src/main/AndroidManifest.xml
@@ -1,7 +1,12 @@
-
+
+
-
- >
+
+
+
diff --git a/WordPressEditor/src/main/assets/ZSSRichTextEditor.js b/WordPressEditor/src/main/assets/ZSSRichTextEditor.js
deleted file mode 100755
index 52dabe1e..00000000
--- a/WordPressEditor/src/main/assets/ZSSRichTextEditor.js
+++ /dev/null
@@ -1,3847 +0,0 @@
-/*!
- *
- * 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 is used instead of
- // . 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 '
'). 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