jQuery automatically group non-specific matching items? - javascript

Is there a jQuery or vanilla JS function that will group items of matching text content, without me prompting class names, elements, etc?
More specifically for this project, I need to group modules together, and I do not know how many will eventually be made. I can name them all similarly, say "Module 1 DOC, Module 1 XLSX, Module 1 PPT, Module 2 DOC, Module 2 XLSX, Module 2 PPT," etc, so it could be something like:
$("div#page").each(function(index) {
// check titles for generic matching content
if ($(this).find("h1:contains('*[words-that-match]')").length > 0) {
}
});
or [same-title] something like that? I'm not sure what the syntax would look like.
I apologize that my JS/JQ knowledge is so lacking, I am pretty new to this. Thanks in advance!

If you can, I recommend separating your grouping logic from the display, that way you can easily change the label without impacting the logic.
For example instead of only having a name make it into an object that looked something like:
{
displayName: "Module 1 PPT",
fileType: "PPT"
}
Then you could use the JS Array.reduce function to group objects together.
var groupBy = function(xs, key) {
return xs.reduce(function(rv, x) {
(rv[x[key]] = rv[x[key]] || []).push(x);
return rv;
}, {});
};
const moduleFiles = [
{
displayName: "Module 1 PTT",
fileType: "PTT"
},{
displayName: "Module 1 DOC",
fileType: "DOC"
},{
displayName: "Module 2 PTT",
fileType: "PTT"
},{
displayName: "Module 2 DOC",
fileType: "DOC"
}
]
console.log(groupBy(moduleFiles, 'fileType'));
The above code was copied from: Another SO answer

EDIT: The solution I used ended up using relied on a using a .nextAll().first() element search and targeting with a fake class. Disclosure: I'm working front-end in an LMS without access to any stylesheets or PHP.
$(document).ready(function(index) {
var URL = "li.modtype_url"
var BOOK = "li.modtype_book"
var H5P = "li.modtype_h5pactivity"
// select only items titled module...
if ($(this).find("p.instancename:contains('module')").length > 0) {
//-- CHANGE THE URL MODTYPE TITLE
$(URL).each(function(index) {
// variables
let oldTitle = $(this).find(".snap-asset-link")
let newTitle = $(this).find(".snap-asset-content .contentafterlink a.tile-title")
// save the title, remove it, and add to new location
oldTitle.remove();
newTitle.append(oldTitle);
/// hide or remove additional unwanted asset styling
$(this).find(".snap-assettype").css("height", "0px");
$(this).find(".snap-asset-content .no-overflow").removeClass("no-overflow");
});
//-- MOVE ICONS
$(this).each(function () {
var oldIcon = $(".snap-header-card .snap-header-card-icons .snap-asset-completion-tracking img")
var newIcon = ".snap-asset-content .contentafterlink div.tile-completion"
// first of set
$(this).each(function () {
// find the first of each, add empty class to mark
let oldIcon_URL = $(URL).first().find(oldIcon).addClass("moved")
let oldIcon_BOOK = $(BOOK).first().addClass("moved").find(oldIcon)
let oldIcon_H5P = $(H5P).first().addClass("moved").find(oldIcon)
let newIconLocation = $(URL).first().addClass("moved").find(newIcon)
// relocated all moved-targeted items to new location
newIconLocation.append(oldIcon_URL).append(oldIcon_H5P).append(oldIcon_BOOK);
});
//next of each modtype, repeated
setInterval(function() {
// find the first non-targeted of each, add empty class to mark
let oldIcon_URL = $("li.modtype_url:not(.moved)").first().find(oldIcon).addClass("moved")
let oldIcon_BOOK = $("li.modtype_book:not(.moved)").first().addClass("moved").find(oldIcon)
let oldIcon_H5P = $("li.modtype_h5pactivity:not(.moved)").first().addClass("moved").find(oldIcon)
let newIconLocation = $("li.modtype_url:not(.moved)").first().addClass("moved").find(newIcon)
// relocated all moved-targeted items to new location
newIconLocation.append(oldIcon_URL).append(oldIcon_H5P).append(oldIcon_BOOK);
});
});
};
});
This solution is a) majorly convoluted and b) while it is an automated process, is relies modules to cascade (ie 1, 2, 3 only) to group. Not ideal.

