トップへ戻る
BLOGS

シンタックスハイライトブロックのUPDATE

シンタックスハイライトブロックのUPDATE

こんにちは

今日の積み上げとして以前作成した、本サイトで使用しているコードをシンタックスハイライトするブロックのアップデートを行ったのでそのブロックの仕様書として書き込んでおこうと思いました。

Oja Code Guteberg

こちらの作成方法は下記のサイトの記事が大変参考になりました
というよりほぼほぼコピー、、、笑

WordPress Logo Code-Prettify でシンタックスハイライトするブロックの作成

しかしこれからリファクタリングなどで自分の成果物にしてしまえばよき!!
ということで、どんどん更新していきます。。笑

今回のUPDATE内容

今回のやりたいこととその簡単な詳細をまとめてみました

codeの表示方法を設定する

全文表示する
特に何もしない。現在の状態
全文省略する
コードのファイル名をクリックするとアコーディオンでコード内容が開く
行数を指定して表示する
行数を指定するinputタグを出力し入力された値に応じてコードを省略する
コードをワンクリックでコピーできるようにする
コピーするためのボタンの実装と、JSを追記

上記の他にインスペクターの簡単なリファクタリングもしっかり行います

それでは早速書いていきます

必要な属性値の追加

oja-code-gutenberg.php

'attributes' => [
  //コードの表示方法
  'codeMode' => [
    'type' => 'string',
    'default' => 'code-all'
  ],
  //抜粋表示数
  'excerptLength' => [
    'type' => 'number',
    'default' => 3
  ],
]

codeModeは選択したコードの表示形式を保存するのに使用します

excerptLengthは「抜粋表示」を選択した際に入力してもらう「行数の数値」を保存するのに使用します

さてこれらの属性値を利用して管理画面を作っていくわけですがその前に現在のEditメソッド(default export)のコード量がとても多く視認性に良くないため現在のコードを切り分けてモジュールを読み込むようにリファクタリングしていこうと思います

コードを切り分ける(リファクタリング)

まずはedit.jsを編集していきます

edit.js

import {
  TextareaControl,
  TextControl,
  Button,
} from "@wordpress/components";
import { Fragment, useEffect } from "@wordpress/element";
import { getMode } from "./components/getMode";
import { getPreview } from "../views/getPreview";
import { getInspector } from "./components/getInspector";
import "./editor.scss";

export default function Edit(props) {
  const { className, attributes, setAttributes } = props;

  // ▼管理画面上でcode−prettifyを実行する関数を登録するフック
  useEffect(() => {
    PR.prettyPrint();
  }, [attributes.isEditMode]);

  //▼テキストエリア(TextareaControl)の行数
  let codeAreaRows =
    attributes.codeArea.split(/\r|\r\n|\n/).length > 3
      ? attributes.codeArea.split(/\r|\r\n|\n/).length
      : 3;

  // returnを組み立てる
  return [
    getMode(props),
    getInspector(props),
    <Fragment>
      {attributes.isEditMode && [
        <div className={className}>
          <TextControl
            label="File Name"
            type="string"
            className="filename"
            value={attributes.fileName}
            onChange={(val) => setAttributes({ fileName: val })}
          />
          <TextareaControl
            placeholder="Code..."
            value={attributes.codeArea}
            onChange={(code) => setAttributes({ codeArea: code })}
            rows={codeAreaRows}
          />
        </div>,
      ]}
      {!attributes.isEditMode && [
        <Button
          onClick={() => setAttributes({ isEditMode: true })}
          isLink
          icon="edit"
        >
          編集モード
        </Button>,
        getPreview(props),
      ]}
    </Fragment>,
  ];
} //export default function Edit(props)

主な変更点は以下です

//それぞれの関数を定義している場所へ引数を渡す
//更にそれを定義しているメソッドはこのファイルでは削除しておきましょう
getMode(props),
getInspector(props),
getPreview(props),

続いてこれらの関数を外からimportできるようにしておきましょう

import { getMode } from "./components/getMode";
import { getPreview } from "../views/getPreview";
import { getInspector } from "./components/getInspector";

それではここで定義したモジュールを別ファイルにしてきます

getMode()モジュールを作成

これはプレビューモードを判定してその真偽値を返す関数ですが
edit.jsのメソッドをコピーしてそれを書き換えます

また新たにsrcディレクトリにcomponentsフォルダを作成してその中にファイルを作成します

components/getMode.js

import { BlockControls } from "@wordpress/block-editor";
import { Toolbar, Button } from "@wordpress/components";
// ▼プレビューボタンの判定関数
export const getMode = ({attributes, setAttributes}) => {
  return (
    <BlockControls>
      <Toolbar>
        <Button
          //属性 isEditMode の値により表示するラベルを切り替え
          label={attributes["isEditMode"] ? "Preview" : "Edit"}
          //属性 isEditMode の値により表示するアイコンを切り替え
          icon={attributes["isEditMode"] ? "format-image" : "edit"}
          className="preview-button"
          //setAttributes を使って属性の値を更新(真偽値を反転)
          onClick={() => setAttributes({ isEditMode: !attributes["isEditMode"] })}
        />
      </Toolbar>
    </BlockControls>
  );
};

