(function (window, $) { 'use strict'; if (!$) { console.error('ScrollSig requiere jQuery.'); return; } var SCROLL_RENDERED = 'scrollsigrendered'; var SCROLL_CONTAINER = 'scrollsig-container'; var SCROLL_STICKY = 'scrollsig-sticky'; /** * ============================================================ * RENDERIZAR SCROLL SIG * ============================================================ * * Busca: * * table.scrollsig * * que todavía no tengan: * * scrollsigrendered */ window.RenderizarScrollSig = function () { $('table.scrollsig') .not('.' + SCROLL_RENDERED) .each(function () { var $grid = $(this); renderizarGrid($grid); }); }; /** * ============================================================ * RENDERIZAR UN GRID * ============================================================ */ function renderizarGrid($grid) { /* * -------------------------------------------------------- * 1. EJECUTAR FUNCIÓN PREVIA * -------------------------------------------------------- * * Ejemplo: * * data-scrollfuncion="PrepararGrid" * * function PrepararGrid(grid) { * } */ ejecutarFuncionPrevia($grid); /* * -------------------------------------------------------- * 2. CREAR CONTENEDOR * -------------------------------------------------------- */ crearContenedor($grid); /* * -------------------------------------------------------- * 3. CONFIGURAR TAMAÑO * -------------------------------------------------------- */ configurarTamano($grid); /* * -------------------------------------------------------- * 4. CONFIGURAR FILAS STICKY * -------------------------------------------------------- */ configurarFilasSticky($grid); /* * -------------------------------------------------------- * 5. MARCAR COMO RENDERIZADO * -------------------------------------------------------- */ $grid.addClass( SCROLL_RENDERED ); } /** * ============================================================ * FUNCIÓN PREVIA * ============================================================ * * data-scrollfuncion="MiFuncion" * * Se busca: * * window.MiFuncion * * y se ejecuta: * * MiFuncion(grid); */ function ejecutarFuncionPrevia($grid) { var nombreFuncion = $.trim( $grid.attr('data-scrollfuncion') || '' ); if (!nombreFuncion) { return; } var funcion = obtenerFuncionGlobal(nombreFuncion); if (typeof funcion !== 'function') { console.warn( 'ScrollSig: no se encontró la función "' + nombreFuncion + '".' ); return; } /* * Mandamos el elemento TABLE real. */ funcion( $grid[0] ); } /** * Permite incluso nombres como: * * MiFuncion * * MiModulo.PrepararGrid */ function obtenerFuncionGlobal(nombre) { var partes = nombre.split('.'); var contexto = window; for ( var i = 0; i < partes.length; i++ ) { if ( contexto === null || contexto === undefined ) { return null; } contexto = contexto[partes[i]]; } return contexto; } /** * ============================================================ * CREAR CONTENEDOR * ============================================================ * * Antes: * * * * * Después: * *
* *
* * */ function crearContenedor($grid) { /* * Protección adicional. * * Si por alguna razón ya está dentro * del contenedor no volvemos a envolverlo. */ if ( $grid.parent().hasClass( SCROLL_CONTAINER ) ) { return; } $grid.wrap( $('
', { 'class': SCROLL_CONTAINER }) ); } /** * ============================================================ * CONFIGURAR TAMAÑO * ============================================================ * * PRIORIDAD: * * 1. data-scrollmargintop * * 2. data-scrollsize * * 3. 300px */ function configurarTamano($grid) { var $container = obtenerContenedor($grid); if (!$container.length) { return; } var marginTop = $.trim( $grid.attr( 'data-scrollmargintop' ) || '' ); var scrollSize = $.trim( $grid.attr( 'data-scrollsize' ) || '' ); /* * -------------------------------------------------------- * MARGIN TOP * -------------------------------------------------------- * * Ejemplo: * * data-scrollmargintop="250" * * viewport: * * 900px * * resultado: * * 650px */ if (marginTop !== '') { var margen = obtenerNumero( marginTop ); if (margen !== null) { $container.attr( 'data-scrollsig-mode', 'margin' ); $container.attr( 'data-scrollsig-margin', margen ); aplicarTamanoPorMargen( $container, margen ); return; } } /* * -------------------------------------------------------- * SIZE * -------------------------------------------------------- * * Ejemplos: * * data-scrollsize="500px" * * data-scrollsize="50vh" * * data-scrollsize="30rem" * * data-scrollsize="400" */ if (scrollSize !== '') { scrollSize = normalizarTamano( scrollSize ); $container.attr( 'data-scrollsig-mode', 'size' ); $container.css( 'max-height', scrollSize ); return; } /* * -------------------------------------------------------- * DEFAULT * -------------------------------------------------------- */ $container.attr( 'data-scrollsig-mode', 'default' ); $container.css( 'max-height', '300px' ); } /** * ============================================================ * TAMAÑO POR MARGEN * ============================================================ */ function aplicarTamanoPorMargen( $container, margen ) { var altura = window.innerHeight - margen; /* * Protección para evitar tamaños negativos * o absurdamente pequeños. */ if (altura < 50) { altura = 50; } $container.css( 'max-height', altura + 'px' ); } /** * ============================================================ * NORMALIZAR TAMAÑO * ============================================================ * * 400 * * se convierte en: * * 400px * * * 400px * 50vh * 20rem * * se conservan. */ function normalizarTamano(valor) { valor = $.trim( String(valor) ); if ( /^\d+(\.\d+)?$/.test( valor ) ) { return valor + 'px'; } return valor; } /** * ============================================================ * OBTENER NÚMERO * ============================================================ * * Acepta: * * 250 * 250px */ function obtenerNumero(valor) { if ( valor === null || valor === undefined ) { return null; } valor = String(valor) .replace('px', '') .trim(); var numero = parseFloat( valor ); if (isNaN(numero)) { return null; } return numero; } /** * ============================================================ * FILAS STICKY * ============================================================ * * data-scrollrowsheader="1" * * Por defecto: * * 1 * * * Si: * * data-scrollrowsheader="3" * * las primeras 3 filas serán sticky. * * * IMPORTANTE: * * El sticky se aplica EXCLUSIVAMENTE al TR. * * NO se aplica: * * position: sticky * top * z-index * * a TH ni TD. */ function configurarFilasSticky($grid) { /* * Primero limpiamos cualquier configuración * anterior por seguridad. */ limpiarSticky($grid); var cantidad = parseInt( $grid.attr( 'data-scrollrowsheader' ), 10 ); /* * Por defecto la primera fila. */ if ( isNaN(cantidad) ) { cantidad = 1; } /* * Permitir explícitamente: * * data-scrollrowsheader="0" * * para no tener sticky. */ if (cantidad <= 0) { return; } /* * Tomamos las primeras N filas * reales de la tabla. */ var $filas = obtenerFilasSuperiores( $grid, cantidad ); if (!$filas.length) { return; } /* * Empezamos en -1px. * * Esto desplaza todo el bloque sticky * 1px hacia arriba. * * Evita la pequeña línea transparente * que puede aparecer encima del header. * * Si tenemos varias filas sticky: * * fila 1: * * top = -1 * * fila 2: * * top = altura fila 1 - 1 * * fila 3: * * top = altura fila 1 * + altura fila 2 * - 1 */ var topAcumulado = -1; $filas.each(function (indice) { var $fila = $(this); /* * IMPORTANTE: * * Toda la configuración sticky * se aplica directamente al TR. */ $fila .addClass( SCROLL_STICKY ) .css({ 'top': topAcumulado + 'px', 'z-index': 3 - indice }); /* * Obtener altura REAL de la fila. * * Se utiliza para posicionar la * siguiente fila sticky. */ var altura = $fila.outerHeight(); if ( !altura || altura < 1 ) { altura = 0; } topAcumulado += altura; }); } /** * ============================================================ * OBTENER PRIMERAS N FILAS * ============================================================ * * Preferimos THEAD. * * Si el GridView no genera THEAD, * usamos las primeras filas de la tabla. */ function obtenerFilasSuperiores( $grid, cantidad ) { var $filasThead = $grid.find( '> thead > tr' ); /* * Si THEAD tiene suficientes filas, * utilizamos esas. */ if ($filasThead.length) { var $resultado = $filasThead.slice( 0, cantidad ); /* * Si necesitamos más filas que las * disponibles en THEAD, completamos * con TBODY. */ if ( $resultado.length < cantidad ) { var faltantes = cantidad - $resultado.length; var $tbody = $grid .find( '> tbody > tr' ) .slice( 0, faltantes ); $resultado = $resultado.add( $tbody ); } return $resultado; } /* * GridView sin THEAD. */ return $grid .find('> tbody > tr') .slice( 0, cantidad ); } /** * ============================================================ * LIMPIAR STICKY * ============================================================ * * Ahora solamente limpiamos el TR. * * Ya NO existe: * * scrollsig-sticky-cell */ function limpiarSticky($grid) { $grid .find('.' + SCROLL_STICKY) .removeClass( SCROLL_STICKY ) .css({ 'top': '', 'z-index': '' }); } /** * ============================================================ * OBTENER CONTENEDOR * ============================================================ */ function obtenerContenedor($grid) { return $grid.closest( '.' + SCROLL_CONTAINER ); } /** * ============================================================ * RECALCULAR SCROLLSIG * ============================================================ * * Método público por si en algún momento * necesitas recalcular manualmente: * * RecalcularScrollSig(); * * o: * * RecalcularScrollSig('GvTramites'); */ window.RecalcularScrollSig = function ( grid ) { /* * Un Grid específico. */ if (grid) { var $grid = obtenerGrid( grid ); if (!$grid.length) { return; } recalcularGrid( $grid ); return; } /* * Todos los ScrollSig. */ $('table.scrollsig.' + SCROLL_RENDERED) .each(function () { recalcularGrid( $(this) ); }); }; /** * ============================================================ * RECALCULAR GRID * ============================================================ */ function recalcularGrid($grid) { var $container = obtenerContenedor( $grid ); if (!$container.length) { return; } /* * Si usa margin top recalculamos * contra window.innerHeight. */ if ( $container.attr( 'data-scrollsig-mode' ) === 'margin' ) { var margen = parseFloat( $container.attr( 'data-scrollsig-margin' ) ); if (!isNaN(margen)) { aplicarTamanoPorMargen( $container, margen ); } } /* * Recalcular TOP de sticky rows. * * Esto es importante si alguna función * cambió la altura de los headers. */ configurarFilasSticky( $grid ); } /** * ============================================================ * OBTENER GRID * ============================================================ */ function obtenerGrid(grid) { if (grid instanceof jQuery) { return grid; } if ( typeof HTMLElement !== 'undefined' && grid instanceof HTMLElement ) { return $(grid); } if ( typeof grid === 'string' ) { if ( grid.charAt(0) === '#' ) { return $(grid); } return $( '#' + grid ); } return $(); } /** * ============================================================ * RESIZE DE VENTANA * ============================================================ * * No ejecutamos el cálculo inmediatamente en cada pixel * que cambia el navegador. * * Esperamos 100 ms para evitar trabajo excesivo. */ var timeoutResize = null; $(window).on( 'resize.scrollsig', function () { if ( timeoutResize !== null ) { clearTimeout( timeoutResize ); } timeoutResize = setTimeout( function () { RecalcularScrollSig(); timeoutResize = null; }, 100 ); } ); /** * ============================================================ * PRIMERA CARGA * ============================================================ */ $(function () { RenderizarScrollSig(); }); /** * ============================================================ * ASP.NET WEBFORMS / UPDATEPANEL * ============================================================ * * Si un UpdatePanel reemplaza el GridView: * * el nuevo TABLE ya no tendrá: * * scrollsigrendered * * por lo tanto se procesará nuevamente. */ if ( typeof Sys !== 'undefined' && Sys.WebForms && Sys.WebForms.PageRequestManager ) { Sys.WebForms.PageRequestManager .getInstance() .add_endRequest( function () { RenderizarScrollSig(); /* * Recalculamos también los grids que * sobrevivieron al postback parcial. */ RecalcularScrollSig(); } ); } })(window, window.jQuery);