verbb / vizy

A flexible visual editor for Craft CMS
Other
44 stars 8 forks source link

Question: add HTML and VizyBlock via PHP #276

Closed ishetnogferre closed 9 months ago

ishetnogferre commented 9 months ago

Question

Hi, so we have a Redactor text field with a custom button extension, we want to convert to a Vizy field, but just the text functionality and 1 Vizy block for that button.

Since the website has quite a lot of these text fields filled out, manually changing them would be quite a hassle therefore we started looking into doing this via a console command.

I found this article for a similar thing in Statamic, which I later found out was quite similar to how the FeedMe field works.

So far so good, all the HTML was converted correctly, now only the button remained.

In that article they mention you can add a custom Node extension, so I'm trying that to catch the button with following code in that extension

public function parseHTML()
    {
        return [
            [
                'tag' => 'a[data-track-id]',
            ],
        ];
    }

and then we return the attributes like they would be in the Vizy field:

return [
    "enabled" => true,
    "collapsed" => false,
    "values" => [
        "type" => $blockType,
            "content" => [
                "fields" => [
                       "commonStore" => [$parsedAttributes[$storeId]],
                       "storeLinkText" => $urlText,
                       "storeUrlSelect" => "commonWebsiteUrl",
                   ]
               ]
           ]
];

And finally the code for going through the HTML and parsing the content into Vizy/Tiptap nodes:

$editor = new Editor([
    'content' => $html,
    'extensions' => [
        ...
        new VizyBlock,
     ],
]);

$doc = $editor->getDocument();
if (is_array($doc) && array_key_exists('content', $doc)) {
    // Vizy field expects JSON encoded data
    $value = Json::encode($doc['content']);
}

And that works for the HTML but the Vizy block isn't showing up. Is this currently possible? Do we need to extend on the Vizy block/node somewhere? Thanks in advance!

Additional context

No response

engram-design commented 9 months ago

Vizy blocks will be different to Statamic Bard Set's. Indeed, the Feed Me integration handles all the rich text content, but doesn't handle Vizy Blocks, as that's very difficult/complex to map in Feed Me.

So - there's no handling at the moment for converting Vizy Block HTML to its TipTap/ProseMirror schema doc, but it's also totally fine to implement this yourself. We may very well add this to core soon.

<?php
namespace modules\vizytest;

use craft\helpers\Json;

use Tiptap\Core\Node;

use DOMElement;

class VizyBlock extends Node
{
    public static $name = 'vizyBlock';
    public static $priority = 200;

    public function addOptions()
    {
        return [
            'HTMLAttributes' => [],
        ];
    }

    public function addAttributes()
    {
        return [
            'id' => [
                'parseHTML' => fn ($DOMNode) => $this->_getNodeContent($DOMNode, 'id'),
            ],
            'enabled' => [
                'parseHTML' => fn ($DOMNode) => $this->_getNodeContent($DOMNode, 'enabled'),
            ],
            'collapsed' => [
                'parseHTML' => fn ($DOMNode) => $this->_getNodeContent($DOMNode, 'collapsed'),
            ],
            'values' => [
                'parseHTML' => fn ($DOMNode) => $this->_getNodeContent($DOMNode, 'values'),
            ],
        ];
    }

    public function parseHTML()
    {
        return [
            [
                'tag' => 'vizy-block',
            ],
        ];
    }

    private function _getNodeContent(DOMElement $node, string $item)
    {
        $content = Json::decode($node->getAttribute('data-content'));

        return $content[$item] ?? null;
    }
}

The above will be used as the definition for how to handle mapping HTML to a Node. We don't actually have an HTML structure for a Vizy Block node (because we don't need one), so we'll make one up. We need to represent the following:

[
    'id' => 'vizy-block-' . rand(),
    'enabled' => true,
    'collapsed' => false,
    'values' => [
        'type' => $blockType->id,
        'content' => [
            'fields' => [
                'myInnerField' => 'Some imported content',
            ],
        ],
    ],
]

Which we can do by adding a custom <vizy-block> tag. Let's use a data-content attribute to store this info as JSON.

This will look something like:

