1/** 2 * @defgroup js_controllers_grid 3 */ 4/** 5 * @file js/controllers/grid/GridHandler.js 6 * 7 * Copyright (c) 2013-2019 Simon Fraser University 8 * Copyright (c) 2000-2019 John Willinsky 9 * Distributed under the GNU GPL v2. For full terms see the file docs/COPYING. 10 * 11 * @class GridHandler 12 * @ingroup js_controllers_grid 13 * 14 * @brief Grid row handler. 15 */ 16(function($) { 17 18 // Define the namespace. 19 $.pkp.controllers.grid = $.pkp.controllers.grid || {}; 20 21 22 23 /** 24 * @constructor 25 * 26 * @extends $.pkp.classes.Handler 27 * 28 * @param {jQueryObject} $grid The grid this handler is 29 * attached to. 30 * @param {{features}} options Grid handler configuration. 31 */ 32 $.pkp.controllers.grid.GridHandler = function($grid, options) { 33 this.parent($grid, options); 34 35 // We give a chance for this handler to initialize 36 // before we initialize its features. 37 this.initialize(options); 38 39 this.initFeatures_(options.features); 40 }; 41 $.pkp.classes.Helper.inherits($.pkp.controllers.grid.GridHandler, 42 $.pkp.classes.Handler); 43 44 45 // 46 // Private properties 47 // 48 /** 49 * The id of the grid. 50 * @private 51 * @type {?string} 52 */ 53 $.pkp.controllers.grid.GridHandler.prototype.gridId_ = null; 54 55 56 /** 57 * The URL to fetch a grid row. 58 * @private 59 * @type {?string} 60 */ 61 $.pkp.controllers.grid.GridHandler.prototype.fetchRowUrl_ = null; 62 63 64 /** 65 * The URL to fetch the entire grid. 66 * @private 67 * @type {?string} 68 */ 69 $.pkp.controllers.grid.GridHandler.prototype.fetchGridUrl_ = null; 70 71 72 /** 73 * The selector for the grid body. 74 * @private 75 * @type {?string} 76 */ 77 $.pkp.controllers.grid.GridHandler.prototype.bodySelector_ = null; 78 79 80 /** 81 * This grid features. 82 * @private 83 * @type {Object} 84 */ 85 $.pkp.controllers.grid.GridHandler.prototype.features_ = null; 86 87 88 // 89 // Public methods 90 // 91 /** 92 * Get the fetch row URL. 93 * @return {?string} URL to the "fetch row" operation handler. 94 */ 95 $.pkp.controllers.grid.GridHandler.prototype.getFetchRowUrl = 96 function() { 97 98 return this.fetchRowUrl_; 99 }; 100 101 102 /** 103 * Get all grid rows. 104 * 105 * @return {jQueryObject} The rows as a JQuery object. 106 */ 107 $.pkp.controllers.grid.GridHandler.prototype.getRows = 108 function() { 109 return $('.gridRow', this.getHtmlElement()); 110 }; 111 112 113 /** 114 * Get the id prefix of this grid. 115 * @return {string} The ID prefix of this grid. 116 */ 117 $.pkp.controllers.grid.GridHandler.prototype.getGridIdPrefix = 118 function() { 119 return 'component-' + this.gridId_; 120 }; 121 122 123 /** 124 * Get the id prefix of this grid row. 125 * @return {string} The id prefix of this grid row. 126 */ 127 $.pkp.controllers.grid.GridHandler.prototype.getRowIdPrefix = 128 function() { 129 return this.getGridIdPrefix() + '-row-'; 130 }; 131 132 133 /** 134 * Get the data element id of the passed grid row. 135 * @param {jQueryObject} $gridRow The grid row JQuery object. 136 * @return {string|undefined} The data element id of the passed grid row. 137 */ 138 $.pkp.controllers.grid.GridHandler.prototype.getRowDataId = 139 function($gridRow) { 140 141 var gridRowHtmlClasses, rowDataIdPrefix, index, rowDataId, 142 startExtractPosition; 143 144 gridRowHtmlClasses = $gridRow.attr('class').split(' '); 145 rowDataIdPrefix = 'element'; 146 for (index in gridRowHtmlClasses) { 147 startExtractPosition = gridRowHtmlClasses[index] 148 .indexOf(rowDataIdPrefix); 149 if (startExtractPosition != -1) { 150 rowDataId = gridRowHtmlClasses[index].slice(rowDataIdPrefix.length); 151 break; 152 } 153 } 154 155 return rowDataId; 156 }; 157 158 159 /** 160 * Get the parent grid row of the passed element, if any. 161 * @param {jQueryObject} $element The element that is inside the row. 162 * @return {jQueryObject} The element parent grid row. 163 */ 164 $.pkp.controllers.grid.GridHandler.prototype.getParentRow = 165 function($element) { 166 return $element.parents('.gridRow:first'); 167 }; 168 169 170 /** 171 * Get the same type elements of the passed element. 172 * @param {jQueryObject} $element The element to get the type from. 173 * @return {jQueryObject} The grid elements with the same type 174 * of the passed element. 175 */ 176 $.pkp.controllers.grid.GridHandler.prototype.getElementsByType = 177 function($element) { 178 if ($element.hasClass('gridRow')) { 179 var $container = $element.parents('tbody:first'); 180 return $('.gridRow', $container); 181 } 182 183 return null; 184 }; 185 186 187 /** 188 * Get the empty element based on the type of the passed element. 189 * @param {jQueryObject} $element The element to get the type from. 190 * @return {jQueryObject} The empty element. 191 */ 192 $.pkp.controllers.grid.GridHandler.prototype.getEmptyElement = 193 function($element) { 194 if ($element.hasClass('gridRow')) { 195 // Return the rows empty element placeholder. 196 var $container = $element.parents('tbody:first'); 197 return $container.next('.empty'); 198 } 199 200 return null; 201 }; 202 203 204 /** 205 * Show/hide row actions. 206 * 207 * @param {HTMLElement} sourceElement The element that 208 * issued the event. 209 */ 210 $.pkp.controllers.grid.GridHandler.prototype.toggleRowActions = 211 function(sourceElement) { 212 213 // Toggle the row actions. 214 var $controlRow = $(sourceElement).parents('tr').next('.row_controls'); 215 this.applyToggleRowActionEffect_($controlRow); 216 }; 217 218 219 /** 220 * Hide all visible row controls. 221 */ 222 $.pkp.controllers.grid.GridHandler.prototype.hideAllVisibleRowActions = 223 function() { 224 var $visibleControlRows, index, limit; 225 226 $visibleControlRows = $('.row_controls:visible', this.getHtmlElement()); 227 for (index = 0, limit = $visibleControlRows.length; index < limit; index++) { 228 this.applyToggleRowActionEffect_($($visibleControlRows[index])); 229 } 230 }; 231 232 233 /** 234 * Hide row actions div container. 235 */ 236 $.pkp.controllers.grid.GridHandler.prototype.hideRowActionsDiv = 237 function() { 238 var $rowActionDivs, index, limit, $div; 239 240 $rowActionDivs = $('.gridRow div.row_actions', this.getHtmlElement()); 241 $rowActionDivs.hide(); 242 243 // FIXME: Hack to correctly align the first column cell content after 244 // hiding the row actions div. 245 for (index = 0, limit = $rowActionDivs.length; index < limit; index++) { 246 $div = $($rowActionDivs[index]); 247 $div.parents('.row_container:first'). 248 attr('style', 'padding-left:0px !important'); 249 } 250 }; 251 252 253 /** 254 * Show row actions div container. 255 */ 256 $.pkp.controllers.grid.GridHandler.prototype.showRowActionsDiv = 257 function() { 258 var $rowActionDivs, index, limit, $div; 259 260 $rowActionDivs = $('.gridRow div.row_actions', this.getHtmlElement()); 261 $rowActionDivs.show(); 262 263 // FIXME: This is a hack. It removes the inline style that grid handler 264 // inserts in the row container when it hides the row actions div. 265 // See $.pkp.controllers.grid.GridHandler.prototype.hideRowActionsDiv 266 for (index = 0, limit = $rowActionDivs.length; index < limit; index++) { 267 $div = $($rowActionDivs[index]); 268 $div.parents('.row_container:first').removeAttr('style'); 269 } 270 }; 271 272 273 /** 274 * Enable/disable all link actions inside this grid. 275 * @param {boolean} enable Enable/disable flag. 276 * @param {jQueryObject} $linkElements Link elements JQuery object. 277 */ 278 $.pkp.controllers.grid.GridHandler.prototype.changeLinkActionsState = 279 function(enable, $linkElements) { 280 if ($linkElements === undefined) { 281 $linkElements = $('.pkp_controllers_linkAction', this.getHtmlElement()); 282 } 283 $linkElements.each(function() { 284 /** {$.pkp.controllers.LinkActionHandler} */ 285 var linkHandler; 286 linkHandler = $.pkp.classes.Handler.getHandler($(this)); 287 if (enable) { 288 linkHandler.enableLink(); 289 } else { 290 linkHandler.disableLink(); 291 } 292 }); 293 }; 294 295 296 /** 297 * Re-sequence all grid rows based on the passed sequence map. 298 * @param {Array} sequenceMap A sequence array with the row id as value. 299 */ 300 $.pkp.controllers.grid.GridHandler.prototype.resequenceRows = 301 function(sequenceMap) { 302 var rowId, index, $row; 303 for (index in sequenceMap) { 304 rowId = sequenceMap[index]; 305 $row = $('#' + rowId); 306 this.appendElement($row); 307 } 308 this.updateControlRowsPosition(); 309 }; 310 311 312 /** 313 * Move all grid control rows to their correct position, 314 * below of each correspondent data grid row. 315 */ 316 $.pkp.controllers.grid.GridHandler.prototype.updateControlRowsPosition = 317 function() { 318 var $rows, index, limit, $row, $controlRow; 319 320 $rows = this.getRows(); 321 for (index = 0, limit = $rows.length; index < limit; index++) { 322 $row = $($rows[index]); 323 $controlRow = this.getControlRowByGridRow($row); 324 if ($controlRow.length > 0) { 325 $controlRow.insertAfter($row); 326 } 327 } 328 }; 329 330 331 // 332 // Protected methods 333 // 334 /** 335 * Set data and execute operations to initialize. 336 * 337 * @protected 338 * 339 * @param {Object} options Grid options. 340 */ 341 $.pkp.controllers.grid.GridHandler.prototype.initialize = 342 function(options) { 343 // Bind the handler for the "elements changed" event. 344 this.bind('dataChanged', this.refreshGridHandler); 345 346 // Bind the handler for the "add new row" event. 347 this.bind('addRow', this.addRowHandler_); 348 349 // Handle grid filter events. 350 this.bind('formSubmitted', this.refreshGridWithFilterHandler_); 351 352 // Save the ID of this grid. 353 this.gridId_ = options.gridId; 354 355 // Save the URL to fetch a row. 356 this.fetchRowUrl_ = options.fetchRowUrl; 357 358 // Save the URL to fetch the entire grid 359 this.fetchGridUrl_ = options.fetchGridUrl; 360 361 // Save the selector for the grid body. 362 this.bodySelector_ = options.bodySelector; 363 364 // Show/hide row action feature. 365 this.activateRowActions_(); 366 367 this.trigger('gridInitialized'); 368 }; 369 370 371 /** 372 * Call features hooks. 373 * 374 * @protected 375 * 376 * @param {string} hookName The name of the hook. 377 * @param {Array|jQueryObject} args The arguments array. 378 */ 379 $.pkp.controllers.grid.GridHandler.prototype.callFeaturesHook = 380 function(hookName, args) { 381 var featureName; 382 if (!$.isArray(args)) { 383 args = [args]; 384 } 385 for (featureName in this.features_) { 386 this.features_[featureName][hookName]. 387 apply(this.features_[featureName], args); 388 } 389 }; 390 391 392 /** 393 * Refresh either a single row of the grid or the whole grid. 394 * 395 * @protected 396 * 397 * @param {HTMLElement} sourceElement The element that 398 * issued the event. 399 * @param {Event} event The triggering event. 400 * @param {number=} opt_elementId The id of a data element that was 401 * updated, added or deleted. If not given then the whole grid 402 * will be refreshed. 403 * @param {Boolean=} opt_fetchedAlready Flag that subclasses can send 404 * telling that a fetch operation was already handled there. 405 */ 406 /*jslint unparam: true*/ 407 $.pkp.controllers.grid.GridHandler.prototype.refreshGridHandler = 408 function(sourceElement, event, opt_elementId, opt_fetchedAlready) { 409 410 // Check if subclasses already handled the fetch of new elements. 411 if (!opt_fetchedAlready) { 412 if (opt_elementId) { 413 // Retrieve a single row from the server. 414 $.get(this.fetchRowUrl_, {rowId: opt_elementId}, 415 this.callbackWrapper(this.replaceElementResponseHandler_), 'json'); 416 } else { 417 // Retrieve the whole grid from the server. 418 $.get(this.fetchGridUrl_, null, 419 this.callbackWrapper(this.replaceGridResponseHandler_), 'json'); 420 } 421 } 422 423 // Let the calling context (page?) know that the grids are being redrawn. 424 this.trigger('gridRefreshRequested'); 425 this.publishChangeEvents(); 426 }; 427 /*jslint unparam: false*/ 428 429 430 /** 431 * Delete a grid element. 432 * 433 * @protected 434 * 435 * @param {jQueryObject} $element The element to be deleted. 436 */ 437 $.pkp.controllers.grid.GridHandler.prototype.deleteElement = 438 function($element) { 439 var lastElement, $emptyElement; 440 441 // Check whether we really only match one element. 442 if ($element.length !== 1) { 443 throw new Error('There were ' + $element.length + 444 ' rather than 1 element to delete!'); 445 } 446 447 // Check whether this is the last row. 448 lastElement = false;
449 if (this.getElementsByType($element).length == 1) { 450 lastElement = true; 451 } 452 453 // Remove the controls row, if any. 454 if ($element.hasClass('gridRow')) { 455 this.deleteControlsRow_($element); 456 } 457 458 $emptyElement = this.getEmptyElement($element); 459 $element.fadeOut(500, function() { 460 $(this).remove(); 461 if (lastElement) { 462 $emptyElement.fadeIn(500); 463 } 464 }); 465 }; 466 467 468 /** 469 * Append a new row to the end of the list. 470 * 471 * @protected 472 * 473 * @param {jQueryObject} $newRow The new row to append. 474 * @param {jQueryObject=} opt_$gridBody The tbody container element. 475 */ 476 $.pkp.controllers.grid.GridHandler.prototype.appendElement = 477 function($newRow, opt_$gridBody) { 478 479 if (opt_$gridBody === undefined) { 480 opt_$gridBody = this.getHtmlElement().find(this.bodySelector_); 481 } 482 483 // Add the new element. 484 opt_$gridBody.append($newRow); 485 486 // Hide the empty placeholder. 487 var $emptyElement = this.getEmptyElement($newRow); 488 $emptyElement.hide(); 489 490 this.callFeaturesHook('appendElement', $newRow); 491 }; 492 493 494 /** 495 * Update an existing element using the passed new element content. 496 * 497 * @protected 498 * 499 * @param {jQueryObject} $existingElement The element that is already 500 * in grid. 501 * @param {jQueryObject} $newElement The element with new content. 502 */ 503 $.pkp.controllers.grid.GridHandler.prototype.replaceElement = 504 function($existingElement, $newElement) { 505 506 if ($newElement.hasClass('gridRow')) { 507 this.deleteControlsRow_($existingElement); 508 } 509 510 $existingElement.replaceWith($newElement); 511 this.callFeaturesHook('replaceElement', $newElement); 512 }; 513 514 515 /** 516 * Does the passed row have a different number of columns than the 517 * existing grid? 518 * 519 * @protected 520 * 521 * @param {jQueryObject} $row The row to be checked against grid columns. 522 * @param {Boolean=} opt_checkColSpan Will get the number of row columns 523 * by column span. 524 * @return {boolean} Whether it has the same number of grid columns 525 * or not. 526 */ 527 $.pkp.controllers.grid.GridHandler.prototype.hasSameNumOfColumns = 528 function($row, opt_checkColSpan) { 529 var $grid, numColumns, $tdElements, numCellsInNewRow; 530 531 $grid = this.getHtmlElement(); 532 numColumns = $grid.find('th').length; 533 $tdElements = $row.first().find('td'); 534 if (opt_checkColSpan) { 535 numCellsInNewRow = $tdElements.attr('colspan'); 536 } else { 537 numCellsInNewRow = $tdElements.length; 538 } 539 540 return (numColumns == numCellsInNewRow); 541 }; 542 543 544 // 545 // Private methods 546 // 547 /** 548 * Refresh the grid after its filter has changed. 549 * 550 * @private 551 * 552 * @param {$.pkp.controllers.form.ClientFormHandler} filterForm 553 * The filter form. 554 * @param {Event} event A "formSubmitted" event. 555 * @param {string} filterData Serialized filter data. 556 */ 557 /*jslint unparam: true*/ 558 $.pkp.controllers.grid.GridHandler.prototype.refreshGridWithFilterHandler_ = 559 function(filterForm, event, filterData) { 560 561 // Retrieve the grid from the server and add the 562 // filter data as form data. 563 $.post(this.fetchGridUrl_, filterData, 564 this.callbackWrapper(this.replaceGridResponseHandler_), 'json'); 565 }; 566 /*jslint unparam: false*/ 567 568 569 /** 570 * Add a new row to the grid. 571 * 572 * @private 573 * 574 * @param {HTMLElement} sourceElement The element that 575 * issued the event. 576 * @param {Event} event The triggering event. 577 * @param {Object} params The request parameters to use to generate 578 * the new row. 579 */ 580 /*jslint unparam: true*/ 581 $.pkp.controllers.grid.GridHandler.prototype.addRowHandler_ = 582 function(sourceElement, event, params) { 583 584 // Retrieve a single new row from the server. 585 $.get(this.fetchRowUrl_, params, 586 this.callbackWrapper(this.replaceElementResponseHandler_), 'json'); 587 }; 588 /*jslint unparam: false*/ 589 590 591 /** 592 * Callback to insert, remove or replace a row after an 593 * element has been inserted, update or deleted. 594 * 595 * @private 596 * 597 * @param {Object} ajaxContext The AJAX request context. 598 * @param {Object} jsonData A parsed JSON response object. 599 * @return {boolean|undefined} Return false when no replace action is taken. 600 */ 601 /*jslint unparam: true*/ 602 $.pkp.controllers.grid.GridHandler.prototype.replaceElementResponseHandler_ = 603 function(ajaxContext, jsonData) { 604 var elementId, $element, handledJsonData; 605 606 handledJsonData = this.handleJson(jsonData); 607 if (handledJsonData !== false) { 608 if (handledJsonData.elementNotFound) { 609 // The server reported that this element no 610 // longer exists in the database so let's 611 // delete it. 612 elementId = handledJsonData.elementNotFound; 613 $element = this.getHtmlElement(). 614 find('.element' + elementId); 615 616 // Sometimes we get a delete event before the 617 // element has actually been inserted (e.g. when deleting 618 // elements due to a cancel action or similar). 619 if ($element.length === 0) { 620 return false;
621 } 622 623 this.deleteElement($element); 624 } else { 625 // The server returned mark-up to replace 626 // or insert the row. 627 this.insertOrReplaceElement_(handledJsonData.content); 628 629 // Refresh row action event binding. 630 this.activateRowActions_(); 631 } 632 } 633 }; 634 /*jslint unparam: false*/ 635 636 637 /** 638 * Callback to replace a grid's content. 639 * 640 * @private 641 * 642 * @param {Object} ajaxContext The AJAX request context. 643 * @param {Object} jsonData A parsed JSON response object. 644 */ 645 /*jslint unparam: true*/ 646 $.pkp.controllers.grid.GridHandler.prototype.replaceGridResponseHandler_ = 647 function(ajaxContext, jsonData) { 648 var handledJsonData, $grid; 649 650 handledJsonData = this.handleJson(jsonData); 651 if (handledJsonData !== false) { 652 // Get the grid that we're updating 653 $grid = this.getHtmlElement(); 654 655 // Replace the grid content 656 $grid.replaceWith(handledJsonData.content); 657 658 // Refresh row action event binding. 659 this.activateRowActions_(); 660 } 661 }; 662 /*jslint unparam: false*/ 663 664 665 /** 666 * Helper that inserts or replaces an element. 667 * 668 * @private 669 * 670 * @param {string} elementContent The new mark-up of the element. 671 */ 672 $.pkp.controllers.grid.GridHandler.prototype.insertOrReplaceElement_ = 673 function(elementContent) { 674 var $newElement, newElementId, $grid, $existingElement; 675 676 // Parse the HTML returned from the server. 677 $newElement = $(elementContent); 678 newElementId = $newElement.attr('id'); 679 680 // Does the element exist already? 681 $grid = this.getHtmlElement(); 682 $existingElement = newElementId ? $grid.find('#' + newElementId) : null; 683 684 if ($existingElement !== null && $existingElement.length > 1) { 685 throw new Error('There were ' + $existingElement.length + 686 ' rather than 0 or 1 elements to be replaced!'); 687 } 688 689 if (!this.hasSameNumOfColumns($newElement)) { 690 // Redraw the whole grid so new columns 691 // get added/removed to match element. 692 $.get(this.fetchGridUrl_, null, 693 this.callbackWrapper(this.replaceGridResponseHandler_), 'json'); 694 } else { 695 if ($existingElement !== null && $existingElement.length === 1) { 696 // Update element. 697 this.replaceElement($existingElement, $newElement); 698 } else { 699 // Insert row. 700 this.appendElement($newElement); 701 } 702 } 703 }; 704 705 706 /** 707 * Helper that deletes the row of controls (if present). 708 * 709 * @private 710 * 711 * @param {jQueryObject} $row The row whose matching control row should be 712 * deleted. 713 */ 714 $.pkp.controllers.grid.GridHandler.prototype.deleteControlsRow_ = 715 function($row) { 716 var $controlRow = $('#' + $row.attr('id') + '-control-row', 717 this.getHtmlElement()); 718 719 if ($controlRow.is('tr') && $controlRow.hasClass('row_controls')) { 720 $controlRow.remove(); 721 } 722 }; 723 724 725 /** 726 * Get the control row for the passed the grid row. 727 * 728 * @param {jQueryObject} $gridRow The grid row JQuery object. 729 * @return {jQueryObject} The control row JQuery object. 730 */ 731 $.pkp.controllers.grid.GridHandler.prototype.getControlRowByGridRow = 732 function($gridRow) { 733 var rowId, controlRowId; 734 735 rowId = $gridRow.attr('id'); 736 controlRowId = rowId + '-control-row'; 737 return $('#' + controlRowId); 738 }; 739 740 741 /** 742 * Helper that attaches click events to row actions. 743 * 744 * @private 745 */ 746 $.pkp.controllers.grid.GridHandler.prototype.activateRowActions_ = 747 function() { 748 749 var $grid = this.getHtmlElement(); 750 $grid.find('a.settings').unbind('click').bind('click', 751 this.callbackWrapper(this.toggleRowActions)); 752 }; 753 754 755 /** 756 * Apply the effect for hide/show row actions. 757 * 758 * @private 759 * 760 * @param {jQueryObject} $controlRow The control row JQuery object. 761 */ 762 $.pkp.controllers.grid.GridHandler.prototype.applyToggleRowActionEffect_ = 763 function($controlRow) { 764 var delay, $row; 765 766 // FIXME #7582: IE8 and Safari don't work well with delay to show 767 // or hide the control grid rows. 768 delay = 0; 769 770 $row = $controlRow.prev().find('td:not(.indent_row)'); 771 $row = $row.add($controlRow.prev()); 772 if ($controlRow.is(':visible')) { 773 setTimeout(function() { 774 $row.removeClass('no_border'); 775 }, delay); 776 $controlRow.hide(delay); 777 } else { 778 $row.addClass('no_border'); 779 $controlRow.show(delay); 780 } 781 }; 782 783 784 /** 785 * Add a grid feature. 786 * 787 * @private 788 * 789 * @param {string} id Feature id. 790 * @param {$.pkp.classes.features.Feature} $feature The grid 791 * feature to be added. 792 */ 793 $.pkp.controllers.grid.GridHandler.prototype.addFeature_ = 794 function(id, $feature) { 795 if (!this.features_) { 796 this.features_ = []; 797 } 798 this.features_[id] = $feature; 799 }; 800 801 802 /** 803 * Add grid features. 804 * 805 * @private 806 * 807 * @param {Array.<{JSClass, options}>} features The features options array.
808 */ 809 $.pkp.controllers.grid.GridHandler.prototype.initFeatures_ = 810 function(features) { 811 812 var id, $feature; 813 for (id in features) { 814 $feature = 815 /** @type {$.pkp.classes.features.Feature} */ 816 ($.pkp.classes.Helper.objectFactory( 817 features[id].JSClass, 818 [this, features[id].options])); 819 820 this.addFeature_(id, $feature); 821 this.features_[id].init(); 822 } 823 }; 824 825 826/** @param {jQuery} $ jQuery closure. */ 827}(jQuery));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.