Skip to content

Commit

Permalink
Merge pull request wikimedia#4700 from wikimedia/native-editor-paragr…
Browse files Browse the repository at this point in the history
…aph-header-actions

Native Editor - Add heading support
  • Loading branch information
mazevedofs authored Jan 10, 2024
2 parents b11bd89 + b5e7429 commit ec2e21d
Show file tree
Hide file tree
Showing 18 changed files with 1,045 additions and 130 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ protocol WKEditorInputViewDelegate: AnyObject {
func didTapBold(isSelected: Bool)
func didTapItalics(isSelected: Bool)
func didTapTemplate(isSelected: Bool)
func didTapHeading(type: WKEditorInputView.HeadingButtonType)
func didTapStrikethrough(isSelected: Bool)
}

Expand Down Expand Up @@ -221,6 +222,18 @@ class WKEditorInputView: WKComponentView {
])

updateColors()

NotificationCenter.default.addObserver(self, selector: #selector(updateButtonSelectionState(_:)), name: Notification.WKSourceEditorSelectionState, object: nil)
}

// MARK: - Notifications

@objc private func updateButtonSelectionState(_ notification: NSNotification) {
guard let selectionState = notification.userInfo?[Notification.WKSourceEditorSelectionStateKey] as? WKSourceEditorSelectionState else {
return
}

configure(selectionState: selectionState)
}

// MARK: - Overrides
Expand Down Expand Up @@ -283,20 +296,40 @@ class WKEditorInputView: WKComponentView {

switch type {
case .paragraph:
paragraphButton.isSelected.toggle()
paragraphButton.isSelected = true
case .heading:
headerButton.isSelected.toggle()
headerButton.isSelected = true
case .subheading1:
subheader1Button.isSelected.toggle()
subheader1Button.isSelected = true
case .subheading2:
subheader2Button.isSelected.toggle()
subheader2Button.isSelected = true
case .subheading3:
subheader3Button.isSelected.toggle()
subheader3Button.isSelected = true
case .subheading4:
subheader4Button.isSelected.toggle()
subheader4Button.isSelected = true
}

delegate?.didTapHeading(type: type)
})

return UIButton(configuration: configuration, primaryAction: action)
}

