1/***********************************************************/ 2/* */ 3/* Plataforma e-ducativa - Argentina */ 4/* */ 5/* Copyright (c) 2026 de e-ducativa Educación Virtual S.A. */ 6/* */ 7/***********************************************************/ 8/** 9 * DoubleList - Componente vanilla JS para seleccion de items entre dos listas 10 * 11 * Reemplazo de Educativa.Control.DoubleList sin dependencia de Prototype.js 12 * Estilo visual compatible con DynList 13 * 14 * @module Educativa/Modules/Components/DoubleList 15 * Copyright (c) 2026 Educativa 16 */ 17 18export class DoubleList { 19 /** 20 * @typedef {Object} DoubleListOptions 21 * @property {string} container - ID del elemento contenedor 22 * @property {string} id - ID base para las listas 23 * @property {Array} [items=[]] - Items disponibles [{value, title, class?, position?}] 24 * @property {Array} [selected=[]] - Items preseleccionados 25 * @property {string} [titleLeft] - Titulo lista izquierda 26 * @property {string} [titleRight] - Titulo lista derecha 27 * @property {boolean} [showSearch=false] - Mostrar buscador 28 * @property {boolean} [showInactiveFilter=false] - Mostrar filtro de inactivos 29 * @property {string} [inactiveFilterLabel] - Texto del filtro de inactivos 30 * @property {number} [maxAllowed] - Maximo de items seleccionables 31 * @property {Function} [onChange] - Callback cuando cambia seleccion 32 */ 33 34 /** 35 * @param {DoubleListOptions} args - Opciones de configuracion 36 */ 37 constructor( args ) { 38 this.container = document.getElementById( args.container ); 39 this.id = args.id; 40 this.maxAllowed = args.maxAllowed || null; 41 this.onChange = args.onChange || null; 42 this.showSearch = args.showSearch || false; 43 this.showInactiveFilter = args.showInactiveFilter || false; 44 this.inactiveFilterLabel = args.inactiveFilterLabel || 'Mostrar inactivos'; 45 46 this.titleLeft = args.titleLeft || this._getTerm( 'DISPONIBLES' ); 47 this.titleRight = args.titleRight || this._getTerm( 'SELECCIONADOS' ); 48 49 // Listas internas 50 this.leftItems = []; 51 this.rightItems = []; 52 53 // Elementos DOM 54 this.leftList = null; 55 this.rightList = null; 56 this.leftSearch = null; 57 this.rightSearch = null; 58 59 this._render(); 60 61 this._loadItems( args.items || [], args.selected || [] ); 62 this._bindEvents(); 63 } 64 65 /** 66 * Obtiene termino del diccionario si existe 67 * @param {string} key - Clave del termino 68 * @returns {string} Termino traducido o la clave 69 */ 70 _getTerm( key ) { 71 if ( typeof Dict !== 'undefined' && Dict.termcap ) { 72 return Dict.termcap( key ); 73 } 74 return key; 75 } 76 77 /** 78 * Renderiza la estructura HTML del componente 79 */ 80 _render() { 81 this.container.innerHTML = ''; 82 this.container.classList.add( 'double_list_wrapper' ); 83 84 // Lista izquierda 85 const leftContainer = document.createElement( 'div' ); 86 leftContainer.className = 'double_list_container double_list_left'; 87 leftContainer.id = `${this.id}_left`; 88 89 const leftLabel = document.createElement( 'span' ); 90 leftLabel.className = 'double_list_label'; 91 leftLabel.textContent = this.titleLeft; 92 leftContainer.appendChild( leftLabel ); 93 94 if ( this.showSearch ) { 95 const searchContainer = this._createSearchInput( 'left' ); 96 leftContainer.appendChild( searchContainer ); 97 } 98 99 this.leftList = document.createElement( 'ul' ); 100 this.leftList.className = 'double_list_items'; 101 this.leftList.tabIndex = 0; 102 leftContainer.appendChild( this.leftList ); 103 104 if ( this.showInactiveFilter ) { 105 const filterContainer = this._createInactiveFilter(); 106 leftContainer.appendChild( filterContainer ); 107 } 108 109 this.leftNotFound = document.createElement( 'span' ); 110 this.leftNotFound.className = 'double_list_not_found double_list_hidden'; 111 this.leftNotFound.textContent = this._getTerm( 'NO_EXISTEN_RESULTADOS_BUSQUEDA' ); 112 leftContainer.appendChild( this.leftNotFound ); 113 114 this.container.appendChild( leftContainer ); 115 116 // Botones centrales 117 const btnContainer = this._createButtons(); 118 this.container.appendChild( btnContainer ); 119 120 // Lista derecha 121 const rightContainer = document.createElement( 'div' ); 122 rightContainer.className = 'double_list_container double_list_right';
123 rightContainer.id = this.id; 124 125 this.rightLabel = document.createElement( 'span' ); 126 this.rightLabel.className = 'double_list_label'; 127 this._updateRightLabel(); 128 rightContainer.appendChild( this.rightLabel ); 129 130 if ( this.showSearch ) { 131 const searchContainer = this._createSearchInput( 'right' ); 132 rightContainer.appendChild( searchContainer ); 133 } 134 135 this.rightList = document.createElement( 'ul' ); 136 this.rightList.className = 'double_list_items'; 137 this.rightList.tabIndex = 0; 138 rightContainer.appendChild( this.rightList ); 139 140 this.rightNotFound = document.createElement( 'span' ); 141 this.rightNotFound.className = 'double_list_not_found double_list_hidden'; 142 this.rightNotFound.textContent = this._getTerm( 'NO_EXISTEN_RESULTADOS_BUSQUEDA' ); 143 rightContainer.appendChild( this.rightNotFound ); 144 145 this.rightError = document.createElement( 'span' ); 146 this.rightError.className = 'double_list_error double_list_hidden'; 147 rightContainer.appendChild( this.rightError ); 148 149 this.container.appendChild( rightContainer ); 150 } 151 152 /** 153 * Crea el input de busqueda 154 * @param {string} side - 'left' o 'right' 155 * @returns {HTMLElement} Contenedor del buscador 156 */ 157 _createSearchInput( side ) { 158 const container = document.createElement( 'div' ); 159 container.className = 'double_list_search'; 160 161 const input = document.createElement( 'input' ); 162 input.type = 'text'; 163 input.className = 'double_list_search_input'; 164 input.placeholder = this._getTerm( 'BUSCAR' ) + '...'; 165 166 const clearBtn = document.createElement( 'button' ); 167 clearBtn.type = 'button'; 168 clearBtn.className = 'double_list_search_clear double_list_hidden'; 169 clearBtn.innerHTML = '<span class="fal fa-times"></span>'; 170 171 container.appendChild( input ); 172 container.appendChild( clearBtn ); 173 174 if ( side === 'left' ) { 175 this.leftSearch = input; 176 this.leftSearchClear = clearBtn; 177 } else { 178 this.rightSearch = input; 179 this.rightSearchClear = clearBtn; 180 } 181 182 return container; 183 } 184 185 /** 186 * Crea el filtro de items inactivos 187 * @returns {HTMLElement} Contenedor del filtro 188 */ 189 _createInactiveFilter() { 190 const container = document.createElement( 'div' ); 191 container.className = 'double_list_filter'; 192 193 const label = document.createElement( 'label' ); 194 195 this.inactiveCheckbox = document.createElement( 'input' ); 196 this.inactiveCheckbox.type = 'checkbox'; 197 198 const text = document.createElement( 'span' ); 199 text.textContent = this.inactiveFilterLabel; 200 201 label.appendChild( this.inactiveCheckbox ); 202 label.appendChild( text ); 203 container.appendChild( label ); 204 205 return container; 206 } 207 208 /** 209 * Crea los botones centrales 210 * @returns {HTMLElement} Contenedor de botones 211 */ 212 _createButtons() { 213 const container = document.createElement( 'div' ); 214 container.className = 'double_list_buttons'; 215 216 const btnRightAll = document.createElement( 'a' ); 217 btnRightAll.className = 'double_list_btn'; 218 btnRightAll.href = 'javascript:void(0)'; 219 btnRightAll.innerHTML = '<i class="far fa-angle-double-right"></i>'; 220 btnRightAll.title = this._getTerm( 'SELECCIONAR_TODOS' ); 221 222 const btnRight = document.createElement( 'a' ); 223 btnRight.className = 'double_list_btn'; 224 btnRight.href = 'javascript:void(0)'; 225 btnRight.innerHTML = '<i class="fal fa-long-arrow-right"></i>'; 226 btnRight.title = this._getTerm( 'SELECCIONAR' ); 227 228 const btnLeft = document.createElement( 'a' ); 229 btnLeft.className = 'double_list_btn'; 230 btnLeft.href = 'javascript:void(0)'; 231 btnLeft.innerHTML = '<i class="fal fa-long-arrow-left"></i>'; 232 btnLeft.title = this._getTerm( 'QUITAR' ); 233 234 const btnLeftAll = document.createElement( 'a' ); 235 btnLeftAll.className = 'double_list_btn'; 236 btnLeftAll.href = 'javascript:void(0)'; 237 btnLeftAll.innerHTML = '<i class="far fa-angle-double-left"></i>'; 238 btnLeftAll.title = this._getTerm( 'QUITAR_TODOS' ); 239 240 container.appendChild( btnRightAll ); 241 container.appendChild( btnRight ); 242 container.appendChild( btnLeft ); 243 container.appendChild( btnLeftAll ); 244 245 this.btnRightAll = btnRightAll; 246 this.btnRight = btnRight; 247 this.btnLeft = btnLeft; 248 this.btnLeftAll = btnLeftAll; 249 250 return container; 251 } 252 253 /** 254 * Carga los items iniciales 255 * @param {Array} available - Items disponibles 256 * @param {Array} selected - Items preseleccionados 257 */ 258 _loadItems( available, selected ) { 259 // Procesar seleccionados primero 260 selected.forEach( item => { 261 this.rightItems.push( item ); 262 this._renderItem( item, 'right' ); 263 } ); 264 265 // Procesar disponibles 266 available.forEach( item => { 267 this.leftItems.push( item ); 268 this._renderItem( item, 'left' ); 269 } ); 270 271 this._updateRightLabel(); 272 } 273 274 /**
275 * Renderiza un item en una lista 276 * @param {Object} item - Item a renderizar 277 * @param {string} side - 'left' o 'right' 278 */ 279 _renderItem( item, side ) { 280 const li = document.createElement( 'li' ); 281 li.className = 'double_list_item'; 282 if ( item.class ) { 283 li.classList.add( item.class ); 284 } 285 li.textContent = item.title; 286 li.dataset.value = item.value; 287 288 const input = document.createElement( 'input' ); 289 input.type = 'hidden'; 290 input.name = side === 'right' ? this.id : `${this.id}_left`; 291 input.value = item.value; 292 li.appendChild( input ); 293 294 item.element = li; 295 296 if ( side === 'left' ) { 297 this.leftList.appendChild( li ); 298 } else { 299 this.rightList.appendChild( li ); 300 } 301 } 302 303 /** 304 * Vincula los eventos 305 */ 306 _bindEvents() { 307 // Botones 308 this.btnRightAll.addEventListener( 'click', () => this._moveAllToRight() ); 309 this.btnRight.addEventListener( 'click', () => this._moveSelectedToRight() ); 310 this.btnLeft.addEventListener( 'click', () => this._moveSelectedToLeft() ); 311 this.btnLeftAll.addEventListener( 'click', () => this._moveAllToLeft() ); 312 313 // Doble click en items 314 this.leftList.addEventListener( 'dblclick', ( e ) => { 315 const li = e.target.closest( '.double_list_item' ); 316 if ( li ) this._moveItemToRight( li.dataset.value ); 317 } ); 318 319 this.rightList.addEventListener( 'dblclick', ( e ) => { 320 const li = e.target.closest( '.double_list_item' ); 321 if ( li ) this._moveItemToLeft( li.dataset.value ); 322 } ); 323 324 // Click para seleccionar 325 this.leftList.addEventListener( 'click', ( e ) => { 326 const li = e.target.closest( '.double_list_item' ); 327 if ( li ) this._selectItem( li, 'left' ); 328 } ); 329 330 this.rightList.addEventListener( 'click', ( e ) => { 331 const li = e.target.closest( '.double_list_item' ); 332 if ( li ) this._selectItem( li, 'right' ); 333 } ); 334 335 // Teclado 336 this.leftList.addEventListener( 'keydown', ( e ) => this._handleKeyboard( e, 'left' ) ); 337 this.rightList.addEventListener( 'keydown', ( e ) => this._handleKeyboard( e, 'right' ) ); 338 339 // Busqueda 340 if ( this.leftSearch ) { 341 this.leftSearch.addEventListener( 'input', () => this._filterList( 'left' ) ); 342 this.leftSearchClear.addEventListener( 'click', () => { 343 this.leftSearch.value = ''; 344 this._filterList( 'left' ); 345 } ); 346 } 347 348 if ( this.rightSearch ) { 349 this.rightSearch.addEventListener( 'input', () => this._filterList( 'right' ) ); 350 this.rightSearchClear.addEventListener( 'click', () => { 351 this.rightSearch.value = ''; 352 this._filterList( 'right' ); 353 } ); 354 } 355 356 // Filtro de inactivos 357 if ( this.inactiveCheckbox ) { 358 this.inactiveCheckbox.addEventListener( 'change', () => this._filterList( 'left' ) ); 359 window.onload = () => this._filterList( 'left' ); 360 } 361 } 362 363 /** 364 * Maneja eventos de teclado 365 * @param {KeyboardEvent} e - Evento de teclado 366 * @param {string} side - 'left' o 'right' 367 */ 368 _handleKeyboard( e, side ) { 369 const list = side === 'left' ? this.leftList : this.rightList; 370 const selected = list.querySelector( '.double_list_item_selected' ); 371 372 switch ( e.keyCode ) { 373 case 38: // Arriba 374 e.preventDefault(); 375 if ( selected && selected.previousElementSibling ) { 376 this._selectItem( selected.previousElementSibling, side ); 377 } 378 break; 379 case 40: // Abajo 380 e.preventDefault(); 381 if ( selected && selected.nextElementSibling ) { 382 this._selectItem( selected.nextElementSibling, side ); 383 } else if ( !selected ) { 384 const first = list.querySelector( '.double_list_item' ); 385 if ( first ) this._selectItem( first, side ); 386 } 387 break; 388 case 37: // Izquierda 389 if ( side === 'right' && selected ) { 390 this._moveItemToLeft( selected.dataset.value ); 391 } 392 break; 393 case 39: // Derecha 394 if ( side === 'left' && selected ) { 395 this._moveItemToRight( selected.dataset.value ); 396 } 397 break; 398 case 13: // Enter 399 if ( selected ) { 400 if ( side === 'left' ) { 401 this._moveItemToRight( selected.dataset.value ); 402 } else { 403 this._moveItemToLeft( selected.dataset.value ); 404 } 405 } 406 break; 407 } 408 } 409 410 /** 411 * Selecciona un item visualmente 412 * @param {HTMLElement} li - Elemento LI a seleccionar 413 * @param {string} side - 'left' o 'right' 414 */ 415 _selectItem( li, side ) {
416 const list = side === 'left' ? this.leftList : this.rightList; 417 418 // Deseleccionar anterior 419 const prev = list.querySelector( '.double_list_item_selected' ); 420 if ( prev ) prev.classList.remove( 'double_list_item_selected' ); 421 422 // Seleccionar nuevo 423 li.classList.add( 'double_list_item_selected' ); 424 li.scrollIntoView( { block: 'nearest' } ); 425 } 426 427 /** 428 * Obtiene el siguiente sibling visible (no oculto por filtros) 429 * @param {HTMLElement} element - Elemento actual 430 * @returns {HTMLElement|null} Siguiente elemento visible o null 431 */ 432 _getNextVisibleSibling( element ) { 433 let sibling = element.nextElementSibling; 434 while ( sibling ) { 435 if ( sibling.style.display !== 'none' 436 && sibling.classList.contains( 'double_list_item' ) ) { 437 return sibling; 438 } 439 sibling = sibling.nextElementSibling; 440 } 441 return null; 442 } 443 444 /** 445 * Obtiene el sibling anterior visible (no oculto por filtros) 446 * @param {HTMLElement} element - Elemento actual 447 * @returns {HTMLElement|null} Elemento anterior visible o null 448 */ 449 _getPrevVisibleSibling( element ) { 450 let sibling = element.previousElementSibling; 451 while ( sibling ) { 452 if ( sibling.style.display !== 'none' 453 && sibling.classList.contains( 'double_list_item' ) ) { 454 return sibling; 455 } 456 sibling = sibling.previousElementSibling; 457 } 458 return null; 459 } 460 461 /** 462 * Filtra items de una lista 463 * @param {string} side - 'left' o 'right' 464 */ 465 _filterList( side ) { 466 const items = side === 'left' ? this.leftItems : this.rightItems; 467 const search = side === 'left' ? this.leftSearch : this.rightSearch; 468 const clearBtn = side === 'left' ? this.leftSearchClear : this.rightSearchClear; 469 const notFound = side === 'left' ? this.leftNotFound : this.rightNotFound; 470 471 const searchValue = search ? search.value.toLowerCase() : ''; 472 const showInactive = this.inactiveCheckbox ? this.inactiveCheckbox.checked : true; 473 474 if ( clearBtn ) { 475 clearBtn.classList.toggle( 'double_list_hidden', !searchValue ); 476 } 477 478 let visibleCount = 0; 479 480 items.forEach( item => { 481 if ( !item.element ) return; 482 483 const matchesSearch = !searchValue || 484 this._normalizeString( item.title ).includes( this._normalizeString( searchValue ) ); 485 486 const isInactive = item.class && item.class.includes( 'inactivo' ); 487 const matchesFilter = showInactive || !isInactive; 488 489 const isVisible = matchesSearch && ( side === 'right' || matchesFilter ); 490 491 item.element.style.display = isVisible ? '' : 'none'; 492 if ( isVisible ) visibleCount++; 493 } ); 494 495 notFound.classList.toggle( 'double_list_hidden', visibleCount > 0 || !searchValue ); 496 } 497 498 /** 499 * Normaliza string removiendo acentos 500 * @param {string} str - String a normalizar 501 * @returns {string} String normalizado 502 */ 503 _normalizeString( str ) { 504 return str.toLowerCase() 505 .normalize( 'NFD' ) 506 .replace( /[\u0300-\u036f]/g, '' ); 507 } 508 509 /** 510 * Mueve un item a la derecha 511 * @param {string} value - Valor del item a mover 512 */ 513 _moveItemToRight( value ) { 514 const index = this.leftItems.findIndex( item => item.value == value ); 515 if ( index === -1 ) return; 516 517 const item = this.leftItems[ index ]; 518 519 // Verificar si esta oculto 520 if ( item.element && item.element.style.display === 'none' ) return; 521 522 // Verificar limite 523 if ( this.maxAllowed && this.rightItems.length >= this.maxAllowed ) { 524 this._showError( this._getTerm( 'NO_SE_PUEDEN_SELECCIONAR_MAS_DE_N_ELEMENTOS' ) 525 .replace( '#{cant}', this.maxAllowed ) ); 526 return; 527 } 528 529 // Recordar el siguiente elemento visible para seleccionarlo despues 530 let nextToSelect = null; 531 if ( item.element ) { 532 nextToSelect = this._getNextVisibleSibling( item.element ) 533 || this._getPrevVisibleSibling( item.element ); 534 } 535 536 // Mover item 537 this.leftItems.splice( index, 1 ); 538 this.rightItems.push( item ); 539 540 // Actualizar DOM 541 if ( item.element ) { 542 item.element.classList.remove( 'double_list_item_selected' ); 543 item.element.remove(); 544 item.element.querySelector( 'input' ).name = this.id; 545 this.rightList.appendChild( item.element ); 546 } 547 548 // Seleccionar siguiente item en lista izquierda 549 if ( nextToSelect ) { 550 this._selectItem( nextToSelect, 'left' ); 551 } 552 553 this._clearError(); 554 this._updateRightLabel(); 555 this._triggerChange(); 556 } 557 558 /** 559 * Mueve un item a la izquierda 560 * @param {string} value - Valor del item a mover 561 */ 562 _moveItemToLeft( value ) { 563 const index = this.rightItems.findIndex( item => item.value == value ); 564 if ( index === -1 ) return; 565 566 const item = this.rightItems[ index ]; 567 568 // Verificar si esta oculto 569 if ( item.element && item.element.style.display === 'none' ) return; 570 571 // Recordar el siguiente elemento visible para seleccionarlo despues 572 let nextToSelect = null; 573 if ( item.element ) { 574 nextToSelect = this._getNextVisibleSibling( item.element ) 575 || this._getPrevVisibleSibling( item.element ); 576 } 577 578 // Mover item 579 this.rightItems.splice( index, 1 ); 580 this.leftItems.push( item ); 581 582 // Actualizar DOM - insertar ordenado por position 583 if ( item.element ) { 584 item.element.classList.remove( 'double_list_item_selected' ); 585 item.element.remove(); 586 item.element.querySelector( 'input' ).name = `${this.id}_left`; 587 this._insertSorted( item ); 588 } 589 590 // Seleccionar siguiente item en lista derecha 591 if ( nextToSelect ) { 592 this._selectItem( nextToSelect, 'right' ); 593 } 594 595 this._clearError(); 596 this._updateRightLabel(); 597 this._triggerChange(); 598 } 599 600 /** 601 * Inserta un item ordenado en la lista izquierda 602 * @param {Object} item - Item a insertar 603 */ 604 _insertSorted( item ) { 605 let inserted = false;
606 for ( const existingItem of this.leftItems ) { 607 if ( existingItem === item ) continue; 608 if ( existingItem.element && existingItem.position > item.position ) { 609 this.leftList.insertBefore( item.element, existingItem.element ); 610 inserted = true; 611 break; 612 } 613 } 614 if ( !inserted ) { 615 this.leftList.appendChild( item.element ); 616 } 617 } 618 619 /** 620 * Mueve el item seleccionado a la derecha 621 */ 622 _moveSelectedToRight() { 623 const selected = this.leftList.querySelector( '.double_list_item_selected' ); 624 if ( selected ) { 625 this._moveItemToRight( selected.dataset.value ); 626 } 627 } 628 629 /** 630 * Mueve el item seleccionado a la izquierda 631 */ 632 _moveSelectedToLeft() { 633 const selected = this.rightList.querySelector( '.double_list_item_selected' ); 634 if ( selected ) { 635 this._moveItemToLeft( selected.dataset.value ); 636 } 637 } 638 639 /** 640 * Mueve todos los items visibles a la derecha 641 */ 642 _moveAllToRight() { 643 const visibleItems = this.leftItems.filter( item => 644 item.element && item.element.style.display !== 'none' 645 ); 646 647 for ( const item of visibleItems ) { 648 if ( this.maxAllowed && this.rightItems.length >= this.maxAllowed ) { 649 this._showError( this._getTerm( 'NO_SE_PUEDEN_SELECCIONAR_MAS_DE_N_ELEMENTOS' ) 650 .replace( '#{cant}', this.maxAllowed ) ); 651 break; 652 } 653 this._moveItemToRight( item.value ); 654 } 655 } 656 657 /** 658 * Mueve todos los items visibles a la izquierda 659 */ 660 _moveAllToLeft() { 661 const visibleItems = this.rightItems.filter( item => 662 item.element && item.element.style.display !== 'none' 663 ); 664 665 for ( const item of [ ...visibleItems ] ) { 666 this._moveItemToLeft( item.value ); 667 } 668 this._clearError(); 669 } 670 671 /** 672 * Muestra mensaje de error 673 * @param {string} message - Mensaje a mostrar 674 */ 675 _showError( message ) { 676 this.rightError.textContent = message; 677 this.rightError.classList.remove( 'double_list_hidden' ); 678 } 679 680 /** 681 * Limpia mensaje de error 682 */ 683 _clearError() { 684 this.rightError.classList.add( 'double_list_hidden' ); 685 this.rightError.textContent = ''; 686 } 687 688 /** 689 * Actualiza el label de la lista derecha 690 */ 691 _updateRightLabel() { 692 let label = this.titleRight; 693 if ( this.maxAllowed ) { 694 label += ` (${this.rightItems.length}/${this.maxAllowed})`; 695 } 696 if ( this.rightLabel ) { 697 this.rightLabel.textContent = label; 698 } 699 } 700 701 /** 702 * Dispara el evento onChange 703 */ 704 _triggerChange() { 705 if ( this.onChange ) { 706 this.onChange( this ); 707 } 708 } 709 710 /** 711 * Obtiene los valores seleccionados (lista derecha) 712 * @returns {Array} Array de valores 713 */ 714 getSelected() { 715 return this.rightItems.map( item => item.value ); 716 } 717 718 /** 719 * Obtiene los items seleccionados completos 720 * @returns {Array} Array de objetos {value, title} 721 */ 722 getSelectedItems() { 723 return this.rightItems.map( item => ( { 724 value: item.value, 725 title: item.title 726 } ) ); 727 } 728 729 /** 730 * Valida que haya al menos un item seleccionado 731 * @returns {boolean} 732 */ 733 validate() { 734 return this.rightItems.length > 0; 735 } 736 737 /** 738 * Limpia todas las selecciones 739 */ 740 clear() { 741 this._moveAllToLeft(); 742 } 743}
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.