area17 / twill

Twill is an open source CMS toolkit for Laravel that helps developers rapidly create a custom admin console that is intuitive, powerful and flexible. Chat with us on Discord at https://discord.gg/cnWk7EFv8R.
https://twillcms.com
Apache License 2.0
3.76k stars 574 forks source link

Quill wysiwyg: Cannot read property 'mutations' of undefined ... #458

Closed raymondtri closed 2 years ago

raymondtri commented 4 years ago

There is a strange issue I'm running into with the Quill WYSIWYG. I believe it is related to this topic:

https://github.com/quilljs/quill/issues/2312

I wish I could provide a better bug report, but I can't figure out a replicatable set of circumstances for this. I'll be keeping an eye out for this, and I will be adding more information as it is found.

The effects of this bug are that once it affects a wysiwyg form field, that field can no longer be edited without needing to be completely deleted and recreated.

raymondtri commented 4 years ago

There is another machine we have here locally that is exhibiting similar issues and I will be posting more information from it tomorrow morning. The delete / recreate block fixed it for me for now, but that can really be a pain on larger sites.

ccrawfordhatfield commented 4 years ago

These errors occur on page load:

image

[Vue warn]: Error in mounted hook: "TypeError: Cannot read property 'mutations' of undefined"

found in

---> <A17Wysiwyg> at frontend/js/components/Wysiwyg.vue
       <A17Locale> at frontend/js/components/LocaleField.vue
         <A17BlockCustomPageContent> at frontend/js/components/blocks/customs/BlockCustom_Page_Content.vue
           <A17Block> at frontend/js/components/blocks/Block.vue
             <TransitionGroup>
               <Draggable>
                 <A17Content> at frontend/js/components/Content.vue
                   <A17Fieldset> at frontend/js/components/Fieldset.vue
                     <Root>
TypeError: Cannot read property 'mutations' of undefined
    at mark (quill.js?9339:7166)
    at eval (quill.js?9339:7204)
    at LinkedList.forEach (quill.js?9339:7048)
    at eval (quill.js?9339:7203)
    at NodeList.forEach (<anonymous>)
    at eval (quill.js?9339:7199)
    at Array.forEach (<anonymous>)
    at ScrollBlot.optimize (quill.js?9339:7192)
    at Scroll.optimize (quill.js?9339:4307)
    at Scroll.batchEnd (quill.js?9339:4180)

This error occurs after making a change in wysiwig:

image

Uncaught Error: diff() called with non-document
    at eval (quill.js?9339:492)
    at Array.map (<anonymous>)
    at Delta.map (quill.js?9339:386)
    at eval (quill.js?9339:487)
    at Array.map (<anonymous>)
    at Delta.diff (quill.js?9339:486)
    at History.record (quill.js?9339:6522)
    at Emitter.eval (quill.js?9339:6481)
    at Emitter.emit (quill.js?9339:8598)
    at Emitter.emit (quill.js?9339:1893)
adam-jones-net commented 3 years ago

I'm now getting this problem on multiple clients installs.

It seems that some really simple html can cause this problem.

Just edit a wysiwyg field directly in your db editor and set the fields content to <em><br>def</em>. That will cause the JS error I believe. Whereas <em>abc<br>def</em> will work without issue.

It seems that editing in Quill allows for this kind of html to be generated, so you can easily end up with a document that has such an issue. What's so frustrating is that Twill doesn't acknowledge any problem occured, so a user can update the doc and think it was saved without issue as the normal yellow success msg appears.

This is really causing me serious grief.... Is there no solution @ifox ?

dacastro4 commented 3 years ago

@raymondtri @arkid did you guys fix this issue? I'm currently having it with our wysiwygs

adam-jones-net commented 3 years ago

@dacastro4 no fix :(

lukasfehling commented 3 years ago

facing the same issue

adam-jones-net commented 2 years ago

@ifox Do you know what is going on here ? I'm getting this on various installs and its a total pain as it stops twill functioning for me in the most fundamental of ways - ie changes to wysiwyg fields dont save on an update (even though twill thinks everything went ok and reports success ).

Is there a work around at least ?

ifox commented 2 years ago

Hi @arkid, what's going on is that the database column for your wysiwyg field contains non printable chars that make Quill crash on page load. It really became an issue for a single instance out of many on our side and what we ended up doing was creating a command and post-save action that was replacing offending characters from the html string in database. I will create a gist later today to share the approach here. Hope that helps, I know it can be a very annoying issue.

adam-jones-net commented 2 years ago

Ah ok understood, thanks @ifox that would be amazing! :)

haringsrob commented 2 years ago

Managed to reproduce, but going to leave this open until quill 2.x is released and hope it will be fixed by then.

haringsrob commented 2 years ago

I have (I think) implemented a fix here: https://github.com/area17/twill/pull/1680

