20211124のHTMLに関する記事は5件です。

メッセージを送信後、画面最下部まで自動でスクロールする(React)

何をやるのか LINEのように、チャット画面で新規メッセージを送信後、画面最下部まで自動でスクロールする機能の実装方法について解説します。 Reactで作ったSPAが前提の方法です。 1. チャット画面を実装 まずはサンプルとなるチャット画面を実装。 ChatScreen.jsx import React, { useState } from 'react'; import './ChatScreen.css'; // ダミーデータを生成 const dummyMessages = Array(30) .fill({}) .map((_, index) => ({ id: index, content: `ダミーメッセージ #${index + 1}`, })); export const ChatScreen = () => { const [messages, setMessages] = useState(dummyMessages); const [inputValue, setInputValue] = useState(''); const onSendMessage = () => { setMessages(messages.concat({ id: messages.length + 1, content: inputValue })); setInputValue(''); }; return ( <div className='container'> <ul className='messages-container'> {messages.map((message, index) => ( <li key={message.id} className='message'> <p>{message.content}</p> </li> ))} </ul> <div className='input-container'> <input type='text' className='input' value={inputValue} onChange={({ target }) => setInputValue(target.value)} /> <button className='button' onClick={onSendMessage}> 送信 </button> </div> </div> ); }; ChatScreen.css .container { overflow-y: overlay; display: inline-block; } .messages-container { display: flex; flex-direction: column; padding: 50px; margin-bottom: 100px; } .message { padding: 5px 20px; background-color: #dcf8c6; display: block; border-radius: 10px; margin-top: 10px; margin-bottom: 10px; clear: both; } .input-container { display: flex; flex-direction: column; justify-content: space-around; height: 100px; padding: 5px; width: 100%; position: fixed; bottom: 30px; } .input { width: 60%; margin: auto; border: 1px silver solid; font-size: 15px; line-height: 40px; } .button { width: 100px; margin: auto; } 仕上がりはこんな感じです。 2. Reactのrefを使って末尾のメッセージを管理 (1) useRefでrefを定義 ChatScreen.tsx const footRef = useRef(null) React HookのuseRefでfootRefオブジェクトを定義。 デフォルト値はnull。 (2) 末尾のメッセージへrefを付与 ChatScreen.jsx {messages.map((message, index) => ( <li key={message.id} className='message'> <p>{message.content}</p> {/* 以下の一行を追加 */} {index === messages.length - 1 && <div ref={footRef}></div>} </li> ))} messages配列の末尾の要素、つまり最下部に表示されるメッセージへfootRefを付与。 3. useLayoutEffectで自動スクロール ChatScreen.jsx useLayoutEffect(() => { footRef.current.scrollIntoView(); }, [messages]); useLayoutEffect関数を記述し、第二引数の配列に渡された値(messages)の変更があるたびに, "footRef.current.scrollIntoView()"で最下部の要素を画面上に表示。 最終的なコードは以下。 ChatScreen.jsx import React, { useLayoutEffect, useRef, useState } from 'react'; import './ChatScreen.css'; // ダミーデータ const dummyMessages = Array(30) .fill({}) .map((_, index) => ({ id: index, content: `ダミーメッセージ #${index + 1}`, })); export const ChatScreen = () => { const [messages, setMessages] = useState(messagesData); const [inputValue, setInputValue] = useState(''); const footRef = useRef(null); const onSendMessage = () => { setMessages(messages.concat({ id: messages.length + 1, content: inputValue })); setInputValue(''); }; useLayoutEffect(() => { footRef.current.scrollIntoView(); }, [messages]); return ( <div className='container'> <ul className='messages-container'> {messages.map((message, index) => ( <li key={message.id} className='message'> <p>{message.content}</p> {/* 以下の一行を追加 */} {index === messages.length - 1 && <div ref={footRef}></div>} </li> ))} </ul> <div className='input-container'> <input type='text' className='input' value={inputValue} onChange={({ target }) => setInputValue(target.value)} /> <button className='button' onClick={onSendMessage}> 送信 </button> </div> </div> ); }; こんな感じで自動スクロールが実現できました。 今回はチャット画面を題材として扱いましたが、こちらのテクニックは割といろんな場面で応用できるのではないかなと思います。 こちらあくまで一方法となりますのでご参考までに。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Webの勉強はじめてみた その2

