Measuring overflow is incorrect in Vue - javascript

I'm trying to make a overflow with tagging, which fades out in the beginning to give the user a hint that there's more. This is what it looks like:
I put the fading gradient as a :after inside the CSS and "activate" it by Vue's style binding, when scrollWidth > offsetWidth (overflow bigger than the box itself).
But the problem is that it sometimes (lags?) behind and does not calculate the scrollWidth right, especially when I enter a long word and then delete it. It doesn't "like" that and it says that the overflow is false, but there's no tag in the box. Basically this happens:
I tried to put the calculation inside this $nextTick(), but it didn't solve the issue. I also tried using Vue's keyDown, keyUp and keyPress listeners, but nothing solved this also.
This (also on CodePen) demonstrates the issue:
new Vue({
el: '#tagsinput',
data: {
input_value: "",
tags: []
},
methods: {
addTag: function() {
if (this.input_value > "") {
this.tags.push(this.input_value)
this.input_value = "";
// Refocus the text input, so it stays at the end
this.$refs.input.blur();
this.$nextTick(function() {
this.$refs.input.focus();
})
}
},
deleteTag: function() {
if (this.input_value == "") {
this.tags.pop()
}
}
}
})
.outter {
border: 1px solid red;
width: 250px;
overflow: hidden;
display: flex;
}
.inner {
border: 1px solid blue;
margin: 2px;
display: flex;
}
.tag {
border: 1px solid black;
margin: 2px;
}
input {
min-width: 80px;
width: 80px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.2/vue.min.js"></script>
<div id="tagsinput">
<div class="outter" ref="outter">
<div class="inner" ref="inner">
<div class="tag" v-for="tag in tags">{{tag}}</div><input type="text" v-model="input_value" #keydown.enter="addTag" #keydown.delete="deleteTag">
</div>
</div>
Outter div scrollwidth: {{ $refs.outter ? $refs.outter.scrollWidth : null }}<br> Outter div offsetWidth: {{ $refs.outter ? $refs.outter.offsetWidth : null }}<br>
<br> Is overflowing: {{ ($refs.outter ? $refs.outter.scrollWidth : null) > ($refs.outter ?$refs.outter.offsetWidth : null) }}
</div>
<br><br> Type a really long word in, add and then delete it. "Is overflowing" will be the inverse, until you press Backspace <b>again</b>.
Any help is greatly appreciated.

You should call the check for overflow after the moment you've added or deleted the tag, so you check the overflow at the right moment. Vue isn't databinding an inline condition like that. The following code should work for you. It calls a checkOverflow function within $nextTick, setting a data-binded variable isOverflowed that you then can use to bind some styles.
new Vue({
el: '#tagsinput',
data: {
input_value: null,
tags: [],
isOverflowed: false
},
methods: {
addTag: function() {
if(this.input_value) {
this.tags.push(this.input_value)
this.input_value = null;
// Refocus the text input, so it stays at the end
this.$refs.input.blur();
this.$nextTick(function() {
this.$refs.input.focus();
this.checkOverflow()
})
}
},
deleteTag: function() {
if(!this.input_value) {
this.tags.pop()
this.$nextTick(function() {
this.checkOverflow()
})
}
},
checkOverflow: function() {
this.isOverflowed = (this.$refs.outter ? this.$refs.outter.scrollWidth : null) >
(this.$refs.outter ? this.$refs.outter.offsetWidth : null)
}
}
})
.outter {
border: 1px solid red;
width: 250px;
overflow: hidden;
display: flex;
}
.inner {
border: 1px solid blue;
margin: 2px;
display: flex;
}
.tag {
border: 1px solid black;
margin: 2px;
}
input {
min-width: 80px;
width: 80px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="tagsinput">
<div class="outter" ref="outter">
<div class="inner" ref="inner">
<div class="tag" v-for="tag in tags">{{tag}}</div>
<input type="text" v-model="input_value" #keydown.enter="addTag" #keydown.delete="deleteTag" ref="input">
</div>
</div>
<br>
Is overflowing:
{{ isOverflowed }}
</div>
<br><br>
Type a really long word in, add and then delete it. "Is overflowing" will be the inverse, until you press Backspace <b>again</b>.

More of a CSS/HTML hack ...
Add <div id="spaceFade"></div> after <div id="tagsinput">, then add the following CSS :
#spaceFade {
background-image: linear-gradient(to right, rgba(255,255,255,1), rgba(255,255,255,1), rgba(0,0,0,0));
position : absolute;
height : 2em;
width : 3em;
}
#tagsinput {
position : relative;
}
.outter {
justify-content: flex-end;
}

