Puppeteer - how to click a link with certain text - puppeteer

I want to click a link with certain text using Puppeteer.
<a class="text-major ev-pick-this-event"
href="/cgi-bin/ncommerce3/SEGetEventInfo?ticketCode=GS%3AAMTX%3AHUSKERS%3ASLP2%3A&linkID=BQFN80-AMTX&shopperContext=&pc=&caller=&appCode=&groupCode=SLP&cgc=&dataAccId=129&locale=en_US&siteId=ev_BQFN80-AMTX">
HUSKERS - SLP2 - Ranges
</a>

It can be done with XPath's contains() and text() methods, for example:
const [link] = await page.$x('//a[contains(text(), "certain text")]') // returns an array, as the first element will be used it can be destructured
await link.click()

let click = await page.evaluate(() => {
try {
const getText = e => (e ? e.innerText.trim() : '')
let links = document.querySelectorAll('nav a')
for (let i = 0, n = links.length; i < n; i++) {
if (getText(links[i]) === 'Home') {
links[i].click()
return getText(links[i])
}
}
} catch (e) {
return e.toString()
}
})
console.log({ click })

Related

Make own calibration dialog with comma delimiter decimal

Is there an event similar as "FINISHED_CALIBRATION" when the style for the "calibration panel" is set to display: block? I want to create a custom calibration dialog with comma separator. I want to open own calibration dialog instead of your dialog. I check two point for calibration with this code:
const calibrationEndpoints: HTMLCollection = document.getElementsByClassName('calibration-endpoint');
const countPoint = Array.from(calibrationEndpoints).filter(e => e.classList.contains('editable')).length;
if (countPoint === 2) {
this.openCalibrationDialog();
}
Please review this sample
this.showPanel = function() {
var self = this;
const _window = this.getWindow();
if (_calibrationPanel) {
_window.setTimeout(function () { _calibrationPanel.requestedSizeTextbox.focus();}, 0);
_calibrationPanel.setVisible(true);
_calibrationPanel.updatePanelPosition(_measurement.indicator.labelPosition, _measurement.indicator.p1, _measurement.indicator.p2, _measurement.indicator.calibrationLabel.clientHeight);
self.addWindowEventListener("keyup", function onKeyUp(e){
var key = e.key || String.fromCharCode(e.keyCode);
if (key == "Escape" && self.isActive()) {
self.hidePanel();
self.clearSize();
self.showAddCalibrationLabel();
self.removeWindowEventListener("keyup", onKeyUp);
}
});
}
else {
_viewer.dispatchEvent({ type: MeasureCommon.Events.OPEN_CALIBRATION_PANEL_EVENT, data: {size: _selectedSize, units: _selectedUnits } });
}
};

Rooms with Forge