"<vizy-block data-content='{"id":"vizy-block-2119062279","enabled":true,"collapsed":false,"values":{"type":"type-h78sfEIGgq","content":{"fields":{"myInnerField":"Some imported content"}}}}'></vizy-block>

Once we have that, the above class will handle converting that HTML text to a Node.

A full example of it will look like:

// Get the Vizy field and the block type you want to use
$vizyField = Craft::$app->getFields()->getFieldByHandle('myVizyField');
$blockType = $vizyField->getBlockTypes()[0];

// Generate a `<vizy-block ...>` HTML element, according to the Vizy Block spec
$html = \craft\helpers\Html::tag('vizy-block', null, [
    'data-content' => [
        'id' => 'vizy-block-' . rand(),
        'enabled' => true,
        'collapsed' => false,
        'values' => [
            'type' => $blockType->id,
            'content' => [
                'fields' => [
                    'myInnerField' => 'Some imported content',
                ],
            ],
        ],
    ],
]);

// Create a TipTap editor with regular WYSIWYG extensions, and our custom Vizy Block node class handler (above)
$editor = new \Tiptap\Editor([
    'content' => $html,
    'extensions' => [
        new \Tiptap\Extensions\StarterKit,
        new VizyBlockNode,
     ],
]);

$doc = $editor->getDocument();

if (is_array($doc) && array_key_exists('content', $doc)) {
    $value = \craft\helpers\Json::encode($doc['content']);
}

// Create an entry and use the value 
$entry = new \craft\elements\Entry();
$entry->title = 'test ' . rand();
$entry->sectionId = 123;
$entry->typeId = 123;

$entry->setFieldValues([
    'myVizyField' => $value,
]);

Craft::$app->getElements()->saveElement($entry);

Hope that helps!

ishetnogferre commented 9 months ago

Aha thanks! Found what was missing in our code 🙌

The VizyBlock file was a bit different in our case, because we directly wanted to map certain HTML elements (in this case anchor tags with certain data attributes) to VizyBlocks we already have in the field.

In below (simplified) code we were missing the parseHTML in each attribute

<?php
use Craft;
use craft\helpers\Json;

use Tiptap\Core\Node;

use DOMElement;
class VizyBlock extends Node
{
    public static $name = 'vizyBlock';
    public static $priority = 200;

    public function addOptions()
    {
        return [
            'HTMLAttributes' => [],
        ];
    }

    public function parseHTML()
    {
        return [
            [
                'tag' => 'a[data-track-id]',
            ],
        ];
    }

    public function addAttributes()
    {
        return [
            'id' => [
                'parseHTML' => function() {
                    $blockId = 'vizy-block-'. rand();
                    return $blockId;
                }
            ],
            'enabled' => [
                'parseHTML' => function() {
                    $isEnabled = true;
                    return $isEnabled;
                },
            ],
            'collapsed' => [
                'parseHTML' => function(){
                    $isCollapsed = false;
                    return $isCollapsed;
                },
            ],
            'values' => [
                'parseHTML' => function ($DOMNode) {
                    $storeId = "data-track-id";
                    $storeUrl = "href";
                    $urlText = "";
                    $blockType = "type-kjZG7uw4M8";

                    $parsedAttributes = [];
                    $attributes = $DOMNode->attributes;
                    $children = $DOMNode->childNodes;

                    // Get all the attributes
                    foreach($attributes as $attr) {
                        $parsedAttributes[($attr->name)] = $attr->value;
                    }

                    // Get the inner text via the childNodes
                    foreach ($children as $child) {
                        // Check if the node is a text node
                        if ($child->nodeType === XML_TEXT_NODE) {
                            // Get the HTML content of the text node
                            $urlText = $child->textContent;
                        }
                    }

                    return [
                        "type" => $blockType,
                        "content" => [
                            "fields" => [
                                "commonStore" => [$parsedAttributes[$storeId]],
                                "storeLinkText" => $urlText,
                                "storeLinkUrl" => $storeUrl,
                                "storeUrlSelect" => "commonWebsiteUrl",
                            ]
                        ]
                    ];
                },
            ]
        ];
    }
}