Animating/Referencing dynamic elements with Jquery - javascript

I have a number of dynamically created elements that I want to later animate. I know that dynamic elements can by referenced using:
$(document).on(event, selector, cb)
but I am not sure how to implement this with animate. Here is my code if it helps. "state.headings" and "state.text" reference the dynamic elements
var state = {
sm: () => {return $(window).width() < "576"},
blocks: [
$("#first-block"),
$("#second-block")
],
pairs: [
$("#first-pair"),
],
headings: [
$("#first-heading"),
$("#second-heading"),
],
text: [
$("#first-text"),
$("#second-text"),
],
watching: 0,
}
$(window).on("scroll", () => {
if (state.sm()) {
if (isInViewport(state.blocks[state.watching])) {
if (state.headings[state.watching].css("right") !== "0px") {
state.headings[state.watching].animate({
right: "0px"
})
state.text[state.watching].animate({
left: "0px"
}, () => {
if (state.watching < state.blocks.length -1){
state.watching++
}
})
}
}
} else {
//handle animations for larger devices
}
})

I was able to get around it by setting my selectors to regular strings and using
$(document.getElementById(selector)).animate()
but I am still interested to know if jQuery provides its own solution.

Related

Get a list from javascript object

I am a new beginner in javascript and I am trying to figure out how to get a list from this object components.
I only need to select js keys from mndatory
var components = {
mandatory: {
alert: {
js: ['./bootstrap/js/alert.js'],
css: ['./bootstrap/css/alert.css', './bootstrap/css/alert2.css'],
},
button: {
js: ['./bootstrap/js/button.js'],
css: ['./bootstrap/css/button.css'],
},
dropdown: {
js: ['./bootstrap/js/dropdown.js'],
css: ['./bootstrap/css/dropdown.css'],
},
},
optional: {
carousel: {
js: ['./bootstrap/js/carousel.js'],
css: ['./bootstrap/css/carousel.css'],
},
modal: {
js: ['./bootstrap/js/modal.js'],
css: ['./bootstrap/css/modal.css'],
},
},
};
So the result will be the selection of only the js keys:
[
'./bootstrap/js/alert.js',
'./bootstrap/js/button.js',
'./bootstrap/js/dropdown.js'
]
I really appreciate your help.
You can use a for-in loop:
for (const property in components.mandatory) {
console.log(property.js);
}
You can probably take it from here.
Documentation:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in
Something like the below code should help you. Please try to understand how the below code is working.
var components = {
mandatory: {
alert: {
js: ['./bootstrap/js/alert.js'],
css: [
'./bootstrap/css/alert.css',
'./bootstrap/css/alert2.css'
]
},
button: {
js: ['./bootstrap/js/button.js'],
css: ['./bootstrap/css/button.css']
},
dropdown: {
js: ['./bootstrap/js/dropdown.js'],
css: ['./bootstrap/css/dropdown.css']
}
},
optional: {
carousel: {
js: ['./bootstrap/js/carousel.js'],
css: ['./bootstrap/css/carousel.css']
},
modal: {
js: ['./bootstrap/js/modal.js'],
css: ['./bootstrap/css/modal.css']
}
}
}
var array = [components.mandatory.alert.js, components.mandatory.button.js, components.mandatory.dropdown.js].flat();
console.log(array)
You can iterate over the attributes and then add the js elements to a global list as follows:
$(document).ready(function() {
var components = {
mandatory: {
alert: {
js: ['./bootstrap/js/alert.js'],
css: [
'./bootstrap/css/alert.css',
'./bootstrap/css/alert2.css'
]
},
button: {
js: ['./bootstrap/js/button.js'],
css: ['./bootstrap/css/button.css']
},
dropdown: {
js: ['./bootstrap/js/dropdown.js'],
css: ['./bootstrap/css/dropdown.css']
}
},
optional: {
carousel: {
js: ['./bootstrap/js/carousel.js'],
css: ['./bootstrap/css/carousel.css']
},
modal: {
js: ['./bootstrap/js/modal.js'],
css: ['./bootstrap/css/modal.css']
}
}
};
let mandatory = components.mandatory;
let list = [];
for (var key in mandatory) {
if (mandatory.hasOwnProperty(key)) {
let current = mandatory[key]['js'];
if(current){
for(var i = 0; i < current.length; i++)
list.push(current[i]);
}
}
}
console.log(list)
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
Well, as others have answered without waiting for effort from the OP, let me throw my suggestion out there:
const extractJs = components =>
Object .values (components .mandatory) .flatMap (x => x .js)
const components = {mandatory: {alert: {js: ["./bootstrap/js/alert.js"], css: ["./bootstrap/css/alert.css", "./bootstrap/css/alert2.css"]}, button: {js: ["./bootstrap/js/button.js"], css: ["./bootstrap/css/button.css"]}, dropdown: {js: ["./bootstrap/js/dropdown.js"], css: ["./bootstrap/css/dropdown.css"]}}, optional: {carousel: {js: ["./bootstrap/js/carousel.js"], css: ["./bootstrap/css/carousel.css"]}, modal: {js: ["./bootstrap/js/modal.js"], css: ["./bootstrap/css/modal.css"]}}};
console .log (extractJs (components))
We first take the mandatory property, then use Object .values to extract the values of each of its properties. We flatMap over the resulting objects, combining their .js properties. The flatMap call will flatten the resulting arrays into one as it goes.
You might want to add some checking along the way. Is components actually an object?, Does it have an object mandatory property?, etc. I leave that to you.
var result = []
Object.keys(components.mandatory).forEach(x => {
components.mandatory[x].js.forEach(y => result.push(y));
})

VueJS / JS DOM Watch / Observer in a multi phase render scenario

Scenario:
I’m developing a Vue scroll component that wraps around a dynamic number of HTML sections and then dynamically builds out vertical page navigation allowing the user to scroll or jump to page locations onScroll.
Detail:
a. In my example my scroll component wraps 3 sections. All section id’s start with "js-page-section-{{index}}"
b. The objective is to get the list of section nodes (above) and then dynamically build out vertical page (nav) navigation based on the n number of nodes found in the query matching selector criteria. Therefore, three sections will result in three page section navigation items. All side navigation start with “js-side-nav-{{index}}>".
c. Once the side navigation is rendered I need to query all the navigation nodes in order to control classes, heights, display, opacity, etc. i.e document.querySelectorAll('*[id^="js-side-nav"]');
EDIT
Based on some research here are the options for my problem. Again my problem being 3 phase DOM state management i.e. STEP 1. Read all nodes equal to x, then STEP 2. Build Side Nav scroll based on n number of nodes in document, and then STEP 3. Read all nav nodes to sync with scroll of document nodes:
Create some sort of event system is $emit() && $on. In my opinion this gets messy very quickly and feels like a poor solution. I found myself quickly jumping to $root
Vuex. but that feels like an overkill
sync. Works but really that is for parent child property state management but that again requires $emit() && $on.
Promise. based service class. This seems like the right solution, but frankly it became a bit of pain managing multiple promises.
I attempted to use Vue $ref but frankly it seems better for managing state rather than multi stage DOM manipulation where a observer event approach is better.
The solution that seems to work is Vues $nextTick(). which seems to be similar to AngularJS $digest. In essence it is a . setTimeout(). type approach just pausing for next digest cycle. That said there is the scenario where the tick doesn’t sync the time requires so I built a throttle method. Below is the code update for what is worth.
The refactored watch with nextTick()
watch: {
'page.sections': {
handler(nodeList, oldNodeList){
if (this.isNodeList(nodeList) && _.size(nodeList) && this.sideNavActive) {
return this.$nextTick(this.sideNavInit);
}
},
deep: true
},
},
The REFACTORED Vue component
<template>
<div v-scroll="handleScroll">
<nav class="nav__wrapper" id="navbar-example">
<ul class="nav">
<li role="presentation"
:id="sideNavPrefix + '-' + (index + 1)"
v-for="(item, key,index) in page.sections">
<a :href="'#' + getAttribute(item,'id')">
<p class="nav__counter" v-text="('0' + (index + 1))"></p>
<h3 class="nav__title" v-text="getAttribute(item,'data-title')"></h3>
<p class="nav__body" v-text="getAttribute(item,'data-body')"></p>
</a>
</li>
</ul>
</nav>
<slot></slot>
</div>
</template>
<script>
import ScrollPageService from '../services/ScrollPageService.js';
const _S = "section", _N = "sidenavs";
export default {
name: "ScrollSection",
props: {
nodeId: {
type: String,
required: true
},
sideNavActive: {
type: Boolean,
default: true,
required: false
},
sideNavPrefix: {
type: String,
default: "js-side-nav",
required: false
},
sideNavClass: {
type: String,
default: "active",
required: false
},
sectionClass: {
type: String,
default: "inview",
required: false
}
},
directives: {
scroll: {
inserted: function (el, binding, vnode) {
let f = function(evt) {
if (binding.value(evt, el)) {
window.removeEventListener('scroll', f);
}
};
window.addEventListener('scroll', f);
}
},
},
data: function () {
return {
scrollService: {},
page: {
sections: {},
sidenavs: {}
}
}
},
methods: {
getAttribute: function(element, key) {
return element.getAttribute(key);
},
updateViewPort: function() {
if (this.scrollService.isInCurrent(window.scrollY)) return;
[this.page.sections, this.page.sidenavs] = this.scrollService.updateNodeList(window.scrollY);
},
handleScroll: function(evt, el) {
if ( !(this.isScrollInstance()) ) {
return this.$nextTick(this.inViewportInit);
}
this.updateViewPort();
},
getNodeList: function(key) {
this.page[key] = this.scrollService.getNodeList(key);
},
isScrollInstance: function() {
return this.scrollService instanceof ScrollPageService;
},
sideNavInit: function() {
if (this.isScrollInstance() && this.scrollService.navInit(this.sideNavPrefix, this.sideNavClass)) this.getNodeList(_N);
},
inViewportInit: function() {
if (!(this.isScrollInstance()) && ((this.scrollService = new ScrollPageService(this.nodeId, this.sectionClass)) instanceof ScrollPageService)) this.getNodeList(_S);
},
isNodeList: function(nodes) {
return NodeList.prototype.isPrototypeOf(nodes);
},
},
watch: {
'page.sections': {
handler(nodeList, oldNodeList){
if (this.isNodeList(nodeList) && _.size(nodeList) && this.sideNavActive) {
return this.$nextTick(this.sideNavInit);
}
},
deep: true
},
},
mounted() {
return this.$nextTick(this.inViewportInit);
},
}
</script>
END EDIT
ORIGINAL POST
Problem & Question:
PROBLEM:
The query of sections and render of navs work fine. However, querying the nav elements fails as the DOM has not completed the render. Therefore, I’m forced to use a setTimeout() function. Even if I use a watch I’m still forced to use timeout.
QUESTION:
Is there a promise or observer in Vue or JS I can use to check to see when the DOM has finished rendering the nav elements so that I can then read them? Example in AngularJS we might use $observe
HTML EXAMPLE
<html>
<head></head>
<body>
<scroll-section>
<div id="js-page-section-1"
data-title="One"
data-body="One Body">
</div>
<div id="js-page-section-2"
data-title="Two"
data-body="Two Body">
</div>
<div id="js-page-section-3"
data-title="Three"
data-body="THree Body">
</div>
</scroll-section>
</body>
</html>
Vue Compenent
<template>
<div v-scroll="handleScroll">
<nav class="nav__wrapper" id="navbar-example">
<ul class="nav">
<li role="presentation"
:id="[idOfSideNav(key)]"
v-for="(item, key,index) in page.sections.items">
<a :href="getId(item)">
<p class="nav__counter">{{key}}</p>
<h3 class="nav__title" v-text="item.getAttribute('data-title')"></h3>
<p class="nav__body" v-text="item.getAttribute('data-body')"></p>
</a>
</li>
</ul>
</nav>
<slot></slot>
</div>
</template>
<script>
export default {
name: "ScrollSection",
directives: {
scroll: {
inserted: function (el, binding, vnode) {
let f = function(evt) {
_.forEach(vnode.context.page.sections.items, function (elem,k) {
if (window.scrollY >= elem.offsetTop && window.scrollY <= (elem.offsetTop + elem.offsetHeight)) {
if (!vnode.context.page.sections.items[k].classList.contains("in-viewport") ) {
vnode.context.page.sections.items[k].classList.add("in-viewport");
}
if (!vnode.context.page.sidenavs.items[k].classList.contains("active") ) {
vnode.context.page.sidenavs.items[k].classList.add("active");
}
} else {
if (elem.classList.contains("in-viewport") ) {
elem.classList.remove("in-viewport");
}
vnode.context.page.sidenavs.items[k].classList.remove("active");
}
});
if (binding.value(evt, el)) {
window.removeEventListener('scroll', f);
}
};
window.addEventListener('scroll', f);
},
},
},
data: function () {
return {
page: {
sections: {},
sidenavs: {}
}
}
},
methods: {
handleScroll: function(evt, el) {
// Remove for brevity
},
idOfSideNav: function(key) {
return "js-side-nav-" + (key+1);
},
classOfSideNav: function(key) {
if (key==="0") {return "active"}
},
elementsOfSideNav:function() {
this.page.sidenavs = document.querySelectorAll('*[id^="js-side-nav"]');
},
elementsOfSections:function() {
this.page.sections = document.querySelectorAll('*[id^="page-section"]');
},
},
watch: {
'page.sections': function (val) {
if (_.has(val,'items') && _.size(val.items)) {
var self = this;
setTimeout(function(){
self.elementsOfSideNavs();
}, 300);
}
}
},
mounted() {
this.elementsOfSections();
},
}
</script>
I hope I can help you with what I'm going to post here. A friend of mine developed a function that we use in several places, and reading your question reminded me of it.
"Is there a promise or observer in Vue or JS I can use to check to see when the DOM has finished rendering the nav elements so that I can then read them?"
I thought about this function (source), here below. It takes a function (observe) and tries to satisfy it a number of times.
I believe you can use it at some point in component creation or page initialization; I admit that I didn't understand your scenario very well. However, some points of your question immediately made me think about this functionality. "...wait for something to happen and then make something else happen."
<> Credits to #Markkop the creator of that snippet/func =)
/**
* Waits for object existence using a function to retrieve its value.
*
* #param { function() : T } getValueFunction
* #param { number } [maxTries=10] - Number of tries before the error catch.
* #param { number } [timeInterval=200] - Time interval between the requests in milis.
* #returns { Promise.<T> } Promise of the checked value.
*/
export function waitForExistence(getValueFunction, maxTries = 10, timeInterval = 200) {
return new Promise((resolve, reject) => {
let tries = 0
const interval = setInterval(() => {
tries += 1
const value = getValueFunction()
if (value) {
clearInterval(interval)
return resolve(value)
}
if (tries >= maxTries) {
clearInterval(interval)
return reject(new Error(`Could not find any value using ${tries} tentatives`))
}
}, timeInterval)
})
}
Example
function getPotatoElement () {
return window.document.querySelector('#potato-scroller')
}
function hasPotatoElement () {
return Boolean(getPotatoElement())
}
// when something load
window.document.addEventListener('load', async () => {
// we try sometimes to check if our element exists
const has = await waitForExistence(hasPotatoElement)
if (has) {
// and if it exists, we do this
doThingThatNeedPotato()
}
// or you could use a promise chain
waitForExistence(hasPotatoElement)
.then(returnFromWaitedFunction => { /* hasPotatoElement */
if (has) {
doThingThatNeedPotato(getPotatoElement())
}
})
})

Ember component's template issue with jQuery sortable

I'm trying to create a simple Ember component that wraps jQuery UI Sortable plugin. Unfortunately I have an issue with component's template when sortable is cancelled and model is updated manually. It looks like the DOM does not reflect the state of the model. I'm not able to find why.
I've created JS Bin to present this issue. When you change item position, the first one in the group should be removed. Unfortunately it works randomly.
What's wrong with this code?
Here is your JS Bin sortable component:
App.MyListComponent = Ember.Component.extend({
tagName: 'ul',
didInsertElement() {
let opts = {};
opts.update = this.updateList.bind(this);
this.$().sortable(opts);
},
updateList() {
this.$().sortable('cancel');
Ember.run.next(() => {
this.get('content').removeAt(0);
});
}
});
And then this is your JS Bin updated with code from the ember-ui-sortable repo to the following:
App.MyListComponent = Ember.Component.extend({
tagName: 'ul',
uiOptions: [
'axis',
'containment',
'cursor',
'cursorAt',
'delay',
'disabled',
'distance',
'forceHelperSize',
'forcePlaceholderSize',
'grid',
'handle',
'helper',
'opacity',
'placeholder',
'revert',
'scroll',
'scrollSensitivity',
'scrollSpeed',
'tolerance',
'zIndex'
],
destroySortable: Ember.on('willDestroyElement', function() {
this.$().sortable('destroy');
}),
initSortable: Ember.on('didInsertElement', function () {
let opts = {};
['start', 'stop'].forEach((callback) => {
opts[callback] = Ember.run.bind(this, callback);
});
this.$().sortable(opts);
this.get('uiOptions').forEach((option) => {
this._bindSortableOption(option);
});
}),
contentObserver: Ember.observer('content.[]', function () {
Ember.run.scheduleOnce('afterRender', this, this._refreshSortable);
}),
move(oldIndex, newIndex) {
let content = this.get('content');
let mutate = this.getWithDefault('mutate', true);
let item = content.objectAt(oldIndex);
if (content && mutate) {
content.removeAt(oldIndex);
content.insertAt(newIndex, item);
}
if(!mutate){
this.attrs.moved(item, oldIndex, newIndex);
}
},
start(event, ui) {
ui.item.data('oldIndex', ui.item.index());
},
stop(event, ui) {
const oldIndex = ui.item.data('oldIndex');
const newIndex = ui.item.index();
this.move(oldIndex, newIndex);
},
_bindSortableOption: function(key) {
this.addObserver(key, this, this._optionDidChange);
if (key in this) {
this._optionDidChange(this, key);
}
this.on('willDestroyElement', this, function() {
this.removeObserver(key, this, this._optionDidChange);
});
},
_optionDidChange(sender, key) {
this.$().sortable('option', key, this.get(key));
},
_refreshSortable() {
if (this.isDestroying) { return; }
this.$().sortable('refresh');
}
});
As you'll see, there is quite a bit extra going on versus your original, so you can have a look at what you missed and hopefully this helps you.
It might be a good idea to install that component addon via ember-cli, but also have a look at competing solutions like ember-sortable and others first by using something like ember-observer.

move item control bar videojs

I'm using the video.js 4.12 library and I want replace control bar items. For example, move one of my custom buttons to the 2nd slot of the control bar.
How do I change the order of items on the taskbar? I had no luck on Google.
Videojs place good class on elements. By this way you can identify control bar's elements.
To handle the item's order I used Jquery :
var createPrevButton = function() {
var props = {
className: 'vjs-control player-prev-button', //We use this class in Jquery
innerHTML: '<div class="vjs-control-content"></div>',
role: 'button',
'aria-live': 'polite',
tabIndex: 0
};
return videojs.Component.prototype.createEl(null, props);
};
var myPlayer = me.player = videojs(me.idVideo, {
plugins : { chapters : {} },
children: {
controlBar: {
children: [
{
name: 'playToggle'
},
{
name: 'currentTimeDisplay'
},
{
name: 'timeDivider'
},
{
name: 'durationDisplay'
}
/*
...........
*/
]
}
}
});
$(".player-prev-button").insertAfter(".vjs-play-control");
$(".player-next-button").insertAfter(".player-prev-button");
After the instanciation of my player just handle item by Jquery.
I think it's better than use CSS.
But the best way should be by videojs's option or somethink like that

Get element that called qTip?

I would like to get the element that called the qtip popup. In the documentation here it lets you set the position. I want to set the position using a jquery selector like $(this).find('.icon'). The problem is that this isn't the element that called the qtip (I think it's window).
Does anyone know how I can get the handle that called it (like it would if I set target to false)?
Thanks.
In the qtip source code I found this:
if(config.position.target === false) config.position.target = $(this);
Here's the solution I came up with and it seems to work. There probably is a better way to do it if I modified the qtip script but I want to leave that alone.
$(".report-error").qtip(
{
content: 'test content',
position:
{
adjust:
{
screen: true
},
target: false, //it is changed in the 'beforeRender' part in api section. by leaving it as false here in the qtip it will set it as the position I need using $(this)
corner:
{
target: 'bottomMiddle',
tooltip: 'topRight'
}
},
show:
{
when:
{
event: 'click'
}
},
style:
{
name: 'cream',
tip:
{
corner: 'topRight'
},
padding: 0,
width: 400,
border:
{
radius: 5,
width: 0
}
},
hide:
{
when:
{
event: 'unfocus'
}
},
api:
{
beforeRender: function() { //get the position that qtip found with $(this) in it's script and change it using that as the start position
this.options.position.target = $(this.elements.target).find('.icon');
this.elements.target = this.options.position.target; //update this as well. I don't actually know what it's use is
}
}
});
It's working on the site now at http://wncba.co.uk/results/

Categories

Resources