Related

Blur event for custom vue component

What I have
A custom DropDown with a filter text input above. The DropDown can be opened independently from the filter text input.
What I want
The intended behavior would be, that the dropdown closes when the filter input loses focus and also when I click with the mouse outside of the DropDown, so that the DropDown loses the focus.
What I tried
Bind to the blur event on my root div element in the control, which doesn't fire at all.
I also couldn't find anything about internal component methods which I could override.
Code
<div #blur="onRootLostFocus">
...
</div>
...
...
...
onRootLostFocus() {
console.log('LostFocus');
this.deactivateSearchPanel();
this.deactivateSelectionPanel();
}
Solution
I missed, that a div needs tabindex="0" to be focusable, this fixed my problem
Something like this?
Answer: You need to set tabindex="0" to make it focusable.
Here an custom dropdown how you could do it:
Vue.component("dropdown", {
props: ["value"],
data(){
return {
open: false,
options: ["BMW", "Fiat", "Citroen", "Audi", "Tesla"]
}
},
methods: {
toggleDropdown() {
this.open = !this.open;
},
closeDropdown(){
this.open = false;
},
selectOption(option) {
this.$emit("input", option);
}
},
template: `<div class="dropdown">
<div #blur="closeDropdown" tabindex="0" ref="dropdown" #click="toggleDropdown" class="select">
{{ value }}
</div>
<div class="options" :style="{'max-height': open ? '300px' : '0px'}">
<div v-for="option in options" #mousedown="selectOption(option)" class="option">
{{ option }}
</div>
</div>
</div>`
})
new Vue({
el: "#app",
data: {
selectedCar: "BMW"
}
})
.dropdown {
width: 200px;
position: relative;
}
.select {
height: 40px;
position: absolute;
left: 0;
width: 100%;
background: green;
display: flex;
justify-content: center;
align-items: center;
color: white;
cursor: pointer;
}
.option {
width: 100%;
height: 40px;
background: darkgreen;
color: white;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.option:hover {
background: green;
}
.options {
overflow: hidden;
transition: max-height 200ms;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"> <p>
{{ selectedCar }}</p>
<dropdown v-model="selectedCar" />
</div>
Note there is also tabindex="-1" to make it not reachable via sequential keyboard navigation.
Also consider using a <button> instead because of accessibility concerns.
See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex

Why is this code using .parentNode not working?

Here is the method I’m trying to make. Basically what it’s supposed to do is, when an <input> with the type of button is clicked, it makes the next <div> (in this case hard-coded) go from display: none to display: block. However it’s not working.
matchDivs() {
let showInputs = document.querySelectorAll('input')
const inputs = Array.from(showInputs)
inputs.map(input => {
if (input.parentNode.getAttribute('class') === 'first-employee') {
document.querySelector('.second-employee').setAttribute('style', 'display: block')
}
return input
})
}
When you use if (node.getAttribute('class') === 'first-employee') this is return:
class='first-employee' // true
class='employee first-employee' // false
You must use:
if (node.classList.contains("first-employee")):
class='first-employee' // true
class='employee first-employee' // true
If I have understand your question properly that on button click you want to show/hide DIV tag next to it, which is inside common parent div for both, button and 'second-employee'.
I think below will be helpful.
// For single Div show/hide on button click
let btnMatchDiv = document.querySelector('#btnMatchDivs');
btnMatchDiv.addEventListener('click', matchDivs, true);
function matchDivs() {
let showInputs = document.querySelectorAll('input')
const inputs = Array.from(showInputs)
inputs.map(input => {
// Method 1
// ===========================
/*if (input.parentNode.getAttribute('class') === 'first-employee') {
document.querySelector('.second-employee').setAttribute('style', 'display: block')
}*/
// Method 2 : you can use new classList method
// ===========================
if (input.parentNode.classList.contains('first-employee')) {
input.nextElementSibling.classList.toggle('hidden');
}
//return input
})
}
body {
font-family: Arial;
}
.first-employee {
display: block;
padding: 1em;
border: 1px solid green;
}
.second-employee {
padding: 1em;
border: 1px solid red;
}
#btnMatchDivs {
padding: 1em 0.5em;
border: 1px solid #777;
}
.hidden {
display: none
}
<div class="first-employee">
<h2>
First Employee
</h2>
<input type="button" id="btnMatchDivs" value="Toggle Second Employee" />
<div class="second-employee hidden">
Second Employee
</div>
</div>
Please let me know if you need further help on this.
Thanks,
Jignesh Raval
Also if you want to toggle multiple items then you can try below code.
// For multiple Div show/hide on button click
// ===================================
let showInputs = document.querySelectorAll('input')
const btnInputs = Array.from(showInputs)
// Bind click event to each button input
btnInputs.map(input => {
input.addEventListener('click', matchDivs, false);
})
function matchDivs(event) {
let buttonEle = event.currentTarget;
if (buttonEle.parentNode.classList.contains('first-employee')) {
buttonEle.nextElementSibling.classList.toggle('hidden');
}
}
body {
font-family: Arial;
}
.first-employee {
display: block;
padding: 1em;
border: 1px solid green;
margin-bottom: 1em;
}
.second-employee {
margin: 1em 0;
padding: 1em;
border: 1px solid red;
}
#btnMatchDivs {
padding: 1em 0.5em;
border: 1px solid #777;
}
.hidden {
display: none
}
<div class="first-employee">
<h2>
First Employee 1
</h2>
<input type="button" id="btnMatchDivs" value="Match Divs" />
<div class="second-employee hidden">
Second Employee 1
</div>
</div>
<div class="first-employee">
<h2>
First Employee 2
</h2>
<input type="button" id="btnMatchDivs" value="Match Divs" />
<div class="second-employee hidden">
Second Employee 2
</div>
</div>
<div class="first-employee">
<h2>
First Employee 3
</h2>
<input type="button" id="btnMatchDivs" value="Match Divs" />
<div class="second-employee hidden">
Second Employee 3
</div>
</div>
I hope this will be helpful.
Thanks,
Jignesh Raval

