001 package edu.nrao.sss.webapp.faces.component; 002 003 import javax.faces.component.UIInput; 004 import javax.faces.context.FacesContext; 005 import javax.faces.context.ResponseWriter; 006 import javax.faces.convert.ConverterException; 007 import javax.faces.el.ValueBinding; 008 009 import java.io.IOException; 010 import java.util.Map; 011 import java.util.List; 012 import java.util.ArrayList; 013 import java.util.Hashtable; 014 import java.util.Collections; 015 016 import org.apache.log4j.Logger; 017 018 import edu.nrao.sss.webapp.faces.renderer.RendererUtils; 019 020 public class NotesUIComponent extends UIInput 021 { 022 private static final Logger log = Logger.getLogger(NotesUIComponent.class); 023 024 protected static final String RENDERED_SCRIPT_KEY = 025 "edu.nrao.sss.webapp.faces.component.NotesUIComponent.JavaScriptRendered"; 026 027 private Boolean enabled = null; 028 private Hashtable<String, Boolean> collapsedState = new Hashtable<String, Boolean>(); 029 030 public void encodeBegin(FacesContext context) 031 throws IOException 032 { 033 if (!isRendered()) 034 return; 035 036 List notes = (List)getValue(); 037 ResponseWriter writer = context.getResponseWriter(); 038 039 String id = getClientId(context); 040 Map attrs = getAttributes(); 041 String styleClass = (String)attrs.get("styleClass"); 042 043 RendererUtils.writeLineSeparator(writer); 044 writer.startElement("div", this); 045 writer.writeAttribute("id", id, null); 046 writer.writeAttribute("class", styleClass, null); 047 048 if (notes != null && notes.size() > 0) 049 { 050 int size = notes.size(); 051 052 if (isEnabled()) 053 { 054 //render new note link 055 writer.write('\n'); 056 encodeNewLink(writer, id); 057 } 058 059 for (int i = 0; i < size; i++) 060 { 061 Object n = notes.get(i); 062 writer.write('\n'); 063 encodeNote(context, id, n.toString(), i); 064 } 065 } 066 067 else 068 { 069 writer.write('\n'); 070 writer.startElement("em", this); 071 writer.write("There are currently no notes available."); 072 writer.endElement("em"); 073 writer.write('\n'); 074 075 if (isEnabled()) 076 { 077 //render new note link 078 writer.write('\n'); 079 encodeNewLink(writer, id); 080 } 081 } 082 083 writer.write('\n'); 084 writer.endElement("div"); 085 writer.write('\n'); 086 } 087 088 /** 089 * Encodes <code>note</code> rendering the note, a collapse/expand button, 090 * and a remove link (depending on the isEnabled flag). 091 */ 092 protected void encodeNote(FacesContext context, String id, String note, int index) 093 throws IOException 094 { 095 String nid = getNoteId(context, index); 096 String hid = getNoteHeaderId(nid); 097 String sumid = getNoteSummaryId(nid); 098 099 if (note == null) 100 { 101 note = ""; 102 } 103 104 ResponseWriter writer = context.getResponseWriter(); 105 Map attrs = getAttributes(); 106 String headerClass = (String)attrs.get("headerClass"); 107 108 writer.startElement("span", this); 109 writer.writeAttribute("id", hid, null); 110 writer.writeAttribute("class", headerClass, null); 111 writer.write("Note #" + (index + 1)); 112 writer.write("\n"); 113 114 if (isEnabled()) 115 { 116 //Remove link that pops up a warning dialog... 117 encodeRemoveLink(writer, id, nid, hid); 118 writer.write('\n'); 119 } 120 121 //If collapsed, show summary 122 String summary = note; 123 if (note.length() > 30) 124 { 125 summary = note.substring(0, 27) + "..."; 126 } 127 128 writer.startElement("span", this); 129 writer.writeAttribute("id", sumid, null); 130 writer.writeAttribute("style", (isCollapsed(nid))? "" : "display:none;", null); 131 writer.write(summary); 132 writer.endElement("span"); 133 writer.write('\n'); 134 135 //collapse button w/ styleClass 136 encodeCollapseButton(writer, nid, sumid); 137 writer.write('\n'); 138 139 writer.endElement("span"); 140 writer.write('\n'); 141 142 if (isEnabled()) 143 { 144 //textarea w/styleClass 145 writer.startElement("textarea", this); 146 writer.writeAttribute("class", "xxx", null); 147 writer.writeAttribute("id", nid, null); 148 writer.writeAttribute("name", nid, null); 149 writer.writeAttribute("style", (isCollapsed(nid))? "display:none;" : "", null); 150 writer.write('\n'); 151 writer.write(note.toString()); 152 writer.endElement("textarea"); 153 } 154 155 else 156 { 157 //span with note if not enabled. 158 writer.startElement("div", this); 159 writer.writeAttribute("class", "xxx", null); 160 writer.writeAttribute("id", nid, null); 161 writer.writeAttribute("style", (isCollapsed(nid))? "display:none;" : "", null); 162 String formattedNote = note.replaceAll("\n", "<p/>"); 163 writer.write('\n'); 164 writer.write(formattedNote); 165 writer.endElement("div"); 166 } 167 } 168 169 /** 170 * Writes a link that will create a new note widget (via JS). 171 * @param id The client id of this component 172 */ 173 protected void encodeNewLink(ResponseWriter writer, String id) 174 throws IOException 175 { 176 writer.startElement("a", this); 177 writer.writeAttribute("href", "#", null); 178 writer.writeAttribute("onclick", 179 "return NotesUIComponent_NewNote('" + id + "');", 180 null 181 ); 182 writer.write("(New Note)"); 183 writer.endElement("a"); 184 writer.write('\n'); 185 } 186 187 protected void encodeRemoveLink(ResponseWriter writer, String pid, String nid, String hid) 188 throws IOException 189 { 190 String linkStyleClass = (String)getAttributes().get("linkStyleClass"); 191 String dialogStyleClass = (String)getAttributes().get("dialogStyleClass"); 192 String removeLinkClass = (String)getAttributes().get("removeLinkClass"); 193 194 String dialogId = nid + "_popup"; 195 196 writer.write('\n'); 197 writer.write('\n'); 198 199 //Start a wrapper div with position: relative so the popup can be 200 //positioned relative to it. 201 writer.startElement("div", this); 202 203 if (removeLinkClass != null) 204 writer.writeAttribute("class", removeLinkClass, null); 205 206 writer.write('\n'); 207 208 //This is our link section. 209 writer.startElement("a", this); 210 writer.writeAttribute("href", "#", null); 211 212 if (linkStyleClass != null) 213 writer.writeAttribute("class", linkStyleClass, null); 214 215 writer.writeAttribute("onclick", 216 "return NotesUIComponent_OpenRemoveNoteWarning('" + dialogId + "');", 217 null 218 ); 219 220 writer.write("(Remove Note)"); 221 222 writer.endElement("a"); 223 writer.write('\n'); 224 225 //This is our popupDialog 226 writer.startElement("div", this); 227 writer.writeAttribute("id", dialogId, null); 228 writer.writeAttribute("style", "display:none;", null); 229 230 if (dialogStyleClass != null) 231 writer.writeAttribute("class", dialogStyleClass, null); 232 233 writer.write('\n'); 234 235 //XXX render dialog content here 236 writer.startElement("em", this); 237 writer.write("Are you sure you want to remove this note?"); 238 writer.endElement("em"); 239 240 writer.startElement("div", this); 241 242 writer.startElement("input", this); 243 writer.writeAttribute("type", "button", null); 244 245 StringBuilder sb = new StringBuilder("return NotesUIComponent_RemoveNote('"); 246 sb.append(pid); 247 sb.append("', '"); 248 sb.append(nid); 249 sb.append("', '"); 250 sb.append(hid); 251 sb.append("');"); 252 writer.writeAttribute("onclick", sb.toString(), null); 253 254 writer.writeAttribute("value", "Remove", null); 255 writer.endElement("input"); 256 257 writer.startElement("input", this); 258 writer.writeAttribute("type", "button", null); 259 writer.writeAttribute("onclick", 260 "return NotesUIComponent_CloseRemoveNoteWarning('" + dialogId + "');", 261 null 262 ); 263 264 writer.writeAttribute("value", "Cancel", null); 265 writer.endElement("input"); 266 267 writer.endElement("div"); 268 269 writer.write('\n'); 270 271 //End the hidden popup 272 writer.endElement("div"); 273 writer.write('\n'); 274 275 //End the wrapper; 276 writer.endElement("div"); 277 writer.write('\n'); 278 writer.write('\n'); 279 } 280 281 /** 282 * encodes a link that calls javascript functions to expand and collapse a 283 * note. Also encodes a hidden field to keep track of the expanded state of 284 * the note. 285 */ 286 protected void encodeCollapseButton(ResponseWriter writer, String nid, String sumid) 287 throws IOException 288 { 289 //Render hidden field with known id so that we know the collapsed state of 290 //each note. 291 String sid = getNoteStateId(nid); 292 293 writer.write('\n'); 294 writer.startElement("input", this); 295 writer.writeAttribute("type", "hidden", null); 296 writer.writeAttribute("id", sid, null); 297 writer.writeAttribute("name", sid, null); 298 299 writer.writeAttribute("value", (isCollapsed(nid))? "true" : "false", null); 300 writer.endElement("input"); 301 writer.write('\n'); 302 303 304 Map attrs = getAttributes(); 305 String collapseLinkClass = (String)attrs.get("collapseLinkClass"); 306 307 //Write out the link 308 writer.startElement("a", this); 309 writer.writeAttribute("href", "#", null); 310 writer.writeAttribute("class", collapseLinkClass, null); 311 312 StringBuilder onclick = new StringBuilder("return NotesUIComponent_ToggleCollapse('"); 313 onclick.append(nid); 314 onclick.append("', '"); 315 onclick.append(sid); 316 onclick.append("', '"); 317 onclick.append(sumid); 318 onclick.append("');"); 319 320 writer.writeAttribute("onclick", onclick.toString(), null); 321 writer.endElement("a"); 322 writer.write('\n'); 323 } 324 325 /** 326 * Overridden to output the necessary javascript for this component. If the 327 * javascript has already be rendered for this response, nothing is done. 328 */ 329 @SuppressWarnings("unchecked") 330 public void encodeEnd(FacesContext context) 331 throws IOException 332 { 333 if (!isRendered()) 334 return; 335 336 List notes = (List)getValue(); 337 338 Map requestMap = context.getExternalContext().getRequestMap(); 339 Boolean scriptRendered = (Boolean)requestMap.get(RENDERED_SCRIPT_KEY); 340 341 if (scriptRendered != Boolean.TRUE) 342 { 343 requestMap.put(RENDERED_SCRIPT_KEY, Boolean.TRUE); 344 ResponseWriter writer = context.getResponseWriter(); 345 346 writer.startElement("script", this); 347 writer.writeAttribute("type", "text/javascript", null); 348 encodeNewNoteJSFunction(writer, (notes == null)? 0 : notes.size()); 349 encodeRemoveNoteJSFunctions(writer); 350 encodeCollapseNoteJSFunctions(writer); 351 writer.endElement("script"); 352 } 353 } 354 355 /** 356 * writes a javascript function that inserts a new notes widget into the html 357 * at the end of the list of notes widgets. 358 */ 359 protected void encodeNewNoteJSFunction(ResponseWriter writer, int numNotes) 360 throws IOException 361 { 362 Map attrs = getAttributes(); 363 String headerClass = (String)attrs.get("headerClass"); 364 String collapseLinkClass = (String)attrs.get("collapseLinkClass"); 365 366 StringBuilder sb = new StringBuilder("\nvar NotesUIComponent_NewNoteIndex = "); 367 sb.append(numNotes); 368 sb.append(";\n"); 369 sb.append("function NotesUIComponent_NewNote(pid) {\n"); 370 sb.append("\tnid = pid + ':' + NotesUIComponent_NewNoteIndex;\n"); 371 sb.append("\thid = nid + '_header';\n"); 372 sb.append("\tsid = nid + '_state';\n"); 373 sb.append("\tdialogId = nid + '_popup';\n"); 374 sb.append("\tsumid = nid + '_summary';\n\n"); 375 376 //Now we Start the code that actually adds components to the DOM and whatnot. 377 sb.append("\tparent = document.getElementById(pid);\n"); 378 sb.append("\tnoteHeader = document.createElement('span');\n"); 379 sb.append("\tnoteHeader.id = hid;\n"); 380 sb.append("\tnoteHeader.className = '" + headerClass + "';\n"); 381 sb.append("\tnoteHeader.innerHTML = 'Note #' + (NotesUIComponent_NewNoteIndex + 1);\n"); 382 383 //Begin wrapper for floating dialog (for the remove link warning) 384 sb.append("\n\tdialogWrapper = document.createElement('div');\n"); 385 sb.append("\tdialogWrapper.className = 'xxx'\n"); 386 sb.append("\tnoteHeader.appendChild(dialogWrapper);\n"); 387 388 //Now start on contents of wrapper 389 sb.append("\tdialogTrigger = document.createElement('a');\n"); 390 sb.append("\tdialogTrigger.href = '#';\n"); 391 sb.append("\tdialogTrigger.className = 'xxx';\n"); 392 sb.append("\tdialogTrigger.onclick = function(){return NotesUIComponent_OpenRemoveNoteWarning(dialogId);};\n"); 393 sb.append("\tdialogTrigger.innerHTML = '(Remove Note)';\n"); 394 sb.append("\tdialogWrapper.appendChild(dialogTrigger);\n"); 395 396 //Add the popup div... 397 sb.append("\tpopup = document.createElement('div');\n"); 398 sb.append("\tpopup.id = dialogId;\n"); 399 sb.append("\tpopup.className = 'xxx';\n"); 400 sb.append("\tpopup.style.display = 'none';\n"); 401 sb.append("\tdialogWrapper.appendChild(popup);\n"); 402 403 //Contents of popup 404 sb.append("\tquestion = document.createElement('em');\n"); 405 sb.append("\tquestion.innerHTML = 'Are you sure you want to remove this note?';\n"); 406 sb.append("\tpopup.appendChild(question);\n"); 407 408 sb.append("\tbuttonBar = document.createElement('div');\n"); 409 sb.append("\tpopup.appendChild(buttonBar);\n"); 410 sb.append("\tconfirm = document.createElement('input');\n"); 411 sb.append("\tconfirm.type = 'button';\n"); 412 sb.append("\tconfirm.value = 'Remove';\n"); 413 sb.append("\tconfirm.onclick = function(){return NotesUIComponent_RemoveNote(pid, nid, hid);};\n"); 414 sb.append("\tbuttonBar.appendChild(confirm);\n"); 415 416 //cancel button 417 sb.append("\tcancel = document.createElement('input');\n"); 418 sb.append("\tcancel.type = 'button';\n"); 419 sb.append("\tcancel.value = 'Cancel';\n"); 420 sb.append("\tcancel.onclick = function(){return NotesUIComponent_CloseRemoveNoteWarning(dialogId);};\n"); 421 sb.append("\tbuttonBar.appendChild(cancel);\n"); 422 423 //Done with popup now! Moving on to the note summary 424 sb.append("\tsummary = document.createElement('span');\n"); 425 sb.append("\tsummary.id = sumid;\n"); 426 sb.append("\tsummary.style.display = 'none';\n"); 427 sb.append("\tsummary.innerHTML = 'Enter new note text here.';\n"); 428 sb.append("\tnoteHeader.appendChild(summary);\n"); 429 430 //Now the collapse button foo. 431 sb.append("\tcollapsedState = document.createElement('input');\n"); 432 sb.append("\tcollapsedState.type = 'hidden';\n"); 433 sb.append("\tcollapsedState.id = sid;\n"); 434 sb.append("\tcollapsedState.name = sid;\n"); 435 sb.append("\tcollapsedState.value = false;\n"); 436 sb.append("\tnoteHeader.appendChild(collapsedState);\n"); 437 438 sb.append("\tcollapseLink = document.createElement('a');\n"); 439 sb.append("\tcollapseLink.href = '#';\n"); 440 sb.append("\tcollapseLink.className = '" + collapseLinkClass + "';\n"); 441 sb.append("\tcollapseLink.onclick = function (){ return NotesUIComponent_ToggleCollapse(nid, sid, sumid); };\n"); 442 sb.append("\tnoteHeader.appendChild(collapseLink);\n"); 443 444 //Deep breath...almost done. Now we can render the actual textarea 445 sb.append("\tta = document.createElement('textarea');\n"); 446 sb.append("\tta.id = nid;\n"); 447 sb.append("\tta.name = nid;\n"); 448 sb.append("\tta.className = 'xxx';\n"); 449 sb.append("\tta.innerHTML = 'Enter new note text here.';\n"); 450 451 //Now connect our 2 new high level elements (header and textarea) to the actual document. 452 sb.append("\tparent.appendChild(noteHeader);\n"); 453 sb.append("\tparent.appendChild(ta);\n"); 454 455 456 //don't forget to increment our NewNoteIndex so it works again next time. 457 sb.append("\n\tNotesUIComponent_NewNoteIndex = NotesUIComponent_NewNoteIndex + 1;\n"); 458 sb.append("\treturn false;\n"); 459 sb.append("}\n\n"); 460 461 writer.write(sb.toString()); 462 } 463 464 /** 465 * writes javascript functions that handle removing a note and poping up a 466 * dialog warning them with confirm and cancel options. 467 */ 468 protected void encodeRemoveNoteJSFunctions(ResponseWriter writer) 469 throws IOException 470 { 471 StringBuilder sb = new StringBuilder("function NotesUIComponent_OpenRemoveNoteWarning(dialogId) {\n"); 472 sb.append("\tvar elementStyle = document.getElementById(dialogId).style;\n"); 473 sb.append("\telementStyle.display='block';\n"); 474 sb.append("\treturn false;\n"); 475 sb.append("}\n\n"); 476 477 sb.append("function NotesUIComponent_CloseRemoveNoteWarning(dialogId) {\n"); 478 sb.append("\tvar elementStyle = document.getElementById(dialogId).style;\n"); 479 sb.append("\telementStyle.display='none';\n"); 480 sb.append("\treturn false;\n"); 481 sb.append("}\n\n"); 482 483 sb.append("function NotesUIComponent_RemoveNote(pid, nid, hid) {\n"); 484 sb.append("\tvar parent = document.getElementById(pid);\n"); 485 sb.append("\tvar note = document.getElementById(nid);\n"); 486 sb.append("\tvar header = document.getElementById(hid);\n"); 487 sb.append("\tparent.removeChild(header);\n"); 488 sb.append("\tparent.removeChild(note);\n"); 489 sb.append("\treturn false;\n"); 490 sb.append("}\n\n"); 491 492 writer.write(sb.toString()); 493 } 494 495 /** 496 * writes a javascript function that toggles the collapsed state of a note. 497 * Also outputs hidden form fields to keep track of said state. 498 */ 499 protected void encodeCollapseNoteJSFunctions(ResponseWriter writer) 500 throws IOException 501 { 502 StringBuilder sb = new StringBuilder("function NotesUIComponent_ToggleCollapse(nid, sid, sumid) {\n"); 503 sb.append("\tcollapsed = document.getElementById(sid);\n"); 504 sb.append("\tnote = document.getElementById(nid);\n"); 505 sb.append("\tsummary = document.getElementById(sumid);\n"); 506 sb.append("\tif (collapsed.value == 'true') {\n"); 507 sb.append("\t\tcollapsed.value = false;\n"); 508 sb.append("\t\tnote.style.display = 'block';\n"); 509 sb.append("\t\tsummary.style.display = 'none';\n"); 510 sb.append("\t}\n"); 511 sb.append("\telse {\n"); 512 sb.append("\t\tcollapsed.value = true;\n"); 513 sb.append("\t\tnote.style.display = 'none';\n"); 514 sb.append("\t\tsummary.style.display = 'inline';\n"); 515 sb.append("\t}\n"); 516 sb.append("\treturn false;\n"); 517 sb.append("}\n\n"); 518 519 writer.write(sb.toString()); 520 } 521 522 public void decode(FacesContext context) 523 { 524 String id = getClientId(context); 525 526 Map parms = context.getExternalContext().getRequestParameterMap(); 527 528 List<String> notes = new ArrayList<String>(); 529 List<String> noteIds = new ArrayList<String>(); 530 531 Hashtable<String, Boolean> state = new Hashtable<String, Boolean>(); 532 for (Object key : parms.keySet()) 533 { 534 String sKey = (String)key; 535 //XXX probably want to compile a pattern here to speed things up. 536 if (sKey.matches("^" + id + ":\\d+$")) 537 { 538 noteIds.add(sKey); 539 String sid = getNoteStateId(sKey); 540 String noteState = (String)parms.get(sid); 541 noteState = (noteState == null)? "true" : noteState; 542 state.put(sKey, Boolean.valueOf(noteState)); 543 } 544 } 545 546 setCollapsed(state); 547 548 Collections.sort(noteIds); 549 550 for (String key : noteIds) 551 { 552 notes.add((String)parms.get(key)); 553 } 554 555 setSubmittedValue(notes); 556 } 557 558 /** 559 * Override the getConvertedValue method to return the List? Or just return * the thing it gets unchanged? 560 */ 561 protected Object getConvertedValue(FacesContext context, Object newSubmittedValue) 562 throws ConverterException 563 { 564 return newSubmittedValue; 565 } 566 567 /** 568 * Overridden to compare 2 lists. 569 * @return true if the new value is different from the old value. 570 */ 571 protected boolean compareValues(Object previous, Object value) 572 { 573 log.debug("compareValues called"); 574 575 if (previous == null) 576 return value != null; 577 578 if (value == null) 579 return previous != null; 580 581 log.debug("compareValues: both non-null."); 582 if (previous == value) 583 return false; 584 585 List lP = (List)previous; 586 List lV = (List)value; 587 588 if (lP.size() != lV.size()) 589 return true; 590 591 log.debug("compareValues: both same size."); 592 for (int i = 0, size = lP.size(); i < size; i++) 593 { 594 if (!lP.get(i).equals(lV.get(i))) 595 return true; 596 } 597 598 log.debug("compareValues: identical lists."); 599 return false; 600 } 601 602 public Object saveState(FacesContext context) 603 { 604 Object[] state = new Object[3]; 605 state[0] = super.saveState(context); 606 state[1] = this.enabled; 607 state[2] = this.collapsedState; 608 609 return state; 610 } 611 612 @SuppressWarnings("unchecked") 613 public void restoreState(FacesContext context, Object state) 614 { 615 Object[] myState = (Object[])state; 616 super.restoreState(context, myState[0]); 617 setEnabled((Boolean)myState[1]); 618 setCollapsed((Hashtable)myState[2]); 619 } 620 621 public boolean isEnabled() 622 { 623 if (this.enabled != null) 624 return this.enabled; 625 626 ValueBinding vb = getValueBinding("enabled"); 627 if (vb != null) 628 { 629 Object val = vb.getValue(getFacesContext()); 630 631 if (val instanceof Boolean && val != null) 632 return (Boolean)val; 633 } 634 635 //else 636 return Boolean.TRUE; 637 } 638 639 public void setEnabled(Boolean e) 640 { 641 this.enabled = e; 642 } 643 644 public Boolean isCollapsed(String nid) 645 { 646 Boolean c = this.collapsedState.get(nid); 647 if (c == null) 648 c = Boolean.TRUE; 649 650 return c; 651 } 652 653 public void setCollapsed(Hashtable<String, Boolean> state) 654 { 655 this.collapsedState = state; 656 } 657 658 protected String getNoteId(FacesContext context, int noteIndex) 659 { 660 return getClientId(context) + ":" + noteIndex; 661 } 662 663 protected String getNoteStateId(String nid) 664 { 665 return nid + "_state"; 666 } 667 668 protected String getNoteHeaderId(String nid) 669 { 670 return nid + "_header"; 671 } 672 673 protected String getNoteSummaryId(String nid) 674 { 675 return nid + "_summary"; 676 } 677 678 /** Overridden to be empty. */ 679 public void encodeChildren(FacesContext context) throws IOException {} 680 }