Skip to content

Commit 9a93d2a

Browse files
authored
Merge pull request #960 from wordpress-mobile/feature/add-task-list
Add support for a task list
2 parents f844bfd + d200277 commit 9a93d2a

23 files changed

+1468
-27
lines changed

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

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

433+
toolbar.enableTaskList()
434+
433435
aztec = Aztec.with(visualEditor, sourceEditor, toolbar, this)
434436
.setImageGetter(GlideImageLoader(this))
435437
.setVideoThumbnailGetter(GlideVideoThumbnailLoader(this))
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.wordpress.aztec
2+
3+
import org.xml.sax.Attributes
4+
5+
fun Attributes.isTaskList() = this.getValue(TYPE) == TASK_LIST_TYPE
6+
fun AztecAttributes.setTaskList() {
7+
if (!this.hasAttribute(TYPE)) {
8+
this.setValue(TYPE, TASK_LIST_TYPE)
9+
}
10+
}
11+
private const val TYPE = "type"
12+
private const val TASK_LIST_TYPE = "task-list"

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

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,12 @@ import org.wordpress.aztec.plugins.html2visual.IHtmlTagHandler
3131
import org.wordpress.aztec.spans.AztecAudioSpan
3232
import org.wordpress.aztec.spans.AztecHorizontalRuleSpan
3333
import org.wordpress.aztec.spans.AztecImageSpan
34+
import org.wordpress.aztec.spans.AztecListItemSpan
35+
import org.wordpress.aztec.spans.AztecListItemSpan.Companion.CHECKED
3436
import org.wordpress.aztec.spans.AztecMediaClickableSpan
3537
import org.wordpress.aztec.spans.AztecMediaSpan
3638
import org.wordpress.aztec.spans.AztecStrikethroughSpan
39+
import org.wordpress.aztec.spans.AztecTaskListSpan
3740
import org.wordpress.aztec.spans.AztecVideoSpan
3841
import org.wordpress.aztec.spans.HiddenHtmlSpan
3942
import org.wordpress.aztec.spans.IAztecAttributedSpan
@@ -46,10 +49,10 @@ import org.wordpress.aztec.spans.createListItemSpan
4649
import org.wordpress.aztec.spans.createOrderedListSpan
4750
import org.wordpress.aztec.spans.createParagraphSpan
4851
import org.wordpress.aztec.spans.createPreformatSpan
52+
import org.wordpress.aztec.spans.createTaskListSpan
4953
import org.wordpress.aztec.spans.createUnorderedListSpan
5054
import org.wordpress.aztec.util.getLast
5155
import org.xml.sax.Attributes
52-
import java.util.ArrayList
5356

