How to detect if an element is outside of its container? - javascript

So I've created the the following codesandbox. I got a webapp that relies heavily on user input. For demonstration purposes I've kept it simple by displaying a bunch of authors on a a4 formatted page. The page and font-size both use vw unit to make it responsive.
As you can see in the codesandbox, the last few authors are forced off the page because it no longer fits inside the container. Ideally I'd like to detect the content that doesn't fit on the page anymore, and generate a second identical a4 page to display that particular content.
Currently in my webapp I've just added overflow: scroll; to the page div where all the content is placed in, so that it at least looks somewhat 'ok'. But it isn't a very good User Experience and I'd like to improve it.
I don't have a clue where to start so any help in the right direction would be very much appreciated.
Thanks in advance.
CSS
#app {
-webkit-font-smoothing: antialiased;
font: 12pt "Tahoma";
}
.book {
margin: 0;
padding: 0;
background-color: #FAFAFA;
font: 3vw "Tahoma";
}
* {
box-sizing: border-box;
-moz-box-sizing: border-box;
}
.page {
/* overflow: scroll; */
display: block;
width: calc(100 / 23 * 21vw);
height: calc(100 / 23 * 29.7vw);
margin: calc(100 / 23 * 1vw) auto;
border: 1px #D3D3D3 solid;
border-radius: 5px;
background: white;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
.subpage {
margin: calc(100 / 23 * 1vw);
width: calc(100 / 23 * 19vw);
height: calc(100 / 23 * 27.7vw);
line-height: 2;
border: 1px solid red;
outline: 0cm #FAFAFA solid;
}
.subpage-content {
height: 100%;
}
Javascript
export default {
name: "App",
data() {
return {
authors: [
{ id: 1, name: "Smith" },
{ id: 2, name: "Johnson" },
{ id: 3, name: "Williams" },
{ id: 4, name: "Jones" },
{ id: 5, name: "Brown" },
{ id: 6, name: "Davis" },
{ id: 7, name: "Miller" },
{ id: 8, name: "Wilson" },
{ id: 9, name: "Moore" },
{ id: 10, name: "Taylor" },
{ id: 11, name: "Anderson" },
{ id: 12, name: "Thomas" },
{ id: 13, name: "Jackson" },
{ id: 14, name: "White" },
{ id: 15, name: "Harris" },
{ id: 16, name: "Martin" },
{ id: 17, name: "Thomspson" },
{ id: 18, name: "Garcia" },
{ id: 19, name: "Martinez" },
{ id: 20, name: "Robinson" },
{ id: 21, name: "Clark" },
{ id: 22, name: "Rodeiquez" },
{ id: 23, name: "Lewis" },
{ id: 24, name: "Lee" }
]
};
}
};
HTML
<template>
<div id="app">
<div class="container-fluid">
<div class="book">
<div class="page">HEADER
<div class="subpage" id="editor-container">Authors:
<!-- <div class="subpage-content">The real content</div> -->
<div v-for="item in authors" :key="item.id">{{ item.name }}</div>
</div>
</div>
</div>
</div>
</div>
</template>

You can view a fork of your code sandbox here.
I changed the data structure (and template) to have a pages array in which each page has an authors array, instead of a single one. Initially, the first page holds all the authors.
data() {
return {
pages: [
{
authors: [
{ id: 1, name: "Smith" },
...
]
}
]
}
}
<div class="page" v-for="(page, pageIndex) in pages" :key="pageIndex">HEADER
<div class="subpage" id="editor-container">
<template v-if="pageIndex < 1">Authors:</template>
<!-- <div class="subpage-content">The real content</div> -->
<div v-for="item in page.authors" :key="item.id" class="author">{{ item.name }}</div>
</div>
</div>
I then created a method recalcPages that gets called when the component is mounted:
methods: {
recalcPages() {
let pageElements = this.$el.querySelectorAll(".page");
Array.from(pageElements).some((p, pi) => {
let authors = p.querySelectorAll(".author");
if (authors.length) {
return Array.from(authors).some((a, ai) => {
let offPage = a.offsetTop + a.offsetHeight > p.offsetHeight;
if (offPage) {
let currentAuthors = this.pages[pi].authors;
var p1 = currentAuthors.slice(0, ai);
var p2 = currentAuthors.slice(ai);
this.pages[pi].authors = p1;
this.pages.push({ authors: p2 });
}
return offPage;
});
}
return false;
});
}
},
It iterates the actual DOM nodes and uses offsetTop + offsetHeight to calculate whether an author is off the page or not. As soon as an element leaves the page, it and all following elements are split from the current page's authors and a second page is inserted.
You'll also need to call this.recalcPages() after updating the contents deleting all pages and set a new authors array on the first one to be split up automatically again, unless you're only adding to the last page. You could also try to use the updated hook to achieve this automatically, I haven't tried that.
Of course it's quite a heavy operation, as it renders the component just to trigger re-rendering again by modifying the data. But unless you don't know the exact height of every element, there's no way around it (at least none I'm aware of).
By the way (although your final data will probably look different, but just for the sake of completeness of this demonstration) I also wrapped your Authors: headline in <template v-if="pageIndex < 1">Authors:</template> in order to display it only on the first page.

