Skip to content

Commit af5fa2b

Browse files
authored
Merge pull request #704 from wordpress-mobile/feature/detect-change-in-html-editor
Detect change in html editor
2 parents f42061a + efd43ed commit af5fa2b

File tree

6 files changed

+124
-56
lines changed

6 files changed

+124
-56
lines changed

app/src/androidTest/kotlin/org/wordpress/aztec/demo/Matchers.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import android.widget.EditText
55
import org.hamcrest.Description
66
import org.hamcrest.Matcher
77
import org.hamcrest.TypeSafeMatcher
8+
import org.wordpress.aztec.AztecText
89
import org.wordpress.aztec.source.Format
10+
import org.wordpress.aztec.source.SourceViewEditText
911

1012
object Matchers {
1113
fun withRegex(expected: Regex): Matcher<View> {
@@ -45,4 +47,23 @@ object Matchers {
4547
}
4648
}
4749
}
50+
51+
fun hasContentChanges(shouldHaveChanges: AztecText.EditorHasChanges): TypeSafeMatcher<View> {
52+
53+
return object : TypeSafeMatcher<View>() {
54+
override fun describeTo(description: Description) {
55+
description.appendText("User has made changes to the post: $shouldHaveChanges")
56+
}
57+
58+
public override fun matchesSafely(view: View): Boolean {
59+
if (view is SourceViewEditText) {
60+
return view.hasChanges() == shouldHaveChanges
61+
}
62+
if (view is AztecText) {
63+
return view.hasChanges() == shouldHaveChanges
64+
}
65+
return false
66+
}
67+
}
68+
}
4869
}

app/src/androidTest/kotlin/org/wordpress/aztec/demo/pages/EditorPage.kt

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,9 @@ import android.support.test.espresso.matcher.ViewMatchers.withId
1414
import android.support.test.espresso.matcher.ViewMatchers.withText
1515
import android.view.KeyEvent
1616
import android.view.View
17-
import org.hamcrest.Description
1817
import org.hamcrest.Matcher
1918
import org.hamcrest.Matchers.allOf
2019
import org.hamcrest.Matchers.hasToString
21-
import org.hamcrest.TypeSafeMatcher
2220
import org.wordpress.aztec.AztecText
2321
import org.wordpress.aztec.demo.Actions
2422
import org.wordpress.aztec.demo.BasePage
@@ -371,21 +369,12 @@ class EditorPage : BasePage() {
371369
}
372370

373371
fun hasChanges(shouldHaveChanges : AztecText.EditorHasChanges): EditorPage {
374-
val hasNoChangesMatcher = object : TypeSafeMatcher<View>() {
375-
override fun describeTo(description: Description) {
376-
description.appendText("User has made changes to the post: $shouldHaveChanges")
377-
}
378-
379-
public override fun matchesSafely(view: View): Boolean {
380-
if (view is AztecText) {
381-
return view.hasChanges() == shouldHaveChanges
382-
}
383-
384-
return false
385-
}
386-
}
372+
editor.check(matches(Matchers.hasContentChanges(shouldHaveChanges)))
373+
return this
374+
}
387375

388-
editor.check(matches(hasNoChangesMatcher))
376+
fun hasChangesHTML(shouldHaveChanges : AztecText.EditorHasChanges): EditorPage {
377+
htmlEditor.check(matches(Matchers.hasContentChanges(shouldHaveChanges)))
389378
return this
390379
}
391380

app/src/androidTest/kotlin/org/wordpress/aztec/demo/tests/MixedTextFormattingTests.kt

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,6 @@ class MixedTextFormattingTests : BaseTest() {
254254
.hasChanges(AztecText.EditorHasChanges.NO_CHANGES) // Verify that the user had not changed the input
255255
}
256256

