//Pickle tree component created by Kadir Barış Bozat class PickleTree { /** * * @param {object} obj as tree object */ constructor(obj) { //target div id this.target = obj.c_target; //building area this.area = ""; //available nodes list this.nodeList = {}; //row create callback this.rowCreateCallback = obj.rowCreateCallback; //draw callback this.drawCallback = obj.drawCallback; //switch callback this.switchCallback = obj.switchCallback; //drag callback this.dragCallback = obj.dragCallback; //drop callback this.dropCallback = obj.dropCallback; //order callback this.orderCallback = obj.orderCallback; //node removed callback this.nodeRemove = obj.nodeRemoveCallback; //tree json data this.data = obj.c_data; //build tree this.build(obj.c_config); //start events this.staticEvents(); } /** * this method will contains static events for tree */ staticEvents() { //close menu this.main_container.addEventListener("click", (e) => { let elm = e.target; //close all first document.querySelectorAll(".ptreemenuCont").forEach((menu) => { menu.outerHTML = ""; }); if (elm.classList.contains("menuIcon")) { //menu toggle event for node setTimeout(() => { this.getMenu(e.target, this.getNode(elm.id.split("node_")[1])); }, 10); } }); //drag - drop events if (this.config.drag) { this.invalid_area = { container: null, top: 0, left: 0, right: 0, bottom: 0, }; //drag start this.main_container.addEventListener("dragstart", async (e) => { //give border to container //container this.invalid_area.container = document.getElementById(this.target + "node_" + e.target.id.split("node_")[1]); this.invalid_area.top = this.invalid_area.container.getBoundingClientRect().top; this.invalid_area.left = this.invalid_area.container.getBoundingClientRect().left; this.invalid_area.right = this.invalid_area.left + this.invalid_area.container.offsetWidth; this.invalid_area.bottom = this.invalid_area.top + this.invalid_area.container.offsetHeight; setTimeout(() => { this.invalid_area.container.classList.add("valid"); this._lock(); }, 300); //drag callback if (this.dragCallback) { this.dragCallback(this.nodeList[parseInt(e.target.id.split("node_")[1])]); } /////// ***** /////// //draging //clone element when drag start const id = e.target.id.split("node_")[1]; this.clone = document.getElementById(this.target +'node_' + id).cloneNode(true); this.clone.style.position = 'absolute'; this.clone.style.zIndex = 1000; this.clone.querySelectorAll('div').forEach(el=>el.style.backgroundColor = 'grey'); this.clone.querySelectorAll('li').forEach(el=>el.style.border = 'unset !important'); this.clone.style.width = '50vh'; //this.clone.querySelector('ul').remove(); this.clone.querySelectorAll('.switch').forEach(el => el.remove()); const rul = document.createElement('ul'); rul.appendChild(this.clone); const rdiv = document.createElement('div'); rdiv.appendChild(rul); rdiv.classList.add('dragging-element'); this.clone = rdiv; this.main_container.appendChild(this.clone); /////// ***** /////// }); //draging this.main_container.addEventListener("drag", (e) => { const bounds = e.currentTarget.getBoundingClientRect(); /////// ***** /////// this.clone.style.position = "absolute"; this.clone.style.left = `${e.clientX - bounds.left + 30}px`; this.clone.style.top = `${e.clientY - bounds.top + 30 }px`; /////// ***** /////// }); //drag end this.main_container.addEventListener("dragend", async (e) => { this.clone.remove(); //console.log('drag end') //remove border to container this.invalid_area.container.classList.remove("invalid"); this.invalid_area.container.classList.remove("valid"); //make all elements pointer clean this._lock(false); //clear old targets this.clearDebris(); //get node const node = this.nodeList[parseInt(e.target.id.split("node_")[1])]; //check is valid node.old_parent = node.parent; if (!this.invalid_area.valid) { node.parent = { id: 0 }; } else { console.log('target' , this.drag_target); const drop = this.getNode(this.drag_target); if (this.drag_target === parseInt(e.target.id.split("node_")[1]) || this.drag_target === undefined || drop === undefined || drop.parent.value === node.value) { //this means it dragged to outside node.parent = { id: 0 }; } else { node.parent = drop; } } this.nodeList[node.value] = node.updateNode(); console.log(this.nodeList[node.value]); //drop callback if (this.dropCallback) { this.dropCallback(node); } }); //drag location this.main_container.addEventListener("dragenter", (e) => { //console.log('drag enter') this.clearDebris(); try { //check position is valid let target = { left: e.target.getBoundingClientRect().left, top: e.target.getBoundingClientRect().top, }; if (target.top > this.invalid_area.top && target.top < this.invalid_area.bottom && target.left > this.invalid_area.left && target.left < this.invalid_area.right) { this.invalid_area.valid = false; this.invalid_area.container.classList.add("invalid"); this.invalid_area.container.classList.remove("valid"); } else { this.invalid_area.valid = true; this.invalid_area.container.classList.remove("invalid"); this.invalid_area.container.classList.add("valid"); } if (e.target.classList) { if (e.target.classList.contains("drop_target")) { e.target.classList.add("drag_triggered"); //this is for updating node parent to current this.drag_target = parseInt(e.target.id.split("node_")[1]); } } } catch (e) { //console.log('dragging have exception..'); this.drag_target = undefined; } }); } } //#region Helper Methods /** * */ async destroy() { //remove all menus document.querySelectorAll(".ptreemenuCont").forEach((menu) => { menu.outerHTML = ""; }); //remove all items document.getElementById(this.target).innerHTML = ""; } /** * this method will lock elements when dragging */ async _lock(type = true) { const elms = document.querySelectorAll(".drop_target"); for (let i = 0; i < elms.length; i++) { if (type) { elms[i].classList.add("disabled"); } else { elms[i].classList.remove("disabled"); } } } /** * * @param {string} message for log messages */ log(message) { if (this.config.logMode) { console.log(message); } } /** * Building main details */ build(c_config) { //set default config this.config = { key: new Date().getTime(), //logs are open or close logMode: false, //switch mode switchMode: false, hasLink: false, //family mode //for child autoChild: true, //for parent autoParent: true, //fold icon foldedIcon: "fa fa-plus", //unfold icon unFoldedIcon: "fa fa-minus", //menu icon menuIcon: ["fa", "fa-list-ul"], //link icon linkIcon: "fa fa-list-ul", //start status is collapsed or not foldedStatus: false, //drag drag: false, //order order: false, // context menu position contextPos: 'after' }; //check config here!! for (let key in this.config) { if (c_config[key] !== undefined) { this.config[key] = c_config[key]; } } //check if key is exist somewhere in document if (document.getElementById(this.config.key + "_div_pickletree") !== null) { this.config.key = new Date().getTime() + 10; } //referance for some events this.main_container = document.getElementById(this.target); this.main_container.classList.add('ptree'); this.main_container.innerHTML = '
'; //console.log(this.main_container.getElementById(this.config.key+'_tree_picklemain')); this.area = document.getElementById(this.config.key + "_tree_picklemain"); this.log("tree build started.."); this.drawData(); } /** * * @param {integer} id node id for finding node */ getNode(id) { this.log("node returned.."); //return node return this.nodeList[id]; } /** * set child nodes for parent node * @param {object} node */ setChildNodes(node) { //update node parent for (let key in this.nodeList) { if (this.nodeList[key].id === node.parent.id) { this.nodeList[key].childs.push(node.id); //show icon for childs const ic = document.getElementById("i_" + this.nodeList[key].id); if (ic !== null) ic.style.display = ""; } } } /** * this method will return switched nodes */ getSelected() { let nodes = []; //get all checked nodes for (let key in this.nodeList) { if (this.nodeList[key].checkStatus) nodes.push(this.nodeList[key]); } return nodes; } /** * this method will reset switched nodes */ resetSelected(type = false) { //get all checked nodes for (let key in this.nodeList) { this.nodeList[key].checkStatus = type; this.checkNode(this.nodeList[key]); } return true; } //#endregion //#region drag - drop events helpers /** * this method will clean entered areas after drag events */ clearDebris() { //first clean all entered areas let elms = document.querySelectorAll(".drag_triggered"); for (let i = 0; i < elms.length; i++) { elms[i].classList.remove("drag_triggered"); } } //#endregion //#region Node Events /** * this method will order element * @param {event} e */ orderNode(e) { const isBefore = e.target.dataset.target == 1; const main = e.target.parentNode.parentNode.parentNode; const target = isBefore ? main.previousElementSibling : main.nextElementSibling; //get nodes if (target !== null) { //replace data const targetNode = this.getNode(target.id.split("_").at(-1)); const mainNode = this.getNode(main.id.split("_").at(-1)); const currentOrder = mainNode.order; const targetOrder = targetNode.order === mainNode.order ? (isBefore ? targetNode.order - 1 : targetNode.order + 1) : targetNode.order; //change order data targetNode.order = currentOrder; mainNode.order = targetOrder; target.dataset.order = 'order_'+currentOrder; main.dataset.order = 'order_'+targetOrder; //replace element main.parentNode.replaceChild(main, target); main.parentNode.insertBefore(target, isBefore ? main.nextSibling : main); } if (typeof this.orderCallback == "function") this.orderCallback(main, target); } /** * get child nodes list of node * @param {object} node */ getChilds(node) { let list = []; for (let key in this.nodeList) { if (node.childs.includes(this.nodeList[key].id)) { list.push(this.nodeList[key]); } } this.log("node childs returned.."); return list; } /** * toggle open or close node childs * @param {object} node */ toggleNode(node) { if (node.childs.length > 0) { let ie = document.getElementById("i_" + node.id); let ule = document.getElementById("c_" + node.id); if (node.foldedStatus === false) { //change icon ie.classList.remove("fa-minus"); ie.classList.add("fa-plus"); //hide element //ule.style.display = "none"; ule.classList.remove("active"); ule.classList.add("not-active"); } else { //change icon ie.classList.remove("fa-plus"); ie.classList.add("fa-minus"); //show element //ule.style.display = ""; ule.classList.remove("not-active"); ule.classList.add("active"); } node.foldedStatus = !node.foldedStatus; //change node status for (let key in this.nodeList) { if (this.nodeList[key].id === node.id) { this.nodeList[key].foldedStatus = node.foldedStatus; } } this.log("node toggled.."); } else { this.log("node not has childs...!"); } } /** * remove node from dom * @param {object} node */ deleteNode(node) { //remove node from old parent's child data !!!! let elm = document.getElementById(node.id); let childs = node.getChilds(); if (childs.length > 0) { for (let i = 0; i < childs.length; i++) { this.deleteNode(childs[i]); } } //remove node from container //delete this.nodeList[node.value]; if (elm !== null) elm.parentNode.removeChild(elm); this.log("node removed..(" + node.id + ")"); if (this.nodeRemove !== undefined) this.nodeRemove(node); } /** * this method will check node and its family. * @param {object} node */ checkNode(node) { //console.log(node); //then if is checked and folded unfold and open childs let clength = node.childs.length; if (node.checkStatus && clength > 0) { //make element looks like is folded node.foldedStatus = true; this.toggleNode(node); } //trigger callback if exists if (typeof this.switchCallback == "function") this.switchCallback(node); //check html element if family mode is open document.getElementById("ck_" + node.id).checked = node.checkStatus; } /** * this method will check node childs and his parents if not checked. * @param {object} node */ checkNodeFamily(node) { let status = node.checkStatus; let parentCheck = async (node) => { //first check if has parent if (node.parent.id !== 0) { //get parent node node = node.parent; let trans = () => { //change parent node status node.checkStatus = status; //check parent node this.checkNode(node); //then restart process parentCheck(node); }; //decide for uncheck if (!status) { //if all childs is unchecked or child count is equal to 1 let valid = true; let childs = node.getChilds(); for (let i = 0; i < childs.length; i++) { if (childs[i].checkStatus) { valid = false; } } if (valid) trans(); } else { trans(); } } }; let childCheck = async (node) => { //first check main node this.checkNode(node); //then check childs if exist if (node.childs.length > 0) { //foreach child for (let i = 0; i < node.childs.length; i++) { let c_node = this.getNode(node.childs[i].split("node_")[1]); c_node.checkStatus = status; //restart process childCheck(c_node); } } }; if (this.config.autoChild) childCheck(node); if (this.config.autoParent) parentCheck(node); } /** * this method will unfold all parents of node * @param {object} node */ async showFamily(node) { //check if has parent if (node.parent.id !== 0) { //then make node status closed node.parent.foldedStatus = true; //after send parent node for toggle this.toggleNode(node.parent); //make recursive for another parents this.showFamily(node.parent); } } //#endregion //#region Node Creator /** * creating node * @param {object} obj */ createNode(obj) { const id = Date.now(); const node = { //node value value: id, //node id id: this.target + "node_" + id, //node title title: "untitled " + id, //node html elements elements: [], //order number order: null, //node parent element parent: { id: 0 }, // child element ids childs: [], //addional info addional: {}, //childs status (child list opened or not) foldedStatus: this.config.foldedStatus, //check status for node checkStatus: false, //this method will return child nodes getChilds: () => this.getChilds(node), //this method will remove node from dom deleteNode: () => this.deleteNode(node), //this method will update node updateNode: () => this.updateNode(node), //this method will toggle node toggleNode: () => this.toggleNode(node), //this method will show node location showFamily: () => this.showFamily(node), //check node toggleCheck: (status) => { node.checkStatus = status; this.checkNode(node); }, //scroll to node scroll:() => document.getElementById(node.id).scrollIntoView(), //find child nodes from text find :(text) => { const nodes = []; document.getElementById(node.id).querySelectorAll('li').forEach(el=>{ if(el.innerHTML.includes(text)){ nodes.push(this.getNode(el.id.split("node_")[1])) } }); return nodes; } }; //check setted values here!! for (let key in obj) { if (obj[key] !== undefined) node[key.split("_")[1]] = obj[key]; if (key === "n_id") node["id"] = this.target + "node_" + obj["n_id"]; } if (node.order === null) node.order = 0; //node is added to container this.nodeList[obj["n_id"]] = node; //node is drawed this.drawNode(node); //logged this.log("Node is created (" + node.id + ")"); //node is returned return node; } /** * this method will update node * !! id is recommended */ updateNode(node) { //first remove old node //console.log(this.getNode(node.id.split('_')[1])) this.getNode(node.id.split("node_")[1]).deleteNode(); //clear old parent's childs if old parent info is exist if (node.old_parent !== undefined && node.old_parent.id !== 0) { this.nodeList[node.old_parent.value].childs = this.nodeList[node.old_parent.value].childs.filter((x) => { return x !== node.id; }); //if child count is 0 then remove minus icon if (this.nodeList[node.old_parent.value].childs.length === 0) { document.getElementById("i_" + node.old_parent.id).style.display = "none"; } } //draw new node with childs const set = (data) => { this.drawNode(data); let childs = data.getChilds(); if (childs.length > 0) { for (let i = 0; i < childs.length; i++) { set(childs[i]); } } }; set(node); //log this.log("Node is created (" + node.id + ")"); //return node return node; } /** * * @param {object} node object for creating html element */ drawNode(node) { let icon = this.config.unFoldedIcon; let style = ""; let defaultClass = "active"; if (node.foldedStatus) { icon = this.config.foldedIcon; style = "none"; defaultClass = "not-active"; } //#region elements //node li item let li_item = document.createElement("li"); //node a item let a_item = document.createElement("a"); //node i item let i_item = document.createElement("i"); //node ul item let ul_item = document.createElement("ul"); //node group item let div_item = document.createElement("div"); //make node ordarable if (this.config.order) { const o_div = document.createElement("div"); o_div.id = "order_" + node.id; //create buttons const up_i = document.createElement("i"); const dw_i = document.createElement("i"); o_div.classList.add("ptree_order_div"); up_i.classList.add("fa", "fa-arrow-up"); up_i.dataset.target = "1"; dw_i.classList.add("fa", "fa-arrow-down"); dw_i.dataset.target = "0"; o_div.appendChild(up_i); o_div.appendChild(dw_i); //ordering event o_div.onclick = (e) => (e.target.tagName == "I" ? this.orderNode(e) : false); div_item.appendChild(o_div); } //make node dragable if (this.config.drag) { //add drag button to start const a_ditem = document.createElement("a"); const i_ditem = document.createElement("i"); //set icon drag button i_ditem.classList.add("fa"); i_ditem.classList.add("fa-bars"); a_ditem.classList.add("drag-handler"); a_ditem.id = "a_dr_" + node.id; a_ditem.appendChild(i_ditem); a_ditem.href = "javascript:;"; a_ditem.setAttribute("dragable", true); a_ditem.setAttribute("drag-title", node.title); //icon added to div div_item.appendChild(a_ditem); div_item.classList.add("drop_target"); } //set i item id i_item.id = "i_" + node.id; //set i item style i_item.style.color = "black"; //set i item icon icon = icon.split(" "); for (let i = 0; i < icon.length; i++) { i_item.classList.add(icon[i]); } i_item.style.display = "none"; //set ul item id ul_item.id = "c_" + node.id; //set ul item style //ul_item.style.display = style; //set ul item class ul_item.classList.add(defaultClass); //set a item id a_item.id = "a_toggle_" + node.id; //set i tag to a item a_item.appendChild(i_item); //set a item href a_item.href = "javascript:;"; //set a_item title a_item.innerHTML += " " + node.title; a_item.onclick = (e) => this.toggleNode(node); //set li item id li_item.id = node.id; li_item.dataset.order = "order_" + node.order; div_item.id = "div_g_" + node.id; //set a tag to div item div_item.appendChild(a_item); //set switch to li item if user is wanted if (this.config.switchMode) { const sw_item = document.createElement("label"); const ck_item = document.createElement("input"); const spn_item = document.createElement("span"); spn_item.classList.add("slider"); spn_item.classList.add("round"); ck_item.type = "checkbox"; sw_item.classList.add("switch"); sw_item.appendChild(ck_item); sw_item.appendChild(spn_item); //id definitions ck_item.id = "ck_" + node.id; sw_item.id = "sw_" + node.id; ck_item.value = node.value; //if item created as checked ck_item.checked = node.checkStatus; ck_item.onclick = (e) => { node.checkStatus = e.target.checked; if (this.config.autoChild || this.config.autoParent) { this.checkNodeFamily(node); }else{ this.checkNode(node); } }; //switch is added to li element div_item.appendChild(sw_item); } //if node has extra elements if (node.elements.length > 0) { //add menu button to end let a_item = document.createElement("a"); let i_item = document.createElement("i"); //set icon for menu for (let i = 0; i < this.config.menuIcon.length; i++) { i_item.classList.add(this.config.menuIcon[i]); } a_item.id = "a_me_" + node.id; a_item.appendChild(i_item); // a_item.href = "javascript:;"; a_item.classList.add("menuIcon"); //icon added to div div_item.appendChild(a_item); } li_item.appendChild(div_item); //set ul tag to li item li_item.appendChild(ul_item); //#endregion //if is main node //check if element is exist for preventing copy elements if (node.parent.id === 0) { //put item to area this.area.appendChild(li_item); } else { //if has parent set to parents childs this.setChildNodes(node); //then put item const cont = document.getElementById("c_" + node.parent.id); if (cont !== null) cont.appendChild(li_item); } //node.element = li_item; //set node events this.setNodeEvents(node, div_item); //draw callback method if (typeof this.rowCreateCallback == "function") this.rowCreateCallback(node); } setNodeEvents(node, parent) { //order event for node if (this.config.order) { document.getElementById('order_' + node.id).addEventListener('click', e => { if (e.target.tagName == 'I') { const isBefore = e.target.dataset.target == 1; const main = e.target.parentNode.parentNode.parentNode; const target = isBefore ? main.previousElementSibling : main.nextElementSibling; //get nodes if (target !== null) { //replace data const targetNode = this.getNode(target.id.split("_").at(-1)); const mainNode = this.getNode(main.id.split("_").at(-1)); //console.log( mainNode.order,targetNode.order); const currentOrder = mainNode.order; const targetOrder = targetNode.order === mainNode.order ? (isBefore ? targetNode.order - 1 : targetNode.order + 1) : targetNode.order; //change order data targetNode.order = currentOrder; mainNode.order = targetOrder; //console.log( mainNode.order,targetNode.order); //replace element main.parentNode.replaceChild(main, target); main.parentNode.insertBefore(target, isBefore ? main.nextSibling : main); } } }); } } /** * this method will draw multiple data */ drawData() { //start loading //if data is exist if (this.data.length > 0) { //first reshape data let order = (list, p = { n_id: 0, Child: [] }, tree = []) => { let childrens = list.filter((y) => y.n_parentid === p.n_id); if (childrens.length > 0) { // order items by order_num param if exist childrens.sort((a, b) => parseFloat(a.n_order_num === undefined ? 0 : a.n_order_num) - parseFloat(b.n_order_num === undefined ? 0 : b.n_order_num)); if (p.n_id === 0) { tree = childrens; } else { p.Child = childrens; } for (let i = 0; i < childrens.length; i++) { order(list, childrens[i]); } } return tree; }; //then create nodes let set = (list) => { for (let i = 0; i < list.length; i++) { this.createNode({ n_data: list[i], n_addional: list[i].n_addional, n_value: list[i].n_id, n_title: list[i].n_title, n_id: list[i].n_id, n_elements: list[i].n_elements, n_parent: this.getNode(list[i].n_parentid), n_checkStatus: list[i].n_checkStatus === undefined ? false : list[i].n_checkStatus, n_order: list[i].n_order_num, }); if (list[i].Child) { set(list[i].Child); } } }; //start chain set(order(this.data)); } //start drawcallback if (this.drawCallback !== undefined) this.drawCallback(); //end loading } //#endregion //#region Menu // brings up the context menu getMenu(element, node) { //get element location const rect = element.getBoundingClientRect(); const origin = { node: node, left: rect.x, top: rect.y + rect.height, }; //draw menu this.drawMenu(origin); } drawMenu(obj) { //check if menu already exist if (document.getElementById("div_menu_" + obj.node.id) === null) { //create menu div const menu_item = document.createElement("div"); //add to body document.body.appendChild(menu_item); menu_item.id = "div_menu_" + obj.node.id; menu_item.classList.add("ptreemenuCont"); //for each menu item let span_item; let icon; for (let i = 0; i < obj.node.elements.length; i++) { span_item = document.createElement("span"); span_item.setAttribute("data-node", obj.node.id); icon = obj.node.elements[i].icon.trim().length > 0 ? '' : ""; span_item.innerHTML = icon + " " + obj.node.elements[i].title.trim(); menu_item.appendChild(span_item); //then add click event span_item.addEventListener("click", (e) => { obj.node.elements[i].onClick(this.getNode(e.target.getAttribute("data-node").split("node_")[1])); //remove menu after click menu_item.outerHTML = ""; }); } switch (this.config.contextPos) { case 'above': menu_item.offsetPosition = -menu_item.offsetHeight; break; case 'after': menu_item.offsetPosition = menu_item.offsetWidth; break; case 'below': menu_item.offsetPosition = menu_item.offsetHeight; break; case 'before': menu_item.offsetPosition = -menu_item.offsetWidth; break; default: } //calculate location if (screen.width - obj.left < menu_item.offsetWidth) { menu_item.style.left = obj.left - menu_item.offsetWidth + "px"; } else { menu_item.style.left = obj.left + menu_item.offsetPosition + "px"; } menu_item.style.top = obj.top + "px"; // listen mouse out menu_item.onmouseleave = (e) => { menu_item.remove(); }; } } collapseAll() { for( let key in this.nodeList) { this.getNode(key).foldedStatus = true; this.updateNode(this.getNode(key)) } } uncheckAll() { this.getSelected().forEach(n => { n.checkStatus = false; this.checkNode(n) }); } //#endregion }