Related

How to use javascript to achieve scroll loading, the mouse can automatically scroll to the bottom to display the newly loaded content?

I want to realize that when scrolling to the bottom of the window, more information can be loaded in, so far it has been done! But I have two problems: 1. I don't know how to make the scroll scroll to the bottom to see the newly loaded data when the data is loaded? 2. Since I will trigger a loading function when I scroll to the bottom, if it will automatically scroll to the bottom when the data is loaded, this will not trigger the loading function. What I want is to only load each time I scroll Enter four data. I have tried many ways and still don't know how to solve the above two problems I encountered, I hope to get your help, thank you very much.
let str = ""
let limit = 4;
let offset = 0;
let data = [{
id: 5,
title: "title5"
},
{
id: 6,
title: "title6"
},
{
id: 7,
title: "title7"
},
{
id: 8,
title: "title8"
},
{
id: 9,
title: "title9"
},
{
id: 10,
title: "title10"
},
{
id: 11,
title: "title11"
},
{
id: 12,
title: "title12"
},
{
id: 13,
title: "title13"
},
{
id: 14,
title: "title14"
},
{
id: 15,
title: "title15"
}
];
function addData() {
let newData = data.splice(0, limit);
if (newData.length === 0) return;
str = "";
newData.forEach(e => {
str += ` <li class="message">
<div class='id'>${e.id}</div>
<div>${e.title}</div>
</li>`
});
content.innerHTML += str;
}
let loading = false;
let content = document.querySelector('.content');
content.addEventListener('scroll', function() {
if (loading || data.length === 0) return;
if (content.scrollTop + content.clientHeight >= content.scrollHeight) {
loading = true;
document.querySelector('.loading').style.display = 'block';
setTimeout(() => {
addData();
document.querySelector('.loading').style.display = 'none';
loading = false;
}, 1000);
}
});
.content {
width: 600px;
height: 200px;
overflow: auto;
border: 1px solid;
}
.message {
display: flex;
padding: 30px;
border: 1px solid #ddd;
}
.id {
margin-right: 8px;
}
.loading {
display: none;
width: 60px;
height: 60px;
margin: 0 auto;
background: url('https://media.giphy.com/media/3o7bu3XilJ5BOiSGic/giphy.gif');
}
<ul class="content">
<li class="message">
<div class='id'>1</div>
<div>title1</div>
</li>
<li class="message">
<div class='id'>2</div>
<div>title2</div>
</li>
<li class="message">
<div class='id'>3</div>
<div>title3</div>
</li>
<li class="message">
<div class='id'>4</div>
<div>title4</div>
</li>
<div class="loading"></div>
</ul>