変更点はまず export 宣言を行います

そして必要なモジュールのimportをトップレベルで宣言しておきます

また、引数としてpropsを渡していたのでこれを分割代入にて受け取ります

//  {}で引数を展開する
export const getMode = ({attributes, setAttributes})

またモジュールに切り分けることに際してattributesオブジェクトへのアクセス方法がドット記法ではうまく行かなかったためブラケット記法に変更しております
attributes["isEditMode"]のような記法ですね

これでgetMode()モジュールはできました
他のモジュールも同じ要領で移行していきましょう

getPreview()モジュールを作成

こちらも先のメソッドを習って書き換えていきます

ディレクトリは表示に関わるモジュールのため「views」ディレクトリを作成しました

./views/getPreview.js

//プレビューをレンダリングする関数
export const getPreview = ({ className, attributes}) => {
  const {
    codeArea,
    align,
    skin,
    fileName,
    maxWidthEnable,
    linenumStart,
    linenums,
    lang,
    fontFamily,
    maxWidth,
  } = attributes;
  // コードが入力されていない時
  if (codeArea === "") {
    return null;
  }

  // ブロックスタイル用クラス
  let addBlockClass  = "",
      addInlineStyle = {},
      addPreClass    = "",
      addFontFamily  = {};
  //配置
  if (align) {
    addBlockClass = " align" + align;
  }

  //デザインスキン
  if (skin) {
    addBlockClass += " " + skin;
  }

  //ファイル名が指定されていれば filename_wrapper クラスを追加
  if (fileName) {
    addBlockClass += " filename_wrapper";
  }

  // maxWidthEnable が true なら max-width をインラインスタイルで設定
  if (maxWidthEnable) {
    addInlineStyle = { maxWidth: maxWidth };
  }

  // linenums がtrueならクラスを追加
  if (linenums) {
    addPreClass = " linenums";
    // 行の開始番号が指定されていればその値をクラスに追加
    if (linenumStart !== 1) {
      addPreClass += ":" + linenumStart;
    }
  }

  //言語が指定されていればそのクラスを指定
  if (lang) {
    addPreClass += " lang-" + lang;
  }

  //書式設定があればインラインスタイルで指定
  if (fontFamily !== "default") {
    addFontFamily = { fontFamily: fontFamily };
  }

  //最終的なマークアップをレンダリング
  return (
    <div className={className + addBlockClass} style={addInlineStyle}>
      {attributes.fileName && (
        <p className="file_name">{attributes.fileName}</p>
      )}
      <pre className={"prettyprint" + addPreClass} style={addFontFamily}>
        {attributes.codeArea}
      </pre>
    </div>
  );
};

こちらのモジュールも先のメソッドと同じ要領で変更点としては分割代入を用いてattributesからプロパティを分かりやすく受け取りつつ、要所コピーしています

上のモジュールもここからのモジュールも参考ファイルは折りたたんでクリックで表示するようにしています
やっていることは「値の受け渡し」だけであるがゆえ、ご自身の考えで出来る方はやってみることが最善であり、ご自身のアウトプットとしての価値が高まります。
きっと私のやり方も最善ではなく、正直穴だらけでしょうから、、笑

次モジュールを作成していきます

getInspector()モジュールを作成

同じ要領で定義していきます

./components/getInspector.js

//BlockControls を追加でインポート
import { InspectorControls } from "@wordpress/block-editor";
//Button,Toolbar を追加でインポート
import {
  PanelBody,
  PanelRow,
  ToggleControl,
  SelectControl,
  TextControl,
  RangeControl,
  CheckboxControl,
} from "@wordpress/components";

