お知らせ

現在サイトのリニューアル作業中のため、全体的にページの表示が乱れています。
投稿日:
言語::HTML言語::CSS

昔のサイトには以下のように、更新履歴を書く用途でtextarea要素が使われていたことがよくあったと思う。

<TEXTAREA>== 2024/01/15 ==
ギャラリーにイラストを一点追加

== 2024/01/01 ==
トップページを更新</TEXTAREA>

しかし、これをLighthouseで見るとアクセシビリティ違反になることがある(labelがないとか言われる)。label要素を使うのも一つの手だが、使わずやる場合にどう回避するかというのを紹介する。

以下は実装の一例だ。

<div style="resize: vertical; border: 1px solid #ccc; overflow-y: scroll; height: 5rem; min-height: 5rem;"><small><pre>== 2024/01/15 ==
ギャラリーにイラストを一点追加

== 2024/01/01 ==
トップページを更新</pre></small></div>

描画サンプルとしては以下のような形になる。

textareaみたいなUI

内容的にはよくあるoverflow: scrollなコンテナだが、ポイントは"resize: vertical;height: 5rem; min-height: 5rem;だ。"resize: vertical;によってtextareaの様に拡縮可能なUIを提供できるようにしている。height: 5rem; min-height: 5rem;は標準の高さと最低の高さを両方指定することで、UI縮小時にUIが潰れてしまうのを防いでいる。

そもそもlabelがあった方が見やすいし、何かが分かりやすいというのはそうなのだが、なんか中二病みたいなレイアウトにしたいとか、そもそもフォームではなく、単なる表示枠なのでからlabelを使いたくないとかいうケースで有用になるだろう。

そういやSSI使ったことなかったなと思ったので。

やり方

  1. .shtmlを作る
  2. CGI・PHP・SSI を利用したい | さくらのサポート情報の書式を見てコードを書く

任意の処理結果を出させる例

現在の年を出す場合のやり方

  1. 以下の内容でhoge.plを作成

    #!/usr/local/bin/perl
    
     print qx(date "+%Y");
    
  2. chmod 705 hoge.pl
  3. 以下の内容でpiyo.shtmlを作成
    <!--#exec cmd="./hoge.pl"-->
    

注意点

同一パスにあるコマンドしか実行できないため、以下のような内容は動かない

グローバルなコマンドは動かない

<!--#exec cmd="date +%Y"-->

こんなことをしても動かない

<!--#echo VAR="date +%Y"-->

サブシェルを書いても動かない

<!--#echo VAR="$(date +%Y)"-->

参考

投稿日:
言語::JavaScript言語::HTML

adiaryの改造でやったのでメモがてら

環境

Env Ver
Microsoft Edge 120.0.2210.144
PHP 8.2.10

サンプルコード

FORM形式での送信

Web標準でサポートされているため実装が非常に容易。特に理由がなければこれでいい

HTMLコード

<textarea onpaste="handlePasteForm(event)"></textarea>

JSコード

fetch()のヘッダ指定がないが、 fetch()を使う場合はFormDataを使うと勝手に生えるので気にしなくていい。他にもXMLHttpRequest.send()Navigator.sendBeacon()でも生えるらしい。

今回試したEdgeではバウンダリー文字列もちゃんと生えていた。

/** @param {ClipboardEvent} e */
const handlePasteForm = (e) => {
  if (e.clipboardData.files.length) {
    const pasteFile = e.clipboardData.files[0];
    const form = new FormData();
    form.append('image', pasteFile);

    fetch('./test.php', {
      method: 'POST',
      body: form
    });
  } else {
    // 何もしない
  }
};

PHPコード

Content-Typemultipart/form-dataの時だけ中身が入ってくる
参考:https://www.php.net/manual/ja/reserved.variables.files.php

<?php
$file_name = $_FILES['image']['name'];
move_uploaded_file($_FILES['image']['tmp_name'], './'. $file_name);
>

$_FILESの中身はvar_dumpした限りこんな感じだった

array(1) {
  ["image"]=>
  array(6) {
    ["name"]=>
    string(9) "image.png"
    ["full_path"]=>
    string(9) "image.png"
    ["type"]=>
    string(9) "image/png"
    ["tmp_name"]=>
    string(29) "C:\env\msys64\tmp\php3B90.tmp"
    ["error"]=>
    int(0)
    ["size"]=>
    int(3988)
  }
}

JSON形式での送信

Web標準ではないので、これといったやり方もなく、正直面倒だが、どうしてもJSONで送らないといけないときに。

HTMLコード

<textarea onpaste="handlePasteJson(event)"></textarea>

JSコード

HTTPではバイナリを送ることができないため、Base64にデコードして送る。Base64デコードに含まれる情報はファイルバイナリのみ。もしFORM送信のようにファイル名も送りたければ、別途送ってやる必要がある。

/** @param {File} file */
const encodeBase64 = (file) => {
  return new Promise ((resolve, reject) => {
    const fr = new FileReader();

    fr.onload = () => {
      const data = fr.result.split(',')[1];
      resolve(fr.result.split(',')[1]);
    };

    fr.onerror = (ev) => {
      reject(ev);
    };

    fr.readAsDataURL(file);
  });
};

/** @param {ClipboardEvent} e */
const handlePasteJson = async (e) => {
  if (e.clipboardData.files.length) {
    const pasteFile = e.clipboardData.files[0];

    const base64File = await encodeBase64(pasteFile);

    fetch('./test.php', {
      method: 'POST',
      headers: {
        'content-type': 'application/json'
      },
      body: JSON.stringify({ image: base64File })
    });
  } else {
    // 何もしない
  }
};

