[{"data":1,"prerenderedAt":2224},["ShallowReactive",2],{"\u002Fblog\u002Fprogramming\u002Fchat-scroll":3,"relation-articles\u002Fblog\u002Fprogramming\u002Fchat-scrollシステム開発":2138,"recommented-articles\u002Fblog\u002Fprogramming\u002Fchat-scrollシステム開発":2187},{"id":4,"title":5,"body":6,"category":2123,"createdAt":2124,"description":2125,"extension":2126,"isRecommend":214,"meta":2127,"navigation":214,"path":2128,"seo":2129,"stem":2130,"subCategory":2131,"tags":2132,"thumbnail":2135,"updatedAt":2136,"__hash__":2137},"blog\u002Fblog\u002Fprogramming\u002Fchat-scroll.md","メッセージ送受信時にスクロール位置を最下部に維持する処理を実装【TypeScript\u002FVue.js】",{"type":7,"value":8,"toc":2105},"minimark",[9,13,17,30,34,38,41,44,53,56,65,68,71,84,86,89,489,496,498,501,643,653,666,668,671,931,946,959,972,975,1074,1108,1115,1257,1261,1264,1930,1945,1948,1952,1967,1971,1974,1981,1984,1993,1996,2006,2009,2061,2102],[10,11,12],"h2",{"id":12},"概要",[14,15,16],"p",{},"SlackやLINE、ChatGPTのようなチャットのUIでは最新のメッセージが最下部になっていることが多いです。",[14,18,19,20,23,24,26,27,29],{},"上記のUIでは新規のメッセージが追加されたとき、",[21,22],"br",{},"\n元のスクロール位置が最下部 → 最下部を維持",[21,25],{},"\n元のスクロール位置が最下部以外 → 元のスクロール位置を維持",[21,28],{},"\nという仕様になっています。",[31,32,33],"h3",{"id":33},"動作イメージ",[35,36],"img",{"alt":33,"src":37},"https:\u002F\u002Fimg-blog.ashcolor.jp\u002Fvideo\u002Fprogramming_chat-scroll_chat-scroll.gif",[14,39,40],{},"この機能をVue.jsで実装したので、実装方法を記載します。",[31,42,43],{"id":43},"完成品",[14,45,46,52],{},[47,48,51],"span",{"className":49},[50],"marker","Vue3（Composition API）","で作成しました。",[14,54,55],{},"以下のリンク先でコードとプレビューを確認できます。",[14,57,58],{},[59,60,64],"a",{"href":61,"rel":62},"https:\u002F\u002Fplay.vuejs.org\u002F#eNqNVc1u00AQfpWRL91IiZNWHJBxAkGq1EMjJOgNc3DiTbzC9lq7a6clyqW58RTwFkiAeJgIicdg1uvfuC1covXMt\u002FPNzDc72VnzNLXzjFqO5cqVYKkCSVWWQuQnm6lnKelZMy9hccqFgh0Iuh7C1lercAgrHqeZosEQeLLgWYJH2MNa8Bg8C2N61gsv8ZIVT6SCmErpb6iEqY7hzoXw71ypBEs2sxlJ6BY6JvJ8YK9ZFBHPOh6+Hg+H4\u002F334+HH8f6bZw0GGNdE9YNgYQJjXKLorXLARBjAdAY7L4Ga2c79KKN2msmwQOoo+36Gc0H9y4jGNFFlrlc3i+vSMiMN9ypiaLmibBOWSDJpvNhMHkVPe2942nZVTibfFe65es2Vwm5O61YT0tQlUCeRANFngIWvQttfStImNiXDqJNrbayTMJYBuHCugxWNMQnVwraJ+40qW4tqXOb4ec2kogkVKJ7h8KwhEKpdDhSIJhicpoHVFkhb+WJDlV27MSGAMi\u002FAjuhWCSrZJ\u002FpmKanIqcCrepDedoyEYDDBqGxzmtuGQRMaxPvJh5clbUGGuH7fpuU1u+1rctO6tOltbg7ksa51CyrRVSmLTPmK8aQpJi4t\u002FWoQb+amHroKi3V1u9lKGoCtgZyOXDURJcETmlfykBoKoHjq9NJ5aAiHzZ0lDf2cceHg7pAx5yrEoancZWfx0B6ZE1lOCNuSVF39pxhDXHGrkEWBHmEHlNAmmS2VoNR8mpDFjzs2GxP3I34oGqeRryh+AbgBy\u002FXDxg3ap\u002FIsbIUvZeMc+egtFq2urricj9ZcNAhgSb3IaqCB1h8Au10Fgn3ZKsSMG1Bzbp2WGUqewCvUZ\u002FURKZulSs5Olu\u002FZAMlPbH9+\u002Ffz9+Ys7NmHq8lvFzNrKO5hkZ3Xu950M9bGtr8Z3lumjeNwRDVjv1geQp4OuL\u002FT27QP3qmZ2MumOdHujYthGlf+8Uw97qVxfK3fcGjL8lOouohiApzRAi92eJvN0tyxQoQMXzybpbfESQtNTuJjkYWFYchFQfHXn6S1IHrHAPBh8LOuIb0d3+H9aJKiHvhh5zYlk1v4vzvbmAg==",[63],"nofollow","Vue-SFC-Playground",[10,66,67],{"id":67},"実装方法",[14,69,70],{},"以下の流れで実装します。",[72,73,74,78,81],"ol",{},[75,76,77],"li",{},"メッセージエリアの作成",[75,79,80],{},"最下部へのスクロール",[75,82,83],{},"最下部にスクロールしているかの判定",[31,85,77],{"id":77},[14,87,88],{},"まずは、メッセージを表示する領域とメッセージを追加するためのボタンを用意します。",[90,91,97],"pre",{"className":92,"code":93,"filename":94,"language":95,"meta":96,"style":96},"language-vue shiki shiki-themes github-dark","\u003Cscript setup lang=\"ts\">\nconst messages = ref\u003CArray\u003Cstring>>([]);\nconst addMessage = (text: string) => {\n  messages.value.push(text);\n};\n\nconst messageAreaElement = ref\u003CHTMLElement>();\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv ref=\"messageAreaElement\" class=\"message-area\">\n    \u003Cdiv v-for=\"message in messages\">\n      \u003Cdiv>\n        {{ message }}\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  \u003C\u002Fdiv>\n  \u003Cbutton @click=\"addMessage('メッセージ')\">メッセージ追加\u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n\n\u003Cstyle scoped>\n.message-area {\n  width: 240px;\n  height: 20vh;\n  border: 1px solid;\n  overflow-y: scroll;\n}\n\u003C\u002Fstyle>\n","App.ts","vue","",[98,99,100,129,159,191,203,209,216,236,246,251,261,287,305,315,321,331,341,351,374,383,388,401,409,427,443,461,474,480],"code",{"__ignoreMap":96},[47,101,104,108,112,116,119,122,126],{"class":102,"line":103},"line",1,[47,105,107],{"class":106},"s95oV","\u003C",[47,109,111],{"class":110},"s4JwU","script",[47,113,115],{"class":114},"svObZ"," setup",[47,117,118],{"class":114}," lang",[47,120,121],{"class":106},"=",[47,123,125],{"class":124},"sU2Wk","\"ts\"",[47,127,128],{"class":106},">\n",[47,130,132,136,140,143,146,148,151,153,156],{"class":102,"line":131},2,[47,133,135],{"class":134},"snl16","const",[47,137,139],{"class":138},"sDLfK"," messages",[47,141,142],{"class":134}," =",[47,144,145],{"class":114}," ref",[47,147,107],{"class":106},[47,149,150],{"class":114},"Array",[47,152,107],{"class":106},[47,154,155],{"class":138},"string",[47,157,158],{"class":106},">>([]);\n",[47,160,162,164,167,169,172,176,179,182,185,188],{"class":102,"line":161},3,[47,163,135],{"class":134},[47,165,166],{"class":114}," addMessage",[47,168,142],{"class":134},[47,170,171],{"class":106}," (",[47,173,175],{"class":174},"s9osk","text",[47,177,178],{"class":134},":",[47,180,181],{"class":138}," string",[47,183,184],{"class":106},") ",[47,186,187],{"class":134},"=>",[47,189,190],{"class":106}," {\n",[47,192,194,197,200],{"class":102,"line":193},4,[47,195,196],{"class":106},"  messages.value.",[47,198,199],{"class":114},"push",[47,201,202],{"class":106},"(text);\n",[47,204,206],{"class":102,"line":205},5,[47,207,208],{"class":106},"};\n",[47,210,212],{"class":102,"line":211},6,[47,213,215],{"emptyLinePlaceholder":214},true,"\n",[47,217,219,221,224,226,228,230,233],{"class":102,"line":218},7,[47,220,135],{"class":134},[47,222,223],{"class":138}," messageAreaElement",[47,225,142],{"class":134},[47,227,145],{"class":114},[47,229,107],{"class":106},[47,231,232],{"class":114},"HTMLElement",[47,234,235],{"class":106},">();\n",[47,237,239,242,244],{"class":102,"line":238},8,[47,240,241],{"class":106},"\u003C\u002F",[47,243,111],{"class":110},[47,245,128],{"class":106},[47,247,249],{"class":102,"line":248},9,[47,250,215],{"emptyLinePlaceholder":214},[47,252,254,256,259],{"class":102,"line":253},10,[47,255,107],{"class":106},[47,257,258],{"class":110},"template",[47,260,128],{"class":106},[47,262,264,267,270,272,274,277,280,282,285],{"class":102,"line":263},11,[47,265,266],{"class":106},"  \u003C",[47,268,269],{"class":110},"div",[47,271,145],{"class":114},[47,273,121],{"class":106},[47,275,276],{"class":124},"\"messageAreaElement\"",[47,278,279],{"class":114}," class",[47,281,121],{"class":106},[47,283,284],{"class":124},"\"message-area\"",[47,286,128],{"class":106},[47,288,290,293,295,298,300,303],{"class":102,"line":289},12,[47,291,292],{"class":106},"    \u003C",[47,294,269],{"class":110},[47,296,297],{"class":114}," v-for",[47,299,121],{"class":106},[47,301,302],{"class":124},"\"message in messages\"",[47,304,128],{"class":106},[47,306,308,311,313],{"class":102,"line":307},13,[47,309,310],{"class":106},"      \u003C",[47,312,269],{"class":110},[47,314,128],{"class":106},[47,316,318],{"class":102,"line":317},14,[47,319,320],{"class":106},"        {{ message }}\n",[47,322,324,327,329],{"class":102,"line":323},15,[47,325,326],{"class":106},"      \u003C\u002F",[47,328,269],{"class":110},[47,330,128],{"class":106},[47,332,334,337,339],{"class":102,"line":333},16,[47,335,336],{"class":106},"    \u003C\u002F",[47,338,269],{"class":110},[47,340,128],{"class":106},[47,342,344,347,349],{"class":102,"line":343},17,[47,345,346],{"class":106},"  \u003C\u002F",[47,348,269],{"class":110},[47,350,128],{"class":106},[47,352,354,356,359,362,364,367,370,372],{"class":102,"line":353},18,[47,355,266],{"class":106},[47,357,358],{"class":110},"button",[47,360,361],{"class":114}," @click",[47,363,121],{"class":106},[47,365,366],{"class":124},"\"addMessage('メッセージ')\"",[47,368,369],{"class":106},">メッセージ追加\u003C\u002F",[47,371,358],{"class":110},[47,373,128],{"class":106},[47,375,377,379,381],{"class":102,"line":376},19,[47,378,241],{"class":106},[47,380,258],{"class":110},[47,382,128],{"class":106},[47,384,386],{"class":102,"line":385},20,[47,387,215],{"emptyLinePlaceholder":214},[47,389,391,393,396,399],{"class":102,"line":390},21,[47,392,107],{"class":106},[47,394,395],{"class":110},"style",[47,397,398],{"class":114}," scoped",[47,400,128],{"class":106},[47,402,404,407],{"class":102,"line":403},22,[47,405,406],{"class":114},".message-area",[47,408,190],{"class":106},[47,410,412,415,418,421,424],{"class":102,"line":411},23,[47,413,414],{"class":138},"  width",[47,416,417],{"class":106},": ",[47,419,420],{"class":138},"240",[47,422,423],{"class":134},"px",[47,425,426],{"class":106},";\n",[47,428,430,433,435,438,441],{"class":102,"line":429},24,[47,431,432],{"class":138},"  height",[47,434,417],{"class":106},[47,436,437],{"class":138},"20",[47,439,440],{"class":134},"vh",[47,442,426],{"class":106},[47,444,446,449,451,454,456,459],{"class":102,"line":445},25,[47,447,448],{"class":138},"  border",[47,450,417],{"class":106},[47,452,453],{"class":138},"1",[47,455,423],{"class":134},[47,457,458],{"class":138}," solid",[47,460,426],{"class":106},[47,462,464,467,469,472],{"class":102,"line":463},26,[47,465,466],{"class":138},"  overflow-y",[47,468,417],{"class":106},[47,470,471],{"class":138},"scroll",[47,473,426],{"class":106},[47,475,477],{"class":102,"line":476},27,[47,478,479],{"class":106},"}\n",[47,481,483,485,487],{"class":102,"line":482},28,[47,484,241],{"class":106},[47,486,395],{"class":110},[47,488,128],{"class":106},[14,490,491,492,495],{},"refオブジェクトを宣言したあと、操作したいDOM要素の",[98,493,494],{},"ref","属性に宣言した変数を渡すとDOMにアクセスできるようになります。",[31,497,80],{"id":80},[14,499,500],{},"次にメッセージエリアにメッセージが追加されたらスクロール位置を最下部に変更する処理を実装します。",[90,502,504],{"className":92,"code":503,"filename":94,"language":95,"meta":96,"style":96},"\u003Cscript setup lang=\"ts\">\n...\nonMounted(() => {\n  const observer = new MutationObserver((mutation) => {\n    messageAreaElement.value.scrollTo({\n      top: 10000\n      behavior: \"smooth\",\n    });\n  });\n  observer.observe(messageAreaElement.value, {\n    childList: true\n  });\n});\n\u003C\u002Fscript>\n",[98,505,506,522,527,539,567,578,586,597,602,607,618,626,630,635],{"__ignoreMap":96},[47,507,508,510,512,514,516,518,520],{"class":102,"line":103},[47,509,107],{"class":106},[47,511,111],{"class":110},[47,513,115],{"class":114},[47,515,118],{"class":114},[47,517,121],{"class":106},[47,519,125],{"class":124},[47,521,128],{"class":106},[47,523,524],{"class":102,"line":131},[47,525,526],{"class":134},"...\n",[47,528,529,532,535,537],{"class":102,"line":161},[47,530,531],{"class":114},"onMounted",[47,533,534],{"class":106},"(() ",[47,536,187],{"class":134},[47,538,190],{"class":106},[47,540,541,544,547,549,552,555,558,561,563,565],{"class":102,"line":193},[47,542,543],{"class":134},"  const",[47,545,546],{"class":138}," observer",[47,548,142],{"class":134},[47,550,551],{"class":134}," new",[47,553,554],{"class":114}," MutationObserver",[47,556,557],{"class":106},"((",[47,559,560],{"class":174},"mutation",[47,562,184],{"class":106},[47,564,187],{"class":134},[47,566,190],{"class":106},[47,568,569,572,575],{"class":102,"line":205},[47,570,571],{"class":106},"    messageAreaElement.value.",[47,573,574],{"class":114},"scrollTo",[47,576,577],{"class":106},"({\n",[47,579,580,583],{"class":102,"line":211},[47,581,582],{"class":106},"      top: ",[47,584,585],{"class":138},"10000\n",[47,587,588,591,594],{"class":102,"line":218},[47,589,590],{"class":106},"      behavior: ",[47,592,593],{"class":124},"\"smooth\"",[47,595,596],{"class":106},",\n",[47,598,599],{"class":102,"line":238},[47,600,601],{"class":106},"    });\n",[47,603,604],{"class":102,"line":248},[47,605,606],{"class":106},"  });\n",[47,608,609,612,615],{"class":102,"line":253},[47,610,611],{"class":106},"  observer.",[47,613,614],{"class":114},"observe",[47,616,617],{"class":106},"(messageAreaElement.value, {\n",[47,619,620,623],{"class":102,"line":263},[47,621,622],{"class":106},"    childList: ",[47,624,625],{"class":138},"true\n",[47,627,628],{"class":102,"line":289},[47,629,606],{"class":106},[47,631,632],{"class":102,"line":307},[47,633,634],{"class":106},"});\n",[47,636,637,639,641],{"class":102,"line":317},[47,638,241],{"class":106},[47,640,111],{"class":110},[47,642,128],{"class":106},[14,644,645,648,649,652],{},[98,646,647],{},"MutationObserver","を",[98,650,651],{},"childList: true","として呼び出すと、DOMに要素が追加されたことを検知できます。",[14,654,655,658,659,661,662,665],{},[98,656,657],{},"scrollTo()","を使うとスクロール位置を変更できます。",[21,660],{},"\nスクロール位置は",[98,663,664],{},"top:スクロールするピクセル数","で指定するのですが、最大値を超えていたらスクロールできる最大値に設定されるため、ここでは仮に大きめな値を入れてあります。（後で正しい値に修正します。）",[31,667,83],{"id":83},[14,669,670],{},"最下部にスクロールしているかの判定に必要な変数を用意します。",[90,672,674],{"className":92,"code":673,"filename":94,"language":95,"meta":96,"style":96},"\u003Cscript setup lang=\"ts\">\n...\nconst clientHeight = ref(0);\nconst scrollHeight = ref(0);\nconst scrollTop = ref(0);\n\nonMounted(() => {\n  messageAreaElement.value.addEventListener(\"scroll\", (event: Event) => {\n    scrollTop.value = event.target.scrollTop;\n  });\n\n  const resizeObserver = new ResizeObserver((entries) => {\n    const target = entries[0]?.target;\n    clientHeight.value = target.clientHeight;\n  });\n  resizeObserver.observe(messageAreaElement.value);\n\n  const observer = new MutationObserver((mutation) => {\n    scrollHeight.value = newScrollHeight;\n  });\n});\n\u003C\u002Fscript>\n",[98,675,676,692,696,716,733,750,754,764,794,804,808,812,837,855,865,869,879,883,905,915,919,923],{"__ignoreMap":96},[47,677,678,680,682,684,686,688,690],{"class":102,"line":103},[47,679,107],{"class":106},[47,681,111],{"class":110},[47,683,115],{"class":114},[47,685,118],{"class":114},[47,687,121],{"class":106},[47,689,125],{"class":124},[47,691,128],{"class":106},[47,693,694],{"class":102,"line":131},[47,695,526],{"class":134},[47,697,698,700,703,705,707,710,713],{"class":102,"line":161},[47,699,135],{"class":134},[47,701,702],{"class":138}," clientHeight",[47,704,142],{"class":134},[47,706,145],{"class":114},[47,708,709],{"class":106},"(",[47,711,712],{"class":138},"0",[47,714,715],{"class":106},");\n",[47,717,718,720,723,725,727,729,731],{"class":102,"line":193},[47,719,135],{"class":134},[47,721,722],{"class":138}," scrollHeight",[47,724,142],{"class":134},[47,726,145],{"class":114},[47,728,709],{"class":106},[47,730,712],{"class":138},[47,732,715],{"class":106},[47,734,735,737,740,742,744,746,748],{"class":102,"line":205},[47,736,135],{"class":134},[47,738,739],{"class":138}," scrollTop",[47,741,142],{"class":134},[47,743,145],{"class":114},[47,745,709],{"class":106},[47,747,712],{"class":138},[47,749,715],{"class":106},[47,751,752],{"class":102,"line":211},[47,753,215],{"emptyLinePlaceholder":214},[47,755,756,758,760,762],{"class":102,"line":218},[47,757,531],{"class":114},[47,759,534],{"class":106},[47,761,187],{"class":134},[47,763,190],{"class":106},[47,765,766,769,772,774,777,780,783,785,788,790,792],{"class":102,"line":238},[47,767,768],{"class":106},"  messageAreaElement.value.",[47,770,771],{"class":114},"addEventListener",[47,773,709],{"class":106},[47,775,776],{"class":124},"\"scroll\"",[47,778,779],{"class":106},", (",[47,781,782],{"class":174},"event",[47,784,178],{"class":134},[47,786,787],{"class":114}," Event",[47,789,184],{"class":106},[47,791,187],{"class":134},[47,793,190],{"class":106},[47,795,796,799,801],{"class":102,"line":248},[47,797,798],{"class":106},"    scrollTop.value ",[47,800,121],{"class":134},[47,802,803],{"class":106}," event.target.scrollTop;\n",[47,805,806],{"class":102,"line":253},[47,807,606],{"class":106},[47,809,810],{"class":102,"line":263},[47,811,215],{"emptyLinePlaceholder":214},[47,813,814,816,819,821,823,826,828,831,833,835],{"class":102,"line":289},[47,815,543],{"class":134},[47,817,818],{"class":138}," resizeObserver",[47,820,142],{"class":134},[47,822,551],{"class":134},[47,824,825],{"class":114}," ResizeObserver",[47,827,557],{"class":106},[47,829,830],{"class":174},"entries",[47,832,184],{"class":106},[47,834,187],{"class":134},[47,836,190],{"class":106},[47,838,839,842,845,847,850,852],{"class":102,"line":307},[47,840,841],{"class":134},"    const",[47,843,844],{"class":138}," target",[47,846,142],{"class":134},[47,848,849],{"class":106}," entries[",[47,851,712],{"class":138},[47,853,854],{"class":106},"]?.target;\n",[47,856,857,860,862],{"class":102,"line":317},[47,858,859],{"class":106},"    clientHeight.value ",[47,861,121],{"class":134},[47,863,864],{"class":106}," target.clientHeight;\n",[47,866,867],{"class":102,"line":323},[47,868,606],{"class":106},[47,870,871,874,876],{"class":102,"line":333},[47,872,873],{"class":106},"  resizeObserver.",[47,875,614],{"class":114},[47,877,878],{"class":106},"(messageAreaElement.value);\n",[47,880,881],{"class":102,"line":343},[47,882,215],{"emptyLinePlaceholder":214},[47,884,885,887,889,891,893,895,897,899,901,903],{"class":102,"line":353},[47,886,543],{"class":134},[47,888,546],{"class":138},[47,890,142],{"class":134},[47,892,551],{"class":134},[47,894,554],{"class":114},[47,896,557],{"class":106},[47,898,560],{"class":174},[47,900,184],{"class":106},[47,902,187],{"class":134},[47,904,190],{"class":106},[47,906,907,910,912],{"class":102,"line":376},[47,908,909],{"class":106},"    scrollHeight.value ",[47,911,121],{"class":134},[47,913,914],{"class":106}," newScrollHeight;\n",[47,916,917],{"class":102,"line":385},[47,918,606],{"class":106},[47,920,921],{"class":102,"line":390},[47,922,634],{"class":106},[47,924,925,927,929],{"class":102,"line":403},[47,926,241],{"class":106},[47,928,111],{"class":110},[47,930,128],{"class":106},[14,932,933,936,937,939,942,943,945],{},[98,934,935],{},"clientHeight","は見た目上の要素の高さです。",[21,938],{},[98,940,941],{},"resizeObserver","を使うことによって",[98,944,935],{},"の変更を検知します。",[14,947,948,951,952,954,956,957,945],{},[98,949,950],{},"scrollHeight","はoverflowしていて画面上に表示されない部分を含めた要素の中身の高さです。",[21,953],{},[98,955,647],{},"を使うことによって、メッセージエリアに要素が追加されたときの",[98,958,950],{},[14,960,961,964,965,967,969,970,945],{},[98,962,963],{},"scrollTop","は垂直方向にスクロールされている距離です。",[21,966],{},[98,968,471],{},"イベントが発生したときに",[98,971,963],{},[14,973,974],{},"次にスクロール位置が最下部になっているかの判定をします。",[90,976,978],{"className":92,"code":977,"filename":94,"language":95,"meta":96,"style":96},"\u003Cscript setup lang=\"ts\">\n...\nconst isScrollAtBottom = computed(() => {\n  return (\n    Math.abs(scrollHeight.value - clientHeight.value - scrollTop.value) \u003C 1\n  );\n});\n...\n\u003C\u002Fscript>\n",[98,979,980,996,1000,1018,1026,1053,1058,1062,1066],{"__ignoreMap":96},[47,981,982,984,986,988,990,992,994],{"class":102,"line":103},[47,983,107],{"class":106},[47,985,111],{"class":110},[47,987,115],{"class":114},[47,989,118],{"class":114},[47,991,121],{"class":106},[47,993,125],{"class":124},[47,995,128],{"class":106},[47,997,998],{"class":102,"line":131},[47,999,526],{"class":134},[47,1001,1002,1004,1007,1009,1012,1014,1016],{"class":102,"line":161},[47,1003,135],{"class":134},[47,1005,1006],{"class":138}," isScrollAtBottom",[47,1008,142],{"class":134},[47,1010,1011],{"class":114}," computed",[47,1013,534],{"class":106},[47,1015,187],{"class":134},[47,1017,190],{"class":106},[47,1019,1020,1023],{"class":102,"line":193},[47,1021,1022],{"class":134},"  return",[47,1024,1025],{"class":106}," (\n",[47,1027,1028,1031,1034,1037,1040,1043,1045,1048,1050],{"class":102,"line":205},[47,1029,1030],{"class":106},"    Math.",[47,1032,1033],{"class":114},"abs",[47,1035,1036],{"class":106},"(scrollHeight.value ",[47,1038,1039],{"class":134},"-",[47,1041,1042],{"class":106}," clientHeight.value ",[47,1044,1039],{"class":134},[47,1046,1047],{"class":106}," scrollTop.value) ",[47,1049,107],{"class":134},[47,1051,1052],{"class":138}," 1\n",[47,1054,1055],{"class":102,"line":211},[47,1056,1057],{"class":106},"  );\n",[47,1059,1060],{"class":102,"line":218},[47,1061,634],{"class":106},[47,1063,1064],{"class":102,"line":238},[47,1065,526],{"class":134},[47,1067,1068,1070,1072],{"class":102,"line":248},[47,1069,241],{"class":106},[47,1071,111],{"class":110},[47,1073,128],{"class":106},[14,1075,1076,1077,1080,1081,1083,1084,1086,1089,1090,1092,1093,1095,1096,1098,1099],{},"理論上は",[98,1078,1079],{},"scrollHeight - clientHeight - scrollTop","が",[98,1082,712],{},"のときにスクロール位置が最下部になります。",[21,1085],{},[98,1087,1088],{},"===","で判定していないのは、",[98,1091,963],{},"は小数を含む可能性があるのに対して、",[98,1094,950],{},"と",[98,1097,935],{},"は整数に丸められるため、スクロール量が閾値に十分に近いかで判定する必要があるからです。",[1100,1101,1102],"sup",{},[59,1103,453],{"href":1104,"ariaDescribedBy":1105,"dataFootnoteRef":96,"id":1107},"#user-content-fn-scroll",[1106],"footnote-label","user-content-fnref-scroll",[14,1109,1110,1111,1114],{},"あとはメッセージ追加時に",[98,1112,1113],{},"isScrollAtBottom","がtrueのときだけスクロール位置を最下部にする処理を実行するようにします。",[90,1116,1118],{"className":92,"code":1117,"filename":94,"language":95,"meta":96,"style":96},"\u003Cscript setup lang=\"ts\">\n...\nonMounted(() => {\n  ...\n  const observer = new MutationObserver((mutation) => {\n    const newScrollHeight = mutation[0].target.scrollHeight;\n    if (isScrollAtBottom.value) {\n      messageAreaElement.value.scrollTo({\n        top: newScrollHeight - clientHeight.value,\n        behavior: \"smooth\",\n      });\n    }\n    ...\n});\n\u003C\u002Fscript>\n",[98,1119,1120,1136,1140,1150,1155,1177,1194,1202,1211,1221,1230,1235,1240,1245,1249],{"__ignoreMap":96},[47,1121,1122,1124,1126,1128,1130,1132,1134],{"class":102,"line":103},[47,1123,107],{"class":106},[47,1125,111],{"class":110},[47,1127,115],{"class":114},[47,1129,118],{"class":114},[47,1131,121],{"class":106},[47,1133,125],{"class":124},[47,1135,128],{"class":106},[47,1137,1138],{"class":102,"line":131},[47,1139,526],{"class":134},[47,1141,1142,1144,1146,1148],{"class":102,"line":161},[47,1143,531],{"class":114},[47,1145,534],{"class":106},[47,1147,187],{"class":134},[47,1149,190],{"class":106},[47,1151,1152],{"class":102,"line":193},[47,1153,1154],{"class":134},"  ...\n",[47,1156,1157,1159,1161,1163,1165,1167,1169,1171,1173,1175],{"class":102,"line":205},[47,1158,543],{"class":134},[47,1160,546],{"class":138},[47,1162,142],{"class":134},[47,1164,551],{"class":134},[47,1166,554],{"class":114},[47,1168,557],{"class":106},[47,1170,560],{"class":174},[47,1172,184],{"class":106},[47,1174,187],{"class":134},[47,1176,190],{"class":106},[47,1178,1179,1181,1184,1186,1189,1191],{"class":102,"line":211},[47,1180,841],{"class":134},[47,1182,1183],{"class":138}," newScrollHeight",[47,1185,142],{"class":134},[47,1187,1188],{"class":106}," mutation[",[47,1190,712],{"class":138},[47,1192,1193],{"class":106},"].target.scrollHeight;\n",[47,1195,1196,1199],{"class":102,"line":218},[47,1197,1198],{"class":134},"    if",[47,1200,1201],{"class":106}," (isScrollAtBottom.value) {\n",[47,1203,1204,1207,1209],{"class":102,"line":238},[47,1205,1206],{"class":106},"      messageAreaElement.value.",[47,1208,574],{"class":114},[47,1210,577],{"class":106},[47,1212,1213,1216,1218],{"class":102,"line":248},[47,1214,1215],{"class":106},"        top: newScrollHeight ",[47,1217,1039],{"class":134},[47,1219,1220],{"class":106}," clientHeight.value,\n",[47,1222,1223,1226,1228],{"class":102,"line":253},[47,1224,1225],{"class":106},"        behavior: ",[47,1227,593],{"class":124},[47,1229,596],{"class":106},[47,1231,1232],{"class":102,"line":263},[47,1233,1234],{"class":106},"      });\n",[47,1236,1237],{"class":102,"line":289},[47,1238,1239],{"class":106},"    }\n",[47,1241,1242],{"class":102,"line":307},[47,1243,1244],{"class":134},"    ...\n",[47,1246,1247],{"class":102,"line":317},[47,1248,634],{"class":106},[47,1250,1251,1253,1255],{"class":102,"line":323},[47,1252,241],{"class":106},[47,1254,111],{"class":110},[47,1256,128],{"class":106},[1258,1259,1260],"h4",{"id":1260},"完成",[14,1262,1263],{},"全体のコードは以下のようになります。",[90,1265,1267],{"className":92,"code":1266,"filename":94,"language":95,"meta":96,"style":96},"\u003Cscript setup lang=\"ts\">\nimport { ref, watch, computed, onMounted } from \"vue\";\n\nconst messages = ref\u003CArray\u003Cstring>>([]);\nconst addMessage = (text: string) => {\n  messages.value.push(text);\n};\n\nconst messageAreaElement = ref\u003CHTMLElement>();\nconst clientHeight = ref(0);\nconst scrollHeight = ref(0);\nconst scrollTop = ref(0);\n\nconst isScrollAtBottom = computed(() => {\n  return Math.abs(scrollHeight.value - clientHeight.value - scrollTop.value) \u003C 1;\n});\n\nonMounted(() => {\n  messageAreaElement.value.addEventListener(\"scroll\", (event: Event) => {\n    scrollTop.value = event.target.scrollTop;\n  });\n\n  const resizeObserver = new ResizeObserver((entries) => {\n    const target = entries[0]?.target;\n    clientHeight.value = target.clientHeight;\n  });\n  resizeObserver.observe(messageAreaElement.value);\n\n  const observer = new MutationObserver((mutation) => {\n    const newScrollHeight = mutation[0].target.scrollHeight;\n    if (isScrollAtBottom.value) {\n      messageAreaElement.value.scrollTo({\n        top: newScrollHeight - clientHeight.value,\n        behavior: \"smooth\",\n      });\n    }\n    scrollHeight.value = newScrollHeight;\n  });\n  observer.observe(messageAreaElement.value, { childList: true, subtree: true });\n});\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv ref=\"messageAreaElement\" class=\"message-area\">\n    \u003Cdiv v-for=\"message in messages\">\n      \u003Cdiv>\n        {{ message }}\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  \u003C\u002Fdiv>\n  \u003Cbutton @click=\"addMessage('メッセージ')\">メッセージ追加\u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n\n\u003Cstyle scoped>\n.message-area {\n  width: 240px;\n  height: 20vh;\n  border: 1px solid;\n  overflow-y: scroll;\n}\n\u003C\u002Fstyle>\n",[98,1268,1269,1285,1301,1305,1325,1347,1355,1359,1363,1379,1395,1411,1427,1431,1447,1473,1477,1481,1491,1515,1523,1527,1531,1553,1567,1575,1579,1587,1591,1614,1629,1636,1645,1654,1663,1668,1673,1682,1687,1708,1713,1722,1727,1736,1757,1772,1781,1786,1795,1804,1813,1832,1841,1846,1857,1864,1877,1890,1905,1916,1921],{"__ignoreMap":96},[47,1270,1271,1273,1275,1277,1279,1281,1283],{"class":102,"line":103},[47,1272,107],{"class":106},[47,1274,111],{"class":110},[47,1276,115],{"class":114},[47,1278,118],{"class":114},[47,1280,121],{"class":106},[47,1282,125],{"class":124},[47,1284,128],{"class":106},[47,1286,1287,1290,1293,1296,1299],{"class":102,"line":131},[47,1288,1289],{"class":134},"import",[47,1291,1292],{"class":106}," { ref, watch, computed, onMounted } ",[47,1294,1295],{"class":134},"from",[47,1297,1298],{"class":124}," \"vue\"",[47,1300,426],{"class":106},[47,1302,1303],{"class":102,"line":161},[47,1304,215],{"emptyLinePlaceholder":214},[47,1306,1307,1309,1311,1313,1315,1317,1319,1321,1323],{"class":102,"line":193},[47,1308,135],{"class":134},[47,1310,139],{"class":138},[47,1312,142],{"class":134},[47,1314,145],{"class":114},[47,1316,107],{"class":106},[47,1318,150],{"class":114},[47,1320,107],{"class":106},[47,1322,155],{"class":138},[47,1324,158],{"class":106},[47,1326,1327,1329,1331,1333,1335,1337,1339,1341,1343,1345],{"class":102,"line":205},[47,1328,135],{"class":134},[47,1330,166],{"class":114},[47,1332,142],{"class":134},[47,1334,171],{"class":106},[47,1336,175],{"class":174},[47,1338,178],{"class":134},[47,1340,181],{"class":138},[47,1342,184],{"class":106},[47,1344,187],{"class":134},[47,1346,190],{"class":106},[47,1348,1349,1351,1353],{"class":102,"line":211},[47,1350,196],{"class":106},[47,1352,199],{"class":114},[47,1354,202],{"class":106},[47,1356,1357],{"class":102,"line":218},[47,1358,208],{"class":106},[47,1360,1361],{"class":102,"line":238},[47,1362,215],{"emptyLinePlaceholder":214},[47,1364,1365,1367,1369,1371,1373,1375,1377],{"class":102,"line":248},[47,1366,135],{"class":134},[47,1368,223],{"class":138},[47,1370,142],{"class":134},[47,1372,145],{"class":114},[47,1374,107],{"class":106},[47,1376,232],{"class":114},[47,1378,235],{"class":106},[47,1380,1381,1383,1385,1387,1389,1391,1393],{"class":102,"line":253},[47,1382,135],{"class":134},[47,1384,702],{"class":138},[47,1386,142],{"class":134},[47,1388,145],{"class":114},[47,1390,709],{"class":106},[47,1392,712],{"class":138},[47,1394,715],{"class":106},[47,1396,1397,1399,1401,1403,1405,1407,1409],{"class":102,"line":263},[47,1398,135],{"class":134},[47,1400,722],{"class":138},[47,1402,142],{"class":134},[47,1404,145],{"class":114},[47,1406,709],{"class":106},[47,1408,712],{"class":138},[47,1410,715],{"class":106},[47,1412,1413,1415,1417,1419,1421,1423,1425],{"class":102,"line":289},[47,1414,135],{"class":134},[47,1416,739],{"class":138},[47,1418,142],{"class":134},[47,1420,145],{"class":114},[47,1422,709],{"class":106},[47,1424,712],{"class":138},[47,1426,715],{"class":106},[47,1428,1429],{"class":102,"line":307},[47,1430,215],{"emptyLinePlaceholder":214},[47,1432,1433,1435,1437,1439,1441,1443,1445],{"class":102,"line":317},[47,1434,135],{"class":134},[47,1436,1006],{"class":138},[47,1438,142],{"class":134},[47,1440,1011],{"class":114},[47,1442,534],{"class":106},[47,1444,187],{"class":134},[47,1446,190],{"class":106},[47,1448,1449,1451,1454,1456,1458,1460,1462,1464,1466,1468,1471],{"class":102,"line":323},[47,1450,1022],{"class":134},[47,1452,1453],{"class":106}," Math.",[47,1455,1033],{"class":114},[47,1457,1036],{"class":106},[47,1459,1039],{"class":134},[47,1461,1042],{"class":106},[47,1463,1039],{"class":134},[47,1465,1047],{"class":106},[47,1467,107],{"class":134},[47,1469,1470],{"class":138}," 1",[47,1472,426],{"class":106},[47,1474,1475],{"class":102,"line":333},[47,1476,634],{"class":106},[47,1478,1479],{"class":102,"line":343},[47,1480,215],{"emptyLinePlaceholder":214},[47,1482,1483,1485,1487,1489],{"class":102,"line":353},[47,1484,531],{"class":114},[47,1486,534],{"class":106},[47,1488,187],{"class":134},[47,1490,190],{"class":106},[47,1492,1493,1495,1497,1499,1501,1503,1505,1507,1509,1511,1513],{"class":102,"line":376},[47,1494,768],{"class":106},[47,1496,771],{"class":114},[47,1498,709],{"class":106},[47,1500,776],{"class":124},[47,1502,779],{"class":106},[47,1504,782],{"class":174},[47,1506,178],{"class":134},[47,1508,787],{"class":114},[47,1510,184],{"class":106},[47,1512,187],{"class":134},[47,1514,190],{"class":106},[47,1516,1517,1519,1521],{"class":102,"line":385},[47,1518,798],{"class":106},[47,1520,121],{"class":134},[47,1522,803],{"class":106},[47,1524,1525],{"class":102,"line":390},[47,1526,606],{"class":106},[47,1528,1529],{"class":102,"line":403},[47,1530,215],{"emptyLinePlaceholder":214},[47,1532,1533,1535,1537,1539,1541,1543,1545,1547,1549,1551],{"class":102,"line":411},[47,1534,543],{"class":134},[47,1536,818],{"class":138},[47,1538,142],{"class":134},[47,1540,551],{"class":134},[47,1542,825],{"class":114},[47,1544,557],{"class":106},[47,1546,830],{"class":174},[47,1548,184],{"class":106},[47,1550,187],{"class":134},[47,1552,190],{"class":106},[47,1554,1555,1557,1559,1561,1563,1565],{"class":102,"line":429},[47,1556,841],{"class":134},[47,1558,844],{"class":138},[47,1560,142],{"class":134},[47,1562,849],{"class":106},[47,1564,712],{"class":138},[47,1566,854],{"class":106},[47,1568,1569,1571,1573],{"class":102,"line":445},[47,1570,859],{"class":106},[47,1572,121],{"class":134},[47,1574,864],{"class":106},[47,1576,1577],{"class":102,"line":463},[47,1578,606],{"class":106},[47,1580,1581,1583,1585],{"class":102,"line":476},[47,1582,873],{"class":106},[47,1584,614],{"class":114},[47,1586,878],{"class":106},[47,1588,1589],{"class":102,"line":482},[47,1590,215],{"emptyLinePlaceholder":214},[47,1592,1594,1596,1598,1600,1602,1604,1606,1608,1610,1612],{"class":102,"line":1593},29,[47,1595,543],{"class":134},[47,1597,546],{"class":138},[47,1599,142],{"class":134},[47,1601,551],{"class":134},[47,1603,554],{"class":114},[47,1605,557],{"class":106},[47,1607,560],{"class":174},[47,1609,184],{"class":106},[47,1611,187],{"class":134},[47,1613,190],{"class":106},[47,1615,1617,1619,1621,1623,1625,1627],{"class":102,"line":1616},30,[47,1618,841],{"class":134},[47,1620,1183],{"class":138},[47,1622,142],{"class":134},[47,1624,1188],{"class":106},[47,1626,712],{"class":138},[47,1628,1193],{"class":106},[47,1630,1632,1634],{"class":102,"line":1631},31,[47,1633,1198],{"class":134},[47,1635,1201],{"class":106},[47,1637,1639,1641,1643],{"class":102,"line":1638},32,[47,1640,1206],{"class":106},[47,1642,574],{"class":114},[47,1644,577],{"class":106},[47,1646,1648,1650,1652],{"class":102,"line":1647},33,[47,1649,1215],{"class":106},[47,1651,1039],{"class":134},[47,1653,1220],{"class":106},[47,1655,1657,1659,1661],{"class":102,"line":1656},34,[47,1658,1225],{"class":106},[47,1660,593],{"class":124},[47,1662,596],{"class":106},[47,1664,1666],{"class":102,"line":1665},35,[47,1667,1234],{"class":106},[47,1669,1671],{"class":102,"line":1670},36,[47,1672,1239],{"class":106},[47,1674,1676,1678,1680],{"class":102,"line":1675},37,[47,1677,909],{"class":106},[47,1679,121],{"class":134},[47,1681,914],{"class":106},[47,1683,1685],{"class":102,"line":1684},38,[47,1686,606],{"class":106},[47,1688,1690,1692,1694,1697,1700,1703,1705],{"class":102,"line":1689},39,[47,1691,611],{"class":106},[47,1693,614],{"class":114},[47,1695,1696],{"class":106},"(messageAreaElement.value, { childList: ",[47,1698,1699],{"class":138},"true",[47,1701,1702],{"class":106},", subtree: ",[47,1704,1699],{"class":138},[47,1706,1707],{"class":106}," });\n",[47,1709,1711],{"class":102,"line":1710},40,[47,1712,634],{"class":106},[47,1714,1716,1718,1720],{"class":102,"line":1715},41,[47,1717,241],{"class":106},[47,1719,111],{"class":110},[47,1721,128],{"class":106},[47,1723,1725],{"class":102,"line":1724},42,[47,1726,215],{"emptyLinePlaceholder":214},[47,1728,1730,1732,1734],{"class":102,"line":1729},43,[47,1731,107],{"class":106},[47,1733,258],{"class":110},[47,1735,128],{"class":106},[47,1737,1739,1741,1743,1745,1747,1749,1751,1753,1755],{"class":102,"line":1738},44,[47,1740,266],{"class":106},[47,1742,269],{"class":110},[47,1744,145],{"class":114},[47,1746,121],{"class":106},[47,1748,276],{"class":124},[47,1750,279],{"class":114},[47,1752,121],{"class":106},[47,1754,284],{"class":124},[47,1756,128],{"class":106},[47,1758,1760,1762,1764,1766,1768,1770],{"class":102,"line":1759},45,[47,1761,292],{"class":106},[47,1763,269],{"class":110},[47,1765,297],{"class":114},[47,1767,121],{"class":106},[47,1769,302],{"class":124},[47,1771,128],{"class":106},[47,1773,1775,1777,1779],{"class":102,"line":1774},46,[47,1776,310],{"class":106},[47,1778,269],{"class":110},[47,1780,128],{"class":106},[47,1782,1784],{"class":102,"line":1783},47,[47,1785,320],{"class":106},[47,1787,1789,1791,1793],{"class":102,"line":1788},48,[47,1790,326],{"class":106},[47,1792,269],{"class":110},[47,1794,128],{"class":106},[47,1796,1798,1800,1802],{"class":102,"line":1797},49,[47,1799,336],{"class":106},[47,1801,269],{"class":110},[47,1803,128],{"class":106},[47,1805,1807,1809,1811],{"class":102,"line":1806},50,[47,1808,346],{"class":106},[47,1810,269],{"class":110},[47,1812,128],{"class":106},[47,1814,1816,1818,1820,1822,1824,1826,1828,1830],{"class":102,"line":1815},51,[47,1817,266],{"class":106},[47,1819,358],{"class":110},[47,1821,361],{"class":114},[47,1823,121],{"class":106},[47,1825,366],{"class":124},[47,1827,369],{"class":106},[47,1829,358],{"class":110},[47,1831,128],{"class":106},[47,1833,1835,1837,1839],{"class":102,"line":1834},52,[47,1836,241],{"class":106},[47,1838,258],{"class":110},[47,1840,128],{"class":106},[47,1842,1844],{"class":102,"line":1843},53,[47,1845,215],{"emptyLinePlaceholder":214},[47,1847,1849,1851,1853,1855],{"class":102,"line":1848},54,[47,1850,107],{"class":106},[47,1852,395],{"class":110},[47,1854,398],{"class":114},[47,1856,128],{"class":106},[47,1858,1860,1862],{"class":102,"line":1859},55,[47,1861,406],{"class":114},[47,1863,190],{"class":106},[47,1865,1867,1869,1871,1873,1875],{"class":102,"line":1866},56,[47,1868,414],{"class":138},[47,1870,417],{"class":106},[47,1872,420],{"class":138},[47,1874,423],{"class":134},[47,1876,426],{"class":106},[47,1878,1880,1882,1884,1886,1888],{"class":102,"line":1879},57,[47,1881,432],{"class":138},[47,1883,417],{"class":106},[47,1885,437],{"class":138},[47,1887,440],{"class":134},[47,1889,426],{"class":106},[47,1891,1893,1895,1897,1899,1901,1903],{"class":102,"line":1892},58,[47,1894,448],{"class":138},[47,1896,417],{"class":106},[47,1898,453],{"class":138},[47,1900,423],{"class":134},[47,1902,458],{"class":138},[47,1904,426],{"class":106},[47,1906,1908,1910,1912,1914],{"class":102,"line":1907},59,[47,1909,466],{"class":138},[47,1911,417],{"class":106},[47,1913,471],{"class":138},[47,1915,426],{"class":106},[47,1917,1919],{"class":102,"line":1918},60,[47,1920,479],{"class":106},[47,1922,1924,1926,1928],{"class":102,"line":1923},61,[47,1925,241],{"class":106},[47,1927,395],{"class":110},[47,1929,128],{"class":106},[14,1931,1932,1933,1936,1937,1940,1941,1944],{},"最後に",[98,1934,1935],{},"scrollTo({top: 10000})","の",[98,1938,1939],{},"top","の指定を",[98,1942,1943],{},"scrollHeight - clientHeight","としました。",[10,1946,1947],{"id":1947},"補足",[31,1949,1951],{"id":1950},"domnodeinsertedについて","DOMNodeInsertedについて",[14,1953,1954,1955,1958,1959],{},"DOMの追加は",[98,1956,1957],{},"DOMNodeInserted","でも検知できるようですが、こちらは現在deprecatedのようです。",[1100,1960,1961],{},[59,1962,1966],{"href":1963,"ariaDescribedBy":1964,"dataFootnoteRef":96,"id":1965},"#user-content-fn-domnodeinserted",[1106],"user-content-fnref-domnodeinserted","2",[31,1968,1970],{"id":1969},"intersection-observer-apiについて","Intersection Observer APIについて",[14,1972,1973],{},"ここでは記載しませんが、仕様によってはIntersection Observer APIを使った実装もありかもしれません。",[14,1975,1976],{},[59,1977,1980],{"href":1978,"rel":1979},"https:\u002F\u002Ftech.arms-soft.co.jp\u002Fentry\u002F2021\u002F06\u002F16\u002F090000",[63],"Intersection Observer APIを使って、ページの最下部までスクロールしたかを判定する",[31,1982,1983],{"id":1983},"behaviorオプションについて",[14,1985,1986,1988,1989,1992],{},[98,1987,657],{},"のオプションで",[98,1990,1991],{},"behavior: \"smooth\"","とすると、スクロール動作が滑らかになります。",[14,1994,1995],{},"ただし、ChatGPTのような一文字ずつ文字が増加するUIの場合、スクロールが完了する前に次のスクロール位置判定処理が走ってしまい、スクロール位置は最下部に固定されない場合があります。",[14,1997,1998,1999,2001,2002,2005],{},"対策としては、スクロール判定の距離を短くしたり、文字の更新の間隔を長めにするなどが考えられます。",[21,2000],{},"\n見た目にこだわらないのであれば、",[98,2003,2004],{},"smooth","オプションを付けないのが楽です。",[10,2007,2008],{"id":2008},"参考",[2010,2011,2012,2019,2026,2033,2040,2047,2054],"ul",{},[75,2013,2014],{},[59,2015,2018],{"href":2016,"rel":2017},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fja\u002Fdocs\u002FWeb\u002FAPI\u002FElement\u002FscrollTo",[63],"MDN Web Docs | Element.scrollTo()",[75,2020,2021],{},[59,2022,2025],{"href":2023,"rel":2024},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fja\u002Fdocs\u002FWeb\u002FAPI\u002FElement\u002FclientHeight",[63],"MDN Web Docs | Element.clientHeight",[75,2027,2028],{},[59,2029,2032],{"href":2030,"rel":2031},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fja\u002Fdocs\u002FWeb\u002FAPI\u002FElement\u002FscrollHeight",[63],"MDN Web Docs | Element.scrollHeight",[75,2034,2035],{},[59,2036,2039],{"href":2037,"rel":2038},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FElement\u002FscrollTop",[63],"MDN Web Docs | Element: scrollTop",[75,2041,2042],{},[59,2043,2046],{"href":2044,"rel":2045},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fja\u002Fdocs\u002FWeb\u002FAPI\u002FDocument\u002Fscroll_event",[63],"MDN Web Docs | scroll イベント",[75,2048,2049],{},[59,2050,2053],{"href":2051,"rel":2052},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fja\u002Fdocs\u002FWeb\u002FAPI\u002FResizeObserver",[63],"MDN Web Docs | ResizeObserver",[75,2055,2056],{},[59,2057,2060],{"href":2058,"rel":2059},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fja\u002Fdocs\u002FWeb\u002FAPI\u002FMutationObserver",[63],"MDN Web Docs | MutationObserver",[2062,2063,2066,2071],"section",{"className":2064,"dataFootnotes":96},[2065],"footnotes",[10,2067,2070],{"className":2068,"id":1106},[2069],"sr-only","Footnotes",[72,2072,2073,2089],{},[75,2074,2076,2081,2082],{"id":2075},"user-content-fn-scroll",[59,2077,2080],{"href":2078,"rel":2079},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fja\u002Fdocs\u002FWeb\u002FAPI\u002FElement\u002FscrollHeight#%E8%A6%81%E7%B4%A0%E3%81%8C%E5%AE%8C%E5%85%A8%E3%81%AB%E3%82%B9%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%AB%E3%81%95%E3%82%8C%E3%81%9F%E3%81%8B%E3%81%A9%E3%81%86%E3%81%8B%E3%81%AE%E5%88%A4%E5%AE%9A",[63],"Element.scrollHeight | 要素が完全にスクロールされたかどうかの判定"," ",[59,2083,2088],{"href":2084,"ariaLabel":2085,"className":2086,"dataFootnoteBackref":96},"#user-content-fnref-scroll","Back to reference 1",[2087],"data-footnote-backref","↩",[75,2090,2092,2081,2097],{"id":2091},"user-content-fn-domnodeinserted",[59,2093,2096],{"href":2094,"rel":2095},"https:\u002F\u002Fwww.w3.org\u002FTR\u002FDOM-Level-3-Events\u002F#event-type-DOMNodeInserted",[63],"W3C | DOMNodeInserted",[59,2098,2088],{"href":2099,"ariaLabel":2100,"className":2101,"dataFootnoteBackref":96},"#user-content-fnref-domnodeinserted","Back to reference 2",[2087],[395,2103,2104],{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":96,"searchDepth":131,"depth":131,"links":2106},[2107,2111,2116,2121,2122],{"id":12,"depth":131,"text":12,"children":2108},[2109,2110],{"id":33,"depth":161,"text":33},{"id":43,"depth":161,"text":43},{"id":67,"depth":131,"text":67,"children":2112},[2113,2114,2115],{"id":77,"depth":161,"text":77},{"id":80,"depth":161,"text":80},{"id":83,"depth":161,"text":83},{"id":1947,"depth":131,"text":1947,"children":2117},[2118,2119,2120],{"id":1950,"depth":161,"text":1951},{"id":1969,"depth":161,"text":1970},{"id":1983,"depth":161,"text":1983},{"id":2008,"depth":131,"text":2008},{"id":1106,"depth":131,"text":2070},"システム開発","2023\u002F08\u002F01","チャットUIによくあるメッセージ送受信時にスクロール位置を最下部に維持する処理を実装する方法を解説します。","md",{},"\u002Fblog\u002Fprogramming\u002Fchat-scroll",{"title":5,"description":2125},"blog\u002Fprogramming\u002Fchat-scroll","Vue.js",[2133,2134,2131],"JavaScript","TypeScript","https:\u002F\u002Fimg-blog.ashcolor.jp\u002Fimg\u002Fpc\u002Fprogramming_chat-scroll_thumbnail.webp",null,"q4lJtffR4UBd4-YOG_kCWOqb8o6EtdW9HuMiJOgeT4U",[2139,2148,2156,2163,2172,2178],{"id":2140,"path":2141,"title":2142,"category":2123,"tags":2143,"thumbnail":2146,"createdAt":2147,"updatedAt":2136},"blog\u002Fblog\u002Fprogramming\u002Faws-iam-passkey.md","\u002Fblog\u002Fprogramming\u002Faws-iam-passkey","AWS IAMでPasskeyが利用可能に Windows PCで指紋認証を設定してみました",[2144,2145],"AWS","Windows","https:\u002F\u002Fimg-blog.ashcolor.jp\u002Fimg\u002Fpc\u002F2024-08-22_11-12-41_nvre36gj.webp","2024\u002F08\u002F22",{"id":2149,"path":2150,"title":2151,"category":2123,"tags":2152,"thumbnail":2154,"createdAt":2155,"updatedAt":2136},"blog\u002Fblog\u002Fprogramming\u002Freact-tutorial-vue.md","\u002Fblog\u002Fprogramming\u002Freact-tutorial-vue","React公式チュートリアルをVueでも書いて比較してみた",[2153,2131],"React.js","https:\u002F\u002Fimg-blog.ashcolor.jp\u002Fimg\u002Fpc\u002Fprogramming_react-vue_react-vue.webp","2024\u002F06\u002F22",{"id":2157,"path":2158,"title":2159,"category":2123,"tags":2160,"thumbnail":2161,"createdAt":2162,"updatedAt":2136},"blog\u002Fblog\u002Fprogramming\u002Fvue-scoped-css.md","\u002Fblog\u002Fprogramming\u002Fvue-scoped-css","Vueのスコープ付きCSSで子コンポーネントにCSSを適用する【Vue3】",[2131],"https:\u002F\u002Fimg-blog.ashcolor.jp\u002Fimg\u002Fpc\u002Fprogramming_vue-define-model_vue.webp","2024\u002F06\u002F12",{"id":2164,"path":2165,"title":2166,"category":2123,"tags":2167,"thumbnail":2170,"createdAt":2171,"updatedAt":2136},"blog\u002Fblog\u002Fprogramming\u002Fvercel-vs-amplify.md","\u002Fblog\u002Fprogramming\u002Fvercel-vs-amplify","Vercel(Hobby)→AWS Amplify 移行して分かった両者の違い",[2168,2169],"Vercel","Amplify","https:\u002F\u002Fimg-blog.ashcolor.jp\u002Fimg\u002Fpc\u002Fprogramming_vercel-vs-amplify_vercel-amplify.webp","2024\u002F05\u002F17",{"id":2173,"path":2174,"title":2175,"category":2123,"tags":2176,"thumbnail":2161,"createdAt":2177,"updatedAt":2136},"blog\u002Fblog\u002Fprogramming\u002Fvue-playground.md","\u002Fblog\u002Fprogramming\u002Fvue-playground","手軽に使えるVueプレイグラウンド環境。Vue SFC Playground解説",[2131],"2024\u002F04\u002F08",{"id":2179,"path":2180,"title":2181,"category":2123,"tags":2182,"thumbnail":2185,"createdAt":2186,"updatedAt":2136},"blog\u002Fblog\u002Fprogramming\u002Ftailwind-css-v4-install.md","\u002Fblog\u002Fprogramming\u002Ftailwind-css-v4-install","Vite、TailwindCSS v4環境を構築【Tailwind CSS v4-alpha】",[2183,2184],"Tailwind CSS","Vite","https:\u002F\u002Fimg-blog.ashcolor.jp\u002Fimg\u002Fpc\u002Fprogramming_tailwind-css-v4-install_tailwind-css.webp","2024\u002F03\u002F07",[2188,2190,2199,2205,2215],{"id":2149,"path":2150,"title":2151,"category":2123,"tags":2189,"thumbnail":2154,"createdAt":2155,"updatedAt":2136},[2153,2131],{"id":2191,"path":2192,"title":2193,"category":2123,"tags":2194,"thumbnail":2196,"createdAt":2197,"updatedAt":2198},"blog\u002Fblog\u002Fprogramming\u002Fnpm-develop.md","\u002Fblog\u002Fprogramming\u002Fnpm-develop","フロントエンド開発体験を向上させる10のnpmライブラリ【2024年版】",[2133,2134,2195],"npm","https:\u002F\u002Fimg-blog.ashcolor.jp\u002Fimg\u002Fpc\u002Fprogramming_npm-develop_thumbnail.webp","2024\u002F01\u002F14","2024\u002F01\u002F19",{"id":2200,"path":2201,"title":2202,"category":2123,"tags":2203,"thumbnail":2161,"createdAt":2204,"updatedAt":2186},"blog\u002Fblog\u002Fprogramming\u002Fvue-define-model.md","\u002Fblog\u002Fprogramming\u002Fvue-define-model","VueのdefineModelを活用した入力コンポーネントの作成方法【Vue3.4】",[2131],"2024\u002F01\u002F07",{"id":2206,"path":2207,"title":2208,"category":2123,"tags":2209,"thumbnail":2212,"createdAt":2213,"updatedAt":2214},"blog\u002Fblog\u002Fprogramming\u002Fnuxt-storybook.md","\u002Fblog\u002Fprogramming\u002Fnuxt-storybook","Nuxt3にStorybookを導入する方法",[2210,2211],"NuxtJS","Storybook","https:\u002F\u002Fimg-blog.ashcolor.jp\u002Fimg\u002Fpc\u002Fprogramming_nuxt-storybook_thumbnail.webp","2023\u002F11\u002F22","2024\u002F03\u002F18",{"id":2216,"path":2217,"title":2218,"category":2123,"tags":2219,"thumbnail":2222,"createdAt":2223,"updatedAt":2198},"blog\u002Fblog\u002Fprogramming\u002Fnuxt-blog.md","\u002Fblog\u002Fprogramming\u002Fnuxt-blog","NuxtJSを使ってブログを作ってみた【Nuxt Content\u002FTailwindCSS\u002FdaisyUI】",[2210,2131,2220,2221],"TailwindCSS","ブログ","https:\u002F\u002Fimg-blog.ashcolor.jp\u002Fimg\u002Fpc\u002Fprogramming_nuxt-blog_thumbnail.webp","2023\u002F08\u002F25",1794101385011]