horizon/horizon/static/framework/widgets/metadata-tree/metadata-tree-service.js

499 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(function () {
'use strict';
angular.module('hz.widget.metadata-tree')
/**
* @ngdoc service
* @name hz.widget.metadata-tree.metadataTreeService
*/
.factory('metadataTreeService', [function () {
/**
* Parse value into boolean
*
* @param {(string|boolean)} value
* @returns {boolean}
*/
function parseBool(value) {
var value_type = typeof(value);
if(value_type === 'boolean') {
return value;
}
else if(value_type === 'string') {
value = value.toLowerCase();
if(value === 'true') {
return true;
}
else if(value === 'false') {
return false;
}
}
return null;
}
/**
* Construct a new property
*
* @class Property
* @param {string} name
* @param {Object} [json]
*
* @property {string} name Property key name
* @property {string} title Property display name
* @property {string} description Property description
* @property {*} value Property value
* @property {string} default Property default value
* @property {string} type Property type
* @property {boolean} readonly Property readonly state
* @property {string[]} operators Property available operators when type='array'
* @property {string} operator Property operator when type='array'
*/
function Property(name, json) {
this.name = name;
this.title = name;
this.description = '';
this.value = null;
this.default = null;
this.type = 'string';
this.readonly = false;
this.operators = ['<in>'];
angular.extend(this, json);
this.operator = this.operators[0];
this.setValue(this.default);
}
/**
* Deserialize value and assign it to {@link Property#value}
*
* @param {string} value
*/
Property.prototype.setValue = function(value) {
if(value === null) {
this.value = this.type !== 'array' ? null : [];
return;
}
switch (this.type) {
case 'integer': this.value = parseInt(value); break;
case 'number': this.value = parseFloat(value); break;
case 'array':
var data = /^(<.*?>) (.*)$/.exec(value);
if(data) {
this.operator = data[1];
this.value = data[2].split(',');
} break;
case 'boolean': this.value = parseBool(value); break;
default: this.value = value;
}
};
/**
* Serialize {@link Property#value} and returns it
*
* @returns {*}
*/
Property.prototype.getValue = function() {
switch (this.type) {
case 'array': return this.operator + ' ' + this.value.join(',');
default: return this.value;
}
};
/**
* Construct a new tree node
*
* @class Item
* @param {Item} parent
*
* @property {Item} parent Item parent
* @property {Item[]} children Item children
* @property {boolean} visible Item visibility
*/
function Item(parent) {
// parent as property to prevent infinite recursion in angular filter
Object.defineProperty(this, 'parent', {
value: typeof parent !== 'undefined' ? parent : null
});
this.children = [];
// Node properties
this.visible = false;
this.expanded = false;
this.label = '';
this.description = '';
this.level = parent ? parent.level + 1 : 0;
this.addedCount = 0;
this.custom = false;
// Leaf properties
this.leaf = null;
this.added = false;
}
/**
* Load Item values and child Items from namespace definition
*
* @param {object} namespace Metadata namespace definition
* @returns {Item}
*/
Item.prototype.fromNamespace = function (namespace) {
this.label = namespace.display_name;
this.description = namespace.description;
if(namespace.objects) {
angular.forEach(namespace.objects, function (object) {
this.children.push(new Item(this).fromObject(object));
}, this);
}
if(namespace.properties) {
angular.forEach(namespace.properties, function (property, key) {
this.children.push(new Item(this).fromProperty(key, property));
}, this);
}
this.sortChildren();
return this;
};
/**
* Load Item values and child Items from object definition
*
* @param {object} object Metadata object definition
* @returns {Item}
*/
Item.prototype.fromObject = function (object) {
this.label = object.name;
this.description = object.description;
if(object.properties) {
angular.forEach(object.properties, function (property, key) {
this.children.push(new Item(this).fromProperty(key, property));
}, this);
}
this.sortChildren();
return this;
};
/**
* Load Item values from property definition
*
* @param {string} name Property name
* @param {object} property Metadata property definition
* @returns {Item}
*/
Item.prototype.fromProperty = function (name, property) {
this.leaf = new Property(name, property);
this.label = this.leaf.title;
this.description = this.leaf.description;
return this;
};
/**
* Load Item values from property definition and mark as custom
*
* @param {string} name Property name
* @param {object} property Metadata property definition
* @returns {Item}
*/
Item.prototype.customProperty = function (name, property) {
this.fromProperty(name, property);
this.custom = true;
return this;
};
/**
* Expand Item by marking all children as visible
*
* @param {boolean} deep Whether to recursively expand all child Items
*/
Item.prototype.expand = function (deep) {
this.expanded = true;
angular.forEach(this.children, function (child) {
if(deep) {
child.expand(deep);
}
child.visible = true;
}, this);
};
/**
* Collapse Item by recursively unmarking all children as visible
*/
Item.prototype.collapse = function () {
this.expanded = false;
angular.forEach(this.children, function (child) {
child.collapse();
child.visible = false;
}, this);
};
/**
* Sort children Items by label
*/
Item.prototype.sortChildren = function () {
this.children.sort(function (a, b) {
return a.label.localeCompare(b.label);
});
};
/**
* Recursively mark Item and all children as added
*
* @param {=} caller Used internally to prevent infinite recursion
*/
Item.prototype.markAsAdded = function (caller) {
if(this.parent && !this.added) {
this.parent.addedCount += 1;
if(this.parent.addedCount === this.parent.children.length) {
this.parent.markAsAdded(this);
}
}
this.added = true;
if(!caller) { // prevent infinite recursion
angular.forEach(this.children, function (item) {
item.markAsAdded();
}, this);
}
};
/**
* Recursively unmark Item and all children as added
*
* @param {boolean=} expand Whether to expand parent of unmarked Item
* @param {=} caller Used internally to prevent infinite recursion
*/
Item.prototype.unmarkAsAdded = function (expand, caller) {
if(this.parent) {
if(expand) {
this.parent.expand();
}
if(this.added) {
this.parent.addedCount -= 1;
this.parent.unmarkAsAdded(expand, this);
}
}
this.added = false;
if(!caller) { // prevent infinite recursion
angular.forEach(this.children, function (item) {
item.unmarkAsAdded(expand);
}, this);
}
};
/**
* Returns list of Items from top-most parent to this Item
*
* @param {[]=} path Used internally
* @returns {Item[]}
*/
Item.prototype.path = function (path) {
path = typeof path !== 'undefined' ? path : [];
if(this.parent) {
this.parent.path(path);
}
path.push(this);
return path;
};
/**
* Returns breadcrumb string for this Item
*
* @returns {string}
*/
Item.prototype.breadcrumb = function () {
return this.path().map(function (item) {
return item.label;
}).join(' ');
};
/**
* Parse string parameter into leaf value
*
* @param {string} value
*/
Item.prototype.setLeafValue = function (value) {
if(this.leaf) {
this.leaf.setValue(value);
}
};
/**
* Serialize leaf value into string
*
* @returns {string}
*/
Item.prototype.getLeafValue = function () {
if(this.leaf) {
return this.leaf.getValue();
}
};
/**
* Construct a new tree
*
* @class Tree
* @param {object[]} available List of available namespaces
* @param {object} existing Key-value pairs for existing metadata
*
* @property {Item[]} tree List available namespaces parsed into Item-s
* @property {Item[]} flatTree List of Item-s flattened from tree structure
* @property {Item} selected Selected Item
*/
function Tree(available, existing) {
this.tree = [];
this.loadNamespaces(available);
this.flatTree = this.flatten(this.tree);
this.selected = null;
this.loadExisting(existing);
}
/**
* Load Item values and child Items from namespace definition
*
* @param {object[]} namespaces list of Metadata namespace definitions
* @returns {Tree}
*/
Tree.prototype.loadNamespaces = function (namespaces) {
angular.forEach(namespaces, function (namespace) {
var item = new Item().fromNamespace(namespace);
item.visible = true;
this.tree.push(item);
}, this);
this.tree.sort(function (a, b) {
return a.label.localeCompare(b.label);
});
return this;
};
/**
* Crete flat representation of branch
*
* @param {Item[]} branch List of Items to flatten
* @param {[]=} items Used internally
* @returns {Item[]}
*/
Tree.prototype.flatten = function (branch, items) {
items = typeof items !== 'undefined' ? items : [];
angular.forEach(branch, function (item) {
items.push(item);
this.flatten(item.children, items);
}, this);
return items;
};
/**
* Load Property.value for each value from existing and mark corresponding
* Items as added. If no corresponding Item is found new Item is added and
* marked as custom.
*
* @param {object} existing
*/
Tree.prototype.loadExisting = function (existing) {
var itemsMapping = {};
angular.forEach(this.flatTree, function (item) {
if(item.leaf && item.leaf.name in existing) {
itemsMapping[item.leaf.name] = item;
}
});
angular.forEach(existing, function (value, key) {
var item = itemsMapping[key];
if(typeof item === 'undefined') {
item = new Item().customProperty(key);
this.flatTree.push(item);
}
item.setLeafValue(value);
item.markAsAdded();
}, this);
};
/**
* Returns key-value mapping of leaf Items that was marked as added
*
* @returns {object}
*/
Tree.prototype.getExisting = function () {
var existing = {};
angular.forEach(this.flatTree, function(item) {
if(item.added && item.leaf) {
existing[item.leaf.name] = item.getLeafValue();
}
});
return existing;
};
/**
* Selects item and expands / collapses it
*
* @param {Item} item
*/
Tree.prototype.select = function (item) {
this.selected = item;
if(!item.expanded) {
item.expand();
} else {
item.collapse();
}
};
/**
* Selects item and marks it as added
*
* @param {Item} item
*/
Tree.prototype.markAsAdded = function (item) {
this.selected = item;
item.markAsAdded();
};
/**
* Selects item, unmarks it as added and expands it's parent
*
* @param {Item} item
*/
Tree.prototype.unmarkAsAdded = function (item) {
if(!item.custom) {
this.selected = item;
item.unmarkAsAdded(true);
} else {
this.selected = null;
var i = this.flatTree.indexOf(item);
if(i > -1) {
this.flatTree.splice(i, 1);
}
}
};
/**
* Adds new Item, selects it and marks it as custom and added
*
* @param {string} name Name of leaf
*/
Tree.prototype.addCustom = function (name) {
var item = new Item().customProperty(name);
item.markAsAdded();
this.flatTree.push(item);
this.selected = item;
};
return {
Item: Item,
Property: Property,
Tree: Tree
};
}]);
}());