@apollohg/react-native-prose-editor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +160 -0
- package/README.md +143 -0
- package/android/build.gradle +39 -0
- package/android/src/main/assets/editor-icons/MaterialIcons.json +2236 -0
- package/android/src/main/assets/editor-icons/MaterialIcons.ttf +0 -0
- package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +131 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +1057 -0
- package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +14 -0
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +191 -0
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +325 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +647 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +257 -0
- package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +714 -0
- package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +76 -0
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +1044 -0
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +211 -0
- package/expo-module.config.json +9 -0
- package/ios/EditorAddons.swift +228 -0
- package/ios/EditorCore.xcframework/Info.plist +44 -0
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/EditorLayoutManager.swift +254 -0
- package/ios/EditorTheme.swift +372 -0
- package/ios/Generated_editor_core.swift +1143 -0
- package/ios/NativeEditorExpoView.swift +1417 -0
- package/ios/NativeEditorModule.swift +263 -0
- package/ios/PositionBridge.swift +278 -0
- package/ios/ReactNativeProseEditor.podspec +49 -0
- package/ios/RenderBridge.swift +825 -0
- package/ios/RichTextEditorView.swift +1559 -0
- package/ios/editor_coreFFI/editor_coreFFI.h +1014 -0
- package/ios/editor_coreFFI/module.modulemap +7 -0
- package/ios/editor_coreFFI.h +904 -0
- package/ios/editor_coreFFI.modulemap +7 -0
- package/package.json +66 -0
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2014 -0
- package/src/EditorTheme.ts +130 -0
- package/src/EditorToolbar.tsx +620 -0
- package/src/NativeEditorBridge.ts +607 -0
- package/src/NativeRichTextEditor.tsx +951 -0
- package/src/addons.ts +158 -0
- package/src/index.ts +63 -0
- package/src/schemas.ts +153 -0
- package/src/useNativeEditor.ts +173 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
package com.apollohg.editor
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Color
|
|
5
|
+
import android.graphics.drawable.GradientDrawable
|
|
6
|
+
import android.util.AttributeSet
|
|
7
|
+
import android.view.MotionEvent
|
|
8
|
+
import android.view.ViewGroup
|
|
9
|
+
import android.widget.FrameLayout
|
|
10
|
+
import android.widget.LinearLayout
|
|
11
|
+
import android.widget.ScrollView
|
|
12
|
+
import uniffi.editor_core.*
|
|
13
|
+
|
|
14
|
+
/** Container view that owns the native editor text field. */
|
|
15
|
+
class RichTextEditorView @JvmOverloads constructor(
|
|
16
|
+
context: Context,
|
|
17
|
+
attrs: AttributeSet? = null,
|
|
18
|
+
defStyleAttr: Int = 0
|
|
19
|
+
) : LinearLayout(context, attrs, defStyleAttr) {
|
|
20
|
+
|
|
21
|
+
private class EditorScrollView(context: Context) : ScrollView(context) {
|
|
22
|
+
private fun updateParentIntercept(action: Int) {
|
|
23
|
+
val canScroll = canScrollVertically(-1) || canScrollVertically(1)
|
|
24
|
+
if (!canScroll) return
|
|
25
|
+
when (action) {
|
|
26
|
+
MotionEvent.ACTION_DOWN,
|
|
27
|
+
MotionEvent.ACTION_MOVE -> parent?.requestDisallowInterceptTouchEvent(true)
|
|
28
|
+
MotionEvent.ACTION_UP,
|
|
29
|
+
MotionEvent.ACTION_CANCEL -> parent?.requestDisallowInterceptTouchEvent(false)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
|
|
34
|
+
updateParentIntercept(ev.actionMasked)
|
|
35
|
+
return super.onInterceptTouchEvent(ev)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
override fun onTouchEvent(ev: MotionEvent): Boolean {
|
|
39
|
+
updateParentIntercept(ev.actionMasked)
|
|
40
|
+
return super.onTouchEvent(ev)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
val editorEditText: EditorEditText
|
|
45
|
+
val editorScrollView: ScrollView
|
|
46
|
+
|
|
47
|
+
private var heightBehavior: EditorHeightBehavior = EditorHeightBehavior.FIXED
|
|
48
|
+
private var theme: EditorTheme? = null
|
|
49
|
+
private var baseBackgroundColor: Int = Color.WHITE
|
|
50
|
+
private var viewportBottomInsetPx: Int = 0
|
|
51
|
+
internal var appliedCornerRadiusPx: Float = 0f
|
|
52
|
+
|
|
53
|
+
/** Binds or unbinds the Rust editor instance. */
|
|
54
|
+
var editorId: Long = 0
|
|
55
|
+
set(value) {
|
|
56
|
+
field = value
|
|
57
|
+
if (value != 0L) {
|
|
58
|
+
editorEditText.bindEditor(value)
|
|
59
|
+
} else {
|
|
60
|
+
editorEditText.unbindEditor()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
init {
|
|
65
|
+
orientation = VERTICAL
|
|
66
|
+
|
|
67
|
+
editorEditText = EditorEditText(context)
|
|
68
|
+
editorScrollView = EditorScrollView(context).apply {
|
|
69
|
+
clipToPadding = false
|
|
70
|
+
isFillViewport = false
|
|
71
|
+
}
|
|
72
|
+
editorScrollView.addView(editorEditText, createEditorLayoutParams())
|
|
73
|
+
|
|
74
|
+
addView(editorScrollView, createContainerLayoutParams())
|
|
75
|
+
updateScrollContainerAppearance()
|
|
76
|
+
updateScrollContainerInsets()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fun configure(
|
|
80
|
+
textSizePx: Float = 16f * resources.displayMetrics.density,
|
|
81
|
+
textColor: Int = Color.BLACK,
|
|
82
|
+
backgroundColor: Int = Color.WHITE
|
|
83
|
+
) {
|
|
84
|
+
baseBackgroundColor = backgroundColor
|
|
85
|
+
editorEditText.setBaseStyle(textSizePx, textColor, backgroundColor)
|
|
86
|
+
updateScrollContainerAppearance()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fun applyTheme(theme: EditorTheme?) {
|
|
90
|
+
this.theme = theme
|
|
91
|
+
val previousScrollY = editorScrollView.scrollY
|
|
92
|
+
editorEditText.applyTheme(theme)
|
|
93
|
+
updateScrollContainerAppearance()
|
|
94
|
+
updateScrollContainerInsets()
|
|
95
|
+
if (heightBehavior == EditorHeightBehavior.FIXED) {
|
|
96
|
+
editorScrollView.post {
|
|
97
|
+
val childHeight = editorScrollView.getChildAt(0)?.height ?: 0
|
|
98
|
+
val maxScrollY = maxOf(
|
|
99
|
+
0,
|
|
100
|
+
childHeight + editorScrollView.paddingTop + editorScrollView.paddingBottom - editorScrollView.height
|
|
101
|
+
)
|
|
102
|
+
editorScrollView.scrollTo(0, previousScrollY.coerceIn(0, maxScrollY))
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fun setHeightBehavior(heightBehavior: EditorHeightBehavior) {
|
|
108
|
+
if (this.heightBehavior == heightBehavior) return
|
|
109
|
+
this.heightBehavior = heightBehavior
|
|
110
|
+
editorEditText.setHeightBehavior(heightBehavior)
|
|
111
|
+
editorEditText.layoutParams = createEditorLayoutParams()
|
|
112
|
+
editorScrollView.layoutParams = createContainerLayoutParams()
|
|
113
|
+
editorScrollView.isVerticalScrollBarEnabled = heightBehavior == EditorHeightBehavior.FIXED
|
|
114
|
+
editorScrollView.overScrollMode = if (heightBehavior == EditorHeightBehavior.FIXED) {
|
|
115
|
+
OVER_SCROLL_IF_CONTENT_SCROLLS
|
|
116
|
+
} else {
|
|
117
|
+
OVER_SCROLL_NEVER
|
|
118
|
+
}
|
|
119
|
+
updateScrollContainerInsets()
|
|
120
|
+
requestLayout()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fun setViewportBottomInsetPx(bottomInsetPx: Int) {
|
|
124
|
+
val clampedInset = bottomInsetPx.coerceAtLeast(0)
|
|
125
|
+
if (viewportBottomInsetPx == clampedInset) return
|
|
126
|
+
viewportBottomInsetPx = clampedInset
|
|
127
|
+
updateScrollContainerInsets()
|
|
128
|
+
editorEditText.setViewportBottomInsetPx(clampedInset)
|
|
129
|
+
requestLayout()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fun setContent(html: String) {
|
|
133
|
+
if (editorId == 0L) return
|
|
134
|
+
val renderJSON = editorSetHtml(editorId.toULong(), html)
|
|
135
|
+
editorEditText.applyRenderJSON(renderJSON)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fun setContent(json: org.json.JSONObject) {
|
|
139
|
+
if (editorId == 0L) return
|
|
140
|
+
val renderJSON = editorSetJson(editorId.toULong(), json.toString())
|
|
141
|
+
editorEditText.applyRenderJSON(renderJSON)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
override fun onDetachedFromWindow() {
|
|
145
|
+
super.onDetachedFromWindow()
|
|
146
|
+
if (editorId != 0L) {
|
|
147
|
+
editorEditText.unbindEditor()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
152
|
+
if (heightBehavior != EditorHeightBehavior.AUTO_GROW) {
|
|
153
|
+
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
val childWidthSpec = getChildMeasureSpec(
|
|
158
|
+
widthMeasureSpec,
|
|
159
|
+
paddingLeft + paddingRight,
|
|
160
|
+
editorScrollView.layoutParams.width
|
|
161
|
+
)
|
|
162
|
+
val childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
|
|
163
|
+
editorScrollView.measure(childWidthSpec, childHeightSpec)
|
|
164
|
+
|
|
165
|
+
val measuredWidth = resolveSize(
|
|
166
|
+
editorScrollView.measuredWidth + paddingLeft + paddingRight,
|
|
167
|
+
widthMeasureSpec
|
|
168
|
+
)
|
|
169
|
+
val desiredHeight = editorScrollView.measuredHeight + paddingTop + paddingBottom
|
|
170
|
+
val measuredHeight = when (MeasureSpec.getMode(heightMeasureSpec)) {
|
|
171
|
+
MeasureSpec.AT_MOST -> desiredHeight.coerceAtMost(MeasureSpec.getSize(heightMeasureSpec))
|
|
172
|
+
else -> desiredHeight
|
|
173
|
+
}
|
|
174
|
+
setMeasuredDimension(measuredWidth, measuredHeight)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private fun updateScrollContainerAppearance() {
|
|
178
|
+
val cornerRadiusPx = (theme?.borderRadius ?: 0f) * resources.displayMetrics.density
|
|
179
|
+
editorScrollView.background = GradientDrawable().apply {
|
|
180
|
+
cornerRadius = cornerRadiusPx
|
|
181
|
+
setColor(theme?.backgroundColor ?: baseBackgroundColor)
|
|
182
|
+
}
|
|
183
|
+
editorScrollView.clipToOutline = cornerRadiusPx > 0f
|
|
184
|
+
appliedCornerRadiusPx = cornerRadiusPx
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private fun updateScrollContainerInsets() {
|
|
188
|
+
if (heightBehavior != EditorHeightBehavior.FIXED) {
|
|
189
|
+
editorScrollView.setPadding(0, 0, 0, 0)
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
val density = resources.displayMetrics.density
|
|
194
|
+
val topInset = ((theme?.contentInsets?.top ?: 0f) * density).toInt()
|
|
195
|
+
val bottomInset = ((theme?.contentInsets?.bottom ?: 0f) * density).toInt()
|
|
196
|
+
editorScrollView.setPadding(0, topInset, 0, bottomInset + viewportBottomInsetPx)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private fun createContainerLayoutParams(): LayoutParams =
|
|
200
|
+
if (heightBehavior == EditorHeightBehavior.AUTO_GROW) {
|
|
201
|
+
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
|
202
|
+
} else {
|
|
203
|
+
LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private fun createEditorLayoutParams(): FrameLayout.LayoutParams =
|
|
207
|
+
FrameLayout.LayoutParams(
|
|
208
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
209
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
210
|
+
)
|
|
211
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
struct NativeMentionSuggestion {
|
|
4
|
+
let key: String
|
|
5
|
+
let title: String
|
|
6
|
+
let subtitle: String?
|
|
7
|
+
let label: String
|
|
8
|
+
let attrs: [String: Any]
|
|
9
|
+
|
|
10
|
+
init?(dictionary: [String: Any]) {
|
|
11
|
+
guard let key = dictionary["key"] as? String,
|
|
12
|
+
let title = dictionary["title"] as? String,
|
|
13
|
+
let label = dictionary["label"] as? String
|
|
14
|
+
else {
|
|
15
|
+
return nil
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
self.key = key
|
|
19
|
+
self.title = title
|
|
20
|
+
self.subtitle = dictionary["subtitle"] as? String
|
|
21
|
+
self.label = label
|
|
22
|
+
self.attrs = dictionary["attrs"] as? [String: Any] ?? [:]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
struct NativeMentionsAddonConfig {
|
|
27
|
+
let trigger: String
|
|
28
|
+
let suggestions: [NativeMentionSuggestion]
|
|
29
|
+
let theme: EditorMentionTheme?
|
|
30
|
+
|
|
31
|
+
init?(dictionary: [String: Any]) {
|
|
32
|
+
let trigger = (dictionary["trigger"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
33
|
+
self.trigger = (trigger?.isEmpty == false ? trigger : "@") ?? "@"
|
|
34
|
+
self.suggestions = ((dictionary["suggestions"] as? [[String: Any]]) ?? []).compactMap(NativeMentionSuggestion.init(dictionary:))
|
|
35
|
+
if let theme = dictionary["theme"] as? [String: Any] {
|
|
36
|
+
self.theme = EditorMentionTheme(dictionary: theme)
|
|
37
|
+
} else {
|
|
38
|
+
self.theme = nil
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
struct NativeEditorAddons {
|
|
44
|
+
let mentions: NativeMentionsAddonConfig?
|
|
45
|
+
|
|
46
|
+
static func from(json: String?) -> NativeEditorAddons {
|
|
47
|
+
guard let json,
|
|
48
|
+
let data = json.data(using: .utf8),
|
|
49
|
+
let raw = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
50
|
+
else {
|
|
51
|
+
return NativeEditorAddons(mentions: nil)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return NativeEditorAddons(
|
|
55
|
+
mentions: (raw["mentions"] as? [String: Any]).flatMap(NativeMentionsAddonConfig.init(dictionary:))
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
struct MentionQueryState: Equatable {
|
|
61
|
+
let query: String
|
|
62
|
+
let trigger: String
|
|
63
|
+
let anchor: UInt32
|
|
64
|
+
let head: UInt32
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func isMentionIdentifierScalar(_ scalar: Unicode.Scalar) -> Bool {
|
|
68
|
+
CharacterSet.alphanumerics.contains(scalar) || scalar == "_" || scalar == "-"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func resolveMentionQueryState(
|
|
72
|
+
in text: String,
|
|
73
|
+
cursorScalar: UInt32,
|
|
74
|
+
trigger: String,
|
|
75
|
+
isCaretInsideMention: Bool
|
|
76
|
+
) -> MentionQueryState? {
|
|
77
|
+
guard !isCaretInsideMention else { return nil }
|
|
78
|
+
|
|
79
|
+
let scalars = Array(text.unicodeScalars)
|
|
80
|
+
let scalarCount = UInt32(scalars.count)
|
|
81
|
+
guard cursorScalar <= scalarCount else { return nil }
|
|
82
|
+
|
|
83
|
+
let triggerScalars = Array(trigger.unicodeScalars)
|
|
84
|
+
guard triggerScalars.count == 1, let triggerScalar = triggerScalars.first else {
|
|
85
|
+
return nil
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
var start = Int(cursorScalar)
|
|
89
|
+
while start > 0 {
|
|
90
|
+
let previous = scalars[start - 1]
|
|
91
|
+
if previous.properties.isWhitespace
|
|
92
|
+
|| previous == "\n"
|
|
93
|
+
|| previous == "\u{FFFC}"
|
|
94
|
+
|| (!isMentionIdentifierScalar(previous) && previous != triggerScalar)
|
|
95
|
+
{
|
|
96
|
+
break
|
|
97
|
+
}
|
|
98
|
+
start -= 1
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
guard start < scalars.count, scalars[start] == triggerScalar else {
|
|
102
|
+
return nil
|
|
103
|
+
}
|
|
104
|
+
if start > 0 {
|
|
105
|
+
let previous = scalars[start - 1]
|
|
106
|
+
if isMentionIdentifierScalar(previous) {
|
|
107
|
+
return nil
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let queryStart = start + 1
|
|
112
|
+
let cursor = Int(cursorScalar)
|
|
113
|
+
guard queryStart <= cursor else { return nil }
|
|
114
|
+
let query = String(String.UnicodeScalarView(scalars[queryStart..<cursor]))
|
|
115
|
+
if query.unicodeScalars.contains(where: {
|
|
116
|
+
$0.properties.isWhitespace || $0 == "\n" || $0 == "\u{FFFC}"
|
|
117
|
+
}) {
|
|
118
|
+
return nil
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return MentionQueryState(
|
|
122
|
+
query: query,
|
|
123
|
+
trigger: trigger,
|
|
124
|
+
anchor: UInt32(start),
|
|
125
|
+
head: cursorScalar
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
final class MentionSuggestionChipButton: UIButton {
|
|
130
|
+
private let titleLabelView = UILabel()
|
|
131
|
+
private let subtitleLabelView = UILabel()
|
|
132
|
+
private let stackView = UIStackView()
|
|
133
|
+
private var theme: EditorMentionTheme?
|
|
134
|
+
|
|
135
|
+
let suggestion: NativeMentionSuggestion
|
|
136
|
+
|
|
137
|
+
init(suggestion: NativeMentionSuggestion, theme: EditorMentionTheme?) {
|
|
138
|
+
self.suggestion = suggestion
|
|
139
|
+
self.theme = theme
|
|
140
|
+
super.init(frame: .zero)
|
|
141
|
+
translatesAutoresizingMaskIntoConstraints = false
|
|
142
|
+
layer.cornerRadius = 12
|
|
143
|
+
clipsToBounds = true
|
|
144
|
+
if #available(iOS 15.0, *) {
|
|
145
|
+
var configuration = UIButton.Configuration.plain()
|
|
146
|
+
configuration.contentInsets = .zero
|
|
147
|
+
self.configuration = configuration
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
titleLabelView.translatesAutoresizingMaskIntoConstraints = false
|
|
151
|
+
titleLabelView.isUserInteractionEnabled = false
|
|
152
|
+
titleLabelView.font = .systemFont(ofSize: 14, weight: .semibold)
|
|
153
|
+
titleLabelView.text = suggestion.label
|
|
154
|
+
titleLabelView.numberOfLines = 1
|
|
155
|
+
|
|
156
|
+
subtitleLabelView.translatesAutoresizingMaskIntoConstraints = false
|
|
157
|
+
subtitleLabelView.isUserInteractionEnabled = false
|
|
158
|
+
subtitleLabelView.font = .systemFont(ofSize: 12)
|
|
159
|
+
subtitleLabelView.text = suggestion.subtitle
|
|
160
|
+
subtitleLabelView.numberOfLines = 1
|
|
161
|
+
subtitleLabelView.isHidden = suggestion.subtitle == nil
|
|
162
|
+
|
|
163
|
+
stackView.translatesAutoresizingMaskIntoConstraints = false
|
|
164
|
+
stackView.isUserInteractionEnabled = false
|
|
165
|
+
stackView.axis = .vertical
|
|
166
|
+
stackView.alignment = .fill
|
|
167
|
+
stackView.spacing = 1
|
|
168
|
+
stackView.addArrangedSubview(titleLabelView)
|
|
169
|
+
stackView.addArrangedSubview(subtitleLabelView)
|
|
170
|
+
addSubview(stackView)
|
|
171
|
+
|
|
172
|
+
NSLayoutConstraint.activate([
|
|
173
|
+
stackView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
|
174
|
+
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
|
|
175
|
+
stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
|
|
176
|
+
stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),
|
|
177
|
+
heightAnchor.constraint(greaterThanOrEqualToConstant: 40),
|
|
178
|
+
])
|
|
179
|
+
|
|
180
|
+
addTarget(self, action: #selector(handleTouchDown), for: [.touchDown, .touchDragEnter])
|
|
181
|
+
addTarget(self, action: #selector(handleTouchUp), for: [.touchCancel, .touchDragExit, .touchUpInside, .touchUpOutside])
|
|
182
|
+
apply(theme: theme)
|
|
183
|
+
updateAppearance(highlighted: false)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
required init?(coder: NSCoder) {
|
|
187
|
+
return nil
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
func apply(theme: EditorMentionTheme?) {
|
|
191
|
+
self.theme = theme
|
|
192
|
+
layer.cornerRadius = theme?.borderRadius ?? 12
|
|
193
|
+
layer.borderColor = (theme?.borderColor ?? UIColor.clear).cgColor
|
|
194
|
+
layer.borderWidth = theme?.borderWidth ?? 0
|
|
195
|
+
subtitleLabelView.isHidden = suggestion.subtitle == nil
|
|
196
|
+
updateAppearance(highlighted: isHighlighted)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
override var isHighlighted: Bool {
|
|
200
|
+
didSet {
|
|
201
|
+
updateAppearance(highlighted: isHighlighted)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@objc private func handleTouchDown() {
|
|
206
|
+
updateAppearance(highlighted: true)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@objc private func handleTouchUp() {
|
|
210
|
+
updateAppearance(highlighted: false)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private func updateAppearance(highlighted: Bool) {
|
|
214
|
+
backgroundColor = highlighted
|
|
215
|
+
? (theme?.optionHighlightedBackgroundColor ?? UIColor.systemBlue.withAlphaComponent(0.12))
|
|
216
|
+
: (theme?.backgroundColor ?? UIColor.secondarySystemBackground)
|
|
217
|
+
titleLabelView.textColor = highlighted
|
|
218
|
+
? (theme?.optionHighlightedTextColor ?? theme?.optionTextColor ?? .label)
|
|
219
|
+
: (theme?.optionTextColor ?? theme?.textColor ?? .label)
|
|
220
|
+
subtitleLabelView.textColor = theme?.optionSecondaryTextColor ?? .secondaryLabel
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
func contentViewsAllowTouchPassthroughForTesting() -> Bool {
|
|
224
|
+
!stackView.isUserInteractionEnabled
|
|
225
|
+
&& !titleLabelView.isUserInteractionEnabled
|
|
226
|
+
&& !subtitleLabelView.isUserInteractionEnabled
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>AvailableLibraries</key>
|
|
6
|
+
<array>
|
|
7
|
+
<dict>
|
|
8
|
+
<key>BinaryPath</key>
|
|
9
|
+
<string>libeditor_core.a</string>
|
|
10
|
+
<key>LibraryIdentifier</key>
|
|
11
|
+
<string>ios-arm64_x86_64-simulator</string>
|
|
12
|
+
<key>LibraryPath</key>
|
|
13
|
+
<string>libeditor_core.a</string>
|
|
14
|
+
<key>SupportedArchitectures</key>
|
|
15
|
+
<array>
|
|
16
|
+
<string>arm64</string>
|
|
17
|
+
<string>x86_64</string>
|
|
18
|
+
</array>
|
|
19
|
+
<key>SupportedPlatform</key>
|
|
20
|
+
<string>ios</string>
|
|
21
|
+
<key>SupportedPlatformVariant</key>
|
|
22
|
+
<string>simulator</string>
|
|
23
|
+
</dict>
|
|
24
|
+
<dict>
|
|
25
|
+
<key>BinaryPath</key>
|
|
26
|
+
<string>libeditor_core.a</string>
|
|
27
|
+
<key>LibraryIdentifier</key>
|
|
28
|
+
<string>ios-arm64</string>
|
|
29
|
+
<key>LibraryPath</key>
|
|
30
|
+
<string>libeditor_core.a</string>
|
|
31
|
+
<key>SupportedArchitectures</key>
|
|
32
|
+
<array>
|
|
33
|
+
<string>arm64</string>
|
|
34
|
+
</array>
|
|
35
|
+
<key>SupportedPlatform</key>
|
|
36
|
+
<string>ios</string>
|
|
37
|
+
</dict>
|
|
38
|
+
</array>
|
|
39
|
+
<key>CFBundlePackageType</key>
|
|
40
|
+
<string>XFWK</string>
|
|
41
|
+
<key>XCFrameworkFormatVersion</key>
|
|
42
|
+
<string>1.0</string>
|
|
43
|
+
</dict>
|
|
44
|
+
</plist>
|
|
Binary file
|