I am seeing this using puppeteer-stream to record video (it's a thin 1-file wrapper around puppeteer).
Notice how it's not centered vertically. There is no scrollable area either, this is it.
The HTML is this:
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<style>
html, body {
margin: 0;
padding: 0;
height: 800px;
width: 800px;
}
#content {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
background-color: #000;
}
#row {
display: inline-block;
font-size: 32px;
width: 600px;
text-align: center;
}
#row * {
text-align: center;
position: relative;
color: #9A7FAE;
}
#row .highlight {
color: #BEE5B0;
}
</style>
</head>
<body>
<div id='content'><div id='row'><span>i am some text</span></div></div>
</body>
</html>
And the Puppeteer script is essentially this:
const { launch, getStream } = require('puppeteer-stream')
const fs = require('fs')
const file = fs.createWriteStream(`${__dirname}/poem.webm`)
const wait = (ms) => new Promise((res, rej) => setTimeout(res, ms))
async function start() {
const browser = await launch({
defaultViewport: null,
args: [`--window-size=${800},${800}`]
})
const page = await browser.newPage();
await page.setViewport({
width: 800,
height: 800,
})
await page.goto('file:///Users/me/poem.html')
const stream = await getStream(page, { audio: true, video: true })
await page.click('#row')
console.log("recording")
stream.pipe(file).on('end', () => {
console.log('end')
})
setTimeout(async () => {
await stream.destroy()
file.close()
console.log("finished")
await browser.close()
}, 1000 * 30)
}
start()
Why is it not centering vertically? How do I get it to vertically center?
Related
I cached the offline.html and image files using a service worker. Navigating to offline.html works fine. But it fails to load image from offline.html. At offline.html, it tries to get the image cached by the service worker directly without fetching it, but it fails because the internet is disconnected.
I tried changing the cache.match('/offline.html') part to cache.match(event.request), but this does not move to offline.html. How do I configure the cached files to be used in offline.html when the Internet is disconnected?
// service-worker.js
const OFFLINE_VERSION = 1;
const CACHE_NAME = "offline";
const ASSETS = ["offline.html", "image/icon_replay_b_15pt.png"];
self.addEventListener("install", (event) => {
event.waitUntil(
(async () => {
const cache = await caches.open(CACHE_NAME);
cache.addAll(ASSETS);
})()
);
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
if ("navigationPreload" in self.registration) {
await self.registration.navigationPreload.enable();
}
})()
);
self.clients.claim();
});
self.addEventListener("fetch", (event) => {
if (event.request.mode === "navigate") {
console.log(event.request.url);
event.respondWith(
(async () => {
try {
const preloadResponse = await event.preloadResponse;
if (preloadResponse) {
return preloadResponse;
}
const networkResponse = await fetch(event.request);
return networkResponse;
} catch (error) {
console.log("Fetch failed; returning offline page instead.", error);
const cache = await caches.open(CACHE_NAME);
return cache.match("/offline.html");
}
})()
);
}
});
<!-- offline.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OFFLINE</title>
<style>
body { font-family:'NanumSquare'; }
body, html {width:100%; height:100%; margin:0; }
* {
box-sizing: border-box;
}
.btn-outline-01 {
border:1px solid #C9CACA;
}
.btn {
height:44px;
background:transparent;
border-radius: 5px;
font-size: 16px;
font-weight: 700;
}
.icon-wrap {
width:24px;
height:24px;
}
.icon-wrap img {
width:100%;
height:100%;
}
* { margin:0; }
.internet-err-wrap {
width:100%;
height:100%;
overflow: hidden;
}
.internet-err {
width:800px;
margin:0 auto;
position:relative;
}
.internet-err .err-text {
position:absolute;
left:5%;
top:35%;
}
.internet-err .err-text h2 {
font-weight:900;
font-size: 26px;
color:#333333;
}
.internet-err .err-text > p {
color:#9FA0A0;
margin-top:10px;
}
.internet-err .err-text button {
display: flex;
justify-content: center;
align-items: center;
margin-top:40px;
font-family:'NanumSquare';
padding:0 20px;
}
.internet-err .err-text button .icon-wrap {
margin-right:5px;
}
.internet-err svg {
width:160%;
}
</style>
</head>
<body>
<div class="internet-err-wrap">
<div class="internet-err">
<div class="err-text">
<button class="btn btn-outline-01" onclick="window.location.reload()">
<div class="icon-wrap">
<img src="/image/icon_replay_b_15pt.png" alt="" />
</div>
<p>retry</p>
</button>
</div>
</div>
</body>
</html>
So here you have written the code only mode of navigate but if you want to fetch the image and css from cache then you should use mode as image and style.
This will allow you to fetch the images and css.
self.addEventListener("fetch", (event) => {
if (event.request.mode === "navigate") {
// Open the cache
event.respondWith(
caches.open(cacheName).then((cache) => {
// Go to the network first
return fetch(event.request.url)
.then((fetchedResponse) => {
cache.put(event.request, fetchedResponse.clone());
return fetchedResponse;
})
.catch(() => {
// If the network is unavailable, get
// return cache.match(event.request.url);
return cache.match("offline.html");
});
})
);
} else if (
event.request.destination === "image" ||
event.request.destination === "style"
) {
event.respondWith(
caches.open(cacheName).then((cache) => {
return cache.match(event.request);
})
);
// return;
} else {
return;
}
});
I am trying to make an animation that scrolls from left to right the child elements of a div, I have looked around but none of the other questions with answers seem to work, so far this is what I have accomplished:
Home.css:
#keyframes scroll {
20% {
transform: translateX(-100vw);
}
40% {
transform: translateX(-200vw);
}
60% {
transform: translateX(-300vw);
}
80% {
transform: translateX(-400vw);
}
100% {
transform: translateX(0vw);
}
}
.section {
display: flex;
flex-direction: row;
font-family: Arial, sans-serif;
font-weight: bold;
transition: 1s;
width: 100vw;
height: 100vh;
animation-name: scroll;
animation-duration: 1s;
}
.child1 {
background-color: #c0392b;
flex: none;
width: 100vw;
height: 100vh;
}
.child2 {
background-color: #e67e22;
flex: none;
width: 100vw;
height: 100vh;
}
.child3 {
background-color: #27ae60;
flex: none;
width: 100vw;
height: 100vh;
}
.child4 {
background-color: #2980b9;
flex: none;
width: 100vw;
height: 100vh;
}
.child5 {
background-color: #8e44ad;
flex: none;
width: 100vw;
height: 100vh;
}
Home.jsx:
import { forwardRef, useEffect, useRef, useState } from "react";
import { Link } from "react-router-dom";
import './Home.css';
const Child1 = forwardRef((props, ref) => {
return <div className="child1">
<h1>Child1</h1>
</div>;
});
const Child2 = forwardRef((props, ref) => {
return <div className="child2">
<h1>Child2</h1>
</div>;
});
const Child3 = forwardRef((props, ref) => {
return <div className="child3">
<h1>Child3</h1>
</div>;
});
const Child4 = forwardRef((props, ref) => {
return <div className="child4">
<h1>Child4</h1>
</div>;
});
const Child5 = forwardRef((props, ref) => {
return <div className="child5">
<h1>Child5</h1>
</div>;
});
function Home() {
let section = useRef();
let [currentSection, setCurrentSection] = useState(0);
useEffect(() => {
window.addEventListener("keypress", (event) => {
if(event.key === "d") {
// scroll right (play animation forwards by one step)
} else if(event.key === "a") {
// scroll left (play animation backwards by one step)
}
});
});
return <div ref={section} className="section">
<Child1></Child1>
<Child2></Child2>
<Child3></Child3>
<Child4></Child4>
<Child5></Child5>
</div>
}
export default Home;
The problem is that the animation plays all at the beginning and I cannot figure out a way to play it only when the keyboard event is triggered, if the event.key is an "a" then the elements should scroll to the left otherwise if the event.key is a "d" then the elements should scroll to the right.
Here is the link to the CodeSandbox.
As I understood you want to make parent scrollable only with pressing keys "a" and "d". I think I have found a solution which would work for you.
My solution:
Removing keyframes and separating it into 5 different classes. So here is CSS file:
.section1 {
transform: translateX(0);
}
.section2 {
transform: translateX(-100vw);
}
.section3 {
transform: translateX(-200vw);
}
.section4 {
transform: translateX(-300vw);
}
.section5 {
transform: translateX(-400vw);
}
.section {
display: flex;
flex-direction: row;
font-family: Arial, sans-serif;
font-weight: bold;
transition: 1s;
width: 100vw;
height: 100vh;
animation-name: scroll;
animation-duration: 1s;
}
.child1 {
background-color: #c0392b;
flex: none;
width: 100vw;
height: 100vh;
}
.child2 {
background-color: #e67e22;
flex: none;
width: 100vw;
height: 100vh;
}
.child3 {
background-color: #27ae60;
flex: none;
width: 100vw;
height: 100vh;
}
.child4 {
background-color: #2980b9;
flex: none;
width: 100vw;
height: 100vh;
}
.child5 {
background-color: #8e44ad;
flex: none;
width: 100vw;
height: 100vh;
}
Now I added some JS behaviour to appropriately 'scroll' left or right. I divided moving left or right into functions, and adding numberOfSections as constant. Here is JS file:
import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
import { Link } from "react-router-dom";
import "./Home.css";
const Child1 = forwardRef((props, ref) => {
return (
<div className="child1">
<h1>Child1</h1>
</div>
);
});
const Child2 = forwardRef((props, ref) => {
return (
<div className="child2">
<h1>Child2</h1>
</div>
);
});
const Child3 = forwardRef((props, ref) => {
return (
<div className="child3">
<h1>Child3</h1>
</div>
);
});
const Child4 = forwardRef((props, ref) => {
return (
<div className="child4">
<h1>Child4</h1>
</div>
);
});
const Child5 = forwardRef((props, ref) => {
return (
<div className="child5">
<h1>Child5</h1>
</div>
);
});
const numberOfSections = 5;
function Home() {
const section = useRef();
const [currentSection, setCurrentSection] = useState(1);
const moveLeft = () => {
section.current.classList.remove(`section${currentSection}`);
section.current.classList.add(`section${currentSection - 1}`);
setCurrentSection((prevSection) => prevSection - 1);
};
const moveRight = () => {
section.current.classList.remove(`section${currentSection}`);
section.current.classList.add(`section${currentSection + 1}`);
setCurrentSection((prevSection) => prevSection + 1);
};
const changeSection = (event) => {
if (event.key === "d") {
if (currentSection < numberOfSections) {
moveRight();
}
} else if (event.key === "a") {
if (currentSection > 1) {
moveLeft();
}
}
};
useEffect(() => {
window.addEventListener("keypress", changeSection);
return () => window.removeEventListener("keypress", changeSection);
}, [currentSection]);
return (
<div ref={section} className="section">
<Child1></Child1>
<Child2></Child2>
<Child3></Child3>
<Child4></Child4>
<Child5></Child5>
</div>
);
}
export default Home;
I have tested it and it works.
By the way, my suggestions is to wrap some of these functions into useCallback if you plan to expand Home component. And I suggest to use some of npm packages compatible with React for directly manipulating CSS within JS, such as styled-components, which in this case would make it a lot easier to create Carousel, becouse now in order to create more Child components, you need to make new CSS classes.
But this above fixes the given problem. I hope I helped you :)
this solution is tightly cuppled to your example but from my perspective, you could make it more generic.
I didn't use CSS animation instead I used the scrollTo method to achieve scroll behavior.
codesandbox link
Home.jsx
import { forwardRef, useEffect, useRef, useState } from "react";
import "./styles.css";
// I not using forwardRef and ref at all
const Child1 = forwardRef((props, ref) => {
return (
<div className="child1">
<h1>Child1</h1>
</div>
);
});
const Child2 = forwardRef((props, ref) => {
return (
<div className="child2">
<h1>Child2</h1>
</div>
);
});
const Child3 = forwardRef((props, ref) => {
return (
<div className="child3">
<h1>Child3</h1>
</div>
);
});
const Child4 = forwardRef((props, ref) => {
return (
<div className="child4">
<h1>Child4</h1>
</div>
);
});
const Child5 = forwardRef((props, ref) => {
return (
<div className="child5">
<h1>Child5</h1>
</div>
);
});
function App() {
let section = useRef();
let [currentSection, setCurrentSection] = useState(0);
useEffect(() => {
const handler = (event) => {
if (event.key === "d") {
if (currentSection === 4) return;
setCurrentSection((prev) => prev + 1);
} else if (event.key === "a") {
if (currentSection === 0) return;
setCurrentSection((prev) => prev - 1);
}
};
window.addEventListener("keypress", handler);
// you should clean up you EventListener
return () => {
window.removeEventListener("keypress", handler);
};
}, [currentSection]);
useEffect(() => {
window.scrollTo({
behavior: "smooth",
left: window.innerWidth * currentSection,
top: 0,
});
}, [currentSection]);
return (
<div ref={section} className="section">
<Child1></Child1>
<Child2></Child2>
<Child3></Child3>
<Child4></Child4>
<Child5></Child5>
</div>
);
}
export default App;
Home.css
.section {
display: flex;
flex-direction: row;
font-family: Arial, sans-serif;
font-weight: bold;
transition: 1s;
width: 100vw;
height: 100vh;
}
.child1 {
background-color: #c0392b;
flex: none;
width: 100vw;
height: 100vh;
}
.child2 {
background-color: #e67e22;
flex: none;
width: 100vw;
height: 100vh;
}
.child3 {
background-color: #27ae60;
flex: none;
width: 100vw;
height: 100vh;
}
.child4 {
background-color: #2980b9;
flex: none;
width: 100vw;
height: 100vh;
}
.child5 {
background-color: #8e44ad;
flex: none;
width: 100vw;
height: 100vh;
}
I have a timer that sits ontop of an image. The timer represents a countdown to a specific date. The problem is when the browser is resized the timer will hold true but the banner will stretch etc. I have noticed if I set the website to width 130% it will work flawlessly. The only issue is the horizonal scrollbar upon launch will default to the left. I am trying to have it default to the center.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name= viewport content= width=device-width initial-scale=1.0 maximum-scale=1.0 user-scalable=no">
<title>Document</title>
<style>
#import url('https://fonts.googleapis.com/css2?family=Poppins:wght#600&display=swap');
#import url('https://fonts.googleapis.com/css2?family=Poppins:wght#400;700&display=swap');
#import url('https://fonts.googleapis.com/css2?family=Lobster&display=swap');
#import url('https://fonts.googleapis.com/css2?family=Exo:ital,wght#1,800&family=Oswald&display=swap');
#import url('https://fonts.googleapis.com/css2?family=Rowdies:wght#300;400;700&display=swap');
#import url('https://fonts.googleapis.com/css2?family=Festive&display=swap');
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Poppins', sans-serif;
}
body {
background-color: #00628c;
width: 100%;
min-height: 100vh;
padding: 10px;
display: flex;
justify-content: center;
align-items: center;
}
.remaining-time {
display: flex;
align-items: center;
position: absolute;
top: var(--time-unit-wrapper-top);
left: var(--time-unit-wrapper-left);
}
.remaining-time .separator {
padding: var(--time-unit-seperator-padding);
color: #FFFFFF00;
font-size: 12px;
}
.remaining-time .days,
.remaining-time .hours,
.remaining-time .minutes,
.remaining-time .seconds {
display: flex;
justify-content: center;
align-items: center;
width: var(--time-unit-width);
background-color: var(--time-unit-background-color);
color: var(--time-unit-color);
font-size: var(--time-unit-font-size);
font-family: var(--time-unit-font-family);
border-radius: 25%;
}
:root {
--time-unit-wrapper-top: 27.5%;
--time-unit-wrapper-left: 44%;
--time-unit-seperator-padding: 0 8px;
--time-unit-width: 20px;
--time-unit-background-color: #FFFFFF00;
--time-unit-color: #E5D5B5;
--time-unit-border-radius: 4px;
--time-unit-font-size: 13px;
--time-unit-font-family: 'Exo';
}
</style>
</head>
<body>
<div class="hpf_body">
<img src="/CServer/HomepageFeed/7D30D9006D4E4156BE0F54B95A6CFB69/LMSNEW.gif" alt="banner" width="500%" style="width: 100%;">
<div class="remaining-time">
<span class="days"></span>
<span class="separator">:</span>
<span class="hours"></span>
<span class="separator">:</span>
<span class="minutes"></span>
<span class="separator">:</span>
<span class="seconds"></span>
</div>
</div>
</header>
<script>
let intervalHandler;
// (year, months, date, hours, minutes, seconds)
const startTime = new Date(2022, 1, 9, 10, 45, 0);
const endTime = new Date(2022, 7, 12, 0, 0, 0);
const header = document.querySelector('.header');
const remainingDaysSpan = document.querySelector('.remaining-time .days');
const remainingHoursSpan = document.querySelector('.remaining-time .hours');
const remainingMinutesSpan = document.querySelector('.remaining-time .minutes');
const remainingSecondsSpan = document.querySelector('.remaining-time .seconds');
const addLeadingZero = str => {
return (str.length == 1) ? '0' + str : str;
}
const updateRemainingTime = () => {
const curTime = new Date();
const timeDffSecs = (endTime - curTime) / 1000;
if(timeDffSecs <= 0) {
clearInterval(intervalHandler);
return;
}
const remainingSeconds = timeDffSecs;
const remainingMinutes = remainingSeconds / 60;
const remainingHours = remainingMinutes / 60;
const remainingDays = remainingHours / 24;
remainingDaysSpan.innerText = addLeadingZero(Math.floor(remainingDays).toString());
remainingHoursSpan.innerText = addLeadingZero(Math.floor(remainingHours % 24).toString());
remainingMinutesSpan.innerText = addLeadingZero(Math.floor(remainingMinutes % 60).toString());
remainingSecondsSpan.innerText = addLeadingZero(Math.floor(remainingSeconds % 60).toString());
}
intervalHandler = setInterval(() => {
updateRemainingTime();
}, 100);
</script>
</body>
</html>
I am attempting a visual effect of dislocating a div from one container to another, both also divs in this case. To do this, I first obtain the DOMRect from the initial position. Then, I briefly add a clone of the element to its destination and obtain the DOMRect of that, promptly removing the clone. I then remove the element from its source, and append it as a child of an outmost div, with absolute positioning, and "left" and "top" properties in px according to the obtained values from the DOMRect. window.scrollX and window.scrollY are added, in order to get the absolute position. Once appended, I add a CSS class to the element which states that it transitions on all properties. Following this, I change the element's "left" and "top" properties to match those of the destination's DOMRect. Once the transition is finished, I append the element as a child of the destination container div. All works nearly as expected, the apparent defect being that the element begins its transition slightly off of its initial position, and ends similarly. I will add my used code below.
index.ts:
type PlacementStage = 'source-removal' | 'medium-placement' | 'medium-removal' | 'destination-placement';
type Tuple<V, N extends number, T extends V[] = []> =
N extends T['length'] ?
T :
Tuple<V, N, [...T, V]>;
const defaultPlacementStageSequence: Tuple<PlacementStage, 4> = [
'source-removal',
'medium-placement',
'medium-removal',
'destination-placement'
];
type SurjectivePlacementStageMapping<T> = {
[V in PlacementStage]: T
};
interface DefaultPlacementStageInstructionsParameters {
element: HTMLElement,
source: HTMLElement,
medium: HTMLElement,
destination: HTMLElement
};
function makeDefaultPlacementStageInstructions(
{
element,
source,
medium,
destination
}: DefaultPlacementStageInstructionsParameters
): SurjectivePlacementStageMapping<() => void> {
const {
initial: initialBoundingClientRect,
final: finalBoundingClientRect
} = getInitialAndFinalBoundingClientRects({ element, destination });
return {
'source-removal': () => {
source.removeChild(element);
},
'medium-placement': () => {
element.classList.add('moves-smoothly');
medium.appendChild(element);
console.log({ initialBoundingClientRect, finalBoundingClientRect });
element.style.top = `${initialBoundingClientRect.top + window.scrollY}px`;
element.style.left = `${initialBoundingClientRect.left + window.scrollX}px`;
element.style.top = `${finalBoundingClientRect.top + window.scrollY}px`;
element.style.left = `${finalBoundingClientRect.left + window.scrollX}px`;
},
'medium-removal': () => {
element.classList.remove('moves-smoothly');
medium.removeChild(element);
},
'destination-placement': () => {
destination.appendChild(element);
}
};
}
function getInitialAndFinalBoundingClientRects(
{
element,
destination
}: {
element: HTMLElement,
destination: HTMLElement
}
): { initial: DOMRect, final: DOMRect } {
const initialBoundingClientRect = element.getBoundingClientRect();
const elementCopy = element.cloneNode() as HTMLElement;
destination.appendChild(elementCopy);
const finalBoundingClientRect = elementCopy.getBoundingClientRect();
destination.removeChild(elementCopy);
return {
initial: initialBoundingClientRect,
final: finalBoundingClientRect
};
}
function moveElementInstantly(parameters: DefaultPlacementStageInstructionsParameters): void {
const instructions = makeDefaultPlacementStageInstructions(parameters);
defaultPlacementStageSequence.forEach(stage => instructions[stage]());
}
const defaultPlacementStageDelays: SurjectivePlacementStageMapping<number> = {
'source-removal': 0,
'medium-placement': 0,
'medium-removal': 1000,
'destination-placement': 1000
};
function moveElementWithDelay(parameters: DefaultPlacementStageInstructionsParameters): void {
const instructions = makeDefaultPlacementStageInstructions(parameters);
defaultPlacementStageSequence.forEach(stage => {
setTimeout(instructions[stage], defaultPlacementStageDelays[stage])
});
}
document.querySelectorAll('.small-box').forEach((nonHtmlElement) => {
const element = nonHtmlElement as HTMLElement;
element.addEventListener(
'click',
() => {
moveElementWithDelay({
element,
source: document.querySelector('#left-panel') as HTMLElement,
medium: document.querySelector('#main-content') as HTMLElement,
destination: document.querySelector('#main-display-area') as HTMLElement
});
}
);
});
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style.css">
<title>Experiment</title>
</head>
<body>
<div id="root-div">
<div id="main-content">
<div id="left-panel" class="panel">
<div class="small-box" style="background-color: red;">
</div>
<div class="small-box" style="background-color: blue;">
</div>
</div>
<div id="main-display-area" class="panel">
</div>
</div>
</div>
<script src="index.js"></script>
</body>
</html>
style.css:
body {
margin: 0;
}
#main-content {
width: 100vw;
margin: 0;
}
#main-content {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: row;
}
.panel {
display: flex;
flex-direction: column;
width: 50%;
justify-content: center;
align-items: center;
}
.small-box {
width: 50px;
height: 50px;
margin: 10px;
}
.moves-smoothly {
position: absolute;
transition: all 1s ease-in-out;
}
#left-panel {
background-color: lightblue;
}
#main-display-area {
background-color: lightgreen;
}
height: 100vh;
display: flex;
flex-direction: row;
}
.panel {
display: flex;
flex-direction: column;
width: 50%;
justify-content: center;
align-items: center;
}
.small-box {
width: 50px;
height: 50px;
margin: 10px;
}
.moves-smoothly {
position: absolute;
transition: all 1s ease-in-out;
}
#left-panel {
background-color: lightblue;
}
#main-display-area {
background-color: lightgreen;
}
I'm using the microsoft/BotFramework-WebChat, but I'm having problems getting it to scroll properly.
Often when the bot responds the user is forced to manually scroll to the bottom of the chat log.
I can't find any documentation about hooks that would let me call an API to scroll it.
Is there a way to get it so that the chat window scrolls automatically?
HTML:
<div id="bot-button" style="display:none" >
<p id="need-help" class="triangle-isosceles">Hey! Need any help?</p>
<div id="bot-open" token="temptoken">
<span>
<img id="avatar" src="/img/avatar.png"/>
<i id="message-count">2</i>
</span>
</div>
<div id="bot-close"><img src="/img/close.png" height="20px"/>Close</div>
<div id="webchat" role="main"></div>
</div>
<script src="https://cdn.botframework.com/botframework-webchat/master/webchat.js"></script>
<script src="/js/chat.js"></script>
JavaScript:
(async function () {
// In this demo, we are using Direct Line token from MockBot.
// To talk to your bot, you should use the token exchanged using your Direct Line secret.
// You should never put the Direct Line secret in the browser or client app.
// https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-direct-line-3-0-authentication
var bearer_token = document.getElementById("bot-open").getAttribute("token");
const res = await fetch('https://directline.botframework.com/v3/directline/tokens/generate', {
method: 'POST',
headers: {
"Authorization": "Bearer " + bearer_token
}
});
const {
token
} = await res.json();
// We are using a customized store to add hooks to connect event
const store = window.WebChat.createStore({}, ({
dispatch
}) => next => action => {
if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED') {
// When we receive DIRECT_LINE/CONNECT_FULFILLED action, we will send an event activity using WEB_CHAT/SEND_EVENT
dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'webchat/join',
value: {
language: window.navigator.language
}
}
});
}
return next(action);
});
const styleOptions = {
bubbleBackground: 'rgba(0, 0, 255, .1)',
bubbleFromUserBackground: 'rgba(0, 255, 0, .1)',
hideUploadButton: true,
botAvatarInitials: 'DA',
};
window.WebChat.renderWebChat({
directLine: window.WebChat.createDirectLine({
token
}),
userID: guid(),
store,
styleOptions
}, document.getElementById('webchat'));
sizeBotChat();
document.querySelector('#webchat > *').focus();
})().catch(err => console.error(err));
function sizeBotChat() {
let bot_container = document.getElementById("bot-button");
if (isMobileDevice()) {
bot_container.style.width = "100%";
bot_container.style.bottom = "0px";
bot_container.style.right = "0px";
let max_height = screen.height - 50;
document.getElementById("webchat").style.maxHeight = max_height + "px";
console.log(screen.height);
} else {
bot_container.style.width = "400px";
bot_container.style.right = "50px";
document.getElementById("webchat").style.maxHeight = "400px";
}
}
CSS (loaded by javascript inserting a link into the head element):
.triangle-isosceles {
position: relative;
padding: 15px;
color: black;
background: white;
border-radius: 10px;
}
/* creates triangle */
.triangle-isosceles:after {
content: "";
display: block;
/* reduce the damage in FF3.0 */
position: absolute;
bottom: -15px;
right: 30px;
width: 0;
border-width: 15px 15px 0;
border-style: solid;
border-color: white transparent;
}
#avatar {
height: 50px;
}
#need-help {
display: none;
}
/* based on badge progress-bar-danger from bootstrap */
#message-count {
display: inline-block;
min-width: 10px;
padding: 3px 7px 3px 7px;
font-size: 12px;
font-weight: 700;
line-height: 1;
color: white;
text-align: center;
white-space: nowrap;
vertical-align: middle;
border-radius: 10px;
background-color: #d9534f;
position: relative;
top: -20px;
right: 20px;
}
#bot-button {
position: fixed;
bottom: 50px;
right: 0px;
width: 100%;
}
#bot-open {
height: 50px;
width: 100%;
text-align: right;
}
#bot-close {
background-color: blue;
background-image: url("https://localhost/img/avatar.png");
background-repeat: no-repeat;
color: white;
height: 22px;
display: none;
height: 50px;
padding: 15px 15px 0 0;
text-align: right;
vertical-align: middle;
}
/* hide chat on load */
#webchat {
display: none;
max-height: 400px;
overflow: scroll;
}
#webchat div.row.message {
margin: 0;
}
The developers designed WebChat to scroll the conversation to the bottom if the user hasn't scrolled up. If the user has scrolled up, there should be a 'New Message' button that appears in the bottom right corner of the chat when the bot sends a new message.
You can modify this behavior by using a custom middleware - which it looks like you already are - and scrolling the last element in the conversation into view when the user receives a message from the bot. See the code snippet below.
const store = window.WebChat.createStore(
{},
({ dispatch }) => next => action => {
if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY') {
document.querySelector('ul[role="list"]').lastChild.scrollIntoView({behavior: 'smooth', block: 'start'});
}
...
return next(action);
}
);
Hope this helps!