- 投稿日:2020-05-16T23:35:32+09:00
WebページにTwitterのタイムラインを表示させる
概要
Webアプリを作りたいと思い勉強し始めました。様々なWebアプリを参考にしていると目に入るのが「Twitterのタイムライン」でした。
どうやってTwitterのタイムラインを埋め込むかを調査した備忘録を残します。最終的に次のようにできました。(今回はTwitterのAPIを使用せずに実装しています。APIを使用するほうが一般的?)
開発環境
- Webブラウザ : Google Chrome
作ってみる
埋め込むURLを入力
Twitter Publishにアクセス。
次のような画面が出るので、TwitterのアカウントURLを入力する。
私の場合は次のURLを入力します。https://twitter.com/kmaepuウィジットタイプの選択
画面を下にスクロールし、ウィジットのタイプを選びます。左側の「Embedded Timeline」を選びます。
参考にこのまま使用した場合のWeb画面は次のようになります。
ちなみに、
右側の「Twitter Buttons」を選択すると、次のようにフォローかメンションのボタンを埋め込むことができます。
ウィジットのオプション設定
このまま進むと、画面いっぱいにタイムラインが表示されてしまうので整形します。ウィジット選択画面の下の「set customization options」を選択します。
タイムライン型ウィジットの場合、次のような設定画面が表示されます。
ウィジットの
- 高さ(px)
- 幅(px)
- デザイン(白 or 黒)
- 表示基本言語を入力します。設定したら右下の「Update」ボタンを押します。
HTMLのコードをコピー
次のようにHTMKのコードが表れうので、右側の「Copy Code」をおすと。コードをコピーできます。あとは使用したいhtmkファイルにペーストするだけです。
完成!
おわりに
ウィジットを埋め込むだけなので、コードを書かずに実現できて便利でした。余談ですが、趣味で水耕栽培や家庭菜園をしておりその様子をTwitterでつぶやいてます。
↓
Follow @kmaepu
- 投稿日:2020-05-16T23:05:44+09:00
Expressでcssとjavascriptを読み込む方法
1. ディレクトリ構成
Expressでプロジェクトを作成するとディレクトリは以下のような構成になります。
詳細は今後に書くとして、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
は現在実行されているファイルの絶対パスが入っています。
これで表示ができると思います。
- 投稿日:2020-05-16T22:51:16+09:00
XSS を試してみる
注意事項
この検証は自身の管理下にある仮想マシン上のWebサーバで行っているものです.
自身の管理下にないサーバの攻撃行為は罪に問われる可能性があります。概要
クロスサイトスクリプティング(以下XSS)を実際に試してざっくり理解する目的.
簡単なXSSが成り立つ環境の構築とXSSの実行です.本記事ではサーバで行っている処理の内容とXSSの方法について記します.
環境
VirtualBox 6.0.2.0
Ubuntu 18.04.4 LTS
Apache 2.4.29XSS試してみる
内容としては,入力を5つ受け取ってPerlのスクリプトに値を渡す感じです.
という感じで飛んだ先のページで入力内容をそのまま表示する以下のようなページとなっています.
<html> <body> your input<br> 1<br> 2<br> 3<br> 4<br> 5<br> </body> </html>
ここからは攻撃者として
飛んだ先でそのまま表示されました.
これは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(下の画像)
にリダイレクトされます.怪しい処理が行われてしまいました.
なぜスクリプトが実行されたのか?
今回使用したページでは入力された内容をそのまま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>
- 投稿日:2020-05-16T20:41:04+09:00
【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仕様を確認します。
- https://zip-cloud.appspot.com/api/search をベースにする
- リクエストパラメータ
パラメータ名 項目名 必須 備考 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.javapublic 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.動作確認
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.javapublic 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を組み合わせて新しいサービスを作れそう。
- 投稿日:2020-05-16T19:19:55+09:00
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
時点では動画を指定せず、onPlayerReady
でcueVideoById
を使っています。
今回は開始時にワンクリック挟みたかったのでわざとcueVideoById
としていますが、loadVideoById
にすると勝手に再生が始まります。最後に
分かってしまえばとても手軽に使えますので、自分の推しのも作りたい!と思った方はご参考にして頂けるととても嬉しいです。
(Twitterで「使ったよ!」と一声頂けたら喜び勇んで見に行きます)
- 投稿日:2020-05-16T17:15:35+09:00
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) },
- 投稿日:2020-05-16T16:47:40+09:00
【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を作れているのでメモしました。
こうした方がいいだったり、アドバイス等ありましたらコメントくださると幸いです。
もっとコーディングに関しての知識が深まればまた更新したいと思います。
以上、ここまで読んでくださり、ありがとうございました。では!
- 投稿日:2020-05-16T13:07:58+09:00
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);性別がonでは困る。
ナゼだろう?
しばらく試行錯誤してtypeの位置が悪い事が分かった。
検証コードを書いて詳しく調べるとtypeを設定する前にvalueを設定するとvalueが無視される事が分かった。radio1.jsfunction 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ボタン以外はどうだろうね?要検証。
- 投稿日:2020-05-16T10:55:12+09:00
HTML、CSS番外編②~Webアイコンフォント、Font Awesomeを使ってみよう‼~
超便利なFont Awesomeを使って、Webアイコンフォントを手に入れよう
今回はWebページ内にアイコンを導入するやり方を紹介していきます
Font Awesomeというサービスを使って、簡単にWebアイコンフォントが手に入ります
Font Awesomeの導入方法から、アイコンの編集の仕方も紹介していきます。今回の記事
- Webアイコンフォントって何?
- Font Awesome導入方法
- アイコンを編集してみよう
- アイコンの大きさを変える
- アイコンの色を変える
- アイコンと文字の間にスペースを作る
- 回転させる(アニメーション)
Webアイコンフォントって何?
Webアイコンフォントとはフォントとして使用することができるアイコン素材です。
よくWebページなどで目に付くアイコンはTwitterやInstagramなどのSNSのアイコンがありますねアイコンはWebページをデザインうえで欠かせないものなのです
アイコンフォントを使用するメリットとしては、CSSで大きさや色が変えられたり、ファイルサイズも小さいので、ファイルサイズを抑えることができます。
Font Awesome導入方法
そんなWebアイコンフォントを導入するには、いつくかサービスはありますが、Font Awesomeがおすすめです
ここではFont Awesomeの使い方を紹介していきます。導入方法は簡単で、アイコンの種類もたくさんあります。まず以下のコードをHTMLの<head>内に書きます。
HTMLの<head>内<link href="https://use.fontawesome.com/releases/v5.6.1/css/all.css" rel="stylesheet">Font Awesomeのサイトに行き、使いたいアイコンを選びます
使いたいアイコンをクリックし、Start Using This Iconをクリック
※最近の新しいバージョンはメールアドレスを登録して使えるようになっています。
メールアドレスを入力し、Create & Use This kitをクリック
以下のようなメールが届くので、Click To Confirm Your Email Address + Set Thisgs upをクリックし、設定します
会員登録を無事完了するとKit CodeというものがCopyできるので、それをHTMLの<head>内に埋め込みましょう
後は先程と同じように導入したいアイコンを選択して入れます。アイコンを編集してみよう
アイコンの導入が完了しました!!導入しただけのアイコンは、小さく、色は黒です。
大きさを変えたり、CSSを使ってよりおしゃれなアイコンにしてみましょうアイコンの大きさを変える
FontAwesomeでは、アイコンの大きさを簡単に変えられます。
使用するアイコン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倍
自分のページのバランスなどを考えるといいですね!
もちろん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; }色が付けられました。SNSのマークなどは、そのままの色を再現するとわかりやすいです。
アイコンと文字の間にスペースを作る
アイコンと文字の間にスペースがないと、詰まって見えてしまいます。
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>少し余白が入り、違和感がなくなりましたね。
回転させる(アニメーション)
何と簡単に回転させることなんかも可能です。
使用するアイコン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>便利なのでぜひFont Awesome使ってみてくださいね
- 投稿日:2020-05-16T08:44:01+09:00
【Rails】テーブルを分けて複数の画像をアップロードする
複数枚の写真を一度に保存する機能の実装において、
【itemテーブル】と【item_imageテーブル】の二つに
分けて複数枚の写真を保存する機能を実装した際の手順をまとめたのでご紹介します。完成形
○ 商品写真は最大で10枚まで投稿可能。
○ 一つのファイルフィールドに一つの写真でアップロードしていく。
○ 5枚投稿時点で下段に切り替わる。
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.rbdef item_images_number errors.add(:item_images, "は10枚までです") if item_images.size > 10 end
以上で終了です。
ご覧いただきありがとうございました。
- 投稿日:2020-05-16T05:04:09+09:00
初心者によるプログラミング学習ログ 317日目
100日チャレンジの317日目
twitterの100日チャレンジ#タグ、#100DaysOfCode実施中です。
すでに100日超えましたが、継続。
100日チャレンジは、ぱぺまぺの中ではプログラミングに限らず継続学習のために使っています。
317日目は、おはようございます
— ぱぺまぺ@webエンジニアを目指したい社畜 (@yudapinokio) May 15, 2020
317日目 2h
・架空LP模写8日目1h(@ririru_123)
・架空LP模写:制作一覧、スキル部分を作成
・ポートフォリオ作成 1h
寝落ちした#早起きチャレンジ#駆け出しエンジニアと繋がりたい#100DaysOfCode
- 投稿日:2020-05-16T02:19:10+09:00
html&css markerで任意の目次を作る
最近
極鶏のラーメンが食べたいです
ついでに言うと猪一のラーメンも食べたいです目次
- 目標
- HTML
- CSS
目標
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.cssli{ 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)'. '; }おまけ
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