Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to insert a custom embed in autoformat/heuristics #369

Closed
cotw-fabier opened this issue Jun 22, 2024 · 5 comments
Closed

How to insert a custom embed in autoformat/heuristics #369

cotw-fabier opened this issue Jun 22, 2024 · 5 comments
Labels
question Further information is requested

Comments

@cotw-fabier
Copy link

cotw-fabier commented Jun 22, 2024

Hey Guys,

Please forgive my possibly dumb question but I've been struggling trying to figure out how to do this for a while now. I have a custom embed I've built for displaying Youtube videos inline which feels pretty slick. I created a button to insert a dummy video. Code looks like this to insert:

controller.replaceText(
                    selection.baseOffset,
                    selection.extentOffset - selection.baseOffset,
                    EmbeddableObject('youtube', inline: false, data: {
                      "url": 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
                      "subtitles": 'English',
                      "language": 'en',
                      "thumbUrl": 'https://img.youtube.com/vi/dQw4w9WgXcQ/0.jpg'
                    }),
                    selection: TextSelection.collapsed(
                        offset: selection.baseOffset + 1),
                  ); 

Then, I had an idea. What if I could make a shortcut to insert a Youtube Video by adding a bang "!" and the youtube link and it would convert it into the proper embed.

At first I did a bunch of stuff with Heuristics trying to get this to work, but then after searching discussions I learned you had introduced Autoformats to accomplish this very use-case.

But I am running into two problems.

1.) How do I insert an embed when I only can access the Document? I am getting errors when I attempt to insert an object with my document, I've using

Delta()..insert({
        "insert": {
          "_type": "youtube",
          "_inline": false,
          "url": url,
          "subtitles": "English",
          "language": "en",
          "thumbUrl": thumbUrl
        }
      }); 

And also:

Delta()..insert({
     
          "_type": "youtube",
          "_inline": false,
          "url": url,
          "subtitles": "English",
          "language": "en",
          "thumbUrl": thumbUrl
        
      }); 

And also:

Delta()..insert({
        "youtube": {
          "_inline": false,
          "url": url,
          "subtitles": "English",
          "language": "en",
          "thumbUrl": thumbUrl
        }
      }); 

All three of these throw errors for some reason. Either "No attribute key is registered" or "The following _TypeError was thrown during method call TextInputClient.updateEditingStateWithDeltas:
type 'Null' is not a subtype of type 'String' in type cast"


What am I doing wrong here?

Also, my second question. When adding autoformats. How do I not override the current ones? I can't spread the fallback ones afaik. This won't work:

final autoFormats = AutoFormats(autoFormats: [
      myAutoFormat(),
      ...AutoFormats.fallback()
    ]);

Thanks! I'm sorry for the giant post. I've spent hours on this and am throwing up the white flag. Any help would be greatly appreciated!

@cotw-fabier
Copy link
Author

This is the full function, FYI:

import 'package:fleather/fleather.dart';
import 'package:flutter/material.dart';
import 'package:parchment_delta/parchment_delta.dart';

class AutoFormatYoutubeEmbed extends AutoFormat {
  static final _youtubePattern =
      RegExp(r'!https:\/\/www\.youtube\.com\/watch\?v=([a-zA-Z0-9_-]+)$');

  const AutoFormatYoutubeEmbed();

  @override
  AutoFormatResult? apply(
      ParchmentDocument document, int position, String data) {
    // This rule applies to a space inserted after a YouTube URL, so we can ignore everything else.
    if (data != ' ') return null;

    debugPrint("This fired");

    final documentDelta = document.toDelta();
    final iter = DeltaIterator(documentDelta);
    final previous = iter.skip(position);
    // No previous operation means nothing to analyze.
    if (previous == null || previous.data is! String) return null;
    final previousText = previous.data as String;

    // Split text of previous operation in lines and words and take the last word to test.
    final candidate = previousText.split('\n').last.split(' ').last;
    final match = _youtubePattern.firstMatch(candidate);
    if (match == null) return null;

    final videoId = match.group(1);
    final url = 'https://www.youtube.com/watch?v=$videoId';
    final thumbUrl = 'https://img.youtube.com/vi/$videoId/0.jpg';

    final youtubeEmbedDelta = {
      "youtube": {
        "_inline": false,
        "url": url,
        "subtitles": "English",
        "language": "en",
        "thumbUrl": thumbUrl
      }
    };

    final change = Delta()
      ..retain(position - candidate.length)
      ..delete(candidate.length)
      ..insert(youtubeEmbedDelta);

    final undo = change.invert(documentDelta);
    document.compose(change, ChangeSource.local);
    return AutoFormatResult(
        change: change, undo: undo, undoPositionCandidate: position);
  }
}

@Amir-P Amir-P added the question Further information is requested label Jun 22, 2024
@Amir-P
Copy link
Member

Amir-P commented Jun 22, 2024

Hi @cotw-fabier.

Please forgive my possibly dumb question but I've been struggling trying to figure out how to do this for a while now.

