2018-05-14 07:26:04 -05:00
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
//
|
|
|
|
// pgAdmin 4 - PostgreSQL Tools
|
|
|
|
//
|
2020-01-02 08:43:50 -06:00
|
|
|
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
2018-05-14 07:26:04 -05:00
|
|
|
// This software is released under the PostgreSQL Licence
|
|
|
|
//
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
|
2018-07-25 01:40:46 -05:00
|
|
|
import {isValidData} from 'sources/utils';
|
2019-06-27 09:30:05 -05:00
|
|
|
import $ from 'jquery';
|
2019-11-25 21:34:41 -06:00
|
|
|
import Alertify from 'pgadmin.alertifyjs';
|
2018-07-25 01:40:46 -05:00
|
|
|
|
2018-05-14 07:26:04 -05:00
|
|
|
export class TreeNode {
|
|
|
|
constructor(id, data, domNode, parent) {
|
|
|
|
this.id = id;
|
|
|
|
this.data = data;
|
|
|
|
this.setParent(parent);
|
|
|
|
this.children = [];
|
|
|
|
this.domNode = domNode;
|
|
|
|
}
|
|
|
|
|
|
|
|
hasParent() {
|
|
|
|
return this.parentNode !== null && this.parentNode !== undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
parent() {
|
|
|
|
return this.parentNode;
|
|
|
|
}
|
|
|
|
|
|
|
|
setParent(parent) {
|
|
|
|
this.parentNode = parent;
|
|
|
|
this.path = this.id;
|
|
|
|
if (parent !== null && parent !== undefined && parent.path !== undefined) {
|
|
|
|
this.path = parent.path + '.' + this.id;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getData() {
|
|
|
|
if (this.data === undefined) {
|
|
|
|
return undefined;
|
|
|
|
} else if (this.data === null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return Object.assign({}, this.data);
|
|
|
|
}
|
|
|
|
|
|
|
|
getHtmlIdentifier() {
|
|
|
|
return this.domNode;
|
|
|
|
}
|
|
|
|
|
|
|
|
reload(tree) {
|
2020-04-06 07:03:07 -05:00
|
|
|
return new Promise((resolve)=>{
|
|
|
|
this.unload(tree)
|
|
|
|
.then(()=>{
|
|
|
|
tree.aciTreeApi.setInode(this.domNode);
|
|
|
|
tree.aciTreeApi.deselect(this.domNode);
|
|
|
|
setTimeout(() => {
|
|
|
|
tree.selectNode(this.domNode);
|
|
|
|
}, 0);
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
});
|
2018-05-14 07:26:04 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
unload(tree) {
|
2020-04-06 07:03:07 -05:00
|
|
|
return new Promise((resolve, reject)=>{
|
|
|
|
this.children = [];
|
|
|
|
tree.aciTreeApi.unload(this.domNode, {
|
|
|
|
success: ()=>{
|
|
|
|
resolve(true);
|
|
|
|
},
|
|
|
|
fail: ()=>{
|
|
|
|
reject();
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
open(tree, suppressNoDom) {
|
|
|
|
return new Promise((resolve, reject)=>{
|
|
|
|
if(suppressNoDom && (this.domNode == null || typeof(this.domNode) === 'undefined')) {
|
|
|
|
resolve(true);
|
|
|
|
} else if(tree.aciTreeApi.isOpen(this.domNode)) {
|
|
|
|
resolve(true);
|
|
|
|
} else {
|
|
|
|
tree.aciTreeApi.open(this.domNode, {
|
|
|
|
success: ()=>{
|
|
|
|
resolve(true);
|
|
|
|
},
|
|
|
|
fail: ()=>{
|
|
|
|
reject(true);
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2018-05-14 07:26:04 -05:00
|
|
|
}
|
|
|
|
|
2018-06-05 05:36:19 -05:00
|
|
|
/*
|
|
|
|
* Find the ancestor with matches this condition
|
|
|
|
*/
|
|
|
|
ancestorNode(condition) {
|
2018-05-14 07:26:04 -05:00
|
|
|
let node = this;
|
|
|
|
|
|
|
|
while (node.hasParent()) {
|
|
|
|
node = node.parent();
|
|
|
|
if (condition(node)) {
|
2018-06-05 05:36:19 -05:00
|
|
|
return node;
|
2018-05-14 07:26:04 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-05 05:36:19 -05:00
|
|
|
return null;
|
2018-05-14 07:26:04 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given a condition returns true if the current node
|
|
|
|
* or any of the parent nodes condition result is true
|
|
|
|
*/
|
|
|
|
anyFamilyMember(condition) {
|
|
|
|
if(condition(this)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2018-06-05 05:36:19 -05:00
|
|
|
return this.ancestorNode(condition) !== null;
|
|
|
|
}
|
|
|
|
anyParent(condition) {
|
|
|
|
return this.ancestorNode(condition) !== null;
|
2018-05-14 07:26:04 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class Tree {
|
|
|
|
constructor() {
|
|
|
|
this.rootNode = new TreeNode(undefined, {});
|
|
|
|
this.aciTreeApi = undefined;
|
2019-06-27 09:30:05 -05:00
|
|
|
this.draggableTypes = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
*
|
|
|
|
* The dropDetailsFunc should return an object of sample
|
|
|
|
* {text: 'xyz', cur: {from:0, to:0} where text is the drop text and
|
|
|
|
* cur is selection range of text after dropping. If returned as
|
|
|
|
* string, by default cursor will be set to the end of text
|
|
|
|
*/
|
|
|
|
registerDraggableType(typeOrTypeDict, dropDetailsFunc=null) {
|
|
|
|
if(typeof typeOrTypeDict == 'object') {
|
|
|
|
Object.keys(typeOrTypeDict).forEach((type)=>{
|
|
|
|
this.registerDraggableType(type, typeOrTypeDict[type]);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
if(dropDetailsFunc != null) {
|
|
|
|
typeOrTypeDict.replace(/ +/, ' ').split(' ').forEach((type)=>{
|
|
|
|
this.draggableTypes[type] = dropDetailsFunc;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getDraggable(type) {
|
|
|
|
if(this.draggableTypes[type]) {
|
|
|
|
return this.draggableTypes[type];
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
prepareDraggable(data, item) {
|
|
|
|
let dropDetailsFunc = this.getDraggable(data._type);
|
|
|
|
|
|
|
|
if(dropDetailsFunc != null) {
|
2019-08-02 05:28:57 -05:00
|
|
|
|
|
|
|
/* addEventListener is used here because import jquery.drag.event
|
|
|
|
* overrides the dragstart event set using element.on('dragstart')
|
|
|
|
* This will avoid conflict.
|
|
|
|
*/
|
2019-06-27 09:30:05 -05:00
|
|
|
item.find('.aciTreeItem')
|
2019-08-02 05:28:57 -05:00
|
|
|
.attr('draggable', true)[0]
|
|
|
|
.addEventListener('dragstart', (e)=> {
|
2019-06-27 09:30:05 -05:00
|
|
|
let dropDetails = dropDetailsFunc(data, item);
|
|
|
|
|
|
|
|
if(typeof dropDetails == 'string') {
|
|
|
|
dropDetails = {
|
|
|
|
text:dropDetails,
|
|
|
|
cur:{
|
|
|
|
from:dropDetails.length,
|
|
|
|
to: dropDetails.length,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
if(!dropDetails.cur) {
|
|
|
|
dropDetails = {
|
|
|
|
...dropDetails,
|
|
|
|
cur:{
|
|
|
|
from:dropDetails.text.length,
|
|
|
|
to: dropDetails.text.length,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-02 05:28:57 -05:00
|
|
|
e.dataTransfer.setData('text', JSON.stringify(dropDetails));
|
2019-07-04 08:49:09 -05:00
|
|
|
/* Required by Firefox */
|
2019-08-02 05:28:57 -05:00
|
|
|
if(e.dataTransfer.dropEffect) {
|
|
|
|
e.dataTransfer.dropEffect = 'move';
|
2019-07-04 08:49:09 -05:00
|
|
|
}
|
2019-06-27 09:30:05 -05:00
|
|
|
|
|
|
|
/* setDragImage is not supported in IE. We leave it to
|
|
|
|
* its default look and feel
|
|
|
|
*/
|
2019-08-02 05:28:57 -05:00
|
|
|
if(e.dataTransfer.setDragImage) {
|
2019-06-27 09:30:05 -05:00
|
|
|
let dragItem = $(`
|
|
|
|
<div class="drag-tree-node">
|
2019-06-28 07:17:04 -05:00
|
|
|
<span>${_.escape(dropDetails.text)}</span>
|
2019-06-27 09:30:05 -05:00
|
|
|
</div>`
|
|
|
|
);
|
|
|
|
|
|
|
|
$('body .drag-tree-node').remove();
|
|
|
|
$('body').append(dragItem);
|
|
|
|
|
2019-08-02 05:28:57 -05:00
|
|
|
e.dataTransfer.setDragImage(dragItem[0], 0, 0);
|
2019-06-27 09:30:05 -05:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2018-05-14 07:26:04 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
addNewNode(id, data, domNode, parentPath) {
|
|
|
|
const parent = this.findNode(parentPath);
|
|
|
|
return this.createOrUpdateNode(id, data, parent, domNode);
|
|
|
|
}
|
|
|
|
|
|
|
|
findNode(path) {
|
|
|
|
if (path === null || path === undefined || path.length === 0) {
|
|
|
|
return this.rootNode;
|
|
|
|
}
|
|
|
|
return findInTree(this.rootNode, path.join('.'));
|
|
|
|
}
|
|
|
|
|
2020-04-06 07:03:07 -05:00
|
|
|
findNodeWithToggle(path) {
|
|
|
|
let tree = this;
|
2020-04-27 09:21:56 -05:00
|
|
|
|
|
|
|
if(path == null || !Array.isArray(path)) {
|
|
|
|
return Promise.reject();
|
|
|
|
}
|
2020-04-06 07:03:07 -05:00
|
|
|
path = path.join('.');
|
|
|
|
|
|
|
|
let onCorrectPath = function(matchPath) {
|
|
|
|
return (matchPath !== undefined && path !== undefined
|
|
|
|
&& (path.startsWith(matchPath + '.') || path === matchPath));
|
|
|
|
};
|
|
|
|
|
|
|
|
return (function findInNode(currentNode) {
|
|
|
|
return new Promise((resolve, reject)=>{
|
|
|
|
if (path === null || path === undefined || path.length === 0) {
|
|
|
|
resolve(null);
|
|
|
|
}
|
|
|
|
/* No point in checking the children if
|
|
|
|
* the path for currentNode itself is not matching
|
|
|
|
*/
|
|
|
|
if (currentNode.path !== undefined && !onCorrectPath(currentNode.path)) {
|
|
|
|
reject(null);
|
|
|
|
} else if (currentNode.path === path) {
|
|
|
|
resolve(currentNode);
|
|
|
|
} else {
|
|
|
|
currentNode.open(tree, true)
|
|
|
|
.then(()=>{
|
|
|
|
for (let i = 0, length = currentNode.children.length; i < length; i++) {
|
|
|
|
let childNode = currentNode.children[i];
|
|
|
|
if(onCorrectPath(childNode.path)) {
|
|
|
|
resolve(findInNode(childNode));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
reject(null);
|
|
|
|
})
|
|
|
|
.catch(()=>{
|
|
|
|
reject(null);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
})(this.rootNode);
|
|
|
|
}
|
|
|
|
|
2018-05-14 07:26:04 -05:00
|
|
|
findNodeByDomElement(domElement) {
|
|
|
|
const path = this.translateTreeNodeIdFromACITree(domElement);
|
|
|
|
if(!path || !path[0]) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.findNode(path);
|
|
|
|
}
|
|
|
|
|
|
|
|
selected() {
|
|
|
|
return this.aciTreeApi.selected();
|
|
|
|
}
|
|
|
|
|
2020-04-06 07:03:07 -05:00
|
|
|
/* scrollIntoView will scroll only to top and bottom
|
|
|
|
* Logic can be added for scroll to middle
|
|
|
|
*/
|
|
|
|
scrollTo(domElement) {
|
|
|
|
domElement.scrollIntoView();
|
|
|
|
}
|
|
|
|
|
|
|
|
selectNode(aciTreeIdentifier, scrollOnSelect) {
|
2018-05-14 07:26:04 -05:00
|
|
|
this.aciTreeApi.select(aciTreeIdentifier);
|
2020-04-06 07:03:07 -05:00
|
|
|
|
|
|
|
if(scrollOnSelect) {
|
|
|
|
this.scrollTo(aciTreeIdentifier[0]);
|
|
|
|
}
|
2018-05-14 07:26:04 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
createOrUpdateNode(id, data, parent, domNode) {
|
|
|
|
let oldNodePath = [id];
|
|
|
|
if(parent !== null && parent !== undefined) {
|
|
|
|
oldNodePath = [parent.path, id];
|
|
|
|
}
|
|
|
|
const oldNode = this.findNode(oldNodePath);
|
|
|
|
if (oldNode !== null) {
|
2019-01-10 00:22:52 -06:00
|
|
|
oldNode.data = data;
|
2020-04-06 07:03:07 -05:00
|
|
|
oldNode.domNode = domNode;
|
2018-05-14 07:26:04 -05:00
|
|
|
return oldNode;
|
|
|
|
}
|
|
|
|
|
|
|
|
const node = new TreeNode(id, data, domNode, parent);
|
|
|
|
if (parent === this.rootNode) {
|
|
|
|
node.parentNode = null;
|
|
|
|
}
|
2020-06-18 10:20:34 -05:00
|
|
|
|
|
|
|
if (parent !== null && parent !== undefined)
|
|
|
|
parent.children.push(node);
|
2018-05-14 07:26:04 -05:00
|
|
|
return node;
|
|
|
|
}
|
|
|
|
|
2020-04-06 07:03:07 -05:00
|
|
|
unloadNode(id, data, domNode, parentPath) {
|
|
|
|
let oldNodePath = [id];
|
|
|
|
const parent = this.findNode(parentPath);
|
|
|
|
if(parent !== null && parent !== undefined) {
|
|
|
|
oldNodePath = [parent.path, id];
|
|
|
|
}
|
|
|
|
const oldNode = this.findNode(oldNodePath);
|
|
|
|
if(oldNode) {
|
|
|
|
oldNode.children = [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-14 07:26:04 -05:00
|
|
|
/**
|
|
|
|
* Given the JQuery object that contains the ACI Tree
|
|
|
|
* this method is responsible for registering this tree class
|
|
|
|
* to listen to all the events that happen in the ACI Tree.
|
|
|
|
*
|
|
|
|
* At this point in time the only event that we care about is
|
|
|
|
* the addition of a new node.
|
|
|
|
* The function will create a new tree node to store the information
|
|
|
|
* that exist in the ACI for it.
|
|
|
|
*/
|
|
|
|
register($treeJQuery) {
|
|
|
|
$treeJQuery.on('acitree', function (event, api, item, eventName) {
|
|
|
|
if (api.isItem(item)) {
|
2019-08-05 02:18:27 -05:00
|
|
|
/* If the id of node is changed, the path should also be changed */
|
2020-04-06 07:03:07 -05:00
|
|
|
if (['added', 'idset', 'beforeunload'].indexOf(eventName) != -1) {
|
2018-05-14 07:26:04 -05:00
|
|
|
const id = api.getId(item);
|
|
|
|
const data = api.itemData(item);
|
2020-04-06 07:03:07 -05:00
|
|
|
const parentId = this.translateTreeNodeIdFromACITree(api.parent(item));
|
2019-06-27 09:30:05 -05:00
|
|
|
|
2020-04-06 07:03:07 -05:00
|
|
|
if(eventName === 'beforeunload') {
|
|
|
|
this.unloadNode(id, data, item, parentId);
|
|
|
|
} else {
|
|
|
|
if(eventName === 'added') {
|
|
|
|
this.prepareDraggable(data, item);
|
|
|
|
}
|
2019-06-27 09:30:05 -05:00
|
|
|
|
2020-04-06 07:03:07 -05:00
|
|
|
this.addNewNode(id, data, item, parentId);
|
|
|
|
}
|
2019-11-25 21:34:41 -06:00
|
|
|
if(data.errmsg) {
|
|
|
|
Alertify.error(data.errmsg);
|
|
|
|
}
|
2018-05-14 07:26:04 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}.bind(this));
|
|
|
|
this.aciTreeApi = $treeJQuery.aciTree('api');
|
2020-04-10 07:04:57 -05:00
|
|
|
|
|
|
|
/* Ctrl + Click will trigger context menu. Select the node when Ctrl+Clicked.
|
|
|
|
* When the context menu is visible, the tree should lose focus
|
|
|
|
* to use context menu with keyboard. Otherwise, the tree functions
|
|
|
|
* overrides the keyboard events.
|
|
|
|
*/
|
|
|
|
let contextHandler = (ev)=>{
|
|
|
|
let treeItem = this.aciTreeApi.itemFrom(ev.target);
|
|
|
|
if(treeItem.length) {
|
|
|
|
if(ev.ctrlKey) {
|
|
|
|
this.aciTreeApi.select(treeItem);
|
|
|
|
}
|
|
|
|
$(treeItem).on('contextmenu:visible', ()=>{
|
|
|
|
$(treeItem).trigger('blur');
|
|
|
|
$(treeItem).off('contextmenu:visible');
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
$treeJQuery
|
|
|
|
.off('mousedown', contextHandler)
|
|
|
|
.on('mousedown', contextHandler);
|
2018-05-14 07:26:04 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* As the name hints this functions works as a layer in between ACI and
|
|
|
|
* the adaptor. Given a ACITree JQuery node find the location of it in the
|
|
|
|
* Tree and then returns and array with the path to to the Tree Node in
|
|
|
|
* question
|
|
|
|
*
|
|
|
|
* This is not optimized and will always go through the full tree
|
|
|
|
*/
|
|
|
|
translateTreeNodeIdFromACITree(aciTreeNode) {
|
|
|
|
let currentTreeNode = aciTreeNode;
|
|
|
|
let path = [];
|
|
|
|
while (currentTreeNode !== null && currentTreeNode !== undefined && currentTreeNode.length > 0) {
|
|
|
|
path.unshift(this.aciTreeApi.getId(currentTreeNode));
|
|
|
|
if (this.aciTreeApi.hasParent(currentTreeNode)) {
|
|
|
|
currentTreeNode = this.aciTreeApi.parent(currentTreeNode);
|
|
|
|
} else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return path;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given an initial node and a path, it will navigate through
|
|
|
|
* the new tree to find the node that matches the path
|
|
|
|
*/
|
|
|
|
function findInTree(rootNode, path) {
|
|
|
|
if (path === null) {
|
|
|
|
return rootNode;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (function findInNode(currentNode) {
|
2019-08-06 08:02:57 -05:00
|
|
|
|
|
|
|
/* No point in checking the children if
|
|
|
|
* the path for currentNode itself is not matching
|
|
|
|
*/
|
|
|
|
if (currentNode.path !== undefined && path !== undefined
|
|
|
|
&& !path.startsWith(currentNode.path)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-05-14 07:26:04 -05:00
|
|
|
for (let i = 0, length = currentNode.children.length; i < length; i++) {
|
|
|
|
const calculatedNode = findInNode(currentNode.children[i]);
|
|
|
|
if (calculatedNode !== null) {
|
|
|
|
return calculatedNode;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (currentNode.path === path) {
|
|
|
|
return currentNode;
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
})(rootNode);
|
|
|
|
}
|
2018-06-05 05:36:19 -05:00
|
|
|
|
2018-07-25 01:40:46 -05:00
|
|
|
let isValidTreeNodeData = isValidData;
|
|
|
|
|
|
|
|
export {isValidTreeNodeData};
|