I am trying to create a directive with the following functionalty:
when the line breaks (no more place in the div) a tooltip will be created (with the full text) and the text will get cut and replaced by 3 dots.
everything i found so far is for multi line, the best i got is this:
css:
.trim-info {
max-width: 50px;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 15px;
position: relative;
}
template:
'<div class="trim-info" uib-tooltip="{{lineCtrl.text}}">{{lineCtrl.text}}</div>'
But, as you can see the width is hardcoded.
My question is how can I make it dynamcily to the parent width.
In css you can do
.parent-div {
display: flex;
}
.text-div {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
In your directive you can check
angular.module('myApp').directive('tooltip', function() {
function isEllipsisActive(e) {
return (e.offsetWidth < e.scrollWidth);
}
return {
restrict: 'E',
link: function(scope, el, attr) {
var addTooltip = isEllipsisActive(el);
}
};
}
And then depending on this value enable tooltip.
Here is a realy simple way to do it with some jQuery.
The styling is up to you :)
$('.tooltip-bottom').each(function() {
var after = $(this).clone();
after.removeClass('tooltip-bottom')
.removeClass('small')
.addClass('tooltip')
.css({"position": "absolute",
"top": ($(this).position().top + $(this).height() ) + "px",
"left": $(this).position().left + "px"});
$('.MyContainer').append(after);
});
.small {
width: 50px;
height: 15px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tooltip {
display: none;
transition: all 0.5s;
}
.small:hover ~ .tooltip {
display: block;
overflow-x: visible;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="MyContainer">
<div class="small tooltip-bottom">this is a very small div</div>
</div>
Related
I'm using javascript to display dynamic text and image, but having trouble with formatting.
I'm using display: flex to put text and image next to each other, but am having trouble horizontally aligning them. Right now, it looks like:
But I'd like to horizontally align them so that it becomes:
I've tried the following, but this didn't work
#conA #container {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
Update:
Implementing the following code but NOT setting the height of #heroText and #images the same
#conA #container {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
does center text and images horizontally when both are present. However, once the images disappear, the text jumps. Here's how it's behaving https://imgur.com/a/7yYl8zO I'd like the text to not move when images disappear
Once I set the heights of #heroText and #images the same, it then turns to this:
I'm also looking to center the whole text+image in the parent div (#conA, which takes up full screen 100vh). I tried the following:
#conA {
position: relative;
}
#conA #container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
But it looks odd b/c text keeps moving/jumping depending on whether there's an image displayed.
So when there's no image, it looks like:
When when images appear, text moves to left so that the whole thing becomes centered:
How can I fix the position of text here? I'd like the whole thing is centered when there's an image. So when there's no image:
When there's an image:
html
<section id="conA">
<div id="container">
<div id="heroText">
<div id="text-fixed">I'm a fixed text</div>
<div id="text"></div>
</div>
<div id="images"></div>
</div>
</section>
css
#conA {
height: 100vh;
position: relative;
}
#conA #container {
margin: 0;
}
#conA #text {
display: initial;
border-right: 3px solid #56525E;
}
#heroText {
line-height: 1.7;
font-size: 30px;
width: 800px;
}
#conA #container {
margin: 0;
display: flex;
}
#heroText {
height: 400px;
}
#conA #images {
height: 400px;
}
#conA img
{
display:none;
height: 400px;
}
#conA img.invisible
{
visibility: hidden;
}
#conA img.show
{
display:inline;
}
#conA img.anim1
{
animation-duration: 2000ms;
}
#conA img.anim2
{
animation-duration: 2000ms;
}
#conA img.anim3
{
animation-duration: 2000ms;
}
.fadeIn
{
animation-name: fadeIn;
}
#keyframes fadeIn
{
0% {opacity: 0;}
100% {opacity: 1;}
}
.fadeOut
{
animation-name: fadeOut;
}
javascript code for dynamic display of text and image
// List of sentences
var _CONTENT = [ "I'm the first sentence.", "I'm the second sentence."
, "I'm the third sentence.", "I'm the fourth sentence." ];
var IMAGE_URLS = ['img/image1.png', 'img/image2.jpg', 'img/image3.png', 'img/image1.png','img/image2.png','img/image3.png', 'img/image4.png','img/image5.png'];
var IMAGES = jQuery.map(IMAGE_URLS, function (url, index){
var img = document.createElement('img');
img.setAttribute('src', url);
img.classList.add('anim'+((index%2)+1));
img.classList.add('fadeOut');
document.getElementById('images').appendChild(img);
return img;
});
// Current sentence being processed
var _PART = 0;
// Character number of the current sentence being processed
var _PART_INDEX = 0;
// Holds the handle returned from setInterval
var _INTERVAL_VAL;
// Element that holds the text
var _ELEMENT = document.querySelector("#text");
// Implements typing effect
function Type() {
var text = _CONTENT[_PART].substring(0, _PART_INDEX + 1);
_ELEMENT.innerHTML = text;
_PART_INDEX++;
// If full sentence has been displayed then start to delete the sentence after some time
if(text === _CONTENT[_PART]) {
var imgIndexBase = _PART*2;
IMAGES[imgIndexBase].classList.remove('fadeOut');
IMAGES[imgIndexBase+1].classList.remove('fadeOut');
setTimeout(function () { IMAGES[imgIndexBase].classList.add('fadeIn'); }, 0);
setTimeout(function () { IMAGES[imgIndexBase].classList.add('show'); }, 0);
setTimeout(function () { IMAGES[imgIndexBase].classList.add('fadeOut'); }, 2000);
setTimeout(function () { IMAGES[imgIndexBase].classList.remove('fadeOut'); }, 3000);
setTimeout(function () { IMAGES[imgIndexBase].classList.remove('show'); }, 3000);
setTimeout(function () { IMAGES[imgIndexBase + 1].classList.add('fadeIn'); }, 0);
setTimeout(function () { IMAGES[imgIndexBase + 1].classList.add('show'); }, 0);
setTimeout(function () { IMAGES[imgIndexBase + 1].classList.add('fadeOut'); }, 2000);
setTimeout(function () { IMAGES[imgIndexBase + 1].classList.remove('fadeOut'); }, 3000);
setTimeout(function () { IMAGES[imgIndexBase + 1].classList.remove('show'); }, 3000);
clearInterval(_INTERVAL_VAL);
setTimeout(function() {
_INTERVAL_VAL = setInterval(Delete, 50);
}, 4000);
}
}
// Implements deleting effect
function Delete() {
var text = _CONTENT[_PART].substring(0, _PART_INDEX - 1);
_ELEMENT.innerHTML = text;
_PART_INDEX--;
// If sentence has been deleted then start to display the next sentence
if(text === '') {
clearInterval(_INTERVAL_VAL);
// If last sentence then display the first one, else move to the next
if(_PART == (_CONTENT.length - 1))
_PART = 0;
else
_PART++;
_PART_INDEX = 0;
// Start to display the next sentence after some time
setTimeout(function() {
_INTERVAL_VAL = setInterval(Type, 100);
}, 500);
}
}
// Start the typing effect on load
_INTERVAL_VAL = setInterval(Type, 100);
See example below using flex CSS. First section with image, second section with no images in .images div.
There is a lot to explain with flex, but it is super powerful when it comes to dynamic layout. Hope this gets you on the right track.
Also you need to relax on your id attribute usage, valid html only allow single usage of an id attribute value. Use class attribute for multiple instances, and id for single usage instances.
id attribute value should only ever be used once, never multiple times.
BODY {
padding: 1rem;
margin: 0;
}
SECTION .container {
border: 1px black solid;
padding: 2rem;
display: flex;
align-items: center;
flex-wrap: wrap;
flex-direction: initial;
min-height: 100px;
justify-content: center;
margin-bottom: 1rem;
}
SECTION .container .hero-text {
width: 50%;
background: cyan;
}
SECTION .container .images {
width: 50%;
background: red;
}
SECTION .container .images IMG {
display: block;
width: 100%;
}
<section>
<div class="container">
<div class="hero-text">
<div class="text-fixed">I'm a fixed text</div>
<div class="text">And I am loving life :)</div>
</div>
<div class="images">
<img src="https://i.imgur.com/q5Y5RCH.png" alt="" />
</div>
</div>
</section>
<section>
<div class="container">
<div class="hero-text">
<div class="text-fixed">I'm a fixed text</div>
<div class="text">And I am loving life :)</div>
</div>
<div class="images">
<!-- no images -->
</div>
</div>
</section>
My page reads as follows.
The problem is that when I use draggabilly to drag the move-line, dragging to the left is fine, but when I drag it to the right, it doesn't work in the area where the tinymce compiler is located. How do I fix this problem?
<style>
html,
body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
}
.all-warp {
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
}
.left {
width: 200px;
background-color: burlywood;
}
.move-line {
width: 4px;
background-color: crimson;
cursor:e-resize;
}
.editor-warp {
flex-grow: 2;
}
</style>
<body>
<div class="all-warp">
<div class="left"></div>
<div class="move-line"></div>
<div class="editor-warp">
<textarea id="editor"></textarea>
</div>
</div>
<script>
$(document).ready(function () {
var $draggable = $('.move-line').draggabilly({
axis: 'x',
containment: '.all-warp'
})
});
tinymce.init({
selector: '#editor',
menubar: false,
statusbar: false,
resize: false,
height:500,
});
</script>
</body>
</html>
Without seeing actual running code I cannot say for sure but I would suspect this is because the TinyMCE editing region itself is an iframe? Does the drag code you have work if you replace TinyMCE with a regular iframe? If not that would be your issue.
I need to show my paragraph inside my <div> when click on the <div>. This is my code:
const area = document.getElementById("area");
const popup = document.getElementById("popup");
function showPopup(event) {
let x = event.clientX;
let y = event.clientY;
popup.style.left = `${x}px`;
popup.style.top = `${y}px`;
popup.style.visibility = "visible";
}
area.addEventListener("mousedown", showPopup);
.area {
border: 1px solid;
position: absolute;
height: 200px;
width: 200px;
}
.popup {
visibility: hidden;
position: absolute;
display: inline-block;
}
<div id="area" class="area">
<p class="popup" id="popup">popup</p>
</div>
Note that this is inside another main body <div> (also with position: absolute).
Try this
<div id="area">
<div class="area""
<p class="popup" id="popup">popup</p>
</div>
</div>
I tested your code and observed you are changing style.top property based on your ClientX value which is causing popup element to appear over random position. Use following updated code and it's should be good.
function showPopup(event) {
console.log(event.clientX, event.clientY)
let x = event.clientX;
let y = event.clientY;
popup.style.left = `${x - 8}px`;
popup.style.top = `${y - 8}px`;
popup.style.visibility = "visible";
}
and few CSS changes as -
.area {
border: 1px solid;
position: relative;
height: 200px;
width: 200px;
}
.popup {
visibility: hidden;
position: absolute;
display: inline-block;
margin: 0;
}
For more close positioning of popup element.
Having a flex layout with top, left, middle and right.
Middle is divided into main and foot.
Within the main I want to have fixed elements, kind of like an MDI, as well as static elements.
If one scroll the fixed element should stay in same position of view. But, it should be contained within the main element if it is moved above or to the left of main. As in: not overlap the top, left, right etc.
THIS:
Colors and margins added to make a visual representation of the layout
NOT THIS:
Below is a simplified sample with a container within a container.
If one select the fixed positioning for the sub "window" it stay in place on scrolling, but it overlaps the parent if moved outside.
I can use absolute and reposition it on scroll by using JavaScript, but wondered if there was a pure CSS / layout way to get the same result.
function set_style_pos (e) {
moveable.style.position = e.target.value;
}
function halt (e) {
e.preventDefault();
e.stopPropagation();
}
const drag = {
el: null,
ex: 0,
ey: 0,
xs: 0,
ys: 0,
move: function (e) {
halt(e);
drag.el.style.marginLeft = (e.clientX - drag.sx + drag.ex) + 'px';
drag.el.style.marginTop = (e.clientY - drag.sy + drag.ey) + 'px';
},
end: function (e) {
halt(e);
window.removeEventListener('mouseup', drag.end);
window.removeEventListener('mousemove', drag.move);
},
start: function (e) {
let cs;
halt(e);
window.addEventListener('mouseup', drag.end);
window.addEventListener('mousemove', drag.move);
drag.el = e.target;
cs = getComputedStyle(drag.el);
drag.ex = parseInt(cs.getPropertyValue('margin-left')) || 0;
drag.ey = parseInt(cs.getPropertyValue('margin-top')) || 0;
drag.sx = e.clientX;
drag.sy = e.clientY;
},
check: function (e) {
let t = e.target;
if (t.dataset.moveable == "1")
drag.start(e);
}
};
document.addEventListener('mousedown', drag.check);
document.addEventListener('change', set_style_pos);
lines.textContent = "scroll me\n".repeat(100);
* {
box-sizing: border-box;
}
body {
margin: 0;
height: 100vh;
display: flex;
flex-direction: column;
color: #444;
font: 14px sans-serif;
}
label {
cursor: pointer;
}
.outer {
display: flex;
padding: 20px;
background: goldenrod;
flex-grow: 1;
overflow: hidden;
}
.inner {
position: relative;
overflow: scroll;
background: gray;
flex-grow: 1;
}
.box {
position: absolute;
width: 140px;
height: 150px;
background: silver;
box-shadow: 0 0 3px red;
cursor: move;
margin-left: 90px;
margin-top: -5px;
padding: 20px;
}
.box div {
font-weight: 700;
pointer-events: none;
text-align: center;
}
<div class="outer">
<div class="inner">
<div class="box" id="moveable" data-moveable="1">
<div>Move Me</div><br />
<label><input type="radio" name="p" value="absolute" checked />absolute</label><br />
<label><input type="radio" name="p" value="fixed" />fixed</label>
</div>
<pre id="lines"></pre>
</div>
</div>
Just use z-index.
Example:
function set_style_pos (e) {
moveable.style.position = e.target.value;
}
function halt (e) {
e.preventDefault();
e.stopPropagation();
}
const drag = {
el: null,
ex: 0,
ey: 0,
xs: 0,
ys: 0,
move: function (e) {
halt(e);
drag.el.style.marginLeft = (e.clientX - drag.sx + drag.ex) + 'px';
drag.el.style.marginTop = (e.clientY - drag.sy + drag.ey) + 'px';
},
end: function (e) {
halt(e);
window.removeEventListener('mouseup', drag.end);
window.removeEventListener('mousemove', drag.move);
},
start: function (e) {
let cs;
halt(e);
window.addEventListener('mouseup', drag.end);
window.addEventListener('mousemove', drag.move);
drag.el = e.target;
cs = getComputedStyle(drag.el);
drag.ex = parseInt(cs.getPropertyValue('margin-left')) || 0;
drag.ey = parseInt(cs.getPropertyValue('margin-top')) || 0;
drag.sx = e.clientX;
drag.sy = e.clientY;
},
check: function (e) {
let t = e.target;
if (t.dataset.moveable == "1")
drag.start(e);
}
};
document.addEventListener('mousedown', drag.check);
document.addEventListener('change', set_style_pos);
lines.textContent = "scroll me\n".repeat(100);
* {
box-sizing: border-box;
}
body {
margin: 0;
height: 100vh;
display: flex;
flex-direction: column;
color: #444;
font: 14px sans-serif;
}
label {
cursor: pointer;
}
.outer {
display: flex;
padding: 20px;
background: goldenrod;
/*flex-grow:1; Disable to control the height for presentaion*/
height:200px !important;
overflow:hidden; /*to hide scrollme lines*/
}
.inner {
position: relative;
overflow: scroll;
background: gray;
flex-grow: 1;
}
.box {
position: absolute;
width: 140px;
height: 150px;
background: silver;
box-shadow: 0 0 3px red;
cursor: move;
margin-left: 90px;
margin-top: -5px;
padding: 20px;
}
.box div {
font-weight: 700;
pointer-events: none;
text-align: center;
}
.prevent{
width:200px;
height:200px;
display:flex;
background-color:blue;
color:white;
justify-content:center;
align-items:center;
font-weight:bold;
/*--The solution--*/
z-index:1;
}
<div class="outer">
<div class="inner">
<div class="box" id="moveable" data-moveable="1">
<div>Move Me</div><br />
<label><input type="radio" name="p" value="absolute" checked />absolute</label><br />
<label><input type="radio" name="p" value="fixed" />fixed</label>
</div>
<pre id="lines"></pre>
</div>
</div>
<div class="prevent">
Prevent overlap
</div>
I hope this helps.
Use a sticky container and let children be absolute.
Had tested with z-index, all over, before posting but had not found any satisfactory solution that way.
I also tried various with position: sticky, and there is where I found the solution at last :)
One can wrap the sub windows in a sticky container which is positioned top left of the main container.
Pros:
Simple
Fairly clean HTML structure
The window stay below scroll-bars of container
Positioning relative to content wrapper
Cons:
If one want to make it non-fixed / non-sticy one have to move the element to parent and vice versa.
Absolute positioned children will not expand the container – thus not rearranging the DOM flow. (Which was the issue on earlier attempts using sticky).
Tested in FireFox, Chrome, Vivaldi, Opera Mini and Opera.
The core of it:
<div class="outer">
<div class="main">
<div class="wrap-sticky">
<div class="sub-window">
Fixed Window
</div>
</div>
Other "normal" content
</div>
</div>
And:
.outer {
overflow: hidden;
}
.main {
position: relative;
overflow: scroll;
}
.wrap-sticky {
position: sticky;
top: 0;
left: 0;
}
.sub-window {
position: absolute;
}
function get_pos (el) {
let cs = getComputedStyle(el);
return [
parseInt(cs.getPropertyValue('left')) || 0,
parseInt(cs.getPropertyValue('top')) || 0
];
}
function set_style_pos (e) {
let [x, y] = get_pos (moveable);
if (e.target.value == "sticky") {
wrap_sticky.appendChild(moveable);
moveable.style.left = (x - inner.scrollLeft) + 'px';
moveable.style.top = (y - inner.scrollTop) + 'px';
} else {
inner.appendChild(moveable);
moveable.style.left = (x + inner.scrollLeft) + 'px';
moveable.style.top = (y + inner.scrollTop) + 'px';
}
}
function halt (e) {
e.preventDefault();
e.stopPropagation();
}
const drag = {
el: null,
ex: 0,
ey: 0,
xs: 0,
ys: 0,
move: function (e) {
halt(e);
drag.el.style.left = (e.clientX - drag.sx + drag.ex) + 'px';
drag.el.style.top = (e.clientY - drag.sy + drag.ey) + 'px';
},
end: function (e) {
halt(e);
window.removeEventListener('mouseup', drag.end);
window.removeEventListener('mousemove', drag.move);
},
start: function (e) {
halt(e);
window.addEventListener('mouseup', drag.end);
window.addEventListener('mousemove', drag.move);
drag.el = e.target;
[drag.ex, drag.ey] = get_pos(drag.el);
drag.sx = e.clientX;
drag.sy = e.clientY;
},
check: function (e) {
let t = e.target;
if (t.dataset.moveable == "1")
drag.start(e);
}
};
document.addEventListener('mousedown', drag.check);
document.addEventListener('change', set_style_pos);
lines.textContent = "scroll me\n".repeat(100) + "horiz".repeat(100) + 'END';
* {
box-sizing: border-box;
}
body {
margin: 0;
height: 100vh;
display: flex;
flex-direction: column;
color: #444;
font: 14px sans-serif;
}
label {
cursor: pointer;
}
.outer {
display: flex;
padding: 20px;
background: goldenrod;
flex-grow: 1;
overflow: hidden;
}
.inner {
position: relative;
overflow: scroll;
background: gray;
flex-grow: 1;
}
.box {
position: absolute;
width: 160px;
height: 100px;
background: silver;
box-shadow: 0 0 3px red;
cursor: move;
padding: 20px;
top: 20px;
left: 20px;
}
.box div {
font-weight: 700;
pointer-events: none;
text-align: center;
}
.wrap-sticky {
position: sticky;
top: 0;
left: 0;
}
<div class="outer">
<div class="inner" id="inner">
<div class="wrap-sticky" id="wrap_sticky">
<div class="box" id="moveable" data-moveable="1">
<div>Drag & Move Me</div>
<label><input type="radio" name="p" value="sticky" checked />In sticky</label><br />
<label><input type="radio" name="p" value="absolute" />In main</label>
</div>
</div>
<pre id="lines"></pre>
</div>
</div>
Here is an example chat app ->
The idea here is to have the .messages-container take up as much of the screen as it can. Within .messages-container, .scroll holds the list of messages, and in case there are more messages then the size of the screen, scrolls.
Now, consider this case:
The user scrolls to the bottom of the conversation
The .text-input, dynamically gets bigger
Now, instead of the user staying scrolled to the bottom of the conversation, the text-input increases, and they no longer see the bottom.
One way to fix it, if we are using react, calculate the height of text-input, and if anything changes, let .messages-container know
componentDidUpdate() {
window.setTimeout(_ => {
const newHeight = this.calcHeight();
if (newHeight !== this._oldHeight) {
this.props.onResize();
}
this._oldHeight = newHeight;
});
}
But, this causes visible performance issues, and it's sad to be passing messages around like this.
Is there a better way? Could I use css in such a way, to express that when .text-input-increases, I want to essentially shift up all of .messages-container
2:nd revision of this answer
Your friend here is flex-direction: column-reverse; which does all you ask while align the messages at the bottom of the message container, just like for example Skype and many other chat apps do.
.chat-window{
display:flex;
flex-direction:column;
height:100%;
}
.chat-messages{
flex: 1;
height:100%;
overflow: auto;
display: flex;
flex-direction: column-reverse;
}
.chat-input { border-top: 1px solid #999; padding: 20px 5px }
.chat-input-text { width: 60%; min-height: 40px; max-width: 60%; }
The downside with flex-direction: column-reverse; is a bug in IE/Edge/Firefox, where the scrollbar doesn't show, which your can read more about here: Flexbox column-reverse and overflow in Firefox/IE
The upside is you have ~ 90% browser support on mobile/tablets and ~ 65% for desktop, and counting as the bug gets fixed, ...and there is a workaround.
// scroll to bottom
function updateScroll(el){
el.scrollTop = el.scrollHeight;
}
// only shift-up if at bottom
function scrollAtBottom(el){
return (el.scrollTop + 5 >= (el.scrollHeight - el.offsetHeight));
}
In the below code snippet I've added the 2 functions from above, to make IE/Edge/Firefox behave in the same way flex-direction: column-reverse; does.
function addContent () {
var msgdiv = document.getElementById('messages');
var msgtxt = document.getElementById('inputs');
var atbottom = scrollAtBottom(msgdiv);
if (msgtxt.value.length > 0) {
msgdiv.innerHTML += msgtxt.value + '<br/>';
msgtxt.value = "";
} else {
msgdiv.innerHTML += 'Long long content ' + (tempCounter++) + '!<br/>';
}
/* if at bottom and is IE/Edge/Firefox */
if (atbottom && (!isWebkit || isEdge)) {
updateScroll(msgdiv);
}
}
function resizeInput () {
var msgdiv = document.getElementById('messages');
var msgtxt = document.getElementById('inputs');
var atbottom = scrollAtBottom(msgdiv);
if (msgtxt.style.height == '120px') {
msgtxt.style.height = 'auto';
} else {
msgtxt.style.height = '120px';
}
/* if at bottom and is IE/Edge/Firefox */
if (atbottom && (!isWebkit || isEdge)) {
updateScroll(msgdiv);
}
}
/* fix for IE/Edge/Firefox */
var isWebkit = ('WebkitAppearance' in document.documentElement.style);
var isEdge = ('-ms-accelerator' in document.documentElement.style);
var tempCounter = 6;
function updateScroll(el){
el.scrollTop = el.scrollHeight;
}
function scrollAtBottom(el){
return (el.scrollTop + 5 >= (el.scrollHeight - el.offsetHeight));
}
html, body { height:100%; margin:0; padding:0; }
.chat-window{
display:flex;
flex-direction:column;
height:100%;
}
.chat-messages{
flex: 1;
height:100%;
overflow: auto;
display: flex;
flex-direction: column-reverse;
}
.chat-input { border-top: 1px solid #999; padding: 20px 5px }
.chat-input-text { width: 60%; min-height: 40px; max-width: 60%; }
/* temp. buttons for demo */
button { width: 12%; height: 44px; margin-left: 5%; vertical-align: top; }
/* begin - fix for hidden scrollbar in IE/Edge/Firefox */
.chat-messages-text{ overflow: auto; }
#media screen and (-webkit-min-device-pixel-ratio:0) {
.chat-messages-text{ overflow: visible; }
/* reset Edge as it identifies itself as webkit */
#supports (-ms-accelerator:true) { .chat-messages-text{ overflow: auto; } }
}
/* hide resize FF */
#-moz-document url-prefix() { .chat-input-text { resize: none } }
/* end - fix for hidden scrollbar in IE/Edge/Firefox */
<div class="chat-window">
<div class="chat-messages">
<div class="chat-messages-text" id="messages">
Long long content 1!<br/>
Long long content 2!<br/>
Long long content 3!<br/>
Long long content 4!<br/>
Long long content 5!<br/>
</div>
</div>
<div class="chat-input">
<textarea class="chat-input-text" placeholder="Type your message here..." id="inputs"></textarea>
<button onclick="addContent();">Add msg</button>
<button onclick="resizeInput();">Resize input</button>
</div>
</div>
Side note 1: The detection method is not fully tested, but it should work on newer browsers.
Side note 2: Attach a resize event handler for the chat-input might be more efficient then calling the updateScroll function.
Note: Credits to HaZardouS for reusing his html structure
You just need one CSS rule set:
.messages-container, .scroll {transform: scale(1,-1);}
That's it, you're done!
How it works: First, it vertically flips the container element so that the top becomes the bottom (giving us the desired scroll orientation), then it flips the content element so that the messages won't be upside down.
This approach works in all modern browsers. It does have a strange side effect, though: when you use a mouse wheel in the message box, the scroll direction is reversed. This can be fixed with a few lines of JavaScript, as shown below.
Here's a demo and a fiddle to play with:
//Reverse wheel direction
document.querySelector('.messages-container').addEventListener('wheel', function(e) {
if(e.deltaY) {
e.preventDefault();
e.currentTarget.scrollTop -= e.deltaY;
}
});
//The rest of the JS just handles the test buttons and is not part of the solution
send = function() {
var inp = document.querySelector('.text-input');
document.querySelector('.scroll').insertAdjacentHTML('beforeend', '<p>' + inp.value);
inp.value = '';
inp.focus();
}
resize = function() {
var inp = document.querySelector('.text-input');
inp.style.height = inp.style.height === '50%' ? null : '50%';
}
html,body {height: 100%;margin: 0;}
.conversation {
display: flex;
flex-direction: column;
height: 100%;
}
.messages-container {
flex-shrink: 10;
height: 100%;
overflow: auto;
}
.messages-container, .scroll {transform: scale(1,-1);}
.text-input {resize: vertical;}
<div class="conversation">
<div class="messages-container">
<div class="scroll">
<p>Message 1<p>Message 2<p>Message 3<p>Message 4<p>Message 5
<p>Message 6<p>Message 7<p>Message 8<p>Message 9<p>Message 10<p>Message 11<p>Message 12<p>Message 13<p>Message 14<p>Message 15<p>Message 16<p>Message 17<p>Message 18<p>Message 19<p>Message 20
</div>
</div>
<textarea class="text-input" autofocus>Your message</textarea>
<div>
<button id="send" onclick="send();">Send input</button>
<button id="resize" onclick="resize();">Resize input box</button>
</div>
</div>
Edit: thanks to #SomeoneSpecial for suggesting a simplification to the scroll code!
Please try the following fiddle - https://jsfiddle.net/Hazardous/bypxg25c/. Although the fiddle is currently using jQuery to grow/resize the text area, the crux is in the flex related styles used for the messages-container and input-container classes -
.messages-container{
order:1;
flex:0.9 1 auto;
overflow-y:auto;
display:flex;
flex-direction:row;
flex-wrap:nowrap;
justify-content:flex-start;
align-items:stretch;
align-content:stretch;
}
.input-container{
order:2;
flex:0.1 0 auto;
}
The flex-shrink value is set to 1 for .messages-container and 0 for .input-container. This ensures that messages-container shrinks when there is a reallocation of size.
I've moved text-input within messages, absolute positioned it to the bottom of the container and given messages enough bottom padding to space accordingly.
Run some code to add a class to conversation, which changes the height of text-input and bottom padding of messages using a nice CSS transition animation.
The JavaScript runs a "scrollTo" function at the same time as the CSS transition is running to keep the scroll at the bottom.
When the scroll comes off the bottom again, we remove the class from conversation
Hope this helps.
https://jsfiddle.net/cnvzLfso/5/
var doScollCheck = true;
var objConv = document.querySelector('.conversation');
var objMessages = document.querySelector('.messages');
var objInput = document.querySelector('.text-input');
function scrollTo(element, to, duration) {
if (duration <= 0) {
doScollCheck = true;
return;
}
var difference = to - element.scrollTop;
var perTick = difference / duration * 10;
setTimeout(function() {
element.scrollTop = element.scrollTop + perTick;
if (element.scrollTop === to) {
doScollCheck = true;
return;
}
scrollTo(element, to, duration - 10);
}, 10);
}
function resizeInput(atBottom) {
var className = 'bigger',
hasClass;
if (objConv.classList) {
hasClass = objConv.classList.contains(className);
} else {
hasClass = new RegExp('(^| )' + className + '( |$)', 'gi').test(objConv.className);
}
if (atBottom) {
if (!hasClass) {
doScollCheck = false;
if (objConv.classList) {
objConv.classList.add(className);
} else {
objConv.className += ' ' + className;
}
scrollTo(objMessages, (objMessages.scrollHeight - objMessages.offsetHeight) + 50, 500);
}
} else {
if (hasClass) {
if (objConv.classList) {
objConv.classList.remove(className);
} else {
objConv.className = objConv.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
}
}
}
}
objMessages.addEventListener('scroll', function() {
if (doScollCheck) {
var isBottom = ((this.scrollHeight - this.offsetHeight) === this.scrollTop);
resizeInput(isBottom);
}
});
html,
body {
height: 100%;
width: 100%;
background: white;
}
body {
margin: 0;
padding: 0;
}
.conversation {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
position: relative;
}
.messages {
overflow-y: scroll;
padding: 10px 10px 60px 10px;
-webkit-transition: padding .5s;
-moz-transition: padding .5s;
transition: padding .5s;
}
.text-input {
padding: 10px;
-webkit-transition: height .5s;
-moz-transition: height .5s;
transition: height .5s;
position: absolute;
bottom: 0;
height: 50px;
background: white;
}
.conversation.bigger .messages {
padding-bottom: 110px;
}
.conversation.bigger .text-input {
height: 100px;
}
.text-input input {
height: 100%;
}
<div class="conversation">
<div class="messages">
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is the last message
</p>
<div class="text-input">
<input type="text" />
</div>
</div>
</div>
You write;
Now, consider this case:
The user scrolls to the bottom of the conversation
The .text-input, dynamically gets bigger
Wouldn't the method that dynamically sets the .text-input be the logical place to fire this.props.onResize().
To whom it may concern,
The answers above did not suffice my question.
The solution I found was to make my innerWidth and innerHeight variable constant - as the innerWidth of the browser changes on scroll to adapt for the scrollbar.
var innerWidth = window.innerWidth
var innerHeight = window.innerHeight
OR FOR REACT
this.setState({width: window.innerWidth, height: window.innerHeight})
In other words, to ignore it, you must make everything constant as if it were never scrolling. Do remember to update these on Resize / Orientation Change !
IMHO current answer is not a correct one:
1/ flex-direction: column-reverse; reverses the order of messages - I didn't want that.
2/ javascript there is also a bit hacky and obsolete
If you want to make it like a PRO use spacer-box which has properties:
flex-grow: 1;
flex-basis: 0;
and is located above messages. It pushes them down to the chat input.
When user is typing new messages and input height is growing the scrollbar moves up, but when the message is sent (input is cleared) scrollbar is back at bottom.
Check my snippet:
body {
background: #ccc;
}
.chat {
display: flex;
flex-direction: column;
width: 300px;
max-height: 300px;
max-width: 90%;
background: #fff;
}
.spacer-box {
flex-basis: 0;
flex-grow: 1;
}
.messages {
display: flex;
flex-direction: column;
overflow-y: auto;
flex-grow: 1;
padding: 24px 24px 4px;
}
.footer {
padding: 4px 24px 24px;
}
#chat-input {
width: 100%;
max-height: 100px;
overflow-y: auto;
border: 1px solid pink;
outline: none;
user-select: text;
white-space: pre-wrap;
overflow-wrap: break-word;
}
<div class="chat">
<div class="messages">
<div class="spacer-box"></div>
<div class="message">1</div>
<div class="message">2</div>
<div class="message">3</div>
<div class="message">4</div>
<div class="message">5</div>
<div class="message">6</div>
<div class="message">7</div>
<div class="message">8</div>
<div class="message">9</div>
<div class="message">10</div>
<div class="message">11</div>
<div class="message">12</div>
<div class="message">13</div>
<div class="message">14</div>
<div class="message">15</div>
<div class="message">16</div>
<div class="message">17</div>
<div class="message">18</div>
</div>
<div class="footer">
<div contenteditable role="textbox" id="chat-input"></div>
</div>
<div>
Hope I could help :)
Cheers