//▼インスペクターを表示する関数
export const getInspector = ({ attributes, setAttributes }) => {
  return (
    <InspectorControls>
      <PanelBody title="シンタックスハイライト設定" initialOpen={true}>
        <PanelRow>
          <ToggleControl
            label={attributes.linenums ? "行番号(表示)" : "行番号(非表示)"}
            checked={attributes.linenums}
            onChange={(val) => setAttributes({ linenums: val })}
          />
        </PanelRow>
        {attributes.linenums && (
          <PanelRow>
            <TextControl
              label="開始する行番号"
              type="number"
              value={attributes.linenumStart}
              onChange={(val) => setAttributes({ linenumStart: parseInt(val) })}
            />
          </PanelRow>
        )}
        <PanelRow>
          <SelectControl
            label="ブロックの配置"
            value={attributes.align}
            options={[
              { label: "なし", value: "" },
              { label: "左寄せ", value: "left" },
              { label: "中央寄せ", value: "center" },
              { label: "右寄せ", value: "right" },
              { label: "幅広", value: "wide" },
            ]}
            onChange={(val) => setAttributes({ align: val })}
          />
        </PanelRow>
        <PanelRow>
          <CheckboxControl
            label="最大幅を指定"
            checked={attributes.maxWidthEnable}
            onChange={(val) => setAttributes({ maxWidthEnable: val })}
            help="※ インラインスタイルで設定します"
          />
        </PanelRow>
        {attributes.maxWidthEnable && (
          <PanelRow>
            <RangeControl
              label="最大幅設定"
              value={attributes.maxWidth}
              onChange={(val) => setAttributes({ maxWidth: parseInt(val) })}
              min={300}
              max={1800}
              step={10}
              help="最大幅を px で指定します"
            />
          </PanelRow>
        )}
        <PanelRow>
          <SelectControl
            label="言語選択"
            value={attributes.lang}
            options={[
              { label: "Default", value: "" },
              { label: "CSS", value: "css" },
              { label: "SQL", value: "sql" },
              { label: "PHP", value: "php" },
              { label: "JavaScript", value: "js" },
              { label: "SASS", value: "scss" },
              { label: "Bash", value: "bash" },
              { label: "Java", value: "java" },
              { label: "C++", value: "c++" },
            ]}
            help="※ 適切な言語を設定することで綺麗にハイライトされます"
            onChange={(val) => setAttributes({ lang: val })}
          />
        </PanelRow>
        <PanelRow>
          <SelectControl
            label="書式設定"
            value={attributes.fontFamily}
            options={[
              { label: "デフォルト", value: "default" },
              { label: "MS P明朝", value: "MS P明朝" },
              { label: "Menlo", value: "Menlo" },
              { label: "monospace", value: "monospace" },
              { label: "Courier", value: "Courier" },
              { label: "Ricty Diminished", value: "Ricty Diminished" },
            ]}
            onChange={(val) => setAttributes({ fontFamily: val })}
          />
        </PanelRow>
        <PanelRow>
          <SelectControl
            label="デザインスキン"
            value={attributes.skin}
            options={[
              { label: "Basic", value: "" },
              { label: "Desert", value: "desert" },
              { label: "Doxy", value: "doxy" },
              { label: "Sons-of-obsidian", value: "sons-of-obsidian" },
              { label: "Sunburst", value: "sunburst" },
            ]}
            onChange={(val) => setAttributes({ skin: val })}
          />
        </PanelRow>
      </PanelBody>
    </InspectorControls>
  );
};

ここまで記述できて、通常通り動作していればリファクタリングは問題ありません

次からは今回のアップデートの追加内容を追記していきます

RadioContorolを追加

まずやることとしてはラジオボタンで選択肢をブロック上に表示してその値を先に設定しておいた属性値へと紐付けるようにします

TextControlTextareaControlの間に記述していきます

edit.js

<RadioControl
  className="fileShowMode"
  label="Codeの表示方法"
  selected={attributes.codeMode}
  options={[
    { label: "全文表示", value: "code-all" },
    { label: "全文省略(ClickでOPEN)", value: "code-onclick" },
    { label: "行数指定", value: "code-excerpt" },
  ]}
  onChange={(val) => setAttributes({ codeMode: val })}
/>;

ここではシンプルにclassNameを指定しわかりやすい文字列をコンポーネントの値として設定します

ここで指定したvalue値をそれぞれクラスなどで割り振って設定することで要件としては実装できそうですが、
やりたいことの詳細としては

ファイル名をクリックしてもらいアコーディオンが開くということは?
> ファイル名が未入力の場合エラーメッセージを出す
行数を指定するinputコンポーネントを出力するということは?
> 行数指定の選択肢以外の場合は不要なので表示しないようにする

上記の内容で進めていきます

ファイル名の有無を判定するコンポーネント

ここではファイル名が入力されていない場合にエラーメッセージを出すコンポーネントを簡単に作成します
editのreturnメソッドの中で&&演算子を利用して記述することも可能ですがメソッドの視認性が低下するため分けていきます

edit.js

const RequiredFileName = () => {
  if (attributes.codeMode === "code-onclick" && attributes.fileName === "") {
    return <p className="filename-err">※表示方法が全文省略の場合必須です</p>;
  }
  return null;
};

わかりやすさでif文にしましたが3項演算でも問題ありません
同じ要領で行数を入力するコンポーネントも作成していきます

行数を入力するコンポーネント

edit.js

const InputRowNumber = () => {
  if (attributes.codeMode === "code-excerpt") {
    return (
      <TextControl
        label="表示する行数を設定"
        type="number"
        className="row-number"
        value={attributes.excerptLength}
        onChange={(val) => setAttributes({ excerptLength: val })}
      />
    );
  }
  return null;
};