Using vue-virtual-scroller not showing

I'm trying out https://github.com/Akryum/vue-virtual-scroller but I cannot get it to work. What am I doing wrong?
I use pug / jade for server-side HTML rendering. I don't receive any error messages but nothing gets rendered...
Pug / Jade
#test
recyclescroller.scroller(:items='active_projects' :item-size='32' key-field='id' v-slot='{ item }')
.user
| {{ item.name }}
CSS
.user {
height: 32%;
padding: 0 12px;
display: flex;
align-items: center;
}
javascript
Vue.component('RecycleScroller', VueVirtualScroller.RecycleScroller)
test = new Vue({
el: '#test',
data: {
active_projects : [{name : 'test'}]
}
})
When you use Recyclescroller you should inside each item has id
Because the uniqueness of the item is done by using the id
Your list should look like this :
data () {
return {
active_projects: [
{
name: 'test', id: 1
},
{
name: 'test', id: 2
},
{
name: 'test', id: 3
},
{
name: 'test', id: 4
},
]
}
}
Doc - Important notes
If the items are objects, the scroller needs to be able to identify them. By default it will look for an id field on the items. This can be configured with the keyField prop if you are using another field name.

display 4 rows per items and other remained item go other row

<script>
let cats = [
{ id: 'OUtn3pvWmp1', name: 'Cat1' },
{ id: 'OUtn3pvWmp2', name: 'Cat2' },
{ id: 'OUtn3pvWmp3', name: 'Cat3' },
{ id: 'OUtn3pvWmp4', name: 'Cat4' },
{ id: 'OUtn3pvWmp5', name: 'Cat4' },
{ id: 'OUtn3pvWmp6', name: 'Cat6' },
{ id: 'OUtn3pvWmp7', name: 'Cat7' },
{ id: 'OUtn3pvWmp8', name: 'Cat8' },
{ id: 'OUtn3pvWmp9', name: 'Cat9' },
{ id: 'OUtn3pvWmp10', name: 'Cat10' },
{ id: 'OUtn3pvWmp11', name: 'Cat11' },
{ id: 'OUtn3pvWmp12', name: 'Cat12' },
//{ id: 'OUtn3pvWmp13', name: 'Cat13' },
];
</script>
<style>
.row {
display:grid;
grid-columns: 4;
}
.card{
background-color: gray !important;
color: white;
border-radius:10px;
border-color: #404040;
padding-left: 15px;
padding-right: 15px;
}
.card-group {
display: flex;
}
</style>
<h1>EXPECTED OUTPUT, BY LOOP OVER cats </h1>
{#each cats as cat, i}
{#if i % 4 === 0}
<div class="row">
<div class="card-group">
<div class="card">{cats[i].name}</div>
<div class="card">{cats[i + 1].name}</div>
<div class="card">{cats[i + 2].name}</div>
<div class="card">{cats[i + 3].name}</div>
</div>
</div>
{/if}
{/each}
I would like cat13 item display in the another row ,anyone please give some trick.
I am using sveltejs 3
My runnable script here
I would create another array out of cats array. the new array will include sub-arrays each array will include at most 4 elements.
then in the HTML, we iterate over the new array then iterate over the sub arrays
/* divide your elements into another array where each index is a sub array array of 4 elements */
let chunkit = (maxItems = 4) => {
/* array to store the sub arrays of 4 elements */
let chunks = [[]]
/* iterate over the cats */
for (let cat of cats) {
/* if the last array of 4 elements has the length of 4, we create a new sub-array */
if (chunks[chunks.length - 1].length == maxItems) {
chunks.push([])
}
/* add the current element to the last sub array */
chunks[chunks.length - 1].push(cat)
}
return chunks
}
then we iterate over the return value of the function chunkit
<div class="row">
{#each chunkit() as fourCats}
<div class="card-group">
{#each fourCats as cat}
<div class="card">{cat.name}</div>
{/each}
</div>
{/each}
</div>
you can pass a number as parameter to the function maxItems to set the number of elements in each sub category
here is an example repl

How to arrow toggle up and down by clicked index? Vue

How do I rotate just that arrow icon based on the clicked item?
new Vue({
el: "#app",
data() {
return {
isToggled: false,
items: [{
id: 1,
name: "Test1"
},
{
id: 2,
name: "Test2"
},
{
id: 3,
name: "Test3"
},
{
id: 4,
name: "Test4"
},
]
}
},
methods: {
arrowToggle() {
this.isToggled = !this.isToggled;
},
getItems() {
return this.items;
}
},
mounted() {
this.getItems();
}
});
i {
border: solid black;
border-width: 0 3px 3px 0;
display: inline-block;
padding: 3px;
}
.down {
transform: rotate(45deg);
}
.up {
transform: rotate(-155deg);
}
.accordion {
display: flex;
background: lightblue;
align-items: center;
width: 100%;
width: 1000px;
justify-content: space-between;
height: 30px;
padding: 0 20px;
}
.arrow {
transform: rotate(-135deg);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app" style="display: flex; justify-content: center; align-items: center;">
<div v-for="(item, index) in items" :key="index">
<div class="accordion" #click="arrowToggle()">
<p> {{ item.name }}</p>
<i :class="{ 'down': item.isToggled }" class="arrow"> </i>
</div>
</div>
</div>
Based on the clicked element do I want my arrow to rotate?
If i have 10 items and click on 2 items i want the icon to rotate there.
Failing to bind id to the clicked item and to bind that class to rotate the item
One thing is very important, I cannot set the isOpen parameter in my json ITEMS which is false which everyone recommends to me. I get it from a database and I don't have a condition for it.
You will have to toggle at individual item level. Note that I have used isToggled per item. Here is full code at: https://jsfiddle.net/kdj62myg/
Even if you get your items from DB, you can iterate through array and add a key named isToggled to each item.
HTML
<div id="app" style="display: flex; justify-content: center; align-items: center;">
<div v-for="(item, index) in items" :key="index">
<div class="accordion" #click="arrowToggle(item)">
<p> {{ item.name }}</p>
<i :class="{ 'down': item.isToggled, 'up': !item.isToggled }"> </i>
</div>
</div>
</div>
Vue
new Vue({
el: "#app",
data() {
return {
isToggled: false,
items: [{
id: 1,
name: "Test1",
isToggled: false
},
{
id: 2,
name: "Test2",
isToggled: false
},
{
id: 3,
name: "Test3",
isToggled: false
},
{
id: 4,
name: "Test4",
isToggled: false
},
]
}
},
methods: {
arrowToggle(item) {
return item.isToggled = !item.isToggled;
},
getItems() {
return this.items;
}
},
mounted() {
this.getItems();
}
});
You have to map your items and attach a custom data on it to solve your problem.
Items data should be like this
items: [{
id: 1,
name: "Test1",
isToggled: false
},
{
id: 2,
name: "Test2",
isToggled: false
},
{
id: 3,
name: "Test3",
isToggled: false
},
{
id: 4,
name: "Test4",
isToggled: false
},
]
and your toogle function should look like this.
arrowToggle(item) {
return item.isToggled = !item.isToggled;
},
Now, after you fetched the items from the server. You have to map it to attach a isToggled data on every item you have. like this.
getItems() {
axios.get('api/for/items')
.then(({data}) => {
this.items = data.map(item => ({
return {
name:item.name,
id:item.id,
isToggled:false
}
}))
});
}
The above arrowToggle function breaks vue reactivity (google vue reactivity for docs). According to the docs, changing an object property directly will break reactivity. To keep reactivity, the function should change to:
arrowToggle(item) {
this.$set(this.item, 'isToggled', item.isToggled = !item.isToggled)
return item.isToggled;
},

How to dynamically render a tree of data in svelte?

I have a tree of data that i'm using to represent a directory of the file system. It look something like this.
{
isRoot: true
path: "/path/to/dir"
fileName: "dir",
isDirectory: true,
parent: null,
children: [
{
isRoot: false
path: "/path/to/dir/file1.txt"
fileName: "file1.txt",
isDirectory: true,
parent: {...spread parent node here},
children: null
},
{
isRoot: false
path: "/path/to/dir/subdir"
fileName: "subdir",
isDirectory: true,
parent: {...spread parent node here},
children: [
{
isRoot: false
path: "/path/to/dir/subdir/file2.txt"
fileName: "file2.txt",
isDirectory: false,
parent: {...spread parent node here},
children: null
}
]
}
]
}
I want to take each node and turn it into this.
<Node branch={some parent from that tree} >
<Node branch={a child of that parent} />
<Node branch={another child of that parent>
<Node branch={a child of the new parent />
</Node>
</Node
I know that svelte allows you to loop through arrays and render content based on each item however there is no way of knowing what the tree looks like.
Is there any way to dynamically render this tree in svelte?
This unanswered question came up in a search two years after being asked, so I'll go ahead and answer it while I'm here. The comments on the question just needed to be filled out to become a proper answer.
As the comments to your question have already explained, you'll need to use the recursive <svelte:self> component. It represents the component that is being defined in that particular module. In other words, if you are defining a <Folder> component and want to have other folders inside it, you would refer to them as <svelte:self> inside the definition of <Folder>. This is because a module cannot import itself.
The Svelte Tutorial has a specific module on this exact scenario, so everyone who writes code in Svelte should go through the entire tutorial (it's not long) and learn the basics of Svelte. You would want to do something quite similar to the example.
Here is the example shown in the tutorial:
App.svelte
<script>
import Folder from './Folder.svelte';
let root = [
{
name: 'Important work stuff',
files: [
{ name: 'quarterly-results.xlsx' }
]
},
{
name: 'Animal GIFs',
files: [
{
name: 'Dogs',
files: [
{ name: 'treadmill.gif' },
{ name: 'rope-jumping.gif' }
]
},
{
name: 'Goats',
files: [
{ name: 'parkour.gif' },
{ name: 'rampage.gif' }
]
},
{ name: 'cat-roomba.gif' },
{ name: 'duck-shuffle.gif' },
{ name: 'monkey-on-a-pig.gif' }
]
},
{ name: 'TODO.md' }
];
</script>
<Folder name="Home" files={root} expanded/>
File.svelte
<script>
export let name;
$: type = name.slice(name.lastIndexOf('.') + 1);
</script>
<span style="background-image: url(/tutorial/icons/{type}.svg)">{name}</span>
<style>
span {
padding: 0 0 0 1.5em;
background: 0 0.1em no-repeat;
background-size: 1em 1em;
}
</style>
And finally, Folder.svelte:
<script>
import File from './File.svelte';
export let expanded = false;
export let name;
export let files;
function toggle() {
expanded = !expanded;
}
</script>
<span class:expanded on:click={toggle}>{name}</span>
{#if expanded}
<ul>
{#each files as file}
<li>
{#if file.files}
<svelte:self {...file}/>
{:else}
<File {...file}/>
{/if}
</li>
{/each}
</ul>
{/if}
<style>
span {
padding: 0 0 0 1.5em;
background: url(/tutorial/icons/folder.svg) 0 0.1em no-repeat;
background-size: 1em 1em;
font-weight: bold;
cursor: pointer;
}
.expanded {
background-image: url(/tutorial/icons/folder-open.svg);
}
ul {
padding: 0.2em 0 0 0.5em;
margin: 0 0 0 0.5em;
list-style: none;
border-left: 1px solid #eee;
}
li {
padding: 0.2em 0;
}
</style>

Categories