Gutenberg: Bloky editovatelné

V minulém článku jsem se ukázal základní principy tvorby bloků na příkladu bloků statických, které toho moc nedělají. Dnes se podíváme tvorbu bloků, které již mají nějaké to kontextové menu pro úpravu a umožní uživateli trochu více interakce.

Errata pro minulé díly: Zde bych také chtěl opravit chybu, která se dostala do předchozích dílů. V rámci Gutenbergu neexistují pouze bloky statické a dynamické. Rozlišujeme 3 typy bloků: statické, editovatelné, a dynamické.

Statické jsme si již ukázali, o editovatelných si povíme v tomto článku, a později se dostanu i k blokům dynamickým. Rozdíl mezi bloky editovatelnými a dynamickými je ten, že bloky editovatelné nepotřebují ke svému vykreslení žádnou komunikaci se serverem. Naopak bloky dynamické v editoru slouží jako “placeholer” pro data, která se později získají ze serveru. Kupříkladu výpis nejnovějších příspěvků.

Marquee

Pro tento tutoriál jsem připravil implementaci bloku reprezentující HTML element <marquee> s možností nastavení vlastního textu, směru a rychlosti pohybu.

Text budeme editovat pomocí textarea, směr pomocí vlastního panelu nástrojů, a rychlost pomocí textového vstupu v inspektoru bloku (panelu na levo od oblasti, kde komponujeme text).

Jak definovat vlastní Gutenberg Blok už jsme si ukázali. Pojďme se tedy zrovna vrhnout na ukázku kódu, který si dnes rozebereme. (Samozřejmě je možné jej nakopírovat a vložit do developer console ve vašem prohlížeči na obrazovce pro editaci příspěvku na webu, kde máte Gutenberg instalovaný a vyzkoušet si jeho funkci).

Velmi podobný kód naleznete také na GitHubu, pro někoho to může být pohodlnější způsob čtení.