257-
@Ignore("Until this issue is fixed: https://github.com/wordpress-mobile/AztecEditor-Android/issues/698")
258257
@Test
259258
fun testHasChangesWithMixedBoldAndItalicFormatting() {
260259
val input = "<b>bold <i>italic</i> bold</b>"
@@ -271,4 +270,35 @@ class MixedTextFormattingTests : BaseTest() {
271270
.hasChanges(AztecText.EditorHasChanges.CHANGES)
272271
.verifyHTML(afterParser)
273272
}
273+
274+
@Test
275+
fun testHasChangesOnHTMLEditor() {
276+
val input = "<b>Test</b>"
277+
val insertedText = " text added"
278+
val afterParser = "<b>Test</b>$insertedText"
279+
280+
EditorPage().toggleHtml()
281+
.insertHTML(input)
282+
.toggleHtml()
283+
.toggleHtml() // switch back to HTML editor
284+
.insertHTML(insertedText)
285+
.hasChangesHTML(AztecText.EditorHasChanges.CHANGES)
286+
.verifyHTML(afterParser)
287+
}
288+
289+
@Test
290+
fun testHasChangesOnHTMLEditorTestedFromVisualEditor() {
291+
val input = "<b>Test</b>"
292+
val insertedText = " text added"
293+
val afterParser = "Test$insertedText"
294+
295+
EditorPage().toggleHtml()
296+
.insertHTML(input)
297+
.toggleHtml()
298+
.toggleHtml() // switch back to HTML editor
299+
.insertHTML(insertedText)
300+
.hasChangesHTML(AztecText.EditorHasChanges.CHANGES)
301+
.toggleHtml() // switch back to Visual editor
302+
.verify(afterParser)
303+
}
274304
}

aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,40 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
160160
bitmap.density = DisplayMetrics.DENSITY_DEFAULT
161161
return BitmapDrawable(context.resources, bitmap)
162162
}
163+
164+
@Throws(NoSuchAlgorithmException::class)
165+
private fun calculateSHA256(s: String): ByteArray {
166+
val digest = MessageDigest.getInstance("SHA-256")
167+
digest.update(s.toByteArray())
168+
return digest.digest()
169+
}
170+
171+
fun calculateInitialHTMLSHA(initialHTMLParsed: String, initialEditorContentParsedSHA256: ByteArray): ByteArray {
172+
try {
173+
// Do not recalculate the hash if it's not the first call to `fromHTML`.
174+
if (initialEditorContentParsedSHA256.isEmpty() || Arrays.equals(initialEditorContentParsedSHA256, calculateSHA256(""))) {
175+
return calculateSHA256(initialHTMLParsed)
176+
} else {
177+
return initialEditorContentParsedSHA256
178+
}
179+
} catch (e: Throwable) {
180+
// Do nothing here. `toPlainHtml` can throw exceptions, also calculateSHA256 -> NoSuchAlgorithmException
181+
}
182+
183+
return ByteArray(0)
184+
}
185+
186+
fun hasChanges(initialEditorContentParsedSHA256: ByteArray, newContent: String): EditorHasChanges {
187+
try {
188+
if (Arrays.equals(initialEditorContentParsedSHA256, calculateSHA256(newContent))) {
189+
return EditorHasChanges.NO_CHANGES
190+
}
191+
return EditorHasChanges.CHANGES
192+
} catch (e: Throwable) {
193+
// Do nothing here. `toPlainHtml` can throw exceptions, also calculateSHA256 -> NoSuchAlgorithmException
194+
return EditorHasChanges.UNKNOWN
195+
}
196+
}
163197
}
164198

165199
enum class EditorHasChanges {
@@ -175,7 +209,8 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
175209
private var consumeSelectionChangedEvent: Boolean = false
176210
private var isInlineTextHandlerEnabled: Boolean = true
177211
private var bypassObservationQueue: Boolean = false
178-
private var initialEditorContentParsedSHA256: ByteArray = ByteArray(0)
212+
213+
var initialEditorContentParsedSHA256: ByteArray = ByteArray(0)
179214

180215
private var onSelectionChangedListener: OnSelectionChangedListener? = null
181216
private var onImeBackListener: OnImeBackListener? = null
@@ -993,7 +1028,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
9931028

9941029
setSelection(cursorPosition)
9951030

996-
calculateInitialHTMLSHA()
1031+
initialEditorContentParsedSHA256 = calculateInitialHTMLSHA(toPlainHtml(false), initialEditorContentParsedSHA256)
9971032

9981033
loadImages()
9991034
loadVideos()
@@ -1069,37 +1104,8 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
10691104
}
10701105
}
10711106

