Skip to content

Commit d25116c

Browse files
committed
Merge branch 'trunk' into feature/implement-delete-or-update
2 parents 41d4c11 + 1717409 commit d25116c

File tree

5 files changed

+170
-54
lines changed

5 files changed

+170
-54
lines changed

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

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2161,18 +2161,15 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
21612161
lineBlockFormatter.insertVideo(shouldAddMediaInline, drawable, attributes, onVideoTappedListener, onMediaDeletedListener)
21622162
}
21632163

2164-
fun removeMedia(notifyContentChange: Boolean = true, predicate: (Attributes) -> Boolean) {
2164+
fun removeMedia(predicate: (Attributes) -> Boolean) {
21652165
removeMedia(object : AttributePredicate {
21662166
override fun matches(attrs: Attributes): Boolean {
21672167
return predicate(attrs)
21682168
}
2169-
}, notifyContentChange)
2169+
})
21702170
}
21712171

2172-
fun removeMedia(attributePredicate: AttributePredicate, notifyContentChange: Boolean = true) {
2173-
if (!notifyContentChange) {
2174-
disableTextChangedListener()
2175-
}
2172+
fun removeMedia(attributePredicate: AttributePredicate) {
21762173
text.getSpans(0, text.length, AztecMediaSpan::class.java)
21772174
.filter {
21782175
attributePredicate.matches(it.attributes)
@@ -2220,8 +2217,33 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
22202217
}
22212218
mediaSpan.onMediaDeleted()
22222219
}
2223-
if (!notifyContentChange) {
2224-
enableTextChangedListener()
2220+
}
2221+
2222+
fun replaceMediaSpan(aztecMediaSpan: AztecMediaSpan, predicate: (Attributes) -> Boolean) {
2223+
replaceMediaSpan(object : AttributePredicate {
2224+
override fun matches(attrs: Attributes): Boolean {
2225+
return predicate(attrs)
2226+
}
2227+
}, aztecMediaSpan)
2228+
}
2229+
2230+
fun replaceMediaSpan(attributePredicate: AttributePredicate, aztecMediaSpan: AztecMediaSpan) {
2231+
history.beforeTextChanged(this@AztecText)
2232+
text.getSpans(0, text.length, AztecMediaSpan::class.java).firstOrNull {
2233+
attributePredicate.matches(it.attributes)
2234+
}?.let { mediaSpan ->
2235+
mediaSpan.beforeMediaDeleted()
2236+
val start = text.getSpanStart(mediaSpan)
2237+
val end = text.getSpanEnd(mediaSpan)
2238+
2239+
val clickableSpan = text.getSpans(start, end, AztecMediaClickableSpan::class.java).firstOrNull()
2240+
2241+
text.removeSpan(clickableSpan)
2242+
text.removeSpan(mediaSpan)
2243+
mediaSpan.onMediaDeleted()
2244+
aztecMediaSpan.onMediaDeletedListener = onMediaDeletedListener
2245+
lineBlockFormatter.insertMediaSpanOverCurrentChar(aztecMediaSpan, start)
2246+
contentChangeWatcher.notifyContentChanged()
22252247
}
22262248
}
22272249

aztec/src/main/kotlin/org/wordpress/aztec/formatting/LineBlockFormatter.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,24 @@ class LineBlockFormatter(editor: AztecText) : AztecFormatter(editor) {
150150
}
151151
}
152152

