diff --git a/Components/Sources/Components/Components/Editors/Common Views/Base/WKEditorToolbarButton.swift b/Components/Sources/Components/Components/Editors/Common Views/Base/WKEditorToolbarButton.swift
index d96c45e9fcf..cf60cdc87e2 100644
--- a/Components/Sources/Components/Components/Editors/Common Views/Base/WKEditorToolbarButton.swift
+++ b/Components/Sources/Components/Components/Editors/Common Views/Base/WKEditorToolbarButton.swift
@@ -64,7 +64,7 @@ class WKEditorToolbarButton: WKComponentView {
set {
button.isSelected = newValue
updateColors()
- accessibilityTraits = newValue ? [.button, .selected] : [.button]
+ accessibilityTraits = button.accessibilityTraits
}
}
@@ -75,7 +75,7 @@ class WKEditorToolbarButton: WKComponentView {
set {
button.isEnabled = newValue
updateColors()
- accessibilityTraits = newValue ? [.button, .selected] : [.button, .notEnabled]
+ accessibilityTraits = button.accessibilityTraits
}
}
diff --git a/Components/Sources/Components/Components/Editors/Common Views/Input Accessory Views/Expanding/WKEditorToolbarExpandingView.swift b/Components/Sources/Components/Components/Editors/Common Views/Input Accessory Views/Expanding/WKEditorToolbarExpandingView.swift
index 5530eab53c1..20448b349b0 100644
--- a/Components/Sources/Components/Components/Editors/Common Views/Input Accessory Views/Expanding/WKEditorToolbarExpandingView.swift
+++ b/Components/Sources/Components/Components/Editors/Common Views/Input Accessory Views/Expanding/WKEditorToolbarExpandingView.swift
@@ -5,6 +5,8 @@ protocol WKEditorToolbarExpandingViewDelegate: AnyObject {
func toolbarExpandingViewDidTapFormatText(toolbarView: WKEditorToolbarExpandingView)
func toolbarExpandingViewDidTapTemplate(toolbarView: WKEditorToolbarExpandingView, isSelected: Bool)
func toolbarExpandingViewDidTapReference(toolbarView: WKEditorToolbarExpandingView, isSelected: Bool)
+ func toolbarExpandingViewDidTapLink(toolbarView: WKEditorToolbarExpandingView, isSelected: Bool)
+ func toolbarExpandingViewDidTapImage(toolbarView: WKEditorToolbarExpandingView)
func toolbarExpandingViewDidTapUnorderedList(toolbarView: WKEditorToolbarExpandingView, isSelected: Bool)
func toolbarExpandingViewDidTapOrderedList(toolbarView: WKEditorToolbarExpandingView, isSelected: Bool)
func toolbarExpandingViewDidTapIncreaseIndent(toolbarView: WKEditorToolbarExpandingView)
@@ -47,7 +49,7 @@ class WKEditorToolbarExpandingView: WKEditorToolbarView {
@IBOutlet private weak var referenceButton: WKEditorToolbarButton!
@IBOutlet private weak var linkButton: WKEditorToolbarButton!
@IBOutlet private weak var templateButton: WKEditorToolbarButton!
- @IBOutlet private weak var mediaButton: WKEditorToolbarButton!
+ @IBOutlet private weak var imageButton: WKEditorToolbarButton!
@IBOutlet private weak var findInPageButton: WKEditorToolbarButton!
@IBOutlet private weak var unorderedListButton: WKEditorToolbarButton!
@@ -97,9 +99,9 @@ class WKEditorToolbarExpandingView: WKEditorToolbarView {
templateButton.addTarget(self, action: #selector(tappedTemplate), for: .touchUpInside)
templateButton.accessibilityLabel = WKSourceEditorLocalizedStrings.current.accessibilityLabelButtonTemplate
- mediaButton.setImage(WKSFSymbolIcon.for(symbol: .photo), for: .normal)
- mediaButton.addTarget(self, action: #selector(tappedMedia), for: .touchUpInside)
- mediaButton.accessibilityLabel = WKSourceEditorLocalizedStrings.current.accessibilityLabelButtonMedia
+ imageButton.setImage(WKSFSymbolIcon.for(symbol: .photo), for: .normal)
+ imageButton.addTarget(self, action: #selector(tappedMedia), for: .touchUpInside)
+ imageButton.accessibilityLabel = WKSourceEditorLocalizedStrings.current.accessibilityLabelButtonMedia
findInPageButton.setImage(WKSFSymbolIcon.for(symbol: .docTextMagnifyingGlass), for: .normal)
findInPageButton.addTarget(self, action: #selector(tappedFindInPage), for: .touchUpInside)
@@ -138,6 +140,7 @@ class WKEditorToolbarExpandingView: WKEditorToolbarView {
cursorRightButton.setImage(WKIcon.chevronRight, for: .normal)
cursorRightButton.addTarget(self, action: #selector(tappedCursorRight), for: .touchUpInside)
+ cursorRightButton.accessibilityLabel = WKSourceEditorLocalizedStrings.current.accessibilityLabelButtonCursorRight
NotificationCenter.default.addObserver(self, selector: #selector(updateButtonSelectionState(_:)), name: Notification.WKSourceEditorSelectionState, object: nil)
}
@@ -151,6 +154,8 @@ class WKEditorToolbarExpandingView: WKEditorToolbarView {
templateButton.isSelected = selectionState.isHorizontalTemplate
referenceButton.isSelected = selectionState.isHorizontalReference
+ linkButton.isSelected = selectionState.isSimpleLink
+ imageButton.isEnabled = !selectionState.isBold && !selectionState.isItalics && !selectionState.isSimpleLink
unorderedListButton.isSelected = selectionState.isBulletSingleList || selectionState.isBulletMultipleList
unorderedListButton.isEnabled = !selectionState.isNumberSingleList && !selectionState.isNumberMultipleList
@@ -226,6 +231,7 @@ class WKEditorToolbarExpandingView: WKEditorToolbarView {
}
@objc private func tappedLink() {
+ delegate?.toolbarExpandingViewDidTapLink(toolbarView: self, isSelected: linkButton.isSelected)
}
@objc private func tappedUnorderedList() {
@@ -265,6 +271,7 @@ class WKEditorToolbarExpandingView: WKEditorToolbarView {
}
@objc private func tappedMedia() {
+ delegate?.toolbarExpandingViewDidTapImage(toolbarView: self)
}
}
diff --git a/Components/Sources/Components/Components/Editors/Common Views/Input Accessory Views/Expanding/WKEditorToolbarExpandingView.xib b/Components/Sources/Components/Components/Editors/Common Views/Input Accessory Views/Expanding/WKEditorToolbarExpandingView.xib
index 0d7bcf68e60..4b9c61e75cc 100644
--- a/Components/Sources/Components/Components/Editors/Common Views/Input Accessory Views/Expanding/WKEditorToolbarExpandingView.xib
+++ b/Components/Sources/Components/Components/Editors/Common Views/Input Accessory Views/Expanding/WKEditorToolbarExpandingView.xib
@@ -275,9 +275,9 @@
+
-
diff --git a/Components/Sources/Components/Components/Editors/Common Views/Input Accessory Views/Highlight/WKEditorToolbarHighlightView.swift b/Components/Sources/Components/Components/Editors/Common Views/Input Accessory Views/Highlight/WKEditorToolbarHighlightView.swift
index f2f319bd914..33238ec9cd9 100644
--- a/Components/Sources/Components/Components/Editors/Common Views/Input Accessory Views/Highlight/WKEditorToolbarHighlightView.swift
+++ b/Components/Sources/Components/Components/Editors/Common Views/Input Accessory Views/Highlight/WKEditorToolbarHighlightView.swift
@@ -5,6 +5,7 @@ protocol WKEditorToolbarHighlightViewDelegate: AnyObject {
func toolbarHighlightViewDidTapItalics(toolbarView: WKEditorToolbarHighlightView, isSelected: Bool)
func toolbarHighlightViewDidTapTemplate(toolbarView: WKEditorToolbarHighlightView, isSelected: Bool)
func toolbarHighlightViewDidTapReference(toolbarView: WKEditorToolbarHighlightView, isSelected: Bool)
+ func toolbarHighlightViewDidTapLink(toolbarView: WKEditorToolbarHighlightView, isSelected: Bool)
func toolbarHighlightViewDidTapShowMore(toolbarView: WKEditorToolbarHighlightView)
}
@@ -69,6 +70,7 @@ class WKEditorToolbarHighlightView: WKEditorToolbarView {
italicsButton.isSelected = selectionState.isItalics
templateButton.isSelected = selectionState.isHorizontalTemplate
referenceButton.isSelected = selectionState.isHorizontalReference
+ linkButton.isSelected = selectionState.isSimpleLink
}
// MARK: - Button Actions
@@ -89,6 +91,7 @@ class WKEditorToolbarHighlightView: WKEditorToolbarView {
}
@objc private func tappedLink() {
+ delegate?.toolbarHighlightViewDidTapLink(toolbarView: self, isSelected: linkButton.isSelected)
}
@objc private func tappedTemplate() {
diff --git a/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorInputView.swift b/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorInputView.swift
index 4c7d228daeb..7aa62ae047f 100644
--- a/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorInputView.swift
+++ b/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorInputView.swift
@@ -13,6 +13,7 @@ protocol WKEditorInputViewDelegate: AnyObject {
func didTapDecreaseIndent()
func didTapHeading(type: WKEditorInputView.HeadingButtonType)
func didTapStrikethrough(isSelected: Bool)
+ func didTapLink(isSelected: Bool)
}
class WKEditorInputView: WKComponentView {
diff --git a/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorToolbarPlainView.swift b/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorToolbarPlainView.swift
index b1a8e02df6b..57468fe1075 100644
--- a/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorToolbarPlainView.swift
+++ b/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorToolbarPlainView.swift
@@ -56,6 +56,7 @@ class WKEditorToolbarPlainView: WKEditorToolbarView {
italicsButton.isSelected = selectionState.isItalics
templateButton.isSelected = selectionState.isHorizontalTemplate
referenceButton.isSelected = selectionState.isHorizontalReference
+ linkButton.isSelected = selectionState.isSimpleLink
}
// MARK: Button Actions
@@ -80,5 +81,6 @@ class WKEditorToolbarPlainView: WKEditorToolbarView {
}
@objc private func tappedLink() {
+ delegate?.didTapLink(isSelected: linkButton.isSelected)
}
}
diff --git a/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatterLink+ButtonActions.swift b/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatterLink+ButtonActions.swift
new file mode 100644
index 00000000000..e1e1dfa2a77
--- /dev/null
+++ b/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatterLink+ButtonActions.swift
@@ -0,0 +1,107 @@
+import Foundation
+import ComponentsObjC
+
+enum WKSourceEditorFormatterLinkButtonAction {
+ case edit
+ case insert
+}
+
+public struct WKSourceEditorFormatterLinkWizardParameters {
+ public let editPageTitle: String?
+ public let editPageLabel: String?
+ public let insertSearchTerm: String?
+ let preselectedTextRange: UITextRange
+}
+
+extension WKSourceEditorFormatterLink {
+
+ func linkWizardParameters(action: WKSourceEditorFormatterLinkButtonAction, in textView: UITextView) -> WKSourceEditorFormatterLinkWizardParameters? {
+
+ switch action {
+ case .edit:
+ expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: "[[", endingFormattingString: "]]", in: textView)
+
+ guard let selectedTextRange = textView.selectedTextRange,
+ let selectedText = textView.text(in: selectedTextRange) else {
+ return nil
+ }
+
+ let splitText = selectedText.split(separator: "|").map {String($0)}
+
+ switch splitText.count {
+ case 1:
+ return WKSourceEditorFormatterLinkWizardParameters(editPageTitle: splitText[0], editPageLabel: nil, insertSearchTerm: nil, preselectedTextRange: selectedTextRange)
+ case 2:
+ return WKSourceEditorFormatterLinkWizardParameters(editPageTitle: splitText[0], editPageLabel: splitText[1], insertSearchTerm: nil, preselectedTextRange: selectedTextRange)
+ default:
+ return nil
+ }
+
+ case .insert:
+
+ guard let selectedTextRange = textView.selectedTextRange,
+ let selectedText = textView.text(in: selectedTextRange) else {
+ return nil
+ }
+
+ return WKSourceEditorFormatterLinkWizardParameters(editPageTitle: nil, editPageLabel: nil, insertSearchTerm: selectedText, preselectedTextRange: selectedTextRange)
+ }
+
+
+ }
+
+ func insertLink(in textView: UITextView, pageTitle: String, preselectedTextRange: UITextRange) {
+ var content = "[[\(pageTitle)]]"
+
+ guard let selectedText = textView.text(in: preselectedTextRange) else {
+ return
+ }
+
+ if pageTitle != selectedText {
+ content = "[[\(pageTitle)|\(selectedText)]]"
+ }
+
+ textView.replace(preselectedTextRange, withText: content)
+
+ if let newStartPosition = textView.position(from: preselectedTextRange.start, offset: 2),
+ let newEndPosition = textView.position(from: preselectedTextRange.start, offset: content.count-2) {
+ textView.selectedTextRange = textView.textRange(from: newStartPosition, to: newEndPosition)
+ }
+
+ }
+
+ func editLink(in textView: UITextView, newPageTitle: String, newPageLabel: String?, preselectedTextRange: UITextRange) {
+ if let newPageLabel, !newPageLabel.isEmpty {
+ textView.replace(preselectedTextRange, withText: "\(newPageTitle)|\(newPageLabel)")
+ } else {
+ textView.replace(preselectedTextRange, withText: "\(newPageTitle)")
+ }
+ }
+
+ func removeLink(in textView: UITextView, preselectedTextRange: UITextRange) {
+
+ guard let selectedText = textView.text(in: preselectedTextRange) else {
+ return
+ }
+
+ if let markupStartPosition = textView.position(from: preselectedTextRange.start, offset: -2),
+ let markupEndPosition = textView.position(from: preselectedTextRange.end, offset: 2),
+ let newSelectedRange = textView.textRange(from: markupStartPosition, to: markupEndPosition) {
+ textView.replace(newSelectedRange, withText: selectedText)
+
+ if let newStartPosition = textView.position(from: preselectedTextRange.start, offset: -2),
+ let newEndPosition = textView.position(from: preselectedTextRange.end, offset: -2) {
+ textView.selectedTextRange = textView.textRange(from: newStartPosition, to: newEndPosition)
+ }
+ }
+ }
+
+ func insertImage(wikitext: String, in textView: UITextView) {
+
+ guard let selectedTextRange = textView.selectedTextRange else {
+ return
+ }
+
+ textView.replace(selectedTextRange, withText: wikitext)
+ }
+}
diff --git a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift
index 730dc741ca7..9859fff34ec 100644
--- a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift
+++ b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift
@@ -28,8 +28,9 @@ fileprivate var needsTextKit2: Bool {
let isSubheading3: Bool
let isSubheading4: Bool
let isStrikethrough: Bool
-
- init(isBold: Bool, isItalics: Bool, isHorizontalTemplate: Bool, isHorizontalReference: Bool, isBulletSingleList: Bool, isBulletMultipleList: Bool, isNumberSingleList: Bool, isNumberMultipleList: Bool, isHeading: Bool, isSubheading1: Bool, isSubheading2: Bool, isSubheading3: Bool, isSubheading4: Bool, isStrikethrough: Bool) {
+ let isSimpleLink: Bool
+ let isLinkWithNestedLink: Bool
+ init(isBold: Bool, isItalics: Bool, isHorizontalTemplate: Bool, isHorizontalReference: Bool, isBulletSingleList: Bool, isBulletMultipleList: Bool, isNumberSingleList: Bool, isNumberMultipleList: Bool, isHeading: Bool, isSubheading1: Bool, isSubheading2: Bool, isSubheading3: Bool, isSubheading4: Bool, isStrikethrough: Bool, isSimpleLink: Bool, isLinkWithNestedLink: Bool) {
self.isBold = isBold
self.isItalics = isItalics
self.isHorizontalTemplate = isHorizontalTemplate
@@ -44,7 +45,10 @@ fileprivate var needsTextKit2: Bool {
self.isSubheading3 = isSubheading3
self.isSubheading4 = isSubheading4
self.isStrikethrough = isStrikethrough
+ self.isSimpleLink = isSimpleLink
+ self.isLinkWithNestedLink = isLinkWithNestedLink
}
+
}
final class WKSourceEditorTextFrameworkMediator: NSObject {
@@ -61,6 +65,7 @@ final class WKSourceEditorTextFrameworkMediator: NSObject {
private(set) var listFormatter: WKSourceEditorFormatterList?
private(set) var headingFormatter: WKSourceEditorFormatterHeading?
private(set) var strikethroughFormatter: WKSourceEditorFormatterStrikethrough?
+ private(set) var linkFormatter: WKSourceEditorFormatterLink?
var isSyntaxHighlightingEnabled: Bool = true {
didSet {
@@ -133,19 +138,21 @@ final class WKSourceEditorTextFrameworkMediator: NSObject {
let listFormatter = WKSourceEditorFormatterList(colors: colors, fonts: fonts)
let headingFormatter = WKSourceEditorFormatterHeading(colors: colors, fonts: fonts)
let strikethroughFormatter = WKSourceEditorFormatterStrikethrough(colors: colors, fonts: fonts)
+ let linkFormatter = WKSourceEditorFormatterLink(colors: colors, fonts: fonts)
self.formatters = [WKSourceEditorFormatterBase(colors: colors, fonts: fonts, textAlignment: viewModel.textAlignment),
templateFormatter,
boldItalicsFormatter,
referenceFormatter,
listFormatter,
headingFormatter,
- strikethroughFormatter]
+ strikethroughFormatter, linkFormatter]
self.boldItalicsFormatter = boldItalicsFormatter
self.templateFormatter = templateFormatter
self.referenceFormatter = referenceFormatter
self.listFormatter = listFormatter
self.headingFormatter = headingFormatter
self.strikethroughFormatter = strikethroughFormatter
+ self.linkFormatter = linkFormatter
if needsTextKit2 {
if #available(iOS 16.0, *) {
@@ -185,7 +192,8 @@ final class WKSourceEditorTextFrameworkMediator: NSObject {
if needsTextKit2 {
guard let textKit2Data = textkit2SelectionData(selectedDocumentRange: selectedDocumentRange) else {
- return WKSourceEditorSelectionState(isBold: false, isItalics: false, isHorizontalTemplate: false, isHorizontalReference: false, isBulletSingleList: false, isBulletMultipleList: false, isNumberSingleList: false, isNumberMultipleList: false, isHeading: false, isSubheading1: false, isSubheading2: false, isSubheading3: false, isSubheading4: false, isStrikethrough: false)
+ return WKSourceEditorSelectionState(isBold: false, isItalics: false, isHorizontalTemplate: false, isHorizontalReference: false, isBulletSingleList: false, isBulletMultipleList: false, isNumberSingleList: false, isNumberMultipleList: false, isHeading: false, isSubheading1: false, isSubheading2: false, isSubheading3: false, isSubheading4: false, isStrikethrough: false, isSimpleLink: false, isLinkWithNestedLink: false)
+
}
let isBold = boldItalicsFormatter?.attributedString(textKit2Data.paragraphAttributedString, isBoldIn: textKit2Data.paragraphSelectedRange) ?? false
@@ -202,13 +210,15 @@ final class WKSourceEditorTextFrameworkMediator: NSObject {
let isSubheading3 = headingFormatter?.attributedString(textKit2Data.paragraphAttributedString, isSubheading3In: textKit2Data.paragraphSelectedRange) ?? false
let isSubheading4 = headingFormatter?.attributedString(textKit2Data.paragraphAttributedString, isSubheading4In: textKit2Data.paragraphSelectedRange) ?? false
let isStrikethrough = strikethroughFormatter?.attributedString(textKit2Data.paragraphAttributedString, isStrikethroughIn: textKit2Data.paragraphSelectedRange) ?? false
-
- return WKSourceEditorSelectionState(isBold: isBold, isItalics: isItalics, isHorizontalTemplate: isHorizontalTemplate, isHorizontalReference: isHorizontalReference, isBulletSingleList: isBulletSingleList, isBulletMultipleList: isBulletMultipleList, isNumberSingleList: isNumberSingleList, isNumberMultipleList: isNumberMultipleList, isHeading: isHeading, isSubheading1: isSubheading1, isSubheading2: isSubheading2, isSubheading3: isSubheading3, isSubheading4: isSubheading4, isStrikethrough: isStrikethrough)
+ let isSimpleLink = linkFormatter?.attributedString(textKit2Data.paragraphAttributedString, isSimpleLinkIn: textKit2Data.paragraphSelectedRange) ?? false
+ let isLinkWithNestedLink = linkFormatter?.attributedString(textKit2Data.paragraphAttributedString, isLinkWithNestedLinkIn: textKit2Data.paragraphSelectedRange) ?? false
+
+ return WKSourceEditorSelectionState(isBold: isBold, isItalics: isItalics, isHorizontalTemplate: isHorizontalTemplate, isHorizontalReference: isHorizontalReference, isBulletSingleList: isBulletSingleList, isBulletMultipleList: isBulletMultipleList, isNumberSingleList: isNumberSingleList, isNumberMultipleList: isNumberMultipleList, isHeading: isHeading, isSubheading1: isSubheading1, isSubheading2: isSubheading2, isSubheading3: isSubheading3, isSubheading4: isSubheading4, isStrikethrough: isStrikethrough, isSimpleLink: isSimpleLink, isLinkWithNestedLink: isLinkWithNestedLink)
} else {
guard let textKit1Storage else {
- return WKSourceEditorSelectionState(isBold: false, isItalics: false, isHorizontalTemplate: false, isHorizontalReference: false, isBulletSingleList: false, isBulletMultipleList: false, isNumberSingleList: false, isNumberMultipleList: false, isHeading: false, isSubheading1: false, isSubheading2: false, isSubheading3: false, isSubheading4: false, isStrikethrough: false)
- }
-
+ return WKSourceEditorSelectionState(isBold: false, isItalics: false, isHorizontalTemplate: false, isHorizontalReference: false, isBulletSingleList: false, isBulletMultipleList: false, isNumberSingleList: false, isNumberMultipleList: false, isHeading: false, isSubheading1: false, isSubheading2: false, isSubheading3: false, isSubheading4: false, isStrikethrough: false, isSimpleLink: false, isLinkWithNestedLink: false)
+ }
+
let isBold = boldItalicsFormatter?.attributedString(textKit1Storage, isBoldIn: selectedDocumentRange) ?? false
let isItalics = boldItalicsFormatter?.attributedString(textKit1Storage, isItalicsIn: selectedDocumentRange) ?? false
let isHorizontalTemplate = templateFormatter?.attributedString(textKit1Storage, isHorizontalTemplateIn: selectedDocumentRange) ?? false
@@ -223,8 +233,10 @@ final class WKSourceEditorTextFrameworkMediator: NSObject {
let isSubheading3 = headingFormatter?.attributedString(textKit1Storage, isSubheading3In: selectedDocumentRange) ?? false
let isSubheading4 = headingFormatter?.attributedString(textKit1Storage, isSubheading4In: selectedDocumentRange) ?? false
let isStrikethrough = strikethroughFormatter?.attributedString(textKit1Storage, isStrikethroughIn: selectedDocumentRange) ?? false
-
- return WKSourceEditorSelectionState(isBold: isBold, isItalics: isItalics, isHorizontalTemplate: isHorizontalTemplate, isHorizontalReference: isHorizontalReference, isBulletSingleList: isBulletSingleList, isBulletMultipleList: isBulletMultipleList, isNumberSingleList: isNumberSingleList, isNumberMultipleList: isNumberMultipleList, isHeading: isHeading, isSubheading1: isSubheading1, isSubheading2: isSubheading2, isSubheading3: isSubheading3, isSubheading4: isSubheading4, isStrikethrough: isStrikethrough)
+ let isSimpleLink = linkFormatter?.attributedString(textKit1Storage, isSimpleLinkIn: selectedDocumentRange) ?? false
+ let isLinkWithNestedLink = linkFormatter?.attributedString(textKit1Storage, isLinkWithNestedLinkIn: selectedDocumentRange) ?? false
+
+ return WKSourceEditorSelectionState(isBold: isBold, isItalics: isItalics, isHorizontalTemplate: isHorizontalTemplate, isHorizontalReference: isHorizontalReference, isBulletSingleList: isBulletSingleList, isBulletMultipleList: isBulletMultipleList, isNumberSingleList: isNumberSingleList, isNumberMultipleList: isNumberMultipleList, isHeading: isHeading, isSubheading1: isSubheading1, isSubheading2: isSubheading2, isSubheading3: isSubheading3, isSubheading4: isSubheading4, isStrikethrough: isStrikethrough, isSimpleLink: isSimpleLink, isLinkWithNestedLink: isLinkWithNestedLink)
}
}
@@ -262,6 +274,7 @@ extension WKSourceEditorTextFrameworkMediator: WKSourceEditorStorageDelegate {
colors.orangeForegroundColor = isSyntaxHighlightingEnabled ? WKAppEnvironment.current.theme.editorOrange : WKAppEnvironment.current.theme.text
colors.purpleForegroundColor = isSyntaxHighlightingEnabled ? WKAppEnvironment.current.theme.editorPurple : WKAppEnvironment.current.theme.text
colors.greenForegroundColor = isSyntaxHighlightingEnabled ? WKAppEnvironment.current.theme.editorGreen : WKAppEnvironment.current.theme.text
+ colors.blueForegroundColor = isSyntaxHighlightingEnabled ? WKAppEnvironment.current.theme.editorBlue : WKAppEnvironment.current.theme.text
return colors
}
diff --git a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift
index 47122157c14..95555725aa5 100644
--- a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift
+++ b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift
@@ -4,6 +4,8 @@ import UIKit
public protocol WKSourceEditorViewControllerDelegate: AnyObject {
func sourceEditorViewControllerDidTapFind(sourceEditorViewController: WKSourceEditorViewController)
func sourceEditorViewControllerDidRemoveFindInputAccessoryView(sourceEditorViewController: WKSourceEditorViewController)
+ func sourceEditorViewControllerDidTapLink(parameters: WKSourceEditorFormatterLinkWizardParameters)
+ func sourceEditorViewControllerDidTapImage()
}
// MARK: NSNotification Names
@@ -32,6 +34,7 @@ public class WKSourceEditorViewController: WKComponentViewController {
private let viewModel: WKSourceEditorViewModel
private weak var delegate: WKSourceEditorViewControllerDelegate?
private let textFrameworkMediator: WKSourceEditorTextFrameworkMediator
+ private var preselectedTextRange: UITextRange?
var textView: UITextView {
return textFrameworkMediator.textView
@@ -186,6 +189,43 @@ public class WKSourceEditorViewController: WKComponentViewController {
viewModel.isSyntaxHighlightingEnabled.toggle()
update(viewModel: viewModel)
}
+
+ public func insertLink(pageTitle: String) {
+
+ guard let preselectedTextRange else {
+ return
+ }
+
+ textFrameworkMediator.linkFormatter?.insertLink(in: textView, pageTitle: pageTitle, preselectedTextRange: preselectedTextRange)
+
+ self.preselectedTextRange = nil
+ }
+
+ public func editLink(newPageTitle: String, newPageLabel: String?) {
+
+ guard let preselectedTextRange else {
+ return
+ }
+
+ textFrameworkMediator.linkFormatter?.editLink(in: textView, newPageTitle: newPageTitle, newPageLabel: newPageLabel, preselectedTextRange: preselectedTextRange)
+
+ self.preselectedTextRange = nil
+ }
+
+ public func removeLink() {
+
+ guard let preselectedTextRange else {
+ return
+ }
+
+ textFrameworkMediator.linkFormatter?.removeLink(in: textView, preselectedTextRange: preselectedTextRange)
+
+ self.preselectedTextRange = nil
+ }
+
+ public func insertImage(wikitext: String) {
+ textFrameworkMediator.linkFormatter?.insertImage(wikitext: wikitext, in: textView)
+ }
}
// MARK: - Private
@@ -227,6 +267,19 @@ private extension WKSourceEditorViewController {
NotificationCenter.default.post(name: Notification.WKSourceEditorSelectionState, object: nil, userInfo: [Notification.WKSourceEditorSelectionStateKey: selectionState])
}
}
+
+ func presentLinkWizard(linkButtonIsSelected: Bool) {
+
+ let action: WKSourceEditorFormatterLinkButtonAction = linkButtonIsSelected ? .edit : .insert
+
+ guard let parameters = textFrameworkMediator.linkFormatter?.linkWizardParameters(action: action, in: textView) else {
+ return
+ }
+
+ // For some reason the editor text view can lose its selectedTextRange when presenting the link wizard, which we need in the formatter button action extension to determine how to change the text after wizard dismissal. We keep track of it here and send it back into the formatter later.
+ self.preselectedTextRange = parameters.preselectedTextRange
+ delegate?.sourceEditorViewControllerDidTapLink(parameters: parameters)
+ }
}
// MARK: - UITextViewDelegate
@@ -261,11 +314,19 @@ extension WKSourceEditorViewController: WKEditorToolbarExpandingViewDelegate {
let action: WKSourceEditorFormatterButtonAction = isSelected ? .remove : .add
textFrameworkMediator.templateFormatter?.toggleTemplateFormatting(action: action, in: textView)
}
-
+
func toolbarExpandingViewDidTapReference(toolbarView: WKEditorToolbarExpandingView, isSelected: Bool) {
let action: WKSourceEditorFormatterButtonAction = isSelected ? .remove : .add
textFrameworkMediator.referenceFormatter?.toggleReferenceFormatting(action: action, in: textView)
}
+
+ func toolbarExpandingViewDidTapLink(toolbarView: WKEditorToolbarExpandingView, isSelected: Bool) {
+ presentLinkWizard(linkButtonIsSelected: isSelected)
+ }
+
+ func toolbarExpandingViewDidTapImage(toolbarView: WKEditorToolbarExpandingView) {
+ delegate?.sourceEditorViewControllerDidTapImage()
+ }
func toolbarExpandingViewDidTapUnorderedList(toolbarView: WKEditorToolbarExpandingView, isSelected: Bool) {
let action: WKSourceEditorFormatterButtonAction = isSelected ? .remove : .add
@@ -289,7 +350,7 @@ extension WKSourceEditorViewController: WKEditorToolbarExpandingViewDelegate {
// MARK: - WKEditorToolbarHighlightViewDelegate
extension WKSourceEditorViewController: WKEditorToolbarHighlightViewDelegate {
-
+
func toolbarHighlightViewDidTapBold(toolbarView: WKEditorToolbarHighlightView, isSelected: Bool) {
let action: WKSourceEditorFormatterButtonAction = isSelected ? .remove : .add
textFrameworkMediator.boldItalicsFormatter?.toggleBoldFormatting(action: action, in: textView)
@@ -304,11 +365,16 @@ extension WKSourceEditorViewController: WKEditorToolbarHighlightViewDelegate {
let action: WKSourceEditorFormatterButtonAction = isSelected ? .remove : .add
textFrameworkMediator.templateFormatter?.toggleTemplateFormatting(action: action, in: textView)
}
-
+
func toolbarHighlightViewDidTapReference(toolbarView: WKEditorToolbarHighlightView, isSelected: Bool) {
let action: WKSourceEditorFormatterButtonAction = isSelected ? .remove : .add
textFrameworkMediator.referenceFormatter?.toggleReferenceFormatting(action: action, in: textView)
}
+
+ func toolbarHighlightViewDidTapLink(toolbarView: WKEditorToolbarHighlightView, isSelected: Bool) {
+ presentLinkWizard(linkButtonIsSelected: isSelected)
+
+ }
func toolbarHighlightViewDidTapShowMore(toolbarView: WKEditorToolbarHighlightView) {
editorInputViewIsShowing = true
@@ -366,6 +432,10 @@ extension WKSourceEditorViewController: WKEditorInputViewDelegate {
textFrameworkMediator.strikethroughFormatter?.toggleStrikethroughFormatting(action: action, in: textView)
}
+ func didTapLink(isSelected: Bool) {
+ presentLinkWizard(linkButtonIsSelected: isSelected)
+ }
+
func didTapClose() {
editorInputViewIsShowing = false
let isRangeSelected = textView.selectedRange.length > 0
diff --git a/Components/Sources/Components/Style/WKTheme.swift b/Components/Sources/Components/Style/WKTheme.swift
index e5751d2af00..5d894d75da0 100644
--- a/Components/Sources/Components/Style/WKTheme.swift
+++ b/Components/Sources/Components/Style/WKTheme.swift
@@ -27,6 +27,7 @@ public struct WKTheme: Equatable {
public let editorOrange: UIColor
public let editorPurple: UIColor
public let editorGreen: UIColor
+ public let editorBlue: UIColor
public static let light = WKTheme(
name: "Light",
@@ -52,7 +53,8 @@ public struct WKTheme: Equatable {
diffCompareAccent: WKColor.orange600,
editorOrange: WKColor.orange600,
editorPurple: WKColor.purple600,
- editorGreen: WKColor.green600
+ editorGreen: WKColor.green600,
+ editorBlue: WKColor.blue600
)
public static let sepia = WKTheme(
@@ -79,7 +81,8 @@ public struct WKTheme: Equatable {
diffCompareAccent: WKColor.orange600,
editorOrange: WKColor.orange600,
editorPurple: WKColor.purple600,
- editorGreen: WKColor.green600
+ editorGreen: WKColor.green600,
+ editorBlue: WKColor.blue600
)
public static let dark = WKTheme(
@@ -106,7 +109,8 @@ public struct WKTheme: Equatable {
diffCompareAccent: WKColor.orange600,
editorOrange: WKColor.yellow600,
editorPurple: WKColor.red100,
- editorGreen: WKColor.green600
+ editorGreen: WKColor.green600,
+ editorBlue: WKColor.blue300
)
public static let black = WKTheme(
@@ -133,7 +137,8 @@ public struct WKTheme: Equatable {
diffCompareAccent: WKColor.orange600,
editorOrange: WKColor.yellow600,
editorPurple: WKColor.red100,
- editorGreen: WKColor.green600
+ editorGreen: WKColor.green600,
+ editorBlue: WKColor.blue300
)
}
diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorColors.h b/Components/Sources/ComponentsObjC/WKSourceEditorColors.h
index 9b3bf77a05f..ca3a79843fd 100644
--- a/Components/Sources/ComponentsObjC/WKSourceEditorColors.h
+++ b/Components/Sources/ComponentsObjC/WKSourceEditorColors.h
@@ -7,6 +7,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, strong) UIColor *orangeForegroundColor;
@property (nonatomic, strong) UIColor *purpleForegroundColor;
@property (nonatomic, strong) UIColor *greenForegroundColor;
+@property (nonatomic, strong) UIColor *blueForegroundColor;
@end
NS_ASSUME_NONNULL_END
diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterLink.h b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterLink.h
new file mode 100644
index 00000000000..321d69279e5
--- /dev/null
+++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterLink.h
@@ -0,0 +1,10 @@
+#import "WKSourceEditorFormatter.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface WKSourceEditorFormatterLink : WKSourceEditorFormatter
+- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isSimpleLinkInRange:(NSRange)range;
+- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isLinkWithNestedLinkInRange:(NSRange)range;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterLink.m b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterLink.m
new file mode 100644
index 00000000000..e3547df8c5e
--- /dev/null
+++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterLink.m
@@ -0,0 +1,243 @@
+#import "WKSourceEditorFormatterLink.h"
+#import "WKSourceEditorColors.h"
+
+@interface WKSourceEditorFormatterLink ()
+
+@property (nonatomic, strong) NSDictionary *simpleLinkAttributes;
+@property (nonatomic, strong) NSDictionary *linkWithNestedLinkAttributes;
+@property (nonatomic, strong) NSRegularExpression *simpleLinkRegex;
+@property (nonatomic, strong) NSRegularExpression *linkWithNestedLinkRegex;
+
+@end
+
+#pragma mark - Custom Attributed String Keys
+
+NSString * const WKSourceEditorCustomKeyColorBlue = @"WKSourceEditorCustomKeyColorBlue";
+NSString * const WKSourceEditorCustomKeyLink = @"WKSourceEditorCustomKeyLink";
+NSString * const WKSourceEditorCustomKeyLinkWithNestedLink = @"WKSourceEditorCustomKeyLinkWithNestedLink";
+
+@implementation WKSourceEditorFormatterLink
+
+#pragma mark - Public
+
+- (instancetype)initWithColors:(nonnull WKSourceEditorColors *)colors fonts:(nonnull WKSourceEditorFonts *)fonts {
+ self = [super initWithColors:colors fonts:fonts];
+ if (self) {
+ _simpleLinkAttributes = @{
+ WKSourceEditorCustomKeyLink: [NSNumber numberWithBool:YES],
+ NSForegroundColorAttributeName: colors.blueForegroundColor,
+ WKSourceEditorCustomKeyColorBlue: [NSNumber numberWithBool:YES]
+ };
+
+ _simpleLinkRegex = [[NSRegularExpression alloc] initWithPattern:@"(\\[{2})([^\\[\\]\\n]*)(\\]{2})" options:0 error:nil];
+
+ _linkWithNestedLinkAttributes = @{
+ WKSourceEditorCustomKeyLinkWithNestedLink: [NSNumber numberWithBool:YES],
+ NSForegroundColorAttributeName: colors.blueForegroundColor,
+ WKSourceEditorCustomKeyColorBlue: [NSNumber numberWithBool:YES]
+ };
+
+ _linkWithNestedLinkRegex = [[NSRegularExpression alloc] initWithPattern:@"\\[{2}[^\\[\\]\\n]*\\[{2}" options:0 error:nil];
+ }
+
+ return self;
+}
+
+#pragma mark - Overrides
+
+- (void)addSyntaxHighlightingToAttributedString:(nonnull NSMutableAttributedString *)attributedString inRange:(NSRange)range {
+
+ // Reset
+ [attributedString removeAttribute:WKSourceEditorCustomKeyColorBlue range:range];
+ [attributedString removeAttribute:WKSourceEditorCustomKeyLink range:range];
+ [attributedString removeAttribute:WKSourceEditorCustomKeyLinkWithNestedLink range:range];
+
+ // This section finds and highlights simple links that do NOT contain nested links, e.g. [[Cat]] and [[Dog|puppy]].
+ [self.simpleLinkRegex enumerateMatchesInString:attributedString.string
+ options:0
+ range:range
+ usingBlock:^(NSTextCheckingResult *_Nullable result, NSMatchingFlags flags, BOOL *_Nonnull stop) {
+ NSRange fullMatch = [result rangeAtIndex:0];
+ NSRange openingRange = [result rangeAtIndex:1];
+ NSRange contentRange = [result rangeAtIndex:2];
+ NSRange closingRange = [result rangeAtIndex:3];
+
+ if (openingRange.location != NSNotFound) {
+ [attributedString addAttributes:self.simpleLinkAttributes range:openingRange];
+ }
+
+ if (contentRange.location != NSNotFound) {
+ [attributedString addAttributes:self.simpleLinkAttributes range:contentRange];
+ }
+
+ if (closingRange.location != NSNotFound) {
+ [attributedString addAttributes:self.simpleLinkAttributes range:closingRange];
+ }
+ }];
+
+ // Note: This section finds and highlights links with nested links, which is common in image links. The regex matches any opening markup [[ followed by non-markup characters, then another opening markup [[. We then start to loop character-by-character, matching opening and closing tags to find and highlight links that contain other links.
+ // Originally I tried to allow for infinite nested links via regex alone, but it performed too poorly.
+ [self.linkWithNestedLinkRegex enumerateMatchesInString:attributedString.string
+ options:0
+ range:range
+ usingBlock:^(NSTextCheckingResult *_Nullable result, NSMatchingFlags flags, BOOL *_Nonnull stop) {
+
+ NSRange match = [result rangeAtIndex:0];
+
+ if (match.location != NSNotFound) {
+
+ NSArray *linkWithNestedLinkRanges = [self linkWithNestedLinkRangesInString:attributedString.string startingIndex:match.location];
+
+ for (NSValue *value in linkWithNestedLinkRanges) {
+ NSRange range = [value rangeValue];
+ if (range.location != NSNotFound) {
+ [attributedString addAttributes:self.linkWithNestedLinkAttributes range:range];
+ }
+ }
+ }
+ }];
+}
+
+- (void)updateColors:(WKSourceEditorColors *)colors inAttributedString:(NSMutableAttributedString *)attributedString inRange:(NSRange)range {
+
+ NSMutableDictionary *mutSimpleLinkAttributes = [[NSMutableDictionary alloc] initWithDictionary:self.simpleLinkAttributes];
+ [mutSimpleLinkAttributes setObject:colors.blueForegroundColor forKey:NSForegroundColorAttributeName];
+ self.simpleLinkAttributes = [[NSDictionary alloc] initWithDictionary:mutSimpleLinkAttributes];
+
+ NSMutableDictionary *mutLinkWithNestedLinkAttributes = [[NSMutableDictionary alloc] initWithDictionary:self.linkWithNestedLinkAttributes];
+ [mutLinkWithNestedLinkAttributes setObject:colors.blueForegroundColor forKey:NSForegroundColorAttributeName];
+ self.linkWithNestedLinkAttributes = [[NSDictionary alloc] initWithDictionary:mutLinkWithNestedLinkAttributes];
+
+ [attributedString enumerateAttribute:WKSourceEditorCustomKeyColorBlue
+ inRange:range
+ options:nil
+ usingBlock:^(id value, NSRange localRange, BOOL *stop) {
+ if ([value isKindOfClass: [NSNumber class]]) {
+ NSNumber *numValue = (NSNumber *)value;
+ if ([numValue boolValue] == YES) {
+ [attributedString addAttributes:@{NSForegroundColorAttributeName: colors.blueForegroundColor} range:localRange];
+ }
+ }
+ }];
+}
+
+- (void)updateFonts:(WKSourceEditorFonts *)fonts inAttributedString:(NSMutableAttributedString *)attributedString inRange:(NSRange)range {
+ // No special font handling needed
+}
+
+#pragma mark - Public
+
+- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isSimpleLinkInRange:(NSRange)range {
+ return [self attributedString:attributedString isKey:WKSourceEditorCustomKeyLink inRange:range];
+}
+
+- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isLinkWithNestedLinkInRange:(NSRange)range {
+
+ return [self attributedString:attributedString isKey:WKSourceEditorCustomKeyLinkWithNestedLink inRange:range];
+}
+
+#pragma mark - Private
+
+- (NSArray *)linkWithNestedLinkRangesInString: (NSString *)string startingIndex: (NSUInteger)index {
+ NSMutableArray *openingRanges = [[NSMutableArray alloc] init];
+ NSMutableArray *completedLinkRanges = [[NSMutableArray alloc] init];
+ NSMutableArray *completedLinkWithNestedLinkRanges = [[NSMutableArray alloc] init];
+
+ // Loop through and evaluate characters in pairs, keeping track of opening and closing pairs
+ BOOL lastCompletedLinkRangeWasNested = NO;
+ for (NSUInteger i = index; i < string.length; i++) {
+
+ unichar currentChar = [string characterAtIndex:i];
+
+ if (currentChar == '\n') {
+ break;
+ }
+
+ if (i + 1 >= string.length) {
+ break;
+ }
+
+ NSString *currentCharString = [NSString stringWithFormat:@"%c", currentChar];
+ unichar nextChar = [string characterAtIndex:i + 1];
+ NSString *nextCharString = [NSString stringWithFormat:@"%c", nextChar];
+ NSString *pair = [NSString stringWithFormat:@"%@%@", currentCharString, nextCharString];
+
+ if ([pair isEqualToString:@"[["]) {
+ [openingRanges addObject:[NSValue valueWithRange:NSMakeRange(i, 2)]];
+ }
+
+ if ([pair isEqualToString:@"]]"] && openingRanges.count == 0) {
+ // invalid, closed markup before opening
+ break;
+ }
+
+ if ([pair isEqualToString:@"]]"]) {
+
+ NSValue *lastOpeningRange = openingRanges.lastObject;
+ if (lastOpeningRange) {
+ [openingRanges removeLastObject];
+ }
+
+ NSRange unionRange = NSUnionRange(lastOpeningRange.rangeValue, NSMakeRange(i, 2));
+ NSValue *linkRange = [NSValue valueWithRange:unionRange];
+ [completedLinkRanges addObject: linkRange];
+
+ if (lastCompletedLinkRangeWasNested && openingRanges.count == 0) {
+ [completedLinkWithNestedLinkRanges addObject:linkRange];
+ }
+
+ if (openingRanges.count > 0) {
+ lastCompletedLinkRangeWasNested = YES;
+ } else {
+ lastCompletedLinkRangeWasNested = NO;
+ }
+ }
+ }
+
+ return [[NSArray alloc] initWithArray:completedLinkWithNestedLinkRanges];
+}
+
+- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isKey:(NSString *)key inRange:(NSRange)range {
+ __block BOOL isKey = NO;
+ if (range.length == 0) {
+
+ if (attributedString.length > range.location) {
+ NSDictionary *attrs = [attributedString attributesAtIndex:range.location effectiveRange:nil];
+
+ if (attrs[key] != nil) {
+ isKey = YES;
+ }
+
+ // Edge case, check previous character if we are up against opening markup
+ if (attrs[WKSourceEditorCustomKeyLink]) {
+ if (attributedString.length > range.location - 1) {
+ attrs = [attributedString attributesAtIndex:range.location - 1 effectiveRange:nil];
+ if (attrs[key] == nil) {
+ isKey = NO;
+ }
+ }
+ }
+ }
+
+ } else {
+ __block NSRange unionRange = NSMakeRange(NSNotFound, 0);
+ [attributedString enumerateAttributesInRange:range options:nil usingBlock:^(NSDictionary * _Nonnull attrs, NSRange loopRange, BOOL * _Nonnull stop) {
+ if (attrs[key] != nil) {
+ if (unionRange.location == NSNotFound) {
+ unionRange = loopRange;
+ } else {
+ unionRange = NSUnionRange(unionRange, loopRange);
+ }
+ stop = YES;
+ }
+ }];
+
+ if (NSEqualRanges(unionRange, range)) {
+ isKey = YES;
+ }
+ }
+
+ return isKey;
+}
+
+@end
diff --git a/Components/Sources/ComponentsObjC/include/ComponentsObjC.h b/Components/Sources/ComponentsObjC/include/ComponentsObjC.h
index 79aa7af983a..c7ef8ee727c 100644
--- a/Components/Sources/ComponentsObjC/include/ComponentsObjC.h
+++ b/Components/Sources/ComponentsObjC/include/ComponentsObjC.h
@@ -12,6 +12,7 @@
#import "WKSourceEditorFormatterList.h"
#import "WKSourceEditorFormatterHeading.h"
#import "WKSourceEditorFormatterStrikethrough.h"
+#import "WKSourceEditorFormatterLink.h"
#import "WKSourceEditorStorageDelegate.h"
#endif /* Header_h */
diff --git a/Components/Sources/ComponentsObjC/include/WKSourceEditorFormatterLink.h b/Components/Sources/ComponentsObjC/include/WKSourceEditorFormatterLink.h
new file mode 120000
index 00000000000..aec15185f91
--- /dev/null
+++ b/Components/Sources/ComponentsObjC/include/WKSourceEditorFormatterLink.h
@@ -0,0 +1 @@
+../WKSourceEditorFormatterLink.h
\ No newline at end of file
diff --git a/Components/Tests/ComponentsTests/WKSourceEditorFormatterButtonActionTests.swift b/Components/Tests/ComponentsTests/WKSourceEditorFormatterButtonActionTests.swift
index 25be4c14238..94f3420f164 100644
--- a/Components/Tests/ComponentsTests/WKSourceEditorFormatterButtonActionTests.swift
+++ b/Components/Tests/ComponentsTests/WKSourceEditorFormatterButtonActionTests.swift
@@ -386,4 +386,123 @@ final class WKSourceEditorFormatterButtonActionTests: XCTestCase {
mediator.strikethroughFormatter?.toggleStrikethroughFormatting(action: .remove, in: mediator.textView)
XCTAssertEqual(mediator.textView.attributedText.string, "One Two Three Four")
}
+
+ func testLinkWizardParametersEdit() throws {
+ let text = "Testing [[Cat]] Testing"
+ mediator.textView.attributedText = NSAttributedString(string: text)
+ mediator.textView.selectedRange = NSRange(location: 10, length:3)
+ let wizardParameters = mediator.linkFormatter?.linkWizardParameters(action: .edit, in: mediator.textView)
+ XCTAssertEqual(wizardParameters?.editPageTitle, "Cat")
+ XCTAssertNil(wizardParameters?.editPageLabel)
+ XCTAssertEqual(wizardParameters?.preselectedTextRange, mediator.textView.selectedTextRange)
+ }
+
+ func testLinkWizardParametersEditWithLabel() throws {
+ let text = "Testing [[Cat|Kitty]] Testing"
+ mediator.textView.attributedText = NSAttributedString(string: text)
+ mediator.textView.selectedRange = NSRange(location: 10, length:3)
+ let wizardParameters = mediator.linkFormatter?.linkWizardParameters(action: .edit, in: mediator.textView)
+ XCTAssertEqual(wizardParameters?.editPageTitle, "Cat")
+ XCTAssertEqual(wizardParameters?.editPageLabel, "Kitty")
+ XCTAssertEqual(wizardParameters?.preselectedTextRange, mediator.textView.selectedTextRange)
+ }
+
+ func testLinkWizardParametersInsert() throws {
+ let text = "Testing Cat Testing"
+ mediator.textView.attributedText = NSAttributedString(string: text)
+ mediator.textView.selectedRange = NSRange(location: 8, length:3)
+ let wizardParameters = mediator.linkFormatter?.linkWizardParameters(action: .insert, in: mediator.textView)
+ XCTAssertEqual(wizardParameters?.insertSearchTerm, "Cat")
+ XCTAssertEqual(wizardParameters?.preselectedTextRange, mediator.textView.selectedTextRange)
+ }
+
+ func testLinkInsert() {
+ let text = "One Two Three Four"
+ let textView = mediator.textView
+ textView.attributedText = NSAttributedString(string: text)
+
+ guard let startPos = textView.position(from: textView.beginningOfDocument, offset: 4),
+ let endPos = textView.position(from: textView.beginningOfDocument, offset: 7),
+ let preselectedTextRange = textView.textRange(from: startPos, to: endPos) else {
+ XCTFail("Failure creating preselectedTextRange")
+ return
+ }
+
+ mediator.linkFormatter?.insertLink(in: textView, pageTitle: "Two", preselectedTextRange: preselectedTextRange)
+ XCTAssertEqual(mediator.textView.attributedText.string, "One [[Two]] Three Four")
+ }
+
+ func testLinkEdit() {
+ let text = "One Two [[Three]] Four"
+ let textView = mediator.textView
+ textView.attributedText = NSAttributedString(string: text)
+
+ guard let startPos = textView.position(from: textView.beginningOfDocument, offset: 10),
+ let endPos = textView.position(from: textView.beginningOfDocument, offset: 15),
+ let preselectedTextRange = textView.textRange(from: startPos, to: endPos) else {
+ XCTFail("Failure creating preselectedTextRange")
+ return
+ }
+
+ mediator.linkFormatter?.editLink(in: textView, newPageTitle: "Five", newPageLabel: nil, preselectedTextRange: preselectedTextRange)
+ XCTAssertEqual(mediator.textView.attributedText.string, "One Two [[Five]] Four")
+ }
+
+ func testLinkEditWithLabel() {
+ let text = "One Two [[Three]] Four"
+ let textView = mediator.textView
+ textView.attributedText = NSAttributedString(string: text)
+
+ guard let startPos = textView.position(from: textView.beginningOfDocument, offset: 10),
+ let endPos = textView.position(from: textView.beginningOfDocument, offset: 15),
+ let preselectedTextRange = textView.textRange(from: startPos, to: endPos) else {
+ XCTFail("Failure creating preselectedTextRange")
+ return
+ }
+
+ mediator.linkFormatter?.editLink(in: textView, newPageTitle: "Five", newPageLabel: "fiver", preselectedTextRange: preselectedTextRange)
+ XCTAssertEqual(mediator.textView.attributedText.string, "One Two [[Five|fiver]] Four")
+ }
+
+ func testLinkRemove() {
+ let text = "One Two [[Three]] Four"
+ let textView = mediator.textView
+ textView.attributedText = NSAttributedString(string: text)
+
+ guard let startPos = textView.position(from: textView.beginningOfDocument, offset: 10),
+ let endPos = textView.position(from: textView.beginningOfDocument, offset: 15),
+ let preselectedTextRange = textView.textRange(from: startPos, to: endPos) else {
+ XCTFail("Failure creating preselectedTextRange")
+ return
+ }
+
+ mediator.linkFormatter?.removeLink(in: textView, preselectedTextRange: preselectedTextRange)
+ XCTAssertEqual(mediator.textView.attributedText.string, "One Two Three Four")
+ }
+
+ func testLinkRemoveWithLabel() {
+ let text = "One Two [[Three|3]] Four"
+ let textView = mediator.textView
+ textView.attributedText = NSAttributedString(string: text)
+
+ guard let startPos = textView.position(from: textView.beginningOfDocument, offset: 10),
+ let endPos = textView.position(from: textView.beginningOfDocument, offset: 17),
+ let preselectedTextRange = textView.textRange(from: startPos, to: endPos) else {
+ XCTFail("Failure creating preselectedTextRange")
+ return
+ }
+
+ mediator.linkFormatter?.removeLink(in: textView, preselectedTextRange: preselectedTextRange)
+ XCTAssertEqual(mediator.textView.attributedText.string, "One Two Three|3 Four")
+ }
+
+ func testLinkInsertImage() {
+ let text = "One Two Three Four"
+ let textView = mediator.textView
+ textView.attributedText = NSAttributedString(string: text)
+ mediator.textView.selectedRange = NSRange(location: 8, length:0)
+
+ mediator.linkFormatter?.insertImage(wikitext: "[[File:Cat November 2010-1a.jpg | thumb | 220x124px | right]]", in: textView)
+ XCTAssertEqual(mediator.textView.attributedText.string, "One Two [[File:Cat November 2010-1a.jpg | thumb | 220x124px | right]]Three Four")
+ }
}
diff --git a/Components/Tests/ComponentsTests/WKSourceEditorFormatterTests.swift b/Components/Tests/ComponentsTests/WKSourceEditorFormatterTests.swift
index d65e0a39dd9..5f694f68043 100644
--- a/Components/Tests/ComponentsTests/WKSourceEditorFormatterTests.swift
+++ b/Components/Tests/ComponentsTests/WKSourceEditorFormatterTests.swift
@@ -14,8 +14,9 @@ final class WKSourceEditorFormatterTests: XCTestCase {
var listFormatter: WKSourceEditorFormatterList!
var headingFormatter: WKSourceEditorFormatterHeading!
var strikethroughFormatter: WKSourceEditorFormatterStrikethrough!
+ var linkFormatter: WKSourceEditorFormatterLink!
var formatters: [WKSourceEditorFormatter] {
- return [baseFormatter, templateFormatter, boldItalicsFormatter, referenceFormatter, listFormatter, headingFormatter, strikethroughFormatter]
+ return [baseFormatter, templateFormatter, boldItalicsFormatter, referenceFormatter, listFormatter, headingFormatter, strikethroughFormatter, linkFormatter]
}
override func setUpWithError() throws {
@@ -26,6 +27,7 @@ final class WKSourceEditorFormatterTests: XCTestCase {
self.colors.orangeForegroundColor = WKTheme.light.editorOrange
self.colors.purpleForegroundColor = WKTheme.light.editorPurple
self.colors.greenForegroundColor = WKTheme.light.editorGreen
+ self.colors.blueForegroundColor = WKTheme.light.editorBlue
self.fonts = WKSourceEditorFonts()
self.fonts.baseFont = WKFont.for(.body, compatibleWith: traitCollection)
@@ -45,6 +47,7 @@ final class WKSourceEditorFormatterTests: XCTestCase {
self.listFormatter = WKSourceEditorFormatterList(colors: colors, fonts: fonts)
self.headingFormatter = WKSourceEditorFormatterHeading(colors: colors, fonts: fonts)
self.strikethroughFormatter = WKSourceEditorFormatterStrikethrough(colors: colors, fonts: fonts)
+ self.linkFormatter = WKSourceEditorFormatterLink(colors: colors, fonts: fonts)
}
override func tearDownWithError() throws {
@@ -1312,6 +1315,7 @@ final class WKSourceEditorFormatterTests: XCTestCase {
func testStrikethrough() {
let string = "Testing. Strikethrough. Testing"
+
let mutAttributedString = NSMutableAttributedString(string: string)
for formatter in formatters {
diff --git a/Components/Tests/ComponentsTests/WKSourceEditorTests.swift b/Components/Tests/ComponentsTests/WKSourceEditorTests.swift
index a31a3cad7dc..a91a5e16d9b 100644
--- a/Components/Tests/ComponentsTests/WKSourceEditorTests.swift
+++ b/Components/Tests/ComponentsTests/WKSourceEditorTests.swift
@@ -34,6 +34,7 @@ final class WKSourceEditorTests: XCTestCase {
}
extension WKSourceEditorTests: WKSourceEditorViewControllerDelegate {
+
func sourceEditorViewControllerDidRemoveFindInputAccessoryView(sourceEditorViewController: Components.WKSourceEditorViewController) {
}
@@ -41,6 +42,14 @@ extension WKSourceEditorTests: WKSourceEditorViewControllerDelegate {
func sourceEditorViewControllerDidTapFind(sourceEditorViewController: Components.WKSourceEditorViewController) {
}
+
+ func sourceEditorViewControllerDidTapLink(parameters: Components.WKSourceEditorFormatterLinkWizardParameters) {
+
+ }
+
+ func sourceEditorViewControllerDidTapImage() {
+
+ }
}
extension WKSourceEditorLocalizedStrings {
diff --git a/Components/Tests/ComponentsTests/WKSourceEditorTextFrameworkMediatorTests.swift b/Components/Tests/ComponentsTests/WKSourceEditorTextFrameworkMediatorTests.swift
index 8b87b855aed..d35cbb2c6ca 100644
--- a/Components/Tests/ComponentsTests/WKSourceEditorTextFrameworkMediatorTests.swift
+++ b/Components/Tests/ComponentsTests/WKSourceEditorTextFrameworkMediatorTests.swift
@@ -369,4 +369,100 @@ final class WKSourceEditorTextFrameworkMediatorTests: XCTestCase {
XCTAssertTrue(selectionStates2.isStrikethrough)
XCTAssertFalse(selectionStates3.isStrikethrough)
}
+
+ func testLinkState() throws {
+ let text = "Testing [[Link with space]] Testing."
+ mediator.textView.attributedText = NSAttributedString(string: text)
+
+ let selectionStates1 = mediator.selectionState(selectedDocumentRange: NSRange(location: 0, length: 7))
+ let selectionStates2 = mediator.selectionState(selectedDocumentRange: NSRange(location: 10, length: 15))
+ let selectionStates3 = mediator.selectionState(selectedDocumentRange: NSRange(location: 28, length: 7))
+ XCTAssertFalse(selectionStates1.isSimpleLink)
+ XCTAssertFalse(selectionStates1.isLinkWithNestedLink)
+
+ XCTAssertTrue(selectionStates2.isSimpleLink)
+ XCTAssertFalse(selectionStates3.isLinkWithNestedLink)
+
+ XCTAssertFalse(selectionStates3.isSimpleLink)
+ XCTAssertFalse(selectionStates3.isLinkWithNestedLink)
+ }
+
+ func testLinkCursorState() throws {
+ let text = "Testing [[Link with space]] Testing."
+ mediator.textView.attributedText = NSAttributedString(string: text)
+
+ let selectionStates1 = mediator.selectionState(selectedDocumentRange: NSRange(location: 3, length: 0))
+ let selectionStates2 = mediator.selectionState(selectedDocumentRange: NSRange(location: 12, length: 0))
+ let selectionStates3 = mediator.selectionState(selectedDocumentRange: NSRange(location: 30, length: 0))
+ XCTAssertFalse(selectionStates1.isSimpleLink)
+ XCTAssertFalse(selectionStates1.isLinkWithNestedLink)
+
+ XCTAssertTrue(selectionStates2.isSimpleLink)
+ XCTAssertFalse(selectionStates2.isLinkWithNestedLink)
+
+ XCTAssertFalse(selectionStates3.isSimpleLink)
+ XCTAssertFalse(selectionStates3.isLinkWithNestedLink)
+ }
+
+ func testNestedLinkState() throws {
+ let text = "Test [[File:Cat with fish.jpg|thumb|left|Cat with [[fish]]|alt=Photo of cat looking at fish]] Test"
+ mediator.textView.attributedText = NSAttributedString(string: text)
+
+ let selectionStates1 = mediator.selectionState(selectedDocumentRange: NSRange(location: 0, length: 4))
+ let selectionStates2 = mediator.selectionState(selectedDocumentRange: NSRange(location: 12, length: 3))
+ let selectionStates3 = mediator.selectionState(selectedDocumentRange: NSRange(location: 52, length: 3))
+ let selectionStates4 = mediator.selectionState(selectedDocumentRange: NSRange(location: 72, length: 3))
+ let selectionStates5 = mediator.selectionState(selectedDocumentRange: NSRange(location: 94, length: 4))
+
+ // "Test"
+ XCTAssertFalse(selectionStates1.isSimpleLink)
+ XCTAssertFalse(selectionStates1.isLinkWithNestedLink)
+
+ // "Cat"
+ XCTAssertFalse(selectionStates2.isSimpleLink)
+ XCTAssertTrue(selectionStates2.isLinkWithNestedLink)
+
+ // "fish"
+ XCTAssertTrue(selectionStates3.isSimpleLink)
+ XCTAssertTrue(selectionStates3.isLinkWithNestedLink)
+
+ // "cat"
+ XCTAssertFalse(selectionStates4.isSimpleLink)
+ XCTAssertTrue(selectionStates4.isLinkWithNestedLink)
+
+ // "Test"
+ XCTAssertFalse(selectionStates5.isSimpleLink)
+ XCTAssertFalse(selectionStates5.isLinkWithNestedLink)
+ }
+
+ func testNestedLinkStateCursor() throws {
+ let text = "Test [[File:Cat with fish.jpg|thumb|left|Cat with [[fish]]|alt=Photo of cat looking at fish]] Test"
+ mediator.textView.attributedText = NSAttributedString(string: text)
+
+ let selectionStates1 = mediator.selectionState(selectedDocumentRange: NSRange(location: 0, length: 0))
+ let selectionStates2 = mediator.selectionState(selectedDocumentRange: NSRange(location: 13, length: 0))
+ let selectionStates3 = mediator.selectionState(selectedDocumentRange: NSRange(location: 54, length: 0))
+ let selectionStates4 = mediator.selectionState(selectedDocumentRange: NSRange(location: 73, length: 0))
+ let selectionStates5 = mediator.selectionState(selectedDocumentRange: NSRange(location: 96, length: 0))
+
+ // "Test"
+ XCTAssertFalse(selectionStates1.isSimpleLink)
+ XCTAssertFalse(selectionStates1.isLinkWithNestedLink)
+
+ // "Cat"
+ XCTAssertFalse(selectionStates2.isSimpleLink)
+ XCTAssertTrue(selectionStates2.isLinkWithNestedLink)
+
+ // "fish"
+ XCTAssertTrue(selectionStates3.isSimpleLink)
+ XCTAssertTrue(selectionStates3.isLinkWithNestedLink)
+
+ // "cat"
+ XCTAssertFalse(selectionStates4.isSimpleLink)
+ XCTAssertTrue(selectionStates4.isLinkWithNestedLink)
+
+ // "Test"
+ XCTAssertFalse(selectionStates5.isSimpleLink)
+ XCTAssertFalse(selectionStates5.isLinkWithNestedLink)
+ }
}
diff --git a/Wikipedia/Code/InsertMediaViewController.swift b/Wikipedia/Code/InsertMediaViewController.swift
index c7f142f84ef..39fd8bbd271 100644
--- a/Wikipedia/Code/InsertMediaViewController.swift
+++ b/Wikipedia/Code/InsertMediaViewController.swift
@@ -130,8 +130,7 @@ final class InsertMediaViewController: ViewController {
switch (mediaSettings.caption, mediaSettings.alternativeText) {
case (let caption?, let alternativeText?):
wikitext = """
- [[\(searchResult.fileTitle) | \(mediaSettings.advanced.imageType.rawValue) | \(mediaSettings.advanced.imageSize.rawValue) | \(mediaSettings.advanced.imagePosition.rawValue) | alt= \(alternativeText) |
- \(caption)]]
+ [[\(searchResult.fileTitle) | \(mediaSettings.advanced.imageType.rawValue) | \(mediaSettings.advanced.imageSize.rawValue) | \(mediaSettings.advanced.imagePosition.rawValue) | alt= \(alternativeText) | \(caption)]]
"""
case (let caption?, nil):
wikitext = """
diff --git a/Wikipedia/Code/PageEditorViewController.swift b/Wikipedia/Code/PageEditorViewController.swift
index e89fc8c9df6..1453bb7e0ef 100644
--- a/Wikipedia/Code/PageEditorViewController.swift
+++ b/Wikipedia/Code/PageEditorViewController.swift
@@ -1,6 +1,7 @@
import UIKit
import Components
import WMF
+import CocoaLumberjackSwift
protocol PageEditorViewControllerDelegate: AnyObject {
func pageEditorDidCancelEditing(_ pageEditor: PageEditorViewController, navigateToURL: URL?)
@@ -230,6 +231,7 @@ extension PageEditorViewController: Themeable {
// MARK: - WKSourceEditorViewControllerDelegate
extension PageEditorViewController: WKSourceEditorViewControllerDelegate {
+
func sourceEditorViewControllerDidTapFind(sourceEditorViewController: WKSourceEditorViewController) {
showFocusNavigationView()
}
@@ -237,6 +239,48 @@ extension PageEditorViewController: WKSourceEditorViewControllerDelegate {
func sourceEditorViewControllerDidRemoveFindInputAccessoryView(sourceEditorViewController: Components.WKSourceEditorViewController) {
hideFocusNavigationView()
}
+
+
+ func sourceEditorViewControllerDidTapLink(parameters: WKSourceEditorFormatterLinkWizardParameters) {
+ guard let siteURL = pageURL.wmf_site else {
+ return
+ }
+
+ if let editPageTitle = parameters.editPageTitle {
+ guard let link = Link(page: editPageTitle, label: parameters.editPageLabel, exists: true) else {
+ return
+ }
+
+ guard let editLinkViewController = EditLinkViewController(link: link, siteURL: pageURL.wmf_site, dataStore: dataStore) else {
+ return
+ }
+
+ editLinkViewController.delegate = self
+ let navigationController = WMFThemeableNavigationController(rootViewController: editLinkViewController, theme: self.theme)
+ navigationController.isNavigationBarHidden = true
+ present(navigationController, animated: true)
+ }
+
+ if let insertSearchTerm = parameters.insertSearchTerm {
+ guard let link = Link(page: insertSearchTerm, label: nil, exists: false) else {
+ return
+ }
+
+ let insertLinkViewController = InsertLinkViewController(link: link, siteURL: siteURL, dataStore: dataStore)
+ insertLinkViewController.delegate = self
+ let navigationController = WMFThemeableNavigationController(rootViewController: insertLinkViewController, theme: self.theme)
+ present(navigationController, animated: true)
+ }
+ }
+
+ func sourceEditorViewControllerDidTapImage() {
+ let insertMediaViewController = InsertMediaViewController(articleTitle: pageURL.wmf_title, siteURL: pageURL.wmf_site)
+ insertMediaViewController.delegate = self
+ insertMediaViewController.apply(theme: theme)
+ let navigationController = WMFThemeableNavigationController(rootViewController: insertMediaViewController, theme: theme)
+ navigationController.isNavigationBarHidden = true
+ present(navigationController, animated: true)
+ }
}
// MARK: - PageEditorNavigationItemControllerDelegate
@@ -312,6 +356,55 @@ extension PageEditorViewController: ReadingThemesControlsPresenting {
}
}
+// MARK: - EditLinkViewControllerDelegate
+
+extension PageEditorViewController: EditLinkViewControllerDelegate {
+ func editLinkViewController(_ editLinkViewController: EditLinkViewController, didTapCloseButton button: UIBarButtonItem) {
+ dismiss(animated: true)
+ }
+
+ func editLinkViewController(_ editLinkViewController: EditLinkViewController, didFinishEditingLink displayText: String?, linkTarget: String) {
+ dismiss(animated: true)
+ sourceEditor.editLink(newPageTitle: linkTarget, newPageLabel: displayText)
+ }
+
+ func editLinkViewController(_ editLinkViewController: EditLinkViewController, didFailToExtractArticleTitleFromArticleURL articleURL: URL) {
+ DDLogError("Failed to extract article title from \(pageURL)")
+ dismiss(animated: true)
+ }
+
+ func editLinkViewControllerDidRemoveLink(_ editLinkViewController: EditLinkViewController) {
+ dismiss(animated: true)
+ sourceEditor.removeLink()
+ }
+}
+
+// MARK: - InsertLinkViewControllerDelegate
+
+extension PageEditorViewController: InsertLinkViewControllerDelegate {
+ func insertLinkViewController(_ insertLinkViewController: InsertLinkViewController, didTapCloseButton button: UIBarButtonItem) {
+ dismiss(animated: true)
+ }
+
+ func insertLinkViewController(_ insertLinkViewController: InsertLinkViewController, didInsertLinkFor page: String, withLabel label: String?) {
+ sourceEditor.insertLink(pageTitle: page)
+ dismiss(animated: true)
+ }
+}
+
+// MARK: - Insert
+
+extension PageEditorViewController: InsertMediaViewControllerDelegate {
+ func insertMediaViewController(_ insertMediaViewController: InsertMediaViewController, didTapCloseButton button: UIBarButtonItem) {
+ dismiss(animated: true)
+ }
+
+ func insertMediaViewController(_ insertMediaViewController: InsertMediaViewController, didPrepareWikitextToInsert wikitext: String) {
+ sourceEditor.insertImage(wikitext: wikitext)
+ dismiss(animated: true)
+ }
+}
+
enum SourceEditorAccessibilityIdentifiers: String {
case entryButton = "Source Editor Entry Button"
case textView = "Source Editor TextView"
diff --git a/WikipediaUITests/UITestHelperViewController.swift b/WikipediaUITests/UITestHelperViewController.swift
index 7e78ec0431e..a6cf0b83acd 100644
--- a/WikipediaUITests/UITestHelperViewController.swift
+++ b/WikipediaUITests/UITestHelperViewController.swift
@@ -150,6 +150,14 @@ public class UITestHelperViewController: WKCanvasViewController {
extension UITestHelperViewController: WKSourceEditorViewControllerDelegate {
+ public func sourceEditorViewControllerDidTapImage() {
+
+ }
+
+ public func sourceEditorViewControllerDidTapLink(parameters: Components.WKSourceEditorFormatterLinkWizardParameters) {
+
+ }
+
public func sourceEditorViewControllerDidRemoveFindInputAccessoryView(sourceEditorViewController: Components.WKSourceEditorViewController) {
}