Changing cursor for draggable not working in chrome using vue.js

I am trying to change the cursor of the draggable item in chrome. Everything i tried it is not working. There are solution on Stackoverflow but they are all outdated and not working with the actual chrome version.
On drag the item is copied to a container which is the dragimage for the draggable.
What i want is to have a grabbing cursor while dragging. How would that be possible? Any Ideas?
See my code snippet for an example.
new Vue({
el: '#app',
data: {
text_drop: 'Droppable Area',
text_drag: 'Drag Area',
drag_elements: [
{text: 'one', selected: true},
{text: 'two', selected: false},
{text: 'three', selected: false},
{text: 'four', selected: false},
]
},
computed: {
selected_elements(){
let selected = [];
this.drag_elements.map((drag) => {
if(drag.selected){
selected.push(drag);
}
})
return selected;
}
},
methods: {
drag_it(event){
let html = document.getElementById("dragElement");
let drop_docs = this.selected_elements;
if(drop_docs.length > 1){
let multiple = document.createElement('div');
multiple.classList.add('dragMultiple');
multiple.innerHTML = drop_docs.length + ' items';
html.innerHTML = '';
html.appendChild(multiple)
}else{
html.innerHTML = event.target.outerHTML;
}
event.dataTransfer.setData('text/plain', '' );
event.dataTransfer.setDragImage(html, 0, 0);
event.dataTransfer.effectAllowed = "move";
},
drag_over(event){
document.documentElement.style.cursor="-webkit-grabbing";
},
drag_end(event){
document.documentElement.style.cursor="default";
},
select(event, drag_element){
if(event.metaKey || event.shiftKey){
drag_element.selected = !drag_element.selected;
} else {
this.drag_elements.map((drag) => {
if(drag === drag_element){
drag.selected = true;
}else{
drag.selected = false;
}
})
}
}
}
})
#Dragme{
width: 200px;
height: 50px;
margin-left: 20px;
text-align: center;
border:1px solid black;
float:left;
}
#Dragme:hover {
cursor: -webkit-grab;
}
#Dragme:active {
cursor: -webkit-grabbing;
}
#Dropzone{
float: left;
width: 500px;
height: 100px;
border: 1px solid;
margin-bottom: 50px;
}
.selected{
border: 2px solid yellow !important;
}
.dragMultiple{
border: 1px solid black;
padding: 10px;
background-color: white;
}
#dragElement{
position: absolute;
top: 400px;
}
<script src="https://vuejs.org/js/vue.min.js"></script>
<div id="app">
<div id="Dropzone">{{text_drop}}</div>
<div id="drag_elements">
<div v-for="drag in drag_elements"
#dragstart="drag_it"
#dragover="drag_over"
#dragend="drag_end"
#mouseup="select($event, drag)"
draggable="true"
:class="{selected: drag.selected}"
id="Dragme">{{drag.text}}</div>
</div>
</div>
<div id="dragElement">
</div>
Update
Actually it can be solved with the following answer
CSS for grabbing cursors (drag & drop)
It is important to add the dndclass
thx
Blockquote
#Carr for the hint
Update
After Dragend or drop the cursor is not set to default. Only when moved it changes back. Any Ideas?
Update
With they command key on mac or the shift key multiple items can be selected and dragged. A new dragitem is created for that purpose but the cursor does not allways fall back after dragend or drop.
Update
Integrate method to from answer -by Carr
In fact, setDragImage api is to set the image for replacing that plain document icon which be aside with default cursor, not cursor itself. So your code about '.dragElement' is not working as you expected, it's unstable and causes weird effect when I am testing, I have removed them in my answer.
What I've done below is a little bit tricky, but I think it's at least in correct logic. However, maybe there is a more elegant solution.
new Vue({
el: '#app',
data: {
text_drop: 'Droppable Area',
text_drag: 'Drag Area'
},
methods: {
drag_it(event){
event.dataTransfer.setData('text/plain', '' );
event.dataTransfer.effectAllowed = "move";
},
drag_over(event){
document.documentElement.style.cursor="-webkit-grabbing";
},
drag_end(event){
document.documentElement.style.cursor="default";
}
}
})
#Dragme{
width: 200px;
height: 50px;
text-align: center;
border:1px solid black;
float:left;
}
#Dragme:hover {
cursor: -webkit-grab;
}
#Dragme:active {
cursor: -webkit-grabbing;
}
#Dropzone{
float: left;
width: 300px;
height: 100px;
border: 1px solid;
margin-bottom: 50px;
}
<script src="https://vuejs.org/js/vue.min.js"></script>
<div id="app">
<div id="Dropzone">{{text_drop}}</div>
<div #dragstart="drag_it"
#dragover="drag_over"
#dragend="drag_end"
draggable="true"
id="Dragme">{{text_drag}}</div>
</div>
Update - derivative problems about original question
"dragImage" sticks at bottom, all elements are disappeared, or flashing sometimes.
And here is still a weird part, id attribute should be unique:
And add quote from MDN document about setDragImage, I wrongly recalled svg in comment, it should be canvas :
... The image will typically be an <image> element but it can also be a
<canvas> or any other image element. ...
We could draw text in canvas, it's another question.