My goal is to see the Revit rooms in the Forge viewer. The application is in .NET Core. I have tried implementing GenerateMasterViews.
The code I am using to achieve this is:
[Route("api/forge/modelderivative/jobs")]
public async Task<dynamic> TranslateObject([FromBody]TranslateObjectModel objModel)
{
dynamic oauth = await OAuthController.GetInternalAsync();
// prepare the payload
var advOutputPayload = new JobSvf2OutputPayloadAdvanced();
advOutputPayload.GenerateMasterViews = true;
List<JobPayloadItem> outputs = new List<JobPayloadItem>()
{
new JobPayloadItem(
JobPayloadItem.TypeEnum.Svf2,
new List<JobPayloadItem.ViewsEnum>()
{
JobPayloadItem.ViewsEnum._2d,
JobPayloadItem.ViewsEnum._3d
},
advOutputPayload
)
};
JobPayload job;
job = new JobPayload(new JobPayloadInput(objModel.objectName), new JobPayloadOutput(outputs));
// start the translation
DerivativesApi derivative = new DerivativesApi();
derivative.Configuration.AccessToken = oauth.access_token;
dynamic jobPosted = await derivative.TranslateAsync(job);
return jobPosted;
}
Autodesk.Viewing.Initializer(options, () => {
viewer = new Autodesk.Viewing.GuiViewer3D(document.getElementById('forgeViewer'));
viewer.start();
var documentId = 'urn:' + urn;
Autodesk.Viewing.Document.load(documentId, onDocumentLoadSuccess, onDocumentLoadFailure);
});
}
function onDocumentLoadSuccess(doc) {
var viewables = doc.getRoot().getDefaultGeometry();
viewer.loadDocumentNode(doc, viewables).then(i => {
// documented loaded, any action?
});
}
But I can't get it to work.
I have looked for information, but this url: https://forge.autodesk.com/en/docs/model-derivative/v2/tutorials/prep-roominfo4viewer/option2/ and this url:
https://forge.autodesk.com/en/docs/model-derivative/v2/tutorials/prep-roominfo4viewer/option1/ they don't work and I couldn't see how to do it.
To check if the object is in the room, we can do the following:
Get bounds for each room and object
getBoundingBox(dbId, model) {
const it = model.getInstanceTree();
const fragList = model.getFragmentList();
let bounds = new THREE.Box3();
it.enumNodeFragments(dbId, (fragId) => {
let box = new THREE.Box3();
fragList.getWorldBounds(fragId, box);
bounds.union(box);
}, true);
return bounds;
}
Iterate rooms and objects and use containsBox or containsPoint to check if their bounding box has intersection.
If you want to do an acute collision check, you can take advantage of the ThreeCSG.js to do geometry intersection. Here is a blog post demonstrating how to integrate ThreeCSG.js with Forge Viewer.
https://forge.autodesk.com/blog/boolean-operations-forge-viewer
Note. This process would reduce the viewer performance since JavaScript is running on a single thread on the Web Browser, so you may use some technologies like the web worker to do the complex calculations on a separate thread.
Update:
Here is a working sample extension demonstrating the above idea:
/////////////////////////////////////////////////////////////////////
// Copyright (c) Autodesk, Inc. All rights reserved
// Written by Forge Partner Development
//
// Permission to use, copy, modify, and distribute this software in
// object code form for any purpose and without fee is hereby granted,
// provided that the above copyright notice appears in all copies and
// that both that copyright notice and the limited warranty and
// restricted rights notice below appear in all supporting
// documentation.
//
// AUTODESK PROVIDES THIS PROGRAM "AS IS" AND WITH ALL FAULTS.
// AUTODESK SPECIFICALLY DISCLAIMS ANY IMPLIED WARRANTY OF
// MERCHANTABILITY OR FITNESS FOR A PARTICULAR USE. AUTODESK, INC.
// DOES NOT WARRANT THAT THE OPERATION OF THE PROGRAM WILL BE
// UNINTERRUPTED OR ERROR FREE.
/////////////////////////////////////////////////////////////////////
(function () {
const Utility = {
/**
* Rest an object
* #param {Object} obj An object to be reset.
* ref: https://stackoverflow.com/a/24090180
*/
resetObject: function (obj) {
for (let key in Object.getOwnPropertyNames(obj)) {
if (!obj.hasOwnProperty(key)) continue;
let val = obj[key];
switch (typeof val) {
case 'string':
obj[key] = ''; break;
case 'number':
obj[key] = 0; break;
case 'boolean':
obj[key] = false; break;
case 'object':
if (val === null) break;
if (val instanceof Array) {
while (obj[key].length > 0) {
obj[key].pop();
}
break;
}
val = {};
//Or recursively clear the sub-object
//resetObject(val);
break;
}
}
}
};
/**
* A Forge Viewer extension for loading and rendering Revit Grids by AEC Model Data
* #class
*/
class RoomLocatorExtension extends Autodesk.Viewing.Extension {
constructor(viewer, options) {
super(viewer, options);
this.roomCategoryName = options.roomCategoryName || 'Revit Rooms';//'Revit Habitaciones'
this.onContextMenu = this.onContextMenu.bind(this);
}
onContextMenu(menu, status) {
if (status.hasSelected) {
menu.push({
title: 'Find room',
target: async () => {
let selSet = this.viewer.getSelection();
this.viewer.clearSelection();
const roomDbIds = await this.locateElementByRoom(selSet[0]);
if (!roomDbIds || roomDbIds.length <= 0) return;
this.viewer.select(roomDbIds);
}
});
}
}
async getPropertiesAsync(dbId, model) {
return new Promise((resolve, reject) => {
model.getProperties2(
dbId,
(result) => resolve(result),
(error) => reject(error)
);
});
}
async getElementsByCategoryAsync(category) {
return new Promise((resolve, reject) => {
this.viewer.search(
category,
(dbIds) => resolve(dbIds),
(error) => reject(error),
['Category'],
{ searchHidden: true }
);
});
}
async getRoomDbIds() {
try {
const roomDbIds = await this.getElementsByCategoryAsync(this.roomCategoryName);
if (!roomDbIds || roomDbIds.length <= 0) {
throw new Error('No Rooms found in current model');
}
return roomDbIds;
} catch (ex) {
console.warn(`[RoomLocatorExtension]: ${ex}`);
throw new Error('No room found');
}
}
getBoundingBox(dbId, model) {
const it = model.getInstanceTree();
const fragList = model.getFragmentList();
let bounds = new THREE.Box3();
it.enumNodeFragments(dbId, (fragId) => {
let box = new THREE.Box3();
fragList.getWorldBounds(fragId, box);
bounds.union(box);
}, true);
return bounds;
}
getLeafFragIds(model, leafId) {
const instanceTree = model.getData().instanceTree;
const fragIds = [];
instanceTree.enumNodeFragments(leafId, function (fragId) {
fragIds.push(fragId);
});
return fragIds;
}
getComponentGeometryInfo(dbId, model) {
const viewer = this.viewer;
const viewerImpl = viewer.impl;
const fragIds = this.getLeafFragIds(model, dbId);
let matrixWorld = null;
const meshes = fragIds.map((fragId) => {
const renderProxy = viewerImpl.getRenderProxy(model, fragId);
const geometry = renderProxy.geometry;
const attributes = geometry.attributes;
const positions = geometry.vb ? geometry.vb : attributes.position.array;
const indices = attributes.index.array || geometry.ib;
const stride = geometry.vb ? geometry.vbstride : 3;
const offsets = geometry.offsets;
matrixWorld = matrixWorld || renderProxy.matrixWorld.elements;
return {
positions,
indices,
offsets,
stride
};
});
return {
matrixWorld,
meshes
};
}
getComponentGeometry(data, vertexArray) {
const offsets = [
{
count: data.indices.length,
index: 0,
start: 0
}
];
for (let oi = 0, ol = offsets.length; oi < ol; ++oi) {
let start = offsets[oi].start;
let count = offsets[oi].count;
let index = offsets[oi].index;
for (let i = start, il = start + count; i < il; i += 3) {
const a = index + data.indices[i];
const b = index + data.indices[i + 1];
const c = index + data.indices[i + 2];
const vA = new THREE.Vector3();
const vB = new THREE.Vector3();
const vC = new THREE.Vector3();
vA.fromArray(data.positions, a * data.stride);
vB.fromArray(data.positions, b * data.stride);
vC.fromArray(data.positions, c * data.stride);
vertexArray.push(vA);
vertexArray.push(vB);
vertexArray.push(vC);
}
}
}
buildComponentMesh(data) {
const vertexArray = [];
for (let idx = 0; idx < data.nbMeshes; ++idx) {
const meshData = {
positions: data['positions' + idx],
indices: data['indices' + idx],
stride: data['stride' + idx]
}
this.getComponentGeometry(meshData, vertexArray);
}
const geometry = new THREE.Geometry();
for (let i = 0; i < vertexArray.length; i += 3) {
geometry.vertices.push(vertexArray[i]);
geometry.vertices.push(vertexArray[i + 1]);
geometry.vertices.push(vertexArray[i + 2]);
const face = new THREE.Face3(i, i + 1, i + 2);
geometry.faces.push(face);
}
const matrixWorld = new THREE.Matrix4();
matrixWorld.fromArray(data.matrixWorld);
const mesh = new THREE.Mesh(geometry);
mesh.applyMatrix(matrixWorld);
mesh.boundingBox = data.boundingBox;
mesh.bsp = new ThreeBSP(mesh)
mesh.dbId = data.dbId;
return mesh;
}
buildCsgMesh(dbId, model) {
const geometry = this.getComponentGeometryInfo(dbId, model);
const data = {
boundingBox: this.getBoundingBox(dbId, model),
matrixWorld: geometry.matrixWorld,
nbMeshes: geometry.meshes.length,
dbId
};
geometry.meshes.forEach((mesh, idx) => {
data['positions' + idx] = mesh.positions;
data['indices' + idx] = mesh.indices;
data['stride' + idx] = mesh.stride;
});
return this.buildComponentMesh(data);
}
async buildBBoxes() {
try {
const model = this.viewer.model;
const roomBBoxes = {};
const roomDbIds = await this.getRoomDbIds();
for (let i = 0; i < roomDbIds.length; i++) {
let dbId = roomDbIds[i];
let bbox = await this.getBoundingBox(dbId, model);
roomBBoxes[dbId] = bbox;
}
this.cachedBBoxes['rooms'] = roomBBoxes;
} catch (ex) {
console.warn(`[RoomLocatorExtension]: ${ex}`);
throw new Error('Cannot build bounding boxes from rooms');
}
}
async locateElementByRoom(dbId) {
let bbox = await this.getBoundingBox(dbId, this.viewer.model);
const roomDbIds = Object.keys(this.cachedBBoxes['rooms']);
const roomBoxes = Object.values(this.cachedBBoxes['rooms']);
// Coarse Phase Collision
const coarseResult = [];
for (let i = 0; i < roomDbIds.length; i++) {
let roomDbId = roomDbIds[i];
let roomBox = roomBoxes[i];
if (roomBox.containsBox(bbox)) {
coarseResult.push(parseInt(roomDbId));
} else {
if (roomBox.containsPoint(bbox.min) || roomBox.containsPoint(bbox.max) || roomBox.containsPoint(bbox.center())) {
coarseResult.push(parseInt(roomDbId));
}
}
}
// Fine Phase Collision
const fineResult = [];
let elementCsgMesh = this.buildCsgMesh(dbId, this.viewer.model);
for (let i = 0; i < coarseResult.length; i++) {
let roomDbId = coarseResult[i];
let roomCsgMesh = this.buildCsgMesh(roomDbId, this.viewer.model);
let result = elementCsgMesh.bsp.intersect(roomCsgMesh.bsp);
if (result.tree.polygons.length <= 0) {
result = roomCsgMesh.bsp.intersect(elementCsgMesh.bsp);
// if (!this.viewer.overlays.hasScene('csg'))
// this.viewer.overlays.addScene('csg');
// else
// this.viewer.overlays.clearScene('csg');
// let mat = new THREE.MeshBasicMaterial({ color: 'red' })
// let mesh = result.toMesh(mat);
// this.viewer.overlays.addMesh(mesh, 'csg')
if (result.tree.polygons.length <= 0) continue;
}
fineResult.push(roomDbId);
}
return fineResult;
}
async load() {
await Autodesk.Viewing.Private.theResourceLoader.loadScript(
'https://cdn.jsdelivr.net/gh/Wilt/ThreeCSG#develop/ThreeCSG.js',
'ThreeBSP'
);
if (!window.ThreeBSP)
throw new Error('Cannot load ThreeCSG.js, please download a copy from https://github.com/Wilt/ThreeCSG/blob/develop/ThreeCSG.js')
await this.viewer.waitForLoadDone();
this.cachedBBoxes = {};
await this.buildBBoxes();
this.viewer.registerContextMenuCallback(
'RoomLocatorExtension',
this.onContextMenu
);
return true;
}
unload() {
Utility.resetObject(this.cachedBBoxes);
this.viewer.unregisterContextMenuCallback(
'RoomLocatorExtension',
this.onContextMenu
);
return true;
}
}
Autodesk.Viewing.theExtensionManager.registerExtension('RoomLocatorExtension', RoomLocatorExtension);
})();
Snapshots:

