//# sourceURL=J_LuaView1_UI7.js /** * J_LuaView1_UI7.js * Configuration interface for LuaView. */ /* globals api,jsonp,jQuery,$,unescape,MultiBox,ace */ /* jshint multistr: true, laxcomma: true */ //"use strict"; // fails on UI7, works fine with ALTUI var LuaView = (function(api, $) { /* unique identifier for this plugin... */ var uuid = '7513412a-a7e8-11e8-afe3-74d4351650de'; var pluginVersion = "1.8develop-20353"; var myModule = {}; var serviceId = "urn:toggledbits-com:serviceId:LuaView1"; // var deviceType = "urn:schemas-toggledbits-com:device:LuaView:1"; var configModified = false; var isOpenLuup = false; var logTab = false; var logAtBottom = false; var logSeenEOF = false; function D(m) { console.log("J_LuaView1_UI7.js: " + m); } function initModule() { D("jQuery version is " + String( jQuery.fn.jquery ) ); var ud = api.getUserData(); for (var i=0; i < ud.devices.length; ++i ) { if ( ud.devices[i].device_type == "openLuup" ) { isOpenLuup = true; break; } } logTab = false; logAtBottom = false; logSeenEOF = false; } /* Push header to document head */ function header() { var html = ""; jQuery('head').append( '' ); var s = api.getDeviceState( api.getCpanelDeviceId(), serviceId, "LoadACE" ) || "1"; if ( "0" !== s && ! window.ace ) { s = api.getDeviceState( api.getCpanelDeviceId(), serviceId, "ACEURL" ) || ""; if ( "" === s ) s = "https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.12/ace.js"; jQuery( "head" ).append( '' ); // jQuery( "head" ).append( '' ); // jQuery( "head" ).append( '' ); } html = ''; jQuery('head').append( html ); } /* Return footer */ function footer() { var html = ''; html += '
'; html += '
Find LuaView useful? Please consider a small one-time donation to support this and my other plugins on my web site. I am grateful for any support you choose to give!
'; html += '
LuaView ver ' + pluginVersion + ' Patrick H. Rigney (rigpapa)' + ' Please use the ' + ' forum thread for support.
'; return html; } /* Closing the control panel. */ function onBeforeCpanelClose(args) { D( 'onBeforeCpanelClose args: ' + JSON.stringify(args) ); } /* Return a Promise that resolves when Luup is reloaded and ready, as evidenced by the functional state of the Reactor plugin's request handler. */ function waitForReloadComplete( msg ) { return new Promise( function( resolve, reject ) { var expire = Date.now() + 90000; var dlg = false; function tryAlive() { $.ajax({ url: api.getDataRequestURL(), data: { id: "lr_LuaView", action: "alive" }, dataType: "json", timeout: 5000 }).done( function( data ) { if ( data && data.status ) { if (dlg) $("#myModal").modal("hide"); resolve( true ); } else { if ( ! $("#myModal").is(":visible") ) { api.showCustomPopup( msg || "Waiting for Luup ready before operation...", { autoHide: false, category: 3 } ); dlg = true; } if ( Date.now() >= expire ) { if (dlg) $("#myModal").modal("hide"); reject( "timeout" ); } else { setTimeout( tryAlive, 2000 ); } } }).fail( function() { if ( Date.now() >= expire ) { if (dlg) $("#myModal").modal("hide"); reject( "timeout" ); } else { if ( ! $("#myModal").is(":visible") ) { api.showCustomPopup( msg || "Waiting for Luup ready before operation...", { autoHide: false, category: 3 } ); dlg = true; } setTimeout( tryAlive, 5000 ); } }); } tryAlive(); }); } /* Swiped from Reactor, this function checks the Lua fragment */ function testLua( lua, row ) { jQuery( 'div.tberrmsg', row ).remove(); $.ajax({ url: api.getDataRequestURL(), method: 'POST', /* data could be long */ data: { id: "lr_LuaView", action: "testlua", lua: lua }, cache: false, dataType: 'json', timeout: 5000 }).done( function( data, statusText, jqXHR ) { if ( data.status ) { /* Good Lua */ return; } else if ( data.status === false ) { /* specific false, not undefined */ jQuery( 'div.editor', row ).prepend( jQuery( '
' ).text( data.message || "Error in Lua" ) ); $( 'button.runlua', row ).prop( 'disabled', true ); } }).fail( function( stat ) { console.log("Failed to check Lua: " + stat); }); } function handleTextAreaChange( ev ) { var url = api.getDataRequestURL(); var f = jQuery( ev.currentTarget ); var row = f.closest( 'div.row' ); row.addClass("modified"); var lua = f.val() || ""; lua = lua.replace( /\r\n/gm, "\n" ).replace( /\r/gm, "\n" ).trimEnd(); lua = unescape( encodeURIComponent( lua ) ); // Fanciness to keep UTF-8 chars well testLua( lua, row ); var scene = row.attr( 'id' ); D("Changed " + scene); if ( scene == "__startup" ) { D("Posting startup lua change to " + url); jQuery.ajax({ url: url, method: "POST", scriptCharset: "utf-8", contentType: "application/x-www-form-urlencoded; charset=utf-8", data: { id: "lr_LuaView", action: "saveStartupLua", lua: lua }, dataType: "text", timeout: 5000 }).done( function( data, statusText, jqXHR ) { if ( "OK" === data ) { row.removeClass("modified"); $('button.runlua', row).prop( 'disabled', false ); if ( ! isOpenLuup ) { try { /* ??? Discovered/undocumented/unpublished */ api.application.sendCommandSaveUserData(true); } catch( e ) { alert("Startup Lua saved, but you must hard-refresh your browser to get the UI to show it consistently."); } } configModified = false; } else { alert("An error occurred while trying to save. Luup may be restarting. Try again in a moment."); throw new Error( "Save returned: " + data ); } }).fail( function( jqXHR, textStatus, errorThrown ) { // Bummer. D("Failed to load scene: " + textStatus + " " + String(errorThrown)); D(jqXHR.responseText); alert("Save failed! Vera may be busy/restarting. Wait a moment, and try again."); }); } else { D("Loading scene data from " + url); scene = parseInt( scene ); /* Query the scene as it currently is. */ jQuery.ajax({ url: url + "?id=scene&action=list&scene=" + scene, dataType: "json", timeout: 5000 }).done( function( data, statusText, jqXHR ) { // Excellent. D( "Loaded the scene, updating..." ); if ( lua == "" || isOpenLuup ) { data.encoded_lua = 0; data.lua = lua; } else { data.encoded_lua = 1; data.lua = btoa( lua ); } // Save it. var ux = JSON.stringify( data ); /* PHR 2018-08-25: jQuery.post() fails on older firmware due to jQuery version/bug, but ajax()+method seems to work fine. */ jQuery.ajax({ url: url, method: "POST", scriptCharset: "utf-8", contentType: "application/x-www-form-urlencoded; charset=utf-8", data: { id: "scene", action: "create", json: ux }, dataType: "text", timeout: 5000 }).done( function( data, statusText, jqXHR ) { D("Save returns " + jqXHR.responseText); if ( data == "OK" ) { row.removeClass("modified"); $('button.runlua', row).prop( 'disabled', false ); if ( ! isOpenLuup ) { try { /* ??? Discovered/undocumented/unpublished */ api.application.sendCommandSaveUserData(true); } catch( e ) { console.log( e ); } } configModified = false; } else { throw new Error( "Save replied: " + String(data) ); } }).fail( function( jqXHR, textStatus, errorThrown ) { D("Failed to save scene: " + textStatus + " " + String(errorThrown)); D(jqXHR.responseText); alert("Save failed! Vera may be busy/restarting. Wait a moment, and try again."); }); }).fail( function( jqXHR, textStatus, errorThrown ) { // Bummer. D("Failed to load scene: " + textStatus + " " + String(errorThrown)); D(jqXHR.responseText); alert("Save failed! Vera may be busy/restarting. Wait a moment, and try again."); }); } } function doTextArea( el, code ) { var t = jQuery( '' ); t.val( code || "" ); t.on( 'change.luaview', handleTextAreaChange ); el.append( t ); } function handleEditorChange( editor, session, delta ) { configModified = true; var $row = jQuery( editor.container ).closest('div.row'); $row.addClass('modified'); $('button.runlua', $row).prop( 'disabled', true ); } function handleEditorSave( editor, session, ev ) { var url = api.getDataRequestURL(); var f = jQuery( ev.currentTarget ); var row = f.closest( 'div.row' ); row.addClass("modified"); var lua = session.getValue(); lua = lua.replace( /\r\n/gm, "\n" ).replace( /\r/gm, "\n" ).trimEnd(); lua = unescape( encodeURIComponent( lua ) ); // Fanciness to keep UTF-8 chars well testLua( lua, row ); var scene = row.attr( 'id' ); D("Changed " + scene); if ( scene == "__startup" ) { D("Posting startup lua change to " + url); jQuery.ajax({ url: url, method: "POST", data: { id: "lr_LuaView", action: "saveStartupLua", lua: lua }, dataType: "text", timeout: 5000 }).done( function( data, statusText, jqXHR ) { if ( "OK" === data ) { row.removeClass("modified"); $( 'button.runlua', row ).prop( 'disabled', false ); if ( ! isOpenLuup ) { try { /* ??? Discovered/undocumented/unpublished */ api.application.sendCommandSaveUserData(true); } catch( e ) { alert("Startup Lua saved, but you must hard-refresh your browser to get the UI to show it consistently."); } } configModified = false; } else { throw new Error( "Save returned: " + data ); } }).fail( function( jqXHR, textStatus, errorThrown ) { // Bummer. D("Failed to load scene: " + textStatus + " " + String(errorThrown)); D(jqXHR.responseText); alert("Save failed! Vera may be busy/restarting. Wait a moment, and try again."); }); } else { D("Loading scene data from " + url); scene = parseInt( scene ); /* Query the scene as it currently is. */ jQuery.ajax({ url: url + "?id=scene&action=list&scene=" + scene, dataType: "json", timeout: 5000 }).done( function( data, statusText, jqXHR ) { // Excellent. D( "Loaded the scene, updating..." ); if ( lua == "" || isOpenLuup ) { data.encoded_lua = 0; data.lua = lua; } else { data.encoded_lua = 1; data.lua = btoa( lua ); } // Save it. var ux = JSON.stringify( data ); /* PHR 2018-08-25: jQuery.post() fails on older firmware due to jQuery version/bug, but ajax()+method seems to work fine. */ jQuery.ajax({ url: url, method: "POST", data: { id: "scene", action: "create", json: ux }, dataType: "text", timeout: 5000 }).done( function( data, statusText, jqXHR ) { D("Save returns " + jqXHR.responseText); if ( data == "OK" ) { row.removeClass("modified"); $( 'button.runlua', row ).prop( 'disabled', false ); if ( ! isOpenLuup ) { try { /* ??? Discovered/undocumented/unpublished */ api.application.sendCommandSaveUserData(true); } catch( e ) { console.log( e ); } } configModified = false; } else { throw new Error( "Save replied: " + String(data) ); } }).fail( function( jqXHR, textStatus, errorThrown ) { D("Failed to save scene: " + textStatus + " " + String(errorThrown)); D(jqXHR.responseText); alert("Save failed! Vera may be busy/restarting. Wait a moment, and try again."); }); }).fail( function( jqXHR, textStatus, errorThrown ) { // Bummer. D("Failed to load scene: " + textStatus + " " + String(errorThrown)); D(jqXHR.responseText); alert("Save failed! Vera may be busy/restarting. Wait a moment, and try again."); }); } } function doEditor( el, code ) { var editor = ace.edit( jQuery(el).get(0), { minLines: 8, maxLines: 32, theme: "ace/theme/xcode", mode: "ace/mode/lua", fontSize: "16px", tabSize: 4 }); /* Apply options from state if set */ var exopts = api.getDeviceState( api.getCpanelDeviceId(), serviceId, "AceOptions" ) || ""; if ( exopts !== "" ) { try { var opts = JSON.parse( exopts ); if ( opts !== undefined ) { editor.setOptions( opts ); } } catch( e ) { alert("Can't apply your custom AceOptions: " + String(e)); } } var session = editor.session; session.setValue( code ); editor.on( 'change', function( delta ) { handleEditorChange( editor, session, delta ); } ); editor.on( 'blur', function( ev ) { handleEditorSave( editor, session, ev ); } ); } function handleReloadClick( ev ) { var btn = jQuery( ev.target ); btn.prop( 'disabled', true ); api.showCustomPopup( "Reloading Luup...", { autoHide: false, category: 3 } ); setTimeout( function() { api.performActionOnDevice( 0, "urn:micasaverde-com:serviceId:HomeAutomationGateway1", "Reload", { actionArguments: { Reason: "User-requested reload from LuaView UI" } } ); setTimeout( function() { waitForReloadComplete().finally( function() { $("#myModal").modal("hide"); btn.prop( 'disabled', false ); }); }, 5000 ); }, 2000 ); } function handleBackupClick( ev ) { var txt = ''; txt += '-- INSTRUCTIONS: RIGHT-CLICK in this window and choose "Save as..." to save this display as a backup of your Lua.'; txt += "\n-- To restore all or part later, just copy/paste from the saved file."; txt += "\n-- Snapshot time is " + String( new Date() ); txt += "\n\n"; var ud = api.getUserData(); var code = ( parseInt(ud.encoded_lua || 0) ? atob( ud.StartupCode ) : ud.StartupCode ) || ""; if ( "" !== code ) { code = decodeURIComponent(escape(code)); /* UTF-8 handling, reversal */ txt += "-- Startup Lua\n"; txt += code; txt += "\n\n"; } var scenes = api.cloneObject( ud.scenes ); for (var i=0; i' ).text( txt ); $body.empty().append( $pre ); }, 1000 ); win.focus(); } else { alert('Please allow popups for this interface.'); } } function updateDisplay( sort ) { var ud = api.getUserData(); var list = jQuery("div#codelist"); list.empty(); if ( sort === undefined ) { sort = api.getDeviceState( api.getCpanelDeviceId(), serviceId, "Sort" ) || "name,asc"; } var sortopt = sort.split(/,/); if ( sortopt.length < 2 ) { sortopt = [ "name", "asc" ]; } var el = jQuery('
'); el.append('
Sort by:
'); jQuery('select#sortby', el).val( sortopt[0] ); jQuery('select#sortasc', el).val( sortopt[1] ); jQuery('select', el).on( 'change', handleSortChange ); var col = jQuery( '
' ) .appendTo( el ); jQuery( '' ) .on( 'click.luaview', handleBackupClick ) .appendTo( col ); jQuery( '' ) .on( 'click.luaview', handleReloadClick ) .appendTo( col ); list.append(el); var code = ( parseInt(ud.encoded_lua || 0) ? atob( ud.StartupCode ) : ud.StartupCode ) || ""; code = decodeURIComponent(escape(code)); /* UTF-8 handling, reversal */ el = jQuery('
'); el.attr('id', '__startup'); el.append('
Startup Lua
'); el.append('
'); if ( ! window.ace ) { doTextArea( jQuery("div.editor", el), code ); } else { jQuery( 'div.editor', el ).append( '
' ); doEditor( jQuery("div.luacode", el), code ); } list.append(el); var scenes = api.cloneObject( ud.scenes ); scenes.sort( function( a, b ) { if ( sortopt[0] == "id" ) { if ( sortopt[1] == 'desc' ) { return a.id < b.id ? 1 : -1; } else { return a.id < b.id ? -1 : 1; } } else { if ( sortopt[1] == 'desc' ) { return a.name.toLowerCase() < b.name.toLowerCase() ? 1 : -1; } else { return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; } } } ); for (var i=0; i
'); el.attr('id', scenes[i].id); el.append('
'); var sn = (scenes[i].name || scenes[i].id) + ' (' + scenes[i].id + ')'; if ( scenes[i].hidden ) { sn += " (hidden)"; } jQuery('div.scenename', el).text( sn ); // .append( '
' ); el.append('
'); code = ( parseInt( scenes[i].encoded_lua || 0 ) ? atob( scenes[i].lua ) : scenes[i].lua ) || ""; try { code = decodeURIComponent(escape(code)); /* UTF-8 handling, reversal */ } catch( e ) { console.log(e); alert(e); } if ( ! window.ace ) { doTextArea( jQuery("div.editor", el), code ); } else { jQuery( 'div.editor', el ).append( '
' ); doEditor( jQuery("div.luacode", el), code ); } list.append(el); } } function handleSortChange() { var sortby = jQuery('select#sortby').val(); var sortasc = jQuery('select#sortasc').val(); var sort = sortby + "," + sortasc; api.setDeviceStatePersistent( api.getCpanelDeviceId(), serviceId, "Sort", sort ); updateDisplay( sort ); } function waitForAce( since ) { var s = api.getDeviceState( api.getCpanelDeviceId(), serviceId, "LoadACE" ) || "1"; if ( "0" == s || window.ace || ( Date.now() - since ) >= 5000 ) { updateDisplay(); return; } setTimeout( function() { waitForAce( since ); }, 500 ); } function doLua() { initModule(); header(); var html = '
Loading... please wait...
'; html += footer(); api.setCpanelContent( html ); waitForAce( Date.now() ); } var logTask = false; var chunkSize = 250; function loadNextLogChunk( tries ) { var container = jQuery( 'div#tblogdata' ); if ( !logTab || 0 === container.length ) { logTab = false; console.log("Log tab no longer foreground/available."); return; } tries = tries || 1; if ( tries > 10 ) { $( "div.tbloadstatus", container ).text( "Too many errors. Giving up." ); return; } var lastline = parseInt( container.data( 'lastline' ) || 0 ); if ( isNaN(lastline) ) lastline = 0; if ( 0 === lastline ) { logSeenEOF = false; } // $( "div.tbloadstatus", container ).text( "Fetching log data" + ( lastline > 0 ? ( " after " + lastline ) : "" ) ); jQuery.ajax( { url: api.getDataRequestURL(), data: { id: "lr_LuaView", action: "log", first: ( lastline + 1 ), count: chunkSize, r: Math.random() }, dataType: "text", timeout: 15000 }).done( function( data ) { if ( ! data.match( /^\$LOC:/ ) ) { console.log("Invalid response data: "+data.substring(0,255)); $( "div.tbloadstatus", container ).text("Error... retrying... " + String(tries)); if ( !logTask ) { logTask = window.setTimeout( function () { logTask = false; loadNextLogChunk( tries+1 ); }, 10000 ); } return; } data = data.replace( /\r\n/g, "\n" ).replace( /\r/g, "\n" ).replace( /[\u2028\u2029]/g, "\n" ); data = data.replace( /^\$LOC:[^\n]*\n/, "" ); data = data.replace( /&/, "&" ).replace( /[<]/g, "<" ).replace( /[>]/g, ">" ); data = data.replace( /\x1b\[(\d+);1m(.*)\x1b\[0m/g, function( m, p1, p2 ) { return '' + p2 + ''; }); data = data.replace( /\x1b\[1m(.*)\x1b\[0m/g, '$1' ); data = data.replace( /\x1b\[4m(.*)\x1b\[0m/g, '$1' ); data = data.replace( /\x1b\[7m(.*)\x1b\[0m/g, '$1' ); /* Below skips tab, NL */ data = data.replace( /[\x00-\x08\x0b-\x1f\x7f]/g, function( c ) { return '<0x' + c.charCodeAt(0).toString(16) + '>'; }); var nl = 0; var search = $( 'input#searchstring' ).val() || ""; if ( data.match( /^[\n\s]*$/ ) ) { console.log("Reached current EOF at " + lastline); logSeenEOF = true; } else { /* Count lines */ data.replace( /\n/g, function( w ) { nl = nl + 1; }); logSeenEOF = false; } if ( "" !== search ) { var tp = new RegExp( search, "g" ); data = data.replace( tp, '$&' ); } console.log("Received line count: " + nl ); var ll = parseInt( container.data( 'lastline' ) || 0 ); if ( nl > 0 ) { if ( ll === lastline ) { var $blk = jQuery( 'div#tblogdata pre' ); ll += nl; container.data( 'lastline', ll ).attr( 'data-lastline', ll ); $blk.append( data ); var n = $( '.tbsrch', $blk ).length; $( 'span#matchcount' ).text( n + " matches" ); logAtBottom = ( $blk.scrollTop() + $blk.height() ) >= ( $blk.get(0).scrollHeight - 32 ); console.log("data handler: logAtBottom = " + logAtBottom); } } if ( !logTask && ( logAtBottom || !logSeenEOF ) ) { logTask = window.setTimeout( function() { logTask = false; loadNextLogChunk( 0 ); }, ( logSeenEOF && logAtBottom ) ? 2000 : 500 ); $( "div.tbloadstatus", container ).text( logSeenEOF ? ( ll + " lines" + ( logAtBottom ? "; waiting for more..." : "" ) ) : ( ll + " so far, requesting more..." ) ); } else { console.log("Update not scheduled; logTask="+logTask+", logAtBottom="+logAtBottom+ ", logSeenEOF="+logSeenEOF); if ( logSeenEOF ) { $( "div.tbloadstatus", container ).text( ll + " lines displayed. Scroll to bottom to load more." ); } } }).fail( function( jqXHR, textStatus, errorThrown ) { console.log( "Request failed... retrying" ); console.log( jqXHR ); console.log( textStatus ); console.log( errorThrown ); $( "div.tbloadstatus", container ).text("Error... Luup may be reloading... retrying... " + String(tries)); if ( !logTask ) { logTask = window.setTimeout( function () { logTask = false; loadNextLogChunk( tries+1 ); }, 10000 ); } }); } function doLog() { initModule(); header(); var html = ' \
\
\ \ \ \ \
\
\ \
\
\
\
Loading... please wait...
\
 \
	
\
'; html += footer(); api.setCpanelContent( html ); logTab = true; logSeenEOF = false; var $blk = jQuery( 'div#tblogdata pre' ); $blk.scroll(function() { if ( logTab ) { // logAtBottom = ( $(window).scrollTop() + $(window).height() ) > ( $(document).height() - 108 ); // Bottom of scroll window > bottom of
 block
				// logAtBottom = ( $(window).scrollTop() + $(window).height() ) > ( $blk.position().top + $blk.height() );
				logAtBottom = ( $blk.scrollTop() + $blk.height() ) >= ( $blk.get(0).scrollHeight - 32 );
				console.log("scroll handler: logAtBottom = " + logAtBottom);
				if ( logAtBottom ) {
					if ( !logTask ) {
						loadNextLogChunk( 0 );
					}
				}
			}
		});

		jQuery( 'div#tblogdata' ).data( 'lastline', 0 ).attr( 'data-lastline', 0 );
		loadNextLogChunk( 0 );

		jQuery( 'button#rotatelogs' ).on( 'click.luaview', function( ev ) {
			var $el = jQuery( ev.target );
			$el.prop( 'disabled', true );
			if ( logTask ) {
				clearTimeout( logTask );
				logTask = false;
			}
			$( 'div.tbloadstatus' ).text( "Please wait... requesting log rotation..." );
			$( 'div#tblogdata pre').empty();
			$( 'div#tblogdata' ).data( 'lastline', 0 ).attr( 'data-lastline', 0 );
			jQuery.ajax({
				url: api.getDataRequestURL(),
				data: {
					id: "lr_LuaView",
					action: "rotatelogs",
					r: Math.random()
				},
				dataType: "text",
				timeout: 30000
			}).always( function() {
				$el.prop( 'disabled', false );
				loadNextLogChunk( 0 );
			});
		});

		jQuery( 'input#searchstring' ).on( 'change.luaview', function( ev ) {
			var $el = jQuery( ev.target );
			var $ct = jQuery( 'div#tblogdata' );
			var $blk = jQuery( 'div#tblogdata pre' );
			var search = $el.val() || "";
			$( '.tbsrch', $blk ).removeClass( 'tbsrch' );
			$ct.data( 'searchpos', 0 );
			if ( "" !== search ) {
				var tp = new RegExp( search, "g" );
				console.log(tp);
				var nmatch = 0;
				$blk.html( $blk.html().replace( tp, function( m ) {
					return '' + m + '';
				}) );
				$( 'span#matchcount' ).text( nmatch + " matches" );
				$( "button#tbnext,button#tbprev" ).prop( 'disabled', 0 === nmatch );
			} else {
				$( 'span#matchcount' ).text( "" );
				$( "button#tbnext,button#tbprev" ).prop( 'disabled', true );
			}
		});

		jQuery( 'button#tbnext' ).on( 'click.luaview', function( ev ) {
			var $ct = $( 'div#tblogdata' );
			var $blk = jQuery( 'pre', $ct );
			var ix = $ct.data( 'searchpos' ) || 0;
			ix += 1;
			$el = $( 'span.tbsrch[data-index="' + ix + '"]', $blk );
			if ( 0 === $el.length ) {
				ix = 1;
				$el = $( 'span.tbsrch[data-index="1"]', $blk );
			}
			if ( $el.length ) {
				console.log("Now at " + ix );
				$blk.animate({
					scrollTop: $blk.scrollTop() + ( $el.position().top - $blk.position().top )
                }, 500);
				$ct.data( 'searchpos', ix );
			}
		});

		jQuery( 'button#tbprev' ).on( 'click.luaview', function( ev ) {
			var $ct = $( 'div#tblogdata' );
			var $blk = jQuery( 'pre', $ct );
			var ix = $ct.data( 'searchpos' ) || 0;
			ix -= 1;
			$el = $( 'span.tbsrch[data-index="' + ix + '"]', $blk );
			if ( ix < 1 || 0 === $el.length ) {
				ix = $( 'span.tbsrch', $blk ).length;
				$el = $( 'span.tbsrch[data-index="' + ix + '"]', $blk );
			}
			if ( $el.length ) {
				console.log("Now at " + ix );
				$blk.animate({
					scrollTop: $blk.scrollTop() + ( $el.position().top - $blk.position().top )
                }, 500);
				$ct.data( 'searchpos', ix );
			}
		});
	}

	function doDonate()
	{
		api.setCpanelContent('

If you find LuaView useful, please consider making a small donation toward its ongoing support! I am grateful for any support you give!

'); } myModule = { initModule: initModule, onBeforeCpanelClose: onBeforeCpanelClose, doLua: function() { try { doLua(); } catch (ex) { console.log(ex); alert(ex); } }, doLog: function() { try { doLog(); } catch (ex) { console.log(ex); alert(ex); } }, doDonate: doDonate }; return myModule; })(api, $);