引言
- 富文本编辑器的应用场景:编辑商品详情
预览:
- 设计思路:编辑器基于WKWebview实现,Editor使用WKWebview加载一个本地editor.html文件,Editor使用
evaluateJavaScript
执行JS往本地html添加标签代码,编辑器最终输出富文本字符串(html代码)传输给服务器。
"remark":"<p>商品详情看看</p>n<p style="text-align: right;">jjjj<img src="http://bug.xxx.com:7000/zentao/file-read-6605.png" alt="dddd" width="750" height="4052" /></p>n<p style="text-align: right;"> </p>n<p style="text-align: right;"> </p>n<p style="text-align: right;"> </p>n<p style="text-align: right;"> </p>n<p style="text-align: right;"> </p>n<p style="text-align: right;"> </p>n<p style="text-align: right;"><img src="http://bug.xxx.com:7000/zentao/file-read-6605.png" alt="" width="750" height="4052" /></p>"
- 使用IQKeyboardManager 键盘管理工具,布局采用Masonry,MVVM数据绑定。
I 前置知识
- 获取当前页面的html : https://blog.csdn.net/z929118967/article/details/77879309
- WKWebView替代UIWebView: https://blog.csdn.net/z929118967/article/details/115673455
- iOS加载本地HTML、pdf、doc、excel文件 & HTML字符串与富文本互转 https://blog.csdn.net/z929118967/article/details/90579369
- IQKeyboardManager 键盘管理工具(个性化设置): https://blog.csdn.net/z929118967/article/details/103766552
- iOS小技能:MVVM数据绑定的实现方式 https://blog.csdn.net/z929118967/article/details/75214212
- base64字符串与图片的互转
1.1 加载本地html
本地html
代码语言:javascript复制<!DOCTYPE html>
<html>
<head>
<title>RichTextEditor</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0,minimum-scale=1.0,user-scalable=no" />
<meta name="referrer" content="no-referrer">
<!-- jQuery Source For RichTextEditor -->
<script>
<!-- jQuery -->
</script>
<script>
<!-- jsbeautifier -->
</script>
<script>
<!--editor-->
</script>
使用[_webView loadHTMLString:html baseURL:baseURL];
进行代码加载
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"editor" ofType:@"html"];
NSData *htmlData = [NSData dataWithContentsOfFile:filePath];
NSString *htmlString = [[NSString alloc] initWithData:htmlData encoding:NSUTF8StringEncoding];
NSString *basePath = [[NSBundle mainBundle] bundlePath];
NSURL *baseURL = [NSURL fileURLWithPath:basePath];
[self.editorView loadHTMLString:htmlString baseURL:baseURL];
iOS加载本地HTML、pdf、doc、excel文件 & HTML字符串与富文本互转 https://blog.csdn.net/z929118967/article/details/90579369
往html追加字符串
代码语言:javascript复制 NSString *source = [[NSBundle mainBundle] pathForResource:@"RichTextEditor" ofType:@"js"];
NSString *jsString = [[NSString alloc] initWithData:[NSData dataWithContentsOfFile:source] encoding:NSUTF8StringEncoding];
htmlString = [htmlString stringByReplacingOccurrencesOfString:@"<!--editor-->" withString:jsString];
1.2 OC执行JS
文字的加粗、下划线、斜体等样式通过- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;
执行js。
加粗
代码语言:javascript复制@implementation WKWebView (JSTool)
#pragma mark - 加粗
- (void)setBold {
NSString *trigger = @"_editor.setBold();";
[self evaluateJavaScript:trigger completionHandler:nil];
}
代码语言:javascript复制 <div id="_editor_content" class="_editor_content" contenteditable="true" placeholder="请输入文章正文"></div>
js
代码语言:javascript复制var _editor = {};
_editor.setBold = function() {
document.execCommand('bold', false, null);
_editor.enabledEditingItems();
}
_editor.getText = function() {
return $('#_editor_content').text();
}
_editor.getHTML = function() {
// Get the contents
var h = document.getElementById("_editor_content").innerHTML;
return h;
}
_editor.enabledEditingItems = function(e) {
var items = [];
var fontSizeblock = document.queryCommandValue('fontSize');
if (fontSizeblock.length > 0) {
items.push(fontSizeblock);
}
if (_editor.isCommandEnabled('bold')) {
items.push('bold');
}
if (_editor.isCommandEnabled('italic')) {
items.push('italic');
}
if (_editor.isCommandEnabled('subscript')) {
items.push('subscript');
}
if (_editor.isCommandEnabled('superscript')) {
items.push('superscript');
}
if (_editor.isCommandEnabled('strikeThrough')) {
items.push('strikeThrough');
}
if (_editor.isCommandEnabled('underline')) {
items.push('underline');
}
if (_editor.isCommandEnabled('insertOrderedList')) {
items.push('orderedList');
}
if (_editor.isCommandEnabled('insertUnorderedList')) {
items.push('unorderedList');
}
if (_editor.isCommandEnabled('justifyCenter')) {
items.push('justifyCenter');
}
if (_editor.isCommandEnabled('justifyFull')) {
items.push('justifyFull');
}
if (_editor.isCommandEnabled('justifyLeft')) {
items.push('justifyLeft');
}
if (_editor.isCommandEnabled('justifyRight')) {
items.push('justifyRight');
}
if (_editor.isCommandEnabled('insertHorizontalRule')) {
items.push('horizontalRule');
}
var formatBlock = document.queryCommandValue('formatBlock');
if (formatBlock.length > 0) {
items.push(formatBlock);
}
// Images
// $('img').bind('touchstart', function(e) {
// $('img').removeClass('zs_active');
// $(this).addClass('zs_active');
// });
// Use jQuery to figure out those that are not supported
if (typeof(e) != "undefined") {
// The target element
var s = _editor.getSelectedNode();
var t = $(s);
var nodeName = e.target.nodeName.toLowerCase();
// Background Color
var bgColor = t.css('backgroundColor');
if (bgColor.length != 0 && bgColor != 'rgba(0, 0, 0, 0)' && bgColor != 'rgb(0, 0, 0)' && bgColor != 'transparent') {
items.push('backgroundColor');
}
// Text Color
var textColor = t.css('color');
if (textColor.length != 0 && textColor != 'rgba(0, 0, 0, 0)' && textColor != 'rgb(0, 0, 0)' && textColor != 'transparent') {
items.push('textColor');
}
//Fonts
var font = t.css('font-family');
if (font.length != 0 && font != 'Arial, Helvetica, sans-serif') {
items.push('fonts');
}
// Link
if (nodeName == 'a') {
_editor.currentEditingLink = t;
var title = t.attr('title');
items.push('link:' t.attr('href'));
if (t.attr('title') !== undefined) {
items.push('link-title:' t.attr('title'));
}
} else {
_editor.currentEditingLink = null;
}
// Blockquote
if (nodeName == 'blockquote') {
items.push('indent');
}
// Image
if (nodeName == 'img') {
_editor.currentEditingImage = t;
items.push('image:' t.attr('src'));
if (t.attr('alt') !== undefined) {
items.push('image-alt:' t.attr('alt'));
}
} else {
_editor.currentEditingImage = null;
}
}
var arttitle = document.getElementById('vj_article_title');
var artAbsTitle = document.getElementById('vj_article_abstract');
var artContent = document.getElementById('_editor_content');
if (arttitle == document.activeElement) {
window.location = "state-title://" items.join(',');
}
if (artAbsTitle == document.activeElement) {
window.location = "state-abstract-title://" items.join(',');
}
if (artContent == document.activeElement) {
window.location = "callback://0/" items.join(',');
}
}
1.3 JS调用iOS
JS侧代码:
代码语言:javascript复制window.webkit.messageHandlers.openImage.postMessage($(this).attr("src"));
// 给openImage 传递SRC参数
// 监听点击事件调研OC方法
<script>
var div = document.getElementById('_column');
div.addEventListener('click', test);
function test(e) {
window.webkit.messageHandlers.column.postMessage({
"body": "buttonActionMessage"
});
}
</script>
OC侧代码使用configuration对象初始化webView,并遵守WKScriptMessageHandler
协议监听JS的调用
NSString * const k_openImage4js = @"openImage";
//使用configuration对象初始化webView
- (WKWebView *)webView {
if (_webView) return _webView;
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
[_webView.configuration.userContentController addScriptMessageHandler:self name:k_openImage4js];
//! 使用configuration对象初始化webView
_webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
return _webView;
}
#pragma mark - ******** 处理与JS的桥接
/**
接收参数
*/
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
if ([message.name caseInsensitiveCompare:k_openImage4js] == NSOrderedSame) {
NSLog(@"message.name:%@,message.body:%@",message.name,message.body);
[self ImageZoomScaleWithUrl:message.body];
}
}
II iOS侧代码
2.1 web页面获取焦点时弹出键盘
- UIWebView 中 keyboardDisplayRequiresUserAction 设置为 NO
A Boolean value indicating whether web content can programmatically display the keyboard.
- WKWebView中需要针对不同操作系统进行相关方法的重写。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self focusTextEditor];
});
- (void)focusTextEditor {
//TODO: Is this behavior correct? Is it the right replacement?
// self.editorView.keyboardDisplayRequiresUserAction = NO;
[ZSSRichTextEditor allowDisplayingKeyboardWithoutUserAction];
NSString *js = [NSString stringWithFormat:@"zss_editor.focusEditor();"];
[self.editorView evaluateJavaScript:js completionHandler:^(NSString *result, NSError *error) {
}];
}
#pragma mark - Convenience replacement for keyboardDisplayRequiresUserAction in WKWebview
(void)allowDisplayingKeyboardWithoutUserAction {
Class class = NSClassFromString(@"WKContentView");
NSOperatingSystemVersion iOS_11_3_0 = (NSOperatingSystemVersion){11, 3, 0};
NSOperatingSystemVersion iOS_12_2_0 = (NSOperatingSystemVersion){12, 2, 0};
NSOperatingSystemVersion iOS_13_0_0 = (NSOperatingSystemVersion){13, 0, 0};
if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_13_0_0]) {
SEL selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject:");
Method method = class_getInstanceMethod(class, selector);
IMP original = method_getImplementation(method);
IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, BOOL arg3, id arg4) {
((void (*)(id, SEL, void*, BOOL, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3, arg4);
});
method_setImplementation(method, override);
}
else if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_12_2_0]) {
SEL selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:changingActivityState:userObject:");
Method method = class_getInstanceMethod(class, selector);
IMP original = method_getImplementation(method);
IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, BOOL arg3, id arg4) {
((void (*)(id, SEL, void*, BOOL, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3, arg4);
});
method_setImplementation(method, override);
}
else if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_11_3_0]) {
SEL selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:");
Method method = class_getInstanceMethod(class, selector);
IMP original = method_getImplementation(method);
IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, BOOL arg3, id arg4) {
((void (*)(id, SEL, void*, BOOL, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3, arg4);
});
method_setImplementation(method, override);
} else {
SEL selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:");
Method method = class_getInstanceMethod(class, selector);
IMP original = method_getImplementation(method);
IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, id arg3) {
((void (*)(id, SEL, void*, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3);
});
method_setImplementation(method, override);
}
}
2.2 去掉键盘自带的工具条
原生中隐藏AccessoryView
代码语言:javascript复制self.textView.inputView = nil
将UIWebBrowserViewMinusAccessoryView的inputAccessoryView替换为空
代码语言:javascript复制UIWebBrowserViewMinusAccessoryView->WKScrollView->WKWebView 去掉WKWebView键盘自带的工具条:修改browserView的inputAccessoryView属性getter方法返回nil
@interface WKWebView (HackishAccessoryHiding)
@property (nonatomic, assign) BOOL hidesInputAccessoryView;
@end
@implementation WKWebView (HackishAccessoryHiding)
static const char * const hackishFixClassName = "WKWebBrowserViewMinusAccessoryView";
static Class hackishFixClass = Nil;
- (void) setHidesInputAccessoryView:(BOOL)value {
UIView *browserView = [self hackishlyFoundBrowserView];//查找browserView
if (browserView == nil) {
return;
}
// 将inputAccessoryView的实现替换为nil
[self ensureHackishSubclassExistsOfBrowserViewClass:[browserView class]];
if (value) {
object_setClass(browserView, hackishFixClass);
}
else {
Class normalClass = objc_getClass("WKWebBrowserView");
object_setClass(browserView, normalClass);
}
[browserView reloadInputViews];
}
- (void)ensureHackishSubclassExistsOfBrowserViewClass:(Class)browserViewClass {
if (!hackishFixClass) {
Class newClass = objc_allocateClassPair(browserViewClass, hackishFixClassName, 0);
newClass = objc_allocateClassPair(browserViewClass, hackishFixClassName, 0);
IMP nilImp = [self methodForSelector:@selector(methodReturningNil)];
class_addMethod(newClass, @selector(inputAccessoryView), nilImp, "@@:");
objc_registerClassPair(newClass);
hackishFixClass = newClass;
}
}
- (id)methodReturningNil {
return nil;
}
2.3 判断键盘的弹出与关闭状态
代码语言:javascript复制-(void)addNotification{
// [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(keyBoardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil];
//isVisable
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidShow)name:UIKeyboardDidShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidHide)name:UIKeyboardWillHideNotification object:nil];
}
- (void)dealloc{
[[NSNotificationCenter defaultCenter]removeObserver:self];;
}
-(void)keyboardDidHide{
self.isVisable = NO;
}
-(void)keyboardDidShow{
self.isVisable = YES;
}
2.4 处理自定义键盘工具条的显示与隐藏
代码语言:javascript复制//处理键盘工具条显示与隐藏
- (void)handleEvent:(NSString *)urlString{
if ([urlString hasPrefix:@"state-title://"] || [urlString hasPrefix:@"state-abstract-title://"]) {
self.fontBar.hidden = YES;
self.toolBarView.hidden = YES;
}else if([urlString rangeOfString:@"callback://0/"].location != NSNotFound){
self.fontBar.hidden = NO;
self.toolBarView.hidden = NO;
//更新 toolbar
NSString *className = [urlString stringByReplacingOccurrencesOfString:@"callback://0/" withString:@""];
[self.fontBar updateFontBarWithButtonName:className];
}
}
2.5 监听alertController的textField的内容
监听alertController的textField的内容,只有文本长度大于0,才可以点击完成按钮
代码语言:javascript复制 UIAlertAction *doneAction = [UIAlertAction actionWithTitle:@"完成" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
UITextField *linkURL = [alertController.textFields objectAtIndex:0];
UITextField *title = [alertController.textFields objectAtIndex:1];
if (!self.viewModel.model4editor.isVisable) {
[self.viewModel.model4editor.editorView focusTextEditor];
}
[self.viewModel.model4editor.editorView prepareInsertImage];
[self.viewModel.model4editor.editorView insertImage:linkURL.text alt:title.text];
}];
doneAction.enabled = NO;
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = @"URL (必填)";
textField.rightViewMode = UITextFieldViewModeAlways;
textField.clearButtonMode = UITextFieldViewModeAlways;
// 监听textField的内容,只有文本长度大于0,才可以点击完成按钮
// [textField addTarget:self action:@selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged];
// textField.delegate = weakSelf;
[[textField rac_signalForControlEvents:(UIControlEventEditingChanged)] subscribeNext:^(__kindof UITextField * _Nullable x) {
if (x.text.length < 1) {//判断是否符合URL
doneAction.enabled = NO;
}else{
doneAction.enabled = YES;
}
}];
}];
III JS侧代码
基于ZSSRichTextEditor实现
3.1 获得焦点
代码语言:javascript复制zss_editor.focusEditor = function() {
var editor = $('#zss_editor_content');
var range = document.createRange();
range.selectNodeContents(editor.get(0));
range.collapse(false);
var selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
editor.focus();
}
3.2 监听网页上选定文本的变化
代码语言:javascript复制 $(document).on('selectionchange',function(e){
zss_editor.calculateEditorHeightWithCaretPosition();
zss_editor.setScrollPosition();
zss_editor.enabledEditingItems(e);
});
//输入文字时,插入符号位置计算
zss_editor.calculateEditorHeightWithCaretPosition = function() {
var padding = 50;
var c = zss_editor.getCaretYPosition();
var editor = $('#zss_editor_content');
var offsetY = window.document.body.scrollTop;
var height = zss_editor.contentHeight;
var newPos = window.pageYOffset;
if (c < offsetY) {
newPos = c;
} else if (c > (offsetY height - padding)) {
newPos = c - height padding - 18;
}
window.scrollTo(0, newPos);
}
IV demo
demo下载
see also
富文本编辑器:基于WKWebview实现,Editor使用WKWebview加载一个本地editor.html文件https://download.csdn.net/download/u011018979/85675638
代码语言:javascript复制editorView4WKWebView :https://github.com/nnhubbard/ZSSRichTextEditor
s.source = { :git => "https://github.com/nnhubbard/ZSSRichTextEditor.git", :tag => "0.5.2.1" }
s.source_files = "**/*.{h,m}"
s.exclude_files = "**/ZSSDemo*.{h,m}", "**/ZSSAppDelegate*.{h,m}", "**/main.m"
s.resources = "**/ZSS*.png", "**/ZSSRichTextEditor.js", "**/editor.html", "**/jQuery.js", "**/JSBeautifier.js"
s.frameworks = "CoreGraphics", "CoreText"
原生 iOS-Rich-Text-Editor