2つやろうと思ったんですが、疲れたので1つだけ。 今日やったこと h/ p/ a/ img/ ul/ ol/ table タグ 気付いたこと h1~h6:余白の大きさも変わる。 p:前後に適当な余白ができる。 余白の考え方というか、たとえばpを二つ並べたときに真ん中にできる余白はどう考えるのか。和になるのか相殺されるのか。 大きいほうが優先? a:target="_blank"の脆弱性とは。 動画では本筋とあまり関係ない話として軽く触れられただけだったので調べてみた。 こちら具体例があって理解しやすかったです。 https://lpeg.info/security/target_blank.html tableタグは現在あまり推奨されていないらしい。 flexboxというのがあるらしい。 floatもないの? このあたりは多分少し先の講義でわかるのかな? まとめ VS CodeのEmmet機能がちょっとした呪文。 table>(tr>th*2)+(tr>td*2)*6
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CodeIgniter3でViewファイルをもとにHTMLファイルを出力する

こんなこと誰かがやってそうなもんですけど、全然記事が見つからないので 自分で実装することにしました。今更こんな枯れたフレームワーク使ってるとこないか もっと良い方法あったら教えてちょ # ファイル出力テスト用のディレクトリを作成して書き込み権限を付与 mkdir /var/www/html/test/ sudo chmod -R 777 /var/www/html/test/ # ファイル出力テスト用のControllerを作成 touch /var/www/html/admin/application/controllers/tools/View.php # ファイル出力テスト用のViewを作成(レンダリングにはTwigライブラリが必要です) touch /var/www/html/admin/application/views/user/test/hello.twig.html /var/www/html/admin/application/controllers/tools/View.php <?php defined('BASEPATH') OR exit('No direct script access allowed'); class View extends CI_Controller { public function __construct() { parent::__construct(); $this->load->helper("file"); } public function test_output() { $data = "Hello World"; // FCPATH は "/var/www/html/admin/public" を返す( "/var/www/html/admin/public/index.php" で定義) if (write_file(FCPATH . '../../test/hello.html', $data) == FALSE){ echo 'Unable to write the file'; } else { echo 'File written!'; } } public function test_output2() { $this->load->library('twig'); $filePath = "user/test/hello"; $data = array( "test" => "Hello World2" ); $html = $this->twig->render($filePath, $data); if (write_file(FCPATH . '../../test/hello2.html', $html) == FALSE){ echo 'Unable to write the file'; } else { echo 'File written!'; } } } /var/www/html/admin/application/views/user/test/hello.twig.html <!DOCTYPE html> <html> <head></head> <body> <p>{{ test }}</p> </body> </html> http://www.example.com/admin/tools/view/test_output にアクセスすれば、以下の場所にHTMLファイルが出力されます cat /var/www/html/test/hello.html Hello World http://www.example.com/admin/tools/view/test_output2 にアクセスすれば、以下の場所にHTMLファイルが出力されます cat /var/www/html/test/hello2.html <!DOCTYPE html> <html> <head></head> <body> <p>Hello World2</p> </body> </html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RTAイベント向け簡易背景システムをつくったよ

