working basic markdown commands

This commit is contained in:
Ryan Nystrom
2017-07-31 23:55:34 -04:00
parent 19d246a5ff
commit 5800f63293
7 changed files with 271 additions and 3 deletions

View File

@@ -0,0 +1,140 @@
//
// IssueTextActionsView.swift
// Freetime
//
// Created by Ryan Nystrom on 7/31/17.
// Copyright © 2017 Ryan Nystrom. All rights reserved.
//
import UIKit
final class IssueTextActionsCell: UICollectionViewCell {
let label = UILabel()
let imageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
let color = Styles.Colors.Gray.dark.color
label.font = Styles.Fonts.button
label.textColor = color
label.backgroundColor = .clear
contentView.addSubview(label)
imageView.tintColor = color
imageView.contentMode = .center
contentView.addSubview(imageView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
label.text = ""
imageView.image = nil
}
override func layoutSubviews() {
super.layoutSubviews()
label.frame = contentView.bounds
imageView.frame = contentView.bounds
}
}
protocol IssueTextActionsViewDelegate: class {
func didSelect(actionsView: IssueTextActionsView, operation: IssueTextActionOperation)
}
struct IssueTextActionOperation {
enum Icon {
case text(NSAttributedString)
case image(UIImage?)
}
enum Operation {
case line(String)
case wrap(String, String)
case execute(() -> ())
}
let icon: Icon
let operation: Operation
}
final class IssueTextActionsView: UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
weak var delegate: IssueTextActionsViewDelegate? = nil
private let operations: [IssueTextActionOperation]
private let identifier = "cell"
private let collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
let c = UICollectionView(frame: .zero, collectionViewLayout: layout)
c.backgroundColor = .clear
c.alwaysBounceVertical = false
c.alwaysBounceHorizontal = true
c.contentInset = UIEdgeInsets(top: 0, left: Styles.Sizes.gutter, bottom: 0, right: Styles.Sizes.gutter)
return c
}()
init(operations: [IssueTextActionOperation]) {
self.operations = operations
super.init(frame: .zero)
translatesAutoresizingMaskIntoConstraints = false
collectionView.register(IssueTextActionsCell.self, forCellWithReuseIdentifier: identifier)
collectionView.dataSource = self
collectionView.delegate = self
addSubview(collectionView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
collectionView.frame = bounds
}
// MARK: UICollectionViewDataSource
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return operations.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) as? IssueTextActionsCell
else { fatalError("Wrong cell type") }
let operation = operations[indexPath.item]
switch operation.icon {
case .image(let image): cell.imageView.image = image
case .text(let text): cell.label.attributedText = text
}
return cell
}
// MARK: UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
delegate?.didSelect(actionsView: self, operation: operations[indexPath.item])
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let height = collectionView.bounds.height
return CGSize(width: height, height: height)
}
}

View File