( function() {
	var el = wp.element.createElement,
	registerBlockType = wp.blocks.registerBlockType,
	PlainText = wp.blocks.PlainText,
	BlockControls = wp.blocks.BlockControls,
	InspectorControls = wp.blocks.InspectorControls,
	TextControl = wp.components.TextControl;

	registerBlockType( 'david-binda/marquee', { // Název bloku - prefix / jméno.

		// Titulek, ikona a kategorie pro zobrazení v nabíce bloků.
		title: 'Marquee',
		icon: 'controls-repeat',
		category: 'layout',

		/*
		 * Všechny atributy, které chceme moci měnit, musí být zaregistrované.
		 */
		attributes: {
			content: { // Vlastnost "content" typu řetězec.
				type: 'string',
			},
			direction: { // Vlastnost "direction" typu řetězec.
				type: 'string',
				default: 'left', // Výchozí hodnota.
			},
			scrollAmount: {
				type: 'number', // Číselný typ.
				default: 6,
			}
		},

		/**
		 * Funkce, která se zavolá při úpravě bloku
		 */
		edit: function( props ) {

			/**
			 * Callback pro případ, kdy se změní hodnota obsahu.
			 */
			function onChangeContent( newContent ) {
				props.setAttributes( { content: newContent } );
			}

			/**
			 * Callback pro případ, kdy se změní hodnota směru posuvu.
			 * onClick očekává funkci, tudíž musíme vrátit funkci.
			 */
			function onChangeDirection( newDirection ) {
				return function() {
					// Pokud jsme na tlačítko s vybraným směrem klikli opakovaně, nastavení zrušíme na defaultní "left".
					props.setAttributes( { 'direction': props.attributes.direction === newDirection ? 'left' : newDirection } );
				};
			}

			function onChangeLoop( newAmount ) {
				props.setAttributes( { 'scrollAmount': newAmount } );
			}

			/*
			 * Jelikož chceme prvek zobrazit už v editoru, pokud není zrovna upravován,
			 * tak jako na frontendu, pomůžeme si defaultní vlastností isSelected.
			 */
			if ( true !== props.isSelected ) {
				// Zde vracíme prvek prakticky stejně, jako v případě funkce "save".
				return el(
					'marquee', // HTML element
					{
						className: props.className, // Třída generovaná Gutenbergem.
						'direction': props.attributes.direction, // vlastní attribute direction.
						'scrollamount': props.attributes.scrollAmount || 6, // vlastní attribute scrollamount.
					},
					props.attributes.content || props.attributes.placeholder || "Hello world!" // obsah prvku, placeholder, nebo defaultní placeholder.
				);
			} else {
				/*
				 * Pokud s prvkem manipulujeme, chceme zobrazit nějaké ovládací prvky
				 * a také změnit element marquee na textarea, aby uživatel mohl editovat text.
				 */

				// Vrátíme celé pole prvků
				return [
					// Nejprve panel nástrojů obsahující možnost změny směru posuvu.
					el(
						BlockControls, // BlockControls je prvek existující v rámci Gutenbergu.
						{ 'controls': [ { // Jednotlivá tlačítka lze snadno definovat jako pole objektů.
							icon: 'arrow-left', // Ikona tlačítka v panelu nástrojů.
							title: 'Direction Left', // Popisek.
							onClick: onChangeDirection( 'left' ), // Akce vykonávající se při kliknutí.
							isActive: ( 'left' === props.attributes.direction ) // zda-li je tlačítko aktivní či nikoli.
						}, {
							icon: 'arrow-right',
							title: 'Direction Right',
							onClick: onChangeDirection( 'right' ),
							isActive: ( 'right' === props.attributes.direction )
						}, {
							icon: 'arrow-up',
							title: 'Direction Up',
							onClick: onChangeDirection( 'up' ),
							isActive: ( 'up' === props.attributes.direction )
						}, {
							icon: 'arrow-down',
							title: 'Direction down',
							onClick: onChangeDirection( 'down' ),
							isActive: ( 'down' === props.attributes.direction )
						} ] }
					),
					// Inspektor (zobrazí se v záložce "Blok" v pravém sloupci).
					el(
						InspectorControls,
						{},
						el(
							TextControl,
							{
								label: 'Scroll speed (defaults to 6):',
							 	value: props.attributes.scrollAmount,
								onChange: onChangeLoop,
								type: 'number',
							}
						)
					),
					// Dále samotný prvek v editovatelné podobě.
					el(
						PlainText, // Textarea.
						{
							className: props.className,
							onChange: onChangeContent, // Callback.
							placeholder: props.attributes.placeholder || "Hello world!", // Placeholder.
							value: props.attributes.content, // Obsah textového pole.
							isSelected: props.isSelected,
							style: {'display':'inline-block'}, // Marquee je defaultně inline-block, tak toto chceme zachovat.
						}
					),
				];
			}
		},

		/**
		 * Funkce, která se zavolá při ukládání prvku.
		 * Například přechod mezi visual a code editorem.
		 */
		save: function( props ) {
			/*
			 * Všimněte si, že toto je stejné, jako v případě, kdy
			 * isSelected vrací false (ve funkci edit).
			 */
			return el(
				'marquee',
				{
					className: props.className,
					'direction': props.attributes.direction,
					'scrollamount': props.attributes.scrollAmount || 6,
				},
				props.attributes.content || props.attributes.placeholder || "Hello world!" // obsah prvku, placeholder, nebo defaultní placeholder.
			);
		},
	} );
})();

V kódu jsem se snažil okomentovat skoro všechny řádky tak, aby i ten, kdo s Gutenbergem doposud nepracoval, pochopil co se na nich děje. Nyní ještě trochu podrobněji.

Editovatelné atributy

Všechny atributy bloku, které chceme uživateli užmoňit upravovat či nastavovat, je nutné předem registrovat pomocí klíče “attributes”. V přípabě ukázkového kódu se jedná o “content”, “direction” a “scrollamount”. U prvních dvou očekáváme jednoduchý řetězec, u posledního poté číselný vstup.

Attributy “direction”, a “scrollamount” mají nastavenou defaultní hodnotu (“left”, respektive 6).

“Attributes” by měl být objekt (v PHP by to bylo asociativní pole), kde klíč slouží k identifikaci atributu, a jako hodnota je poté další pole obsahující hlavně typ atributu (v našem případě řetězec – string).

Typů atributů existuje ovšem více. Jsou velmi dobře popsány v dokumentaci. Téma je to trochu rozsáhlejší, a než abych se o něm rozepisoval zde, věnuji mu později celý článek.