WordPressのコンポーネントではInputControlという入力用のコンポーネントも確認できましたがこちらは最終的に今回使用したTextControlに統合される予定のようでimportの視認性も悪くなるため単純にコード量が減るためこちらを選択しました

typeプロパティをnumberにすることで数値のinputとし、excerptLengthへ値を保存するようにしています

renderメソッドを変更する

./views/oja_code_render.php

<?php//preタグに追加するクラス
$add_pre_class = '';
//Coddfileの表示モード設定
if($attributes['codeMode'] === "code-excerpt") {
  $add_block_class .= " code-excerpt";
  $add_pre_style .= ' style="-webkit-line-clamp: '.$attributes['excerptLength'].'"';
} elseif ($attributes['codeMode'] === "code-onclick") {
  $add_block_class .= " code-onclick";
  $add_pre_class = ' accordion__content';
}
。。。。
// ※省略
。。。。
//書式設定があればインラインスタイルで指定
  if($attributes['fontFamily'] !== 'default' || $attributes['codeMode'] === "code-excerpt") {
    $add_pre_style .= ' style="font-family: '. $attributes['fontFamily']. '"';
    $output .= '<pre class="prettyprint'  . $add_pre_class . '"' . $add_pre_style . '>';
  } else {
    $output .= '<pre class="prettyprint' . $add_pre_class . '">';
  }

ここでは上で設定した属性値を使用して、それぞれにクラス名を設定してしていきます

さてここまで実装できたらラジオボタンでそれぞれ適切な表示させつつ、クラス名の追加がフロントエンドで確認できるはずなので見てみましょう

機能面の確認

ここまでで、必要な機能としての実装は完了です

一通りの機能として実装できているかの確認を行います

  • ラジオボタンを操作した際に適切なコンポーネントを出力できているか?
  • 「全文省略」の際にファイル名未入力の時にエラーメッセージが出るか?
  • フロントエンドで適切なクラス名が付与できているか?

ここまで記述したファイルの全文を載せておきます
クリックで確認できます

edit.js

import {
  TextareaControl,
  TextControl,
  Button,
  RadioControl,
} from "@wordpress/components";
import { Fragment, useEffect } from "@wordpress/element";
import { getMode } from "./components/getMode";
import { getPreview } from "../views/getPreview";
import { getInspector } from "./components/getInspector";
import "./editor.scss";

export default function Edit(props) {
  const { className, attributes, setAttributes } = props;

  // ▼管理画面上でcode−prettifyを実行する関数を登録するフック
  useEffect(() => {
    PR.prettyPrint();
  }, [attributes.isEditMode]);

  //▼テキストエリア(TextareaControl)の行数
  let codeAreaRows =
    attributes.codeArea.split(/\r|\r\n|\n/).length > 3
      ? attributes.codeArea.split(/\r|\r\n|\n/).length
      : 3;

  const RequiredFileName = () => {
    if(attributes.codeMode === "code-onclick" && attributes.fileName === "" ){
      return <p className="filename-err">※表示方法が全文省略の場合必須です</p>;
    }
    return null;
  }
  const InputRowNumber = () => {
    if(attributes.codeMode === "code-excerpt"){
      return(
        <TextControl
        label="表示する行数を設定"
        type="number"
        className="row-number"
        value={attributes.excerptLength}
        onChange={(val) => setAttributes({ excerptLength: val })}
        />
      );
    }
    return null;
  }
  // returnを組み立てる
  return [
    getMode(props),
    getInspector(props),
    <Fragment>
      {attributes.isEditMode && [
        <div className={className}>
          <RequiredFileName />
          <TextControl
            label="File Name"
            type="string"
            className="filename"
            value={attributes.fileName}
            onChange={(val) => setAttributes({ fileName: val })}
          />
          <RadioControl
            className="fileShowMode"
            label="Codeの表示方法"
            selected={attributes.codeMode}
            options={[
              { label: "全文表示", value: "code-all" },
              { label: "全文省略(ClickでOPEN)", value: "code-onclick" },
              { label: "行数指定", value: "code-excerpt" },
            ]}
            onChange={(val) => setAttributes({ codeMode: val })}
          />
          <InputRowNumber />
          <TextareaControl
            placeholder="Code..."
            value={attributes.codeArea}
            onChange={(code) => setAttributes({ codeArea: code })}
            rows={codeAreaRows}
          />
        </div>,
      ]}
      {!attributes.isEditMode && [
        <Button
          onClick={() => setAttributes({ isEditMode: true })}
          isLink
          icon="edit"
        >
          編集モード
        </Button>,
        getPreview(props),
      ]}
    </Fragment>,
  ];
} //export default function Edit(props)

getInspector.js

//BlockControls を追加でインポート
import { InspectorControls } from "@wordpress/block-editor";
//Button,Toolbar を追加でインポート
import {
  PanelBody,
  PanelRow,
  ToggleControl,
  SelectControl,
  TextControl,
  RangeControl,
  CheckboxControl,
} from "@wordpress/components";

