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 59b10a283f7..d96c45e9fcf 100644 --- a/Components/Sources/Components/Components/Editors/Common Views/Base/WKEditorToolbarButton.swift +++ b/Components/Sources/Components/Components/Editors/Common Views/Base/WKEditorToolbarButton.swift @@ -68,6 +68,17 @@ class WKEditorToolbarButton: WKComponentView { } } + var isEnabled: Bool { + get { + return button.isEnabled + } + set { + button.isEnabled = newValue + updateColors() + accessibilityTraits = newValue ? [.button, .selected] : [.button, .notEnabled] + } + } + func setImage(_ image: UIImage?, for state: UIControl.State) { button.setImage(image, for: state) } 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 d4e615eece9..2e44efbe64c 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 @@ -4,6 +4,10 @@ protocol WKEditorToolbarExpandingViewDelegate: AnyObject { func toolbarExpandingViewDidTapFind(toolbarView: WKEditorToolbarExpandingView) func toolbarExpandingViewDidTapFormatText(toolbarView: WKEditorToolbarExpandingView) func toolbarExpandingViewDidTapTemplate(toolbarView: WKEditorToolbarExpandingView, isSelected: Bool) + func toolbarExpandingViewDidTapUnorderedList(toolbarView: WKEditorToolbarExpandingView, isSelected: Bool) + func toolbarExpandingViewDidTapOrderedList(toolbarView: WKEditorToolbarExpandingView, isSelected: Bool) + func toolbarExpandingViewDidTapIncreaseIndent(toolbarView: WKEditorToolbarExpandingView) + func toolbarExpandingViewDidTapDecreaseIndent(toolbarView: WKEditorToolbarExpandingView) } class WKEditorToolbarExpandingView: WKEditorToolbarView { @@ -112,10 +116,12 @@ class WKEditorToolbarExpandingView: WKEditorToolbarView { decreaseIndentionButton.setImage(WKSFSymbolIcon.for(symbol: .decreaseIndent), for: .normal) decreaseIndentionButton.addTarget(self, action: #selector(tappedDecreaseIndentation), for: .touchUpInside) decreaseIndentionButton.accessibilityLabel = WKSourceEditorLocalizedStrings.current.accessibilityLabelButtonDecreaseIndent + decreaseIndentionButton.isEnabled = false increaseIndentionButton.setImage(WKSFSymbolIcon.for(symbol: .increaseIndent), for: .normal) increaseIndentionButton.addTarget(self, action: #selector(tappedIncreaseIndentation), for: .touchUpInside) increaseIndentionButton.accessibilityLabel = WKSourceEditorLocalizedStrings.current.accessibilityLabelButtonInceaseIndent + increaseIndentionButton.isEnabled = false cursorUpButton.setImage(WKIcon.chevronUp, for: .normal) cursorUpButton.addTarget(self, action: #selector(tappedCursorUp), for: .touchUpInside) @@ -143,6 +149,27 @@ class WKEditorToolbarExpandingView: WKEditorToolbarView { } templateButton.isSelected = selectionState.isHorizontalTemplate + + unorderedListButton.isSelected = selectionState.isBulletSingleList || selectionState.isBulletMultipleList + unorderedListButton.isEnabled = !selectionState.isNumberSingleList && !selectionState.isNumberMultipleList + + orderedListButton.isSelected = selectionState.isNumberSingleList || selectionState.isNumberMultipleList + orderedListButton.isEnabled = !selectionState.isBulletSingleList && !selectionState.isBulletMultipleList + + decreaseIndentionButton.isEnabled = false + if selectionState.isBulletMultipleList || selectionState.isNumberMultipleList { + decreaseIndentionButton.isEnabled = true + } + + if selectionState.isBulletSingleList || + selectionState.isBulletMultipleList || + selectionState.isNumberSingleList || + selectionState.isNumberMultipleList { + increaseIndentionButton.isEnabled = true + } else { + increaseIndentionButton.isEnabled = false + } + cursorRightButton.accessibilityLabel = WKSourceEditorLocalizedStrings.current.accessibilityLabelButtonCursorRight } @@ -199,15 +226,19 @@ class WKEditorToolbarExpandingView: WKEditorToolbarView { } @objc private func tappedUnorderedList() { + delegate?.toolbarExpandingViewDidTapUnorderedList(toolbarView: self, isSelected: unorderedListButton.isSelected) } @objc private func tappedOrderedList() { + delegate?.toolbarExpandingViewDidTapOrderedList(toolbarView: self, isSelected: orderedListButton.isSelected) } @objc private func tappedDecreaseIndentation() { + delegate?.toolbarExpandingViewDidTapDecreaseIndent(toolbarView: self) } @objc private func tappedIncreaseIndentation() { + delegate?.toolbarExpandingViewDidTapIncreaseIndent(toolbarView: self) } @objc private func tappedCursorUp() { 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 3b14344aa86..25c1828deb3 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 @@ -6,6 +6,10 @@ protocol WKEditorInputViewDelegate: AnyObject { func didTapBold(isSelected: Bool) func didTapItalics(isSelected: Bool) func didTapTemplate(isSelected: Bool) + func didTapBulletList(isSelected: Bool) + func didTapNumberList(isSelected: Bool) + func didTapIncreaseIndent() + func didTapDecreaseIndent() func didTapHeading(type: WKEditorInputView.HeadingButtonType) func didTapStrikethrough(isSelected: Bool) } diff --git a/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorToolbarGroupedView.swift b/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorToolbarGroupedView.swift index 34c00e0b6c4..0ac283016d0 100644 --- a/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorToolbarGroupedView.swift +++ b/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorToolbarGroupedView.swift @@ -31,10 +31,12 @@ class WKEditorToolbarGroupedView: WKEditorToolbarView { decreaseIndentButton.setImage(WKSFSymbolIcon.for(symbol: .decreaseIndent), for: .normal) decreaseIndentButton.addTarget(self, action: #selector(tappedDecreaseIndent), for: .touchUpInside) decreaseIndentButton.accessibilityLabel = WKSourceEditorLocalizedStrings.current?.accessibilityLabelButtonDecreaseIndent + decreaseIndentButton.isEnabled = false increaseIndentButton.setImage(WKSFSymbolIcon.for(symbol: .increaseIndent), for: .normal) increaseIndentButton.addTarget(self, action: #selector(tappedIncreaseIndent), for: .touchUpInside) increaseIndentButton.accessibilityLabel = WKSourceEditorLocalizedStrings.current?.accessibilityLabelButtonInceaseIndent + increaseIndentButton.isEnabled = false superscriptButton.setImage(WKSFSymbolIcon.for(symbol: .textFormatSuperscript), for: .normal) superscriptButton.addTarget(self, action: #selector(tappedSuperscript), for: .touchUpInside) @@ -56,27 +58,51 @@ class WKEditorToolbarGroupedView: WKEditorToolbarView { } // MARK: - Notifications - + @objc private func updateButtonSelectionState(_ notification: NSNotification) { guard let selectionState = notification.userInfo?[Notification.WKSourceEditorSelectionStateKey] as? WKSourceEditorSelectionState else { return } + unorderedListButton.isSelected = selectionState.isBulletSingleList || selectionState.isBulletMultipleList + unorderedListButton.isEnabled = !selectionState.isNumberSingleList && !selectionState.isNumberMultipleList + + orderedListButton.isSelected = selectionState.isNumberSingleList || selectionState.isNumberMultipleList + orderedListButton.isEnabled = !selectionState.isBulletSingleList && !selectionState.isBulletMultipleList + + decreaseIndentButton.isEnabled = false + if selectionState.isBulletMultipleList || selectionState.isNumberMultipleList { + decreaseIndentButton.isEnabled = true + } + + if selectionState.isBulletSingleList || + selectionState.isBulletMultipleList || + selectionState.isNumberSingleList || + selectionState.isNumberMultipleList { + increaseIndentButton.isEnabled = true + } else { + increaseIndentButton.isEnabled = false + } + strikethroughButton.isSelected = selectionState.isStrikethrough } // MARK: - Button Actions @objc private func tappedIncreaseIndent() { + delegate?.didTapIncreaseIndent() } @objc private func tappedDecreaseIndent() { + delegate?.didTapDecreaseIndent() } @objc private func tappedUnorderedList() { + delegate?.didTapBulletList(isSelected: unorderedListButton.isSelected) } @objc private func tappedOrderedList() { + delegate?.didTapNumberList(isSelected: orderedListButton.isSelected) } @objc private func tappedSuperscript() { diff --git a/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatterList+ButtonActions.swift b/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatterList+ButtonActions.swift new file mode 100644 index 00000000000..66ed0f55ca4 --- /dev/null +++ b/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatterList+ButtonActions.swift @@ -0,0 +1,120 @@ +import Foundation +import ComponentsObjC + +extension WKSourceEditorFormatterList { + func toggleListBullet(action: WKSourceEditorFormatterButtonAction, in textView: UITextView) { + toggleListItem(action: action, formattingCharacter: "*", in: textView) + } + + func toggleListNumber(action: WKSourceEditorFormatterButtonAction, in textView: UITextView) { + toggleListItem(action: action, formattingCharacter: "#", in: textView) + } + + func tappedIncreaseIndent(currentSelectionState: WKSourceEditorSelectionState, textView: UITextView) { + + guard currentSelectionState.isBulletSingleList || + currentSelectionState.isBulletMultipleList || + currentSelectionState.isNumberSingleList || + currentSelectionState.isNumberMultipleList else { + assertionFailure("Increase / Decrease indent buttons should have been disabled.") + return + } + + let formattingCharacter = (currentSelectionState.isBulletSingleList || + currentSelectionState.isBulletMultipleList) ? "*" : "#" + + let nsString = textView.attributedText.string as NSString + let lineRange = nsString.lineRange(for: textView.selectedRange) + + textView.textStorage.insert(NSAttributedString(string: String(formattingCharacter)), at: lineRange.location) + + // reset cursor so it doesn't move + if let selectedRange = textView.selectedTextRange { + if let newStart = textView.position(from: selectedRange.start, offset: 1), + let newEnd = textView.position(from: selectedRange.end, offset: 1) { + textView.selectedTextRange = textView.textRange(from: newStart, to: newEnd) + } + } + } + + func tappedDecreaseIndent(currentSelectionState: WKSourceEditorSelectionState, textView: UITextView) { + + guard currentSelectionState.isBulletSingleList || + currentSelectionState.isBulletMultipleList || + currentSelectionState.isNumberSingleList || + currentSelectionState.isNumberMultipleList else { + assertionFailure("Increase / Decrease indent buttons should have been disabled.") + return + } + + let nsString = textView.attributedText.string as NSString + let lineRange = nsString.lineRange(for: textView.selectedRange) + + guard textView.textStorage.length > lineRange.location else { + return + } + + textView.textStorage.replaceCharacters(in: NSRange(location: lineRange.location, length: 1), with: "") + + // reset cursor so it doesn't move + if let selectedRange = textView.selectedTextRange { + if let newStart = textView.position(from: selectedRange.start, offset: -1), + let newEnd = textView.position(from: selectedRange.end, offset: -1) { + textView.selectedTextRange = textView.textRange(from: newStart, to: newEnd) + } + } + } + + private func toggleListItem(action: WKSourceEditorFormatterButtonAction, formattingCharacter: Character, in textView: UITextView) { + let nsString = textView.attributedText.string as NSString + let lineRange = nsString.lineRange(for: textView.selectedRange) + switch action { + case .add: + + var numBullets = 0 + for char in textView.textStorage.attributedSubstring(from: lineRange).string { + if char == formattingCharacter { + numBullets += 1 + } + } + + let insertString = numBullets == 0 ? "\(String(formattingCharacter)) " : String(formattingCharacter) + textView.textStorage.insert(NSAttributedString(string: insertString), at: lineRange.location) + + // reset cursor so it doesn't move + if let selectedRange = textView.selectedTextRange { + if let newStart = textView.position(from: selectedRange.start, offset: insertString.count), + let newEnd = textView.position(from: selectedRange.end, offset: insertString.count) { + textView.selectedTextRange = textView.textRange(from: newStart, to: newEnd) + } + } + case .remove: + + var numBullets = 0 + var hasSpace = false + for char in textView.textStorage.attributedSubstring(from: lineRange).string { + if char != formattingCharacter { + if char == " " { + hasSpace = true + } + break + } + + if char == formattingCharacter { + numBullets += 1 + } + } + + let replacementLength = numBullets + (hasSpace ? 1 : 0) + textView.textStorage.replaceCharacters(in: NSRange(location: lineRange.location, length: replacementLength), with: "") + + // reset cursor so it doesn't move + if let selectedRange = textView.selectedTextRange { + if let newStart = textView.position(from: selectedRange.start, offset: -1 * replacementLength), + let newEnd = textView.position(from: selectedRange.end, offset: -1 * replacementLength) { + textView.selectedTextRange = textView.textRange(from: newStart, to: newEnd) + } + } + } + } +} diff --git a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift index ffd1305c121..24893b850f1 100644 --- a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift +++ b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift @@ -17,17 +17,25 @@ fileprivate var needsTextKit2: Bool { let isBold: Bool let isItalics: Bool let isHorizontalTemplate: Bool + let isBulletSingleList: Bool + let isBulletMultipleList: Bool + let isNumberSingleList: Bool + let isNumberMultipleList: 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, isHeading: Bool, isSubheading1: Bool, isSubheading2: Bool, isSubheading3: Bool, isSubheading4: Bool, isStrikethrough: Bool) { + + init(isBold: Bool, isItalics: Bool, isHorizontalTemplate: Bool, isBulletSingleList: Bool, isBulletMultipleList: Bool, isNumberSingleList: Bool, isNumberMultipleList: Bool, isHeading: Bool, isSubheading1: Bool, isSubheading2: Bool, isSubheading3: Bool, isSubheading4: Bool, isStrikethrough: Bool) { self.isBold = isBold self.isItalics = isItalics self.isHorizontalTemplate = isHorizontalTemplate + self.isBulletSingleList = isBulletSingleList + self.isBulletMultipleList = isBulletMultipleList + self.isNumberSingleList = isNumberSingleList + self.isNumberMultipleList = isNumberMultipleList self.isHeading = isHeading self.isSubheading1 = isSubheading1 self.isSubheading2 = isSubheading2 @@ -47,6 +55,7 @@ final class WKSourceEditorTextFrameworkMediator: NSObject { private(set) var formatters: [WKSourceEditorFormatter] = [] private(set) var boldItalicsFormatter: WKSourceEditorFormatterBoldItalics? private(set) var templateFormatter: WKSourceEditorFormatterTemplate? + private(set) var listFormatter: WKSourceEditorFormatterList? private(set) var headingFormatter: WKSourceEditorFormatterHeading? private(set) var strikethroughFormatter: WKSourceEditorFormatterStrikethrough? @@ -115,17 +124,20 @@ final class WKSourceEditorTextFrameworkMediator: NSObject { let colors = self.colors let fonts = self.fonts - let boldItalicsFormatter = WKSourceEditorFormatterBoldItalics(colors: colors, fonts: fonts) let templateFormatter = WKSourceEditorFormatterTemplate(colors: colors, fonts: fonts) + let boldItalicsFormatter = WKSourceEditorFormatterBoldItalics(colors: colors, fonts: fonts) + let listFormatter = WKSourceEditorFormatterList(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, + listFormatter, headingFormatter, strikethroughFormatter] self.boldItalicsFormatter = boldItalicsFormatter self.templateFormatter = templateFormatter + self.listFormatter = listFormatter self.headingFormatter = headingFormatter self.strikethroughFormatter = strikethroughFormatter @@ -167,36 +179,44 @@ final class WKSourceEditorTextFrameworkMediator: NSObject { if needsTextKit2 { guard let textKit2Data = textkit2SelectionData(selectedDocumentRange: selectedDocumentRange) else { - return WKSourceEditorSelectionState(isBold: false, isItalics: false, isHorizontalTemplate: false, isHeading: false, isSubheading1: false, isSubheading2: false, isSubheading3: false, isSubheading4: false, isStrikethrough: false) + return WKSourceEditorSelectionState(isBold: false, isItalics: false, isHorizontalTemplate: false, isBulletSingleList: false, isBulletMultipleList: false, isNumberSingleList: false, isNumberMultipleList: 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 isBulletSingleList = listFormatter?.attributedString(textKit2Data.paragraphAttributedString, isBulletSingleIn: textKit2Data.paragraphSelectedRange) ?? false + let isBulletMultipleList = listFormatter?.attributedString(textKit2Data.paragraphAttributedString, isBulletMultipleIn: textKit2Data.paragraphSelectedRange) ?? false + let isNumberSingleList = listFormatter?.attributedString(textKit2Data.paragraphAttributedString, isNumberSingleIn: textKit2Data.paragraphSelectedRange) ?? false + let isNumberMultipleList = listFormatter?.attributedString(textKit2Data.paragraphAttributedString, isNumberMultipleIn: 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, isHeading: isHeading, isSubheading1: isSubheading1, isSubheading2: isSubheading2, isSubheading3: isSubheading3, isSubheading4: isSubheading4, isStrikethrough: isStrikethrough) + + return WKSourceEditorSelectionState(isBold: isBold, isItalics: isItalics, isHorizontalTemplate: isHorizontalTemplate, isBulletSingleList: isBulletSingleList, isBulletMultipleList: isBulletMultipleList, isNumberSingleList: isNumberSingleList, isNumberMultipleList: isNumberMultipleList, 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, isHeading: false, isSubheading1: false, isSubheading2: false, isSubheading3: false, isSubheading4: false, isStrikethrough: false) - } - + return WKSourceEditorSelectionState(isBold: false, isItalics: false, isHorizontalTemplate: false, isBulletSingleList: false, isBulletMultipleList: false, isNumberSingleList: false, isNumberMultipleList: 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 isBulletSingleList = listFormatter?.attributedString(textKit1Storage, isBulletSingleIn: selectedDocumentRange) ?? false + let isBulletMultipleList = listFormatter?.attributedString(textKit1Storage, isBulletMultipleIn: selectedDocumentRange) ?? false + let isNumberSingleList = listFormatter?.attributedString(textKit1Storage, isNumberSingleIn: selectedDocumentRange) ?? false + let isNumberMultipleList = listFormatter?.attributedString(textKit1Storage, isNumberMultipleIn: 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, isHeading: isHeading, isSubheading1: isSubheading1, isSubheading2: isSubheading2, isSubheading3: isSubheading3, isSubheading4: isSubheading4, isStrikethrough: isStrikethrough) + + return WKSourceEditorSelectionState(isBold: isBold, isItalics: isItalics, isHorizontalTemplate: isHorizontalTemplate, isBulletSingleList: isBulletSingleList, isBulletMultipleList: isBulletMultipleList, isNumberSingleList: isNumberSingleList, isNumberMultipleList: isNumberMultipleList, isHeading: isHeading, isSubheading1: isSubheading1, isSubheading2: isSubheading2, isSubheading3: isSubheading3, isSubheading4: isSubheading4, isStrikethrough: isStrikethrough) } } diff --git a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift index bb43e24f3b9..d876ce9501d 100644 --- a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift +++ b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift @@ -261,6 +261,24 @@ extension WKSourceEditorViewController: WKEditorToolbarExpandingViewDelegate { let action: WKSourceEditorFormatterButtonAction = isSelected ? .remove : .add textFrameworkMediator.templateFormatter?.toggleTemplateFormatting(action: action, in: textView) } + + func toolbarExpandingViewDidTapUnorderedList(toolbarView: WKEditorToolbarExpandingView, isSelected: Bool) { + let action: WKSourceEditorFormatterButtonAction = isSelected ? .remove : .add + textFrameworkMediator.listFormatter?.toggleListBullet(action: action, in: textView) + } + + func toolbarExpandingViewDidTapOrderedList(toolbarView: WKEditorToolbarExpandingView, isSelected: Bool) { + let action: WKSourceEditorFormatterButtonAction = isSelected ? .remove : .add + textFrameworkMediator.listFormatter?.toggleListNumber(action: action, in: textView) + } + + func toolbarExpandingViewDidTapIncreaseIndent(toolbarView: WKEditorToolbarExpandingView) { + textFrameworkMediator.listFormatter?.tappedIncreaseIndent(currentSelectionState: selectionState(), textView: textView) + } + + func toolbarExpandingViewDidTapDecreaseIndent(toolbarView: WKEditorToolbarExpandingView) { + textFrameworkMediator.listFormatter?.tappedDecreaseIndent(currentSelectionState: selectionState(), textView: textView) + } } // MARK: - WKEditorToolbarHighlightViewDelegate @@ -310,6 +328,24 @@ extension WKSourceEditorViewController: WKEditorInputViewDelegate { textFrameworkMediator.templateFormatter?.toggleTemplateFormatting(action: action, in: textView) } + func didTapBulletList(isSelected: Bool) { + let action: WKSourceEditorFormatterButtonAction = isSelected ? .remove : .add + textFrameworkMediator.listFormatter?.toggleListBullet(action: action, in: textView) + } + + func didTapNumberList(isSelected: Bool) { + let action: WKSourceEditorFormatterButtonAction = isSelected ? .remove : .add + textFrameworkMediator.listFormatter?.toggleListNumber(action: action, in: textView) + } + + func didTapIncreaseIndent() { + textFrameworkMediator.listFormatter?.tappedIncreaseIndent(currentSelectionState: selectionState(), textView: textView) + } + + func didTapDecreaseIndent() { + textFrameworkMediator.listFormatter?.tappedDecreaseIndent(currentSelectionState: selectionState(), textView: textView) + } + func didTapStrikethrough(isSelected: Bool) { let action: WKSourceEditorFormatterButtonAction = isSelected ? .remove : .add textFrameworkMediator.strikethroughFormatter?.toggleStrikethroughFormatting(action: action, in: textView) diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBase.m b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBase.m index b754fbca1be..ea4e1d64297 100644 --- a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBase.m +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBase.m @@ -36,6 +36,7 @@ - (void)addSyntaxHighlightingToAttributedString:(NSMutableAttributedString *)att // reset shared custom attributes [attributedString removeAttribute:WKSourceEditorCustomKeyColorOrange range:range]; [attributedString removeAttribute:WKSourceEditorCustomKeyColorGreen range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyColorOrange range:range]; [attributedString addAttributes:self.attributes range:range]; } diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterList.h b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterList.h new file mode 100644 index 00000000000..a42dba04e1f --- /dev/null +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterList.h @@ -0,0 +1,12 @@ +#import "WKSourceEditorFormatter.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface WKSourceEditorFormatterList : WKSourceEditorFormatter +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isBulletSingleInRange:(NSRange)range; +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isBulletMultipleInRange:(NSRange)range; +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isNumberSingleInRange:(NSRange)range; +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isNumberMultipleInRange:(NSRange)range; +@end + +NS_ASSUME_NONNULL_END diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterList.m b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterList.m new file mode 100644 index 00000000000..de8ef933334 --- /dev/null +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterList.m @@ -0,0 +1,210 @@ +#import "WKSourceEditorFormatterList.h" +#import "WKSourceEditorColors.h" + +@interface WKSourceEditorFormatterList () + +@property (nonatomic, strong) NSDictionary *orangeAttributes; + +@property (nonatomic, strong) NSDictionary *bulletSingleContentAttributes; +@property (nonatomic, strong) NSDictionary *bulletMultipleContentAttributes; +@property (nonatomic, strong) NSDictionary *numberSingleContentAttributes; +@property (nonatomic, strong) NSDictionary *numberMultipleContentAttributes; + +@property (nonatomic, strong) NSRegularExpression *bulletSingleRegex; +@property (nonatomic, strong) NSRegularExpression *bulletMultipleRegex; +@property (nonatomic, strong) NSRegularExpression *numberSingleRegex; +@property (nonatomic, strong) NSRegularExpression *numberMultipleRegex; + +@end + +@implementation WKSourceEditorFormatterList + +#pragma mark - Custom Attributed String Keys + +NSString * const WKSourceEditorCustomKeyContentBulletSingle = @"WKSourceEditorCustomKeyContentBulletSingle"; +NSString * const WKSourceEditorCustomKeyContentBulletMultiple = @"WKSourceEditorCustomKeyContentBulletMultiple"; +NSString * const WKSourceEditorCustomKeyContentNumberSingle = @"WKSourceEditorCustomKeyContentNumberSingle"; +NSString * const WKSourceEditorCustomKeyContentNumberMultiple = @"WKSourceEditorCustomKeyContentNumberMultiple"; + +#pragma mark - Overrides + +- (instancetype)initWithColors:(WKSourceEditorColors *)colors fonts:(WKSourceEditorFonts *)fonts { + self = [super initWithColors:colors fonts:fonts]; + if (self) { + _orangeAttributes = @{ + NSForegroundColorAttributeName: colors.orangeForegroundColor, + WKSourceEditorCustomKeyColorOrange: [NSNumber numberWithBool:YES] + }; + + _bulletSingleContentAttributes = @{ + WKSourceEditorCustomKeyContentBulletSingle: [NSNumber numberWithBool:YES] + }; + + _bulletMultipleContentAttributes = @{ + WKSourceEditorCustomKeyContentBulletMultiple: [NSNumber numberWithBool:YES] + }; + + _numberSingleContentAttributes = @{ + WKSourceEditorCustomKeyContentNumberSingle: [NSNumber numberWithBool:YES] + }; + + _numberMultipleContentAttributes = @{ + WKSourceEditorCustomKeyContentNumberMultiple: [NSNumber numberWithBool:YES] + }; + + _bulletSingleRegex = [[NSRegularExpression alloc] initWithPattern:@"^(\\*{1})(.*)$" options:NSRegularExpressionAnchorsMatchLines error:nil]; + _bulletMultipleRegex = [[NSRegularExpression alloc] initWithPattern:@"^(\\*{2,})(.*)$" options:NSRegularExpressionAnchorsMatchLines error:nil]; + _numberSingleRegex = [[NSRegularExpression alloc] initWithPattern:@"^(#{1})(.*)$" options:NSRegularExpressionAnchorsMatchLines error:nil]; + _numberMultipleRegex = [[NSRegularExpression alloc] initWithPattern:@"^(#{2,})(.*)$" options:NSRegularExpressionAnchorsMatchLines error:nil]; + } + return self; +} + +- (void)addSyntaxHighlightingToAttributedString:(nonnull NSMutableAttributedString *)attributedString inRange:(NSRange)range { + + [attributedString removeAttribute:WKSourceEditorCustomKeyContentBulletSingle range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyContentBulletMultiple range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyContentNumberSingle range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyContentNumberMultiple range:range]; + + [self enumerateAndHighlightAttributedString:attributedString range:range singleRegex:self.bulletSingleRegex multipleRegex:self.bulletMultipleRegex singleContentAttributes:self.bulletSingleContentAttributes singleContentAttributes:self.bulletMultipleContentAttributes]; + [self enumerateAndHighlightAttributedString:attributedString range:range singleRegex:self.numberSingleRegex multipleRegex:self.numberMultipleRegex singleContentAttributes:self.numberSingleContentAttributes singleContentAttributes:self.numberMultipleContentAttributes]; +} + +- (void)updateColors:(WKSourceEditorColors *)colors inAttributedString:(NSMutableAttributedString *)attributedString inRange:(NSRange)range { + + // First update orangeAttributes property so that addSyntaxHighlighting has the correct color the next time it is called + NSMutableDictionary *mutAttributes = [[NSMutableDictionary alloc] initWithDictionary:self.orangeAttributes]; + [mutAttributes setObject:colors.orangeForegroundColor forKey:NSForegroundColorAttributeName]; + self.orangeAttributes = [[NSDictionary alloc] initWithDictionary:mutAttributes]; + + // Then update entire attributed string orange color + [attributedString enumerateAttribute:WKSourceEditorCustomKeyColorOrange + 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:self.orangeAttributes range:localRange]; + } + } + }]; +} + +- (void)updateFonts:(WKSourceEditorFonts *)fonts inAttributedString:(NSMutableAttributedString *)attributedString inRange:(NSRange)range { + // No special font handling needed for lists +} + +#pragma mark - Public + +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isBulletSingleInRange:(NSRange)range { + return [self attributedString:attributedString isContentKey:WKSourceEditorCustomKeyContentBulletSingle inRange:range]; +} + +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isBulletMultipleInRange:(NSRange)range { + return [self attributedString:attributedString isContentKey:WKSourceEditorCustomKeyContentBulletMultiple inRange:range]; +} + +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isNumberSingleInRange:(NSRange)range { + return [self attributedString:attributedString isContentKey:WKSourceEditorCustomKeyContentNumberSingle inRange:range]; +} + +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isNumberMultipleInRange:(NSRange)range { + return [self attributedString:attributedString isContentKey:WKSourceEditorCustomKeyContentNumberMultiple inRange:range]; +} + +#pragma mark - Private + +- (void)enumerateAndHighlightAttributedString: (nonnull NSMutableAttributedString *)attributedString range:(NSRange)range singleRegex:(NSRegularExpression *)singleRegex multipleRegex:(NSRegularExpression *)multipleRegex singleContentAttributes:(NSDictionary *)singleContentAttributes singleContentAttributes:(NSDictionary *)multipleContentAttributes { + + NSMutableArray *multipleRanges = [[NSMutableArray alloc] init]; + + [multipleRegex enumerateMatchesInString:attributedString.string + options:0 + range:range + usingBlock:^(NSTextCheckingResult *_Nullable result, NSMatchingFlags flags, BOOL *_Nonnull stop) { + NSRange fullMatch = [result rangeAtIndex:0]; + NSRange orangeRange = [result rangeAtIndex:1]; + NSRange contentRange = [result rangeAtIndex:2]; + + if (fullMatch.location != NSNotFound) { + [multipleRanges addObject:[NSValue valueWithRange:fullMatch]]; + } + + if (orangeRange.location != NSNotFound) { + [attributedString addAttributes:self.orangeAttributes range:orangeRange]; + } + + if (contentRange.location != NSNotFound) { + [attributedString addAttributes:multipleContentAttributes range:contentRange]; + } + }]; + + [singleRegex enumerateMatchesInString:attributedString.string + options:0 + range:range + usingBlock:^(NSTextCheckingResult *_Nullable result, NSMatchingFlags flags, BOOL *_Nonnull stop) { + NSRange fullMatch = [result rangeAtIndex:0]; + NSRange orangeRange = [result rangeAtIndex:1]; + NSRange contentRange = [result rangeAtIndex:2]; + + BOOL alreadyMultiple = NO; + for (NSValue *value in multipleRanges) { + NSRange multipleRange = value.rangeValue; + if (NSIntersectionRange(multipleRange, fullMatch).length != 0) { + alreadyMultiple = YES; + } + } + + if (alreadyMultiple) { + return; + } + + + if (orangeRange.location != NSNotFound) { + [attributedString addAttributes:self.orangeAttributes range:orangeRange]; + } + + if (contentRange.location != NSNotFound) { + [attributedString addAttributes:singleContentAttributes range:contentRange]; + } + }]; +} + +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isContentKey:(NSString *)contentKey inRange:(NSRange)range { + __block BOOL isContentKey = NO; + + if (range.length == 0) { + + if (attributedString.length > range.location) { + NSDictionary *attrs = [attributedString attributesAtIndex:range.location effectiveRange:nil]; + + if (attrs[contentKey] != nil) { + isContentKey = YES; + } + + // Edge case, check previous character in case we're at the end of the line and list isn't detected + if ((attributedString.length > range.location - 1)) { + NSDictionary *attrs = [attributedString attributesAtIndex:range.location-1 effectiveRange:nil]; + + if (attrs[contentKey] != nil) { + isContentKey = YES; + } + } + } + + } else { + [attributedString enumerateAttributesInRange:range options:nil usingBlock:^(NSDictionary * _Nonnull attrs, NSRange loopRange, BOOL * _Nonnull stop) { + if ((attrs[contentKey] != nil) && + (loopRange.location == range.location && loopRange.length == range.length)) { + isContentKey = YES; + stop = YES; + } + }]; + } + + return isContentKey; +} + +@end diff --git a/Components/Sources/ComponentsObjC/include/ComponentsObjC.h b/Components/Sources/ComponentsObjC/include/ComponentsObjC.h index 645e82ca254..4813b802b81 100644 --- a/Components/Sources/ComponentsObjC/include/ComponentsObjC.h +++ b/Components/Sources/ComponentsObjC/include/ComponentsObjC.h @@ -8,6 +8,7 @@ #import "WKSourceEditorFormatterBase.h" #import "WKSourceEditorFormatterBoldItalics.h" #import "WKSourceEditorFormatterTemplate.h" +#import "WKSourceEditorFormatterList.h" #import "WKSourceEditorFormatterHeading.h" #import "WKSourceEditorFormatterStrikethrough.h" #import "WKSourceEditorStorageDelegate.h" diff --git a/Components/Sources/ComponentsObjC/include/WKSourceEditorFormatterList.h b/Components/Sources/ComponentsObjC/include/WKSourceEditorFormatterList.h new file mode 120000 index 00000000000..ca459437160 --- /dev/null +++ b/Components/Sources/ComponentsObjC/include/WKSourceEditorFormatterList.h @@ -0,0 +1 @@ +../WKSourceEditorFormatterList.h \ No newline at end of file diff --git a/Components/Tests/ComponentsTests/WKSourceEditorFormatterButtonActionTests.swift b/Components/Tests/ComponentsTests/WKSourceEditorFormatterButtonActionTests.swift index 1365d0741c3..56c3bf99429 100644 --- a/Components/Tests/ComponentsTests/WKSourceEditorFormatterButtonActionTests.swift +++ b/Components/Tests/ComponentsTests/WKSourceEditorFormatterButtonActionTests.swift @@ -163,6 +163,70 @@ final class WKSourceEditorFormatterButtonActionTests: XCTestCase { XCTAssertEqual(mediator.textView.attributedText.string, "One Two Three Four") } + func testListBulletInsertAndRemove() throws { + let text = "Test" + mediator.textView.attributedText = NSAttributedString(string: text) + mediator.textView.selectedRange = NSRange(location: 2, length: 0) + mediator.listFormatter?.toggleListBullet(action: .add, in: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "* Test") + mediator.listFormatter?.toggleListBullet(action: .remove, in: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "Test") + } + + func testListBulletInsertAndIncreaseIndent() throws { + let text = "Test" + mediator.textView.attributedText = NSAttributedString(string: text) + mediator.textView.selectedRange = NSRange(location: 2, length: 0) + mediator.listFormatter?.toggleListBullet(action: .add, in: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "* Test") + mediator.listFormatter?.tappedIncreaseIndent(currentSelectionState: mediator.selectionState(selectedDocumentRange: mediator.textView.selectedRange), textView: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "** Test") + } + + func testListBulletDecreaseIndentAndRemove() throws { + let text = "*** Test" + mediator.textView.attributedText = NSAttributedString(string: text) + mediator.textView.selectedRange = NSRange(location: 4, length: 0) + mediator.listFormatter?.tappedDecreaseIndent(currentSelectionState: mediator.selectionState(selectedDocumentRange: mediator.textView.selectedRange), textView: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "** Test") + mediator.listFormatter?.tappedDecreaseIndent(currentSelectionState: mediator.selectionState(selectedDocumentRange: mediator.textView.selectedRange), textView: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "* Test") + mediator.listFormatter?.toggleListBullet(action: .remove, in: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "Test") + } + + func testListNumberInsertAndRemove() throws { + let text = "Test" + mediator.textView.attributedText = NSAttributedString(string: text) + mediator.textView.selectedRange = NSRange(location: 2, length: 0) + mediator.listFormatter?.toggleListNumber(action: .add, in: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "# Test") + mediator.listFormatter?.toggleListNumber(action: .remove, in: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "Test") + } + + func testListNumberInsertAndIncreaseIndent() throws { + let text = "Test" + mediator.textView.attributedText = NSAttributedString(string: text) + mediator.textView.selectedRange = NSRange(location: 2, length: 0) + mediator.listFormatter?.toggleListNumber(action: .add, in: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "# Test") + mediator.listFormatter?.tappedIncreaseIndent(currentSelectionState: mediator.selectionState(selectedDocumentRange: mediator.textView.selectedRange), textView: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "## Test") + } + + func testListNumberDecreaseIndentAndRemove() throws { + let text = "### Test" + mediator.textView.attributedText = NSAttributedString(string: text) + mediator.textView.selectedRange = NSRange(location: 4, length: 0) + mediator.listFormatter?.tappedDecreaseIndent(currentSelectionState: mediator.selectionState(selectedDocumentRange: mediator.textView.selectedRange), textView: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "## Test") + mediator.listFormatter?.tappedDecreaseIndent(currentSelectionState: mediator.selectionState(selectedDocumentRange: mediator.textView.selectedRange), textView: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "# Test") + mediator.listFormatter?.toggleListNumber(action: .remove, in: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "Test") + } + func testHeadingAdd() throws { let text = "Test" let selectedRange = NSRange(location: 0, length: 4) diff --git a/Components/Tests/ComponentsTests/WKSourceEditorFormatterTests.swift b/Components/Tests/ComponentsTests/WKSourceEditorFormatterTests.swift index c9ab4ca0aca..638407f3005 100644 --- a/Components/Tests/ComponentsTests/WKSourceEditorFormatterTests.swift +++ b/Components/Tests/ComponentsTests/WKSourceEditorFormatterTests.swift @@ -10,10 +10,11 @@ final class WKSourceEditorFormatterTests: XCTestCase { var baseFormatter: WKSourceEditorFormatterBase! var boldItalicsFormatter: WKSourceEditorFormatterBoldItalics! var templateFormatter: WKSourceEditorFormatterTemplate! + var listFormatter: WKSourceEditorFormatterList! var headingFormatter: WKSourceEditorFormatterHeading! var strikethroughFormatter: WKSourceEditorFormatterStrikethrough! var formatters: [WKSourceEditorFormatter] { - return [baseFormatter, templateFormatter, boldItalicsFormatter, headingFormatter, strikethroughFormatter] + return [baseFormatter, templateFormatter, boldItalicsFormatter, listFormatter, headingFormatter, strikethroughFormatter] } override func setUpWithError() throws { @@ -39,6 +40,7 @@ final class WKSourceEditorFormatterTests: XCTestCase { self.baseFormatter = WKSourceEditorFormatterBase(colors: colors, fonts: fonts, textAlignment: .left) self.boldItalicsFormatter = WKSourceEditorFormatterBoldItalics(colors: colors, fonts: fonts) self.templateFormatter = WKSourceEditorFormatterTemplate(colors: colors, fonts: fonts) + self.listFormatter = WKSourceEditorFormatterList(colors: colors, fonts: fonts) self.headingFormatter = WKSourceEditorFormatterHeading(colors: colors, fonts: fonts) self.strikethroughFormatter = WKSourceEditorFormatterStrikethrough(colors: colors, fonts: fonts) } @@ -783,6 +785,114 @@ final class WKSourceEditorFormatterTests: XCTestCase { XCTAssertEqual(refAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect ref formatting") } + func testListSingleBullet() { + let string = "* Testing" + let mutAttributedString = NSMutableAttributedString(string: string) + + for formatter in formatters { + formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) + } + + var bulletRange = NSRange(location: 0, length: 0) + let bulletAttributes = mutAttributedString.attributes(at: 0, effectiveRange: &bulletRange) + + var textRange = NSRange(location: 0, length: 0) + let textAttributes = mutAttributedString.attributes(at: 1, effectiveRange: &textRange) + + // "*" + XCTAssertEqual(bulletRange.location, 0, "Incorrect list formatting") + XCTAssertEqual(bulletRange.length, 1, "Incorrect list formatting") + XCTAssertEqual(bulletAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect list formatting") + XCTAssertEqual(bulletAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect list formatting") + + // " Testing" + XCTAssertEqual(textRange.location, 1, "Incorrect list formatting") + XCTAssertEqual(textRange.length, 8, "Incorrect list formatting") + XCTAssertEqual(textAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect list formatting") + XCTAssertEqual(textAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect list formatting") + } + + func testListSingleNumber() { + let string = "# Testing" + let mutAttributedString = NSMutableAttributedString(string: string) + + for formatter in formatters { + formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) + } + + var numberRange = NSRange(location: 0, length: 0) + let numberAttributes = mutAttributedString.attributes(at: 0, effectiveRange: &numberRange) + + var textRange = NSRange(location: 0, length: 0) + let textAttributes = mutAttributedString.attributes(at: 1, effectiveRange: &textRange) + + // "*" + XCTAssertEqual(numberRange.location, 0, "Incorrect list formatting") + XCTAssertEqual(numberRange.length, 1, "Incorrect list formatting") + XCTAssertEqual(numberAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect list formatting") + XCTAssertEqual(numberAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect list formatting") + + // " Testing" + XCTAssertEqual(textRange.location, 1, "Incorrect list formatting") + XCTAssertEqual(textRange.length, 8, "Incorrect list formatting") + XCTAssertEqual(textAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect list formatting") + XCTAssertEqual(textAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect list formatting") + } + + func testListMultipleBulletNoSpace() { + let string = "***Testing" + let mutAttributedString = NSMutableAttributedString(string: string) + + for formatter in formatters { + formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) + } + + var bulletRange = NSRange(location: 0, length: 0) + let bulletAttributes = mutAttributedString.attributes(at: 0, effectiveRange: &bulletRange) + + var textRange = NSRange(location: 0, length: 0) + let textAttributes = mutAttributedString.attributes(at: 3, effectiveRange: &textRange) + + // "*" + XCTAssertEqual(bulletRange.location, 0, "Incorrect list formatting") + XCTAssertEqual(bulletRange.length, 3, "Incorrect list formatting") + XCTAssertEqual(bulletAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect list formatting") + XCTAssertEqual(bulletAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect list formatting") + + // " Testing" + XCTAssertEqual(textRange.location, 3, "Incorrect list formatting") + XCTAssertEqual(textRange.length, 7, "Incorrect list formatting") + XCTAssertEqual(textAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect list formatting") + XCTAssertEqual(textAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect list formatting") + } + + func testListMultipleNumberNoSpace() { + let string = "###Testing" + let mutAttributedString = NSMutableAttributedString(string: string) + + for formatter in formatters { + formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) + } + + var numberRange = NSRange(location: 0, length: 0) + let numberAttributes = mutAttributedString.attributes(at: 0, effectiveRange: &numberRange) + + var textRange = NSRange(location: 0, length: 0) + let textAttributes = mutAttributedString.attributes(at: 3, effectiveRange: &textRange) + + // "*" + XCTAssertEqual(numberRange.location, 0, "Incorrect list formatting") + XCTAssertEqual(numberRange.length, 3, "Incorrect list formatting") + XCTAssertEqual(numberAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect list formatting") + XCTAssertEqual(numberAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect list formatting") + + // " Testing" + XCTAssertEqual(textRange.location, 3, "Incorrect list formatting") + XCTAssertEqual(textRange.length, 7, "Incorrect list formatting") + XCTAssertEqual(textAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect list formatting") + XCTAssertEqual(textAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect list formatting") + } + func testHeading() { let string = "== Test ==" let mutAttributedString = NSMutableAttributedString(string: string) diff --git a/Components/Tests/ComponentsTests/WKSourceEditorTextFrameworkMediatorTests.swift b/Components/Tests/ComponentsTests/WKSourceEditorTextFrameworkMediatorTests.swift index f08fc586c86..c5d0f029efa 100644 --- a/Components/Tests/ComponentsTests/WKSourceEditorTextFrameworkMediatorTests.swift +++ b/Components/Tests/ComponentsTests/WKSourceEditorTextFrameworkMediatorTests.swift @@ -157,6 +157,54 @@ final class WKSourceEditorTextFrameworkMediatorTests: XCTestCase { XCTAssertFalse(selectionStates1.isHorizontalTemplate) } + func testListBulletSingleSelectionState() throws { + + let text = "* Test" + mediator.textView.attributedText = NSAttributedString(string: text) + + let selectionStates = mediator.selectionState(selectedDocumentRange: NSRange(location: 2, length: 4)) + XCTAssertTrue(selectionStates.isBulletSingleList) + XCTAssertFalse(selectionStates.isBulletMultipleList) + XCTAssertFalse(selectionStates.isNumberSingleList) + XCTAssertFalse(selectionStates.isNumberMultipleList) + } + + func testListBulletMultipleSelectionState() throws { + + let text = "** Test" + mediator.textView.attributedText = NSAttributedString(string: text) + + let selectionStates = mediator.selectionState(selectedDocumentRange: NSRange(location: 3, length: 0)) + XCTAssertFalse(selectionStates.isBulletSingleList) + XCTAssertTrue(selectionStates.isBulletMultipleList) + XCTAssertFalse(selectionStates.isNumberSingleList) + XCTAssertFalse(selectionStates.isNumberMultipleList) + } + + func testListNumberSingleSelectionState() throws { + + let text = "# Test" + mediator.textView.attributedText = NSAttributedString(string: text) + + let selectionStates = mediator.selectionState(selectedDocumentRange: NSRange(location: 2, length: 4)) + XCTAssertFalse(selectionStates.isBulletSingleList) + XCTAssertFalse(selectionStates.isBulletMultipleList) + XCTAssertTrue(selectionStates.isNumberSingleList) + XCTAssertFalse(selectionStates.isNumberMultipleList) + } + + func testListNumberMultipleSelectionState() throws { + + let text = "## Test" + mediator.textView.attributedText = NSAttributedString(string: text) + + let selectionStates = mediator.selectionState(selectedDocumentRange: NSRange(location: 3, length: 0)) + XCTAssertFalse(selectionStates.isBulletSingleList) + XCTAssertFalse(selectionStates.isBulletMultipleList) + XCTAssertFalse(selectionStates.isNumberSingleList) + XCTAssertTrue(selectionStates.isNumberMultipleList) + } + func testHorizontalTemplateButtonSelectionStateFormattedRange() throws { let text = "Testing inner formatted {{cite web | url=https://en.wikipedia.org | title = The '''Free''' Encyclopedia}} template example." mediator.textView.attributedText = NSAttributedString(string: text)