Auto-scroll on add ingredient, add Ingredient Group

This commit is contained in:
Tim Holloway 2022-01-07 09:30:34 -05:00
parent a06d8e8f79
commit e859a51772
9 changed files with 203 additions and 13 deletions

View File

@ -92,10 +92,12 @@ public class JSFUtils {
*/ */
public static Object getFlash(String key) { public static Object getFlash(String key) {
Flash flash = flashScope(); Flash flash = flashScope();
if ( flash == null ) { try {
return flash.get(key);
} catch ( NullPointerException nex ) {
// If session is expired, flash.get() will fail!
return null; return null;
} }
return flashScope().get(key);
} }
public static void putFlash(String key, Object value) { public static void putFlash(String key, Object value) {

View File

@ -21,7 +21,6 @@ import javax.inject.Named;
import javax.servlet.http.Part; import javax.servlet.http.Part;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.primefaces.PrimeFaces;
import org.primefaces.event.FileUploadEvent; import org.primefaces.event.FileUploadEvent;
import org.primefaces.model.UploadedFile; import org.primefaces.model.UploadedFile;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -40,8 +39,6 @@ import com.mousetech.gourmetj.springweb.PictureController;
* Backing bean for display/edit recipe detail * Backing bean for display/edit recipe detail
* *
* @author timh * @author timh
* @since Jun 28, 2012 TODO: Cross-reference ingredients to
* keylookup TODO: Cross-reference shopcats
*/ */
@Named @Named
@ -616,7 +613,7 @@ public class RecipeDetailBean implements Serializable {
if (recipeService.save(this.getRecipe())) { if (recipeService.save(this.getRecipe())) {
userSession.setRecipe(null); userSession.setRecipe(null);
return "main"; return "recipeDetails";
} else { } else {
JSFUtils.addErrorMessage("Save recipe failed"); JSFUtils.addErrorMessage("Save recipe failed");
return null; return null;
@ -1031,12 +1028,11 @@ public class RecipeDetailBean implements Serializable {
* Add new group to bottom of model as AJAX operation. * Add new group to bottom of model as AJAX operation.
* @return null * @return null
*/ */
public String doAddGroup() { public void doAddGroup() {
IngredientUI iui = new IngredientUI(null); IngredientUI iui = new IngredientUI(null);
iui.setIngGroup(true); iui.setIngGroup(true);
iui.setItem(this.getNewGroupName()); iui.setItem(this.getNewGroupName());
List<IngredientUI> ingUIList = this.getWrappedIngredients(); List<IngredientUI> ingUIList = this.getWrappedIngredients();
ingUIList.add(iui); ingUIList.add(iui);
return null;
} }
} }

View File

@ -1,5 +1,6 @@
package com.mousetech.gourmetj; package com.mousetech.gourmetj;
import javax.faces.application.ViewExpiredException;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import javax.servlet.ServletException; import javax.servlet.ServletException;
@ -22,6 +23,7 @@ public class SpringPrimeFacesApplication {
final String errorPage = "/error/error.html"; final String errorPage = "/error/error.html";
final String error404Page = "/error/error404.html"; final String error404Page = "/error/error404.html";
final String expiredPage = "/error/viewExpired.html";
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(SpringPrimeFacesApplication.class, SpringApplication.run(SpringPrimeFacesApplication.class,
@ -59,6 +61,9 @@ public class SpringPrimeFacesApplication {
ErrorPageRegistry registry) { ErrorPageRegistry registry) {
registry.addErrorPages(new ErrorPage( registry.addErrorPages(new ErrorPage(
HttpStatus.NOT_FOUND, error404Page)); HttpStatus.NOT_FOUND, error404Page));
registry.addErrorPages(new ErrorPage(
ViewExpiredException.class,
expiredPage));
registry.addErrorPages(new ErrorPage( registry.addErrorPages(new ErrorPage(
HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR,
errorPage)); errorPage));

View File

@ -5,6 +5,10 @@ import java.io.Serializable;
import javax.enterprise.context.SessionScoped; import javax.enterprise.context.SessionScoped;
import javax.inject.Named; import javax.inject.Named;
import org.primefaces.PrimeFaces;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.mousetech.gourmetj.persistence.model.Category; import com.mousetech.gourmetj.persistence.model.Category;
import com.mousetech.gourmetj.persistence.model.Recipe; import com.mousetech.gourmetj.persistence.model.Recipe;
@ -18,6 +22,12 @@ public class UserSession implements Serializable {
private static final long serialVersionUID = private static final long serialVersionUID =
7449440266704831598L; 7449440266704831598L;
/* Logger */
private static final Logger log =
LoggerFactory.getLogger(UserSession.class);
private String lastSearch = ""; private String lastSearch = "";
/** /**
@ -112,7 +122,7 @@ public class UserSession implements Serializable {
} }
return sb.toString(); return sb.toString();
} }
public String formatTime(Long ltime) { public String formatTime(Long ltime) {
if (ltime == null) { if (ltime == null) {
return ""; return "";
@ -145,4 +155,28 @@ public class UserSession implements Serializable {
} }
return sb.toString().trim(); return sb.toString().trim();
} }
// Primefaces handle session timeout
/**
* Session timeout, msec.
*/
private long sessionTimeoutInterval = 300000;
/**
* @return the sessionTimeoutInterval
*/
public long getSessionTimeoutInterval() {
return sessionTimeoutInterval;
}
public void sessionIdleListener() {
log.warn("Session Idle Listener fired.");
PrimeFaces.current()
.executeScript("sessionExpiredConfirmation.show()");
}
public void logoutAction() {
log.warn("Session Idle listener logout");
}
} }

View File

@ -29,6 +29,30 @@
href="http://www.apache.org/licenses/LICENSE-2.0" href="http://www.apache.org/licenses/LICENSE-2.0"
>Apache License, Version 2.0</a>. >Apache License, Version 2.0</a>.
<p>Based on Gourmet Recipe Manager by T. Hinkle</p> <p>Based on Gourmet Recipe Manager by T. Hinkle</p>
<h:form id="ftmTimeout">
<p:idleMonitor
timeout="#{userSession.sessionTimeoutInterval}"
onidle="sessionExpiredConfirmation.show()"
>
<p:ajax event="idle"
listener="#{userSession.sessionIdleListener}"
/>
</p:idleMonitor>
<p:confirmDialog closable="false"
id="sessionExpiredDlg"
message="Your session expired."
header="#{msgs['confirmDialog.initiatingDestroyProcess.label']}"
severity="alert"
widgetVar="sessionExpiredConfirmation"
style="z-index: 25000"
>
<p:commandButton id="confirmRouteDel"
value="Ok"
oncomplete="sessionExpiredConfirmation.hide()"
actionListener="#{userSession.logoutAction}"
/>
</p:confirmDialog>
</h:form>
</h:body> </h:body>
</f:view> </f:view>
</ui:composition> </ui:composition>

View File

@ -12,6 +12,7 @@
<ui:composition template="/WEB-INF/layout/layout.xhtml"> <ui:composition template="/WEB-INF/layout/layout.xhtml">
<ui:define name="title">Gourmet Recipe Manager</ui:define> <ui:define name="title">Gourmet Recipe Manager</ui:define>
<ui:define name="content"> <ui:define name="content">
<h:outputScript name="js/scrolltable.js"/>
<style> <style>
.ingSel { .ingSel {
width: 3em; width: 3em;
@ -307,6 +308,7 @@
action="#{recipeDetailBean.doAddIngredient}" action="#{recipeDetailBean.doAddIngredient}"
process="@this ctlAddIngTxt" process="@this ctlAddIngTxt"
update="pnlIngredients" update="pnlIngredients"
oncomplete="scrollAndFocus('form1:tabGroupClient' , 'ingredientTable', 'ctlAddIngTxt');"
/> />
<p:outputLabel for="@next" <p:outputLabel for="@next"
value="Add Ingredient: " value="Add Ingredient: "
@ -352,12 +354,12 @@
</p:tab> </p:tab>
</p:tabView> </p:tabView>
<p:commandButton id="doSave" value="Save" <p:commandButton id="doSave" value="Save"
icon="pi pi-check" icon="ui-icon-pencil"
disabled="{not recipeDetailBean.dirty}" disabled="{not recipeDetailBean.dirty}"
action="#{recipeDetailBean.doSave}" action="#{recipeDetailBean.doSave}"
/> />
<p:commandButton id="doCancel" value="Cancel" <p:commandButton id="doCancel" value="Cancel"
immediate="true" action="/main.jsf" immediate="true" action="recipeDetails.jsf"
/> />
</h:form> </h:form>
</p:panel> </p:panel>
@ -377,7 +379,7 @@
value="OK" style="width: 6em" value="OK" style="width: 6em"
action="#{recipeDetailBean.doAddGroup}" action="#{recipeDetailBean.doAddGroup}"
update="form1:tabGroupClient:ingredientTable" update="form1:tabGroupClient:ingredientTable"
oncomplete="PF('addGroupDlg').hide()" oncomplete="PF('addGroupDlg').hide(); scrollAndFocus('form1:tabGroupClient' , 'ingredientTable', 'ctlAddIngTxt');"
/> />
<p:commandButton id="agDlgCan" <p:commandButton id="agDlgCan"
value="Cancel" style="width: 6em" value="Cancel" style="width: 6em"

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<html>
<head>
<title>ERROR - Page Expired</title>
</head>
<body>
<h1>Page Expired</h1>
<p>The page state could not be restored because it was left
idle too long.</p>
<p><a href="/main.jsf">Return to Main Page</a></p>
</body>
</html>

View File

@ -0,0 +1,114 @@
// scrolltable.js - scroll functions for dataTable
function scrollAndFocus(rootId, tableId, txtId) {
scrollToBottom(rootId + ":" + tableId);
$(rootId +":" + txtId).focus();
}
function scrollToBottom(idDataTable) {
pTable = digestTable(idDataTable);
scrollToRow(pTable, 9999);
}
function digestTable(idDataTable) {
pTable = {
id: idDataTable
}
pTable.jqId = '#' + idDataTable.replaceAll(':', '\\:');
pTable.jqData = pTable.jqId + "_data";
pTable.heignt = $(pTable.jqId + " .ui-datatable-scrollable-body").height();
//var lichildren = $(pTable.jqId).children("tr");
var lichildren = $(pTable.jqId).find("tr");
pTable.itemHeight = lichildren.height();
pTable.itemCount = lichildren.length;
pTable.visibleItemCount = pTable.height / pTable.itemHeight;
return pTable;
}
function scrollToRow(pTable, selItem) {
var maxScrollItem = pTable.itemCount - pTable.visibleItemCount;
var scrollTopInPx;
if(selItem >= maxScrollItem)
{
// scroll table to the bottom
scrollTopInPx = pTable.itemCount * pTable.itemHeight;
}
else if(selItem < 2)
{
// scroll table to the top
scrollTopInPx = 0;
}
else
{
// scroll selected item to the 1.2 th position
scrollTopInPx = (selItem - 1.2) * pTable.itemHeight;
}
$(pTable.jqId + " .ui-datatable-scrollable-body").animate({scrollTop:scrollTopInPx, duration: 20}, scrollTopInPx);
}
/**
* This function scrolls the selected Item of a
* <p:dataTable id="idDataTable" selectionMode="single"
* into the visible area
* <p:dataTable renderes to a scroll-container with the css-class: ui-datatable-scrollable-body (inside the element with the id : "idDataTable")
* and to a container holding all items with the id: "idDataTable"_data
*
* @param idDataTable z.B.: idForm:idDataTable
*/
function autoScrollPDatatable(idDataTable)
{
// for selection in JQuery the ids with : must be encoded with \\:
var primfcid = idDataTable.replace(':', '\\:');
var idDataTbl = '#' + primfcid;
var idDataContainer = idDataTbl + "_data";
var totalHeight = $(idDataTbl + " .ui-datatable-scrollable-body").height();
var lichildren = $(idDataContainer).children("tr");
var itemHeight = $(idDataContainer).children("tr").height();
var anzItems = lichildren.length;
var anzVisItems = totalHeight / itemHeight;
var selItem = detectDataTableScrollPos(lichildren);
if(selItem == -1)
{
// no selection found...
return;
}
var maxScrollItem = anzItems - anzVisItems;
var scrollTopInPx;
if(selItem >= maxScrollItem)
{
// scroll table to the bottom
scrollTopInPx = maxScrollItem * itemHeight;
}
else if(selItem < 2)
{
// scroll table to the top
scrollTopInPx = 0;
}
else
{
// scroll selected item to the 1.2 th position
scrollTopInPx = (selItem - 1.2) * itemHeight;
}
$(idDataTbl + " .ui-datatable-scrollable-body").animate({scrollTop:scrollTopInPx}, scrollTopInPx);
}
function detectDataTableScrollPos(liChildren)
{
for (i=0;i< liChildren.length;++i)
{
var chd = liChildren[i];
var x = chd.getAttribute("aria-selected");
if(x === "true")
{
return i;
}
}
return -1;
}

View File

@ -25,7 +25,7 @@
<p:commandButton value="Back" <p:commandButton value="Back"
icon="ui-icon-arrowthick-1-w" icon="ui-icon-arrowthick-1-w"
style="margin-left: 2em" immediate="true" style="margin-left: 2em" immediate="true"
action="/main.jsf" action="/recipeDetails.jsf"
/> />
</p:column> </p:column>
</p:panelGrid> </p:panelGrid>