//▼インスペクターを表示する関数
export const getInspector = ({ attributes, setAttributes }) => {
  return (
    <InspectorControls>
      <PanelBody title="シンタックスハイライト設定" initialOpen={true}>
        <PanelRow>
          <ToggleControl
            label={attributes.linenums ? "行番号(表示)" : "行番号(非表示)"}
            checked={attributes.linenums}
            onChange={(val) => setAttributes({ linenums: val })}
          />
        </PanelRow>
        {attributes.linenums && (
          <PanelRow>
            <TextControl
              label="開始する行番号"
              type="number"
              value={attributes.linenumStart}
              onChange={(val) => setAttributes({ linenumStart: parseInt(val) })}
            />
          </PanelRow>
        )}
        <PanelRow>
          <SelectControl
            label="ブロックの配置"
            value={attributes.align}
            options={[
              { label: "なし", value: "" },
              { label: "左寄せ", value: "left" },
              { label: "中央寄せ", value: "center" },
              { label: "右寄せ", value: "right" },
              { label: "幅広", value: "wide" },
            ]}
            onChange={(val) => setAttributes({ align: val })}
          />
        </PanelRow>
        <PanelRow>
          <CheckboxControl
            label="最大幅を指定"
            checked={attributes.maxWidthEnable}
            onChange={(val) => setAttributes({ maxWidthEnable: val })}
            help="※ インラインスタイルで設定します"
          />
        </PanelRow>
        {attributes.maxWidthEnable && (
          <PanelRow>
            <RangeControl
              label="最大幅設定"
              value={attributes.maxWidth}
              onChange={(val) => setAttributes({ maxWidth: parseInt(val) })}
              min={300}
              max={1800}
              step={10}
              help="最大幅を px で指定します"
            />
          </PanelRow>
        )}
        <PanelRow>
          <SelectControl
            label="言語選択"
            value={attributes.lang}
            options={[
              { label: "Default", value: "" },
              { label: "CSS", value: "css" },
              { label: "SQL", value: "sql" },
              { label: "PHP", value: "php" },
              { label: "JavaScript", value: "js" },
              { label: "SASS", value: "scss" },
              { label: "Bash", value: "bash" },
              { label: "Java", value: "java" },
              { label: "C++", value: "c++" },
            ]}
            help="※ 適切な言語を設定することで綺麗にハイライトされます"
            onChange={(val) => setAttributes({ lang: val })}
          />
        </PanelRow>
        <PanelRow>
          <SelectControl
            label="書式設定"
            value={attributes.fontFamily}
            options={[
              { label: "デフォルト", value: "default" },
              { label: "MS P明朝", value: "MS P明朝" },
              { label: "Menlo", value: "Menlo" },
              { label: "monospace", value: "monospace" },
              { label: "Courier", value: "Courier" },
              { label: "Ricty Diminished", value: "Ricty Diminished" },
            ]}
            onChange={(val) => setAttributes({ fontFamily: val })}
          />
        </PanelRow>
        <PanelRow>
          <SelectControl
            label="デザインスキン"
            value={attributes.skin}
            options={[
              { label: "Basic", value: "" },
              { label: "Desert", value: "desert" },
              { label: "Doxy", value: "doxy" },
              { label: "Sons-of-obsidian", value: "sons-of-obsidian" },
              { label: "Sunburst", value: "sunburst" },
            ]}
            onChange={(val) => setAttributes({ skin: val })}
          />
        </PanelRow>
      </PanelBody>
    </InspectorControls>
  );
};

getMode.js

import { BlockControls } from "@wordpress/block-editor";
import { Toolbar, Button } from "@wordpress/components";
// ▼プレビューボタンの判定関数
export const getMode = ({attributes, setAttributes}) => {
  return (
    <BlockControls>
      <Toolbar>
        <Button
          //属性 isEditMode の値により表示するラベルを切り替え
          label={attributes["isEditMode"] ? "Preview" : "Edit"}
          //属性 isEditMode の値により表示するアイコンを切り替え
          icon={attributes["isEditMode"] ? "format-image" : "edit"}
          className="preview-button"
          //setAttributes を使って属性の値を更新(真偽値を反転)
          onClick={() => setAttributes({ isEditMode: !attributes["isEditMode"] })}
        />
      </Toolbar>
    </BlockControls>
  );
};

getPreview.js