Funkce edit a props.isSelected

U editovatelných bloků jako tvůrci chceme uživateli v rámci editoru nabídnout dva způsoby zobrazení. Jeden kdy se zobrazí prvek v “edit” módu a druhý kdy se prvek zobrazí tak, jako by byl na frontendu.

Mezi těmito módy nemusí být velký rozdíl. Obyčejně se bude jednat pouze o změnu z HTML elementu samotného na PlainText či RichText umožňující tvorbu obsahu. Jindy může jít pouze o přidání panelu nástrojů (toolbar), přidání polí do inspektoru, či kombinaci předchozích.

Pro to, abychom mohli mezi jednolivými módy rozlišovat, máme k dispozici props.isSelected, který vrací true v případě, že prvek uživatel chce editovat – tj. klikl na něj a je vybrán.

V případě, že prvek zrovna není editován, většinou budeme nejspíše vracet stejné elementy jako v případě funkce save. V načem příkladě bychom mohli kód zjednodušit na následující:

edit: function( props ) {

    // nějaké ty pomocné funkce etc.

    if ( true !== props.isSelected ) {
        return $this.save( props );
    }

BlockControls

BlockControls je prvek, pomocí kterého lze tvořit panel nástrojů (toolbar). Stačí jen nadefinovat vlastní tlačíka a přidat jim vlastní callbacks.

Přidat tlačítka lze hned několika způsoby. Asi nejjednodušší je využít druhého parametru ( props ) funkce wp.element.createElement, která vychází z Reactu

props by měl být objekt (a to platí u každého prvku tvořeného pomocí wp.element.createElement, nejen pro BlockControls) a v případě, že chceme zobrazit vlastní tlačíka pro BlockControls prvek, využijeme klíč “controls” a jako hodnotu mu předáme pole objektů:

{ // objekt
    'controls': // klíč controls
    [ // pole objektů
        { //objekt
            icon: 'arrow-left',
            title: 'Direction Left',
            onClick: onChangeDirection( 'left' ),
            isActive: ( 'left' === props.attributes.direction || undefined === props.attributes.direction )
        },
        { // objekt
            icon: 'arrow-right',
            title: 'Direction right',
            onClick: onChangeDirection( 'right' ),
            isActive: ( 'right' === props.attributes.direction )
        }
    ]
}

Pokud bychom chtěli tlačítka v liště nástrojů seskupovat, můžeme namísto “pole objeků” jako hodnotu pro klíč “controls” použít “pole polí objektů”:

{
    'controls': // klíč controls
    [ // pole polí objektů
        [ // pole objektů #1
            { //objekt
                icon: 'arrow-left',
                title: 'Direction Left',
                onClick: onChangeDirection( 'left' ),
                isActive: ( 'left' === props.attributes.direction || undefined === props.attributes.direction )
            },
            { // objekt
                icon: 'arrow-right',
                title: 'Direction Right',
                onClick: onChangeDirection( 'right' ),
                isActive: ( 'right' === props.attributes.direction )
            }
        ],
        [ // pole objektů #2
            { //objekt
                icon: 'arrow-up',
                title: 'Direction Up',
                onClick: onChangeDirection( 'up' ),
                isActive: ( 'up' === props.attributes.direction )
            },
            { // objekt
                icon: 'arrow-down',
                title: 'Direction Down',
                onClick: onChangeDirection( 'down' ),
                isActive: ( 'down' === props.attributes.direction )
            }
        ]
    ]
}

Gutenberg by nám pak měl tyto jednotlivé bloky oddělit separatorem:

Panelů nástrojů lze ovšem také vložít více za sebou. To může být výhodné zvláště v případě, kdy si buď vytvoříme vlastní prvek obsahující tlačíka, která se opakují, nebo budeme chtít využít nějaký, který se v Gutenbergu již nativně objevuje (například AlignmentToolbar), a doplnit jej o vlastní sadu nástrojů:

return [
            el( // První panel nástrojů:
                BlockControls,
                { 'controls': [ {
                    icon: 'arrow-left',
                    title: 'Direction Left',
                    onClick: onChangeDirection( 'left' ), // Akce vykonávající se při kliknutí.
                    isActive: ( props.attributes.direction === 'left' || props.attributes.direction === undefined )
                }, {
		    icon: 'arrow-right',
		    title: 'Direction Right',
                    onClick: onChangeDirection( 'right' ),
                    isActive: ( props.attributes.direction === 'right' )
	        } ] }
            ),
            el( // Druhý panel nástrojů.
                BlockControls,
                { key: 'controls' },
                el(
                    wp.blocks.AlignmentToolbar, // Defaultní panel pro zarovnání.
                    {
                        value: alignment,
                        onChange: onChangeAlignment // Je nutné definovat.
                    }
                )
            ),
            // Dále samotný prvek
];

No a v neposlední řadě lze tlačítka vygenerovat jako samostatné prvky, které se do prvku BlockControls předají pomocí třetího (a následujících) parametrů. Podobně jako jsme u statických bloků tvořili obsah HTML elementů.

Callback pro uložení nastavení

Při vytváření vlastních prvků jsme použili jako callback pro akci (kliknutní na tlačíko) následující zápis:

onClick: onChangeDirection( 'right' ),

Funkce onChangeDirection je poté definována následujícím způsobem:

        /**
         * Callback pro případ, kdy se změní hodnota směru posuvu.
         */
        function onChangeDirection( newDirection ) {
            return function() {
                props.setAttributes( { 'direction': props.attributes.direction === newDirection ? 'left' : newDirection } );
            };
        }

Tedy, funkce onChangeDirection vrací anonymní funkci. To proto, že Reactí onClick očekává funkci, jako svou hodnotu, a navíc se musíme vypořádat s různými hodnotami pro každé tlačítko.

Všimněte si toho, že pokud na to aktivní tlačíko klikneme opakovaně, znamená to, že chceme nastavení zrušit. Jelikož má ale Marquee defaultní hodnotu pro attribut direction, resetujeme jen v takovém případě na “left”.

V případě prvku wp.blocks.AlignmentToolbar naopak definujeme “pouze” onChange parametr. Abstraktní prvek Toolbar se poté postará o podbné řešení skrze onClick sám. Prohlédněte si zdrojový kód.

Inspector

Gutenberg vedle panelu nástrojů umožňuje ještě vložení formulářových polí a dalších ovládacích prvků do tzv. Inspektoru. To je pravý sloupec v editoru, kam by se mělo umisťovat to, co se do to panelu nástorojů nehodí.

Podobně jako přidáváme panel nástrojů, přidáme i inspector, nicméně tentokráte budeme čerpat z wp.components. V ukázce níže naleznete jakým způsobem lze přidat wp.components.TextControl. Další *Control componenty lze nalézt na GitHubu Gutenbergu:

// Inspektor (zobrazí se v záložce "Blok" v pravém sloupci).
el(
	InspectorControls,
	{},
	el(
		TextControl,
		{
			label: 'Scroll speed (defaults to 6):', // Popisek.
			value: props.attributes.scrollAmount, // Hodnota.
			onChange: onChangeSpeed, // save callback.
			type: 'number', // attribut prvku input - lze definovat i další.
		}
	)
),// Následující kód již znáte.

Funkci onChangeSpeed jsem v ukázce nedefinoval, nicméně bude hooodně podobná funkci onChangeContent, která je použita v úvodní úkázce.

Textové vstupy – PlainText a RichText

Vedle panelů nástrojů, je třeba také uživateli umožnit tvořit nějaký ten text. Gutenberg v základu nabízí dva způsoby. Textové pole (textarea) a RichText editor, který není nepopodbný TinyMCE známého ze současného WordPressu.

Jelikož ale RichText pracuje s attributy, kde zdrojem (source) je skupina prvků, dovolím si dnes rozebrat pouze PlainText a k RichText se vrátit v dalším díle. Pro zájemce však uvedu několik odkazů:

PlainText

Jednodušší na vysvětlení tedy, zvláště vzhledem k vazbě na “Attributes” vysvětlované o pár odstavců výše, je PlainText (konkrétně wp.blocks.PlainText). Atribut, který PlainText umožňuje vytvořit může být jednoduchý řetězec, jelikož jako vstupní pole je použita textarea, která se sama přizpůsobuje velikosti vstupu, a do níž, z pricipu, nelze vkládat nic jiného než text.

Tedy, abychom si připomněli kód:

                // Dále samotný prvek
                el(
                    PlainText, // Textarea.
                    {
                        className: props.className,
                        placeholder: props.attributes.placeholder || "Hello world!",
                        value: props.attributes.content, // Obsah textového pole.
                        onChange: onChangeContent, // Callback.
                        isSelected: props.isSelected,
                        style: {'display':'inline-block'}, // Marquee je defaultně inline-block, tak toto chceme zachovat

                    }
                ),

“className” je důležité hlavně proto, že Gutenberg generuje class name z názvu, a prefixu, prvku. Díky predikovatelnosti lze toto využít k tvorbě CSS stylů.

Dále máme “placeholder”. Zmiňoval jsem, že Gutenberg pracuje s placeholdery, aby tak umocnil intuitivnost ovládání. I náš marquee blok by tedy nějaký ten placeholder měl obsahovat.

“value” je hodnota, která je nastavena prvku, když je zobrazen. Připomeňme si, že textové pole se zobrazí pouze, pokud je prvek vybrán uživatelem s cílem s ním něco udělat – přesunout jej, nebo upravit obsah, či použít panel nástorů. Hodnota je tedy generována pomocí props.attributes.content, kam se pomocí callbacku definovaném v “onChange” ukládá.

Dále nalezneme “isSelected”, kterým předáváme hodnotu toho, zda-li je prvek vybrán a jako ukázku toho, že lze nadefinovat libovolný HTML atribut, který se poté vypíše, je použit “style”, kterým se v našem případě snažíme docílit toho, aby textarea měla podobné vlastnosti jako marquee – tedy aby byla zobrazena jako inline-block.

“onChange” je callback pro to, co se má stát, když se hodnota změní. Není volána pokaždé, když je připsáno písmeno, ale poté, co uživatel dokončí editaci. Nejedná se o nic komplikovaného:

        /**
         * Callback pro případ, kdy se změní hodnota obsahu.
         */
        function onChangeContent( newContent ) {
            props.setAttributes( { content: newContent } );
        }

Pomocí props.setAttributes() dostaneme hodnotu do props.attributes.content, kterou používáme i jinde v kódu.

Kam se tato data ukládají?

Všechny hodnoty, které lze v rámci marquee bloku upravovat, se ukládají přímo do post_content. V rámci parsovatelných HTML komentářů. Ty se na frontendu nezobrazují, ale Gutenbergu umožňují vygenerovat příslušné uživatelské rozhraní a skladovat dané hodnoty.

Pro zobrazení, pokud máte blok zaregistrovaný a vytvořený, staří přepnout do módu umožňující úpravu zdrojového kódu (stejné zobrazení existuje i u současného editoru obsahu). Zde je ukázka, jak takový uložený blok vypadá:

<!-- wp:david-binda/marquee {"direction":"right","scrollAmount":"10"} -->
<marquee class="wp-block-david-binda-marquee" direction="right" scrollamount="10">Hello world!</marquee>
<!-- /wp:david-binda/marquee -->

Závěrem

Udržet tento článek v rozumném rozsahu bylo opravdu těžké – některá témata budu muset později ještě jednou a tentokráte více do hloubky rozebrat. Ovšem věřím, že i bez hlubšího zastavení se, by měl kód posloužit jako dobrý základ pro další studium.

Určitě doporučuji si s kódem použitým v tomto článku trochu pohrát – stačí je vložit a upravit přímo v developer console vašeho prohlíže. Připomenu, že před opakovanou registrací prvku, lze použít následující kód pro jeho odstranění:

wp.blocks.unregisterBlockType( 'david-binda/marquee' );

Kód, který jsem v tomto článku použil jsem, více či méně, v nezměněné podobně nahrál také na GitHub. Velmi pravděpodobně se k němu v blízké budoucnosti ještě vrátím, rád bych jej přepsal do JSNext, nicméně v podobně v jaké je distukován zde bude navždy žít v tagu 0.1.0. Použitím ES5 pro tento tutoriál jsem chtěl zacílit i na ty z vás, kteří moderní JavaScript a další nástoje nemají ještě natolik zažité.

Pokud byste se rádi podívali na to, jak lze moderně precovat s Gutenbergem a nevíte jak přesně nastavit Webpack a další, vyzkoušjte projekt create-guten-block

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s