func configure(selectionState: WKSourceEditorSelectionState) {
headingButtons.forEach { $0.isSelected = false }

if selectionState.isHeading {
headerButton.isSelected = true
} else if selectionState.isSubheading1 {
subheader1Button.isSelected = true
} else if selectionState.isSubheading2 {
subheader2Button.isSelected = true
} else if selectionState.isSubheading3 {
subheader3Button.isSelected = true
} else if selectionState.isSubheading4 {
subheader4Button.isSelected = true
} else {
paragraphButton.isSelected = true
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,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)
}
Expand Down Expand Up @@ -152,7 +152,7 @@ extension WKSourceEditorFormatter {

// MARK: - Nearby formatting string determination

private func selectedRangeIsSurroundedByFormattingString(formattingString: String, in textView: UITextView) -> Bool {
func selectedRangeIsSurroundedByFormattingString(formattingString: String, in textView: UITextView) -> Bool {
selectedRangeIsSurroundedByFormattingStrings(startingFormattingString: formattingString, endingFormattingString: formattingString, in: textView)
}

Expand All @@ -174,7 +174,7 @@ extension WKSourceEditorFormatter {
return startingString == formattingString
}

private func rangeIsFollowedByFormattingString(range: UITextRange?, formattingString: String, in textView: UITextView) -> Bool {
func rangeIsFollowedByFormattingString(range: UITextRange?, formattingString: String, in textView: UITextView) -> Bool {
guard let range = range,
let newEnd = textView.position(from: range.end, offset: formattingString.count) else {
return false
Expand All @@ -190,7 +190,7 @@ extension WKSourceEditorFormatter {

// MARK: Adding and removing text

private func addStringFormattingCharacters(startingFormattingString: String, endingFormattingString: String, in textView: UITextView) {
func addStringFormattingCharacters(startingFormattingString: String, endingFormattingString: String, in textView: UITextView) {

let startingCursorOffset = startingFormattingString.count
let endingCursorOffset = endingFormattingString.count
Expand All @@ -217,7 +217,7 @@ extension WKSourceEditorFormatter {
}
}

private func removeSurroundingFormattingStringsFromSelectedRange(startingFormattingString: String, endingFormattingString: String, in textView: UITextView) {
func removeSurroundingFormattingStringsFromSelectedRange(startingFormattingString: String, endingFormattingString: String, in textView: UITextView) {

guard let originalSelectedTextRange = textView.selectedTextRange,
let formattingTextStart = textView.position(from: originalSelectedTextRange.start, offset: -startingFormattingString.count),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Foundation
import ComponentsObjC

extension WKSourceEditorFormatterHeading {
func toggleHeadingFormatting(selectedHeading: WKEditorInputView.HeadingButtonType, currentSelectionState: WKSourceEditorSelectionState, textView: UITextView) {

var currentStateIsParagraph = false
if currentSelectionState.isHeading {
expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: "==", endingFormattingString: "==", in: textView)
removeSurroundingFormattingStringsFromSelectedRange(startingFormattingString: "==", endingFormattingString: "==", in: textView)
} else if currentSelectionState.isSubheading1 {
expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: "===", endingFormattingString: "===", in: textView)
removeSurroundingFormattingStringsFromSelectedRange(startingFormattingString: "===", endingFormattingString: "===", in: textView)
} else if currentSelectionState.isSubheading2 {
expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: "====", endingFormattingString: "====", in: textView)
removeSurroundingFormattingStringsFromSelectedRange(startingFormattingString: "====", endingFormattingString: "====", in: textView)
} else if currentSelectionState.isSubheading3 {
expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: "=====", endingFormattingString: "=====", in: textView)
removeSurroundingFormattingStringsFromSelectedRange(startingFormattingString: "=====", endingFormattingString: "=====", in: textView)
} else if currentSelectionState.isSubheading4 {
expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: "======", endingFormattingString: "======", in: textView)
removeSurroundingFormattingStringsFromSelectedRange(startingFormattingString: "======", endingFormattingString: "======", in: textView)
} else {
currentStateIsParagraph = true
}

let currentlySurroundedByLineBreaks = selectedRangeIsSurroundedByFormattingString(formattingString: "\n", in: textView) || textView.selectedRange.location == 0 && rangeIsFollowedByFormattingString(range: textView.selectedTextRange, formattingString: "\n", in: textView)

let surroundingLineBreak = currentStateIsParagraph && !currentlySurroundedByLineBreaks ? "\n" : ""
let startingFormattingString: String
let endingFormattingString: String
switch selectedHeading {
case .paragraph:
return
case .heading:
startingFormattingString = surroundingLineBreak + "=="
endingFormattingString = "==" + surroundingLineBreak
case .subheading1:
startingFormattingString = surroundingLineBreak + "==="
endingFormattingString = "===" + surroundingLineBreak
case .subheading2:
startingFormattingString = surroundingLineBreak + "===="
endingFormattingString = "====" + surroundingLineBreak
case .subheading3:
startingFormattingString = surroundingLineBreak + "====="
endingFormattingString = "=====" + surroundingLineBreak
case .subheading4:
startingFormattingString = surroundingLineBreak + "======"
endingFormattingString = "======" + surroundingLineBreak
}

addStringFormattingCharacters(startingFormattingString: startingFormattingString, endingFormattingString: endingFormattingString, in: textView)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,22 @@ fileprivate var needsTextKit2: Bool {
let isBold: Bool
let isItalics: Bool
let isHorizontalTemplate: Bool
let isHeading: Bool
let isSubheading1: Bool
let isSubheading2: Bool
let isSubheading3: Bool
let isSubheading4: Bool
let isStrikethrough: Bool

init(isBold: Bool, isItalics: Bool, isHorizontalTemplate: Bool, isStrikethrough: Bool) {
init(isBold: Bool, isItalics: Bool, isHorizontalTemplate: Bool, isHeading: Bool, isSubheading1: Bool, isSubheading2: Bool, isSubheading3: Bool, isSubheading4: Bool, isStrikethrough: Bool) {
self.isBold = isBold
self.isItalics = isItalics
self.isHorizontalTemplate = isHorizontalTemplate
self.isHeading = isHeading
self.isSubheading1 = isSubheading1
self.isSubheading2 = isSubheading2
self.isSubheading3 = isSubheading3
self.isSubheading4 = isSubheading4
self.isStrikethrough = isStrikethrough
}
}
Expand All @@ -37,6 +47,7 @@ final class WKSourceEditorTextFrameworkMediator: NSObject {
private(set) var formatters: [WKSourceEditorFormatter] = []
private(set) var boldItalicsFormatter: WKSourceEditorFormatterBoldItalics?
private(set) var templateFormatter: WKSourceEditorFormatterTemplate?
private(set) var headingFormatter: WKSourceEditorFormatterHeading?
private(set) var strikethroughFormatter: WKSourceEditorFormatterStrikethrough?

var isSyntaxHighlightingEnabled: Bool = true {
Expand Down Expand Up @@ -106,14 +117,16 @@ final class WKSourceEditorTextFrameworkMediator: NSObject {

let boldItalicsFormatter = WKSourceEditorFormatterBoldItalics(colors: colors, fonts: fonts)
let templateFormatter = WKSourceEditorFormatterTemplate(colors: colors, fonts: fonts)
let headingFormatter = WKSourceEditorFormatterHeading(colors: colors, fonts: fonts)
let strikethroughFormatter = WKSourceEditorFormatterStrikethrough(colors: colors, fonts: fonts)

self.formatters = [WKSourceEditorFormatterBase(colors: colors, fonts: fonts, textAlignment: viewModel.textAlignment),
templateFormatter,
boldItalicsFormatter,
headingFormatter,
strikethroughFormatter]
self.boldItalicsFormatter = boldItalicsFormatter
self.templateFormatter = templateFormatter
self.headingFormatter = headingFormatter
self.strikethroughFormatter = strikethroughFormatter

if needsTextKit2 {
Expand Down Expand Up @@ -154,26 +167,36 @@ final class WKSourceEditorTextFrameworkMediator: NSObject {

if needsTextKit2 {
guard let textKit2Data = textkit2SelectionData(selectedDocumentRange: selectedDocumentRange) else {
return WKSourceEditorSelectionState(isBold: false, isItalics: false, isHorizontalTemplate: false, isStrikethrough: false)
return WKSourceEditorSelectionState(isBold: false, isItalics: false, isHorizontalTemplate: false, isHeading: false, isSubheading1: false, isSubheading2: false, isSubheading3: false, isSubheading4: false, isStrikethrough: false)
}

let isBold = boldItalicsFormatter?.attributedString(textKit2Data.paragraphAttributedString, isBoldIn: textKit2Data.paragraphSelectedRange) ?? false
let isItalics = boldItalicsFormatter?.attributedString(textKit2Data.paragraphAttributedString, isItalicsIn: textKit2Data.paragraphSelectedRange) ?? false
let isHorizontalTemplate = templateFormatter?.attributedString(textKit2Data.paragraphAttributedString, isHorizontalTemplateIn: textKit2Data.paragraphSelectedRange) ?? false
let isHeading = headingFormatter?.attributedString(textKit2Data.paragraphAttributedString, isHeadingIn: textKit2Data.paragraphSelectedRange) ?? false
let isSubheading1 = headingFormatter?.attributedString(textKit2Data.paragraphAttributedString, isSubheading1In: textKit2Data.paragraphSelectedRange) ?? false
let isSubheading2 = headingFormatter?.attributedString(textKit2Data.paragraphAttributedString, isSubheading2In: textKit2Data.paragraphSelectedRange) ?? false
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, isStrikethrough: isStrikethrough)
return WKSourceEditorSelectionState(isBold: isBold, isItalics: isItalics, isHorizontalTemplate: isHorizontalTemplate, isHeading: isHeading, isSubheading1: isSubheading1, isSubheading2: isSubheading2, isSubheading3: isSubheading3, isSubheading4: isSubheading4, isStrikethrough: isStrikethrough)
} else {
guard let textKit1Storage else {
return WKSourceEditorSelectionState(isBold: false, isItalics: false, isHorizontalTemplate: false, isStrikethrough: false)
return WKSourceEditorSelectionState(isBold: false, isItalics: false, isHorizontalTemplate: false, isHeading: false, isSubheading1: false, isSubheading2: false, isSubheading3: false, isSubheading4: false, isStrikethrough: 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
let isHeading = headingFormatter?.attributedString(textKit1Storage, isHeadingIn: selectedDocumentRange) ?? false
let isSubheading1 = headingFormatter?.attributedString(textKit1Storage, isSubheading1In: selectedDocumentRange) ?? false
let isSubheading2 = headingFormatter?.attributedString(textKit1Storage, isSubheading2In: selectedDocumentRange) ?? false
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, isStrikethrough: isStrikethrough)
return WKSourceEditorSelectionState(isBold: isBold, isItalics: isItalics, isHorizontalTemplate: isHorizontalTemplate, isHeading: isHeading, isSubheading1: isSubheading1, isSubheading2: isSubheading2, isSubheading3: isSubheading3, isSubheading4: isSubheading4, isStrikethrough: isStrikethrough)
}
}

Expand Down Expand Up @@ -223,6 +246,11 @@ extension WKSourceEditorTextFrameworkMediator: WKSourceEditorStorageDelegate {
fonts.boldFont = isSyntaxHighlightingEnabled ? WKFont.for(.boldBody, compatibleWith: traitCollection) : baseFont
fonts.italicsFont = isSyntaxHighlightingEnabled ? WKFont.for(.italicsBody, compatibleWith: traitCollection) : baseFont
fonts.boldItalicsFont = isSyntaxHighlightingEnabled ? WKFont.for(.boldItalicsBody, compatibleWith: traitCollection) : baseFont
fonts.headingFont = isSyntaxHighlightingEnabled ? WKFont.for(.editorHeading, compatibleWith: traitCollection) : baseFont
fonts.subheading1Font = isSyntaxHighlightingEnabled ? WKFont.for(.editorSubheading1, compatibleWith: traitCollection) : baseFont
fonts.subheading2Font = isSyntaxHighlightingEnabled ? WKFont.for(.editorSubheading2, compatibleWith: traitCollection) : baseFont
fonts.subheading3Font = isSyntaxHighlightingEnabled ? WKFont.for(.editorSubheading3, compatibleWith: traitCollection) : baseFont
fonts.subheading4Font = isSyntaxHighlightingEnabled ? WKFont.for(.editorSubheading4, compatibleWith: traitCollection) : baseFont
return fonts
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,10 @@ extension WKSourceEditorViewController: WKEditorToolbarHighlightViewDelegate {
// MARK: - WKEditorInputViewDelegate

extension WKSourceEditorViewController: WKEditorInputViewDelegate {
func didTapHeading(type: WKEditorInputView.HeadingButtonType) {
textFrameworkMediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: type, currentSelectionState: selectionState(), textView: textView)
}

func didTapBold(isSelected: Bool) {
let action: WKSourceEditorFormatterButtonAction = isSelected ? .remove : .add
textFrameworkMediator.boldItalicsFormatter?.toggleBoldFormatting(action: action, in: textView)
Expand Down
Loading

0 comments on commit ec2e21d

Please sign in to comment.