//プレビューをレンダリングする関数
export const getPreview = ({ className, attributes}) => {
  const {
    codeArea,
    align,
    skin,
    fileName,
    maxWidthEnable,
    linenumStart,
    linenums,
    lang,
    fontFamily,
    maxWidth,
  } = attributes;
  // コードが入力されていない時
  if (codeArea === "") {
    return null;
  }

  // ブロックスタイル用クラス
  let addBlockClass  = "",
      addInlineStyle = {},
      addPreClass    = "",
      addFontFamily  = {};
  //配置
  if (align) {
    addBlockClass = " align" + align;
  }

  //デザインスキン
  if (skin) {
    addBlockClass += " " + skin;
  }

  //ファイル名が指定されていれば filename_wrapper クラスを追加
  if (fileName) {
    addBlockClass += " filename_wrapper";
  }

  // maxWidthEnable が true なら max-width をインラインスタイルで設定
  if (maxWidthEnable) {
    addInlineStyle = { maxWidth: maxWidth };
  }

  // linenums がtrueならクラスを追加
  if (linenums) {
    addPreClass = " linenums";
    // 行の開始番号が指定されていればその値をクラスに追加
    if (linenumStart !== 1) {
      addPreClass += ":" + linenumStart;
    }
  }

  //言語が指定されていればそのクラスを指定
  if (lang) {
    addPreClass += " lang-" + lang;
  }

  //書式設定があればインラインスタイルで指定
  if (fontFamily !== "default") {
    addFontFamily = { fontFamily: fontFamily };
  }

  //最終的なマークアップをレンダリング
  return (
    <div className={className + addBlockClass} style={addInlineStyle}>
      {attributes.fileName && (
        <p className="file_name">{attributes.fileName}</p>
      )}
      <pre className={"prettyprint" + addPreClass} style={addFontFamily}>
        {attributes.codeArea}
      </pre>
    </div>
  );
};

oja_code_render.php

<?php
// render_callback 関数の定義
function oja_code_render($attributes) {
  //属性 codeArea が空なら何も表示しない
  if (empty($attributes['codeArea'])) {
    return '';
  }

  //ブロックスタイル用クラス
  $add_block_class  = '';
  $add_inline_style = '';
  $add_pre_style  = '';

  //配置
  if ($attributes['align']) {
    $add_block_class = ' align'. $attributes['align'];
  }

  //デザインスキン
  if ($attributes['skin']) {
    $add_block_class .= ' '.$attributes['skin'];
  }

  //ファイル名が指定されていれば filename_wrapper クラスを追加
  if($attributes['fileName']) {
    $add_block_class .= ' filename_wrapper';
  }

  // maxWidthEnable が true なら max-width をインラインスタイルで設定
  if ($attributes['maxWidthEnable']) {
    $add_inline_style = ' style="max-width: '. $attributes['maxWidth']. 'px"';
  }

  //preタグに追加するクラス
  $add_pre_class = '';
  //Coddfileの表示モード設定
  if($attributes['codeMode'] === "code-excerpt") {
    $add_block_class .= " code-excerpt";
    $add_pre_style .= ' style="-webkit-line-clamp: '.$attributes['excerptLength'].'"';
  } elseif ($attributes['codeMode'] === "code-onclick") {
    $add_block_class .= " code-onclick";
    $add_pre_class = ' accordion__content';
  }

  // ブロックの div 要素に追加のクラスとスタイルを指定
  $output = '<div class="wp-block-oja-code-gutenberg'. $add_block_class. '"'. $add_inline_style. '>';

  //ファイル名の要素を格納する変数
  $file_name = '';
  //ファイル名が指定されていればファイル名を p 要素で出力
  if($attributes['fileName']) {
    $file_name = '<p class="file_name accordion__title">' . esc_html($attributes['fileName']) . '</p>';
    $output .= $file_name;
  }

  // linenums がtrueならクラスを追加
  if($attributes['linenums']) {
    $add_pre_class .= ' linenums';
    // 行の開始番号が指定されていればその値をクラスに追加
    if($attributes['linenumStart'] !== 1 ) {
      $add_pre_class .= ':' . $attributes['linenumStart'];
    }
  }

  // 言語が指定されていればそのクラス(lang-xxxx)を設定
  if ($attributes['lang']) {
    $add_pre_class .= ' lang-' . ($attributes['lang']);
  }

  //書式設定があればインラインスタイルで指定
  if($attributes['fontFamily'] !== 'default' || $attributes['codeMode'] === "code-excerpt") {
    $add_pre_style .= ' style="font-family: '. $attributes['fontFamily']. '"';
    $output .= '<pre class="prettyprint'  . $add_pre_class . '"' . $add_pre_style . '>';
  } else {
    $output .= '<pre class="prettyprint' . $add_pre_class . '">';
  }
  //入力された値を esc_html() でエスケープ処理して出力
  $output .= esc_html($attributes['codeArea']).'</pre></div>';

  return $output;
}

とこんな感じにしてみました

機能面の確認が取れたらこれからのTODOです。

付与したクラスに指定するCSSを設定し、更にアコーディオンやコピーボタンのJSファイルを作成しブロックに動きを付けたいと思います

機能追加用JSファイル

