001: /*
002: * This file is part of the Echo Web Application Framework (hereinafter "Echo").
003: * Copyright (C) 2002-2005 NextApp, Inc.
004: *
005: * Version: MPL 1.1/GPL 2.0/LGPL 2.1
006: *
007: * The contents of this file are subject to the Mozilla Public License Version
008: * 1.1 (the "License"); you may not use this file except in compliance with
009: * the License. You may obtain a copy of the License at
010: * http://www.mozilla.org/MPL/
011: *
012: * Software distributed under the License is distributed on an "AS IS" basis,
013: * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
014: * for the specific language governing rights and limitations under the
015: * License.
016: *
017: * Alternatively, the contents of this file may be used under the terms of
018: * either the GNU General Public License Version 2 or later (the "GPL"), or
019: * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
020: * in which case the provisions of the GPL or the LGPL are applicable instead
021: * of those above. If you wish to allow use of your version of this file only
022: * under the terms of either the GPL or the LGPL, and not to allow others to
023: * use your version of this file under the terms of the MPL, indicate your
024: * decision by deleting the provisions above and replace them with the notice
025: * and other provisions required by the GPL or the LGPL. If you do not delete
026: * the provisions above, a recipient may use your version of this file under
027: * the terms of any one of the MPL, the GPL or the LGPL.
028: */
029:
030: package nextapp.echo2.webcontainer.syncpeer;
031:
032: import org.w3c.dom.Document;
033: import org.w3c.dom.DocumentFragment;
034: import org.w3c.dom.Element;
035: import org.w3c.dom.Node;
036:
037: import nextapp.echo2.app.Border;
038: import nextapp.echo2.app.Color;
039: import nextapp.echo2.app.Component;
040: import nextapp.echo2.app.Extent;
041: import nextapp.echo2.app.Font;
042: import nextapp.echo2.app.ImageReference;
043: import nextapp.echo2.app.Insets;
044: import nextapp.echo2.app.Column;
045: import nextapp.echo2.app.LayoutData;
046: import nextapp.echo2.app.layout.ColumnLayoutData;
047: import nextapp.echo2.app.update.ServerComponentUpdate;
048: import nextapp.echo2.webcontainer.ContainerInstance;
049: import nextapp.echo2.webcontainer.DomUpdateSupport;
050: import nextapp.echo2.webcontainer.PartialUpdateManager;
051: import nextapp.echo2.webcontainer.RenderContext;
052: import nextapp.echo2.webcontainer.RenderState;
053: import nextapp.echo2.webcontainer.ComponentSynchronizePeer;
054: import nextapp.echo2.webcontainer.SynchronizePeerFactory;
055: import nextapp.echo2.webcontainer.image.ImageRenderSupport;
056: import nextapp.echo2.webcontainer.partialupdate.BorderUpdate;
057: import nextapp.echo2.webcontainer.partialupdate.ColorUpdate;
058: import nextapp.echo2.webcontainer.partialupdate.InsetsUpdate;
059: import nextapp.echo2.webcontainer.propertyrender.BorderRender;
060: import nextapp.echo2.webcontainer.propertyrender.CellLayoutDataRender;
061: import nextapp.echo2.webcontainer.propertyrender.ColorRender;
062: import nextapp.echo2.webcontainer.propertyrender.ExtentRender;
063: import nextapp.echo2.webcontainer.propertyrender.FontRender;
064: import nextapp.echo2.webcontainer.propertyrender.InsetsRender;
065: import nextapp.echo2.webrender.output.CssStyle;
066: import nextapp.echo2.webrender.servermessage.DomUpdate;
067:
068: /**
069: * Synchronization peer for <code>nextapp.echo2.app.Column</code> components.
070: * <p>
071: * This class should not be extended or used by classes outside of the
072: * Echo framework.
073: */
074: public class ColumnPeer implements ComponentSynchronizePeer,
075: DomUpdateSupport, ImageRenderSupport {
076:
077: /**
078: * <code>RenderState</code> implementation.
079: */
080: private static class ColumnPeerRenderState implements RenderState {
081:
082: /**
083: * The child <code>Component</code> which had the highest index during
084: * the last rendering. This information is necessary when rendering
085: * cell spacing, as the last component will not have a "spacing" row
086: * beneath it. Thus, if it is no longer the last component due to an
087: * add, one will need to be added beneath it.
088: */
089: public Component lastChild;
090: }
091:
092: private PartialUpdateManager partialUpdateManager;
093:
094: /**
095: * Default constructor.
096: */
097: public ColumnPeer() {
098: partialUpdateManager = new PartialUpdateManager();
099: partialUpdateManager.add(Column.PROPERTY_BORDER,
100: new BorderUpdate(Column.PROPERTY_BORDER, null,
101: BorderUpdate.CSS_BORDER));
102: partialUpdateManager.add(Column.PROPERTY_FOREGROUND,
103: new ColorUpdate(Column.PROPERTY_FOREGROUND, null,
104: ColorUpdate.CSS_COLOR));
105: partialUpdateManager.add(Column.PROPERTY_BACKGROUND,
106: new ColorUpdate(Column.PROPERTY_BACKGROUND, null,
107: ColorUpdate.CSS_BACKGROUND_COLOR));
108: partialUpdateManager.add(Column.PROPERTY_INSETS,
109: new InsetsUpdate(Column.PROPERTY_INSETS, null,
110: InsetsUpdate.CSS_PADDING));
111: }
112:
113: /**
114: * @see nextapp.echo2.webcontainer.ComponentSynchronizePeer#getContainerId(nextapp.echo2.app.Component)
115: */
116: public String getContainerId(Component child) {
117: return ContainerInstance.getElementId(child.getParent())
118: + "_cell_" + ContainerInstance.getElementId(child);
119: }
120:
121: /**
122: * @see nextapp.echo2.webcontainer.image.ImageRenderSupport#getImage(nextapp.echo2.app.Component, java.lang.String)
123: */
124: public ImageReference getImage(Component component, String imageId) {
125: // Only source of ImageReferences from this component are CellLayoutData background images:
126: // Delegate work to CellLayoutDataRender convenience method.
127: return CellLayoutDataRender.getCellLayoutDataBackgroundImage(
128: component, imageId);
129: }
130:
131: /**
132: * Returns the <code>ColumnLayoutData</code> of the given child,
133: * or null if it does not provide layout data.
134: *
135: * @param child the child component
136: * @return the layout data
137: * @throws java.lang.RuntimeException if the the provided
138: * <code>LayoutData</code> is not a <code>ColumnLayoutData</code>
139: */
140: private ColumnLayoutData getLayoutData(Component child) {
141: LayoutData layoutData = (LayoutData) child
142: .getRenderProperty(Component.PROPERTY_LAYOUT_DATA);
143: if (layoutData == null) {
144: return null;
145: } else if (layoutData instanceof ColumnLayoutData) {
146: return (ColumnLayoutData) layoutData;
147: } else {
148: throw new RuntimeException(
149: "Invalid LayoutData for Column Child: "
150: + layoutData.getClass().getName());
151: }
152: }
153:
154: /**
155: * @see nextapp.echo2.webcontainer.ComponentSynchronizePeer#renderAdd(nextapp.echo2.webcontainer.RenderContext,
156: * nextapp.echo2.app.update.ServerComponentUpdate, java.lang.String, nextapp.echo2.app.Component)
157: */
158: public void renderAdd(RenderContext rc,
159: ServerComponentUpdate update, String targetId,
160: Component component) {
161: Element domAddElement = DomUpdate.renderElementAdd(rc
162: .getServerMessage());
163: DocumentFragment htmlFragment = rc.getServerMessage()
164: .getDocument().createDocumentFragment();
165: renderHtml(rc, update, htmlFragment, component);
166: DomUpdate.renderElementAddContent(rc.getServerMessage(),
167: domAddElement, targetId, htmlFragment);
168: }
169:
170: /**
171: * Renders a child component.
172: *
173: * @param rc the relevant <code>RenderContext</code>
174: * @param update the update
175: * @param parentElement the HTML element which should contain the child
176: * @param child the child component to render
177: */
178: private void renderAddChild(RenderContext rc,
179: ServerComponentUpdate update, Element parentElement,
180: Component child) {
181: ComponentSynchronizePeer syncPeer = SynchronizePeerFactory
182: .getPeerForComponent(child.getClass());
183: if (syncPeer instanceof DomUpdateSupport) {
184: ((DomUpdateSupport) syncPeer).renderHtml(rc, update,
185: parentElement, child);
186: } else {
187: syncPeer
188: .renderAdd(rc, update, getContainerId(child), child);
189: }
190: }
191:
192: /**
193: * Renders child components which were added to a <code>Column</code>, as
194: * described in the provided <code>ServerComponentUpdate</code>.
195: *
196: * @param rc the relevant <code>RenderContext</code>
197: * @param update the update
198: */
199: private void renderAddChildren(RenderContext rc,
200: ServerComponentUpdate update) {
201: Element domAddElement = DomUpdate.renderElementAdd(rc
202: .getServerMessage());
203: Column column = (Column) update.getParent();
204: String elementId = ContainerInstance.getElementId(column);
205:
206: Component[] components = update.getParent()
207: .getVisibleComponents();
208: Component[] addedChildren = update.getAddedChildren();
209:
210: for (int componentIndex = components.length - 1; componentIndex >= 0; --componentIndex) {
211: boolean childFound = false;
212: for (int addedChildrenIndex = 0; !childFound
213: && addedChildrenIndex < addedChildren.length; ++addedChildrenIndex) {
214: if (addedChildren[addedChildrenIndex] == components[componentIndex]) {
215: DocumentFragment htmlFragment = rc
216: .getServerMessage().getDocument()
217: .createDocumentFragment();
218: renderChild(rc, update, htmlFragment, column,
219: components[componentIndex]);
220: if (componentIndex == components.length - 1) {
221: DomUpdate.renderElementAddContent(rc
222: .getServerMessage(), domAddElement,
223: elementId, htmlFragment);
224: } else {
225: DomUpdate
226: .renderElementAddContent(
227: rc.getServerMessage(),
228: domAddElement,
229: elementId,
230: elementId
231: + "_cell_"
232: + ContainerInstance
233: .getElementId(components[componentIndex + 1]),
234: htmlFragment);
235: }
236: childFound = true;
237: }
238: }
239: }
240:
241: // Special case: Recall the child which was rendered at the last index of the column on the previous
242: // rendering. If this child is still present but is no longer at the last index, render a spacing
243: // cell beneath it (if necessary).
244: ColumnPeerRenderState renderState = (ColumnPeerRenderState) rc
245: .getContainerInstance().getRenderState(column);
246: if (renderState != null && renderState.lastChild != null) {
247: int previousLastChildIndex = column
248: .visibleIndexOf(renderState.lastChild);
249: if (previousLastChildIndex != -1
250: && previousLastChildIndex != column
251: .getVisibleComponentCount() - 1) {
252: // At this point it is known that the child which was previously last is present, but is no longer last.
253:
254: // In the event the child was removed and re-added, the special case is unnecessary.
255: boolean lastChildMoved = false;
256: for (int i = 0; i < addedChildren.length; ++i) {
257: if (renderState.lastChild == addedChildren[i]) {
258: lastChildMoved = true;
259: }
260: }
261: if (!lastChildMoved) {
262: DocumentFragment htmlFragment = rc
263: .getServerMessage().getDocument()
264: .createDocumentFragment();
265: renderSpacingCell(htmlFragment, column,
266: renderState.lastChild);
267: DomUpdate
268: .renderElementAddContent(
269: rc.getServerMessage(),
270: domAddElement,
271: elementId,
272: elementId
273: + "_cell_"
274: + ContainerInstance
275: .getElementId(components[previousLastChildIndex + 1]),
276: htmlFragment);
277: }
278: }
279: }
280: }
281:
282: /**
283: * Renders an individual child component of the <code>Column</code>.
284: *
285: * @param rc the relevant <code>RenderContext</code>
286: * @param update the <code>ServerComponentUpdate</code> being performed
287: * @param parentNode the containing node to which the child should be
288: * appended
289: * @param child The child <code>Component</code> to be rendered
290: */
291: private void renderChild(RenderContext rc,
292: ServerComponentUpdate update, Node parentNode,
293: Component component, Component child) {
294: Document document = parentNode.getOwnerDocument();
295: String childId = ContainerInstance.getElementId(child);
296: Element divElement = document.createElement("div");
297: String cellId = ContainerInstance.getElementId(component)
298: + "_cell_" + childId;
299: divElement.setAttribute("id", cellId);
300:
301: // Configure cell style.
302: CssStyle cssStyle = new CssStyle();
303: ColumnLayoutData layoutData = getLayoutData(child);
304: CellLayoutDataRender.renderToElementAndStyle(divElement,
305: cssStyle, child, layoutData, "0px");
306: CellLayoutDataRender.renderBackgroundImageToStyle(cssStyle, rc,
307: this , component, child);
308: if (layoutData != null) {
309: ExtentRender.renderToStyle(cssStyle, "height", layoutData
310: .getHeight());
311: }
312: divElement.setAttribute("style", cssStyle.renderInline());
313:
314: parentNode.appendChild(divElement);
315:
316: renderSpacingCell(parentNode, (Column) component, child);
317:
318: renderAddChild(rc, update, divElement, child);
319: }
320:
321: /**
322: * @see nextapp.echo2.webcontainer.ComponentSynchronizePeer#renderDispose(nextapp.echo2.webcontainer.RenderContext,
323: * nextapp.echo2.app.update.ServerComponentUpdate, nextapp.echo2.app.Component)
324: */
325: public void renderDispose(RenderContext rc,
326: ServerComponentUpdate update, Component component) {
327: }
328:
329: /**
330: * @see nextapp.echo2.webcontainer.DomUpdateSupport#renderHtml(nextapp.echo2.webcontainer.RenderContext,
331: * nextapp.echo2.app.update.ServerComponentUpdate, org.w3c.dom.Node, nextapp.echo2.app.Component)
332: */
333: public void renderHtml(RenderContext rc,
334: ServerComponentUpdate update, Node parentNode,
335: Component component) {
336: Column column = (Column) component;
337:
338: Document document = parentNode.getOwnerDocument();
339: Element divElement = document.createElement("div");
340: divElement.setAttribute("id", ContainerInstance
341: .getElementId(column));
342:
343: CssStyle divCssStyle = new CssStyle();
344: BorderRender.renderToStyle(divCssStyle, (Border) column
345: .getRenderProperty(Column.PROPERTY_BORDER));
346: ColorRender.renderToStyle(divCssStyle, (Color) column
347: .getRenderProperty(Column.PROPERTY_FOREGROUND),
348: (Color) column
349: .getRenderProperty(Column.PROPERTY_BACKGROUND));
350: FontRender.renderToStyle(divCssStyle, (Font) column
351: .getRenderProperty(Column.PROPERTY_FONT));
352: Insets insets = (Insets) column
353: .getRenderProperty(Column.PROPERTY_INSETS);
354: if (insets == null) {
355: divCssStyle.setAttribute("padding", "0px");
356: } else {
357: InsetsRender.renderToStyle(divCssStyle, "padding", insets);
358: }
359:
360: divElement.setAttribute("style", divCssStyle.renderInline());
361:
362: parentNode.appendChild(divElement);
363:
364: Component[] children = column.getVisibleComponents();
365: for (int i = 0; i < children.length; ++i) {
366: renderChild(rc, update, divElement, component, children[i]);
367: }
368:
369: storeRenderState(rc, column);
370: }
371:
372: /**
373: * Renders removal operations for child components which were removed from
374: * a <code>Column</code>, as described in the provided
375: * <code>ServerComponentUpdate</code>.
376: *
377: * @param rc the relevant <code>RenderContext</code>
378: * @param update the update
379: */
380: private void renderRemoveChildren(RenderContext rc,
381: ServerComponentUpdate update) {
382: Component[] removedChildren = update.getRemovedChildren();
383: Component parent = update.getParent();
384: String parentId = ContainerInstance.getElementId(parent);
385: for (int i = 0; i < removedChildren.length; ++i) {
386: String childId = ContainerInstance
387: .getElementId(removedChildren[i]);
388: DomUpdate.renderElementRemove(rc.getServerMessage(),
389: parentId + "_cell_" + childId);
390: DomUpdate.renderElementRemove(rc.getServerMessage(),
391: parentId + "_spacing_" + childId);
392: }
393:
394: int componentCount = parent.getVisibleComponentCount();
395: if (componentCount > 0) {
396: DomUpdate
397: .renderElementRemove(
398: rc.getServerMessage(),
399: parentId
400: + "_spacing_"
401: + ContainerInstance
402: .getElementId(parent
403: .getVisibleComponent(componentCount - 1)));
404: }
405: }
406:
407: /**
408: * Renders a "spacing cell" beneath a column cell to provide
409: * cell spacing.
410: *
411: * @param parentNode the containing node to which the child
412: * should be appended
413: * @param column the <code>Column</code> being updated
414: * @param child the child preceeding the spacing column
415: */
416: private void renderSpacingCell(Node parentNode, Column column,
417: Component child) {
418: Extent cellSpacing = (Extent) column
419: .getRenderProperty(Column.PROPERTY_CELL_SPACING);
420: if (!ExtentRender.isZeroLength(cellSpacing)
421: && column.visibleIndexOf(child) != column
422: .getVisibleComponentCount() - 1) {
423: Element spacingElement = parentNode.getOwnerDocument()
424: .createElement("div");
425: spacingElement.setAttribute("id", ContainerInstance
426: .getElementId(column)
427: + "_spacing_"
428: + ContainerInstance.getElementId(child));
429: CssStyle spacingCssStyle = new CssStyle();
430: spacingCssStyle.setAttribute("height", ExtentRender
431: .renderCssAttributeValue(cellSpacing));
432: spacingCssStyle.setAttribute("font-size", "1px");
433: spacingCssStyle.setAttribute("line-height", "0px");
434: spacingElement.setAttribute("style", spacingCssStyle
435: .renderInline());
436: parentNode.appendChild(spacingElement);
437: }
438: }
439:
440: /**
441: * @see nextapp.echo2.webcontainer.ComponentSynchronizePeer#renderUpdate(nextapp.echo2.webcontainer.RenderContext,
442: * nextapp.echo2.app.update.ServerComponentUpdate, java.lang.String)
443: */
444: public boolean renderUpdate(RenderContext rc,
445: ServerComponentUpdate update, String targetId) {
446: // Determine if fully replacing the component is required.
447: boolean fullReplace = false;
448: if (update.hasUpdatedLayoutDataChildren()) {
449: // TODO: Perform fractional update on LayoutData change instead of full replace.
450: fullReplace = true;
451: } else if (update.hasUpdatedProperties()) {
452: if (!partialUpdateManager.canProcess(rc, update)) {
453: fullReplace = true;
454: }
455: }
456:
457: if (fullReplace) {
458: // Perform full update.
459: DomUpdate.renderElementRemove(rc.getServerMessage(),
460: ContainerInstance.getElementId(update.getParent()));
461: renderAdd(rc, update, targetId, update.getParent());
462: } else {
463: // Perform incremental updates.
464: if (update.hasRemovedChildren()) {
465: renderRemoveChildren(rc, update);
466: }
467: if (update.hasUpdatedProperties()) {
468: partialUpdateManager.process(rc, update);
469: }
470: if (update.hasAddedChildren()) {
471: renderAddChildren(rc, update);
472: }
473: }
474:
475: storeRenderState(rc, update.getParent());
476: return fullReplace;
477: }
478:
479: /**
480: * Update the stored <code>RenderState</code>.
481: *
482: * @param rc the relevant <code>RenderContext</code>
483: * @param component the <code>Column</code> component
484: */
485: private void storeRenderState(RenderContext rc, Component component) {
486: int componentCount = component.getVisibleComponentCount();
487: ColumnPeerRenderState renderState = new ColumnPeerRenderState();
488: if (componentCount > 0) {
489: renderState.lastChild = component
490: .getVisibleComponent(componentCount - 1);
491: }
492: rc.getContainerInstance()
493: .setRenderState(component, renderState);
494: }
495: }
|