1072-
private fun calculateInitialHTMLSHA() {
1073-
try {
1074-
// Do not recalculate the hash if it's not the first call to `fromHTML`.
1075-
if (initialEditorContentParsedSHA256.isEmpty() || Arrays.equals(initialEditorContentParsedSHA256, calculateSHA256(""))) {
1076-
val initialHTMLParsed = toPlainHtml(false)
1077-
initialEditorContentParsedSHA256 = calculateSHA256(initialHTMLParsed)
1078-
}
1079-
} catch (e: Throwable) {
1080-
// Do nothing here. `toPlainHtml` can throw exceptions, also calculateSHA256 -> NoSuchAlgorithmException
1081-
}
1082-
}
1083-
1084-
@Throws(NoSuchAlgorithmException::class)
1085-
private fun calculateSHA256(s: String): ByteArray {
1086-
val digest = MessageDigest.getInstance("SHA-256")
1087-
digest.update(s.toByteArray())
1088-
return digest.digest()
1089-
}
1090-
10911107
open fun hasChanges(): EditorHasChanges {
1092-
if (!initialEditorContentParsedSHA256.isEmpty()) {
1093-
try {
1094-
if (Arrays.equals(initialEditorContentParsedSHA256, calculateSHA256(toPlainHtml(false)))) {
1095-
return EditorHasChanges.NO_CHANGES
1096-
}
1097-
return EditorHasChanges.CHANGES
1098-
} catch (e: Throwable) {
1099-
// Do nothing here. `toPlainHtml` can throw exceptions, also calculateSHA256 -> NoSuchAlgorithmException
1100-
}
1101-
}
1102-
return EditorHasChanges.UNKNOWN
1108+
return hasChanges(initialEditorContentParsedSHA256, toPlainHtml(false))
11031109
}
11041110

11051111
// returns regular or "calypso" html depending on the mode

aztec/src/main/kotlin/org/wordpress/aztec/source/SourceViewEditText.kt

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import android.view.KeyEvent
1515
import android.view.MotionEvent
1616
import android.view.View
1717
import org.wordpress.aztec.AztecText
18+
import org.wordpress.aztec.AztecText.EditorHasChanges
1819
import org.wordpress.aztec.AztecTextAccessibilityDelegate
1920
import org.wordpress.aztec.History
2021
import org.wordpress.aztec.R
@@ -44,6 +45,8 @@ open class SourceViewEditText : android.support.v7.widget.AppCompatEditText, Tex
4445

4546
private var accessibilityDelegate = AztecTextAccessibilityDelegate(this)
4647

48+
private var initialEditorContentParsedSHA256: ByteArray = ByteArray(0)
49+
4750
constructor(context: Context) : super(context) {
4851
init(null)
4952
}
@@ -94,6 +97,7 @@ open class SourceViewEditText : android.support.v7.widget.AppCompatEditText, Tex
9497
visibility = customState.getInt("visibility")
9598
val retainedContent = InstanceStateUtils.readAndPurgeTempInstance<String>(RETAINED_CONTENT_KEY, "", savedState.state)
9699
setText(retainedContent)
100+
initialEditorContentParsedSHA256 = customState.getByteArray(AztecText.RETAINED_INITIAL_HTML_PARSED_SHA256_KEY)
97101
}
98102

99103
// Do not include the content of the editor when saving state to bundle.
@@ -106,6 +110,8 @@ open class SourceViewEditText : android.support.v7.widget.AppCompatEditText, Tex
106110