今回のレギュレーション この記事で得られる知識 そもそもRTAとは? 作ったものは? なぜ今回の形になったのか? 当日の運営について 完走した感想 この記事で得られる知識 サーバがない中で構築する動的背景の作成 NodeCGでやろう!と思える心 レガシーできたないソースは読みづらいという見本。 そもそもRTAとは? RTAとはReal Time Attackの略なのですが、 簡単に言うとゲームの早解きのことです。 どんな手を使ってもいいから何人かの人たちが自分の得意なゲームをいかに早くクリアするかを突き詰めるといったもので 世界ではGames Done Quickというアメリカで行われるイベントが一番有名だったりします。 日本でもRTA in Japanというイベントが年に2回開かれており、 今年も12月26日から6日間、24時間開催される予定となっています。 https://nlab.itmedia.co.jp/nl/articles/2012/28/news054.html こちらの記事を見ていただけると多分その様子がよく分かるかと思います。 わからなくなるかもしれませんが、でも実際にこういった物理的な手を使ってでもクリアしたら正義です。 これらのイベントは主にTwitchで行われ、イベント中に得たチアー(投げ銭)やサブスクは 全て国境なき医師団のような慈善事業団体へ寄付されます。(RTA in Japanでは2000万円ほどあつめ、それを募金しています) 皆さん何卒よろしくお願いいたします(私は運営ボランティアでの参加予定) さて、今回このシステムを利用したのは個人が運営する同様のRTAイベントで、 1ゲーム辺り数時間かかる長めのゲームのRTAイベントです。 作ったものは? ゲームを実況する際に表示するこのようなものを作成しました。 走者(プレイをする人)の情報や、解説者の情報、次の走者と走るタイトルの情報、 あと、実際に現在走っているゲームのタイトル、カテゴリ、EST(クリアまでどのぐらいかかるか?の目安)を表示しているのですが、 こちらのデザインとプログラムを担当し、作成いたしました。 今回は2回目の開催となったのですが、1回目がこちら(私はこの時作成には関わってないです) 固定の画像を背景として表示させていたため、突然なにかが変更になったときに対応が取りづらいため、 こちらを今回動的に変更できるようにしました。 今回のがこちら。 こちらなんですが本来はNodeCGを使えばもっと楽ができたのですが、 今回サーバがなかったためそちらを即断念。(後から別の運営担当が自前サーバを用意してくれたんですが…) 最初は静的なHTMLを走者に配り、OBSという配信ソフトでよみこんでもらうことで こちらを実現しておりました。 で、走者情報や次の走者情報などは、スプレッドシートからデータをとってきて、 運営その読み込み対象番号を変える事で切り替わっていくようにしていました。 さて、ここからが実際のソースと内容になります。 一部黒塗りさせていただきましたが、このように走者の一覧がスプレッドシートに書かれているので、 こちらをAppsScriptで読み込むように変更しました function loading() { var ss = SpreadsheetApp.openById("XXXXXXXXXXXXXXXXXXXXXXXXX"); var listss = ss.getSheetByName("スケジュール"); var nowList = listss.getRange("K2").getValue(); let i = 0; while(listss.getRange("A4").offset(i , 0).getValues() != ""){ var nowRow = listss.getRange("K4").offset(i , 0).getValue(); if(nowRow == nowList){ return listss.getRange(4 + i ,3 , 1 ,19 ).getValues(); } i++; } return "Final"; } function doGet(e) { return ContentService.createTextOutput(loading()).setMimeType(ContentService.MimeType.TEXT); } もう少しスマートな書き方があったかとはおもうのですが、 とりあえず自分しかみないソースだし適当で良いか というのが上記のような惨状です。 現在の走者番号がK2のセルに入っているのでそれを取得し、 K2に設定されている番号と同じものを探して見つかったらその行を取得して、行の内容全てを返してあげるというものです。 このGASを直接叩くとこんな感じの内容が帰ってきます。 塩化コボルト,天外魔境ZERO,Any%(with Turbo/Auto Fire),コマンド選択制RPG,Sat Dec 30 1899 14:26:01 GMT+0900 (日本標準時),5:40:00,Sat Dec 30 1899 09:13:59 GMT+0900 (日本標準時),Sat Dec 30 1899 14:41:00 GMT+0900 (日本標準時),2,塩化コボルト,XXXXX,twitch,,,,Ever Oasis 精霊とタネビトの蜃気楼,Any%,ショーダイ, あとはこれを本体のHTMLの方で受け取って、分解して入れ込んでいく作業。…といった感じです。 main.js $(function(){ $.ajax({ type: 'GET', url:'[GASのURL]', dataType:"text", success: function(data){ // HTML上の必要な箇所に値を設定します。 const obj = ["title","cate","est","runName","runID","runIcon","comName","comID","comIcon","nextTitle","nextCategory","nextRunner","nextCommentator"]; const colNum = [1,2,5,9,10,11,12,13,14,15,16,17,18]; const iconImage = { "youtube":"yt.png", "twitch":"tw.png", "nico":"nc.png", "twitter":"tt.png" }; var textLength = data.split(","); $("#cate").text(textLength[0]); var colArr = ["comName","comID"]; for(let i=0;i < obj.length;i++){ var clname = "#"+ obj[i]; if(obj[i] == "comName" && textLength[colNum[i]] == "") { $("#commentator").css("display","none"); $("#commentator2").css("display","none"); $(".freeSpace").css("height","300px"); }else if(obj[i] == "comName" && textLength[colNum[i]].indexOf("/") < 1){ $(".freeSpace").css("height","210px"); $("#commentator2").css("display","none"); }else if(obj[i] == "comName" && textLength[colNum[i]].indexOf("/") >= 1){ $(".freeSpace").css("height","130px"); } if(obj[i] == "comIcon" && textLength[colNum[i]].indexOf("/") >= 1){ var listIcon = textLength[colNum[i]].split("/"); for (let index = 0; index < listIcon.length; index++) { const element = listIcon[index]; $(clname + (index + 1)).addClass(element); } }else if(obj[i] == "comIcon" && textLength[colNum[i]] != "" ){ $(clname + 1).addClass( textLength[colNum[i]]); }else if(obj[i].match(/Icon/) && textLength[colNum[i]] != ""){ $(clname).addClass(textLength[colNum[i]]); }else if(obj[i] == "title" && textLength[colNum[i]] != ""){ var titleLen = textLength[colNum[i]].replace(/\r?\n/g , ""); if (titleLen.length > 40 ){ $(clname).css("font-size","100%"); } $(clname).text(titleLen); }else if(obj[i] == "cate"){ if (textLength[colNum[i]].length > 30 ){ $(clname).css("font-size","16px"); } $(clname).text(textLength[colNum[i]]); }else if(obj[i] == "est"){ $(clname).text("EST:" + textLength[colNum[i]]); }else if(obj[i] == "nextTitle" && textLength[colNum[i]] != ""){ if (titleLen.length > 40 ){ $(clname).css("font-size","16px"); } $(clname).text(textLength[colNum[i]].replace(/\r?\n/g , "")); }else if(obj[i] == "nextRunner" && textLength[colNum[i]] != ""){ $(clname).text("Runner:" + textLength[colNum[i]]); }else if(obj[i] == "nextCommentator" && textLength[colNum[i]] != ""){ $(clname).text("Commentator:" + textLength[colNum[i]]); }else if(obj[i] == "nextCategory" && textLength[colNum[i]] != ""){ if(textLength[colNum[i]].length > 30){ $(clname).css("height","60px") } $(clname).text("Category:" + textLength[colNum[i]]); }else if(obj[i] == "nextCommentator" && textLength[colNum[i]] == ""){ $(clname).remove(); }else if(colArr.includes(obj[i]) && textLength[colNum[i]] != "" ){ var listCom = textLength[colNum[i]].split("/"); for (let index = 0; index < listCom.length; index++) { const element = listCom[index]; $(clname + (index + 1)).text(listCom[index]); } }else{ $(clname).text(textLength[colNum[i]]); } } } }); }); リファクタリングしてないがゆえにここまで汚くなってしまったわけですが、 こうなったのにも理由があり、あとから色々変わっていったのと時間がなく… 自分のレベルの低さに恐れおののいてしまいますね。 もっとクラスを有効に使うべきでしたし、 とりあえず動けばいいやろで作ってしまったのが問題でした。 あと、HTMLの方は割愛しますが、地味に歯車が右へ左へと切り替わりつつ回っているので動画にてご注目いただきたいなというところです。 なぜ今回の形になったのか? 先項でも述べた通り、サーバがない状態で走者にHTMLの塊だけ渡して運営しようとしていたためです。 サーバがない状態でも突然の対応に走者が追われないために作成しました。 当日の運営について 当日はあくまでスプレッドシートをいじりさえすればOKの状態にしていました。 突然なにか変更があった場合もスプレッドシートの情報さえ変更すれば、 走者に影響なく運営はできたので、概ね思った通りの動きができていたと思います。 あと、別途裏でオープニングを作成してほしいと数時間前に言われて 慌てて作成したり… それと、エンディングも必要と言われるだろうと想定し、 エンドロールとエンディング用画面を作成しました https://www.twitch.tv/videos/1159875157?t=00h09m15s 前もっていってほしかった… 完走した感想 二度とこんな形で作りたくない。 サーバがあるならNodeCGを使おう。 まじで。 ライブ配信レイアウトを作るNode.jsのフレームワーク https://qiita.com/Hoishin/items/36dcea6818b0aa9bf1cd 1から学ぶNodeCG#1:NodeCG導入編 https://qiita.com/cma2819/items/e775bd8aa2a2fa911d4c NodeCGで配信を華やかに https://qiita.com/secchanu/items/f422f1e101cc85caf40a RTA in JapanのNodeCGレイアウトを動かしてみる https://qiita.com/pasta04/items/d676d9c2fb716176f665
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javascript カレンダー作成

