/**
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
*/
package org.waveprotocol.wave.client.wavepanel.view.dom;
import static org.waveprotocol.wave.client.wavepanel.view.dom.DomViewHelper.getAfter;
import static org.waveprotocol.wave.client.wavepanel.view.dom.DomViewHelper.getBefore;
import static org.waveprotocol.wave.client.wavepanel.view.dom.DomViewHelper.load;
import static org.waveprotocol.wave.client.wavepanel.view.dom.full.CollapsibleBuilder.COLLAPSED_ATTRIBUTE;
import static org.waveprotocol.wave.client.wavepanel.view.dom.full.CollapsibleBuilder.COLLAPSED_VALUE;
import static org.waveprotocol.wave.client.wavepanel.view.dom.full.CollapsibleBuilder.TOTAL_BLIPS_ATTRIBUTE;
import static org.waveprotocol.wave.client.wavepanel.view.dom.full.CollapsibleBuilder.UNREAD_BLIPS_ATTRIBUTE;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Display;
import org.waveprotocol.wave.client.common.util.UserAgent;
import org.waveprotocol.wave.client.wavepanel.view.dom.full.CollapsibleBuilder;
import org.waveprotocol.wave.client.wavepanel.view.dom.full.CollapsibleBuilder.Components;
import org.waveprotocol.wave.client.wavepanel.view.dom.full.WavePanelResourceLoader;
/**
* DOM implementation of an inline thread.
*
*/
final class CollapsibleDomImpl implements DomView {
/** Shorthand. */
private final static CollapsibleBuilder.Css CSS = WavePanelResourceLoader.getCollapsible().css();
/** The DOM element of this view. */
private final Element self;
/** The HTML id of {@code self}. */
private final String id;
//
// UI fields for both intrinsic and structural elements.
// Element references are loaded lazily and cached.
//
private Element toggle;
private Element dropContainer;
private Element chrome;
private Element countTotal;
private Element countUnread;
/**
* Indicates if this thread has been repaired. Since some HTML parsers rip
* block elements (divs) out of inline elements (spans), the DOM structure of
* this view gets broken when transmitted via remote rendering. Repairing this
* view just means restoring the structure that the browser destroyed.
*/
private boolean isRepaired;
CollapsibleDomImpl(Element e, String id) {
this.self = e;
this.id = id;
}
public static CollapsibleDomImpl of(Element e) {
return new CollapsibleDomImpl(e, e.getId());
}
public static CollapsibleDomImpl ofToggle(Element e) {
return of(Document.get().getElementById(Components.TOGGLE.getBaseId(e.getId())));
}
public boolean isCollapsed() {
// Because of client/server CSS mismatches, we can not use css class names
// to communicate state information. This only leaves the possibility of
// explicit attributes.
return COLLAPSED_VALUE.equals(self.getAttribute(COLLAPSED_ATTRIBUTE));
}
public boolean isRead() {
boolean read = true;
if (self.hasAttribute(UNREAD_BLIPS_ATTRIBUTE)) {
String val = self.getAttribute(UNREAD_BLIPS_ATTRIBUTE);
int unread = Integer.parseInt(val);
read = unread <= 0;
}
return read;
}
public void setCollapsed(boolean collapsed) {
if (collapsed) {
self.setAttribute(COLLAPSED_ATTRIBUTE, COLLAPSED_VALUE);
// Webkit's incremental layout is incorrect, so we have to kick it a bit.
if (UserAgent.isWebkit()) {
self.getStyle().setDisplay(Display.INLINE_BLOCK);
// Force layout.
self.getOffsetParent();
// Revert to CSS display property (layed out correctly now).
self.getStyle().clearDisplay();
}
} else {
self.removeAttribute("c");
}
updatedCssClassNames();
}
/**
* Helper method to update the proper CSS class names on the toggle and
* chrome elements after a state change.
*/
private void updatedCssClassNames() {
String readStateClass = " " + (isRead() ? CSS.read() : CSS.unread());
String collapsedStateClass = " " + (isCollapsed() ? CSS.collapsed() : CSS.expanded());
getToggle().setClassName(CSS.toggle() + collapsedStateClass + readStateClass);
getChrome().setClassName(CSS.chrome() + collapsedStateClass);
getDropContainer().setClassName(CSS.dropContainer() + collapsedStateClass);
}
//
// Structure exposed for external control.
//
private Element getToggle() {
return toggle == null ? toggle = load(id, Components.TOGGLE) : toggle;
}
private Element getDropContainer() {
return dropContainer == null
? dropContainer = load(id, Components.DROP_CONTAINER) : dropContainer;
}
public Element getChrome() {
return chrome == null ? chrome = load(id, Components.CHROME) : chrome;
}
public Element getCountTotal() {
return countTotal == null ? countTotal = load(id, Components.COUNT_TOTAL) : countTotal;
}
public Element getCountUnread() {
return countUnread == null ? countUnread = load(id, Components.COUNT_UNREAD) : countUnread;
}
public void setTotalBlipCount(int totalBlipCount) {
self.setAttribute(TOTAL_BLIPS_ATTRIBUTE, "" + totalBlipCount);
getCountTotal().setInnerText("" + totalBlipCount);
}
public void setUnreadBlipCount(int unreadBlipCount) {
self.setAttribute(UNREAD_BLIPS_ATTRIBUTE, "" + unreadBlipCount);
Element unread = getCountUnread();
unread.setInnerText("(" + unreadBlipCount + ")");
if (unreadBlipCount > 0) {
unread.getStyle().clearDisplay();
} else {
unread.getStyle().setDisplay(Display.NONE);
}
updatedCssClassNames();
}
public void remove() {
getElement().removeFromParent();
}
/**
* Re-assembles this thread's DOM after the expected browser munging from
* static parsing.
*/
private void maybeRepair() {
if (!isRepaired) {
self.appendChild(getChrome());
isRepaired = true;
}
}
//
// DomView nature.
//
@Override
public Element getElement() {
maybeRepair();
return self;
}
@Override
public String getId() {
return id;
}
//
// Structure.
//
Element getBlipBefore(Element ref) {
return getBefore(getChrome(), ref);
}
Element getBlipAfter(Element ref) {
return getAfter(getChrome(), ref);
}
//
// Equality.
//
@Override
public boolean equals(Object obj) {
return DomViewHelper.equals(this, obj);
}
@Override
public int hashCode() {
return DomViewHelper.hashCode(this);
}
}