@@ -11,6 +11,7 @@ import IGListKit
import TUSafariActivity
import SafariServices
import SlackTextViewController
import SnapKit
final class IssuesViewController: SLKTextViewController,
ListAdapterDataSource,
@@ -18,7 +19,8 @@ final class IssuesViewController: SLKTextViewController,
AddCommentListener,
IssueCommentAutocompleteDelegate,
FeedSelectionProviding,
IssueNeckLoadSectionControllerDelegate {
IssueNeckLoadSectionControllerDelegate,
IssueTextActionsViewDelegate {
private let client: GithubClient
private let owner: String
@@ -37,6 +39,9 @@ IssueNeckLoadSectionControllerDelegate {
private var current: IssueResult? = nil {
didSet {
self.setTextInputbarHidden(current == nil, animated: true)
// hack required to get textInputBar.contentView + textView laid out correctly
self.textInputbar.layoutIfNeeded()
}
}
private var sentComments = [ListDiffable]()
@@ -90,6 +95,38 @@ IssueNeckLoadSectionControllerDelegate {
// displayed once an add comment client is created (requires a gql subject id)
// setTextInputbarHidden(true, animated: false)
let operations: [IssueTextActionOperation] = [
IssueTextActionOperation(icon: .image(UIImage(named: "eye-small")), operation: .execute({ [weak self] in
self?.onPreview()
})),
IssueTextActionOperation(icon: .text(NSAttributedString(string: "*b*", attributes: [NSFontAttributeName: Styles.Fonts.bodyBold])), operation: .wrap("**", "**")),
IssueTextActionOperation(icon: .text(NSAttributedString(string: "_i_", attributes: [NSFontAttributeName: Styles.Fonts.bodyItalic])), operation: .wrap("_", "_")),
IssueTextActionOperation(icon: .text(NSAttributedString(string: "`c`", attributes: [NSFontAttributeName: Styles.Fonts.code])), operation: .wrap("`", "`")),
IssueTextActionOperation(icon: .text(NSAttributedString(string: "```", attributes: [NSFontAttributeName: Styles.Fonts.code])), operation: .wrap("```\n", "\n```")),
IssueTextActionOperation(icon: .text(NSAttributedString(string: "St", attributes: [NSFontAttributeName: Styles.Fonts.body, NSStrikethroughStyleAttributeName: NSUnderlineStyle.styleSingle.rawValue])), operation: .wrap("~~", "~~")),
]
let actions = IssueTextActionsView(operations: operations)
actions.delegate = self
// using visual format re: https://github.com/slackhq/SlackTextViewController/issues/596
// i'm not sure exactly what these would be in SnapKit (would pref SK tho)
let contentView = textInputbar.contentView
contentView.addSubview(actions)
let views = ["actions": actions]
contentView.addConstraints(NSLayoutConstraint.constraints(
withVisualFormat: "V:|[actions(30)]|",
options: [],
metrics: nil,
views: views
))
contentView.addConstraints(NSLayoutConstraint.constraints(
withVisualFormat: "H:|[actions]|",
options: [],
metrics: nil,
views: views
))
self.textInputbar.layoutIfNeeded()
let rightItem = UIBarButtonItem(
image: UIImage(named: "bullets-hollow"),
style: .plain,
@@ -207,6 +244,10 @@ IssueNeckLoadSectionControllerDelegate {
}
}
func onPreview() {
print("preview")
}
// MARK: ListAdapterDataSource
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
@@ -306,4 +347,14 @@ IssueNeckLoadSectionControllerDelegate {
fetch(previous: true)
}
// MARK: IssueTextActionsViewDelegate
func didSelect(actionsView: IssueTextActionsView, operation: IssueTextActionOperation) {
switch operation.operation {
case .execute(let block): block()
case .wrap(let left, let right): textView.replace(left: left, right: right, atLineStart: false)
case .line(let left): textView.replace(left: left, right: nil, atLineStart: true)
}
}
}

View File

@@ -59,6 +59,7 @@ enum Styles {
enum Fonts {
static let body = UIFont.systemFont(ofSize: Styles.Sizes.Text.body)
static let bodyBold = UIFont.boldSystemFont(ofSize: Styles.Sizes.Text.body)
static let bodyItalic = UIFont.italicSystemFont(ofSize: Styles.Sizes.Text.body)
static let secondary = UIFont.systemFont(ofSize: Styles.Sizes.Text.secondary)
static let secondaryBold = UIFont.boldSystemFont(ofSize: Styles.Sizes.Text.secondary)
static let title = UIFont.boldSystemFont(ofSize: Styles.Sizes.Text.title)

View File

@@ -0,0 +1,66 @@
//
// UITextView+SelectedRange.swift
// Freetime
//
// Created by Ryan Nystrom on 7/31/17.
// Copyright © 2017 Ryan Nystrom. All rights reserved.
//
import UIKit
extension UITextView {
private func oneCharRange(pos: UITextPosition?) -> UITextRange? {
guard let pos = pos,
let position = self.position(from: pos, offset: 1) else { return nil }
return self.textRange(from: pos, to: position)
}
private func text(atPosition position: UITextPosition?) -> String? {
guard let position = position,
let range = oneCharRange(pos: position) else { return nil }
return text(in: range)
}
func startOfLine(forRange range: UITextRange) -> UITextPosition {
func previousPosition(pos: UITextPosition?) -> UITextPosition? {
guard let pos = pos else { return nil }
return self.position(from: pos, offset: -1)
}
var position: UITextPosition? = previousPosition(pos: range.start)
while let text = text(atPosition: position), text != "\n" { // check if it's the EoL
position = previousPosition(pos: position) // move back 1 char
}
if let position = position, // we have a position
let pos = self.position(from: position, offset: 1) { // need to advance by one...
return pos
}
return beginningOfDocument // not found? Go to the beginning
}
func replace(left: String, right: String?, atLineStart: Bool) {
guard let range = selectedTextRange, // seems to be always set
let text = text(in: range) // no selection = ""
else { return }
let replacementText = "\(left)\(text)\(right ?? "")"
var insertionRange = range
if atLineStart {
let startLinePosition = startOfLine(forRange: range)
insertionRange = textRange(from: startLinePosition, to: startLinePosition) ?? range
}
replace(insertionRange, withText: replacementText)
if range.start == range.end, // single cursor (no selection)
let position = position(from: range.start, // advance by the inserted before
offset: left.lengthOfBytes(using: .utf8)) {
selectedTextRange = textRange(from: position, to: position) // single cursor
}
}
}

View File

@@ -79,6 +79,8 @@
292FF8AA1F2FC3E5009E63F7 /* _notifications.json in Resources */ = {isa = PBXBuildFile; fileRef = 296CD8251F014130001190B9 /* _notifications.json */; };
292FF8AC1F2FD3EC009E63F7 /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 292FF8AB1F2FD3EC009E63F7 /* Settings.storyboard */; };
292FF8AE1F2FD4A8009E63F7 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292FF8AD1F2FD4A8009E63F7 /* SettingsViewController.swift */; };
292FF8B01F2FDC33009E63F7 /* IssueTextActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292FF8AF1F2FDC33009E63F7 /* IssueTextActionsView.swift */; };
292FF8B21F302FE7009E63F7 /* UITextView+SelectedRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292FF8B11F302FE7009E63F7 /* UITextView+SelectedRange.swift */; };
29316DB51ECC7DEB007CAE3F /* ButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29316DB41ECC7DEB007CAE3F /* ButtonCell.swift */; };
29316DBD1ECC8970007CAE3F /* GithubUserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29316DBC1ECC8970007CAE3F /* GithubUserSession.swift */; };
29316DBF1ECC95DB007CAE3F /* RootViewControllers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29316DBE1ECC95DB007CAE3F /* RootViewControllers.swift */; };
@@ -357,6 +359,8 @@
292FCB2B1EE054900026635E /* API.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = API.swift; path = gql/API.swift; sourceTree = "<group>"; };
292FF8AB1F2FD3EC009E63F7 /* Settings.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Settings.storyboard; sourceTree = "<group>"; };
292FF8AD1F2FD4A8009E63F7 /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
292FF8AF1F2FDC33009E63F7 /* IssueTextActionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueTextActionsView.swift; sourceTree = "<group>"; };
292FF8B11F302FE7009E63F7 /* UITextView+SelectedRange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITextView+SelectedRange.swift"; sourceTree = "<group>"; };
29316DB41ECC7DEB007CAE3F /* ButtonCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonCell.swift; sourceTree = "<group>"; };
29316DBC1ECC8970007CAE3F /* GithubUserSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GithubUserSession.swift; sourceTree = "<group>"; };
29316DBE1ECC95DB007CAE3F /* RootViewControllers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootViewControllers.swift; sourceTree = "<group>"; };
@@ -621,6 +625,7 @@
290D2A411F04D3470082E6CC /* IssueStatus.swift */,
295C31CE1F0AA67600521CED /* IssueStatus+ButtonState.swift */,
292FCAE91EDFCC510026635E /* IssuesViewController.swift */,
292FF8AF1F2FDC33009E63F7 /* IssueTextActionsView.swift */,
294563EF1EE5036A00DBCD35 /* IssueType.swift */,
292FCAEA1EDFCC510026635E /* IssueViewModels.swift */,
29FB942A1EE750720016E6D4 /* Labeled */,
@@ -1049,8 +1054,9 @@
2958406E1EE9F21E007723C6 /* UIButton+Label.swift */,
29A195011EC66B8B00C3E289 /* UIColor+Hex.swift */,
29A4769F1ED0E6C6005D0953 /* UIColor+Overlay.swift */,
298BA08E1EC90FEE00B01946 /* UIView+BottomBorder.swift */,
290744B91F26863100FD9E48 /* UIScrollView+ScrollToBottom.swift */,
292FF8B11F302FE7009E63F7 /* UITextView+SelectedRange.swift */,
298BA08E1EC90FEE00B01946 /* UIView+BottomBorder.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -1496,6 +1502,7 @@
292CD3CC1F0DB89600D3D57B /* IssueReviewSectionController.swift in Sources */,
297A37301F1704C10081C04E /* IssueRequestSectionController.swift in Sources */,
292CD3D61F0DC4DA00D3D57B /* UIViewController+AttributedStringViewDelegate.swift in Sources */,
292FF8B21F302FE7009E63F7 /* UITextView+SelectedRange.swift in Sources */,
29F7F0631F2A875C00F6075D /* IssueNeckLoadModel.swift in Sources */,
29C33FDF1F128D4400EC8D40 /* SelectableCell.swift in Sources */,
297AE87C1EC0D5C200B44A1F /* Secrets.swift in Sources */,
@@ -1564,6 +1571,7 @@
292FCB131EDFCC510026635E /* IssueLabelDotCell.swift in Sources */,
292FCB1F1EDFCD750026635E /* IssueCommentReactionViewModel.swift in Sources */,
294967531EFC1EDB00B1CF1A /* IssueCommentHtmlCell.swift in Sources */,
292FF8B01F2FDC33009E63F7 /* IssueTextActionsView.swift in Sources */,
297403DB1F18550200ABA95A /* IssueAssigneeSummaryModel.swift in Sources */,
29FF85A31EE1CFF7007B8762 /* IssueLabelModel.swift in Sources */,
290744BC1F268D8300FD9E48 /* UserAutocomplete.swift in Sources */,

View File

@@ -32,7 +32,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>1217</string>
<string>1252</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationQueriesSchemes</key>

View File

@@ -146,6 +146,8 @@ NS_ASSUME_NONNULL_BEGIN
/** The color used for character counter label when it has exceeded the limit. Default is redColor. */
@property (nonatomic, strong, readwrite) UIColor *charCountLabelWarningColor;
- (void)slk_updateConstraintConstants;
@end
NS_ASSUME_NONNULL_END