-
-
Save fabfelici/837037f7737dc24765fe3035b15255ce to your computer and use it in GitHub Desktop.
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta name="viewport" content="initial-scale=1.0" /> | |
</head> | |
<body> | |
<div id="editor" contenteditable="true"></div> | |
</body> | |
</html> |
var richeditor = {}; | |
var editor = document.getElementById("editor"); | |
richeditor.insertText = function(text) { | |
editor.innerHTML = text; | |
window.webkit.messageHandlers.heightDidChange.postMessage(document.body.offsetHeight); | |
} | |
editor.addEventListener("input", function() { | |
window.webkit.messageHandlers.textDidChange.postMessage(editor.innerHTML); | |
}, false) | |
document.addEventListener("selectionchange", function() { | |
window.webkit.messageHandlers.heightDidChange.postMessage(document.body.offsetHeight); | |
}, false); |
public protocol RichTextEditorDelegate: class { | |
func textDidChange(text: String) | |
func heightDidChange() | |
} | |
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 RichTextEditor: UIView, WKScriptMessageHandler, WKNavigationDelegate, UIScrollViewDelegate { | |
private static let textDidChange = "textDidChange" | |
private static let heightDidChange = "heightDidChange" | |
private static let defaultHeight: CGFloat = 60 | |
public weak var delegate: RichTextEditorDelegate? | |
public var height: CGFloat = RichTextEditor.defaultHeight | |
public var placeholder: String? { | |
didSet { | |
placeholderLabel.text = placeholder | |
} | |
} | |
private var textToLoad: String? | |
public var text: String? { | |
didSet { | |
guard let text = text else { return } | |
if editorView.isLoading { | |
textToLoad = text | |
} else { | |
editorView.evaluateJavaScript("richeditor.insertText(\"\(text.htmlEscapeQuotes)\");", completionHandler: nil) | |
placeholderLabel.isHidden = !text.htmlToPlainText.isEmpty | |
} | |
} | |
} | |
private var editorView: WKWebView! | |
private let placeholderLabel = UILabel() | |
public override init(frame: CGRect = .zero) { | |
placeholderLabel.textColor = UIColor.lightGray.withAlphaComponent(0.65) | |
guard let bundlePath = Bundle(for: type(of: self)).path(forResource: "Resources", ofType: "bundle"), | |
let bundle = Bundle(path: bundlePath), | |
let scriptPath = bundle.path(forResource: "RichTextEditor", ofType: "js"), | |
let scriptContent = try? String(contentsOfFile: scriptPath, encoding: String.Encoding.utf8), | |
let htmlPath = bundle.path(forResource: "RichTextEditor", 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 | |
) | |
) | |
editorView = WKWebView(frame: .zero, configuration: configuration) | |
super.init(frame: frame) | |
[RichTextEditor.textDidChange, RichTextEditor.heightDidChange].forEach { | |
configuration.userContentController.add(WeakScriptMessageHandler(delegate: self), name: $0) | |
} | |
editorView.navigationDelegate = self | |
editorView.isOpaque = false | |
editorView.backgroundColor = .clear | |
editorView.scrollView.isScrollEnabled = false | |
editorView.scrollView.showsHorizontalScrollIndicator = false | |
editorView.scrollView.showsVerticalScrollIndicator = false | |
editorView.scrollView.bounces = false | |
editorView.scrollView.isScrollEnabled = false | |
editorView.scrollView.delegate = self | |
addSubview(placeholderLabel) | |
placeholderLabel.translatesAutoresizingMaskIntoConstraints = false | |
NSLayoutConstraint.activate([ | |
placeholderLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), | |
placeholderLabel.topAnchor.constraint(equalTo: topAnchor), | |
placeholderLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), | |
placeholderLabel.bottomAnchor.constraint(equalTo: bottomAnchor) | |
]) | |
addSubview(editorView) | |
editorView.translatesAutoresizingMaskIntoConstraints = false | |
NSLayoutConstraint.activate([ | |
editorView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8), | |
editorView.topAnchor.constraint(equalTo: topAnchor, constant: 10), | |
editorView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8), | |
editorView.bottomAnchor.constraint(equalTo: bottomAnchor) | |
]) | |
editorView.loadHTMLString(html, baseURL: nil) | |
} | |
public required init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
} | |
public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { | |
switch message.name { | |
case RichTextEditor.textDidChange: | |
guard let body = message.body as? String else { return } | |
placeholderLabel.isHidden = !body.htmlToPlainText.isEmpty | |
delegate?.textDidChange(text: body) | |
case RichTextEditor.heightDidChange: | |
guard let height = message.body as? CGFloat else { return } | |
self.height = height > RichTextEditor.defaultHeight ? height + 30 : RichTextEditor.defaultHeight | |
delegate?.heightDidChange() | |
default: | |
break | |
} | |
} | |
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { | |
if let textToLoad = textToLoad { | |
self.textToLoad = nil | |
text = textToLoad | |
} | |
} | |
public func viewForZooming(in: UIScrollView) -> UIView? { | |
return nil | |
} | |
} | |
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..<self.utf16.count), withTemplate: template) | |
} | |
var htmlEscapeQuotes: String { | |
return [ | |
("\"", "\\\""), | |
("“", """), | |
("\r", "\\r"), | |
("\n", "\\n") | |
].reduce(self) { | |
return $0.replacingOccurrences(of: $1.0, with: $1.1) | |
} | |
} | |
} |
Im curious, in this implementation, why did you subclass
UIView
and have aneditorView
property of typeWKWebView
? Why not just subclassWKWebView
?
Mostly because whoever is using this class does not need to know there's a webview under the hood and I don't want you to change properties/call functions on the webview that could break the functionality.
As you can see the editorView
property is private, so is not accessible from outside this class. By subclassing WKWebView
you would expose everything WKWebView
exposes.
Thanks u
Is there any way to reproduce it on SwiftUI?
Is there any way to reproduce it on SwiftUI?
I didn't test it but UIViewRepresentable
could be your friend here.
@sonnguyen9800 Yes, I have used it in my SwiftUI project:
struct RichTextEditorView: UIViewRepresentable {
@Binding var htmlText: String
@Binding var dynamicHeight: CGFloat
class Coordinator: NSObject, RichTextEditorDelegate {
var parent: RichTextEditorView
init(_ parent: RichTextEditorView) {
self.parent = parent
}
func textDidChange(text: String) {
parent.htmlText = text
}
func heightDidChange(newHeight: CGFloat) {
parent.dynamicHeight = newHeight
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIView(context: Context) -> RichEditorWebView {
let editor = RichEditorWebView()
editor.delegate = context.coordinator
editor.text = htmlText
return editor
}
func updateUIView(_ editor: RichEditorWebView, context: Context) {}
}
Great gist! Any suggestions for a JavaScript library that can apply formatting to the selected text (like toggling bold, etc)?
Im curious, in this implementation, why did you subclass
UIView
and have aneditorView
property of typeWKWebView
? Why not just subclassWKWebView
?