// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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 com.google.collide.client.ui.tree;
import com.google.collide.client.util.Elements;
import com.google.gwt.resources.client.CssResource;
import elemental.events.Event;
import elemental.events.EventListener;
import elemental.events.KeyboardEvent;
import elemental.html.Element;
import elemental.html.InputElement;
/**
* Utility for mutating a tree data model object backing a
* {@link TreeNodeElement}, in place in a {@link Tree}.
*/
public class TreeNodeMutator<D> {
/**
* Encapsulates all needed data to perform a mutation action.
*/
public interface MutationAction<D> {
Element getElementForMutation(TreeNodeElement<D> node);
void onBeforeMutation(TreeNodeElement<D> node);
void onMutationCommit(TreeNodeElement<D> node, String oldLabel, String newLabel);
boolean passValidation(TreeNodeElement<D> node, String newLabel);
}
/**
* Css for the mutator.
*/
public interface Css extends CssResource {
String nodeNameInput();
}
private final EventListener keyListener = new EventListener() {
@Override
public void handleEvent(Event evt) {
KeyboardEvent keyEvent = (KeyboardEvent) evt;
switch (keyEvent.getKeyCode()) {
case KeyboardEvent.KeyCode.ENTER:
commitIfValid(false);
break;
case KeyboardEvent.KeyCode.ESC:
cancel();
break;
default:
return;
}
evt.stopPropagation();
}
};
private final EventListener blurListener = new EventListener() {
@Override
public void handleEvent(Event evt) {
commitIfValid(false);
}
};
private static class State<D> {
final TreeNodeElement<D> node;
final MutationAction<D> callback;
final InputElement input;
final String oldLabel;
State(TreeNodeElement<D> node, MutationAction<D> callback, InputElement input,
String oldLabel) {
this.node = node;
this.callback = callback;
this.input = input;
this.oldLabel = oldLabel;
}
}
private State<D> state;
private Css css;
/**
* @param css can be null
*/
public TreeNodeMutator(Css css) {
this.css = css;
}
public boolean isMutating() {
return state != null;
}
/**
* Replaces the nodes text label with an input box to allow the user to
* rename the node.
*/
public void enterMutation(TreeNodeElement<D> node, MutationAction<D> callback) {
// If we are already mutating, return.
if (isMutating()) {
return;
}
Element element = callback.getElementForMutation(node);
if (element == null) {
return;
}
String oldLabel = element.getTextContent();
callback.onBeforeMutation(node);
// Make a temporary text input box to grab user input, and place it inside
// the label element.
InputElement input = Elements.createInputElement();
if (css != null) {
input.setClassName(css.nodeNameInput());
}
input.setType("text");
input.setValue(oldLabel);
// Wipe the content from the element.
element.setTextContent("");
// Attach the temporary input box.
element.appendChild(input);
input.focus();
input.select();
// If we hit enter, commit the action.
input.addEventListener(Event.KEYUP, keyListener, false);
// If we lose focus, commit the action.
input.addEventListener(Event.BLUR, blurListener, false);
state = new State<D>(node, callback, input, oldLabel);
}
/**
* Cancels the mutation, if any.
*/
public void cancel() {
if (!isMutating()) {
return;
}
state.input.setValue(state.oldLabel);
forceCommit();
}
/**
* Commits the current text if it passes validation, or cancels the mutation.
*/
public void forceCommit() {
commitIfValid(true);
}
private void commitIfValid(boolean forceCommit) {
if (!isMutating()) {
return;
}
State<D> oldState = state;
state = null;
// Update the node label and commit the change if it passes validation.
boolean passedValidation =
oldState.callback.passValidation(oldState.node, oldState.input.getValue());
if (passedValidation || forceCommit) {
// Disconnect the handlers first!
oldState.input.removeEventListener(Event.KEYUP, keyListener, false);
oldState.input.removeEventListener(Event.BLUR, blurListener, false);
// Detach the input box. Note that on Chrome this synchronously dispatches
// a blur event. The guard above saves us.
oldState.input.removeFromParent();
String newLabel = passedValidation ? oldState.input.getValue() : oldState.oldLabel;
oldState.callback.onMutationCommit(oldState.node, oldState.oldLabel, newLabel);
} else {
state = oldState;
state.input.focus();
state.input.select();
}
}
}