153+
fun insertMediaSpanOverCurrentChar(span: AztecMediaSpan, position: Int) {
154+
editor.removeInlineStylesFromRange(selectionStart, selectionEnd)
155+
156+
editor.editableText.setSpan(
157+
span,
158+
position,
159+
position + 1,
160+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
161+
)
162+
163+
editor.editableText.setSpan(
164+
AztecMediaClickableSpan(span),
165+
position,
166+
position + 1,
167+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
168+
)
169+
}
170+
153171
private fun insertMediaInline(span: AztecMediaSpan) {
154172
editor.removeInlineStylesFromRange(selectionStart, selectionEnd)
155173

media-placeholders/src/main/java/org/wordpress/aztec/placeholders/ImageWithCaptionAdapter.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,17 @@ class ImageWithCaptionAdapter(
8282
private const val SRC_ATTRIBUTE = "src"
8383

8484
suspend fun insertImageWithCaption(placeholderManager: PlaceholderManager, src: String, caption: String) {
85-
placeholderManager.insertOrUpdateItem(ADAPTER_TYPE) { currentAttributes, type ->
85+
placeholderManager.insertOrUpdateItem(ADAPTER_TYPE) { currentAttributes, type, placeAtStart ->
8686
if (currentAttributes == null || type != ADAPTER_TYPE) {
8787
mapOf(SRC_ATTRIBUTE to src, CAPTION_ATTRIBUTE to caption)
8888
} else {
8989
val currentCaption = currentAttributes[CAPTION_ATTRIBUTE]
90-
mapOf(SRC_ATTRIBUTE to src, CAPTION_ATTRIBUTE to "$caption - $currentCaption")
90+
val newCaption = if (placeAtStart) {
91+
"$caption - $currentCaption"
92+
} else {
93+
"$currentCaption - $caption"
94+
}
95+
mapOf(SRC_ATTRIBUTE to src, CAPTION_ATTRIBUTE to newCaption)
9196
}
9297
}
9398
}

media-placeholders/src/main/java/org/wordpress/aztec/placeholders/PlaceholderManager.kt

Lines changed: 82 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import org.wordpress.aztec.AztecContentChangeWatcher
2525
import org.wordpress.aztec.AztecText
2626
import org.wordpress.aztec.Constants
2727
import org.wordpress.aztec.Html
28+
import org.wordpress.aztec.plugins.html2visual.IHtmlPreprocessor
2829
import org.wordpress.aztec.plugins.html2visual.IHtmlTagHandler
2930
import org.wordpress.aztec.spans.AztecMediaClickableSpan
3031
import org.xml.sax.Attributes
@@ -52,7 +53,8 @@ class PlaceholderManager(
5253
Html.MediaCallback,
5354
AztecText.OnMediaDeletedListener,
5455
AztecText.OnVisibilityChangeListener,
55-
CoroutineScope {
56+
CoroutineScope,
57+
IHtmlPreprocessor {
5658
private val adapters = mutableMapOf<String, PlaceholderAdapter>()
5759
private val positionToIdMutex = Mutex()
5860
private val positionToId = mutableSetOf<Placeholder>()
@@ -109,49 +111,42 @@ class PlaceholderManager(
109111
* @param shouldMergeItem this method should return true when the previous type is compatible and should be updated
110112
* @param updateItem function to update current parameters with new params
111113
*/
112-
suspend fun insertOrUpdateItem(type: String, shouldMergeItem: (currentItemType: String) -> Boolean = { true }, updateItem: (currentAttributes: Map<String, String>?, currentType: String?) -> Map<String, String>) {
113-
val previousIndex = (aztecText.selectionStart - 1).coerceAtLeast(0)
114-
val indexBeforePrevious = (aztecText.selectionStart - 2).coerceAtLeast(0)
115-
val from = if (aztecText.editableText.length > previousIndex && aztecText.editableText[previousIndex] == Constants.IMG_CHAR) {
116-
previousIndex
117-
} else if (aztecText.editableText.length > previousIndex && aztecText.editableText[previousIndex] == '\n') {
118-
indexBeforePrevious
119-
} else {
120-
aztecText.selectionStart
121-
}
122-
val editableText = aztecText.editableText
123-
val currentItem = editableText.getSpans(
124-
from,
125-
aztecText.selectionStart,
126-
AztecPlaceholderSpan::class.java
127-
).lastOrNull()
128-
val currentType = currentItem?.attributes?.getValue(TYPE_ATTRIBUTE)
114+
suspend fun insertOrUpdateItem(
115+
type: String,
116+
shouldMergeItem: (currentItemType: String) -> Boolean = { true },
117+
updateItem: (
118+
currentAttributes: Map<String, String>?,
119+
currentType: String?,
120+
placeAtStart: Boolean
121+
) -> Map<String, String>
122+
) {
123+
val targetItem = getTargetItem()
124+
val targetSpan = targetItem?.span
125+
val currentType = targetSpan?.attributes?.getValue(TYPE_ATTRIBUTE)
129126
if (currentType != null && shouldMergeItem(currentType)) {
130-
updateSpan(type, currentItem, updateItem, currentType)
127+
updateSpan(type, targetItem.span, targetItem.placeAtStart, updateItem, currentType)
131128
} else {
132-
insertItem(type, *updateItem(null, null).toList().toTypedArray())
129+
insertItem(type, *updateItem(null, null, false).toList().toTypedArray())
133130
}
134131
}
135132

136133
private suspend fun updateSpan(
137134
type: String,
138-
currentItem: AztecPlaceholderSpan,
139-
updateItem: (currentAttributes: Map<String, String>, currentType: String) -> Map<String, String>,
135+
targetSpan: AztecPlaceholderSpan,
136+
placeAtStart: Boolean,
137+
updateItem: (currentAttributes: Map<String, String>, currentType: String, placeAtStart: Boolean) -> Map<String, String>,
140138
currentType: String
141139
) {
142140
val adapter = adapters[type]
143141
?: throw IllegalArgumentException("Adapter for inserted type not found. Register it with `registerAdapter` method")
144142
val currentAttributes = mutableMapOf<String, String>()
145-
val uuid = currentItem.attributes.getValue(UUID_ATTRIBUTE)
146-
for (i in 0 until currentItem.attributes.length) {
147-
val name = currentItem.attributes.getQName(i)
148-
val value = currentItem.attributes.getValue(name)
143+
val uuid = targetSpan.attributes.getValue(UUID_ATTRIBUTE)
144+
for (i in 0 until targetSpan.attributes.length) {
145+
val name = targetSpan.attributes.getQName(i)
146+
val value = targetSpan.attributes.getValue(name)
149147
currentAttributes[name] = value
150148
}
151-
val updatedAttributes = updateItem(currentAttributes, currentType)
152-
removeItem(false) { aztecAttributes ->
153-
aztecAttributes.getValue(UUID_ATTRIBUTE) == uuid
154-
}
149+
val updatedAttributes = updateItem(currentAttributes, currentType, placeAtStart)
155150
val attrs = AztecAttributes().apply {
156151
updatedAttributes.forEach { (key, value) ->
157152
setValue(key, value)
@@ -160,8 +155,11 @@ class PlaceholderManager(
160155
attrs.setValue(UUID_ATTRIBUTE, uuid)
161156
attrs.setValue(TYPE_ATTRIBUTE, type)
162157
val drawable = buildPlaceholderDrawable(adapter, attrs)
163-
aztecText.insertMediaSpan(AztecPlaceholderSpan(aztecText.context, drawable, 0, attrs,
164-
this, aztecText, WeakReference(adapter), TAG = htmlTag))
158+
val span = AztecPlaceholderSpan(aztecText.context, drawable, 0, attrs,
159+
this, aztecText, WeakReference(adapter), TAG = htmlTag)
160+
aztecText.replaceMediaSpan(span) { attributes ->
161+
attributes.getValue(UUID_ATTRIBUTE) == uuid
162+
}
165163
insertContentOverSpanWithId(uuid)
166164
}
167165

@@ -184,22 +182,57 @@ class PlaceholderManager(
184182
val selectionStart = aztecText.selectionStart
185183
val selectionEnd = aztecText.selectionEnd
186184
aztecText.setSelection(aztecText.editableText.getSpanStart(currentItem))
187-
updateSpan(type, currentItem, updateItem = { attributes, _ ->
185+
updateSpan(type, currentItem, updateItem = { attributes, _, _ ->
188186
updateItem(attributes)
189-
}, type)
187+
}, placeAtStart = false, currentType = type)
190188
aztecText.setSelection(selectionStart, selectionEnd)
191189
} else {
192190
removeItem(uuid)
193191
}
194192
return true
195193
}
196194

195+
private data class TargetItem(val span: AztecPlaceholderSpan, val placeAtStart: Boolean)
196+
197+
private fun getTargetItem(): TargetItem? {
198+
if (aztecText.length() == 0) {
199+
return null
200+
}
201+
val limitLength = aztecText.length() - 1
202+
val selectionStart = aztecText.selectionStart
203+
val selectionStartMinusOne = (selectionStart - 1).coerceIn(0, limitLength)
204+
val selectionStartMinusTwo = (selectionStart - 2).coerceIn(0, limitLength)
205+
val selectionEnd = aztecText.selectionEnd
206+
val selectionEndPlusOne = (selectionStart + 1).coerceIn(0, limitLength)
207+
val selectionEndPlusTwo = (selectionStart + 2).coerceIn(0, limitLength)
208+
val editableText = aztecText.editableText
209+
var placeAtStart = false
210+
val (from, to) = if (editableText[selectionStartMinusOne] == Constants.IMG_CHAR) {
211+
selectionStartMinusOne to selectionStart
212+
} else if (editableText[selectionStartMinusOne] == '\n' && editableText[selectionStartMinusTwo] == Constants.IMG_CHAR) {
213+
selectionStartMinusTwo to selectionStart
214+
} else if (editableText[selectionEndPlusOne] == Constants.IMG_CHAR) {
215+
placeAtStart = true
216+
selectionEndPlusOne to (selectionEndPlusOne + 1).coerceIn(0, limitLength)
217+
} else if (editableText[selectionEndPlusOne] == '\n' && editableText[selectionEndPlusTwo] == Constants.IMG_CHAR) {
218+
placeAtStart = true
219+
selectionEndPlusTwo to (selectionEndPlusTwo + 1).coerceIn(0, limitLength)
220+
} else {
221+
selectionStart to selectionEnd
222+
}
223+
return editableText.getSpans(
224+
from,
225+
to,
226+
AztecPlaceholderSpan::class.java
227+
).map { TargetItem(it, placeAtStart) }.lastOrNull()
228+
}
229+
197230
/**
198231
* Call this method to remove a placeholder from both the AztecText and the overlaying layer programmatically.
199232
* @param predicate determines whether a span should be removed
200233
*/
201-
fun removeItem(notifyContentChange: Boolean = true, predicate: (Attributes) -> Boolean) {
202-
aztecText.removeMedia(notifyContentChange) { predicate(it) }
234+
fun removeItem(predicate: (Attributes) -> Boolean) {
235+
aztecText.removeMedia { predicate(it) }
203236
}
204237

205238
/**
@@ -388,6 +421,12 @@ class PlaceholderManager(
388421
override fun handleTag(opening: Boolean, tag: String, output: Editable, attributes: Attributes, nestingLevel: Int): Boolean {
389422
if (opening) {
390423
val type = attributes.getValue(TYPE_ATTRIBUTE)
424+
attributes.getValue(UUID_ATTRIBUTE)?.also { uuid ->
425+
container.findViewWithTag<View>(uuid)?.let {
426+
it.visibility = View.GONE
427+
container.removeView(it)
428+
}
429+
}
391430
val adapter = adapters[type] ?: return false
392431
val aztecAttributes = AztecAttributes(attributes)
393432
aztecAttributes.setValue(UUID_ATTRIBUTE, generateUuid())
@@ -595,4 +634,11 @@ class PlaceholderManager(
595634
private const val TYPE_ATTRIBUTE = "type"
596635
private const val EDITOR_INNER_PADDING = 20
597636
}
637+
638+
override fun beforeHtmlProcessed(source: String): String {
639+
runBlocking {
640+
clearAllViews()
641+
}
642+
return source
643+
}
598644
}

media-placeholders/src/test/java/org/wordpress/aztec/placeholders/PlaceholderTest.kt

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import org.junit.Test
1212
import org.junit.runner.RunWith
1313
import org.robolectric.Robolectric
1414
import org.robolectric.RobolectricTestRunner
15-
import org.wordpress.aztec.AztecAttributes
1615
import org.wordpress.aztec.AztecText
1716
import org.wordpress.aztec.source.SourceViewEditText
1817
import org.wordpress.aztec.toolbar.AztecToolbar
@@ -64,8 +63,6 @@ class PlaceholderTest {
6463
editText.fromHtml(initialHtml)
6564

6665
editText.setSelection(0)
67-
val attributes = AztecAttributes()
68-
attributes.setValue("id", "1234")
6966
ImageWithCaptionAdapter.insertImageWithCaption(placeholderManager, "image.jpg", "Caption 123")
7067

7168
Assert.assertEquals("<placeholder uuid=\"$uuid\" type=\"image_with_caption\" src=\"image.jpg\" caption=\"Caption 123\" /><p>Line 1</p>", editText.toHtml())
@@ -86,8 +83,6 @@ class PlaceholderTest {
8683
editText.fromHtml(initialHtml)
8784

8885
editText.setSelection(editText.editableText.indexOf("1"))
89-
val attributes = AztecAttributes()
90-
attributes.setValue("id", "1234")
9186
ImageWithCaptionAdapter.insertImageWithCaption(placeholderManager, "image.jpg", "Caption 123")
9287

9388
Assert.assertEquals("<p>Line 123<placeholder uuid=\"uuid123\" type=\"image_with_caption\" src=\"image.jpg\" caption=\"Caption 123\" /></p><p>Line 2</p>", editText.toHtml())
@@ -108,12 +103,10 @@ class PlaceholderTest {
108103
editText.fromHtml(initialHtml)
109104

110105
editText.setSelection(0)
111-
val attributes = AztecAttributes()
112-
attributes.setValue("id", "1234")
113106
ImageWithCaptionAdapter.insertImageWithCaption(placeholderManager, "image.jpg", "Caption 1")
114107
ImageWithCaptionAdapter.insertImageWithCaption(placeholderManager, "image.jpg", "Caption 2")
115108

116-
Assert.assertEquals("<placeholder src=\"image.jpg\" caption=\"Caption 2 - Caption 1\" uuid=\"uuid123\" type=\"image_with_caption\" /><p>Line 1</p>", editText.toHtml())
109+
Assert.assertEquals("${placeholderWithCaption("Caption 1 - Caption 2")}<p>Line 1</p>", editText.toHtml())
117110

118111
placeholderManager.removeItem {
119112
it.getValue("uuid") == uuid
@@ -123,6 +116,38 @@ class PlaceholderTest {
123116
}
124117
}
125118

119+
@Test
120+
@Throws(Exception::class)
121+
fun insertOrUpdateAPlaceholderWhenInsertingBeforeNewLine() {
122+
runBlocking {
123+
val initialHtml = "<p>Line 1</p>${placeholderWithCaption("First")}<p>Line 2</p>"
124+
editText.fromHtml(initialHtml)
125+
126+
editText.setSelection(editText.editableText.indexOf("1"))
127+
ImageWithCaptionAdapter.insertImageWithCaption(placeholderManager, "image.jpg", "Second")
128+
129+
Assert.assertEquals("<p>Line 1</p>${placeholderWithCaption("Second - First")}<p>Line 2</p>", editText.toHtml())
130+
}
131+
}
132+
133+
@Test
134+
@Throws(Exception::class)
135+
fun insertOrUpdateAPlaceholderWhenInsertingRightBefore() {
136+
runBlocking {
137+
val initialHtml = "<p>Line 1</p>${placeholderWithCaption("First")}<p>Line 2</p>"
138+
editText.fromHtml(initialHtml)
139+
140+
editText.setSelection(editText.editableText.indexOf("1") + 1)
141+
ImageWithCaptionAdapter.insertImageWithCaption(placeholderManager, "image.jpg", "Second")
142+
143+
Assert.assertEquals("<p>Line 1</p>${placeholderWithCaption("Second - First")}<p>Line 2</p>", editText.toHtml())
144+
}
145+
}
146+
147+
private fun placeholderWithCaption(caption: String): String {
148+
return "<placeholder src=\"image.jpg\" caption=\"$caption\" uuid=\"uuid123\" type=\"image_with_caption\" />"
149+
}
150+
126151
@Test
127152
@Throws(Exception::class)
128153
fun updatePlaceholderWhenItShouldBe() {

0 commit comments

Comments
 (0)