Last active
March 9, 2024 07:47
-
-
Save fabfelici/837037f7737dc24765fe3035b15255ce to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta name="viewport" content="initial-scale=1.0" /> | |
</head> | |
<body> | |
<div id="editor" contenteditable="true"></div> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} |
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)?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 subclassingWKWebView
you would expose everythingWKWebView
exposes.