Focus on custom element when user clicks and create an overlay behind it

I have a custom element which takes input whenever user clicks on it I want to focus on it and create an overlay on other elements and when the user clicks outside the div I want to remove the overlay.
I am trying to do it using iron-overlay-behavior but not able to achieve the expected behavior.
<custom-element
with-backdrop
scroll-action="lock"
clicked="{{isClicked}}"
></decision-view>
All the examples shown are mostly for paper-dialog but how can I use iron-overlay-behavior for a custom element of my own?
The iron-overlay-behavior seems to be meant more for modal dialogs, what you are trying to accomplish is something different (for instance, modal dialogs are only shown one at a time, and require user input before going back to normal application/website behavior). So I think a natural thing for that behavior would be to disable anything else to focus!
When you say:
create an overlay on other elements
what does that mean? Just paint white over them like they were not visible?
I had a similar issue last week. See Showing a gray backdrop with a mixin
.
For a demo, see this pen from Makha:
<dom-module id="parent-component">
<template>
<style>
:host {
display: block;
margin: 10px auto auto auto;
border: 2px solid gray;
border-radius: 8px;
background-color: white;
padding: 5px;
width: 100px;
}
[hidden] {
display: none;
}
paper-button {
background-color: lightblue;
}
#placeholder {
width: 120px;
height: 150px;
}
</style>
<div>Try me.</div>
<paper-button on-tap="_doTap">Push</paper-button>
<div id="placeholder" hidden></div>
</template>
<script>
class ParentComponent extends Polymer.Element {
static get is() { return 'parent-component'; }
static get properties() {
return {
mychild: {
type: Object
}
}
}
_doTap(e) {
let x = (e.detail.x - 50) + 'px';
let y = (e.detail.y - 50) + 'px';
this.mychild = new MyChild();
this.mychild.addEventListener('return-event', e => this._closeChild(e));
this.$.placeholder.style.position = 'absolute';
this.$.placeholder.appendChild(this.mychild);
this.$.placeholder.style.left = x;
this.$.placeholder.style.top = y;
this.$.placeholder.removeAttribute('hidden');
this.mychild.open();
}
_closeChild(e) {
console.log('Child says '+e.detail);
this.mychild.remove();
this.mychild = null;
this.$.placeholder.setAttribute('hidden', '');
}
}
customElements.define(ParentComponent.is, ParentComponent);
</script>
</dom-module>
<parent-component></parent-component>
<dom-module id="my-child">
<template>
<style>
:host {
display: block;
margin: 10px auto auto auto;
border: 2px solid gray;
border-radius: 8px;
background-color: white;
padding: 15px;
}
paper-button {
background-color: lightgray;
}
</style>
<div>I'm a child.</div>
<paper-button on-tap="_doTap">Close</paper-button>
</template>
<script>
class MyChild extends Polymer.mixinBehaviors([Polymer.IronOverlayBehavior], Polymer.Element) {
static get is() { return 'my-child'; }
static get properties() {
return {
withBackdrop: {
type: Boolean,
value: true
}
}
}
ready() {
super.ready();
console.log("Daddy?");
}
_doTap(e) {
this.dispatchEvent(new CustomEvent('return-event',
{ detail: 'Bye!', bubbles: true, composed: true }));
}
}
customElements.define(MyChild.is, MyChild);
</script>
</dom-module>