Related

Javascript - Best way to create array with unique elements from another array

I'll try my best to explain my problem. In my application I have a section that lists all users with a name and image. When adding a new user, the profile picture is taken from an array of default pictures and it has to be different from image of the other users.
Down below there is my solution and it seems to work, but im searching for a cleaner way to do it.
Thank you!
const profileImages = [img1, img2, img3, img4];
let users = [
{
name: "Username1",
image: img1
},
{
name: "Username2",
image: img2
}
];
/*
This array will fill up with the images not already taken by other users,
and I'll randomly pick from these to assign it to the new user
*/
let availableImages = [];
users.forEach(user =>{
if (availableImages.length === 0)
{
availableImages = profileImages.filter(image => image !== user.image);
}
else
{
availableImages = availableImages.filter(image => image !== user.image);
}
});
Use the Array.every() method to check if an image is not used by any user.
let availableImages = profileImages.filter(image => users.every(u => u.image != image));
something like this can work too, but this is a very simplistic approach. you may have some edge cases depending on how these images are assigned if coming from the server-side or something.
// assuming you want to keep a reference to the full list of images
const allImages = [...];
let availableImages = [...allImages];
function getImage() {
const [newImage, ...rest] = availableImages;
availableImages = rest;
return newImage;
}
// use getImage(); when you want to assign a new image
The following code gets you the remaining elements without a second loop. However, programmers must be cautioned that splice() is an expensive operation and it also alters the original array:
profileImages = ['img1', 'img2', 'img3', 'img4'];
users = [
{
name: "Username1",
image: 'img1'
},
{
name: "Username2",
image: 'img2'
}
];
users.map(item => {
let i = profileImages.indexOf(item.image); // get the index
profileImages.splice(i,1); // delete it
});
profileImages; // ['img3', 'img4']

Is it efficient to create multiple keys pointing to the same object?

I have an array of countries, where each country have three properties.
const countries = [
{ name: "Poland", alpha: "POL", code: "616" },
{ name: "Hungary", alpha: "HUN", code: "348" },
// and so on...
]
Later on I want to easily access each country by any of these properties.
I was thinking of reducing this array to an object that would have three keys for each country pointing to the very same country object.
Something like this:
const countriesObject = countries.reduce((object, country) => {
const { name, alpha, code } = country;
// Create individual country object
object[name] = { ...country };
// Reference just created country object
object[code] = object[name];
object[alpha] = object[name];
return object;
});
In the end I could access each country object either by its name, code or alpha.
countriesObject["Poland"] // →
countriesObject["POL"] // → The same object
countriesObject["616"] // →
My question is, would it be considered good practice, or there are some better ways to achieve the same or similar result?
Thank you!
That's fine, as all of those keys, as you correctly noted, will be pointing to the same object. The biggest problem that I see here is that it's easy to reduce readability of the code by using this approach. Let's say we have this fragment:
console.log( countriesObject[id] );
The question is, what is id here? Is it full country name? or just alpha? or code? You might just not care, true, but if you do, consider giving them additional structure:
const countriesObject = countries.reduce((object, country) => {
const { name, alpha, code } = country;
const countryCopy = { ...country };
// it makes sense to place them on the same line to show the intent
object.name[name] = object.code[code] = object.alpha[alpha] = countryCopy;
return object;
}, { name: {}, code: {}, alpha: {} });
Another potential issue is that you won't be able to drop the countries easily from this object; it's not enough to delete just a single key pointing to it, you'll have to go and remove all three. But that doesn't seem to be a big thing here; this looks more like a dictionary.
You can indeed write it like this:
var countries = {[
"poland": {
alpha: "POL", code: "616"
},
"hungary": {
alpha: "HUN", code: "348"
}
]}
Accessing each country like this:
var poland = countries.poland;
This, in turn, produces more readable code:
var alpha = countries.poland.alpha;
instead of
var alpha = countries[0].alpha;
But there is no set preference.
Docs

