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