107111
override fun onSaveInstanceState(): Parcelable {
108112
val bundle = Bundle()
113+
bundle.putByteArray(org.wordpress.aztec.AztecText.RETAINED_INITIAL_HTML_PARSED_SHA256_KEY,
114+
initialEditorContentParsedSHA256)
109115
InstanceStateUtils.writeTempInstance(context, null, RETAINED_CONTENT_KEY, text.toString(), bundle)
110116
val superState = super.onSaveInstanceState()
111117
val savedState = SavedState(superState)
@@ -189,6 +195,8 @@ open class SourceViewEditText : android.support.v7.widget.AppCompatEditText, Tex
189195
disableTextChangedListener()
190196
val cursorPosition = consumeCursorTag(styledHtml)
191197
text = styledHtml
198+
initialEditorContentParsedSHA256 = AztecText.calculateInitialHTMLSHA(getPureHtml(false),
199+
initialEditorContentParsedSHA256)
192200
enableTextChangedListener()
193201

194202
if (cursorPosition > 0)
@@ -248,18 +256,27 @@ open class SourceViewEditText : android.support.v7.widget.AppCompatEditText, Tex
248256
return isThereClosingBracketBeforeOpeningBracket && isThereOpeningBracketBeforeClosingBracket
249257
}
250258

259+
fun hasChanges(): EditorHasChanges {
260+
return AztecText.hasChanges(initialEditorContentParsedSHA256, getPureHtml(false))
261+
}
262+
251263
fun getPureHtml(withCursorTag: Boolean = false): String {
264+
val str: String
265+
252266
if (withCursorTag) {
253-
disableTextChangedListener()
267+
val withCursor = StringBuffer(text)
254268
if (!isCursorInsideTag()) {
255-
text.insert(selectionEnd, "<aztec_cursor></aztec_cursor>")
269+
withCursor.insert(selectionEnd, "<aztec_cursor></aztec_cursor>")
256270
} else {
257-
text.insert(text.lastIndexOf("<", selectionEnd), "<aztec_cursor></aztec_cursor>")
271+
withCursor.insert(withCursor.lastIndexOf("<", selectionEnd), "<aztec_cursor></aztec_cursor>")
258272
}
259-
enableTextChangedListener()
273+
274+
str = withCursor.toString()
275+
} else {
276+
str = text.toString()
260277
}
261278

262-
return Format.removeSourceEditorFormatting(text.toString(), isInCalypsoMode)
279+
return Format.removeSourceEditorFormatting(str, isInCalypsoMode)
263280
}
264281

265282
fun disableTextChangedListener() {

aztec/src/main/kotlin/org/wordpress/aztec/toolbar/AztecToolbar.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import android.widget.Toast
2525
import android.widget.ToggleButton
2626
import org.wordpress.android.util.AppLog
2727
import org.wordpress.aztec.AztecText
28+
import org.wordpress.aztec.AztecText.EditorHasChanges.NO_CHANGES
2829
import org.wordpress.aztec.AztecTextFormat
2930
import org.wordpress.aztec.ITextFormat
3031
import org.wordpress.aztec.R
@@ -565,13 +566,17 @@ class AztecToolbar : FrameLayout, IAztecToolbar, OnMenuItemClickListener {
565566
if (sourceEditor == null) return
566567

567568
if (editor!!.visibility == View.VISIBLE) {
568-
sourceEditor!!.displayStyledAndFormattedHtml(editor!!.toPlainHtml(true))
569+
if (editor!!.hasChanges() != NO_CHANGES) {
570+
sourceEditor!!.displayStyledAndFormattedHtml(editor!!.toPlainHtml(true))
571+
}
569572
editor!!.visibility = View.GONE
570573
sourceEditor!!.visibility = View.VISIBLE
571574

572575
toggleHtmlMode(true)
573576
} else {
574-
editor!!.fromHtml(sourceEditor!!.getPureHtml(true))
577+
if (sourceEditor!!.hasChanges() != NO_CHANGES) {
578+
editor!!.fromHtml(sourceEditor!!.getPureHtml(true))
579+
}
575580
editor!!.visibility = View.VISIBLE
576581
sourceEditor!!.visibility = View.GONE
577582

0 commit comments

Comments
 (0)