ここでは「全文省略」設定の場合のJS関数(クラス)を定義して読み込みます
概要としては「ファイル名」を入力必須にしたのでこちらをアコーディオンのトリガーとして、
その下のコンテンツのCSS、displayプロパティを操作するような感じです

読み込むファイルを追加

今回はプラグインディレクトリ直下に「js」ディレクトリを作成しその中で「main.js」として作成しました
メインのプラグインPHPファイルでこれを読み込むようにしていきます

oja-code-gutenberg.php

//アコーディオン用のオリジナルJS
if(! is_admin()) {
  wp_enqueue_script(
    'code_gutenberg_script',
    plugins_url( '/js/main.js', __FILE__ ),
    array(),
    filemtime( "$dir/js/main.js" ),
    true
  );
}

それではこちらのファイルを編集していきます

アコーディオンclass

.js/main.js


class SlideContent {
  constructor(obj) {
    // アコーディオンを全て取得
    const accordions = document.querySelectorAll(obj.accordeContainer);
    // 取得したアコーディオンをArrayに変換(IE対策)
    const accordionsArr = Array.prototype.slice.call(accordions);

    accordionsArr.forEach((accordion) => {
      // Triggerを全て取得
      const accordionTriggers = accordion.querySelectorAll(obj.triggerEl);
      // TriggerをArrayに変換(IE対策)
      const accordionTriggersArr =
        Array.prototype.slice.call(accordionTriggers);
      accordionTriggersArr.forEach((trigger) => {
        // Triggerにクリックイベントを付与
        trigger.addEventListener("click", () => {
          // '.is-active'クラスを付与or削除
          trigger.classList.toggle("is-active");
          // 開閉させる要素を取得
          const content = trigger.nextElementSibling;
          // 要素を展開or閉じる
          this.slideToggle(content);
        });
      });
    });
  }

  // 要素をスライドしながら非表示にする関数(jQueryのslideUpと同じ)
  slideUp(el, duration = 300) {
    el.style.height = el.offsetHeight + "px";
    el.offsetHeight;
    el.style.transitionProperty = "height, margin, padding";
    el.style.transitionDuration = duration + "ms";
    el.style.transitionTimingFunction = "ease";
    el.style.overflow = "hidden";
    el.style.height = 0;
    el.style.paddingTop = 0;
    el.style.paddingBottom = 0;
    el.style.marginTop = 0;
    el.style.marginBottom = 0;
    setTimeout(() => {
      el.style.display = "none";
      el.style.removeProperty("height");
      el.style.removeProperty("padding-top");
      el.style.removeProperty("padding-bottom");
      el.style.removeProperty("margin-top");
      el.style.removeProperty("margin-bottom");
      el.style.removeProperty("overflow");
      el.style.removeProperty("transition-duration");
      el.style.removeProperty("transition-property");
      el.style.removeProperty("transition-timing-function");
      el.classList.remove("is-open");
    }, duration);
  }

  // 要素をスライドしながら表示する関数(jQueryのslideDownと同じ)
  slideDown(el, duration = 300) {
    el.classList.add("is-open");
    el.style.removeProperty("display");
    let display = window.getComputedStyle(el).display;
    if (display === "none") {
      display = "block";
    }
    el.style.display = display;
    let height = el.offsetHeight;
    el.style.overflow = "hidden";
    el.style.height = 0;
    el.style.paddingTop = 0;
    el.style.paddingBottom = 0;
    el.style.marginTop = 0;
    el.style.marginBottom = 0;
    el.offsetHeight;
    el.style.transitionProperty = "height, margin, padding";
    el.style.transitionDuration = duration + "ms";
    el.style.transitionTimingFunction = "ease";
    el.style.height = height + "px";
    el.style.removeProperty("padding-top");
    el.style.removeProperty("padding-bottom");
    el.style.removeProperty("margin-top");
    el.style.removeProperty("margin-bottom");
    setTimeout(() => {
      el.style.removeProperty("height");
      el.style.removeProperty("overflow");
      el.style.removeProperty("transition-duration");
      el.style.removeProperty("transition-property");
      el.style.removeProperty("transition-timing-function");
    }, duration);
  }

  // 要素をスライドしながら交互に表示/非表示にする関数(jQueryのslideToggleと同じ)
  slideToggle(el, duration = 300) {
    if (window.getComputedStyle(el).display === "none") {
      return this.slideDown(el, duration);
    } else {
      return this.slideUp(el, duration);
    }
  }
}

const codeAccordion = new SlideContent({
  accordeContainer: ".code-onclick",
  triggerEl: ".accordion__title",
});

簡単に説明すると.accordion__titleクラス名の次にある要素のCSSプロパティdisplayをJSで取得、判定してスライド用のメソッドを出し分ける関数をコンストラクターで実行しています
javascriptはClassにしておかないと他のプラグインとの競合で思わぬエラーになったりするためこのようにしました

