1. 1 : /**
  2. 2 : * The Contexts (or Keywords in Context) tool shows each occurrence of a keyword with a bit of surrounding text (the context).
  3. 3 : *
  4. 4 : * @example
  5. 5 : *
  6. 6 : * let config = {
  7. 7 : * columns: null,
  8. 8 : * context: 5,
  9. 9 : * dir: null,
  10. 10 : * docId: null,
  11. 11 : * docIndex: null,
  12. 12 : * expand: null,
  13. 13 : * query: null,
  14. 14 : * sort: null,
  15. 15 : * stopList: null,
  16. 16 : * termColors: null
  17. 17 : * };
  18. 18 : *
  19. 19 : * loadCorpus("austen").tool("Contexts", config);
  20. 20 : *
  21. 21 : * @class Contexts
  22. 22 : * @tutorial contexts
  23. 23 : * @memberof Tools
  24. 24 : */
  25. 25 : Ext.define('Voyant.panel.Contexts', {
  26. 26 : extend: 'Ext.grid.Panel',
  27. 27 : mixins: ['Voyant.panel.Panel'],
  28. 28 : requires: ['Voyant.data.store.Contexts'],
  29. 29 : alias: 'widget.contexts',
  30. 30 : isConsumptive: true,
  31. 31 : statics: {
  32. 32 : i18n: {
  33. 33 : },
  34. 34 : api: {
  35. 35 : /**
  36. 36 : * @memberof Tools.Contexts
  37. 37 : * @instance
  38. 38 : * @property {query}
  39. 39 : */
  40. 40 : query: undefined,
  41. 41 :
  42. 42 : /**
  43. 43 : * @memberof Tools.Contexts
  44. 44 : * @instance
  45. 45 : * @property {docId}
  46. 46 : */
  47. 47 : docId: undefined,
  48. 48 :
  49. 49 : /**
  50. 50 : * @memberof Tools.Contexts
  51. 51 : * @instance
  52. 52 : * @property {docIndex}
  53. 53 : */
  54. 54 : docIndex: undefined,
  55. 55 :
  56. 56 : /**
  57. 57 : * @memberof Tools.Contexts
  58. 58 : * @instance
  59. 59 : * @property {stopList}
  60. 60 : * @default
  61. 61 : */
  62. 62 : stopList: 'auto',
  63. 63 :
  64. 64 : /**
  65. 65 : * @memberof Tools.Contexts
  66. 66 : * @instance
  67. 67 : * @property {context}
  68. 68 : * @default
  69. 69 : */
  70. 70 : context: 5,
  71. 71 :
  72. 72 : /**
  73. 73 : * @memberof Tools.Contexts
  74. 74 : * @instance
  75. 75 : * @property {Number} expand How many terms to show when you expand any given row
  76. 76 : * @default
  77. 77 : */
  78. 78 : expand: 50,
  79. 79 :
  80. 80 : /**
  81. 81 : * @memberof Tools.Contexts
  82. 82 : * @instance
  83. 83 : * @property {columns} columns 'docIndex', 'left', 'term', 'right', 'position'
  84. 84 : */
  85. 85 : columns: undefined,
  86. 86 :
  87. 87 : /**
  88. 88 : * @memberof Tools.Contexts
  89. 89 : * @instance
  90. 90 : * @property {sort}
  91. 91 : */
  92. 92 : sort: undefined,
  93. 93 :
  94. 94 : /**
  95. 95 : * @memberof Tools.Contexts
  96. 96 : * @instance
  97. 97 : * @property {dir}
  98. 98 : */
  99. 99 : dir: undefined,
  100. 100 :
  101. 101 : /**
  102. 102 : * @memberof Tools.Contexts
  103. 103 : * @instance
  104. 104 : * @property {termColors}
  105. 105 : * @default
  106. 106 : */
  107. 107 : termColors: 'categories'
  108. 108 : },
  109. 109 : glyph: 'xf0ce@FontAwesome'
  110. 110 : },
  111. 111 : config: {
  112. 112 : options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'},{xtype: 'termcolorsoption'}]
  113. 113 : },
  114. 114 : constructor: function() {
  115. 115 : this.mixins['Voyant.util.Api'].constructor.apply(this, arguments);
  116. 116 : this.callParent(arguments);
  117. 117 : this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
  118. 118 : },
  119. 119 :
  120. 120 : initComponent: function() {
  121. 121 : var me = this;
  122. 122 :
  123. 123 : Ext.apply(me, {
  124. 124 : title: this.localize('title'),
  125. 125 : emptyText: this.localize("emptyText"),
  126. 126 : store : Ext.create("Voyant.data.store.ContextsBuffered", {
  127. 127 : parentPanel: this,
  128. 128 : proxy: {
  129. 129 : extraParams: {
  130. 130 : stripTags: "all"
  131. 131 : }
  132. 132 : }
  133. 133 : // sortOnLoad: true,
  134. 134 : // sorters: {
  135. 135 : // property: 'position',
  136. 136 : // direction: 'ASC'
  137. 137 : // }
  138. 138 : }),
  139. 139 : selModel: {
  140. 140 : type: 'rowmodel',
  141. 141 : listeners: {
  142. 142 : selectionchange: {
  143. 143 : fn: function(sm, selections) {
  144. 144 : this.getApplication().dispatchEvent('termLocationClicked', this, selections);
  145. 145 : },
  146. 146 : scope: this
  147. 147 : }
  148. 148 : }
  149. 149 : },
  150. 150 : plugins: [{ // the expander slider assumes there's only one plugin, needs to be updated if changed
  151. 151 : ptype: 'rowexpander',
  152. 152 : rowBodyTpl : new Ext.XTemplate('')
  153. 153 : }],
  154. 154 : dockedItems: [{
  155. 155 : dock: 'bottom',
  156. 156 : xtype: 'toolbar',
  157. 157 : overflowHandler: 'scroller',
  158. 158 : items: [{
  159. 159 : xtype: 'querysearchfield'
  160. 160 : }, {
  161. 161 : xtype: 'totalpropertystatus'
  162. 162 : }, this.localize('context'), {
  163. 163 : xtype: 'slider',
  164. 164 : minValue: 5,
  165. 165 : value: 5,
  166. 166 : maxValue: 50,
  167. 167 : increment: 5,
  168. 168 : width: 50,
  169. 169 : listeners: {
  170. 170 : render: function(slider) {
  171. 171 : slider.setValue(me.getApiParam('context'));
  172. 172 : },
  173. 173 : changecomplete: function(slider, newValue) {
  174. 174 : me.setApiParam("context", slider.getValue());
  175. 175 : me.getStore().clearAndLoad({params: me.getApiParams()});
  176. 176 : }
  177. 177 : }
  178. 178 : }, this.localize('expand'), {
  179. 179 : xtype: 'slider',
  180. 180 : minValue: 5,
  181. 181 : value: 5,
  182. 182 : maxValue: 500,
  183. 183 : increment: 10,
  184. 184 : width: 50,
  185. 185 : listeners: {
  186. 186 : render: function(slider) {
  187. 187 : slider.setValue(me.getApiParam('expand'));
  188. 188 : },
  189. 189 : changecomplete: function(slider, newValue) {
  190. 190 : me.setApiParam('expand', newValue);
  191. 191 : var view = me.getView();
  192. 192 : var recordsExpanded = me.plugins[0].recordsExpanded;
  193. 193 : var store = view.getStore();
  194. 194 : for (var id in recordsExpanded) {
  195. 195 : var record = store.getByInternalId(id);
  196. 196 : var row = view.getRow(record);
  197. 197 : var expandRow = row.parentNode.childNodes[1];
  198. 198 : if (recordsExpanded[id]) {
  199. 199 : view.fireEvent("expandbody", row, record, expandRow, {force: true});
  200. 200 : } else {
  201. 201 : Ext.fly(expandRow).down('.x-grid-rowbody').setHtml('');
  202. 202 : }
  203. 203 : }
  204. 204 : }
  205. 205 : }
  206. 206 : },{
  207. 207 : xtype: 'corpusdocumentselector'
  208. 208 : }]
  209. 209 : }],
  210. 210 : columns: [{
  211. 211 : text: this.localize("document"),
  212. 212 : tooltip: this.localize("documentTip"),
  213. 213 : width: 'autoSize',
  214. 214 : dataIndex: 'docIndex',
  215. 215 : sortable: true,
  216. 216 : renderer: function (value, metaData, record, rowIndex, colIndex, store) {
  217. 217 : return store.getCorpus().getDocument(value).getTitle();
  218. 218 : }
  219. 219 : },{
  220. 220 : text: this.localize("left"),
  221. 221 : tooltip: this.localize("leftTip"),
  222. 222 : align: 'right',
  223. 223 : dataIndex: 'left',
  224. 224 : sortable: true,
  225. 225 : flex: 1
  226. 226 : },{
  227. 227 : text: this.localize("term"),
  228. 228 : tooltip: this.localize("termTip"),
  229. 229 : dataIndex: 'term',
  230. 230 : sortable: true,
  231. 231 : width: 'autoSize',
  232. 232 : xtype: 'coloredtermfield'
  233. 233 : },{
  234. 234 : text: this.localize("right"),
  235. 235 : tooltip: this.localize("rightTip"),
  236. 236 : dataIndex: 'right',
  237. 237 : sortable: true,
  238. 238 : flex: 1
  239. 239 : },{
  240. 240 : text: this.localize("position"),
  241. 241 : tooltip: this.localize("positionTip"),
  242. 242 : dataIndex: 'position',
  243. 243 : sortable: true,
  244. 244 : hidden: true,
  245. 245 : flex: 1
  246. 246 : }],
  247. 247 : listeners: {
  248. 248 : scope: this,
  249. 249 : corpusSelected: function() {
  250. 250 : if (this.getStore().getCorpus()) {
  251. 251 : this.setApiParams({docId: undefined, docIndex: undefined})
  252. 252 : this.getStore().clearAndLoad()
  253. 253 : }
  254. 254 : },
  255. 255 :
  256. 256 : documentsSelected: function(src, docs) {
  257. 257 : var docIds = [];
  258. 258 : var corpus = this.getStore().getCorpus();
  259. 259 : docs.forEach(function(doc) {
  260. 260 : docIds.push(corpus.getDocument(doc).getId())
  261. 261 : }, this);
  262. 262 : this.setApiParams({docId: docIds, docIndex: undefined})
  263. 263 : this.getStore().clearAndLoad()
  264. 264 : },
  265. 265 :
  266. 266 : documentSegmentTermClicked: {
  267. 267 : fn: function(src, documentSegmentTerm) {
  268. 268 : if (!documentSegmentTerm.term) {return;}
  269. 269 : params = {query: documentSegmentTerm.term};
  270. 270 : if (documentSegmentTerm.docId) {
  271. 271 : params.docId = documentSegmentTerm.docId;
  272. 272 : }
  273. 273 : else {
  274. 274 : // default to first document
  275. 275 : params.docIndex = documentSegmentTerm.docIndex ? documentSegmentTerm.docIndex : 0;
  276. 276 : }
  277. 277 : this.setApiParams(params);
  278. 278 : if (this.isVisible()) {
  279. 279 : this.getStore().clearAndLoad()
  280. 280 : }
  281. 281 : },
  282. 282 : scope: this
  283. 283 : },
  284. 284 : documentIndexTermsClicked: {
  285. 285 : fn: function(src, documentIndexTerms) {
  286. 286 : // this isn't quite right, since we want every term associated with a docIndex, but for now it will do
  287. 287 : var queriesHash = {};
  288. 288 : var queries = [];
  289. 289 : var docIndexHash = {};
  290. 290 : var docIndex = [];
  291. 291 : documentIndexTerms.forEach(function(item) {
  292. 292 : if (!queriesHash[item.term]) {
  293. 293 : queries.push(item.term);
  294. 294 : queriesHash[item.term]=true;
  295. 295 : }
  296. 296 : if (!docIndexHash[item.docIndex]) {
  297. 297 : docIndex.push(item.docIndex);
  298. 298 : docIndexHash[item.docIndex]=true;
  299. 299 : }
  300. 300 : });
  301. 301 : this.setApiParams({
  302. 302 : docId: undefined,
  303. 303 : docIndex: docIndex,
  304. 304 : query: queries
  305. 305 : });
  306. 306 : if (this.isVisible()) {
  307. 307 : this.getStore().clearAndLoad({params: this.getApiParams()});
  308. 308 : }
  309. 309 : },
  310. 310 : scope: this
  311. 311 : },
  312. 312 : afterrender: function(me) {
  313. 313 : me.getView().on('expandbody', function( rowNode, record, expandRow, eOpts ) {
  314. 314 : if (expandRow.textContent==="" || (eOpts && eOpts.force)) {
  315. 315 : var store = Ext.create("Voyant.data.store.Contexts", {
  316. 316 : stripTags: "all",
  317. 317 : corpus: me.getStore().getCorpus()
  318. 318 : });
  319. 319 : var data = record.getData();
  320. 320 : var query = data.query;
  321. 321 : if (query.match(/^[\^@]/) !== null) {
  322. 322 : query = data.term; // if it's a category query then use term instead
  323. 323 : }
  324. 324 : store.load({
  325. 325 : params: {
  326. 326 : query: query,
  327. 327 : docIndex: data.docIndex,
  328. 328 : position: data.position,
  329. 329 : limit: 1,
  330. 330 : context: me.getApiParam('expand')
  331. 331 : },
  332. 332 : callback: function(records, operation, success) {
  333. 333 : if (success && records.length==1) {
  334. 334 : data = records[0].getData();
  335. 335 : Ext.fly(operation.expandRow).down('.x-grid-rowbody').setHtml(data.left + " <span class='word keyword'>" + data.middle + "</span> " + data.right);
  336. 336 : }
  337. 337 : },
  338. 338 : expandRow: expandRow
  339. 339 : });
  340. 340 :
  341. 341 : }
  342. 342 : });
  343. 343 : }
  344. 344 :
  345. 345 : }
  346. 346 : });
  347. 347 :
  348. 348 : me.on("loadedCorpus", function(src, corpus) {
  349. 349 : if (this.hasCorpusAccess(corpus)==false) {
  350. 350 : this.mask(this.localize('limitedAccess'), 'mask-no-spinner');
  351. 351 : }
  352. 352 : else {
  353. 353 : var query = Ext.Array.from(this.getApiParam("query"));
  354. 354 : if (query.length > 0 && query[0].match(/^[\^@]/) !== null) {
  355. 355 : // query is a category so just load
  356. 356 : this.getStore().clearAndLoad({params: this.getApiParams()});
  357. 357 : } else {
  358. 358 : var corpusTerms = corpus.getCorpusTerms({autoLoad: false});
  359. 359 : corpusTerms.load({
  360. 360 : callback: function(records, operation, success) {
  361. 361 : if (success && records.length>0) {
  362. 362 : this.setApiParam("query", [records[0].getTerm()]);
  363. 363 : this.getStore().clearAndLoad({params: this.getApiParams()});
  364. 364 : }
  365. 365 : },
  366. 366 : scope: me,
  367. 367 : params: {
  368. 368 : limit: 1,
  369. 369 : query: query,
  370. 370 : stopList: this.getApiParam("stopList"),
  371. 371 : forTool: 'contexts'
  372. 372 : }
  373. 373 : });
  374. 374 : }
  375. 375 : }
  376. 376 : });
  377. 377 :
  378. 378 : me.on("query", function(src, query) {
  379. 379 : this.setApiParam('query', query);
  380. 380 : this.getStore().clearAndLoad({params: this.getApiParams()});
  381. 381 : }, me);
  382. 382 :
  383. 383 : me.on("documentTermsClicked", function(src, documentTerms) {
  384. 384 : var documentIndexTerms = [];
  385. 385 : documentTerms.forEach(function(documentTerm) {
  386. 386 : documentIndexTerms.push({
  387. 387 : term: documentTerm.getTerm(),
  388. 388 : docIndex: documentTerm.getDocIndex()
  389. 389 : });
  390. 390 : });
  391. 391 : this.fireEvent("documentIndexTermsClicked", this, documentIndexTerms);
  392. 392 : });
  393. 393 :
  394. 394 : me.on("termsClicked", function(src, terms) {
  395. 395 : var documentIndexTerms = [];
  396. 396 : if (Ext.isString(terms)) {terms = [terms];}
  397. 397 : terms.forEach(function(term) {
  398. 398 : if (term.docIndex !== undefined) {
  399. 399 : documentIndexTerms.push({
  400. 400 : term: term.term,
  401. 401 : docIndex: term.docIndex
  402. 402 : });
  403. 403 : }
  404. 404 : });
  405. 405 : if (documentIndexTerms.length > 0) {
  406. 406 : this.fireEvent("documentIndexTermsClicked", this, documentIndexTerms);
  407. 407 : }
  408. 408 : });
  409. 409 :
  410. 410 : me.callParent(arguments);
  411. 411 : }
  412. 412 :
  413. 413 : });