前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >iOS小技能:富文本编辑器

iOS小技能:富文本编辑器

作者头像
公众号iOS逆向
发布2022-08-22 11:23:21
2.3K0
发布2022-08-22 11:23:21
举报
文章被收录于专栏:iOS逆向与安全

引言

  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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-06-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 iOS逆向 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • I 前置知识
    • 1.1 加载本地html
      • 1.2 OC执行JS
        • 1.3 JS调用iOS
        • II iOS侧代码
          • 2.1 web页面获取焦点时弹出键盘
            • 2.2 去掉键盘自带的工具条
              • 2.3 判断键盘的弹出与关闭状态
                • 2.4 处理自定义键盘工具条的显示与隐藏
                  • 2.5 监听alertController的textField的内容
                  • III JS侧代码
                    • 3.1 获得焦点
                      • 3.2 监听网页上选定文本的变化
                      • IV demo
                      • see also
                      相关产品与服务
                      云服务器
                      云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档