- 1 :
import Load from './load';
- 2 :
- 3 :
/**
- 4 :
* Class for working with categories and features.
- 5 :
* Categories are groupings of terms.
- 6 :
* A term can be present in multiple categories. Category ranking is used to determine which feature value to prioritize.
- 7 :
* Features are arbitrary properties (font, color) that are associated with each category.
- 8 :
* @memberof Spyral
- 9 :
* @class
- 10 :
*/
- 11 :
class Categories {
- 12 :
- 13 :
/**
- 14 :
* Construct a new Categories class.
- 15 :
*
- 16 :
* @example
- 17 :
* new Spyral.Categories({
- 18 :
* categories: {
- 19 :
* positive: ['good', 'happy'],
- 20 :
* negative: ['bad', 'sad']
- 21 :
* },
- 22 :
* categoriesRanking: ['positive','negative'],
- 23 :
* features: {color: {}},
- 24 :
* featureDefaults: {color: '#333333'}
- 25 :
* })
- 26 :
* @constructor
- 27 :
* @param {Object} config The config object
- 28 :
* @param {Object} config.categories An object that maps arrays of terms to category names
- 29 :
* @param {Array} config.categoriesRanking An array of category names that determines their ranking, from high to low
- 30 :
* @param {Object} config.features An object that maps categories to feature names
- 31 :
* @param {Object} config.featureDefaults An object that maps default feature value to feature names
- 32 :
* @returns {Spyral.Categories}
- 33 :
*/
- 34 :
constructor({categories, categoriesRanking, features, featureDefaults} = {categories: {}, categoriesRanking: [], features: {}, featureDefaults: {}}) {
- 35 :
this.categories = categories;
- 36 :
this.categoriesRanking = categoriesRanking;
- 37 :
this.features = features;
- 38 :
this.featureDefaults = featureDefaults;
- 39 :
}
- 40 :
- 41 :
/**
- 42 :
* Get the categories.
- 43 :
* @returns {Object}
- 44 :
*/
- 45 :
getCategories() {
- 46 :
return this.categories;
- 47 :
}
- 48 :
- 49 :
/**
- 50 :
* Get category names as an array.
- 51 :
* @returns {Array}
- 52 :
*/
- 53 :
getCategoryNames() {
- 54 :
return Object.keys(this.getCategories());
- 55 :
}
- 56 :
- 57 :
/**
- 58 :
* Get the terms for a category.
- 59 :
* @param {String} name The category name
- 60 :
* @returns {Array}
- 61 :
*/
- 62 :
getCategoryTerms(name) {
- 63 :
return this.categories[name];
- 64 :
}
- 65 :
- 66 :
/**
- 67 :
* Add a new category.
- 68 :
* @param {String} name The category name
- 69 :
*/
- 70 :
addCategory(name) {
- 71 :
if (this.categories[name] === undefined) {
- 72 :
this.categories[name] = [];
- 73 :
this.categoriesRanking.push(name);
- 74 :
}
- 75 :
}
- 76 :
- 77 :
/**
- 78 :
* Rename a category.
- 79 :
* @param {String} oldName The old category name
- 80 :
* @param {String} newName The new category name
- 81 :
*/
- 82 :
renameCategory(oldName, newName) {
- 83 :
if (oldName !== newName) {
- 84 :
var terms = this.getCategoryTerms(oldName);
- 85 :
var ranking = this.getCategoryRanking(oldName);
- 86 :
this.addTerms(newName, terms);
- 87 :
for (var feature in this.features) {
- 88 :
var value = this.features[feature][oldName];
- 89 :
this.setCategoryFeature(newName, feature, value);
- 90 :
}
- 91 :
this.removeCategory(oldName);
- 92 :
this.setCategoryRanking(newName, ranking);
- 93 :
}
- 94 :
}
- 95 :
- 96 :
/**
- 97 :
* Remove a category.
- 98 :
* @param {String} name The category name
- 99 :
*/
- 100 :
removeCategory(name) {
- 101 :
delete this.categories[name];
- 102 :
var index = this.categoriesRanking.indexOf(name);
- 103 :
if (index !== -1) {
- 104 :
this.categoriesRanking.splice(index, 1);
- 105 :
}
- 106 :
for (var feature in this.features) {
- 107 :
delete this.features[feature][name];
- 108 :
}
- 109 :
}
- 110 :
- 111 :
/**
- 112 :
* Gets the ranking for a category.
- 113 :
* @param {String} name The category name
- 114 :
* @returns {number}
- 115 :
*/
- 116 :
getCategoryRanking(name) {
- 117 :
var ranking = this.categoriesRanking.indexOf(name);
- 118 :
if (ranking === -1) {
- 119 :
return undefined;
- 120 :
} else {
- 121 :
return ranking;
- 122 :
}
- 123 :
}
- 124 :
- 125 :
/**
- 126 :
* Sets the ranking for a category.
- 127 :
* @param {String} name The category name
- 128 :
* @param {number} ranking The category ranking
- 129 :
*/
- 130 :
setCategoryRanking(name, ranking) {
- 131 :
if (this.categories[name] !== undefined) {
- 132 :
ranking = Math.min(this.categoriesRanking.length-1, Math.max(0, ranking));
- 133 :
var index = this.categoriesRanking.indexOf(name);
- 134 :
if (index !== -1) {
- 135 :
this.categoriesRanking.splice(index, 1);
- 136 :
}
- 137 :
this.categoriesRanking.splice(ranking, 0, name);
- 138 :
}
- 139 :
}
- 140 :
- 141 :
/**
- 142 :
* Add a term to a category.
- 143 :
* @param {String} category The category name
- 144 :
* @param {String} term The term
- 145 :
*/
- 146 :
addTerm(category, term) {
- 147 :
this.addTerms(category, [term]);
- 148 :
}
- 149 :
- 150 :
/**
- 151 :
* Add multiple terms to a category.
- 152 :
* @param {String} category The category name
- 153 :
* @param {Array} terms An array of terms
- 154 :
*/
- 155 :
addTerms(category, terms) {
- 156 :
if (!Array.isArray(terms)) {
- 157 :
terms = [terms];
- 158 :
}
- 159 :
if (this.categories[category] === undefined) {
- 160 :
this.addCategory(category);
- 161 :
}
- 162 :
for (var i = 0; i < terms.length; i++) {
- 163 :
var term = terms[i];
- 164 :
if (this.categories[category].indexOf(term) === -1) {
- 165 :
this.categories[category].push(term);
- 166 :
}
- 167 :
}
- 168 :
}
- 169 :
- 170 :
/**
- 171 :
* Remove a term from a category.
- 172 :
* @param {String} category The category name
- 173 :
* @param {String} term The term
- 174 :
*/
- 175 :
removeTerm(category, term) {
- 176 :
this.removeTerms(category, [term]);
- 177 :
}
- 178 :
- 179 :
/**
- 180 :
* Remove multiple terms from a category.
- 181 :
* @param {String} category The category name
- 182 :
* @param {Array} terms An array of terms
- 183 :
*/
- 184 :
removeTerms(category, terms) {
- 185 :
if (!Array.isArray(terms)) {
- 186 :
terms = [terms];
- 187 :
}
- 188 :
if (this.categories[category] !== undefined) {
- 189 :
for (var i = 0; i < terms.length; i++) {
- 190 :
var term = terms[i];
- 191 :
var index = this.categories[category].indexOf(term);
- 192 :
if (index !== -1) {
- 193 :
this.categories[category].splice(index, 1);
- 194 :
}
- 195 :
}
- 196 :
}
- 197 :
}
- 198 :
- 199 :
/**
- 200 :
* Get the category that a term belongs to, taking ranking into account.
- 201 :
* @param {String} term The term
- 202 :
* @returns {string}
- 203 :
*/
- 204 :
getCategoryForTerm(term) {
- 205 :
var ranking = Number.MAX_VALUE;
- 206 :
var cat = undefined;
- 207 :
for (var category in this.categories) {
- 208 :
if (this.categories[category].indexOf(term) !== -1 && this.getCategoryRanking(category) < ranking) {
- 209 :
ranking = this.getCategoryRanking(category);
- 210 :
cat = category;
- 211 :
}
- 212 :
}
- 213 :
return cat;
- 214 :
}
- 215 :
- 216 :
/**
- 217 :
* Get all the categories a term belongs to.
- 218 :
* @param {String} term The term
- 219 :
* @returns {Array}
- 220 :
*/
- 221 :
getCategoriesForTerm(term) {
- 222 :
var cats = [];
- 223 :
for (var category in this.categories) {
- 224 :
if (this.categories[category].indexOf(term) !== -1) {
- 225 :
cats.push(category);
- 226 :
}
- 227 :
}
- 228 :
return cats;
- 229 :
}
- 230 :
- 231 :
/**
- 232 :
* Get the feature for a term.
- 233 :
* @param {String} feature The feature
- 234 :
* @param {String} term The term
- 235 :
* @returns {*}
- 236 :
*/
- 237 :
getFeatureForTerm(feature, term) {
- 238 :
return this.getCategoryFeature(this.getCategoryForTerm(term), feature);
- 239 :
}
- 240 :
- 241 :
/**
- 242 :
* Get the features.
- 243 :
* @returns {Object}
- 244 :
*/
- 245 :
getFeatures() {
- 246 :
return this.features;
- 247 :
}
- 248 :
- 249 :
/**
- 250 :
* Add a feature.
- 251 :
* @param {String} name The feature name
- 252 :
* @param {*} defaultValue The default value
- 253 :
*/
- 254 :
addFeature(name, defaultValue) {
- 255 :
if (this.features[name] === undefined) {
- 256 :
this.features[name] = {};
- 257 :
}
- 258 :
if (defaultValue !== undefined) {
- 259 :
this.featureDefaults[name] = defaultValue;
- 260 :
}
- 261 :
}
- 262 :
- 263 :
/**
- 264 :
* Remove a feature.
- 265 :
* @param {String} name The feature name
- 266 :
*/
- 267 :
removeFeature(name) {
- 268 :
delete this.features[name];
- 269 :
delete this.featureDefaults[name];
- 270 :
}
- 271 :
- 272 :
/**
- 273 :
* Set the feature for a category.
- 274 :
* @param {String} categoryName The category name
- 275 :
* @param {String} featureName The feature name
- 276 :
* @param {*} featureValue The feature value
- 277 :
*/
- 278 :
setCategoryFeature(categoryName, featureName, featureValue) {
- 279 :
if (this.features[featureName] === undefined) {
- 280 :
this.addFeature(featureName);
- 281 :
}
- 282 :
this.features[featureName][categoryName] = featureValue;
- 283 :
}
- 284 :
- 285 :
/**
- 286 :
* Get the feature for a category.
- 287 :
* @param {String} categoryName The category name
- 288 :
* @param {String} featureName The feature name
- 289 :
* @returns {*}
- 290 :
*/
- 291 :
getCategoryFeature(categoryName, featureName) {
- 292 :
var value = undefined;
- 293 :
if (this.features[featureName] !== undefined) {
- 294 :
value = this.features[featureName][categoryName];
- 295 :
if (value === undefined) {
- 296 :
value = this.featureDefaults[featureName];
- 297 :
if (typeof value === 'function') {
- 298 :
value = value();
- 299 :
}
- 300 :
}
- 301 :
}
- 302 :
return value;
- 303 :
}
- 304 :
- 305 :
/**
- 306 :
* Get a copy of the category and feature data.
- 307 :
* @returns {Object}
- 308 :
*/
- 309 :
getCategoryExportData() {
- 310 :
return {
- 311 :
categories: Object.assign({}, this.categories),
- 312 :
categoriesRanking: this.categoriesRanking.map(x => x),
- 313 :
features: Object.assign({}, this.features)
- 314 :
};
- 315 :
}
- 316 :
- 317 :
/**
- 318 :
* Save the categories (if we're in a recognized environment).
- 319 :
* @param {Object} config for the network call (specifying if needed the location of Trombone, etc., see {@link Spyral.Load#trombone}
- 320 :
* @param {Object} [api] an object specifying any parameters for the trombone call
- 321 :
* @returns {Promise<String>} this returns a promise which eventually resolves to a string that is the ID reference for the stored categories
- 322 :
*/
- 323 :
save(config={},api={}) {
- 324 :
const categoriesData = JSON.stringify(this.getCategoryExportData());
- 325 :
return Load.trombone(api, Object.assign(config, {
- 326 :
tool: 'resource.StoredCategories',
- 327 :
storeResource: categoriesData
- 328 :
})).then(data => data.storedCategories.id);
- 329 :
}
- 330 :
- 331 :
/**
- 332 :
* Load the categories (if we're in a recognized environment).
- 333 :
*
- 334 :
* In its simplest form this can be used with a single string ID to load:
- 335 :
*
- 336 :
* new Spyral.Categories().load("categories.en.txt")
- 337 :
*
- 338 :
* Which is equivalent to:
- 339 :
*
- 340 :
* new Spyral.Categories().load({retrieveResourceId: "categories.en.txt"});
- 341 :
*
- 342 :
* @param {(Object|String)} config an object specifying the parameters (see above)
- 343 :
* @param {Object} [api] an object specifying any parameters for the trombone call
- 344 :
* @returns {Promise<Object>} this first returns a promise and when the promise is resolved it returns this categories object (with the loaded data included)
- 345 :
*/
- 346 :
load(config={}, api={}) {
- 347 :
let me = this;
- 348 :
if (typeof config === 'string') {
- 349 :
config = {'retrieveResourceId': config};
- 350 :
}
- 351 :
if (!('retrieveResourceId' in config)) {
- 352 :
throw Error('You must provide a value for the retrieveResourceId parameter');
- 353 :
}
- 354 :
return Load.trombone(api, Object.assign(config, {
- 355 :
tool: 'resource.StoredCategories'
- 356 :
})).then(data => {
- 357 :
const cats = JSON.parse(data.storedCategories.resource);
- 358 :
me.features = cats.features;
- 359 :
me.categories = cats.categories;
- 360 :
me.categoriesRanking = cats.categoriesRanking || [];
- 361 :
if (me.categoriesRanking.length === 0) {
- 362 :
for (var category in me.categories) {
- 363 :
me.categoriesRanking.push(category);
- 364 :
}
- 365 :
}
- 366 :
return me;
- 367 :
});
- 368 :
}
- 369 :
- 370 :
/**
- 371 :
* Load categories and return a promise that resolves to a new Spyral.Categories instance.
- 372 :
*
- 373 :
* @param {(Object|String)} config an object specifying the parameters (see above)
- 374 :
* @param {Object} [api] an object specifying any parameters for the trombone call
- 375 :
* @returns {Promise<Object>} this first returns a promise and when the promise is resolved it returns this categories object (with the loaded data included)
- 376 :
* @static
- 377 :
*/
- 378 :
static load(config={}, api={}) {
- 379 :
const categories = new Categories();
- 380 :
return categories.load(config, api);
- 381 :
}
- 382 :
}
- 383 :
- 384 :
export default Categories;