mirror of
https://github.com/zhigang1992/GitHawk.git
synced 2026-05-27 08:42:56 +08:00
working basic markdown commands
This commit is contained in:
140
Classes/Issues/IssueTextActionsView.swift
Normal file
140
Classes/Issues/IssueTextActionsView.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
66
Classes/Views/UITextView+SelectedRange.swift
Normal file
66
Classes/Views/UITextView+SelectedRange.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 */,
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1217</string>
|
||||
<string>1252</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user