iOS小技能:富文本编辑器

2022-08-22 11:23:21 浏览数 (1)

引言

  1. 富文本编辑器的应用场景:编辑商品详情

预览:

  1. 设计思路:编辑器基于WKWebview实现,Editor使用WKWebview加载一个本地editor.html文件,Editor使用evaluateJavaScript执行JS往本地html添加标签代码,编辑器最终输出富文本字符串(html代码)传输给服务器。
代码语言:javascript复制
"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;">&nbsp;</p>n<p style="text-align: right;">&nbsp;</p>n<p style="text-align: right;">&nbsp;</p>n<p style="text-align: right;">&nbsp;</p>n<p style="text-align: right;">&nbsp;</p>n<p style="text-align: right;">&nbsp;</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>"

  1. 使用IQKeyboardManager 键盘管理工具,布局采用Masonry,MVVM数据绑定。

I 前置知识

  1. 获取当前页面的html : https://blog.csdn.net/z929118967/article/details/77879309
  2. WKWebView替代UIWebView: https://blog.csdn.net/z929118967/article/details/115673455
  3. iOS加载本地HTML、pdf、doc、excel文件 & HTML字符串与富文本互转 https://blog.csdn.net/z929118967/article/details/90579369
  4. IQKeyboardManager 键盘管理工具(个性化设置): https://blog.csdn.net/z929118967/article/details/103766552
  5. iOS小技能:MVVM数据绑定的实现方式 https://blog.csdn.net/z929118967/article/details/75214212
  6. 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]; 进行代码加载

代码语言:javascript复制
    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的调用

代码语言:javascript复制
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页面获取焦点时弹出键盘

  1. UIWebView 中 keyboardDisplayRequiresUserAction 设置为 NO

A Boolean value indicating whether web content can programmatically display the keyboard.

  1. WKWebView中需要针对不同操作系统进行相关方法的重写。
代码语言:javascript复制
        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替换为空

UIWebBrowserViewMinusAccessoryView->WKScrollView->WKWebView 去掉WKWebView键盘自带的工具条:修改browserView的inputAccessoryView属性getter方法返回nil

代码语言:javascript复制
@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

editorView4WKWebView :https://github.com/nnhubbard/ZSSRichTextEditor

代码语言:javascript复制
  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

0 人点赞