PageSourceSearch

https://elearning.ucse.edu.ar//lib/javascript/Educativa/Modules/Components/DoubleList.js?v=1787049104

js ucse.edu.ar collected 2026-10-02 11:55:59 UTC 25,425 bytes, 743 lines download raw bytes

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.