PHPコード

JSONリクエストを取るときはfile_get_contents('php://input')を利用する。$_POSTContent-Typeapplication/x-www-form-urlencodedないしmultipart/form-data時しか取れないからだ。つまりFORMリクエスト以外はfile_get_contents('php://input')で取ると覚えておけばよいだろう。
参考:https://www.php.net/manual/ja/wrappers.php.php#wrappers.php.input

以下のコードはかなりいい加減なので余りアテにしてはいけない(特に拡張子取得の下りがガバガバすぎる)が、取り合えずpngかjpegかgifが送られてきた場合は動いてくれる筈だ。

<?php
// 連想配列にするため、json_decode()の第二引数をtrueにする(指定しないとstdClassになる
$json = json_decode(file_get_contents('php://input'), true);
// Base64エンコードを解除する
$file = base64_decode($json['image']);

// 一時ファイルに落とす
file_put_contents('json_upload', $file);
// 拡張子判別
$mime = mime_content_type('json_upload');
// 拡張子取得
$ext = explode('/', $mime);

// 拡張子付きにリネーム
rename('json_upload', 'image.'. $ext[1]);

参考

JSONでファイルを投げる方式での実装状況については以下の記事が参考になる。

WebAPI でファイルをアップロードする方法アレコレ #API - Qiita

一気通貫で動くサンプルコード

今までの実装が全部動かせるサンプル

<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  if(preg_match('/^multipart\/form-data;/', $_SERVER['CONTENT_TYPE'])) {
    // form
    $file_name = $_FILES['image']['name'];
    move_uploaded_file($_FILES['image']['tmp_name'], './'. $file_name);
  } else {
    // json
    $json = json_decode(file_get_contents('php://input'), true);
    $file = base64_decode($json['image']);

    file_put_contents('json_upload', $file);
    $mime = mime_content_type('json_upload');
    $ext = explode('/', $mime);

    rename('json_upload', 'image.'. $ext[1]);
  }
} else {
?>

<!DOCTYPE html>
<html>
<head>
  <meta charset='utf-8'>
  <meta http-equiv='X-UA-Compatible' content='IE=edge'>
  <title>Upload test</title>
  <meta name='viewport' content='width=device-width, initial-scale=1'>
  <script>
    /** @param {File} file */
    const encodeBase64 = (file) => {
      return new Promise ((resolve, reject) => {
        const fr = new FileReader();

        fr.onload = () => {
          const data = fr.result.split(',')[1];
          resolve(fr.result.split(',')[1]);
        };

        fr.onerror = (ev) => {
          reject(ev);
        };

        fr.readAsDataURL(file);
      });
    };

    /** @param {ClipboardEvent} e */
    const handlePasteForm = (e) => {
      if (e.clipboardData.files.length) {
        const pasteFile = e.clipboardData.files[0];
        const form = new FormData();
        form.append('image', pasteFile);

        fetch('./test.php', {
          method: 'POST',
          body: form
        });
      } else {
        // 何もしない
      }
    };

    /** @param {ClipboardEvent} e */
    const handlePasteJson = async (e) => {
      if (e.clipboardData.files.length) {
        const pasteFile = e.clipboardData.files[0];

        const base64File = await encodeBase64(pasteFile);

        fetch('./test.php', {
          method: 'POST',
          headers: {
            'content-type': 'application/json'
          },
          body: JSON.stringify({ image: base64File })
        });
      } else {
        // 何もしない
      }
    };
  </script>
</head>
<body>
  <div>Form:<textarea onpaste="handlePasteForm(event)"></textarea></div>
  <div>Json:<textarea onpaste="handlePasteJson(event)"></textarea></div>
</body>
</html>

<?php
}
?>

投稿日:
言語::HTML言語::CSS

Bioサイトを作っていた時にimgタグの下に謎の隙間ができることに気がついたので、その対処録。

問題

画像の下に謎の隙間があり、DevToolsで見るとaタグがはみ出ているように見える。marginpadding0にしても効果なし。

imgタグの下に隙間がある

aタグがimgタグの下にはみ出ているように見える

aタグを外して、imgタグ単体にしても起きる。

aタグを外し、imgタグ単体にしても起きる

再現コード例

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>lycolia.info</title>
  </head>
  <body>
    <a href="https://example.com">
      <img src="example.png" width="200" height="40" alt="example">
    </a>
  </body>
</html>

原因

imgタグはinline要素なので vertical-align の既定値が baseline になっている関係で、文字の下端と画像の下端が合う状態になっているために、隙間が生まれていることが確認できる。

文字の下端と画像の下端が合う状態になっている

解決策

imgタグに対してvertical-alignbottomないしtopを指定すれば解消する。

vertical-alignaを指定したときの例、下の隙間がなくなっている

勿論この状態で横に文字を並べると、その文字は当然浮くが、今回はこの解決方法は扱わない。

横に文字を並べた例、空白分文字がずれている

参考コード

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>lycolia.info</title>
  </head>
  <body>
    <a href="https://example.com">
      <img style="vertical-align: bottom;" src="example.png" width="200" height="40" alt="example">
    </a>
  </body>
</html>
投稿日:
言語::HTML言語::JavaScript
  • 素のHTMLにJSを埋め込んでイベントコールバックの引数を取る方法
    • イベントコールバックの引数にeventを指定する
      • 例:onclick="test(event)"

サンプルコード

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <title>Example of DOM Event callback arguments</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <script>
      const test = (ev) => {
        console.log(ev);
      };
    </script>
  </head>
  <body>
    <p onclick="test(event)">aaa</p>
  </body>
</html>