Kendo grid filter - how to use an "OR" filter

I've been tasked with creating an OR filter for a Kendo grid (which was implemented by another development team, who have since left). Basically, we have ID numbers. We can use the code (below) to filter, either to a single record or ones "containing" and so on (all the standard options).
However, what we want is a filter whereby the user can paste in a string of ID numbers (so something like 123,456,789 etc - could be hundreds) and the filter would bring those up.
Currently our code for the filter is:
{ field: "id", title: "ID Number",
filterable: {
cell: {
template: function (args) {
args.element.kendoNumericTextBox({
format: "#",
decimals: 0,
spinners: false
});
}
}
}
},
Sorted it, using my own filter which is really complex and in a lot of different places. But, if anyone finds it useful...
Following is in my js file:
self.init = function () {
var FilterToUse = new String($("#MyFilter").val());
self.filterIDs = FilterToUse !== "" ? FilterToUse.split(" ").map(Number) : [];
}
self.filterMyFilter = function () {
return new RSVP.Promise(function (resolve, reject) {
var promises = [];
promises.push(self.init());
RSVP.all(promises)
.then(function (posts) {
var selectedIdsToFilter = self.filterIDs.join(",");
$("#MyGrid").data("kendoGrid").dataSource.read({ SelectedIds: selectedIdsToFilter });
});
});
};
With self.filterIDs being defined earlier:
self.filterIDs = [];
And MyGrid being my grid, and MyFilter being an input box where I can just paste in my IDs (123 456 789 etc).
Then we use the module, and make sure we add this to the code that creates that grid:
string selectedIds = Request.Form["SelectedIds"]; // SelectedIds comes from paste IDs
List<long> selectedIdList = string.IsNullOrWhiteSpace(selectedIds) == false ? selectedIds.Split(',').Select(long.Parse).ToList() : null;
And, so we can filter on it...
if (selectedIdList != null)
{
where = where.And(w => selectedIdList.Contains(w.Id));
}

JSON iteration based on value using jquery