Appending a 'Blade' into a Container without 'popping-down' existing ones

I am implementing a 'blades' experience in a page. When I append an additional Blade into the Container...the previous blades 'pop' down.
Q: How do I append a new element into view without effecting previous elements?
MY FIDDLE:
I created a JSFiddle...but the service is not currently available...I will append it shortly.
https://jsfiddle.net/PrisonerZ3RO/oynae1hd/4/#
MY CSS:
<style>
/** DASHBOARD CONTAINER **/
.dashboard-container { border-right: solid 1px #000; margin-top: 5px; margin-bottom: 5px; overflow-x: scroll; white-space: nowrap; width: 100%; }
.dashboard-container .widget { clear: both; display: inline-block; vertical-align: top; }
/** FORM CONTAINER **/
.form-container { border: 1px solid #ccc; border-radius: 3px; height: 500px; margin-bottom: 5px; padding: 5px; width: 500px; }
/** BLADE CONTAINER **/
.blade-container .blade { border: 1px solid #ccc; border-radius: 3px; display: inline-block; height: 506px; margin-right: 2px; padding: 2px; width: 200px; }
</style>
MY HTML:
<script id="tmplBlade" type="text/template">
<div class="blade">
Blade
</div>
</script>
<div class="dashboard-container">
<div class="widget">
<div class="form-container">
Form Controls go here
<input id="btnAppend" type="button" value="Append Blade" />
</div>
</div>
<div class="widget">
<div class="blade-container">
</div>
</div>
</div>
MY JAVASCRIPT:
<script type="text/javascript">
$(document).ready(function () {
function PageController()
{
var that = this,
dictionary = {
elements: { btnAppend: null, bladeContainer: null },
selectors: { btnAppend: '#btnAppend', bladeContainer: '.blade-container', tmplBlade: '#tmplBlade' }
};
var initialize = function () {
// Elements
dictionary.elements.btnAppend = $(dictionary.selectors.btnAppend);
dictionary.elements.bladeContainer = $(dictionary.selectors.bladeContainer);
// Events
dictionary.elements.btnAppend.on('click', that.on.click.btnAppend);
};
this.on = {
click: {
btnAppend: function (e) {
var html = $(dictionary.selectors.tmplBlade).html().trim();
var $element = $(html);
$element.hide();
dictionary.elements.bladeContainer.prepend($element);
// Slide-in
$element.show('slide', { direction: 'left' });
}
}
};
initialize();
}
var pageController = new PageController();
});
</script>
I've come across this problem before. The only way I've found to get around it is to do the following:
1) Create a .hidden class width margin-left: -200px
2) Add a CSS transition on margin-left to the .blade class
3) Apply the .hidden class to a new blade
4) Show the new blade
5) Remove the .hidden class from the new blade
Please see the following fork of your fiddle for a working solution: https://jsfiddle.net/yxL4embt/
How do I append a new element into view without effecting previous elements?
I'm not sure if I entirely get what you're asking since you'll always be affecting the other elements by moving them over when you append a new element. You can, however, prevent the pop-down effect you're seeing. The .ui-effects-wrapperadded by jQuery UI is display: block, so add the following to your CSS:
.blade-container .ui-effects-wrapper {
display: inline-block !important;
}
Then make sure your other blades are always aligned to the top of your container:
.blade-container .blade {
...
...
vertical-align: top;
}
This will bump all the blades over (right) and allow a new blade to slide in from the left.

Categories