5457
class AztecTagHandler(val context: Context, val plugins: List<IAztecPlugin> = ArrayList(), private val alignmentRendering: AlignmentRendering
5558
) : Html.TagHandler {
@@ -93,7 +96,13 @@ class AztecTagHandler(val context: Context, val plugins: List<IAztecPlugin> = Ar
9396
return true
9497
}
9598
LIST_UL -> {
96-
handleElement(output, opening, createUnorderedListSpan(nestingLevel, alignmentRendering, AztecAttributes(attributes)))
99+
val lastSpan = tagStack.lastOrNull()
100+
val element = if (attributes.isTaskList() || !opening && lastSpan is AztecTaskListSpan) {
101+
createTaskListSpan(nestingLevel, alignmentRendering, AztecAttributes(attributes), context)
102+
} else {
103+
createUnorderedListSpan(nestingLevel, alignmentRendering, AztecAttributes(attributes))
104+
}
105+
handleElement(output, opening, element)
97106
return true
98107
}
99108
LIST_OL -> {
@@ -144,6 +153,12 @@ class AztecTagHandler(val context: Context, val plugins: List<IAztecPlugin> = Ar
144153
handleElement(output, opening, preformatSpan)
145154
return true
146155
}
156+
INPUT -> {
157+
if (opening && attributes.getValue("type") == "checkbox") {
158+
return handleCheckboxInput(attributes)
159+
}
160+
return false
161+
}
147162
else -> {
148163
if (tag.length == 2 && Character.toLowerCase(tag[0]) == 'h' && tag[1] >= '1' && tag[1] <= '6') {
149164
handleElement(output, opening, createHeadingSpan(nestingLevel, tag, AztecAttributes(attributes), alignmentRendering))
@@ -154,6 +169,20 @@ class AztecTagHandler(val context: Context, val plugins: List<IAztecPlugin> = Ar
154169
return false
155170
}
156171

172+
/**
173+
* This method takes the checkbox input inside a list item and applies a parameter to the parent list item.
174+
* We convert <li><input type=checkbox checked />Test</li>
175+
* into something like this: <li checked="true">Test</li>
176+
* We convert this back when we generate HTML
177+
*/
178+
private fun handleCheckboxInput(attributes: Attributes): Boolean {
179+
val wrappingListItem = tagStack.lastOrNull() as? AztecListItemSpan ?: return false
180+
val checkedAttribute = attributes.getValue("checked")
181+
val isChecked = checkedAttribute != null && checkedAttribute != "false"
182+
wrappingListItem.attributes.setValue(CHECKED, isChecked.toString())
183+
return true
184+
}
185+
157186
private fun processTagHandlerPlugins(tag: String, opening: Boolean, output: Editable, attributes: Attributes, nestingLevel: Int): Boolean {
158187
plugins.filter { it is IHtmlTagHandler }
159188
.map { it as IHtmlTagHandler }
@@ -241,6 +270,7 @@ class AztecTagHandler(val context: Context, val plugins: List<IAztecPlugin> = Ar
241270
private val BLOCKQUOTE = "blockquote"
242271
private val PARAGRAPH = "p"
243272
private val PREFORMAT = "pre"
273+
private val INPUT = "input"
244274
private val IMAGE = "img"
245275
private val VIDEO = "video"
246276
private val AUDIO = "audio"

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

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ import org.wordpress.aztec.spans.AztecImageSpan
8686
import org.wordpress.aztec.spans.AztecListItemSpan
8787
import org.wordpress.aztec.spans.AztecMediaClickableSpan
8888
import org.wordpress.aztec.spans.AztecMediaSpan
89+
import org.wordpress.aztec.spans.AztecTaskListSpan
90+
import org.wordpress.aztec.spans.AztecTaskListSpanAligned
8991
import org.wordpress.aztec.spans.AztecURLSpan
9092
import org.wordpress.aztec.spans.AztecVideoSpan
9193
import org.wordpress.aztec.spans.AztecVisualLinebreak
@@ -125,7 +127,6 @@ import org.wordpress.aztec.watchers.event.text.TextWatcherEvent
125127
import org.xml.sax.Attributes
126128
import java.security.MessageDigest
127129
import java.security.NoSuchAlgorithmException
128-
import java.util.ArrayList
129130
import java.util.Arrays
130131
import java.util.LinkedList
131132

@@ -1149,6 +1150,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
11491150
AztecTextFormat.FORMAT_BOLD,
11501151
AztecTextFormat.FORMAT_STRONG -> inlineFormatter.toggleAny(ToolbarAction.BOLD.textFormats)
11511152
AztecTextFormat.FORMAT_UNORDERED_LIST -> blockFormatter.toggleUnorderedList()
1153+
AztecTextFormat.FORMAT_TASK_LIST -> blockFormatter.toggleTaskList()
11521154
AztecTextFormat.FORMAT_ORDERED_LIST -> blockFormatter.toggleOrderedList()
11531155
AztecTextFormat.FORMAT_ALIGN_LEFT,
11541156
AztecTextFormat.FORMAT_ALIGN_CENTER,
@@ -1184,6 +1186,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
11841186
AztecTextFormat.FORMAT_MARK,
11851187
AztecTextFormat.FORMAT_CODE -> return inlineFormatter.containsInlineStyle(format, selStart, selEnd)
11861188
AztecTextFormat.FORMAT_UNORDERED_LIST,
1189+
AztecTextFormat.FORMAT_TASK_LIST,
11871190
AztecTextFormat.FORMAT_ORDERED_LIST -> return blockFormatter.containsList(format, selStart, selEnd)
11881191
AztecTextFormat.FORMAT_ALIGN_LEFT,
11891192
AztecTextFormat.FORMAT_ALIGN_CENTER,
@@ -1511,6 +1514,8 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
15111514
it.onUnknownHtmlTappedListener = this
15121515
}
15131516

1517+
addRefreshListenersToTaskLists(editable, start, end)
1518+
15141519
if (!commentsVisible) {
15151520
val commentSpans = editable.getSpans(start, end, CommentSpan::class.java)
15161521
commentSpans.forEach {
@@ -1521,6 +1526,37 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
15211526
}
15221527
}
15231528

1529+
fun addRefreshListenersToTaskLists(editable: Editable, start: Int, end: Int) {
1530+
val taskLists = editable.getSpans(start, end, AztecTaskListSpan::class.java)
1531+
taskLists.forEach { taskList ->
1532+
if (taskList.onRefresh == null) {
1533+
taskList.onRefresh = {
1534+
refreshTaskListSpan(it)
1535+
}
1536+
}
1537+
}
1538+
}
1539+
1540+
private fun refreshTaskListSpan(taskList: AztecTaskListSpan) {
1541+
val selStart = selectionStart
1542+
val selEnd = selectionEnd
1543+
val spanStart = this.editableText.getSpanStart(taskList)
1544+
val spanEnd = this.editableText.getSpanEnd(taskList)
1545+
val flags = this.editableText.getSpanFlags(taskList)
1546+
taskList.onRefresh = null
1547+
this.editableText.removeSpan(taskList)
1548+
val newSpan = if (taskList is AztecTaskListSpanAligned) {
1549+
AztecTaskListSpanAligned(taskList.nestingLevel, taskList.attributes, taskList.context, taskList.listStyle, taskList.align)
1550+
} else {
1551+
AztecTaskListSpan(taskList.nestingLevel, taskList.attributes, taskList.context, taskList.listStyle)
1552+
}
1553+
newSpan.onRefresh = {
1554+
refreshTaskListSpan(it)
1555+
}
1556+
this.editableText.setSpan(newSpan, spanStart, spanEnd, flags)
1557+
setSelection(selStart, selEnd)
1558+
}
1559+
15241560
fun disableTextChangedListener() {
15251561
consumeEditEvent = true
15261562
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ enum class AztecTextFormat : ITextFormat {
1313
FORMAT_HEADING_6,
1414
FORMAT_UNORDERED_LIST,
1515
FORMAT_ORDERED_LIST,
16+
FORMAT_TASK_LIST,
1617
FORMAT_BOLD,
1718
FORMAT_STRONG,
1819
FORMAT_ITALIC,

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import android.text.method.ArrowKeyMovementMethod
66
import android.text.style.ClickableSpan
77
import android.view.MotionEvent
88
import android.widget.TextView
9+
import org.wordpress.aztec.spans.AztecListItemSpan
910
import org.wordpress.aztec.spans.AztecMediaClickableSpan
11+
import org.wordpress.aztec.spans.AztecTaskListSpan
1012
import org.wordpress.aztec.spans.AztecURLSpan
1113
import org.wordpress.aztec.spans.UnknownClickableSpan
1214

@@ -62,6 +64,8 @@ object EnhancedMovementMethod : ArrowKeyMovementMethod() {
6264

6365
var link: ClickableSpan? = null
6466

67+
if (handleTaskListClick(text, off)) return true
68+
6569
if (clickedOnSpan) {
6670
if (isClickedSpanAmbiguous) {
6771
if (clickedOnSpanToTheLeftOfCursor) {
@@ -89,4 +93,16 @@ object EnhancedMovementMethod : ArrowKeyMovementMethod() {
8993

9094
return super.onTouchEvent(widget, text, event)
9195
}
96+
97+
private fun handleTaskListClick(text: Spannable, off: Int): Boolean {
98+
val clickedList = text.getSpans(off, off, AztecTaskListSpan::class.java).firstOrNull()
99+
val clickedLine = text.getSpans(off, off, AztecListItemSpan::class.java).lastOrNull()
100+
val spanStart = text.getSpanStart(clickedLine)
101+
if (spanStart == off && clickedList != null && clickedLine != null && clickedList.canToggle()) {
102+
clickedLine.toggleCheck()
103+
clickedList.refresh()
104+
return true
105+
}
106+
return false
107+
}
92108
}

0 commit comments

Comments
 (0)