prevent nested for loops in javascript - javascript

this is my JSON file and i want to create HTML multi level menu without nested loops
[{"text":"Home","href":"http://home.com","icon":"fas fa-home","target":"_top","title":"My Home"},{"text":"Posts","href":"","icon":"fas fa-bell","target":"_self","title":"","children":[{"text":"Sports","href":"","icon":"empty","target":"_self","title":""},{"text":"IT1","href":"","icon":"empty","target":"_self","title":""},{"text":"Web","href":"","icon":"","target":"_self","title":""},{"text":"About","href":"","icon":"fas fa-chart-bar","target":"_self","title":""}]}]
i wrote this but i think its not the best way to do that
thanks for helping
function MenuToHTML(JSON) {
let html = "<ul>";
for (items in JSON) {
html += "<li>";
//console.log(JSON[items].text);
html += JSON[items].text;
if (JSON[items].hasOwnProperty("children")) {
var child = JSON[items].children;
html += "<ul>";
for (subItems in child) {
html += "<li>";
html += child[subItems].text;
if (child[subItems].hasOwnProperty("children")) {
html += "<ul>";
var child = child[subItems].children;
for (SubsubItems in child) {
html += "<li>";
html += child[SubsubItems].text;
html += "</li>";
}
html += "</ul>";
}
html += "</li>";
}
html += "</ul>";
}
html += "</li>";
}
html += "</ul>";
return html;
}

You could build real elements and use a recursion for nested children.
function getMenu(array) {
return array.reduce((ul, { text, href, icon, target, title, children }) => {
var li = document.createElement('li'),
a;
if (href) {
a = document.createElement('a');
a.href = href;
a.target = target;
a.appendChild(document.createTextNode(text));
a.title = title;
li.appendChild(a);
} else {
li.appendChild(document.createTextNode(text));
}
if (children) {
li.appendChild(getMenu(children));
}
ul.appendChild(li);
return ul;
}, document.createElement('ul'));
}
var data = [{ text: "Home", href: "http://home.com", icon: "fas fa-home", target: "_top", title: "My Home" }, { text: "Posts", href: "", icon: "fas fa-bell", target: "_self", title: "", children: [{ text: "Sports", href: "", icon: "empty", target: "_self", title: "" }, { text: "IT1", href: "", icon: "empty", target: "_self", title: "" }, { text: "Web", href: "", icon: "", target: "_self", title: "" }, { text: "About", href: "", icon: "fas fa-chart-bar", target: "_self", title: "" }] }];
document.body.appendChild(getMenu(data));

You donot need to create nested loops like that use recursion and for arrays use for for..of or forEach instead of for..in
let arr = [
{"text":"Home","href":"http://home.com","icon":"fas fa-home","target":"_top","title":"My Home"},
{"text":"Posts","href":"","icon":"fas fa-bell","target":"_self","title":"","children":
[{"text":"Sports","href":"","icon":"empty","target":"_self","title":""},
{"text":"IT1","href":"","icon":"empty","target":"_self","title":""},
{"text":"Web","href":"","icon":"","target":"_self","title":""},
{"text":"About","href":"","icon":"fas fa-chart-bar","target":"_self","title":""}]}
]
/*this function will convert an array to html list. So you can pass the children
array again to the same function and it will to convert children to html list*/
function MenuToHTML(JSON) {
let html = "<ul>";
JSON.forEach(item => {
html += `<li>${item.text}</li>`;
if(item.children){
html += MenuToHTML(item.children);
}
})
return html + '</ul>';
}
document.body.innerHTML = MenuToHTML(arr);

Related

populate HTML form elements from object by jQuery