Please tell me how to appear markup near a specific 2D object in Forge Viewer

I am developing HTML + JS using the forge viewer of Autodesk-forge.
I am looking for a way to display text near a specific object in 2D on the viewer.
Please let me know if there is a fix for the code below, or any other way to do it.
What I tried and result
I practiced the contents of the following article
https://adndevblog.typepad.com/technology_perspective/2020/12/forge-viewer-markup-along-dbid.html
The markup could be displayed on the 3D model as expected.
However, it was not possible to set the markup position on the 2D model.
Code (excerpt)
Calling part
viewer.addEventListener(Autodesk.Viewing.OBJECT_TREE_CREATED_EVENT, () => {
createMarkUp(viewer, {
icons: [
{ dbId: 11146, label: 'コメントあり', css: 'iconWarning fas fa-exclamation-triangle fa-2x
faa-flash animated' },
],
onClick: (id) => {
viewer.select(id);
}
});
});
  Logic part
  
function createMarkUp(viewer, options) {
let _group = null;
let _button = null;
let _icons = options.icons || [];
let _frags = null;
viewer._enabled = true;
load();
showIcons(true);
function load() {
const updateIconsCallback = () => {
if (viewer._enabled) {
updateIcons();
}
};
viewer.addEventListener(Autodesk.Viewing.CAMERA_CHANGE_EVENT, updateIconsCallback);
viewer.addEventListener(Autodesk.Viewing.ISOLATE_EVENT, updateIconsCallback);
viewer.addEventListener(Autodesk.Viewing.HIDE_EVENT, updateIconsCallback);
viewer.addEventListener(Autodesk.Viewing.SHOW_EVENT, updateIconsCallback);
return true;
}
function showIcons(show) {
const $viewer = $('#' + viewer.clientContainer.id + ' div.adsk-viewing-viewer');
// remove previous...
$('#' + viewer.clientContainer.id + ' div.adsk-viewing-viewer label.markup').remove();
if (!show) return;
// do we have anything to show?
if (_icons === undefined || _icons === null) return;
// do we have access to the instance tree?
const tree = viewer.model.getInstanceTree();
if (tree === undefined) { console.log('Loading tree...'); return; }
const onClick = (e) => {
if (options.onClick)
options.onClick($(e.currentTarget).data('id'));
};
_frags = {}
for (var i = 0; i < _icons.length; i++) {
// we need to collect all the fragIds for a given dbId
const icon = _icons[i];
_frags['dbId' + icon.dbId] = []
// create the label for the dbId
const $label = $(`
<label class="markup update" data-id="${icon.dbId}">
<span class="${icon.css}"> ${icon.label || ''}</span>
</label>
`);
$label.css('display', viewer.isNodeVisible(icon.dbId) ? 'block' : 'none');
$label.on('click', onClick);
$viewer.append($label);
// now collect the fragIds
tree.enumNodeFragments(icon.dbId, function (fragId) {
_frags['dbId' + icon.dbId].push(fragId);
updateIcons(); // re-position of each fragId found
});
}
}
function getModifiedWorldBoundingBox(dbId) {
const fragList = viewer.model.getFragmentList();
const nodebBox = new THREE.Box3();
// for each fragId on the list, get the bounding box
for (const fragId of _frags['dbId' + dbId]) {
const fragbBox = new THREE.Box3();
fragList.getWorldBounds(fragId, fragbBox);
nodebBox.union(fragbBox); // create a unifed bounding box
}
return nodebBox
}
function updateIcons() {
for (const label of $('#' + viewer.clientContainer.id + ' div.adsk-viewing-viewer .update'))
{
const $label = $(label);
const id = $label.data('id');
// get the center of the dbId (based on its fragIds bounding boxes)
const pos = viewer.worldToClient(getModifiedWorldBoundingBox(id).center());
// position the label center to it
$label.css('left', Math.floor(pos.x - $label[0].offsetWidth / 2) + 'px');
$label.css('top', Math.floor(pos.y - $label[0].offsetHeight / 2) + 'px');
$label.css('display', viewer.isNodeVisible(id) ? 'block' : 'none');
}
}
 }
Constitution
  HTML+JS
Unfortunately, the code above cannot be applied to 2D drawings as those use a different internal data structure.
With that said, you should still be able to achieve what you want because the Viewer APIs provide some utilities for 2D designs as well. For example, based on this blog post, you can obtain the bounding box of a specific element in your 2D drawing like so:
function getBounds2D(viewer, model, dbId) {
const frags = model.getFragmentList();
let bounds = new THREE.Box3();
let boundsCallback = new Autodesk.Viewing.Private.BoundsCallback(bounds);
let fragIds = frags.fragments.dbId2fragId[dbId]; // Find all fragments including this object's primitives
if (!Array.isArray(fragIds)) {
fragIds = [fragIds];
}
for (const fragId of fragIds) {
// Get the actual mesh with all geometry data for given fragment
const mesh = frags.getVizmesh(fragId);
const vbr = new Autodesk.Viewing.Private.VertexBufferReader(mesh.geometry, viewer.impl.use2dInstancing);
vbr.enumGeomsForObject(dbId, boundsCallback); // Update bounds based on all primitives linked to our dbId
}
return bounds;
}
This will return a THREE.Box3 structure defining the extent of the 2D primitive. From there, you could grab the center of the bounding box, project it to screen coordinates, and use the screen coords to position your HTML overlay elements, for example:
const bounds = getBounds2D(viewer, viewer.model, someDbId);
const coords = viewer.worldToClient(bounds.center());
$label.css('left', Math.floor(coords.x + 'px');
$label.css('top', Math.floor(coords.y + 'px');

How to add simple click event with Vega-lite ObservableHQ?

When someone clicks on points on the data, I want call custom function to show some image or information.
I am using Vega-lite in ObservableHQ notebooks and couldn't find answer?
const chart = (type)=>{
return vl
.markCircle({size: 15, opacity: 0.9})
.autosize('fit')
.data(getData(type))
.encode(
vl.x().fieldQ('slice').title('Slice'),
vl.y().fieldQ('dice').title(type).scale({domain: [0, 1.0]}),
vl.color().field('algorithm').title('Algorithm'),
vl.tooltip(['slice', 'algorithm', 'dice'])
)
}
const types = ['DSC','SDSC_2mm']
const charts = []
types.map(type => {
charts.push(chart(type))
})
return vl.vconcat(vl.hconcat(charts)).render()
}
This is the code I have in notebook.
If you just care about click, you can do something like the first cell in this notebook: https://observablehq.com/#visnup/vega-lite-data-out
Specifically:
clicked = Generators.observe((notify) => {
const clicked = (event, {datum}) => notify(datum);
clickable.addEventListener("click", clicked);
return () => clickable.removeEventListener("click", clicked);
})
where clickable is the name of my chart from the other cell.
A better-than-clickable example would be to do the same thing for Vega-Lite selections. I've added that to the notebook too.
selected = Generators.observe((notify) => {
const signal = Object.keys(selectable.getState({ signals: (name) => name.match(/^sel\d+$/) }).signals)[0];
const selected = (selection, predicates) => {
const within = penguins.filter(d => {
for (const [key, [min, max]] of Object.entries(predicates))
if (isNaN(+d[key]) || d[key] < min || d[key] > max)
return false;
return true;
})
notify(within);
}
selectable.addSignalListener(signal, selected);
return () => selectable.removeEventListener(signal, selected);
})

API call in lazy-load function, limiting the api response

I've set up a project where Im limiting the API response to 5, and upon scrolling to the bottom o the page, I make a new API call to fetch the next 2 items in the API. But with the current code it only checks if the 5 items previously fetched exists in the cards state. Im quite unsure as to how to go about fetching the 2 next items in the API? Does anyone have any suggestions as to how to go about this? Thanks,
var app = new Vue({
el: '#app',
data: {
cards: []
},
methods: {
scroll(card) {
window.onscroll = () => {
let bottomOfWindow = document.documentElement.scrollTop +
window.innerHeight === document.documentElement.offsetHeight;
if(bottomOfWindow) {
const url =
'https://api.jsonbin.io/b/5cab36508b8d1301a25bd8fa/1/';
axios.get(url)
.then(response => {
for (var i = 0; i < this.cards.length; i++) {
console.log('Cards id: ', this.cards[i].id)
if(this.cards[i].id !==
response.data.results[i].id){
for (var x = 0; x < 2; x++) {
this.cards.push(response.data.results[x])
}
} else{
console.log('No more cards to load')
}
}
}
})
}
}
},
getAPI(){
const url = 'https://api.jsonbin.io/b/5cab36508b8d1301a25bd8fa/1/';
axios.get(url)
.then(response => {
for (var i = 0; i < 5; i++) {
this.cards.push(response.data.results[i]);
}
})
.catch(function (error) {
console.log(error);
})
.then(function () {
// always executed
});
console.log(this.cards)
}
},
mounted() {
this.scroll(this.card)
}
})
Changed the method in which you do the checking. Instead of doing it from cards length you loop through the results length and once you reach a card that doesnt exist you add them, keep track of the amount added and return after 2 or when there is none left. I changed the loop logic to so.
//Card with position i from results doesn't exist
if(this.cards[i] == undefined){
//add this card - maybe add more logic to check for duplicate incase of reordering of response
this.cards.push(response.data.results[i])
//track card added
cardsAdded++;
} else {
console.log('No more cards to load')
}
//check if cards added has reach limit of 2
if(cardsAdded == 2)
return;
See: https://jsfiddle.net/as4dm03q/