singerdmx / flutter-quill

Rich text editor for Flutter
https://pub.dev/packages/flutter_quill
MIT License
2.6k stars 840 forks source link

Not applied format-style to inputting Korean on iOS mobile #2208

Open hjkim-mango opened 2 months ago

hjkim-mango commented 2 months ago

Is there an existing issue for this?

Flutter Quill version

10.5.13

Steps to reproduce

Case 1

  1. Input Korean by default format-style in the editor
  2. Set numbered-list on the menu and then input more Korean.
  3. Set bulleted-list on the menu and then input more Korean.

Case 2

  1. Set bold and underline for some characters
  2. Unselect them
  3. Unset bold and underline
  4. Input more Korean.

Expected results

Setting format-styles should be applied to inputted Korean.

Actual results

Setting format-styles are not applied to inputted Korean but preview format-styles are applied.

Code sample

Code sample ```dart import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart' as quill; class FlutterQuillWidget extends StatefulWidget { const FlutterQuillWidget({super.key}); @override State createState() => _FlutterQuillWidgetState(); } class _FlutterQuillWidgetState extends State { late quill.QuillController _controller; @override void initState() { super.initState(); _controller = quill.QuillController( document: quill.Document(), selection: const TextSelection.collapsed(offset: 0)); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, appBar: AppBar( title: const Text('Flutter Quill'), ), body: Column(children: [ quill.QuillToolbar.simple(controller: _controller), quill.QuillEditor( controller: _controller, scrollController: ScrollController(), focusNode: FocusNode(), configurations: const quill.QuillEditorConfigurations( scrollable: true, autoFocus: false, expands: false, placeholder: "Add your data here..."), ), ])); } } ```

Additional Context

Screenshots / Video demonstration https://github.com/user-attachments/assets/4eb191a4-f844-46c0-a525-dd3c2e49ee9f
- Only iOS. No issue on Android.
- I think this issue is similar to #2179
SellReports commented 2 months ago

안녕하세요 제가 도움이 될지는 모르겠지만, 방법을 어느 정도 찾을거 같아서요, 저 같은 경우는 custom으로 toolbar를 만들어서 사용하고 있는데요. 볼드체를 넣으실때 그냥 Delta를 추가하는 방법도 고려 해보면 좋을 거 같아요. 볼드 버튼에 `setState(() { boldSelected = !boldSelected; // 볼드 상태 토글 final selection = _CommunityContentController.selection; final baseOffset = selection.baseOffset; final extentOffset = selection.extentOffset;

                                if (baseOffset != extentOffset) {
                                  bool isCurrentlyBold = _CommunityContentController
                                      .getSelectionStyle()
                                      .attributes[quill.Attribute.bold.key] != null;

                                  // 상태에 따라 볼드 상태 변경
                                  boldSelected = !isCurrentlyBold;
                                  if (boldSelected) {
                                    _CommunityContentController.formatSelection(quill.Attribute.bold);
                                  } else {
                                    _CommunityContentController.formatSelection(
                                        quill.Attribute.clone(quill.Attribute.bold, null));
                                  }
                                } else {
                                  final delta = quillDelta.Delta()
                                    ..retain(baseOffset)
                                    ..insert('\u200b', boldSelected ? {quill.Attribute.bold.key: true} : {});

                                  _CommunityContentController.compose(delta, selection,quill.ChangeSource.local);

                                  _CommunityContentController.updateSelection(
                                      TextSelection.collapsed(offset: baseOffset + 1),
                                      quill.ChangeSource.local
                                  );
                                }
                              });` 

이렇게 해서 만약에 텍스트가 선택이 되었을 경우에는 bold체 해제 밎 적용이 가능하고 만약에 선택이 안된 상황이라면 delta를 임의적으로 추가하는 방법도 있습니다. 이렇게 해서 끝나면 좋을거 같은데 이렇게 하면 위에 같은 문제가 다시 발생을 하더라구요. 그래서 위에 코드에 이 코드

` void _handleTextChange() { // 현재 선택된 텍스트의 볼드 상태 확인 bool isCurrentlyBold = _CommunityContentController .getSelectionStyle() .attributes[quill.Attribute.bold.key] != null;

// 볼드 상태가 선택된 경우
if (boldSelected) {
  if (!isCurrentlyBold) {
    _CommunityContentController.formatSelection(quill.Attribute.bold);
  }
} else {
  // 볼드 상태가 해제된 경우
  if (isCurrentlyBold) {
    _CommunityContentController.formatSelection(
        quill.Attribute.clone(quill.Attribute.bold, null));
  }
}

}`

이런식으로 해서 initState 에서 addlistener를 넣어서 계속 확인 하는 방법도 있습니다. 만약에 볼드체 = true 인데 해제가 되면 다시 볼드체를 넣어주는 방식이죠

SellReports commented 2 months ago

완성본 (리스트 부분은 문제가 아직 많아서 적용을 안하려구요)

컨트롤러 addListener에

` void _handleTextChange(quill.Attribute attribute, String optionBoolKey) { bool selectedOptionBool = _CommunityContentController .getSelectionStyle() .attributes[attribute.key] != null;

if (toolbarOptionsBool[optionBoolKey] ?? false) {
  if (!selectedOptionBool) {
    _CommunityContentController.formatSelection(attribute);
  }
} else {
  if (selectedOptionBool) {
    _CommunityContentController.formatSelection(
        quill.Attribute.clone(attribute, null));
  }
}

}` 이 코드