classのコンストラクターの中でアコーディオンのコンテナタグとそのアコーディオンのトリガーとなるクラス名を指定するため、忘れずにインスタンス化する際にオブジェクトとして定義しておきましょう

accordeContainer: ".code-onclick",
triggerEl: ".accordion__title",

あとはこれらのクラス名の要素用意すればOKです

コピーボタン用のClass

./js/main.js

// コードのクリップボードコピー関数
class CopyAndPaste {
  constructor(obj) {
    this.copyBtn = obj.copyBtn;
    this.copy = obj.copyEl;

    //コピーのイベントリスナー
    if(this.copyBtn && this.copy ) {
      let copyButton = document.querySelectorAll(this.copyBtn);
      let texts = document.querySelectorAll(this.copy);
      for (let i = 0; i < copyButton.length; i++) {
        copyButton[i].addEventListener("click", () => {
          this.copyToClipboard(i)}, false);
      }
    }
  }

  copyToClipboard = (i) => {
    let texts = document.querySelectorAll(this.copy);
    if (navigator.clipboard) {
      navigator.clipboard.writeText(texts[i].innerText).then(
        (success) => alert("テキストのコピーに成功😆"),
        (error) => alert("テキストのコピーに失敗😫")
      );
    } else {
      alert("このブラウザは対応していません");
    }
  };

} //class CopyAndPaste

const codeCopy = new CopyAndPaste({
  copyBtn: ".copyButton",
  copyEl: ".copyCode",
});

同じ要領でテキストをコピーするためのClassを定義します

こちらもオブジェクトの値としてコンストラクターを実行しているため、忘れずにクラス名として設定&インスタンス化しましょう

copyBtn: ".copyButton",
copyEl: ".copyCode",

コピーするメソッドについて

今まだ「JS テキストコピー」などで検索すると下記のようなメソッドがヒットします

Document.execCommand('copy') 

しかし上記のメソッドは2022年現在非推奨となっているため、

navigator.clipboard.writeText

こちらのメソッドを使用しました

writeTextは引数の値をクリップボードにコピーするメソッドで、返り値はプロミスであるためthenチェーンで繋いでいます

コピーボタンの追加

ここまで追加したクラス名を持つ要素をレンダリング関数に追記していきます

./view/oja_code_render.php

$output .= '<pre class="prettyprint copyCode'  . $add_pre_class . '"' . $add_pre_style . '><button class="copyButton">コピーする</button>';

表示されたボタンをクリックして「コピーされました」のメッセージが表示される

ファイル名をクリックしてクラス名が追加されることを開発者ツールで確認しておきましょう

CSSを整える

ここまででアコーディオン用、コピー用の関数も出来たので、最終的なスタイルを整えていきましょう

style.scss

.wp-block-oja-code-gutenberg  {
  position: relative;
  //配置指定設定
  &.alignleft {
    float: left;
    margin-right: 3%;
  }
  &.aligncenter {
    clear: both;
    margin: 0 auto;
  }
  &.alignright {
    float: right;
    margin-left: 3%;
  }
  //行数指定設定
  &.code-excerpt pre {
    display: -webkit-box !important;
    padding-bottom: 0;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
  //アコーディオンCSS
  &.code-onclick {
    .accordion__title {
      cursor: pointer;
    }
    .accordion__content {
      display: none;
      cursor: pointer;
    }
    .accordion__content.is-open {
      display: inline-block;
    }
  } //.code-onclick
}

.wp-block-oja-code-gutenberg {
  .copyButton {
    position: absolute;
    right: 0%;
    top: 0%;
    font-size: 12px;
    margin: 0;
    border: none;
    background: #ecf275;
    border-radius: 2px;
    &:before {
      content: "\f105";
      font-family: "dashicons";
      color: #7270e6;
      font-size: 14px;
    }
    span {
      color: #7270e6;
    }
  }
}

またラジオボタンやエラーメッセージの見た目もわかりやすくキレイにしておきましょう

editor.scss


.fileShowMode {
  label {
    margin-right: 4%;
    margin-top: 1%;
  }
  .components-radio-control__option {
    display: inline;
  }
}

p.filename-err {
  font-size: 13px;
  line-height: 1.5;
  font-weight: 300;
  margin-bottom: 10px;
  margin-top: 20px;
  padding: 0;
  color: red;
  font-family: "Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
}

.row-number input {
  width: 100px;
  display: inline-block;
  margin-left: 15px;
}

まとめ

ここまで前回作成したコードブロックに機能改善としてアップデートを行ってきました

もとは大変有益なサイトの記事を参考にして作成したコードブロックでしたが、こうして仕組みを理解して自分で作り変えることでどんどんとオリジナルブロックとして機能すればよいのかと思います

ですからあえてコードの全文は載せずにあくまで自身の制作物のアウトプットとしておきます

コメントをお待ちしております

お気軽にコメントをどうぞ。

CAPTCHA