はじめに 本稿では下記動画にあるソースコードを解説します。 動画にあるカレンダーは、他のライブラリに依存しない実装になっています。 簡単なカレンダーが表示できれば十分、かつ、依存ライブラリを増やしたくない場合などで使えると思います。 ソースコード https://github.com/lashaNoz/Calendar カレンダー作成に必要な変数 // 今日の日付 EX) 2021/11/24 const date = new Date(); // 月末 EX) 2021/11/30 const lastDay = new Date(date.getFullYear(),date.getMonth() + 1,0).getDate(); // 先月末 EX) 2021/10/31 const prevLastDay = new Date(date.getFullYear(),date.getMonth(),0).getDate(); // 月初の曜日。0 ~ 6 = 日曜 ~ 土曜 EX) 1 (2021/11/1は月曜日) const firstDayIndex = date.getDay(); // 月末の曜日。 EX) 2 (2021/11/30は火曜) const lastDayIndex = new Date(date.getFullYear(), date.getMonth() + 1, 0).getDay(); 日付表示 calndar.js // 1.前の月 // firstDayIndexは月初の曜日。EX) 1(月曜) // prevLastDayは先月の末日。EX) 31 (10/31) for(let i = firstDayIndex; i>0; --i){ days += `<div class="prev-date">${prevLastDay - i + 1}</div>`; } // 2.今月 const today = new Date().getDate(); for(let i = 1; i<=lastDay; ++i){ if(i === today){ days += `<div class="today">${i}</div>`; } else { days += `<div>${i}</div>`; } } // 3.次の月 const sunday = 6; for(let i = 1; i<=sunday-lastDayIndex; ++i){ days += `<div class="prev-date">${i}</div>`; } 前月、次月表示ボタン押下時処理 document.querySelector(".prev").addEventListener("click", () => { // 前月のカレンダーを表示 date.setMonth(date.getMonth() - 1); renderCalendar(); }); document.querySelector(".next").addEventListener("click", () => { // 次月のカレンダーを表示 date.setMonth(date.getMonth() + 1); renderCalendar(); }); DOM読み込み完了後にJavascript実行(おまけ) 動画の実装だと、javascriptを読み込むタイミングによってはカレンダーが表示されません。 window.onload内に処理を定義することで、DOM読み込みが完了した後に処理が実行されます。 DOMContentLoaded イベントに処理を定義すると、DOM読み読み込み完了後に処理が実行されます。 window.addEventListener('DOMContentLoaded', () => { // DOM読み込み完了のタイミングで実行したい処理 const date = new Date(); const renderCalendar = function(){ date.setDate(1); ~~~ });
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む