My from elements are written in Object. I need to populate HTML form elements from that. Problem is that Object contains different input types, structure, custom rules etc. For example, there will be text input, image type input, select box, radio, checkbox etc. So, I don't understand looping over the object will be good idea (I started this, but couldn't complete by myself :( ). I could write the html tag element in html file too. But, I must take the value from that object. So, what's the best solution for it?
Sample Object:
var formObj = {
username: {
value: null,
type: 'text',
placeholder: 'Enter username'
},
password: {
value: null,
type: 'password',
placeholder: 'enter password'
},
country: {
value: null,
type: 'select',
defaultText: 'Choose here',
option: [
{
value: '1',
label: 'Australia'
},
{
value: '2',
label: 'USA'
},
{
value: '3',
label: 'UK'
}
]
},
gender: {
value: null,
type: 'select',
defaultText: null,
option: [
{
value: 'male',
label: 'Male'
},
{
value: 'female',
label: 'Female'
},
{
value: 'other',
label: 'Other'
}
]
}
}
jsfiddle demo
your jsfiddle demo revised
Added some comments to your demo. I'd also look into template strings. They'll make your life easier and code cleaner :) and the single responsibility principle for breaking your code into easier to manage/read pieces.
var html = ''; // <-- Initialize as empty string for `+=`
$.each(formObj, function(key, value) {
if (value.value === null) {
value.value = '';
}
// Add label
html += '<label for="' + key + '">' + key + '</label>';
// Add input
if (value.type === 'select') {
// Type is select
html += '<select class="form-control">' + generateOptionPlaceholder(value.defaultText) + generateOptionMarkup(value.option) + '</select>';
} else {
html += '<input name="' + key + '" type="' + value.type + '" value="' + value.value + '" placeholder="' + value.placeholder + '" class="form-control" />';
}
console.log(html);
});
You could use a strategy pattern for this sort of thing. For any variance, used a dictionary where the keys are based off of the variant, and the values are a function to call for that variant.
For example, if your object with form data had a structure like this:
var form = {
"field1": {
type: "text"
value: "foo",
attrs: {...}
},
...
}
You can use a strategy to handle different field types. Your strategy dictionary might start our like this:
var FIELD_STRATEGY = {
"input": function (name, value, attrs) {
// General purpose method for <input>
// Needs type included in attrs
"text": function (name, value, attrs) {
// Constructs an <input type="text">
attrs.type = "text";
return FIELD_STRATEGY.input(name, value, attrs);
},
"option": function (value, label, attrs) {
// Constructs an <option>
},
"select": function (name, options, attrs {
// Constructs a <select>
var opts = options.map(function(opt) {
return FIELD_STRATEGY.option(
null,
opt.value,
opt.label);
}).join("");
var attr_str = Object.keys(attrs).map(function(attr) {
var value = attrs[attr];
return name + '="' + value '"';
}).join(" ");
return '<select name="' + name + '" ' + attr_str + '>' +
opts + '</select>';
}
};
To use it, loop over the field names and invoke strategies based on type:
var fields = Object.keys(form).map(function (name) {
var conf = form[name] || {};
var strategy = FIELD_STRATEGY[conf.type];
var rendered = "";
if (strategy) {
rendered = strategy(name, conf.value, conf.attrs);
}
return rendered;
});
This will give you a final fields list containing rendered strings based on the strategy for each field type.

Custom number of buttons tinymce

How can I create custom buttons tinymce buttons using JQuery? I would need n "Menu item" buttons. "n" would be defined depending on the selected data before opening the tinymce editor.
My buttons:
editor.addButton('addButtons', {
type: 'menubutton',
text: 'My button',
icon: false,
menu: [
{
text: 'Menu item 1',
onclick: function() {
editor.insertContent(' <strong>item1</strong> ');
}
}, {
text: 'Menu item 2',
onclick: function() {
editor.insertContent(' <strong>item2</strong> ');
}
}, {
text: 'Menu item 3',
onclick: function() {
editor.insertContent(' <strong>item3</strong> ');
}
}
]
});
I can get "n" value from a input type hidden using JQuery $("#totalButtons").val(). If totalButtons is 4, I would need 4 item buttons. Does it make sense? Is it possible to do?
Thanks
Updated Code:
var n = $('#total').val();
var menuItems = [];
tinymce.init({
selector: '#mytextareaTenant',
content_css: 'https://fonts.googleapis.com/css?family=Aguafina+Script|Alex+Brush|Bilbo|Condiment|Great+Vibes|Herr+Von+Muellerhoff|Kristi|Meddon|Monsieur+La+Doulaise|Norican|Nothing+You+Could+Do|Parisienne|Permanent+Marker|Sacramento|Yellowtail',
theme: 'modern',
menubar: false,
plugins: [
"print"
],
setup: function (editor) {
editor.on('init', function (e) {
renderEditorTenant();
for (var i=1; i<=n; i++){
var msg = ' <strong>#item' + i + '#</strong> ';
var obj = {
text: 'Menu item ' + i,
onclick: function() {
editor.insertContent(msg);
}
}
menuItems.push(obj);
}
});
Supposing you have a hidden input like this:
<input type="hidden" id="foo" name="zyx" value="3" />
You can get the value of the input and generate an array with n elements:
var n = $('#foo').val();
var menuItems = [];
for (var i=0; i<n; i++){
var msg = ' <strong>item' + i + '</strong> ';
var obj = {
text: 'Menu item ' + i,
onclick: function() {
editor.insertContent(msg);
}
}
menuItems.push(obj);
}
Now, just pass this array to the function you're using to generate the editor:
editor.addButton('addButtons', {
type: 'menubutton',
text: 'My button',
icon: false,
menu: menuItems
});

Passing data into chart using HTML and JavaScript

The following code is used to display a chart. The problem is the "data" is hard coded in. How can I change this so that the chart displays values which are displayed using:
<div id="name"> </div>
<div id="testscore"></div>
The above 2 div's contain dynamic values. I want to display these values in the chart.
<script type="text/javascript">
window.onload = function () {
var chart = new CanvasJS.Chart("chartContainer",
{
// title:{
// text: "Olympic Medals of all Times (till 2012 Olympics)"
// },
animationEnabled: true,
legend: {
cursor:"pointer",
itemclick : function(e) {
if (typeof (e.dataSeries.visible) === "undefined" || e.dataSeries.visible) {
e.dataSeries.visible = false;
}
else {
e.dataSeries.visible = true;
}
chart.render();
}
},
axisY: {
title: "Time"
},
toolTip: {
shared: true,
content: function(e){
var str = '';
var total = 0 ;
var str3;
var str2 ;
for (var i = 0; i < e.entries.length; i++){
var str1 = "<span style= 'color:"+e.entries[i].dataSeries.color + "'> " + e.entries[i].dataSeries.name + "</span>: <strong>"+ e.entries[i].dataPoint.y + "</strong> <br/>" ;
total = e.entries[i].dataPoint.y + total;
str = str.concat(str1);
}
str2 = "<span style = 'color:DodgerBlue; '><strong>"+e.entries[0].dataPoint.label + "</strong></span><br/>";
str3 = "<span style = 'color:Tomato '>Total: </span><strong>" + total + "</strong><br/>";
return (str2.concat(str)).concat(str3);
}
},
data: [
{
type: "bar",
showInLegend: true,
name: "Black",
color: "#000000",
dataPoints: [
{ y: 0.18, label: "Name"},
{ y: 0.12, label: "Name 1"},
{ y: 0.59, label: "Name 2"},
{ y: 1.15, label: "Name 3"},
]
},
]
});
chart.render();
}
</script>
You can simply call
var data = document.getElementById('name').value;
var data2 = document.getElementById('testscore').value;
If it's a single value, it would work; otherwise, you need to do it this way:
var array = $('#your.id').data('stuff');
Here, "stuff" is nothing but
<div data-stuff="some data" ></div>
Or, create an array by:
var array = new Array();
and finally, store the data into the array.

AngularJS image carousel with filter

I have taken about 6 runs at this and while I can get many PARTS of it working, I can't seem to get everything functioning like I want it to. This is the latest attempt. This was originally written with just JavaScript but as an exercise I am trying to change it to AngularJS.
Template normal.html:
<section id="container" class="container panels-backface-invisible" >
<div id="carousel" class="panels-backface-invisible 3rows">
<figure ng-repeat="movie in movies.data | filterMultiple:{genres:genList,wildcard:wcSearch}" class="folder figure"
style="transform: {{carousel.rotateFn}}({{carousel.theta*$index}}deg) translateZ({{carousel.radius}}px);" >
</figure></div>
</section>
<div class="searchbox"><label>Wildcard Title Search</label><input type="text" style="width: 100px;" ng-model="wcSearch">
</div>
<div style="display: inline-block;
position: absolute;
right: 8%;
bottom: 1px;
width: 15%;"
multi-select
input-model="genreList"
button-label="name"
item-label="icon"
tick-property="ticked"
selection-mode="multiple"
helper-elements=""
on-open="msOpen( data )"
on-close="msClose( data )"
on-item-click="msClick( data )"
>
</div>
app.js:
angular.module('MovieCarousel', ['ui.router','multi-select'])
.config(function config($stateProvider, $urlRouterProvider){
$urlRouterProvider.otherwise('/');
$stateProvider.state("index",{
url:"",
controller:"MovieFlowCtrl",
templateUrl:"templates/normal.html",
resolve:{
movieData: function($http){
var promise = $http.get('moviedata.php').
success(function(data) {
var mData={};
mData.count = data.length*2;
mData.data = data;
return mData;
});
return promise;
}
}
})
$stateProvider.state("new",{
url:"/New",
controller:"MovieFlowCtrl",
templateUrl:"templates/New.html",
resolve:{
movieData: function($http){
var promise = $http.get('moviedata.php?type=N').
success(function(data) {
var mData={};
mData.count = data.length*2;
mData.data = data;
return mData;
});
return promise;
}
}
})
})
.controller('MovieFlowCtrl', function ($scope, $rootScope, movieData, $filter) {
$scope.carousel = {};
$scope.carousel.isHorizontal = true;
$scope.movies = movieData;
$scope.carousel.element = document.getElementById('carousel');
setTimeout( function(){
document.body.addClassName('ready');
}, 0);
$scope.clearSelection=function() {
$rootScope.genrefilter = [];
$('#selectable .ui-selected').removeClass('ui-selected');
}
$rootScope.genrefilter = [];
$scope.msClick = function(Data) {
console.log(Data);
var selectedind = $rootScope.genrefilter.indexOf(Data.name);
if(selectedind>=0){
$rootScope.genrefilter.splice(selectedind,1);
}
else{
$rootScope.genrefilter.push(Data.name);
}
$scope.carousel.filter();
// $filter('filterMultiple');
}
$rootScope.genreList = [
{ icon: "<img src=/img/Action.png />", name: "Action", ticked: false },
{ icon: "<img src=/img/Adventure.png>", name: "Adventure", ticked: false },
{ icon: "<img src=/img/Animation.png>", name: "Animation", ticked: false },
{ icon: "<img src=/img/Biography.png />", name: "Biography", ticked: false },
{ icon: "<img src=/img/Comedy.png />", name: "Comedy", ticked: false },
{ icon: "<img src=/img/Comic-Book.png />", name: "Comic-Book", ticked: false },
{ icon: "<img src=/img/Crime.png>", name: "Crime", ticked: false },
{ icon: "<img src=/img/Disaster.png />", name: "Disaster", ticked: false },
{ icon: "<img src=/img/Drama.png />", name: "Drama", ticked: false },
{ icon: "<img src=/img/Fantasy.png />", name: "Fantasy", ticked: false },
{ icon: "<img src=/img/History.png>", name: "History", ticked: false },
{ icon: "<img src=/img/Horror.png>", name: "Horror", ticked: false },
{ icon: "<img src=/img/Music.png />", name: "Music", ticked: false },
{ icon: "<img src=/img/Musical.png />", name: "Musical", ticked: false },
{ icon: "<img src=/img/Mystery.png />", name: "Mystery", ticked: false },
{ icon: "<img src=/img/Romance.png>", name: "Romance", ticked: false },
{ icon: "<img src=/img/Sci-Fi.png />", name: "Sci-Fi", ticked: false },
{ icon: "<img src=/img/Sport.png />", name: "Sport", ticked: false },
{ icon: "<img src=/img/Thriller.png />", name: "Thriller", ticked: false },
{ icon: "<img src=/img/War.png>", name: "War", ticked: false },
{ icon: "<img src=/img/Western.png />", name: "Western", ticked: false }
];
$scope.msOpen = function( data ) {
console.log("open");
draglock = 1;
}
$scope.msClose = function( data ) {
console.log("closed");
draglock = 0; }
$rootScope.modify = function(){
if(wheight>=930){
sz1=300;
sz2=2.4;
}
else if(wheight>700 && wheight<929) {
sz1=283;
sz2= 3;
}
else if(wheight>500 && wheight<699) {
sz1=221;
sz2= 3.8;
}
else if(wheight>400 && wheight<499) {
sz1=157;
sz2=5.2;
}
else{
sz1=125;
sz2=6.6;
}
console.log("Modify");
// sz1 = $(window).width()*0.19;
panelCount = $rootScope.panelCount;
$scope.carousel.sz1 = sz1;
$scope.carousel.panelSize = sz1;
// if(typeof $scope.carousel.element.children[0] != 'undefined'){$scope.carousel.panelSize = $scope.carousel.element.children[0].clientWidth;}
$scope.carousel.rotateFn = $scope.carousel.isHorizontal ? 'rotateY' : 'rotateX';
// console.log(panelCount);
$scope.carousel.theta = 359 / ((panelCount+mrows));
theta = $scope.carousel.theta;
rotateXY = $scope.carousel.rotateFn;
var sizeadj = 12;
if(mrows == 1){sizeadj = 0.8;}
if(mrows == 2){sizeadj = sz2;}
$scope.carousel.radius = ($scope.carousel.panelSize*(mrows+panelCount/mrows))/(2*Math.PI); //Math.round(( $scope.carousel.panelSize/sizeadj) / Math.tan( Math.PI / (($scope.carousel.panelCount+mrows)) ) );
radius = $scope.carousel.radius;
$scope.carousel.rotation = Math.round( $scope.carousel.rotation / $scope.carousel.theta ) * $scope.carousel.theta;
rotation = $scope.carousel.rotation;
$scope.carousel.rotation = -4;
$scope.carousel.element.style[ transformProp ] = 'translateZ(-' + radius + 'px) ' + rotateXY + '(' + rotation + 'deg)';
// if(typeof $scope.carousel.element.children[0] != 'undefined'){$scope.carousel.sz1 = $scope.carousel.element.children[0].clientWidth;}
}
$scope.doubleClick = function(movieName){
console.log("test");
jQuery.post("/processrequest.php",
{name: movieName },
function(response){
if(response.length>5){alert(response);}
});
}
$rootScope.toggleCarousel = function(toMod){
panelCount = $rootScope.panelCount;
console.log("toggleCarousel");
if(toMod){return;}
if(panelCount > 20){
$('#container').addClass('container');
$('figure').addClass('figure');
filtered = 0;
$rootScope.modify(panelCount);
}
else{
$('figure').attr('style', '');
$('#carousel').attr('style', 'transform: translate(0px,0px)');
$('#container').removeClass('container');
$('figure').removeClass('figure');
filtered = 1;
}
$scope.carousel.element.style[ transformProp ] = 'translateZ(-' + radius + 'px) ' + rotateXY + '(' + rotation + 'deg)';
}
})
.directive('myDraggable', ['$document', function($document) {
return function(scope, element, attr) {
var startX = 0, startY = 0, x = 0, y = 0;
element.css({
position: 'relative',
border: '1px solid red',
backgroundColor: 'lightgrey',
cursor: 'pointer'
});
element.on('mousedown', function(event) {
// Prevent default dragging of selected content
event.preventDefault();
startX = event.pageX - x;
startY = event.pageY - y;
$document.on('mousemove', mousemove);
$document.on('mouseup', mouseup);
});
function mousemove(event) {
y = event.pageY - startY;
x = event.pageX - startX;
element.css({
top: y + 'px',
left: x + 'px'
});
}
function mouseup() {
$document.off('mousemove', mousemove);
$document.off('mouseup', mouseup);
}
};
}])
.filter('filterMultiple',["$rootScope", '$filter', function ($rootScope,$filter) {
return function (items, keyObj) {
var wcSearch = keyObj.wildcard;
var genres = keyObj.genres;
var filterObj=[];
for(var i=0;i<items.length;i++){
var filtered = true;
if(typeof wcSearch === 'undefined'){}
else if(wcSearch.length > 0){
if(wcSearch.toUpperCase().substring(0,wcSearch.length) != items[i].title.toUpperCase().substring(0,wcSearch.length) && wcSearch.toUpperCase().substring(0,wcSearch.length) != items[i].sortTitle.toUpperCase().substring(0,wcSearch.length)){filtered = false;}
}
if(filtered == true){
for(var x=0;x<$rootScope.genrefilter.length;x++){
if(items[i].genres.indexOf($rootScope.genrefilter[x])<0){filtered = false;}
}
}
if(filtered == true){
filterObj.push(items[i]);
}
}
*******Disabled due to infinite loop
// var ticked=[];
// var genreObj={};
// var $tempGenre=[];
// $tempGenre.genreList = $rootScope.genreList;
// for (var i=0;i<$tempGenre.genreList.length;i++){
// ticked[$tempGenre.genreList[i].name]=$tempGenre.genreList[i].ticked;
// }
// $tempGenre.genreList=[];
// if(typeof filterObj !== 'undefined'){
// for (var i=0;i<filterObj.length;i++){
// for (var z=0;z<filterObj[i].genres.length;z++){
// if($tempGenre.genreList.map(function(e) { return e.name; }).indexOf(filterObj[i].genres[z])==-1)
// {
// if(filterObj[i].genres[z] !=""){
// genreObj = {icon: "<img src=/img/"+filterObj[i].genres[z]+".png />", name: filterObj[i].genres[z], ticked: ticked[filterObj[i].genres[z]] };
// $tempGenre.genreList.push(genreObj);
// }
// }
// }
// }
// }
// $tempGenre.genreList.sort();
var toModify=false;
if($rootScope.panelCount == filterObj.length){toModify=true;}
console.log($rootScope.panelCount+","+filterObj.length);
$rootScope.panelCount = filterObj.length;
$rootScope.toggleCarousel(toModify);
$rootScope.genreList = $tempGenre;
return filterObj;
}
}])
.filter('unique', function() {
return function(input, key) {
var unique = {};
var uniqueList = [];
for(var i = 0; i < input.length; i++){
if(typeof unique[input[i][key]] == "undefined"){
unique[input[i][key]] = "";
uniqueList.push(input[i]);
}
}
return uniqueList;
};
})
.directive('resize', function($window) {
return function (scope, element, attr) {
var w = angular.element($window);
scope.$watch(function () {
return {
'h': w.height(),
'w': w.width()
};
}, function (newValue, oldValue) {
scope.windowHeight = newValue.h;
scope.windowWidth = newValue.w;
scope.resizeWithOffset = function (offsetH) {
scope.$eval(attr.notifier);
return {
'height': (newValue.h - offsetH) + 'px'
};
};
}, true);
w.bind('resize', function () {
scope.$apply();
});
}
})
Obviously I'm very new to AngularJS and am likely doing everything wrong. I have searched for similar solutions to learn from but can't find anything that is doing quite what I'm working on. The main issue I am having at this point is if I include the code that limits genres to only those available in the sub-set of movie results it loops infinitely, and no matter which angle I take I can't seem to get it all to work together. Some guidance would be greatly appreciated, even if its just to recommend a better structure, I'm sure I'm putting too much into the controller among other fails.
Other unfinished portions:
- views for each type of content
- re-size elements on window size
- Drag and scroll behavior

Kendo TreeView Search with Highlight

I have a KendoTreeview with spriteclass. I want to highlight the nodes (root as well as child nodes) with my search term. I have implemented the search functionality. But the issue when i search it is highlighting the term in the nodes but missing the SpriteClass in the nodes after first search. Any idea ?
jsFiddle code
$('#search-term').on('keyup', function () {
$('span.k-in > span.highlight').each(function () {
$(this).parent().text($(this).parent().text());
});
// ignore if no search term
if ($.trim($(this).val()) == '') {
return;
}
var term = this.value.toUpperCase();
var tlen = term.length;
$('#treeview-sprites span.k-in').each(function (index) {
var text = $(this).text();
var html = '';
var q = 0;
while ((p = text.toUpperCase().indexOf(term, q)) >= 0) {
html += text.substring(q, p) + '<span class="highlight">' + text.substr(p, tlen) + '</span>';
q = p + tlen;
}
if (q > 0) {
html += text.substring(q);
$(this).html(html);
$(this).parentsUntil('.k-treeview').filter('.k-item').each(
function (index, element) {
$('#treeview-sprites').data('kendoTreeView').expand($(this));
$(this).data('search-term', term);
});
}
});
$("#treeview-sprites").kendoTreeView({
dataSource: [{
text: "My Documents",
expanded: true,
spriteCssClass: "rootfolder",
items: [{
text: "Kendo UI Project",
expanded: true,
spriteCssClass: "folder",
items: [{
text: "about.html",
spriteCssClass: "html"
}, {
text: "index.html",
spriteCssClass: "html"
}, {
text: "logo.png",
spriteCssClass: "image"
}]
}, {
text: "New Web Site",
expanded: true,
spriteCssClass: "folder",
items: [{
text: "mockup.jpg",
spriteCssClass: "image"
}, {
text: "Research.pdf",
spriteCssClass: "pdf"
}, ]
}, {
text: "Reports",
expanded: true,
spriteCssClass: "folder",
items: [{
text: "February.pdf",
spriteCssClass: "pdf"
}, {
text: "March.pdf",
spriteCssClass: "pdf"
}, {
text: "April.pdf",
spriteCssClass: "pdf"
}]
}]
}]
})
;
Kendo's tree view widget doesn't like it if you muck around in its HTML, so I suggest modifying the data source instead (this will require the encoded option for all items in the DS).
In the keyup handler, you reset the DS whenever you search to clear highlighting, then instead of replacing the element's HTML directly, you set the model's text property:
$('#search-term').on('keyup', function () {
var treeView = $("#treeview-sprites").getKendoTreeView();
treeView.dataSource.data(pristine);
// ignore if no search term
if ($.trim($(this).val()) == '') {
return;
}
var term = this.value.toUpperCase();
var tlen = term.length;
$('#treeview-sprites span.k-in').each(function (index) {
var text = $(this).text();
var html = '';
var q = 0;
while ((p = text.toUpperCase().indexOf(term, q)) >= 0) {
html += text.substring(q, p) + '<span class="highlight">' + text.substr(p, tlen) + '</span>';
q = p + tlen;
}
if (q > 0) {
html += text.substring(q);
var dataItem = treeView.dataItem($(this));
dataItem.set("text", html);
$(this).parentsUntil('.k-treeview').filter('.k-item').each(
function (index, element) {
$('#treeview-sprites').data('kendoTreeView').expand($(this));
$(this).data('search-term', term);
});
}
});
$('#treeview-sprites .k-item').each(function () {
if ($(this).data('search-term') != term) {
$('#treeview-sprites').data('kendoTreeView').collapse($(this));
}
});
});
The tree definition needs the encoded option for this to work:
var pristine = [{
encoded: false,
text: "Kendo UI Project",
expanded: true,
spriteCssClass: "folder",
items: [{
encoded: false,
text: "about.html",
spriteCssClass: "html"
}, {
encoded: false,
text: "index.html",
spriteCssClass: "html"
}, {
encoded: false,
text: "logo.png",
spriteCssClass: "image"
}]
}, {
encoded: false,
text: "New Web Site",
expanded: true,
spriteCssClass: "folder",
items: [{
encoded: false,
text: "mockup.jpg",
spriteCssClass: "image"
}, {
encoded: false,
text: "Research.pdf",
spriteCssClass: "pdf"
}, ]
}, {
encoded: false,
text: "Reports",
expanded: true,
spriteCssClass: "folder",
items: [{
encoded: false,
text: "February.pdf",
spriteCssClass: "pdf"
}, {
encoded: false,
text: "March.pdf",
spriteCssClass: "pdf"
}, {
encoded: false,
text: "April.pdf",
spriteCssClass: "pdf"
}]
}];
$("#treeview-sprites").kendoTreeView({
dataSource: [{
text: "My Documents",
expanded: true,
spriteCssClass: "rootfolder",
items: pristine
}]
});
(demo)
Good job guys, just what I neeeded!
Using your code I did a small tweak (actually added just two lines of jquery filtering), so that now when searching for a keyword, the treeview shows only the branches that contain highlighted texts. Easy peasy! :)
Other branches are hidden if they do not contain the higlighted text. Simple as that.
This means we now have a VisualStudio-like treeview search (see the Visual Studio Solution Explorer Search and Filter: http://goo.gl/qr7yVb).
Here's my code and demo on jsfiddle: http://jsfiddle.net/ComboFusion/d0qespaz/2/
HTML:
<input id="treeViewSearchInput"></input>
<ul id="treeview">
<li data-expanded="true">My Web Site
<ul>
<li data-expanded="true">images
<ul>
<li>logo.png</li>
<li>body-back.png</li>
<li>my-photo.jpg</li>
</ul>
</li>
<li data-expanded="true">resources
<ul>
<li data-expanded="true">pdf
<ul>
<li>brochure.pdf</li>
<li>prices.pdf</li>
</ul>
</li>
<li>zip</li>
</ul>
</li>
<li>about.html</li>
<li>contacts.html</li>
<li>index.html</li>
<li>portfolio.html</li>
</ul>
</li>
<li>Another Root</li>
</ul>
CSS
span.k-in > span.highlight {
background: #7EA700;
color: #ffffff;
border: 1px solid green;
padding: 1px;
}
JAVASCRIPT
function InitSearch(treeViewId, searchInputId) {
var tv = $(treeViewId).data('kendoTreeView');
$(searchInputId).on('keyup', function () {
$(treeViewId + ' li.k-item').show();
$('span.k-in > span.highlight').each(function () {
$(this).parent().text($(this).parent().text());
});
// ignore if no search term
if ($.trim($(this).val()) === '') {
return;
}
var term = this.value.toUpperCase();
var tlen = term.length;
$(treeViewId + ' span.k-in').each(function (index) {
var text = $(this).text();
var html = '';
var q = 0;
var p;
while ((p = text.toUpperCase().indexOf(term, q)) >= 0) {
html += text.substring(q, p) + '<span class="highlight">' + text.substr(p, tlen) + '</span>';
q = p + tlen;
}
if (q > 0) {
html += text.substring(q);
$(this).html(html);
$(this).parentsUntil('.k-treeview').filter('.k-item').each(function (index, element) {
tv.expand($(this));
$(this).data('SearchTerm', term);
});
}
});
$(treeViewId + ' li.k-item:not(:has(".highlight"))').hide();
$(treeViewId + ' li.k-item').expand(".k-item");
});
}
var $tv = $("#treeview").kendoTreeView();
InitSearch("#treeview", "#treeViewSearchInput");
Another tweak from me :)
What I did was change the highlight code in order to preserve anything else that might exist in the node html (such as sprite span for example).
I also implemented it as a TypeScript class wrapper around the TreeView.
If you don't want TypeScript stuff just copy the code out and it should work fine :)
export class SearchableTreeView {
TreeView: kendo.ui.TreeView;
emphasisClass: string;
constructor(treeView: kendo.ui.TreeView) {
this.TreeView = treeView;
this.emphasisClass = "bg-warning";
}
search(term: string): void {
var treeElement: JQuery = this.TreeView.element;
var tv: kendo.ui.TreeView = this.TreeView;
var emphClass = this.emphasisClass;
this.resetHighlights();
// ignore if no search term
if ($.trim(term) === '') { return; }
var term = term.toUpperCase();
var tlen = term.length;
$('span.k-in', treeElement).each(function (index) {
// find all corresponding nodes
var node = $(this);
var htmlContent = node.html();
var text = node.text();
var searchPosition = text.toUpperCase().indexOf(term);
if (searchPosition === -1) {
// continue
return true;
}
var generatedHtml = '<span class="highlight-container">' + text.substr(0, searchPosition) + '<span class="' + emphClass + '">' + text.substr(searchPosition, tlen) + '</span>' + text.substr(searchPosition + tlen) + '</span>';
htmlContent = htmlContent.replace(text, generatedHtml);
node.html(htmlContent);
node.parentsUntil('.k-treeview').filter('.k-item').each(
function (index, element) {
tv.expand($(this));
$(this).data('search-term', term);
}
);
});
$('.k-item', treeElement).each(function () {
if ($(this).data('search-term') != term) {
tv.collapse($(this));
}
});
}
resetHighlights(): void {
this.TreeView.element.find("span.k-in:has('." + this.emphasisClass + "')")
.each(function () {
var node = $(this);
var text = node.text();
$(".highlight-container", node).remove();
node.append(text);
});
}
}
$("#textBox").on("input", function () {
var query = this.value.toLowerCase();
var dataSource = $("#Treeview").data("kendoTreeView").dataSource;
filter(dataSource, query);
});
function filter(dataSource, query) {
var uidData = [];
var data = dataSource instanceof kendo.data.DataSource && dataSource.data();
for (var i = 0; i < data.length; i++) {
var item = data[i];
var text = item.text.toLowerCase();
var isChecked = item.checked;
var itemVisible =
query === true
|| query === ""
|| text.indexOf(query) >= 0;
uidData.push({ UID: item.uid, Visible: itemVisible });
}
if (query != "") {
$.each(uidData, function (index, datavalue) {
if (datavalue.Visible) {
$("li[data-uid='" + datavalue.UID + "']").addClass("highlight");
}
else {
$("li[data-uid='" + datavalue.UID + "']").removeClass("highlight");
}
});
}
else {
$.each(uidData, function (index, datavalue) {
$("li[data-uid='" + datavalue.UID + "']").removeClass("highlight");
});
}
}
CSS :
.highlight {
background:#0fa1ba;
color:white;
}
For Angular 2+ you need to create a pipe for this feature.
import { Pipe, PipeTransform } from '#angular/core';
import { DomSanitizer } from '#angular/platform-browser';
import { NGXLogger } from 'ngx-logger';
#Pipe({
name: 'highlight'
})
export class TypeaheadHighlight implements PipeTransform {
constructor(private readonly _sanitizer: DomSanitizer, private readonly logger: NGXLogger) { }
transform(matchItem: any, query: any): string {
let matchedItem: any;
if (matchItem) {
matchedItem = matchItem.toString();
}
if (this.containsHtml(matchedItem)) {
this.logger.warn('Unsafe use of typeahead please use ngSanitize');
}
matchedItem = query ? ('' + matchedItem).replace(new RegExp(this.escapeRegexp(query), 'gi'), '<strong>$&</strong>') : matchedItem; // Replaces the capture string with a the same string inside of a "strong" tag
if (!this._sanitizer) {
matchedItem = this._sanitizer.bypassSecurityTrustHtml(matchedItem);
}
return matchedItem;
}
escapeRegexp = (queryToEscape) => queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
containsHtml = (matchItem) => /<.*>/g.test(matchItem);
}
Use of this pipe in html template....
<input name="searchTerm" type="text" [(ngModel)]="searchTerm" (keyup)='onkeyup(searchTerm)'
/>
</div>
<div style="height: 70vh;overflow: auto">
<kendo-treeview style="margin-top: 50px" id="availableColumns" [nodes]="availableColumns"
textField="displayName" kendoTreeViewExpandable kendoTreeViewFlatDataBinding idField="id"
parentIdField="parentId">
<ng-template kendoTreeViewNodeTemplate let-dataItem>
<span [tooltip]="dataItem.columnDescription"
[innerHtml]="dataItem.displayName | highlight:searchTerm "></span>
</ng-template>
</kendo-treeview>
</div>

Categories