diff --git a/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatter+ButtonActions.swift b/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatter+ButtonActions.swift index e0e70e9d2f6..8dd3b76589f 100644 --- a/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatter+ButtonActions.swift +++ b/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatter+ButtonActions.swift @@ -35,7 +35,7 @@ extension WKSourceEditorFormatter { // MARK: - Expanding selected range methods - private func expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: String, endingFormattingString: String, in textView: UITextView) { + func expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: String, endingFormattingString: String, in textView: UITextView) { if let textPositions = textPositionsCloserToNearestFormattingStrings(startingFormattingString: startingFormattingString, endingFormattingString: endingFormattingString, in: textView) { textView.selectedTextRange = textView.textRange(from: textPositions.startPosition, to: textPositions.endPosition) } 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..1f0dcbfe832 --- /dev/null +++ b/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatterLink+ButtonActions.swift @@ -0,0 +1,101 @@ +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) { + + expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: "[[", endingFormattingString: "]]", in: textView) + + 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) + } + } + } +} diff --git a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift index d3f645adf52..c8215dc9240 100644 --- a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift +++ b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift @@ -4,6 +4,7 @@ import UIKit public protocol WKSourceEditorViewControllerDelegate: AnyObject { func sourceEditorViewControllerDidTapFind(sourceEditorViewController: WKSourceEditorViewController) func sourceEditorViewControllerDidRemoveFindInputAccessoryView(sourceEditorViewController: WKSourceEditorViewController) + func sourceEditorViewControllerDidTapLink(parameters: WKSourceEditorFormatterLinkWizardParameters) } // MARK: NSNotification Names @@ -37,6 +38,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 @@ -231,6 +233,39 @@ 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 + } } // MARK: - Private @@ -272,6 +307,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 @@ -312,7 +360,7 @@ extension WKSourceEditorViewController: WKEditorToolbarExpandingViewDelegate { } func toolbarExpandingViewDidTapLink(toolbarView: WKEditorToolbarExpandingView, isSelected: Bool) { - + presentLinkWizard(linkButtonIsSelected: isSelected) } func toolbarExpandingViewDidTapImage(toolbarView: WKEditorToolbarExpandingView) { @@ -340,7 +388,7 @@ extension WKSourceEditorViewController: WKEditorToolbarHighlightViewDelegate { } func toolbarHighlightViewDidTapLink(toolbarView: WKEditorToolbarHighlightView, isSelected: Bool) { - + presentLinkWizard(linkButtonIsSelected: isSelected) } func toolbarHighlightViewDidTapShowMore(toolbarView: WKEditorToolbarHighlightView) { @@ -378,7 +426,7 @@ extension WKSourceEditorViewController: WKEditorInputViewDelegate { } func didTapLink(isSelected: Bool) { - + presentLinkWizard(linkButtonIsSelected: isSelected) } func didTapClose() { diff --git a/Wikipedia/Code/PageEditorViewController.swift b/Wikipedia/Code/PageEditorViewController.swift index be88b879715..5ed15e64b62 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?) @@ -239,6 +240,39 @@ 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) + } + } } // MARK: - PageEditorNavigationItemControllerDelegate @@ -314,6 +348,42 @@ 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) + } +} + 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 e37203818af..79965132862 100644 --- a/WikipediaUITests/UITestHelperViewController.swift +++ b/WikipediaUITests/UITestHelperViewController.swift @@ -154,6 +154,10 @@ public class UITestHelperViewController: WKCanvasViewController { extension UITestHelperViewController: WKSourceEditorViewControllerDelegate { + public func sourceEditorViewControllerDidTapLink(parameters: Components.WKSourceEditorFormatterLinkWizardParameters) { + + } + public func sourceEditorViewControllerDidRemoveFindInputAccessoryView(sourceEditorViewController: Components.WKSourceEditorViewController) { }