diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/TheRichTextEditor.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/TheRichTextEditor.xcscheme new file mode 100644 index 0000000..3a6c63a --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/TheRichTextEditor.xcscheme @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Package.swift b/Package.swift index 5b0d0e5..8d1feda 100644 --- a/Package.swift +++ b/Package.swift @@ -5,6 +5,9 @@ import PackageDescription let package = Package( name: "TheRichTextEditor", + platforms: [ + .iOS(.v11) + ], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( @@ -20,7 +23,8 @@ let package = Package( // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: "TheRichTextEditor", - dependencies: []), + dependencies: [], + resources: [.process("main.js"), .process("main.html")]), .testTarget( name: "TheRichTextEditorTests", dependencies: ["TheRichTextEditor"]), diff --git a/README.md b/README.md index 9737bae..6aedefd 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # TheRichTextEditor A description of this package. + +
Icons made by bqlqn from www.flaticon.com
diff --git a/Sources/TheRichTextEditor/Assets.xcassets/Contents.json b/Sources/TheRichTextEditor/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Sources/TheRichTextEditor/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/TheRichTextEditor/Assets.xcassets/alignCenter.imageset/Contents.json b/Sources/TheRichTextEditor/Assets.xcassets/alignCenter.imageset/Contents.json new file mode 100644 index 0000000..74944a6 --- /dev/null +++ b/Sources/TheRichTextEditor/Assets.xcassets/alignCenter.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "align-center.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/TheRichTextEditor/Assets.xcassets/alignCenter.imageset/align-center.png b/Sources/TheRichTextEditor/Assets.xcassets/alignCenter.imageset/align-center.png new file mode 100644 index 0000000..682c46c Binary files /dev/null and b/Sources/TheRichTextEditor/Assets.xcassets/alignCenter.imageset/align-center.png differ diff --git a/Sources/TheRichTextEditor/Assets.xcassets/alignLeft.imageset/Contents.json b/Sources/TheRichTextEditor/Assets.xcassets/alignLeft.imageset/Contents.json new file mode 100644 index 0000000..d071c67 --- /dev/null +++ b/Sources/TheRichTextEditor/Assets.xcassets/alignLeft.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "align-left.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/TheRichTextEditor/Assets.xcassets/alignLeft.imageset/align-left.png b/Sources/TheRichTextEditor/Assets.xcassets/alignLeft.imageset/align-left.png new file mode 100644 index 0000000..8b418ac Binary files /dev/null and b/Sources/TheRichTextEditor/Assets.xcassets/alignLeft.imageset/align-left.png differ diff --git a/Sources/TheRichTextEditor/Assets.xcassets/alignRight.imageset/Contents.json b/Sources/TheRichTextEditor/Assets.xcassets/alignRight.imageset/Contents.json new file mode 100644 index 0000000..0dd6a7a --- /dev/null +++ b/Sources/TheRichTextEditor/Assets.xcassets/alignRight.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "align-right.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/TheRichTextEditor/Assets.xcassets/alignRight.imageset/align-right.png b/Sources/TheRichTextEditor/Assets.xcassets/alignRight.imageset/align-right.png new file mode 100644 index 0000000..764230c Binary files /dev/null and b/Sources/TheRichTextEditor/Assets.xcassets/alignRight.imageset/align-right.png differ diff --git a/Sources/TheRichTextEditor/Assets.xcassets/bold.imageset/Contents.json b/Sources/TheRichTextEditor/Assets.xcassets/bold.imageset/Contents.json new file mode 100644 index 0000000..0acc732 --- /dev/null +++ b/Sources/TheRichTextEditor/Assets.xcassets/bold.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "bold.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/TheRichTextEditor/Assets.xcassets/bold.imageset/bold.png b/Sources/TheRichTextEditor/Assets.xcassets/bold.imageset/bold.png new file mode 100644 index 0000000..5c9617f Binary files /dev/null and b/Sources/TheRichTextEditor/Assets.xcassets/bold.imageset/bold.png differ diff --git a/Sources/TheRichTextEditor/Assets.xcassets/clear.imageset/Contents.json b/Sources/TheRichTextEditor/Assets.xcassets/clear.imageset/Contents.json new file mode 100644 index 0000000..1b5e288 --- /dev/null +++ b/Sources/TheRichTextEditor/Assets.xcassets/clear.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "paragraph.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/TheRichTextEditor/Assets.xcassets/clear.imageset/paragraph.png b/Sources/TheRichTextEditor/Assets.xcassets/clear.imageset/paragraph.png new file mode 100644 index 0000000..07a351d Binary files /dev/null and b/Sources/TheRichTextEditor/Assets.xcassets/clear.imageset/paragraph.png differ diff --git a/Sources/TheRichTextEditor/Assets.xcassets/indent.imageset/Contents.json b/Sources/TheRichTextEditor/Assets.xcassets/indent.imageset/Contents.json new file mode 100644 index 0000000..6e0c680 --- /dev/null +++ b/Sources/TheRichTextEditor/Assets.xcassets/indent.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "indent.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/TheRichTextEditor/Assets.xcassets/indent.imageset/indent.png b/Sources/TheRichTextEditor/Assets.xcassets/indent.imageset/indent.png new file mode 100644 index 0000000..bb9e210 Binary files /dev/null and b/Sources/TheRichTextEditor/Assets.xcassets/indent.imageset/indent.png differ diff --git a/Sources/TheRichTextEditor/Assets.xcassets/italic.imageset/Contents.json b/Sources/TheRichTextEditor/Assets.xcassets/italic.imageset/Contents.json new file mode 100644 index 0000000..bd31029 --- /dev/null +++ b/Sources/TheRichTextEditor/Assets.xcassets/italic.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "italic.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/TheRichTextEditor/Assets.xcassets/italic.imageset/italic.png b/Sources/TheRichTextEditor/Assets.xcassets/italic.imageset/italic.png new file mode 100644 index 0000000..670b10d Binary files /dev/null and b/Sources/TheRichTextEditor/Assets.xcassets/italic.imageset/italic.png differ diff --git a/Sources/TheRichTextEditor/Assets.xcassets/outdent.imageset/Contents.json b/Sources/TheRichTextEditor/Assets.xcassets/outdent.imageset/Contents.json new file mode 100644 index 0000000..6c09046 --- /dev/null +++ b/Sources/TheRichTextEditor/Assets.xcassets/outdent.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "right-indent.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/TheRichTextEditor/Assets.xcassets/outdent.imageset/right-indent.png b/Sources/TheRichTextEditor/Assets.xcassets/outdent.imageset/right-indent.png new file mode 100644 index 0000000..16b3028 Binary files /dev/null and b/Sources/TheRichTextEditor/Assets.xcassets/outdent.imageset/right-indent.png differ diff --git a/Sources/TheRichTextEditor/Assets.xcassets/redo.imageset/Contents.json b/Sources/TheRichTextEditor/Assets.xcassets/redo.imageset/Contents.json new file mode 100644 index 0000000..0cd92b1 --- /dev/null +++ b/Sources/TheRichTextEditor/Assets.xcassets/redo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "redo.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/TheRichTextEditor/Assets.xcassets/redo.imageset/redo.png b/Sources/TheRichTextEditor/Assets.xcassets/redo.imageset/redo.png new file mode 100644 index 0000000..3e982da Binary files /dev/null and b/Sources/TheRichTextEditor/Assets.xcassets/redo.imageset/redo.png differ diff --git a/Sources/TheRichTextEditor/Assets.xcassets/undo.imageset/Contents.json b/Sources/TheRichTextEditor/Assets.xcassets/undo.imageset/Contents.json new file mode 100644 index 0000000..ad7cac4 --- /dev/null +++ b/Sources/TheRichTextEditor/Assets.xcassets/undo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "undo.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/TheRichTextEditor/Assets.xcassets/undo.imageset/undo.png b/Sources/TheRichTextEditor/Assets.xcassets/undo.imageset/undo.png new file mode 100644 index 0000000..462a986 Binary files /dev/null and b/Sources/TheRichTextEditor/Assets.xcassets/undo.imageset/undo.png differ diff --git a/Sources/TheRichTextEditor/TheRichTextEditor.swift b/Sources/TheRichTextEditor/TheRichTextEditor.swift index 0ebcc00..b7017b7 100644 --- a/Sources/TheRichTextEditor/TheRichTextEditor.swift +++ b/Sources/TheRichTextEditor/TheRichTextEditor.swift @@ -1,3 +1,351 @@ -struct TheRichTextEditor { - var text = "Hello, World!" +// +// TheRichTextEditor.swift +// iOS-Email-Client +// +// Created by Pedro Iniguez on 12/17/20. +// Copyright © 2020 Criptext Inc. All rights reserved. +// + +import Foundation +import UIKit +import WebKit + +public protocol TheRichTextEditorDelegate: class { + func textDidChange(content: String) + func heightDidChange() + func editorDidLoad() + func scrollOffset(verticalOffset: CGFloat) +} + +fileprivate class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler { + weak var delegate: WKScriptMessageHandler? + + init(delegate: WKScriptMessageHandler) { + self.delegate = delegate + } + + public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + self.delegate?.userContentController(userContentController, didReceive: message) + } +} + +public class TheRichTextEditor: UIView, WKScriptMessageHandler, WKNavigationDelegate, UIScrollViewDelegate { + private static let textDidChange = "textDidChange" + private static let heightDidChange = "heightDidChange" + private static let previewDidChange = "previewDidChange" + private static let documentHasLoaded = "documentHasLoaded" + + private static let defaultHeight: CGFloat = 60 + + public weak var delegate: TheRichTextEditorDelegate? + public var height: CGFloat = TheRichTextEditor.defaultHeight + + public var placeholder: String? { + didSet { + webView.evaluateJavaScript("richeditor.setPlaceholderText('\(placeholder ?? "")')") + } + } + + private var textToLoad: String? + + public var html: String = "" { + didSet { + if webView.isLoading { + textToLoad = html + } else { + webView.evaluateJavaScript("richeditor.insertText(\"\(html.htmlEscapeQuotes)\");") + body = html + } + } + } + + public var preview: String = "" + public var body: String = "" + + var webView: WKWebView! + + public override init(frame: CGRect = .zero) { + super.init(frame: frame) + setup() + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + var enableAccessoryView: Bool { + set { + (webView as? CustomWebview)?.enableAccessoryView = newValue + } + + get { + return (webView as? CustomWebview)?.enableAccessoryView ?? false + } + } + + func setup() { + guard let scriptPath = Bundle.main.path(forResource: "main", ofType: "js"), + let scriptContent = try? String(contentsOfFile: scriptPath, encoding: String.Encoding.utf8), + let htmlPath = Bundle.main.path(forResource: "main", ofType: "html"), + let html = try? String(contentsOfFile: htmlPath, encoding: String.Encoding.utf8) + else { + fatalError("Unable to find javscript/html for text editor") + } + + let configuration = WKWebViewConfiguration() + configuration.userContentController.addUserScript( + WKUserScript(source: scriptContent, + injectionTime: .atDocumentEnd, + forMainFrameOnly: true + ) + ) + + webView = CustomWebview(frame: .zero, configuration: configuration) + (webView as? CustomWebview)?.toolbarDelegate = self + + [TheRichTextEditor.textDidChange, TheRichTextEditor.heightDidChange, TheRichTextEditor.previewDidChange, TheRichTextEditor.documentHasLoaded].forEach { + configuration.userContentController.add(WeakScriptMessageHandler(delegate: self), name: $0) + } + + webView.keyboardDisplayRequiresUserAction = false + webView.navigationDelegate = self + webView.isOpaque = false + webView.backgroundColor = .clear + webView.scrollView.maximumZoomScale = 1 + webView.scrollView.minimumZoomScale = 1 + webView.scrollView.showsHorizontalScrollIndicator = false + webView.scrollView.showsVerticalScrollIndicator = false + webView.scrollView.bounces = false + webView.scrollView.isScrollEnabled = false + webView.scrollView.delegate = self + + addSubview(webView) + webView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + webView.leadingAnchor.constraint(equalTo: leadingAnchor), + webView.topAnchor.constraint(equalTo: topAnchor, constant: 10), + webView.trailingAnchor.constraint(equalTo: trailingAnchor), + webView.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + + webView.loadHTMLString(html, baseURL: Bundle.main.bundleURL) + } + + public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + switch message.name { + case TheRichTextEditor.textDidChange: + guard let body = message.body as? String else { return } + self.body = body + delegate?.textDidChange(content: body) + case TheRichTextEditor.heightDidChange: + guard let height = message.body as? CGFloat else { return } + if (height + 20 != self.height) { + print(self.height) + self.height = height + 20 + delegate?.heightDidChange() + } + case TheRichTextEditor.previewDidChange: + guard let preview = message.body as? String else { return } + self.preview = preview + case TheRichTextEditor.documentHasLoaded: + delegate?.editorDidLoad() + default: + break + } + } + + public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + if let textToLoad = textToLoad { + self.textToLoad = nil + html = textToLoad + } + } + + public func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { + scrollView.pinchGestureRecognizer?.isEnabled = false + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + if (scrollView.contentOffset.y != 0) { + delegate?.scrollOffset(verticalOffset: scrollView.contentOffset.y) + } + scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: false) + } + + public func viewForZooming(in: UIScrollView) -> UIView? { + return nil + } + + public override func endEditing(_ force: Bool) -> Bool { + webView.endEditing(force) + return super.endEditing(force) + } + + public func setEditorFontColor(_ color: UIColor) { + webView.evaluateJavaScript("richeditor.setBaseTextColor('#\(color.toHexString())');", completionHandler: nil) + } + + public func setEditorBackgroundColor(_ color: UIColor) { + webView.evaluateJavaScript("richeditor.setBackgroundColor('#\(color.toHexString())');", completionHandler: nil) + } + + public func focus() { + webView.evaluateJavaScript("richeditor.focus();", completionHandler: nil) + } + + public func focus(at: CGPoint) { + webView.evaluateJavaScript("richeditor.focusAtPoint(\(at.x), \(at.y));", completionHandler: nil) + } + + public func runCommand(_ command: String) { + webView.evaluateJavaScript("document.execCommand('\(command)', false, null);", completionHandler: nil) + } +} + +extension TheRichTextEditor: WebviewToolbarDelegate { + func onUndoPress() { + self.runCommand("undo") + } + + func onRedoPress() { + self.runCommand("redo") + } + + func onTextAlignCenter() { + self.runCommand("justifyCenter") + } + + func onIndentPress() { + self.runCommand("indent") + } + + func onOutdentPress() { + self.runCommand("outdent") + } + + func onClearPress() { + self.runCommand("removeFormat") + } + + func onItalicPress() { + self.runCommand("italic") + } + + func onTextAlignLeft() { + self.runCommand("justifyLeft") + } + + func onTextAlignRight() { + self.runCommand("justifyRight") + } + + func onBoldPress() { + self.runCommand("bold") + } +} + +fileprivate extension String { + + var htmlToPlainText: String { + return [ + ("(<[^>]*>)|(&\\w+;)", " "), + ("[ ]+", " ") + ].reduce(self) { + try! $0.replacing(pattern: $1.0, with: $1.1) + }.resolvedHTMLEntities + } + + var resolvedHTMLEntities: String { + return self + .replacingOccurrences(of: "'", with: "'") + .replacingOccurrences(of: "'", with: "'") + .replacingOccurrences(of: "&", with: "&") + .replacingOccurrences(of: " ", with: " ") + } + + func replacing(pattern: String, with template: String) throws -> String { + let regex = try NSRegularExpression(pattern: pattern, options: .caseInsensitive) + return regex.stringByReplacingMatches(in: self, options: [], range: NSRange(0.. Void +typealias NewClosureType = @convention(c) (Any, Selector, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void + +extension WKWebView{ + var keyboardDisplayRequiresUserAction: Bool? { + get { + return self.keyboardDisplayRequiresUserAction + } + set { + self.setKeyboardRequiresUserInteraction(newValue ?? true) + } + } + + func setKeyboardRequiresUserInteraction( _ value: Bool) { + guard let WKContentView: AnyClass = NSClassFromString("WKContentView") else { + print("keyboardDisplayRequiresUserAction extension: Cannot find the WKContentView class") + return + } + // For iOS 10, * + let sel_10: Selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:") + // For iOS 11.3, * + let sel_11_3: Selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:") + // For iOS 12.2, * + let sel_12_2: Selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:changingActivityState:userObject:") + // For iOS 13.0, * + let sel_13_0: Selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject:") + + if let method = class_getInstanceMethod(WKContentView, sel_10) { + let originalImp: IMP = method_getImplementation(method) + let original: OldClosureType = unsafeBitCast(originalImp, to: OldClosureType.self) + let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3) in + original(me, sel_10, arg0, !value, arg2, arg3) + } + let imp: IMP = imp_implementationWithBlock(block) + method_setImplementation(method, imp) + } + + if let method = class_getInstanceMethod(WKContentView, sel_11_3) { + let originalImp: IMP = method_getImplementation(method) + let original: NewClosureType = unsafeBitCast(originalImp, to: NewClosureType.self) + let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3, arg4) in + original(me, sel_11_3, arg0, !value, arg2, arg3, arg4) + } + let imp: IMP = imp_implementationWithBlock(block) + method_setImplementation(method, imp) + } + + if let method = class_getInstanceMethod(WKContentView, sel_12_2) { + let originalImp: IMP = method_getImplementation(method) + let original: NewClosureType = unsafeBitCast(originalImp, to: NewClosureType.self) + let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3, arg4) in + original(me, sel_12_2, arg0, !value, arg2, arg3, arg4) + } + let imp: IMP = imp_implementationWithBlock(block) + method_setImplementation(method, imp) + } + + if let method = class_getInstanceMethod(WKContentView, sel_13_0) { + let originalImp: IMP = method_getImplementation(method) + let original: NewClosureType = unsafeBitCast(originalImp, to: NewClosureType.self) + let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3, arg4) in + original(me, sel_13_0, arg0, !value, arg2, arg3, arg4) + } + let imp: IMP = imp_implementationWithBlock(block) + method_setImplementation(method, imp) + } + } } diff --git a/Sources/TheRichTextEditor/Utils/Extensions.swift b/Sources/TheRichTextEditor/Utils/Extensions.swift new file mode 100644 index 0000000..6072b78 --- /dev/null +++ b/Sources/TheRichTextEditor/Utils/Extensions.swift @@ -0,0 +1,21 @@ +// +// File.swift +// +// +// Created by Pedro Iniguez on 12/29/20. +// + +import Foundation +import UIKit + +extension UIColor { + func toHexString() -> String { + var r:CGFloat = 0 + var g:CGFloat = 0 + var b:CGFloat = 0 + var a:CGFloat = 0 + getRed(&r, green: &g, blue: &b, alpha: &a) + let rgb:Int = (Int)(r*255)<<16 | (Int)(g*255)<<8 | (Int)(b*255)<<0 + return String(format:"%06x", rgb) + } +} diff --git a/Sources/TheRichTextEditor/Views/AccessoryUICollectionViewCell.swift b/Sources/TheRichTextEditor/Views/AccessoryUICollectionViewCell.swift new file mode 100644 index 0000000..8552274 --- /dev/null +++ b/Sources/TheRichTextEditor/Views/AccessoryUICollectionViewCell.swift @@ -0,0 +1,20 @@ +// +// AccessoryUICollectionViewCell.swift +// iOS-Email-Client +// +// Created by Pedro Iniguez on 12/28/20. +// Copyright © 2020 Criptext Inc. All rights reserved. +// + +import Foundation +import UIKit + +class AccessoryUICollectionViewCell: UICollectionViewCell { + @IBOutlet weak var iconImageView: UIImageView! + + override func awakeFromNib() { + super.awakeFromNib() + + backgroundColor = .clear + } +} diff --git a/Sources/TheRichTextEditor/Views/AccessoryUICollectionViewCell.xib b/Sources/TheRichTextEditor/Views/AccessoryUICollectionViewCell.xib new file mode 100644 index 0000000..6890ec9 --- /dev/null +++ b/Sources/TheRichTextEditor/Views/AccessoryUICollectionViewCell.xib @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/TheRichTextEditor/Views/CustomWebview.swift b/Sources/TheRichTextEditor/Views/CustomWebview.swift new file mode 100644 index 0000000..4c1101c --- /dev/null +++ b/Sources/TheRichTextEditor/Views/CustomWebview.swift @@ -0,0 +1,169 @@ +// +// CustomWebview.swift +// iOS-Email-Client +// +// Created by Pedro Iniguez on 12/28/20. +// Copyright © 2020 Criptext Inc. All rights reserved. +// + +import Foundation +import UIKit +import WebKit + +protocol WebviewToolbarDelegate: class { + func onBoldPress() + func onItalicPress() + func onTextAlignLeft() + func onTextAlignRight() + func onTextAlignCenter() + func onIndentPress() + func onOutdentPress() + func onClearPress() + func onUndoPress() + func onRedoPress() +} + +class CustomWebview: WKWebView { + + enum Modifier { + case bold + case italic + case textAlignLeft + case textAlignCenter + case textAlignRight + case indent + case outdent + case clear + case undo + case redo + + var desc: String { + switch(self) { + case .bold: + return "Bold" + case .italic: + return "Italic" + case .textAlignLeft: + return "Left" + case .textAlignCenter: + return "Center" + case .textAlignRight: + return "Right" + case .indent: + return "Indent" + case .outdent: + return "Outdent" + case .clear: + return "Clear" + case .undo: + return "Undo" + case .redo: + return "Redo" + } + } + + var image: UIImage { + switch(self) { + case .bold: + return UIImage(named: "bold")! + case .italic: + return UIImage(named: "italic")! + case .textAlignLeft: + return UIImage(named: "alignLeft")! + case .textAlignCenter: + return UIImage(named: "alignCenter")! + case .textAlignRight: + return UIImage(named: "alignRight")! + case .indent: + return UIImage(named: "indent")! + case .outdent: + return UIImage(named: "outdent")! + case .clear: + return UIImage(named: "clear")! + case .undo: + return UIImage(named: "undo")! + case .redo: + return UIImage(named: "redo")! + } + } + } + weak var toolbarDelegate: WebviewToolbarDelegate? = nil + var enableAccessoryView = true + var accessoryView: UIView? = nil + var modifiers: [Modifier] = [.bold, .italic, .textAlignRight, .textAlignCenter, .textAlignLeft, .indent, .outdent, .clear, .undo, .redo] + + override var inputAccessoryView: UIView? { + get { + if enableAccessoryView, + accessoryView == nil { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + + let collectionView = UICollectionView(frame: CGRect(x: 0, y: 0, width: 100, height: 50), collectionViewLayout: layout) + //collectionView.backgroundColor = theme.background + collectionView.dataSource = self + collectionView.delegate = self + collectionView.isScrollEnabled = true + collectionView.bounces = false + collectionView.showsHorizontalScrollIndicator = false + + let accessoryNib = UINib(nibName: "AccessoryUICollectionViewCell", bundle: nil) + collectionView.register(accessoryNib, forCellWithReuseIdentifier: "accessoryCell") + + accessoryView = collectionView + } + return accessoryView + } + set { + accessoryView = newValue + } + } +} + +extension CustomWebview: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return modifiers.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let modifier = modifiers[indexPath.item] + + let collectionCell = collectionView.dequeueReusableCell(withReuseIdentifier: "accessoryCell", for: indexPath) as! AccessoryUICollectionViewCell + collectionCell.iconImageView.image = modifier.image + return collectionCell + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CGSize(width: 40, height: 50) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + return UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5) + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let modifier = modifiers[indexPath.item] + switch modifier { + case .bold: + self.toolbarDelegate?.onBoldPress() + case .italic: + self.toolbarDelegate?.onItalicPress() + case .textAlignLeft: + self.toolbarDelegate?.onTextAlignLeft() + case .textAlignRight: + self.toolbarDelegate?.onTextAlignRight() + case .textAlignCenter: + self.toolbarDelegate?.onTextAlignCenter() + case .indent: + self.toolbarDelegate?.onIndentPress() + case .outdent: + self.toolbarDelegate?.onOutdentPress() + case .clear: + self.toolbarDelegate?.onClearPress() + case .undo: + self.toolbarDelegate?.onUndoPress() + case .redo: + self.toolbarDelegate?.onRedoPress() + } + } +} diff --git a/Sources/TheRichTextEditor/Web Resources/main.html b/Sources/TheRichTextEditor/Web Resources/main.html new file mode 100644 index 0000000..48e4c99 --- /dev/null +++ b/Sources/TheRichTextEditor/Web Resources/main.html @@ -0,0 +1,69 @@ + + + + + + + + +
+
+ + diff --git a/Sources/TheRichTextEditor/Web Resources/main.js b/Sources/TheRichTextEditor/Web Resources/main.js new file mode 100644 index 0000000..81c9393 --- /dev/null +++ b/Sources/TheRichTextEditor/Web Resources/main.js @@ -0,0 +1,81 @@ +var richeditor = {}; +var editor = document.getElementById("editor"); + +window.onload = function() { + window.webkit.messageHandlers.documentHasLoaded.postMessage("ready"); +}; + +richeditor.updatePlaceholder = function() { + if (editor.innerHTML.indexOf('img') !== -1 || (editor.textContent.length > 0 && editor.innerHTML.length > 0)) { + editor.classList.remove("placeholder"); + } else { + editor.classList.add("placeholder"); + } +} + +richeditor.insertText = function(text) { + editor.innerHTML = text; + richeditor.updatePlaceholder(); + window.webkit.messageHandlers.heightDidChange.postMessage(document.body.offsetHeight); +} + +richeditor.setBaseTextColor = function(color) { + editor.style.color = color; +} + +richeditor.setBaseTextColor = function(color) { + editor.style.color = color; +} + +richeditor.setBackgroundColor = function(color) { + editor.style.backgroundColor = color; +} + +richeditor.focus = function() { + var range = document.createRange(); + range.selectNodeContents(editor); + range.collapse(false); + var selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + editor.focus(); +} + +richeditor.focusAtPoint = function(x, y) { + var range = document.caretRangeFromPoint(x, y) || document.createRange(); + var selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + editor.focus(); +}; + +richeditor.setPlaceholderText = function(text) { + editor.setAttribute("placeholder", text); +}; + +editor.addEventListener("input", function() { + window.webkit.messageHandlers.textDidChange.postMessage(editor.innerHTML); + window.webkit.messageHandlers.previewDidChange.postMessage(editor.innerText); + richeditor.updatePlaceholder(); +}, false) + +document.addEventListener("selectionchange", function() { + window.webkit.messageHandlers.heightDidChange.postMessage(editor.clientHeight); +}, false); + +document.getElementById("not-editor").addEventListener("click", () => { + if (editor == document.activeElement) { + editor.blur(); + } else { + editor.focus(); + document.execCommand('selectAll', false, null); + document.getSelection().collapseToEnd(); + } +}) + +document.addEventListener('paste', e => { + var items = (event.clipboardData || event.originalEvent.clipboardData).items; + if (items[0] && items[0].kind === 'file') { + e.preventDefault(); + } +}); diff --git a/Tests/TheRichTextEditorTests/TheRichTextEditorTests.swift b/Tests/TheRichTextEditorTests/TheRichTextEditorTests.swift index 7071781..cba4bb8 100644 --- a/Tests/TheRichTextEditorTests/TheRichTextEditorTests.swift +++ b/Tests/TheRichTextEditorTests/TheRichTextEditorTests.swift @@ -6,7 +6,7 @@ final class TheRichTextEditorTests: XCTestCase { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct // results. - XCTAssertEqual(TheRichTextEditor().text, "Hello, World!") + //XCTAssertEqual(TheRichTextEditor(), "Hello, World!") } static var allTests = [