I'm trying to create a simple display of NBA west leaders in order by seed using the following json file:
http://data.nba.com/data/v2014/json/mobile_teams/nba/2014/00_standings.json
Right now I have the following:
$(document).ready(function() {
$.getJSON('http://data.nba.com/data/v2014/json/mobile_teams/nba/2014/00_standings.json',function(info){
var eastHead = info.sta.co[0].val;
var divi = info.sta.co[0].di[0].val;
/*evaluate East*/
for(i=0;i < divi.length;i++){
var visTeam ='<li>' + divi + '</li>';
document.getElementById("eastHead").innerHTML=eastHead;
}
var seed = info.sta.co[0].di[0].t[0].see;
$.each(menuItems.data, function (i) {
var eastSeed ='<li>' + seed + '</li>';
console.log(eastSeed)
document.getElementById("eastSeed").innerHTML=eastSeed;
});//$.each(menuItems.data, function (i) {
});//getJSON
});//ready
I'm looking just to list out the leaders in order. So right now we have
Golden State 2. Memphis 3. Houston 4. Portland 5. L.A. Clippers 6. Dallas .... and so
forth.
This is based off of the "see" value which means seed in the west.
This issue is I'm getting a single value rather than an iteration.
Updated:
$(document).ready(function() {
$.getJSON('http://data.nba.com/data/v2014/json/mobile_teams/nba/2014/00_standings.json',function(info){
/**************************************************/
//Get info above here
var westDivision = info.sta.co[1].di;
westDivision.forEach(function (subdivision)
{
subdivision.t.forEach(function (team)
{
westTeams.push({
city: team.tc,
name: team.tn,
seed: team.see
});
});
});
function compare(a,b) {
if (a.see < b.see)
return -1;
if (a.see > b.see)
return 1;
return 0;
}
var sorted = westTeams.sort(compare);
sorted.forEach(function (el,i)
{
console.log(i+'. '+el.city+' '+el.name);
});
/**************************************************/
});//getJSON
});//ready
console output :
Portland Trail Blazers
Oklahoma City Thunder
Denver Nuggets
Utah Jazz
Minnesota Timberwolves
Golden State Warriors
Los Angeles Clippers
Phoenix Suns
Sacramento Kings
Los Angeles Lakers
Memphis Grizzlies
Houston Rockets
Dallas Mavericks
San Antonio Spurs
New Orleans Pelicans
I like to iterate with forEach. Rather then having to worry about indexes you can directly reference each item of the array.
Using this code you can put the data you want into an array.
//Get info above here
var westTeams = [];
var westDivision = info.sta.co[1].di;
westDivision.forEach(function (subdivision)
{
subdivision.t.forEach(function (team)
{
westTeams.push({
city: team.tc,
name: team.tn,
seed: team.see
});
});
});
Then you can sort them using obj.sort
function compare(a,b) {
if (a.seed < b.seed)
return -1;
if (a.seed > b.seed)
return 1;
return 0;
}
var sorted = westTeams.sort(compare);
Finally, you can print them in order.
sorted.forEach(function (el,i)
{
console.log((i+1)+'. '+el.city+' '+el.name);
});
Querying a large JavaScript object graph can be a tedious thing, especially if you want to have dynamic output. Implementing support for different filter criteria, sort orders, "top N" restrictions, paging can be difficult. And whatever you come up with tends to be inflexible.
To cover these cases you can (if you don't mind the learning curve) use linq.js (reference), a library that implements .NET's LINQ for JavaScript.
The following showcases what you can do with it. Long post, bear with me.
Preparation
Your NBA data object follows a parent-child hierarchy, but it misses a few essential things:
there are no parent references
the property that contains the children is called differently on every level (i.e. co, di, t)
In order to make the whole thing uniform (and therefore traversable), we first need to build a tree of nodes from it. A tree node would wrap objects from your input graph and would look like this:
{
obj: o, /* the original object, e.g. sta.co[1] */
parent: p, /* the parent tree node, e.g. the one that wraps sta */
children: [] /* array of tree nodes built from e.g. sta.co[1].di */
}
The building of this structure can be done recursively in one function:
function toNode(obj) {
var node = {
obj: obj,
parent: this === window ? null : this,
// we're interested in certain child arrays, either of:
children: obj.co || obj.di || obj.t || []
};
// recursive step (with reference to the parent node)
node.children = node.children.map(toNode, node);
// (*) explanation below
node.parents = Enumerable.Return(node.parent)
.CascadeDepthFirst("$ ? [$.parent] : []").TakeExceptLast(1);
return node;
}
(*) The node.parents property is a convenience facility. It contains an enumeration of all parent nodes except the last one (i.e. the root node, which is null). This enumeration can be used for filtering as shown below.
The result of this function is a nice-and-uniform interlinked tree of nodes. (Expand the code snippet, but unfortunately it currently does not run due to same-origin browser restrictions. Maybe there is something in the NBA REST API that needs to be turned on first.)
function toNode(obj) {
var node = {
obj: obj,
parent: this === window ? null : this,
children: obj.co || obj.di || obj.t || []
};
node.children = node.children.map(toNode, node);
node.parents = Enumerable.Return(node.parent)
.CascadeDepthFirst("$ ? [$.parent] : []").TakeExceptLast(1);
return node;
}
$(function () {
var standingsUrl = 'http://data.nba.com/data/v2014/json/mobile_teams/nba/2014/00_standings.json';
$.getJSON(standingsUrl, function(result) {
var sta = toNode(result.sta);
console.log(sta);
});
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Result
Now that we have a fully traversable tree of nodes, we can use LINQ queries to do complex things with only a few lines of code:
// first build our enumerable stats tree
var stats = Enumerable.Return(toNode(result.sta));
// then traverse into children; the ones with a tid are teams
var teams = stats.CascadeDepthFirst("$.children")
.Where("$.obj.tid");
OK, we have identified all teams, so we can...
// ...select all that have a parent with val 'West' and order them by 'see'
var westernTeams = teams.Where(function (node) {
return node.parents.Any("$.obj.val === 'West'");
})
.OrderByDescending("$.obj.see");
// ...insert the top 5 into our page as list items
westernTeams.Take(5).Do(function (node) {
$("<li></li>", {text: node.obj.tc + ' ' + node.obj.tn}).appendTo("#topFiveList");
});
// ...turn them as an array of names
var names = westernTeams.Select("$.obj.tc + ' ' + $.obj.tn").ToArray();
console.log(names);
Of course what I have done there in several steps could be done in one:
// request details for all Northwest and Southeast teams who have won more than one game (*)
var httpRequests = Enumerable.Return(toNode(result.sta))
.CascadeDepthFirst("$.children")
.Where("$.obj.tid")
.Where(function (node) {
var str = node.obj.str.split(" ");
return str[0] === "W" && str[1] > 1 &&
node.parents.Any("$.obj.val==='Northwest' || $.obj.val==='Southeast'");
})
.Select(function (node) {
return $.getJSON(detailsUrl, {tid: node.obj.tid});
})
.ToArray();
$.when.apply($, httpRequests).done(function () {
var results = [].slice.call(arguments);
// all detail requests have been fetched, do sth. with the results
});
(*) correct me if I'm wrong, I have no idea what the data in the JSON file actually means

underscore find method issue on array of objects

I have following array of objects
var ppl = [
{
name: "John",
content: "<p>description</p>"
},
{
name: "Mike",
content: "<p>Desc</p>"
},
{
name: "Steve",
content: "html"
},
{
name: "Michael",
content: "<p>description</p>"
}
];
What I am doing is to display above array. Then when user clicks on name return his content. Like following
$('a.ppl').on('click', function (e) {
e.preventDefault();
var text = $(this).text();
var content = _.find(ppl, function (desc) { if (desc.name === text) return desc.content; });
console.log(content);
});
What above code does is it finds the content of the person clicked however it returns the entire object of that person e.g. when John is clicked the his entire object {
name: "John",
content: "<p>description</p>"
} is returned by the _.find() function. I just need the content. How can I return content only?
If I were you I would simply do a loop:
var length = ppl.length;
var findcat = function(){
for (var a = 0; a < length; a++) { if(ppl[a].name==text){return ppl[a].content} };
}
var content = findcat();
rather than using underscore.js .
Or if you really want to use underscore.js, change it to this:
var content = _.find(ppl, function (desc) { if (desc.name === text) return desc; });
content = content.content;
and it will work.
Updates (regarding HTML strings in json):
It is okay to store them in json as these HTML strings will simply be considered as normal strings data (just don't forget to escape characters like quotation and forward slash). When real HTML elements are being created from these strings (using jquery functions like .html(string), append(string) ), the browser will need to render these new contents and it may cause a slow performance comparing to leaving all the page-rendering at the start for the browser, but the difference will be pretty subtle. So in terms of performance, it is always okay to have them in json. But in terms of security, you should be careful when there were HTML markup in your data because you are making XSS easier to be accomplished. (Here is a wikipedia article that provides more details on XSS, also known as Cross-site scripting.)
I don't think you need an array here. A simpler and more efficient way would be to use names as properties.
var ppl = {"John": "<p>description</p>", "Mike": "<p>Desc</p>" };
$('a.ppl').on('click', function (e) {
e.preventDefault();
var text = $(this).text();
console.log(ppl[text]);
});
This is the expected Behavior of find operator which returns whole found item ! , why dont use content.content
the _.find looks through each value in the list, returning the first one that passes a truth test, when you return desc.content, it is evalued to true, so the desc object is return. so you can't return inside the find. but you can just access the content as desc.content. here is jsfiddle code:
$('a.ppl').on('click', function (e) {
e.preventDefault();
var text = $(this).text();
var desc = _.find(ppl, function (desc) {
return desc.name === text;
});
console.log(desc.content);
});

Categories