커스텀 버튼에

` void toggleTextOptions(quill.Attribute attribute, String optionBoolKey) { setState(() { toolbarOptionsBool[optionBoolKey] = !toolbarOptionsBool[optionBoolKey]!; final selection = _CommunityContentController.selection; final baseOffset = selection.baseOffset; final extentOffset = selection.extentOffset;

  if (baseOffset != extentOffset) {
    bool selectedOptionBool = _CommunityContentController
        .getSelectionStyle()
        .attributes[attribute.key] != null;

    toolbarOptionsBool[optionBoolKey] = !selectedOptionBool;
    if (toolbarOptionsBool[optionBoolKey] ?? false) {
      _CommunityContentController.formatSelection(attribute);
    } else {
      _CommunityContentController.formatSelection(
          quill.Attribute.clone(attribute, null));
    }
  } else {
    final delta = quillDelta.Delta()
      ..retain(baseOffset)
      ..insert('\u200b', toolbarOptionsBool[optionBoolKey] ?? false ? {attribute.key: true} : {});

    _CommunityContentController.compose(delta, selection,quill.ChangeSource.local);

    _CommunityContentController.updateSelection(
        TextSelection.collapsed(offset: baseOffset + 1),
        quill.ChangeSource.local
    );
  }
});

}` 이거 추가 하시면 될거 같아요

hjkim-mango commented 2 months ago

답변 감사합니다. 커스텀 버튼에 추가하는 내용은 이해했는데 컨트롤러에 addListener 적용하는 부분이 이해되지 않습니다. 말씀하신 컨트롤러가 QuillController 을 말씀하시는 것 맞죠? QuillController 에 addListener 를 적용하면 attribute 와 optionBoolKey 값을 어떻게 받아올 수 있는지 모르겠습니다. 그리고 위 방식으로는 옵션 중첩사용(ex. bold & Italic)이 불가하네요.

궁극적인 원인은 한글 입력시 조합이 이루어질 때 이전 입력을 제거한 후에 조합된 글자를 입력하던데 그 시점에 툴바로 적용한 옵션 값도 같이 제거되는 것이 문제로 추정됩니다.

SellReports commented 1 month ago

답변이 늦어서 죄송합니다. 저도 위에 방법으로 고쳐 보려고 노력을 많이 했는데요. IME 문제는 쉽게 고쳐지질 않네요. 혹시 다른 방법을 찾고 계시면, toast ui를 html 파일로 만들어서 webview로 띄우는 거도 고려를 해보시는 게 좋을 거 같아요. 저는 지금 flutter webview에 NHN에서 서비스 중인 toast ui를 html 파일로 만들어서 보여주고 있습니다. 스타일 적용도 한국 개발사에서 만든 거라서 문제가 적더라고요. 툴바를 컨트롤할 수 있는 js function을 runJavaScript로 만들어서 flutter 커스텀 툴바에서 적용할 수도 있어요. 아마 이게 더 좋은 차선책 같아요. 만약에 html로 output이 필요한 상황이시면 더더욱 좋은 방법이고요. 제가 보니까 네이버 카페나 다른 애플리케이션에서도 에디터는 웹 뷰로 보여주는 거 같아요.

만약에 toast ui 적용 예정이시면 로컬 서버를 핸드폰 내부에서 직접 실행해야 합니다 (다른 에디터는 잘 불러오는데 toast ui 는 CDN 불러올때 문제가 있더라구요). 이거는 shelf 라이브러리 사용하셔서 할 수 있어요

` Future _startServer() async { // HTTP 서버 시작 server = await shelf_io.serve(_handler, InternetAddress.anyIPv4, 8080); baseUrl = Platform.isAndroid ? 'http://10.0.2.2:8080' : 'http://localhost:8080';

// Initialize the WebViewController once the server is started
webViewController = WebViewController()
  ..setJavaScriptMode(JavaScriptMode.unrestricted) // Allow JavaScript
  ..loadRequest(Uri.parse(baseUrl!))` 

그리고 옵션을 이렇게 호출 하면 되요.

// Text Option call JS void applyBoldJS() { webViewController.runJavaScript("applyBold()"); // JavaScript 함수 호출 } void applyItalicJS() { webViewController.runJavaScript("applyItalic()"); // JavaScript 함수 호출 } void applyStrikeJS() { webViewController.runJavaScript("applyStrike()"); // JavaScript 함수 호출 }

SellReports commented 1 month ago

아 그리고 web뷰 사용하시면, ios에서 키보드 위에 툴바? 같은게 나올텐데요. 이거 없애고 싶으시면 AppDelegate에

func removeInputAccessoryView() { let className = "WKContentView" if let wkContentViewClass = NSClassFromString(className) { let originalMethod = class_getInstanceMethod(wkContentViewClass, #selector(getter: UIResponder.inputAccessoryView)) let swizzledMethod = class_getInstanceMethod(AppDelegate.self, #selector(AppDelegate.noInputAccessoryView)) if let originalMethod = originalMethod, let swizzledMethod = swizzledMethod { method_exchangeImplementations(originalMethod, swizzledMethod) } } }

이 코드 추가하시면 반 강제적으로 앱 상에서 키보드 툴바를 없앨수 있습니다