Your confusion is totally normal. In fact, I myself have to read lots of code and try different things every time I need to tweak something in the editor and it's mostly due to the complexity of the project and poor documentation. Here's a working sample based on your code which catches YouTube links after pressing space or enter and turns them into an embedded object inside editor:

import 'package:fleather/fleather.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:parchment_delta/parchment_delta.dart';

void main() {
  runApp(const FleatherApp());
}

class FleatherApp extends StatelessWidget {
  const FleatherApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) => MaterialApp(
        debugShowCheckedModeBanner: false,
        theme: ThemeData.light(),
        darkTheme: ThemeData.dark(),
        title: 'Fleather - rich-text editor for Flutter',
        home: HomePage(),
      );
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final FocusNode _focusNode = FocusNode();
  final _controller = FleatherController(
      autoFormats: AutoFormats(autoFormats: [AutoFormatYoutubeEmbed()]));

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(elevation: 0, title: Text('Fleather Demo')),
      body: Column(
        children: [
          FleatherToolbar.basic(controller: _controller),
          Divider(height: 1, thickness: 1, color: Colors.grey.shade200),
          Expanded(
            child: FleatherEditor(
              controller: _controller,
              focusNode: _focusNode,
              padding: EdgeInsets.only(
                left: 16,
                right: 16,
                bottom: MediaQuery.of(context).padding.bottom,
              ),
              maxContentWidth: 800,
              embedBuilder: _embedBuilder,
              spellCheckConfiguration: SpellCheckConfiguration(
                  spellCheckService: DefaultSpellCheckService(),
                  misspelledSelectionColor: Colors.red,
                  misspelledTextStyle: DefaultTextStyle.of(context).style),
            ),
          ),
        ],
      ),
    );
  }

  Widget _embedBuilder(BuildContext context, EmbedNode node) {
    if (node.value.type == 'youtube') {
      return Container(
        height: 200,
        color: Colors.red,
        child: Center(
            child: Text(
                "A placeholder for youtube video: ${node.value.data['url']}")),
      );
    }
    return defaultFleatherEmbedBuilder(context, node);
  }
}

class AutoFormatYoutubeEmbed extends AutoFormat {
  static final _youtubePattern =
      RegExp(r'https:\/\/www\.youtube\.com\/watch\?v=([a-zA-Z0-9_-]+)');

  const AutoFormatYoutubeEmbed();

  @override
  AutoFormatResult? apply(
      ParchmentDocument document, int position, String data) {
    if (data != ' ' && data != '\n') return null;

    final documentDelta = document.toDelta();
    final iter = DeltaIterator(documentDelta);
    final previous = iter.skip(position);

    if (previous == null || previous.data is! String) return null;
    final previousText = previous.data as String;

    final candidate = previousText.split('\n').last.split(' ').last;
    final match = _youtubePattern.firstMatch(candidate);
    if (match == null) return null;

    final videoId = match.group(1);
    final url = 'https://www.youtube.com/watch?v=$videoId';
    final thumbUrl = 'https://img.youtube.com/vi/$videoId/0.jpg';

    final youtubeEmbedDelta = {
      "_type": "youtube",
      "_inline": false,
      "url": url,
      "subtitles": "English",
      "language": "en",
      "thumbUrl": thumbUrl
    };

    final change = Delta()
      ..retain(position - candidate.length)
      ..delete(candidate.length + 1)
      ..insert(youtubeEmbedDelta)
      ..insert('\n');

    final undo = change.invert(documentDelta);
    document.compose(change, ChangeSource.local);
    return AutoFormatResult(
      change: change,
      undo: undo,
      undoPositionCandidate: position - candidate.length + 1,
      selection:
          TextSelection.collapsed(offset: position - candidate.length + 2),
      undoSelection: TextSelection.collapsed(offset: position),
    );
  }
}

Also, my second question. When adding autoformats. How do I not override the current ones? I can't spread the fallback ones afaik.

You're right. It's impossible to extend the default auto formats right now. Will work on it.

@cotw-fabier
Copy link
Author

My man! Thank you for the speedy reply.

I was thinking about documentation. Would you guys be open to some help with writing documentation? I would also be interested in submitting a PR to add a way to simply spread the default autoformats.

I'm not the best dart developer you've ever met, but I think you guys have an excellent package and would love to do my part to help others realize it as well :).

@cotw-fabier
Copy link
Author

FYI your solution works perfectly!! Many thanks!

@Amir-P
Copy link
Member

Amir-P commented Jun 23, 2024

FYI your solution works perfectly!! Many thanks!

You're welcome. Glad it worked. I will close the issue but feel free to reopen it.

I was thinking about documentation. Would you guys be open to some help with writing documentation? I would also be interested in submitting a PR to add a way to simply spread the default autoformats.

We will be more than happy to have your help. You can contribute to the documentation at https://github.com/fleather-editor/fleather-editor.github.io. Although we have an old PR (fleather-editor/fleather-editor.github.io#3) waiting for a few months now but we will try to review them soon.

I'm not the best dart developer you've ever met, but I think you guys have an excellent package and would love to do my part to help others realize it as well :).

Thanks for the kind words! Don't forget to give it a star! ⭐️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants