20200516のHTMLに関する記事は12件です。

WebページにTwitterのタイムラインを表示させる

概要

 Webアプリを作りたいと思い勉強し始めました。様々なWebアプリを参考にしていると目に入るのが「Twitterのタイムライン」でした。

 どうやってTwitterのタイムラインを埋め込むかを調査した備忘録を残します。最終的に次のようにできました。(今回はTwitterのAPIを使用せずに実装しています。APIを使用するほうが一般的?)

image.png

開発環境

  • Webブラウザ : Google Chrome
  • Twitter

作ってみる

埋め込むURLを入力

Twitter Publishにアクセス。

次のような画面が出るので、TwitterのアカウントURLを入力する。
私の場合は次のURLを入力します。

https://twitter.com/kmaepu

image.png

ウィジットタイプの選択

画面を下にスクロールし、ウィジットのタイプを選びます。左側の「Embedded Timeline」を選びます。
image.png

参考にこのまま使用した場合のWeb画面は次のようになります。
image.png

ちなみに、
右側の「Twitter Buttons」を選択すると、次のようにフォローかメンションのボタンを埋め込むことができます。
image.png

ウィジットのオプション設定

このまま進むと、画面いっぱいにタイムラインが表示されてしまうので整形します。ウィジット選択画面の下の「set customization options」を選択します。
2.png

タイムライン型ウィジットの場合、次のような設定画面が表示されます。
ウィジットの
- 高さ(px)
- 幅(px)
- デザイン(白 or 黒)
- 表示基本言語

を入力します。設定したら右下の「Update」ボタンを押します。

image.png

HTMLのコードをコピー

次のようにHTMKのコードが表れうので、右側の「Copy Code」をおすと。コードをコピーできます。あとは使用したいhtmkファイルにペーストするだけです。
image.png

完成!

image.png

おわりに

ウィジットを埋め込むだけなので、コードを書かずに実現できて便利でした。余談ですが、趣味で水耕栽培や家庭菜園をしておりその様子をTwitterでつぶやいてます。

Follow @kmaepu

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Expressでcssとjavascriptを読み込む方法

1. ディレクトリ構成

Expressでプロジェクトを作成するとディレクトリは以下のような構成になります。

スクリーンショット 2020-05-16 22.33.17.png

詳細は今後に書くとして、cssとjavascriptをテンプレートエンジンへ適用させたい時は以下の手順を行ってください。

2. css作成

publicフォルダ直下のstylesheetsフォルダにcssファイルを作成してください。

3. javascript作成

publicフォルダ直下のjavascriptsフォルダにjsファイルを作成してください。
javascriptの場合はAPIの作成だったりルーティングの兼ね合いがありますので、
その点を考慮した実装としておいてください。

4.適用

viewsフォルダに作成した(プロジェクト作成時は1ファイルのみ作成されている)テンプレートエンジンのファイルに読み込みをさせていきます。

4-1. cssの場合

おなじみ、linkタグを使いましょう
e.g.
<link rel='stylesheet' href='/stylesheets/style.css' />
publicフォルダ直下のstylesheetsフォルダのstyle.cssファイルなので良いですね?

4-2. javascriptの場合

こちらもおなじみ、scriptタグを使いましょう
<script type="text/javascript" src='/javascripts/test.js'></script>
こちらも問題ないですね?

5. 最後に

まだ終わりません。
生htmlと違い、タグで読み込むだけでは表示ができません。
どうしたら良いかと言うと、ディレクトリ構成の中にある、app.jsファイルに1行書き込みます。
app.jsはルーティングを定義したりしていますので、全体のコントローラと思ってください。
そこで以下を追加します。
app.use(express.static('public'));
若しくは
app.use(express.static(path.join(__dirname, 'public')));
__dirnameは現在実行されているファイルの絶対パスが入っています。
これで表示ができると思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

XSS を試してみる

注意事項

この検証は自身の管理下にある仮想マシン上のWebサーバで行っているものです.
自身の管理下にないサーバの攻撃行為は罪に問われる可能性があります。

概要

クロスサイトスクリプティング(以下XSS)を実際に試してざっくり理解する目的.
簡単なXSSが成り立つ環境の構築とXSSの実行です.

本記事ではサーバで行っている処理の内容とXSSの方法について記します.

環境

VirtualBox 6.0.2.0
Ubuntu 18.04.4 LTS
Apache 2.4.29

XSS試してみる

Webサーバに以下のページを用意しました.
top.PNG

内容としては,入力を5つ受け取ってPerlのスクリプトに値を渡す感じです.

実際に値を渡してみると
input.PNG

↓ ページ遷移
result.PNG

という感じで飛んだ先のページで入力内容をそのまま表示する以下のようなページとなっています.

<html>
    <body>
        your input<br>
        1<br>
        2<br>
        3<br>
        4<br>
        5<br>
    </body>
</html>



ここからは攻撃者として

試しに<>'"\$などの制御文字を入力してみると
escape.PNG

飛んだ先でそのまま表示されました.
これはXSSいけそうですね!

試しに以下のコードを入力してみます.
これは表示されてすぐに ayashii.htmlに飛ぶという処理です.

<script>
setTimeout("location.href='http://サーバのIPアドレス/ayashii.html'",1);
</script>

このスクリプトが入力されると,以下のようなHTMLのページが表示されます.

<html>
    <body>
        your input<br>
        <script> 
            setTimeout("location.href='http://サーバのIPアドレス/ayashii.html'",1); 
        </script><br>
        2<br>
        3<br>
        4<br>
        5<br>
    </body>
</html>

上のページからayashii.html(下の画像) にリダイレクトされます.

ayashii.PNG

怪しい処理が行われてしまいました.

なぜスクリプトが実行されたのか?

今回使用したページでは入力された内容をそのままHTMLとして出力する処理が行われていました.

入力内容にスクリプトが入力されてしまったことでその部分も含めてHTML文章として成立してしまったのですね.

対策として

入力されるスクリプトが成立しないようにしてしまえば,この方法で任意のスクリプトが実行することができないようになります.

処理で,<>'"\$などの制御文字を別の文字に置き換えてしまう もしくは そもそも制御文字の入力を禁止する などの対策が考えられます.

おしまい

雑にWebページを実装してXSSを試してみました.
この例では,入力したユーザのみがスクリプトを影響を受けるようになっています.
実際にこの攻撃が意味を成すのはここのコメント欄のような,入力したものが他のユーザにも公開されるような場所でしょう.
コメントに攻撃者がサイトを見た人を悪意のあるページ誘導するようなスクリプトを入力するというようなイメージで.

XSS はここで紹介したもののほかにもいくつか種類があるようなので調べてみるのも良いかもしれません.

付録

入力用ページ

input.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>入力フォーム</title>
    </head>

    <body>
        <form action="cgi-bin/cal.cgi" method="GET">
            <h3>5つの数値を入力</h3>
            <p>
            num1 : <input type="text" name="value1" size="10"><br>
            num2 : <input type="text" name="value2" size="10"><br>
            num3 : <input type="text" name="value3" size="10"><br>
            num4 : <input type="text" name="value4" size="10"><br>
            num5 : <input type="text" name="value5" size="10"><br>
            </p>
            <p>
            <input type="submit" value="OK"><input type="reset" value="reset">
            </p>
        </form>
    </body>
</html>


入力されたものを出力するやつ

cal.cgi
#!/usr/bin/perl
use CGI;
$q = new CGI;

$value1 = $q->param('value1');
$value2 = $q->param('value2');
$value3 = $q->param('value3');
$value4 = $q->param('value4');
$value5 = $q->param('value5');

print"Content-type: text/html\n\n";
print"<html><body>\n";
print"your input<br>";
print"$value1<br>";
print"$value2<br>";
print"$value3<br>";
print"$value4<br>";
print"$value5<br>";
print"</body></html>\n";
exit;

あやしいサイト

ayashii.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>とても怪しいサイト</title>
    </head>

<style>

h1{
    color:red
}
</style>

    <body>
            <h1>なにかあやしい処理</h1>
    </body>
</html>

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【API】郵便番号検索APIを使ってみた

APIを使用したアプリを作ってみる

背景

  • APIを使用したアプリケーションが増えてきている
  • APIを使用する、作るエンジニアの仕事も増えている
  • APIを扱う技術の需要が増えそう

目次

0.環境確認

  • OS: Windows10 home
  • IDE : Eclipse(Photon 4.8)
  • ビルドツール : Gradle
  • サーバーサイド言語 : Java(1.8)
  • フレームワーク : SpringBoot(2.2.7)
  • JavaScript ライブラリ : jQuery(3.3.1)
  • テンプレートエンジン : thymeleaf

1.APIの確認

  • 以下のサイトからAPIの使用を確認します。
    https://zip-cloud.appspot.com/doc/api

  • 仕様を確認します。

    パラメータ名 項目名 必須 備考
    zipcode 郵便番号 7桁の数字。ハイフン付きでも可。完全一致検索。
    callback コールバック関数名 - JSONPとして出力する際のコールバック関数名。UTF-8でURLエンコードした文字列。
    limit 最大件数 - 同一の郵便番号で複数件のデータが存在する場合に返される件数の上限値(数字) ※デフォルト:20
    • レスポンスパラメータ
    フィールド名 項目名 備考
    status ステータス 正常時は 200、エラー発生時にはエラーコードが返される
    message メッセージ エラー発生時に、エラーの内容が返される。
    results zipcode(郵便番号)
    prefcode(都道府県コード)
    address1(都道府県名)
    address2(市区町村名)
    address3(町域名)
    kana1(都道府県名カナ)
    kana2 (市区町村名カナ)
    kana3(町域名カナ)
    複数の場合、配列となる
    • (例)郵便番号「7830060」で検索する場合
      • リクエストURL https://zip-cloud.appspot.com/api/search?zipcode=7830060
      • レスポンス

        { "message": null, "results": [ { "address1": "北海道", "address2": "美唄市", "address3": "上美唄町協和", "kana1": "ホッカイドウ", "kana2": "ビバイシ", "kana3": "カミビバイチョウキョウワ", "prefcode": "1", "zipcode": "0790177" }, { "address1": "北海道", "address2": "美唄市", "address3": "上美唄町南", "kana1": "ホッカイドウ", "kana2": "ビバイシ", "kana3": "カミビバイチョウミナミ", "prefcode": "1", "zipcode": "0790177" } ], "status": 200 }

2.プロジェクトの作成

別の記事で詳細に紹介しているので、そちらを参照ください。
GradleのSpringBootプロジェクトを作成する

3.バックエンドの実装

  • build.gradle
build.gradle
//中略
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    compile("com.fasterxml.jackson.core:jackson-databind")
}


  • Controllerクラス
    • パターン1:Json形式の文字列で受け取り
FrontController.java
@Controller
public class FrontController {

    @Autowired
    private FrontService frontService;

    @RequestMapping({ "/", "/index" })
    public String index() {
        return "index";
    }
    @ResponseBody
    @RequestMapping(value = "/getAddress" ,method = RequestMethod.POST, produces="application/json;charset=UTF-8")
    public String getAddress(@RequestBody(required = false) AddressForm addressForm) {
        return frontService.getAddress(addressForm.getZipcode());
    }
}
  • Serviceクラス
FrontService.java
public interface FrontService {
    public String getAddress(String zipCode);
}
FrontServiceImpl.java
@Service
public class FrontServiceImpl implements FrontService {

    /** 郵便番号検索API リクエストURL */
    private static final String URL = "https://zip-cloud.appspot.com/api/search?zipcode={zipcode}";

    @Override
    public String getAddress(String zipCode) {
        String zipCodeJson = restTemplate.getForObject(URL, String.class, zipCode);
        return zipCodeJson;
    }
}
  • formクラス
AddressForm.java
@Data
public class AddressForm {

    /** 郵便番号 */
    String zipcode;
}

4.フロントエンドの実装

  • html
index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>address</title>
        <script type="text/javascript" th:src="@{/jquery/jquery-3.3.1.js}"></script>
        <script th:src="@{/js/index.js}"></script>
    </head>
    <body>
        <form name="getAddress">
            <input id="zipcode" type="text">
            <button type="button" id="getAddressBtn">住所取得</button>
            <div id="dispAddress"></div>
        </form>
    </body>
</html>
  • JavaScript
index.js
$(function() {
    $('#getAddressBtn').on('click', function() {
        var params = {
                "zipcode" : $('#zipcode').val()
        };
        $.ajax({
            url : 'getAddress',
            type: 'POST',
            contentType: "application/json",
            data: JSON.stringify(params),
            dataType : 'json',
            async: false,
            success: function (data) {
                $("#dispAddress").empty();
                var dispAddress = document.getElementById("dispAddress");
                var table = document.createElement("table");
                table.setAttribute("border","2");
                table.setAttribute("cellpadding","15");
                table.setAttribute("style","margin :15px");

                $(data.results).each(function(index, result){
                    table.appendChild(createRow("郵便番号",result.zipcode));
                    table.appendChild(createRow("都道府県コード",result.prefcode));
                    table.appendChild(createRow("都道府県名",result.address1));
                    table.appendChild(createRow("市区町村名",result.address2));
                    table.appendChild(createRow("町域名",result.address3));
                    table.appendChild(createRow("都道府県名カナ",result.kana1));
                    table.appendChild(createRow("市区町村名カナ",result.kana2));
                    table.appendChild(createRow("町域名カナ",result.kana3));
                });
                dispAddress.appendChild(table);
            }
        });
    });
});

function createRow(header , value){
    var tr = document.createElement("tr");
    var th = document.createElement("th");
    th.append(header);
    var td = document.createElement("td");
    td.append(value);
    tr.appendChild(th);
    tr.appendChild(td);
    return tr;
}

5.動作確認

  • ブラウザで「localhost:8080」にアクセス
    ブラウザ起動.png

  • 「100-0001」(皇居の郵便番号)を入力して住所取得ボタン押下
    ブラウザ起動2.png

  • 住所が表示された。

6.おまけ

Json形式をDTOクラスに変換して受け取る方法
- Controllerクラス

FrontController.java
@Controller
public class FrontController {

    @Autowired
    private FrontService frontService;

    @RequestMapping({ "/", "/index" })
    public String index() {
        return "index";
    }
    @ResponseBody
    @RequestMapping(value = "/getAddress" ,method = RequestMethod.POST, produces="application/json;charset=UTF-8")
    // 戻り値をString → ZipcodeDto に変更
    public ZipcodeDto getAddress(@RequestBody(required = false) AddressForm addressForm) {
        return frontService.getAddress(addressForm.getZipcode());
    }
}
  • Serviceクラス(修正)
FrontService.java
public interface FrontService {
    // 戻り値をString → ZipcodeDto に変更
    public ZipcodeDto getAddress(String zipCode); 
}
  • DTOクラス(追加)
ZipcodeDto.java
@Data
public class ZipcodeDto {
    /** ステータス */
    int status;

    /** メッセージ */
    String message;

    /** 郵便番号情報リスト */
    List<ZipcodeResultDto> results = new ArrayList<>();
}
  • DTOクラス(追加)
ZipcodeResultDto.java
@Data
public class ZipcodeResultDto {

    /** 郵便番号 */
    String zipcode;

    /** 都道府県コード */
    String prefcode;

    /** 都道府県名 */
    String address1;

    /** 市区町村名 */
    String address2;

    /** 町域名 */
    String address3;

    /** 都道府県名カナ */
    String kana1;

    /** 市区町村名カナ */
    String kana2;

    /** 町域名カナ */
    String kana3;
}

  • Service実装クラス
    • ObjectMapperにURLを渡してDTOクラスに変換するパターン
FrontServiceImpl.java
@Service
public class FrontServiceImpl implements FrontService {
     // ObjectMapperを追加
    @Autowired
    ObjectMapper objectMapper;

     // URLのパラメータを正規表現に変更
    private static final String URL = "https://zip-cloud.appspot.com/api/search?zipcode=%s";

    @Override
    public ZipcodeDto getAddress(String zipCode) {
        ZipcodeDto zipcodeDto = null;;
        try {
            // ObjectMapperでURLと受け取りクラスを指定
            java.net.URL url = new java.net.URL(String.format(URL,zipCode));
            zipcodeDto = objectMapper.readValue(url, ZipcodeDto.class);
        } catch (Exception e) {
            e.getStackTrace();
        }
        return zipcodeDto;
    }
}
  • Service実装クラス
    • restTemplateにMappingJackson2HttpMessageConverterを設定して変換するパターン
FrontServiceImpl.java
@Service
public class FrontServiceImpl implements FrontService {

     // restTemplateを追加
    RestTemplate restTemplate = new RestTemplate();

    private static final String URL = "https://zip-cloud.appspot.com/api/search?zipcode={zipCode}";

    @Override
    public ZipcodeDto getAddress(String zipCode) {
        ZipcodeDto zipcodeDto = null;;
        try {

           // reatTemplateのmessageConverterにMappingJackson2HttpMessageConverter を追加
            MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
            List<MediaType> supportedMediaTypes = new ArrayList<>(messageConverter.getSupportedMediaTypes());
            supportedMediaTypes.add(MediaType.TEXT_PLAIN);
            messageConverter.setSupportedMediaTypes(supportedMediaTypes);
            restTemplate.setMessageConverters(Collections.singletonList(messageConverter));
            zipcodeDto = restTemplate.getForObject(URL, ZipcodeDto.class, zipCode);
        } catch (Exception e) {
            e.getStackTrace();
        }
        return zipcodeDto;
    }
}

7.まとめ

  • APIを使用するのは簡単(認証機能付きはもう少し難しい)
  • 他APIからデータ活用できる
  • いろんなAPIを組み合わせて新しいサービスを作れそう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Youtubeの好きな動画の好きな部分だけをリスト再生するWebページの作り方

はじめまして。ひょんなことからVtuberにハマり、色々な物を書いている夕星(ゆうづつ)と申します。
この度、推しのプレイリストが欲しいなぁという欲望の結果、簡素ながらもそれなりに動く物が出来たので備忘録を兼ねて記します。

まず完成品がこちらになります。
加賀美ハヤト「ロックコンピ」プレイヤー|夕星の長い話置き場
(にじさんじ所属Vtuber「加賀美ハヤト」の配信から楽曲を抜粋したリストです)

概要

構成は静的HTML+ピュアJavascript(Youtube Iframe API使用)。
ネットに繋がってさえいればローカルでも動作しますので、自分用かWebサイトに載せて仲間内で楽しむ等の用途が想定されます。
なおAPIの利用規約はご自身で最新版をご確認ください。(基本は非営利利用限定の様子?)

動画の再生数は本家にカウントされ、広告も再生されます。
「切り抜き動画では本家に還元されない」「プレイリストだと動画丸ごとしか再生出来ない」「作業BGMとして良い場面だけリピート再生したい」という悩みやニーズに応えてます。
(あと上手くやるとスマホでバックグラウンド再生出来たりもします)

ソースコード

ほぼ最小構成が以下です(リストが必要無いなら更に削れます)。デザイン等はご自由に調整ください。
かなり雑でDOM構成もやっつけなので、実際に使用する場合はご利用中の環境に合わせて適宜編集推奨です。

少し長いので折り畳んでいます
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>ページタイトル</title>

    <style type="text/css">
      #playlist {
        background-color: black;
        color: #aaaaaa;
      }
      .playparts {
        padding: 10px;
        cursor: pointer;
      }
      .playparts:not(:last-child) {
        border-bottom: 1px #aaa solid;
      }
      .songtitle {
        color:white;
      }
    </style>
  </head>
  <body>
    <div id="player"></div>
    <div id="playlist"></div>

    <script>
      // 動画リスト
      var playList = [
        {videoId:'',startSeconds:0, endSeconds:0, title:'', artist:''},
      ];

      // Iframe APIの準備
      var tag = document.createElement('script');
      tag.src = "https://www.youtube.com/iframe_api";
      var firstScriptTag = document.getElementsByTagName('script')[0];
      firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

      // プレイヤーの設定
      var player;
      function onYouTubeIframeAPIReady() {
        player = new YT.Player('player', {
          height: '360',
          width: '640',
          events: {
            'onReady': onPlayerReady,
            'onStateChange': onPlayerStateChange
          }
        });
      }

      // プレイヤーの準備完了後の処理(最初の動画のセット)
      var isReady = false;
      var nowPlaying = 0;
      var isNextVideo = false;
      function onPlayerReady(event) {
        player.cueVideoById(playList[nowPlaying]);
        document.getElementById("song"+nowPlaying).style.color = 'red';
        isReady = true;
      }

      // プレイヤーの状態が変化した時の処理(次の動画へ進む)
      function onPlayerStateChange(event) {
        if (event.data == YT.PlayerState.ENDED && !isNextVideo) {
          document.getElementById("song"+nowPlaying).style.color = 'white';
          nowPlaying += 1;
          if (playList.length <= nowPlaying) nowPlaying = 0;
          isNextVideo = true;
          player.loadVideoById(playList[nowPlaying]);
          document.getElementById("song"+nowPlaying).style.color = 'red';
        } else if (event.data == YT.PlayerState.PLAYING && isNextVideo) {
          isNextVideo = false;
        }
      }

      // リストの表示
      function showList () {
        var parent = document.getElementById("playlist");
        for (let i = 0; i < playList.length; i++) {
          // 動画の時間計算
          var min = Math.floor((playList[i].endSeconds - playList[i].startSeconds) / 60);
          var sec = ( '00' + (playList[i].endSeconds - playList[i].startSeconds) % 60 ).slice( -2 );

          // 要素を記述
          var parts = document.createElement("div");
          parent.appendChild(parts);
          var html = '<p><span class="songtitle" id="song'+i+'">';
          html += playList[i].title;
          html += '</span><span class="time">('+min+':'+sec+')</span></p><p class="artist">';
          html += playList[i].artist;
          html += '</p>';
          parts.innerHTML += html;

          parts.classList.add("playparts")
          parts.addEventListener("click", () => { pushList(i+""); }, false);
        }
      }
      showList();

      // リストをクリックされた時の処理
      function pushList (id) {
        if (!isReady) return;
        document.getElementById("song"+nowPlaying).style.color = 'white';
        nowPlaying = Number(id);
        player.loadVideoById(playList[nowPlaying]);
        document.getElementById("song"+nowPlaying).style.color = 'red';
      }
    </script>
  </body>
</html>

動画リストの設定

playListの中にオブジェクトを突っ込みます。
videoId, startSeconds, endSeconds はAPIで必要な情報なのでプロパティ名変更不可。
それ以外はリストで表示する為の情報なのでお好きに変更可能です。

プロパティ名 内容
videoId 動画のID。動画のURLに含まれています。(もしURLが「https://youtu.be/fGvYwVW38mc」なら「fGvYwVW38mc」がIDです)
startSeconds 開始地点の秒数。
endSeconds 終了時点の秒数。動画まるごとの場合は「0」指定でも動きます。

以下、設定例です。

var playList = [
  {videoId:'fGvYwVW38mc',startSeconds:0, endSeconds:262, title:'WITHIN', artist:'加賀美ハヤト'},
  {videoId:'SSY5z5vUgHA',startSeconds:462, endSeconds:616, title:'どうにもとまらない', artist:'9mm Parabellum Bullet(山本リンダ)'},
  {videoId:'Wq8i7lJVYGw',startSeconds:1062,endSeconds:1330, title:'ソラニン', artist:'ASIAN KUNG-FU GENERATION'},
  {videoId:'MpBnxbMzaXA',startSeconds:0,endSeconds:260, title:'シュガーソングとビターステップ(with SMC組)', artist:'UNISON SQUARE GARDEN'}
];

その他技術的な話

基本的に全部公式リファレンスに書いてあります。というか半分ここのコピペです。
iframe 組み込みの YouTube Player API リファレンス | YouTube IFrame Player API

但し動画のデータがオブジェクト型でないとendSecondsが使えません。上記ページのコピペだと最初の動画をオブジェクト型で渡せませんでした。
その為にnew YT.Player時点では動画を指定せず、onPlayerReadycueVideoByIdを使っています。
今回は開始時にワンクリック挟みたかったのでわざとcueVideoByIdとしていますが、loadVideoByIdにすると勝手に再生が始まります。

最後に

分かってしまえばとても手軽に使えますので、自分の推しのも作りたい!と思った方はご参考にして頂けるととても嬉しいです。
Twitterで「使ったよ!」と一声頂けたら喜び勇んで見に行きます)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsでアップロードされた画像の中心から正方形にくり抜いてexif情報処理してFirebaseStorageにアップする

自分用
詰め込みすぎだけど色々なアプリで使うため

<input type="file" accept="image/*" style="display:none;" @change="upload"/>
<canvas id="canvas" hidden></canvas>
    upload(ev) {
      const TRIM_SIZE = 512
      let blob = null
      const storageRef = firebase.storage().ref()
      if (!ev) return
      const file = ev.target.files[0]
      if (file.type !== 'image/jpeg' && file.type !== 'image/png') {
        return
      }
      const image = new Image()
      const reader = new FileReader()
      reader.onload = e => {
        image.onload = () => {
          let width, height, xOffset, yOffset
          if (image.width > image.height) {
            height = TRIM_SIZE
            width = image.width * (TRIM_SIZE / image.height)
            xOffset = -(width - TRIM_SIZE) / 2
            yOffset = 0
          } else {
            width = TRIM_SIZE
            height = image.height * (TRIM_SIZE / image.width)
            yOffset = -(height - TRIM_SIZE) / 2
            xOffset = 0
          }
          const canvas = $('#canvas')
            .attr('width', TRIM_SIZE)
            .attr('height', TRIM_SIZE)
          const ctx = canvas[0].getContext('2d')
          ctx.clearRect(0, 0, width, height)
          let orientation = ''
          EXIF.getData(file, () => {
            orientation = file.exifdata.Orientation
            switch (orientation) {
              case 2:
                ctx.transform(-1, 0, 0, 1, width, 0)
                break
              case 3:
                ctx.transform(-1, 0, 0, -1, width, height)
                break
              case 4:
                ctx.transform(1, 0, 0, -1, 0, height)
                break
              case 5:
                ctx.transform(0, 1, 1, 0, 0, 0)
                break
              case 6:
                ctx.transform(0, 1, -1, 0, height, 0)
                break
              case 7:
                ctx.transform(0, -1, -1, 0, height, width)
                break
              case 8:
                ctx.transform(0, -1, 1, 0, 0, width)
                break
              default:
                break
            }
            ctx.drawImage(image, xOffset, yOffset, width, height)
            const base64 = canvas.get(0).toDataURL('image/jpeg')
            const bin = atob(base64.split('base64,')[1])
            const len = bin.length
            const barr = new Uint8Array(len)
            let i = 0
            while (i < len) {
              barr[i] = bin.charCodeAt(i)
              i += 1
            }
            blob = new Blob([barr], { type: 'image/jpeg' })
            ctx.clearRect(0, 0, width, height)
            const uploadRef = storageRef.child(//パス
            )
            uploadRef.put(blob).then(() => {
              uploadRef.getDownloadURL().then(url => {
                //URLをどうにかする処理
              })
            })
          })
        }
        image.src = e.target.result
      }
      reader.readAsDataURL(file)
    },
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【HTML, CSS基礎】インライン要素、ブロック要素の配置方法

HTMLとCSSの基礎をすぐ忘れてしまうので、メモしたいと思います。

ブロック要素、インライン要素とは

ブロック要素

文字通りかたまりを表す要素のことで、箱をイメージするとわかりやすいと思います。
ブロック要素は高さや幅を指定することができます。
このブロック要素の大きさを調整して、ウェブサイトをおおまかな見た目を作っていきます。

例) divタグなど

インライン要素

文章の一部として扱われる要素で、主にブロック要素の中に入れて使用します。
インライン要素は高さや幅の指定ができないので、そのまま使うと狙い通りの配置にならずダサくなります。
なので、インライン要素は必ずブロック要素の中に入れて使うようにしましょう。

例) iタグ、aタグ、inputタグ、buttonタグ、pタグなど

なぜ要素を把握することが重要なのか

ブロック要素とインライン要素では大きくコーディングのやり方が変わってくるからです。
具体的にどう違うのかは次で説明します。
必ず、どちらの要素なのかを知った上でタグを使うようにしましょう。

要素ごとの配置方法

インライン要素の左右中央寄せ

インライン要素を親のブロック要素の幅に対して、左右中央寄せしたいときのCSSの記載です。
親ブロックに対してtext-align: centerを適用します。

(補足)
text-align: centerは一つ親のブロックに対して左右中央寄せされ、二つ以上親のブロックは無視されます。

インライン要素の上下中央寄せ

インライン要素を親のブロック要素の高さに対して、上下中央寄せしたいときのCSSの記載です。
親ブロックに対してline-height: ○○px(親ブロックのheightに合わせる)を適用します。

ブロック要素の左右中央寄せ

ブロック要素をその親ブロックの幅に対して、左右中央寄せしたいときのCSSの記載です。
こちらは2つ方法があります。

① 動かしたいブロック要素にmargin: 0 autoを適用します。
  基本的にはこれを使ったほうがいいです。

② 動かしたいブロック要素の親要素にdisplay: flexとjustify-content: centerを適用します。
  こちらはフレックスボックスと呼ばれる手法になり、
  中央寄せ以外にも様々な配置を実現することができる便利な手法になります。

しかし、display: flexには要素を横並びするという役割もあるため、結果として意図した配置にならないことがあります。そういう意味では①の方法を使ったほうが事故が起こりにくいです。

ブロック要素の上下中央寄せ

ブロック要素をその親ブロックの高さに対して、上下中央寄せしたいときのCSSの記載です。
親ブロック要素に対してdisplay: flexとalign-items: centerを適用します。

まとめ

以上まとめますと、コーディングをする際は要素がブロック要素なのかインライン要素なのか把握しましょう。
その理由は、要素によって配置の仕方が変わってくるからということでした。
具体的には、下のようにCSSを記載しました。

  • インライン要素の左右中央寄せ ⇒ 親要素にtext-align: center
  • インライン要素の上下中央寄せ ⇒ 親要素にline-height: ○○px(親ブロックのheightに合わせる)
  • ブロック要素の左右中央寄せ ⇒ 動かしたい要素にmargin: 0 auto
  • ブロック要素の上下中央寄せ ⇒ 親要素にdisplay: flexとalign-items: center

もっといい方法はあると思いますが、ひとまずこのやり方で問題なくviewを作れているのでメモしました。
こうした方がいいだったり、アドバイス等ありましたらコメントくださると幸いです。
もっとコーディングに関しての知識が深まればまた更新したいと思います。
以上、ここまで読んでくださり、ありがとうございました。では!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

javascriptでradioボタンにハマった話

結論

radioボタンのプロパティは最初にtypeを設定しよう。

経緯

javascriptではcreateElementを使ってコンテンツを作る事はよくあると思う。
例えばこんなの

radio.js
    //フォームを生成
    var eForm1 = document.createElement('form');

    //ラジオボタンを生成
    var eRadio1 = document.createElement('input');
    eRadio1.value = '';
    eRadio1.type = 'radio';
    eRadio1.name = 'gender';
    eRadio1.checked = true;

    var eRadio2 = document.createElement('input');
    eRadio2.value = '';
    eRadio2.type = 'radio';
    eRadio2.name = 'gender';

    var eRadio3 = document.createElement('input');
    eRadio3.value = 'その他';
    eRadio3.type = 'radio';
    eRadio3.name = 'gender';

    eForm1.appendChild(eRadio1);
    eForm1.appendChild(eRadio2);
    eForm1.appendChild(eRadio3);
    document.body.appendChild(eForm1);

次のようにするとチェックを入れた部分の内容(value)を取得できる。

    //elementsを取得
    var elements = document.getElementsByName( "gender" ) ;

    //valueを取得
    for ( var output = "", i = elements.length; i--; ) {
        if ( elements[i].checked ) {
            var output = elements[i].value ;
            break ;
        }
    }
    alert(output);

firefoxやchromeでは以下のように表示される。
chrome.png

しかしIE11では上手く動かない。
ie.png

性別がonでは困る。

ナゼだろう?

しばらく試行錯誤してtypeの位置が悪い事が分かった。
検証コードを書いて詳しく調べるとtypeを設定する前にvalueを設定するとvalueが無視される事が分かった。

radio1.js
function init(){
    //フォームを作る
    var eForm = document.createElement('form');

    //vtnc
    var eRadio = document.createElement('input');
    eRadio.value = 'vtnc';
    eRadio.type = 'radio';
    eRadio.name = 'vtnc';
    eRadio.checked = true;
    eForm.appendChild(eRadio);

    //vtcn
    var eRadio = document.createElement('input');
    eRadio.value = 'vtcn';
    eRadio.type = 'radio';
    eRadio.checked = true;
    eRadio.name = 'vtcn';
    eForm.appendChild(eRadio);

    //vntc
    var eRadio = document.createElement('input');
    eRadio.value = 'vntc';
    eRadio.name = 'vntc';
    eRadio.type = 'radio';
    eRadio.checked = true;
    eForm.appendChild(eRadio);

    //vnct
    var eRadio = document.createElement('input');
    eRadio.value = 'vnct';
    eRadio.name = 'vnct';
    eRadio.checked = true;
    eRadio.type = 'radio';
    eForm.appendChild(eRadio);

    //vctn
    var eRadio = document.createElement('input');
    eRadio.value = 'vctn';
    eRadio.checked = true;
    eRadio.type = 'radio';
    eRadio.name = 'vctn';
    eForm.appendChild(eRadio);

    //vcnt
    var eRadio = document.createElement('input');
    eRadio.value = 'vcnt';
    eRadio.checked = true;
    eRadio.name = 'vcnt';
    eRadio.type = 'radio';
    eForm.appendChild(eRadio);

    //tvnc
    var eRadio = document.createElement('input');
    eRadio.type = 'radio';
    eRadio.value = 'tvnc';
    eRadio.name = 'tvnc';
    eRadio.checked = true;
    eForm.appendChild(eRadio);

    //tvcn
    var eRadio = document.createElement('input');
    eRadio.type = 'radio';
    eRadio.value = 'tvcn';
    eRadio.checked = true;
    eRadio.name = 'tvcn';
    eForm.appendChild(eRadio);

    //tncv
    var eRadio = document.createElement('input');
    eRadio.type = 'radio';
    eRadio.name = 'tncv';
    eRadio.checked = true;
    eRadio.value = 'tncv';
    eForm.appendChild(eRadio);

    //tnvc
    var eRadio = document.createElement('input');
    eRadio.type = 'radio';
    eRadio.name = 'tnvc';
    eRadio.value = 'tnvc';
    eRadio.checked = true;
    eForm.appendChild(eRadio);

    //tcvn
    var eRadio = document.createElement('input');
    eRadio.type = 'radio';
    eRadio.checked = true;
    eRadio.value = 'tcvn';
    eRadio.name = 'tcvn';
    eForm.appendChild(eRadio);

    //tcnv
    var eRadio = document.createElement('input');
    eRadio.type = 'radio';
    eRadio.checked = true;
    eRadio.name = 'tcnv';
    eRadio.value = 'tcnv';
    eForm.appendChild(eRadio);

    //nvtc
    var eRadio = document.createElement('input');
    eRadio.name = 'nvtc';
    eRadio.value = 'nvtc';
    eRadio.type = 'radio';
    eRadio.checked = true;
    eForm.appendChild(eRadio);

    //nvct
    var eRadio = document.createElement('input');
    eRadio.name = 'nvct';
    eRadio.value = 'nvct';
    eRadio.checked = true;
    eRadio.type = 'radio';
    eForm.appendChild(eRadio);

    //ntcv
    var eRadio = document.createElement('input');
    eRadio.name = 'ntcv';
    eRadio.type = 'radio';
    eRadio.checked = true;
    eRadio.value = 'ntcv';
    eForm.appendChild(eRadio);

    //ntvc
    var eRadio = document.createElement('input');
    eRadio.name = 'ntvc';
    eRadio.type = 'radio';
    eRadio.value = 'ntvc';
    eRadio.checked = true;
    eForm.appendChild(eRadio);

    //ncvt
    var eRadio = document.createElement('input');
    eRadio.name = 'ncvt';
    eRadio.checked = true;
    eRadio.value = 'ncvt';
    eRadio.type = 'radio';
    eForm.appendChild(eRadio);

    //nctv
    var eRadio = document.createElement('input');
    eRadio.name = 'nctv';
    eRadio.checked = true;
    eRadio.type = 'radio';
    eRadio.value = 'nctv';
    eForm.appendChild(eRadio);

    //cvtn
    var eRadio = document.createElement('input');
    eRadio.checked = true;
    eRadio.value = 'cvtn';
    eRadio.type = 'radio';
    eRadio.name = 'cvtn';
    eForm.appendChild(eRadio);

    //cvnt
    var eRadio = document.createElement('input');
    eRadio.checked = true;
    eRadio.value = 'cvnt';
    eRadio.name = 'cvnt';
    eRadio.type = 'radio';
    eForm.appendChild(eRadio);

    //ctvn
    var eRadio = document.createElement('input');
    eRadio.checked = true;
    eRadio.type = 'radio';
    eRadio.value = 'ctvn';
    eRadio.name = 'ctvn';
    eForm.appendChild(eRadio);

    //ctnv
    var eRadio = document.createElement('input');
    eRadio.checked = true;
    eRadio.type = 'radio';
    eRadio.name = 'ctnv';
    eRadio.value = 'ctnv';
    eForm.appendChild(eRadio);

    //cnvt
    var eRadio = document.createElement('input');
    eRadio.checked = true;
    eRadio.name = 'cnvt';
    eRadio.value = 'cnvt';
    eRadio.type = 'radio';
    eForm.appendChild(eRadio);

    //cntv
    var eRadio = document.createElement('input');
    eRadio.checked = true;
    eRadio.name = 'cntv';
    eRadio.type = 'radio';
    eRadio.value = 'cntv';
    eForm.appendChild(eRadio);

    document.body.appendChild(eForm);

    echoRadio('vtnc');
    echoRadio('vtcn');
    echoRadio('vntc');
    echoRadio('vnct');
    echoRadio('vctn');
    echoRadio('vcnt');
    echoRadio('tvnc');
    echoRadio('tvcn');
    echoRadio('tncv');
    echoRadio('tnvc');
    echoRadio('tcvn');
    echoRadio('tcnv');
    echoRadio('nvtc');
    echoRadio('nvct');
    echoRadio('ntcv');
    echoRadio('ntvc');
    echoRadio('ncvt');
    echoRadio('nctv');
    echoRadio('cvtn');
    echoRadio('cvnt');
    echoRadio('ctvn');
    echoRadio('ctnv');
    echoRadio('cnvt');
    echoRadio('cntv');
}

function echoRadio(elementName){
    //要素を取得
    var elements = document.getElementsByName( elementName ) ;
    //内容を取得
    var output;
    for ( output = "", i = elements.length; i--; ) {
        if ( elements[i].checked ) {
            output = elements[i].value ;
            break ;
        }
    }
    //内容を出力
    document.body.insertAdjacentHTML ( 'beforeend', elementName + ' = '+ output + '<br>');
}

window.addEventListener('load',init,false);

radioボタン以外はどうだろうね?要検証。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HTML、CSS番外編②~Webアイコンフォント、Font Awesomeを使ってみよう‼~

超便利なFont Awesomeを使って、Webアイコンフォントを手に入れよう

今回はWebページ内にアイコンを導入するやり方を紹介していきます:relaxed:
Font Awesomeというサービスを使って、簡単にWebアイコンフォントが手に入ります:point_up_tone2:
Font Awesomeの導入方法から、アイコンの編集の仕方も紹介していきます。

今回の記事:arrow_down:

  • Webアイコンフォントって何?
  • Font Awesome導入方法
  • アイコンを編集してみよう
    • アイコンの大きさを変える
    • アイコンの色を変える
    • アイコンと文字の間にスペースを作る
    • 回転させる(アニメーション)

Webアイコンフォントって何?

Webアイコンフォントとはフォントとして使用することができるアイコン素材です。
よくWebページなどで目に付くアイコンはTwitterやInstagramなどのSNSのアイコンがありますね:grinning:アイコンはWebページをデザインうえで欠かせないものなのです:bangbang:
SNS.png

アイコンフォントを使用するメリットとしては、CSSで大きさや色が変えられたり、ファイルサイズも小さいので、ファイルサイズを抑えることができます。:point_down_tone2:

Font Awesome導入方法

そんなWebアイコンフォントを導入するには、いつくかサービスはありますが、Font Awesomeがおすすめです:heart_exclamation:
ここではFont Awesomeの使い方を紹介していきます。導入方法は簡単で、アイコンの種類もたくさんあります。

まず以下のコードをHTMLの<head>内に書きます。

HTMLの<head>内
<link href="https://use.fontawesome.com/releases/v5.6.1/css/all.css" rel="stylesheet">

Font Awesome:point_down_tone2:
FontAwesome.png

Font Awesomeのサイトに行き、使いたいアイコンを選びます:point_down_tone2:
image.png

使いたいアイコンをクリックし、Start Using This Iconをクリック:point_down_tone2:
image.png

HTMLコードをコピーして、貼り付ければ導入完了です:point_down_tone2:
image.png

※最近の新しいバージョンはメールアドレスを登録して使えるようになっています。

登録する場合はStart for Free:point_down_tone2:
startfree.png

メールアドレスを入力し、Create & Use This kitをクリック:point_down_tone2:
メールアドレス.png

以下のようなメールが届くので、Click To Confirm Your Email Address + Set Thisgs upをクリックし、設定します:point_down_tone2:
image.png

会員登録を無事完了するとKit CodeというものがCopyできるので、それをHTMLの<head>内に埋め込みましょう:exclamation:
後は先程と同じように導入したいアイコンを選択して入れます。

アイコンを編集してみよう

アイコンの導入が完了しました!!導入しただけのアイコンは、小さく、色は黒です。
大きさを変えたり、CSSを使ってよりおしゃれなアイコンにしてみましょう:heart_eyes:

アイコンの大きさを変える

FontAwesomeでは、アイコンの大きさを簡単に変えられます。

今回はこのアイコンを使用していきます。
image.png

使用するアイコンHTML
    <p><i class="fab fa-apple"></i></p>

上記のコードのi class="〜"内に書くことでサイズを大きくすることができます。

アイコン2倍HTML
    <p><i class="fab fa-apple fa-2x"></i></p>

サイズ表

  • fa-xs:0.75倍
  • fa-lg:1.33倍
  • fa-2x:2倍
  • fa-3x:3倍
  • fa-4x:5倍
  • fa-5x:7倍

全部のサイズ感はこんな感じです:point_down_tone2:
apple.png

自分のページのバランスなどを考えるといいですね!
もちろんCSSでも調整可能です。

アイコンの色を変える

次は色を変えていきましょう。CSSを使っていきますのでクラスを追加しましょう。
使用するアイコンは引き続きリンゴです。赤いリンゴと、青いリンゴを作ってみます。

使用するアイコンHTML
    <p><i class="fab fa-apple fa-3x"></i></p>
    <p><i class="fab fa-apple fa-3x"></i></p>

ここにクラス名以下とし、追加します。
クラス名:red_apple 赤いリンゴ
クラス名:blue_apple 青いリンゴ

クラス名追加HTML
    <p><i class="fab fa-apple fa-3x red_apple"></i></p>
    <p><i class="fab fa-apple fa-3x blue_apple"></i></p>
CSS
.red_apple{
    color: red;
}
.blue_apple{
    color: blue;
}

image.png

色が付けられました。SNSのマークなどは、そのままの色を再現するとわかりやすいです。

アイコンと文字の間にスペースを作る

アイコンと文字の間にスペースがないと、詰まって見えてしまいます。
image.png

HTML
    <p><i class="fas fa-home"></i>HOME</p>
    <p><i class="fas fa-bars"></i>MENU</p>
    <p><i class="fas fa-cog"></i>SETTINGS</p>

こちらも簡単にスペースを追加できますので、ぜひ参考にしてみてください。
fa-fwという名前のclassをiタグに追加だけで、前後にちょっとした余白が作られます。

HTML
    <p><i class="fas fa-home fa-fw"></i>HOME</p>
    <p><i class="fas fa-bars fa-fw"></i>MENU</p>
    <p><i class="fas fa-cog fa-fw"></i>SETTINGS</p>

image.png

少し余白が入り、違和感がなくなりましたね。

回転させる(アニメーション)

何と簡単に回転させることなんかも可能です。

image.png

使用するアイコンHTML
    <i class="fas fa-fan"></i>
    <i class="fas fa-question-circle"></i>

fa-spinというclass名を追加するだけでアイコンをくるくると回すことができます。実際に見てみましょう。

回転HTML
    <i class="fas fa-fan fa-spin"></i>
    <i class="fas fa-question-circle fa-spin"></i>

GIF 2020-05-17 11-13-02.gif

便利なのでぜひFont Awesome使ってみてくださいね:kissing_heart:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】テーブルを分けて複数の画像をアップロードする

複数枚の写真を一度に保存する機能の実装において、
【itemテーブル】と【item_imageテーブル】の二つに
分けて複数枚の写真を保存する機能を実装した際の手順をまとめたのでご紹介します。

完成形

○ 商品写真は最大で10枚まで投稿可能。
○ 一つのファイルフィールドに一つの写真でアップロードしていく。
○ 5枚投稿時点で下段に切り替わる。
Image from Gyazo

1. HTMLの用意

sample.haml
~ 省略 ~

.image-container
  .image-container_box
    .form-title
      %span.box-form-explanaion
        商品画像
      %span.indispensable
        必須
    %p.image-container_box_message
      最大10枚までアップロードできます
    .image-container_box_alart-10
      ※ 1枚目は必須です
  .image-container_unit-man    【写真が順にプレビューされていく箱】
    .item-image-container__unit.preview-0  【写真一枚が入る箱 ※投稿ごとに生成されていく】
      = f.fields_for :item_images do |i|
        .image-container__unit--guide
          = i.file_field :image, class: 'img-man', id:"image-label-0",type: 'file'
          .have-image
            %i.fas.fa-camera

~ 省略 ~

◉【写真一枚が入る箱】に2つクラスがあるのは、2枚目以降では毎回クラス名を
変えていくためです。
◉この仕様では、1つのinputに写真は1つなので、multiple属性は付けていません。

2. CSSの用意

sample.scss
  .image-container {
    padding: 40px;
    border-bottom: solid 1px rgb(235, 235, 235);
    &_box {
      &_message {
        height: 19px;
        margin: 16px 0 5px;
        font-size: 14px;
        line-height: 1.4em;
        display: block;
      }
      &_alart-10 {
        margin-bottom: 4px;
        font-size: 14px;
        font-weight: bold;
        color:red;
      }
    }
    &_unit-man {
      min-height: 152px;
      display: flex;
      flex-direction: row;
      flex-wrap: wrap;
      .item-image-container__unit {
        align-content: center;
        align-items: center;
        background-color: rgb(245, 245, 245);
        display: flex;
        justify-content: flex-start;
        height: 150px;
        width: 118px;
        margin-left: 5px;
        margin-bottom: 2px;
        justify-content: center;
        position: relative;
        border-width: 1px;
        border-style: dashed;
        border-color: rgb(204, 204, 204);
        border-image: initial;

        .have-image {
          position: absolute;
            left: 32px;
            top: 40px;
          z-index: 0;
          cursor: pointer;
          font-size: 16px;
          .fas.fa-camera {
            margin-left: 16px;
            font-size: 1.2rem;
          }
          .fas.fa-camera:hover {
            cursor: pointer;
            transform: scale(1.3, 1.3);
            transition: all 0.1s ease 0s;
          }
        }
        .item-image-container__unit__image {
          z-index: 1;
          height: 145px;
          width: 100%;
          margin: 0 3px;
          background-color: #ffffff;
          position: relative;
          img {
            width: 100%;
            height: auto;
          }
          .image-option__list--tag {
            width: 100%;
            background-color: lightblue;
            text-align: center;
            position: absolute;
              bottom: 0;
              left: 0;
          }
          .image-option__list--tag:hover {
            cursor: pointer;
            transform: scale(1.0, 1.0);
            transition: all 0.1s ease 0s;
            background-color: #ea352d;
            color:#ffffff;
          }

        }
      }
      .item-image-container__unit {
        input{
          display: none;
        }
      }
    }
  }

「.unit-man」に適用されている、[flex-direction: row;]と[flex-wrap: wrap;]により、
写真が既定の幅まで投稿されたら下段に折り返してくれます。

3. JSでプレビューさせる

sample.js
$(function(){


~ 省略 ~


  var dataBox = new DataTransfer();  //データ用の箱を作る
  $(document).on('change', '.img-man', function(){    //inputの中身が変化したら発火する
    $.each(this.files, function(i, file){
      var fileReader = new FileReader();
      dataBox.items.add(file)    
      fileReader.readAsDataURL(file);     //ファイルの読み込み
      fileReader.onloadend = function() {     //読み込み完了すると発火
        var num = $('.item-image-container__unit').length  //写真の枚数をnumに代入
        var src = fileReader.result   //写真のデータをsrcに代入
        var html =  `<div class="item-image-container__unit__image">
                        <img src="${src}">
                      <div class="image-option__list--tag btn-${num}">削除</div>
                    </div>`

        var field = `<div class="item-image-container__unit preview-${num}">
                      <div class="image-container__unit--guide">
                        <label for="image-label-${num}">
                          <input class="img-man" id="image-label-${num}" type="file" name="item[item_images_attributes][${num}][image]">
                          <div class="have-image">
                            <i class="fas fa-camera"></i>
                          </div>
                        </label>
                      </div>
                    </div>`
        $(html).appendTo(`.preview-${num - 1}`)  //枚数で該当するクラスに写真を追加する
                      
        if (num < 10 ) {     //10枚分まで新しいinputの生成を行う
          $(field).appendTo('.image-container_unit-man')
        }
      };
    });
  });
  //削除機能 
  $(document).on("click", ".image-option__list--tag", function(){  //削除ボタンクリックで発火
    var num = $('.item-image-container__unit').length
    var field_2 = `<div class="item-image-container__unit preview-0">
                    <div class="image-container__unit--guide">
                      <label for="image-label">
                        <input class="img-man" id="image-label-0" type="file">
                        <div class="have-image">
                          <i class="fas fa-camera"></i>
                        </div>
                      </label>
                    </div>
                  </div>`
    $(this).parent().parent().remove();  //写真が入っている箱ごと削除
    $(".item-image-container__unit").removeClass(`preview-${num - 1}`)
    $(".item-image-container__unit").addClass(`preview-${num - 2}`)
    if(num == 1) {   //全部削除した時に新たに1つフィールドを生成する
      $(field_2).appendTo('.image-container_unit-man')
    }
  });

~ 省略 ~

◉変数【num】は、写真の読み込みごとに代入され、その度に横に生成される
【preview-${num}】クラスが書き換わって横に生成されるようになっています。

◉ 下部の10枚制限の記述によって、10枚までアップされるとinputの生成が止まります。
また、multiple属性が付いていないので写真の一括選択はできなくなります。

◉削除機能に関しても、クラス名の書き換えを行う必要があります。


登録枚数に関しては、モデルにも別でバリーデーションは書いてあります。

sample.rb
  def  item_images_number
    errors.add(:item_images, "は10枚までです") if item_images.size > 10
  end

以上で終了です。
ご覧いただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者によるプログラミング学習ログ 317日目

100日チャレンジの317日目

twitterの100日チャレンジ#タグ、#100DaysOfCode実施中です。
すでに100日超えましたが、継続。
100日チャレンジは、ぱぺまぺの中ではプログラミングに限らず継続学習のために使っています。
317日目は、

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

html&css markerで任意の目次を作る

最近

極鶏のラーメンが食べたいです:ramen:
ついでに言うと猪一のラーメンも食べたいです:ramen:

目次

  • 目標
  • HTML
  • CSS

目標

リストの入れ子で通常のリストだと↓見えにくい。
image.png

↓こんな感じのリストを作る
image.png

HTML

今回はolで括りましたがulでも大丈夫です。
それぞれのolにfirst second thirdなどの対応する階層の任意のクラスをつける。

index.html
<ol class="first-layer">
    <li>1つ目の階層リスト-1</li>
    <li>1つ目の階層リスト-2</li>
    <ol class="second-layer">
      <li>2つ目の階層リスト-1</li>
      <li>2つ目の階層リスト-2</li>
      <ol class="third-layer">
        <li>3つ目の階層リスト-1</li>
      </ol>
    </ol>
    <li>1つ目の階層リスト-3</li>
    <ol class="second-layer">
      <li>2つ目の階層リスト-1</li>
      <ol class="third-layer">
        <li>3つ目の階層リスト-1</li>
        <li>3つ目の階層リスト-2</li>
      </ol>
      <li>2つ目の階層リスト-2</li>
    </ol>
  </ol>

CSS

counterとmarkerを使い、作っていきます。

olが実行される度にcounter-resetで 加算していた num を0にする。
(int num=0;の気持ち)

ol{
    counter-reset: num;
}

counter-incrementで num に加算していく。
リストがある度に1加算される。
(num++; の気持ち)

li{
    counter-increment: num;
}

リストの後ろにmarkerを作り、そのcontentに num の数値を出力する。
(print(num) の気持ち)

li:before{
    display: marker;
    content: counter(num_f)". ";
}

以上を階層ごとに設定していく。

style.css
li{
    list-style-type: none;
    list-style-position: inside;
}
ol.first-layer{
    counter-reset: num_f;
}
.first-layer li{
    counter-increment: num_f;
}
.first-layer li:before{
    display: marker;
    content: counter(num_f)". ";
}
ol.second-layer{
    counter-reset: num_s;
}
.second-layer li{
    counter-increment: num_s;
}
.second-layer li:before{
    display: marker;
    content: counter(num_f)'.' counter(num_s)". ";
}
ol.third-layer{
    counter-reset: num_t;
}
.third-layer li{
    counter-increment: num_t;
}
.third-layer li:before{
    display: marker;
    content: counter(num_f)'.' counter(num_s)"." counter(num_t)'. ';
}

できました!
image.png

おまけ

n.n.n.も作れます。
image.png

ol.first-layer{
    counter-reset: num_f;
    counter-reset: num_s;
    counter-reset: num_t;
}
.first-layer li:before{
    display: marker;
    content: counter(num_f)'.' counter(num_s)"." counter(num_t)'. ';
}
ol.second-layer{
    counter-reset: num_s;
    counter-reset: num_t;
}
.second-layer li:before{
    display: marker;
    content: counter(num_f)'.' counter(num_s)"." counter(num_t)'. ';
}

※変更した箇所を記載

参考

マーカー
http://www6.plala.or.jp/go_west/nextcss/reference/article/marker.htm

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む