If you want you can give this a try by applying the pr as a patch (https://github.com/area17/twill/pull/1680.patch) and run php artisan twill:build.

adam-jones-net commented 1 year ago

@ifox or anyone else, is there a strategy on cleaning up html tags that may reside in existing documents? Is there anything I can write to run over my records and clean it up ?

antonioribeiro commented 1 year ago

Hello @adam-jones-net , we had to use "ezyang/htmlpurifier" to constantly clean in up, because our clients are pasting Word documents in Quill, so on some of our repositories we added this:

public function update($id, $fields)
{
    parent::update($id, $fields);

    $this->fixHtmls($id);
}

protected function fixHtmls($id)
{
        $this->model
            ->findOrFail($id)
            ->blocks->each(
                fn($block) => app(BlockRepository::class)->fixBlockHtmls(
                    $block,
                ),
            );
}

And this is our BlockRepository

<?php

namespace App\Repositories;

use A17\Twill\Models\Block;
use App\Services\Html\Service;

class BlockRepository
{
    /**
     * @var \Illuminate\Contracts\Foundation\Application|mixed
     */
    private $htmlService;

    public function __construct()
    {
        $this->htmlService = app(Service::class);
    }

    private function fixAllHtmlItems($content)
    {
        if (is_string($content)) {
            return $this->fixHtmlString($content);
        }

        if (is_array($content)) {
            foreach ($content as $key => $value) {
                $content[$key] = $this->fixAllHtmlItems($value);
            }
        }

        return $content;
    }

    /**
     * @param \A17\Twill\Models\Block $block
     */
    public function fixBlockHtmls(Block $block): void
    {
        $block['content'] = $this->fixAllHtmlItems($block['content']);

        $block->save();
    }

    public function fixHtml()
    {
        foreach (Block::all() as $block) {
            $this->fixBlockHtmls($block);
        }
    }

    private function fixHtmlString(string $content)
    {
        if (is_html($content)) {
            return $this->htmlService->purify(
                $this->removeNonPrintableChars($content),
            );
        }

        return $content;
    }

    private function removeNonPrintableChars(string $string)
    {
        $string = str_replace(
            ['<br>', '<br />', '<br/>', "\n", "\r", '\n', '\r'],
            '',
            $string,
        );

        return preg_replace('/[\x00-\x1F\x7F]/u', '', $string);
    }
}

And our HTML Service

<?php

namespace App\Services\Html;

use HTMLPurifier;
use HTMLPurifier_Config;
use Gajus\Dindent\Indenter;
use Illuminate\Support\Str;
use TijsVerkoyen\CssToInlineStyles\CssToInlineStyles;

class Service
{
    private function listWrongTags($html)
    {
        preg_match_all('#(<\s?\\\/.{1,5}>)#', $html, $matches);

        $tags = [];

        foreach ($matches[0] as $tag) {
            $newTag = str_replace('<\/', '</', $tag);

            $newTag = str_replace('< \/', '</', $newTag);

            $tags[$tag] = $newTag;
        }

        return $tags;
    }

    private function removeBrokenHtmlTags($html)
    {
        foreach ($this->listWrongTags($html) as $tag => $newTag) {
            $html = str_replace($tag, $newTag, $html);
        }

        return str_replace(['\n\r', '\r\n'], ['', ''], $html);
    }

    public function indent(string $html)
    {
        return app(Indenter::class)->indent($html);
    }

    public function inlineCss($html)
    {
        $cssInliner = new CssToInlineStyles();

        $html = Str::of($html);

        $css = $html->between('<style>', '</style>');

        $html = $html->before('<style>') . $html->after('</style>');

        $html = Str::of($cssInliner->convert($html, $css));

        return $html->before($table = '<table class="email"') .
            sprintf('<style>%s</style>%s', $css, $table) .
            $html->after($table);
    }

    public function inlineCssAndIndent($html)
    {
        return $this->indent($this->inlineCss($html));
    }

    public function purify($html)
    {
        if (!is_html($html)) {
            return $html;
        }

        $pure = $this->removeBrokenHtmlTags($html);

        $config = HTMLPurifier_Config::createDefault();

        $config->set('HTML.Doctype', 'XHTML 1.0 Transitional');
        $config->set('HTML.SafeIframe', true);
        $config->set('HTML.Trusted', true);
        $config->set(
            'URI.SafeIframeRegexp',
            '%^(http://|https://|//)(www.youtube.com/embed/|player.vimeo.com/video/|vernissage.fondationlouisvuitton.eventmaker.io/|player.freecaster.com/|api.soundcloud.com/tracks/|w.soundcloud.com/player/)%',
        );

        $pure = (new HTMLPurifier($config))->purify($pure);

        return $this->removeHtmlentitiedTags($html, $pure);
    }

    private function removeHtmlentitiedTags($html, string $newHtml)
    {
        foreach ($this->listWrongTags($html) as $tag => $newTag) {
            $newHtml = str_replace(
                [htmlentities($tag), htmlentities($newTag)],
                ['', ''],
                $newHtml,
            );
        }

        return $newHtml;
    }
}

It does more than you probably need, like also fixing indentation, but I just pasted here the whole thing unedited.