Skip to content

Commit 4f42371

Browse files
committed
Merge branch 'trunk' into feature/item-indent
2 parents f1a9a35 + e331b7b commit 4f42371

File tree

8 files changed

+355
-39
lines changed

8 files changed

+355
-39
lines changed

README.md

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<h1><img align="center" width=50px height=50px src="https://github.com/wordpress-mobile/AztecEditor-iOS/raw/develop/RepoAssets/aztec.png" alt="Aztec Logo"/>&nbsp;Aztec: Native HTML Editor for Android</h1>
1+
<h1><img align="center" width=50px height=50px src="https://github.com/wordpress-mobile/AztecEditor-Android/raw/trunk/RepoAssets/aztec.png" alt="Aztec Logo"/>&nbsp;Aztec: Native HTML Editor for Android</h1>
22

33
[![CircleCI](https://circleci.com/gh/wordpress-mobile/AztecEditor-Android.svg?style=svg)](https://circleci.com/gh/wordpress-mobile/AztecEditor-Android)
44

@@ -7,7 +7,7 @@ documents in Android.
77

88
Supports Android 4.1+ (API 16 - Jelly Bean)
99

10-
<img align="center" width=360px height=640px src="https://github.com/wordpress-mobile/AztecEditor-Android/raw/develop/visual_editor.png" alt="Visual Editor"/> <img align="center" width=360px height=640px src="https://github.com/wordpress-mobile/AztecEditor-Android/raw/develop/code_editor.png" alt="Visual Editor"/>
10+
<img align="center" width=360px height=640px src="https://github.com/wordpress-mobile/AztecEditor-Android/raw/trunk/visual_editor.png" alt="Visual Editor"/> <img align="center" width=360px height=640px src="https://github.com/wordpress-mobile/AztecEditor-Android/raw/trunk/code_editor.png" alt="Visual Editor"/>
1111

1212
## Getting started
1313

@@ -59,7 +59,7 @@ Aztec.with(visualEditor, sourceEditor, toolbar, context)
5959
.setVideoThumbnailGetter(GlideVideoThumbnailLoader(context))
6060
```
6161

62-
For more options, such as edit history, listeners and plugins please refer to the [demo app implementation](https://github.com/wordpress-mobile/AztecEditor-Android/blob/develop/app/src/main/kotlin/org/wordpress/aztec/demo/MainActivity.kt).
62+
For more options, such as edit history, listeners and plugins please refer to the [demo app implementation](https://github.com/wordpress-mobile/AztecEditor-Android/blob/trunk/app/src/main/kotlin/org/wordpress/aztec/demo/MainActivity.kt).
6363

6464
## Build and test
6565

@@ -119,6 +119,39 @@ dependencies {
119119
}
120120
```
121121

122+
## Modifications
123+
124+
You can use the API to modify Aztec behaviour.
125+
126+
### Toolbar items
127+
128+
If you want to limit the functionality the Aztec library provides, you can change it calling the `setToolbarItems` method on `AztecToolbar`.
129+
The following example will enable only `bold`, `plugins` and `list` items in the given order.
130+
131+
```kotlin
132+
aztecToolbar.setToolbarItems(ToolbarItems.BasicLayout(ToolbarAction.BOLD, ToolbarItems.PLUGINS, ToolbarAction.LIST))
133+
```
134+
135+
You can set new items which are not enabled by default. `ToolbarAction.CODE` and `ToolbarAction.PRE`.
136+
- `CODE` represents inline HTML code
137+
- `PRE` represents a preformat block (including code block)
138+
139+
### Task list
140+
141+
There is an optional list type you can enable in the editor. In addition to ordered and unordered lists you can use `task list`.
142+
A task list is an unordered list which shows and saves checkboxes instead of the bullets. Enable it by calling the following method.
143+
```kotlin
144+
aztecToolbar.enableTaskList()
145+
```
146+
147+
### Nested blocks
148+
149+
By default Aztec allows nested blocks. In certain cases this doesn't have to be the preferred behaviour. There is an option to disable nested blocks.
150+
When switched, this editor will always add media and horizontal rule after the currently selected block, not in the middle of it.
151+
```kotlin
152+
aztecText.addMediaAfterBlocks()
153+
```
154+
122155
## Code formatting
123156

124157
We use [ktlint](https://github.com/shyiko/ktlint) for Kotlin linting. You can run ktlint using `./gradlew ktlint`, and you can also run `./gradlew ktlintFormat` for auto-formatting. There is no IDEA plugin (like Checkstyle's) at this time.

RepoAssets/aztec.png

6.32 KB
Loading

app/src/main/kotlin/org/wordpress/aztec/demo/MainActivity.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -429,8 +429,6 @@ open class MainActivity : AppCompatActivity(),
429429
}
430430
})
431431

432-
toolbar.enableTaskList()
433-
434432
aztec = Aztec.with(visualEditor, sourceEditor, toolbar, this)
435433
.setImageGetter(GlideImageLoader(this))
436434
.setVideoThumbnailGetter(GlideVideoThumbnailLoader(this))

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,9 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
254254
var isInCalypsoMode = true
255255
var isInGutenbergMode: Boolean = false
256256
val alignmentRendering: AlignmentRendering
257+
// If this field is true, the media and horizontal line are added inline. If it's false, they are added after the
258+
// current block.
259+
var shouldAddMediaInline: Boolean = true
257260

258261
var consumeHistoryEvent: Boolean = false
259262

@@ -369,6 +372,15 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
369372
isInGutenbergMode = isCompatibleWithGutenberg
370373
}
371374

375+
/**
376+
* Default behaviour is to add media and horizontal rule inline. They could be added inside lists and quotes.
377+
* If you call this method, this behaviour will be disabled and the media and horizontal rule will be automatically
378+
* added after the currently selected block. For example after the list in which you have your cursor.
379+
*/
380+
fun addMediaAfterBlocks() {
381+
this.shouldAddMediaInline = false
382+
}
383+
372384
// Newer AppCompatEditText returns Editable?, and using that would require changing all of Aztec to not use `text.`
373385
override fun getText(): Editable {
374386
return super.getText()!!
@@ -685,6 +697,9 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
685697
history.beforeTextChanged(this@AztecText)
686698
}
687699
wasStyleRemoved = blockFormatter.tryRemoveBlockStyleFromFirstLine()
700+
if (!shouldAddMediaInline) {
701+
blockFormatter.moveSelectionIfImageSelected()
702+
}
688703

689704
if (selectionStart == 0 || selectionEnd == 0) {
690705
deleteInlineStyleFromTheBeginning()
@@ -1173,7 +1188,9 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
11731188
AztecTextFormat.FORMAT_ALIGN_RIGHT -> return blockFormatter.toggleTextAlignment(textFormat)
11741189
AztecTextFormat.FORMAT_PREFORMAT -> blockFormatter.togglePreformat()
11751190
AztecTextFormat.FORMAT_QUOTE -> blockFormatter.toggleQuote()
1176-
AztecTextFormat.FORMAT_HORIZONTAL_RULE -> lineBlockFormatter.applyHorizontalRule()
1191+
AztecTextFormat.FORMAT_HORIZONTAL_RULE -> {
1192+
lineBlockFormatter.applyHorizontalRule(shouldAddMediaInline)
1193+
}
11771194
else -> {
11781195
plugins.filter { it is IToolbarButton && it.action.textFormats.contains(textFormat) }
11791196
.map { it as IToolbarButton }
@@ -1962,11 +1979,11 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
19621979
}
19631980

19641981
fun insertImage(drawable: Drawable?, attributes: Attributes) {
1965-
lineBlockFormatter.insertImage(drawable, attributes, onImageTappedListener, onMediaDeletedListener)
1982+
lineBlockFormatter.insertImage(shouldAddMediaInline, drawable, attributes, onImageTappedListener, onMediaDeletedListener)
19661983
}
19671984

19681985
fun insertVideo(drawable: Drawable?, attributes: Attributes) {
1969-
lineBlockFormatter.insertVideo(drawable, attributes, onVideoTappedListener, onMediaDeletedListener)
1986+
lineBlockFormatter.insertVideo(shouldAddMediaInline, drawable, attributes, onVideoTappedListener, onMediaDeletedListener)
19701987
}
19711988

19721989
fun removeMedia(attributePredicate: AttributePredicate) {

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ import org.wordpress.aztec.handlers.BlockHandler
1616
import org.wordpress.aztec.handlers.HeadingHandler
1717
import org.wordpress.aztec.handlers.ListItemHandler
1818
import org.wordpress.aztec.spans.AztecHeadingSpan
19+
import org.wordpress.aztec.spans.AztecHorizontalRuleSpan
1920
import org.wordpress.aztec.spans.AztecListItemSpan
2021
import org.wordpress.aztec.spans.AztecListSpan
22+
import org.wordpress.aztec.spans.AztecMediaSpan
2123
import org.wordpress.aztec.spans.AztecOrderedListSpan
2224
import org.wordpress.aztec.spans.AztecPreformatSpan
2325
import org.wordpress.aztec.spans.AztecQuoteSpan
@@ -307,6 +309,23 @@ class BlockFormatter(editor: AztecText,
307309
}
308310
}
309311

312+
fun moveSelectionIfImageSelected() {
313+
if (selectionStart == selectionEnd &&
314+
(hasImageRightAfterSelection() || hasHorizontalRuleRightAfterSelection())) {
315+
editor.setSelection(selectionStart - 1)
316+
}
317+
}
318+
319+
private fun hasImageRightAfterSelection() =
320+
editableText.getSpans(selectionStart, selectionEnd, AztecMediaSpan::class.java).any {
321+
editableText.getSpanStart(it) == selectionStart
322+
}
323+
324+
private fun hasHorizontalRuleRightAfterSelection() =
325+
editableText.getSpans(selectionStart, selectionEnd, AztecHorizontalRuleSpan::class.java).any {
326+
editableText.getSpanStart(it) == selectionStart
327+
}
328+
310329
fun removeBlockStyle(textFormat: ITextFormat) {
311330
removeBlockStyle(textFormat, selectionStart, selectionEnd, makeBlock(textFormat, 0).map { it -> it.javaClass })
312331
}

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

Lines changed: 111 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import android.text.TextUtils
77
import androidx.appcompat.content.res.AppCompatResources
88
import org.wordpress.aztec.AztecAttributes
99
import org.wordpress.aztec.AztecText
10-
import org.wordpress.aztec.AztecText.OnImageTappedListener
11-
import org.wordpress.aztec.AztecText.OnVideoTappedListener
1210
import org.wordpress.aztec.AztecTextFormat
1311
import org.wordpress.aztec.Constants
1412
import org.wordpress.aztec.ITextFormat
@@ -19,10 +17,10 @@ import org.wordpress.aztec.spans.AztecImageSpan
1917
import org.wordpress.aztec.spans.AztecMediaClickableSpan
2018
import org.wordpress.aztec.spans.AztecMediaSpan
2119
import org.wordpress.aztec.spans.AztecVideoSpan
20+
import org.wordpress.aztec.spans.IAztecBlockSpan
2221
import org.wordpress.aztec.spans.IAztecNestable
2322
import org.wordpress.aztec.watchers.EndOfBufferMarkerAdder
2423
import org.xml.sax.Attributes
25-
import java.util.ArrayList
2624

2725
class LineBlockFormatter(editor: AztecText) : AztecFormatter(editor) {
2826

@@ -95,11 +93,14 @@ class LineBlockFormatter(editor: AztecText) : AztecFormatter(editor) {
9593
return false
9694
}
9795

98-
fun applyHorizontalRule() {
99-
editor.removeInlineStylesFromRange(selectionStart, selectionEnd)
100-
editor.removeBlockStylesFromRange(selectionStart, selectionEnd, true)
101-
102-
val nestingLevel = IAztecNestable.getNestingLevelAt(editableText, selectionStart)
96+
fun applyHorizontalRule(inline: Boolean) {
97+
val nestingLevel = if (inline) {
98+
editor.removeInlineStylesFromRange(selectionStart, selectionEnd)
99+
editor.removeBlockStylesFromRange(selectionStart, selectionEnd, true)
100+
IAztecNestable.getNestingLevelAt(editableText, selectionStart)
101+
} else {
102+
0
103+
}
103104

104105
val span = AztecHorizontalRuleSpan(
105106
editor.context,
@@ -112,34 +113,108 @@ class LineBlockFormatter(editor: AztecText) : AztecFormatter(editor) {
112113
val builder = SpannableStringBuilder(Constants.MAGIC_STRING)
113114
builder.setSpan(span, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
114115

115-
val start = selectionStart
116-
editableText.replace(start, selectionEnd, builder)
117-
118-
val newSelectionPosition = editableText.indexOf(Constants.MAGIC_CHAR, start) + 1
119-
editor.setSelection(newSelectionPosition)
116+
if (inline) {
117+
editableText.replace(selectionStart, selectionEnd, builder)
118+
val newSelectionPosition = editableText.indexOf(Constants.MAGIC_CHAR, selectionStart) + 1
119+
editor.setSelection(newSelectionPosition)
120+
} else {
121+
insertSpanAfterBlock(builder)
122+
}
120123
}
121124

122-
fun insertVideo(drawable: Drawable?, attributes: Attributes, onVideoTappedListener: OnVideoTappedListener?,
125+
fun insertVideo(inline: Boolean, drawable: Drawable?, attributes: Attributes, onVideoTappedListener: AztecText.OnVideoTappedListener?,
123126
onMediaDeletedListener: AztecText.OnMediaDeletedListener?) {
124-
val nestingLevel = IAztecNestable.getNestingLevelAt(editableText, selectionStart)
127+
val nestingLevel = if (inline) IAztecNestable.getNestingLevelAt(editableText, selectionStart) else 0
125128
val span = AztecVideoSpan(editor.context, drawable, nestingLevel, AztecAttributes(attributes), onVideoTappedListener,
126129
onMediaDeletedListener, editor)
127-
insertMedia(span)
130+
if (inline) {
131+
insertMediaInline(span)
132+
} else {
133+
insertMediaAfterBlock(span)
134+
}
128135
}
129136

130-
fun insertImage(drawable: Drawable?, attributes: Attributes, onImageTappedListener: OnImageTappedListener?,
137+
fun insertImage(inline: Boolean, drawable: Drawable?, attributes: Attributes, onImageTappedListener: AztecText.OnImageTappedListener?,
131138
onMediaDeletedListener: AztecText.OnMediaDeletedListener?) {
132-
val nestingLevel = IAztecNestable.getNestingLevelAt(editableText, selectionStart)
139+
val nestingLevel = if (inline) IAztecNestable.getNestingLevelAt(editableText, selectionStart) else 0
133140
val span = AztecImageSpan(editor.context, drawable, nestingLevel, AztecAttributes(attributes), onImageTappedListener,
134141
onMediaDeletedListener, editor)
135-
insertMedia(span)
142+
if (inline) {
143+
insertMediaInline(span)
144+
} else {
145+
insertMediaAfterBlock(span)
146+
}
136147
}
137148

138-
private fun insertMedia(span: AztecMediaSpan) {
149+
private fun insertMediaInline(span: AztecMediaSpan) {
139150
editor.removeInlineStylesFromRange(selectionStart, selectionEnd)
140151

141152
val ssb = SpannableStringBuilder(Constants.IMG_STRING)
142153

154+
buildClickableMediaSpan(ssb, span)
155+
156+
// We need to be sure the cursor is placed correctly after media insertion
157+
// Note that media has '\n' around them when needed
158+
val isLastItem = selectionEnd == EndOfBufferMarkerAdder.safeLength(editor)
159+
editableText.replace(selectionStart, selectionEnd, ssb)
160+
161+
setSelection(isLastItem, selectionEnd)
162+
}
163+
164+
private fun insertMediaAfterBlock(span: AztecMediaSpan) {
165+
val ssb = SpannableStringBuilder(Constants.IMG_STRING)
166+
buildClickableMediaSpan(ssb, span)
167+
insertSpanAfterBlock(ssb)
168+
}
169+
170+
private fun insertSpanAfterBlock(ssb: SpannableStringBuilder) {
171+
val position = getEndOfBlock()
172+
// We need to be sure the cursor is placed correctly after media insertion
173+
// Note that media has '\n' around them when needed
174+
val isLastItem = position == EndOfBufferMarkerAdder.safeLength(editor)
175+
if (isLastItem) {
176+
editableText.getSpans(position, editableText.length, IAztecBlockSpan::class.java).filter {
177+
it !is AztecMediaSpan && editableText.getSpanEnd(it) == editableText.length
178+
}.map {
179+
SpanData(it, editableText.getSpanStart(it), position + 1, editableText.getSpanFlags(it))
180+
}.applyWithRemovedSpans {
181+
editableText.append(ssb)
182+
}
183+
} else {
184+
ssb.append("\n")
185+
val ssbLength = ssb.length
186+
editableText.getSpans(position, position + ssbLength, IAztecBlockSpan::class.java).filter {
187+
it !is AztecMediaSpan && editableText.getSpanStart(it) == position
188+
}.map {
189+
SpanData(it, editableText.getSpanStart(it) + ssbLength, editableText.getSpanEnd(it) + ssbLength, editableText.getSpanFlags(it))
190+
}.applyWithRemovedSpans {
191+
editableText.insert(position, ssb)
192+
}
193+
}
194+
setSelection(isLastItem, position)
195+
}
196+
197+
private fun List<SpanData>.applyWithRemovedSpans(action: () -> Unit) {
198+
this.onEach { editableText.removeSpan(it.span) }
199+
action()
200+
this.onEach {
201+
editableText.setSpan(it.span, it.spanStart, it.spanEnd, it.spanFlags)
202+
}
203+
}
204+
205+
data class SpanData(val span: IAztecBlockSpan, val spanStart: Int, val spanEnd: Int, val spanFlags: Int)
206+
207+
private fun setSelection(isLastItem: Boolean, position: Int) {
208+
val newSelection = if (isLastItem) {
209+
EndOfBufferMarkerAdder.safeLength(editor)
210+
} else {
211+
if (position < EndOfBufferMarkerAdder.safeLength(editor)) position + 1 else position
212+
}
213+
editor.setSelection(newSelection)
214+
editor.isMediaAdded = true
215+
}
216+
217+
private fun buildClickableMediaSpan(ssb: SpannableStringBuilder, span: AztecMediaSpan) {
143218
ssb.setSpan(
144219
span,
145220
0,
@@ -153,18 +228,23 @@ class LineBlockFormatter(editor: AztecText) : AztecFormatter(editor) {
153228
1,
154229
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
155230
)
231+
}
156232

157-
// We need to be sure the cursor is placed correctly after media insertion
158-
// Note that media has '\n' around them when needed
159-
val isLastItem = selectionEnd == EndOfBufferMarkerAdder.safeLength(editor)
160-
editableText.replace(selectionStart, selectionEnd, ssb)
161-
162-
val newSelection = if (isLastItem) {
163-
EndOfBufferMarkerAdder.safeLength(editor)
164-
} else {
165-
if (selectionEnd < EndOfBufferMarkerAdder.safeLength(editor)) selectionEnd + 1 else selectionEnd
233+
private fun getEndOfBlock(): Int {
234+
if (selectionStart == 0 && selectionEnd == 0) {
235+
return 0
166236
}
167-
editor.setSelection(newSelection)
168-
editor.isMediaAdded = true
237+
var position = 0
238+
editableText.getSpans(selectionStart, selectionEnd, IAztecBlockSpan::class.java).forEach {
239+
val spanEnd = editableText.getSpanEnd(it)
240+
if (spanEnd > position) {
241+
position = spanEnd
242+
}
243+
}
244+
if (position <= 0 && selectionEnd != 0) {
245+
// If the text contains "\n" return that as the position, else set the position to the end of the text
246+
position = editableText.indexOf("\n", selectionEnd).takeIf { it >= 0 } ?: editableText.length
247+
}
248+
return position
169249
}
170250
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,9 @@ class AztecToolbar : FrameLayout, IAztecToolbar, OnMenuItemClickListener {
753753
this.toolbarItems = toolbarItems
754754
}
755755

756+
/**
757+
* Call this method to enable a task list with checkboxes
758+
*/
756759
fun enableTaskList() {
757760
this.tasklistEnabled = true
758761
}

0 commit comments

Comments
 (0)