CellTable с пользовательским заголовком, содержащим SearchBox и проблему с фокусом

Я пытаюсь реализовать CellTable с настраиваемым заголовком столбца, который отображает SearchBox (простое текстовое поле) под обычным текстом столбца.
SearchBox должен разрешить пользователю фильтровать CellTable. Он должен выглядеть примерно так:

  |Header  1|Header 2 |
  |SEARCHBOX|SEARCHBOX|
  -------------------------------------------------------
  |    ROW 1 
  ------------------------------------------------------
  |    ROW 2 

Как только пользователь вводит символ в SearchBox, запускается RangeChangeEvent, который приводит к запросам сервера, а CellTable обновляется с новым фильтрованным списком.

В принципе все работает нормально. Однако, как только CellTable обновляется, SearchBox теряет фокус, и пользователь снова должен щелкнуть мышью в SearchBox, чтобы ввести новый символ.

Это, вероятно, связано с тем, что метод визуализации настраиваемого заголовка и его ячейки вызывается после обновления CellTable.
Есть ли способ вернуть фокус обратно в SearchBox? Я попытался установить tabindex = 0, но это не помогло.

Пользовательский класс заголовка

public static class SearchHeader extends Header<SearchTerm> {
    @Override
    public void render(Context context, SafeHtmlBuilder sb) {
        super.render(context, sb);
    }
    private SearchTerm searchTerm;
    public SearchHeader(SearchTerm searchTerm,ValueUpdater<SearchTerm> valueUpdater) {
        super(new SearchCell());
        setUpdater(valueUpdater);
        this.searchTerm = searchTerm;
    }
    @Override
    public SearchTerm getValue() {
        return searchTerm;
    }
 }

Пользовательская ячейка поиска (используется в пользовательском заголовке)

Булевский флаг isChanged имеет значение true, когда пользователь вводит что-то в SearchBox и возвращается к false, если SearchBox теряет его внимание. Я добавил этот флаг, чтобы отличить, какой SearchBox получает фокус (если я использую несколько SearchBoxes)

public static class SearchCell extends AbstractCell<SearchTerm> {

    interface Template extends SafeHtmlTemplates {
        @Template("<div style=\"\">{0}</div>")
        SafeHtml header(String columnName);

        @Template("<div style=\"\"><input type=\"text\" value=\"{0}\"/></div>")
        SafeHtml input(String value);
    }

    private static Template template;
    private boolean isChanged = false;

    public SearchCell() {
        super("keydown","keyup","change","blur");
        if (template == null) {
            template = GWT.create(Template.class);
        }
    }

    @Override
    public void render(com.google.gwt.cell.client.Cell.Context context,
        SearchTerm value, SafeHtmlBuilder sb) {
        sb.append(template.header(value.getCriteria().toString()));
        sb.append(template.input(value.getValue()));
    }

    @Override
    public void onBrowserEvent(Context context,Element parent, SearchTerm value,NativeEvent event,ValueUpdater<SearchTerm> valueUpdater) {
        if (value == null)
            return;
        super.onBrowserEvent(context, parent, value, event, valueUpdater);
        if ("keyup".equals(event.getType()))
        {
            isChanged = true;
            InputElement elem = getInputElement(parent);
            value.setValue(elem.getValue());
            if (valueUpdater != null)
                valueUpdater.update(value);
        }
        else if ("blur".equals(event.getType())) {
            isChanged =false;
        }
     }

     protected InputElement getInputElement(Element parent) {
         Element elem = parent.getElementsByTagName("input").getItem(0);
         assert(elem.getClass() == InputElement.class);
         return elem.cast();
     }
}

Код инициализации CellTable

NameColumn - это реализация абстрактного класса Column с соответствующими типами. Он использует TextCell внутри.

ValueUpdater<SearchTerm> searchUpdater = new ValueUpdater<SearchTerm>() {
    @Override
    public void update(AccessionCellTableColumns.SearchTerm value) {
        // fires a server request to return the new filtered list
        RangeChangeEvent.fire(table, new Range(table.getPageStart(), table.getPageSize())); 
    }
};

table.addColumn(new NameColumn(searchTerm),new SearchHeader(searchTerm,searchUpdater));

Ответ 1

Skinny

К сожалению, поддержка GWT для настраиваемых заголовков столбцов немного менее важна. Если бы у кого-то была радость работать с классами AbstractCell, вы бы знали, что я имею в виду. Кроме того, надлежащий способ реализации составных (вложенных виджетов) в ячейке заголовка столбца является бюстом, так как я не смог заставить его работать правильно и не нашел каких-либо работоспособных примеров работы CompositeCell.

Если ваш datagrid реализует сортировку ColumnSortHandler (LOL thats phunny), ваши вложенные объекты пользовательского интерфейса, которые могут иметь события key или mouse, будут вызывать сортировку столбцов. ПОТЕРПЕТЬ НЕУДАЧУ. Опять же, я не мог найти способ перегрузить события столбцов, чтобы исключить триггеры, запущенные взаимодействием с вложенным заголовком столбца ui components/widgets. Не говоря уже о том, что вам нужно абстрактно определить вложенные компоненты, написав встроенный HTML в интерфейс Template, который строит вашу ячейку. Не очень элегантный выбор, поскольку он заставляет разработчиков писать собственный JavaScript-код для создания и управления обработчиками, связанными с вложенными компонентами/виджетами в заголовке столбца.

Этот "правильный" метод реализации также не решает проблему фокуса, которую этот вопрос затрагивает, и не является прекрасным решением для сложных данных, которые нуждаются в наборах данных AsyncProvider (или ListProvider) с фильтрацией ячеек столбцов или пользовательским рендерингом. Производительность этого также meh > _ > Далеко от правильного решения IMO

Серьезно???

Чтобы реализовать фильтрацию ячеек функциональных столбцов, вы должны решить это из более традиционного динамического javascript/css appoarch с дней до GWT и сумасшедших библиотек JQuery. Мое функциональное решение - это гибрид "правильного" способа с некоторыми хитрыми css.

код psuedo выглядит следующим образом:

  • убедитесь, что ваша сетка завернута с помощью LayoutPanel
  • убедитесь, что ваши столбцы сетки управляются с помощью коллекции/списка
  • создать собственный заголовок столбца для создания области для фильтрации
  • создайте фильтр-контейнер, чтобы разместить текстовые поля в
  • разместите свои дочерние элементы контейнеров сетки (сетка, фильтр, пейджер)
  • использовать методы CSS для размещения фильтров в заголовках столбцов.
  • добавить обработчики событий для фильтров
  • добавить таймер для обработки задержки ввода фильтра
  • функция обновления сетки огня для обновления данных, асинхронного или локального списка

Вот, надеюсь, что я еще не потерял тебя, поскольку есть много дел, чтобы сделать эту работу.


Шаг 1. Настройка класса сетки для расширения LayoutPanel

Сначала вам нужно убедиться, что ваш класс, который создает вашу сетку, может поддерживать и правильно оценивать ваше приложение. Для этого убедитесь, что ваш класс сетки расширяет LayoutPanel.

public abstract class PagingFilterDataGrid<T> extends LayoutPanel {
     public PagingFilterDataGrid() {
          //ctor initializers
          initDataGrid();
          initColumns();
          updateColumns();
          initPager();
          setupDataGrid();
     }
}

Шаг 2. Создание управляемых столбцов

Этот шаг также довольно прост. Вместо этого непосредственно добавлены новые столбцы в ваш datagrid, сохраняйте их в списке, затем программно добавляйте их в свою сетку с помощью инструкции foreach

ColumnModel (вы должны иметь возможность создавать число или дату или любой другой тип столбца, который вы хотите. Для простоты я обычно работаю со строковыми данными в веб-приложениях, если только я явно не нуждаюсь в специальных функциях арифметики или даты)

public abstract class GridStringColumn<M> extends Column<VwGovernorRule, String> {

    private String  text_;
    private String  tooltip_;
    private boolean defaultShown_ = true;
    private boolean hidden_       = false;

    public GridStringColumn(String fieldName, String text, String tooltip, boolean defaultShown, boolean sortable, boolean hidden) {
        super(new TextCell());
        setDataStoreName(fieldName);
        this.text_ = text;
        this.tooltip_ = tooltip;
        this.defaultShown_ = defaultShown;
        setSortable(sortable);
        this.hidden_ = hidden;
    }
}

создать список в вашем классе datagrid для хранения столбцов в

public abstract class PagingFilterDataGrid<T> extends LayoutPanel {
    private List<GridStringColumn<T>> columns_ = new ArrayList<GridStringColumn<T>>();
}

чтобы создать столбцы, создайте метод initColumn, который вызывается в вашем конструкторе datagrid. Обычно я расширяю базовый класс datagrid, чтобы я мог вставлять свои инициализаторы сетки. Это добавляет столбец в хранилище столбцов. MyPOJODataModel - это ваша структура данных, в которой хранятся ваши записи для datagrid, обычно это POJO вашего спящего режима или что-то из вашего бэкэнда.

@Override
public void initColumns() {
     getColumns().add(new GridStringColumn<MyPOJODataModel>("columnName", "dataStoreFieldName", "column tooltip / description information about this column", true, true, false) {

            @Override
            public String getValue(MyPOJODataModelobject) {
                return object.getFieldValue();
            }
        });
}

создайте код для обновления столбцов в вашей сетке, убедитесь, что вы вызываете этот метод после вызова метода initColumns. метод initFilters, который мы получим в ближайшее время. Но если вам нужно знать сейчас, именно этот метод настраивает ваши фильтры на основе того, какие столбцы у вас есть в вашей коллекции. Вы также можете вызвать эту функцию, когда хотите показывать/скрывать столбцы или изменять порядок столбцов в своей сетке. Я знаю, что ты его любишь!

@SuppressWarnings("unchecked")
    public void updateColumns() {
        if (dataGrid_.getColumnCount() > 0) {
            clearColumns();
        }

        for (GridStringColumn<T> column : getColumns()) {
            if (!column.isHidden()) {
                dataGrid_.addColumn((Column<T, ?>) column, new ColumnHeader(column.getText(), column.getDataStoreName()));
            }
        }

        initFilters();
    }

Шаг 3. Создание пользовательского столбца столбца

Теперь мы начинаем получать удовольствие от того, что у нас есть сетка и столбцы, готовые для фильтрации. Эта часть похожа на пример кода, который задает этот вопрос, но он немного отличается. То, что мы делаем здесь, - это создать новый пользовательский AbstractCell, который мы определяем html-шаблоном для GWT для рендеринга во время выполнения. Затем мы вставляем этот новый шаблон ячейки в наш собственный класс заголовка и передаем его в метод addColumn(), который gwt использует для создания нового столбца в вашей сетке данных.

Ваша пользовательская ячейка:

final public class ColumnHeaderFilterCell extends AbstractCell<String> {

    interface Templates extends SafeHtmlTemplates {
        @SafeHtmlTemplates.Template("<div class=\"headerText\">{0}</div>")
        SafeHtml text(String value);

        @SafeHtmlTemplates.Template("<div class=\"headerFilter\"><input type=\"text\" value=\"\"/></div>")
        SafeHtml filter();
    }

    private static Templates templates = GWT.create(Templates.class);

    @Override
    public void render(Context context, String value, SafeHtmlBuilder sb) {
        if (value == null) {
            return;
        }

        SafeHtml renderedText = templates.text(value);

        sb.append(renderedText);

        SafeHtml renderedFilter = templates.filter();
        sb.append(renderedFilter);
    }
}

Если вы не научились ненавидеть то, как вы создаете пользовательские ячейки, вы вскоре убедитесь, что после того, как вы это сделаете. Затем нам понадобится заголовок, чтобы ввести эту ячейку в

Заголовок столбца:

public static class ColumnHeader extends Header<String> {

        private String name_;

        public ColumnHeader(String name, String field) {
            super(new ColumnHeaderFilterCell());
            this.name_ = name;
            setHeaderStyleNames("columnHeader " + field);
        }

        @Override
        public String getValue() {
            return name_;
        }
    }

как вы можете видеть, это довольно простой и простой класс. Честно говоря, он больше похож на обертку, поэтому GWT подумал о том, чтобы объединить их в конкретную ячейку заголовка столбца, а затем вводить общую ячейку, вне меня. Может быть, не супер-фантазия, но я уверен, что было бы гораздо легче работать с

если вы посмотрите выше на свой метод updateColumns(), вы увидите, что он создает новый экземпляр этого класса headheader, когда он добавляет столбец. Также убедитесь, что вы достаточно точны с тем, что вы делаете статичным и окончательным, чтобы вы не разбивали память, когда создаете очень большие наборы данных... IE 1000 строк в 20 столбцах - 20000 вызовов или экземпляров шаблона или членов, которые вы сохранили. Так что, если один член в вашей ячейке или заголовке имеет 100 байт, который превращается в 2 МБ или ресурсы или больше только для CTOR. Опять же, это не так важно, как создание персонализированных ячеек данных, но оно по-прежнему важно и для ваших заголовков.

Теперь не забудьте добавить свой css

.gridData table {
    overflow: hidden;
    white-space: nowrap;
    table-layout: fixed;
    border-spacing: 0px;
}

.gridData table td {
    border: none;
    border-right: 1px solid #DBDBDB;
    border-bottom: 1px solid #DBDBDB;
    padding: 2px 9px
}

.gridContainer .filterContainer {
    position: relative;
    z-index: 1000;
    top: 28px;
}

.gridContainer .filterContainer td {
    padding: 0 13px 0 5px;
    width: auto;
    text-align: center;
}

.gridContainer .filterContainer .filterInput {
    width: 100%;
}

.gridData table .columnHeader {
    white-space: normal;
    vertical-align: bottom;
    text-align: center;
    background-color: #EEEEEE;
    border-right: 1px solid #D4D4D4;
}

.gridData table .columnHeader  div img {
    position: relative;
    top: -18px;
}

.gridData table .columnHeader .headerText {
    font-size: 90%;
    line-height: 92%;
}

.gridData table .columnHeader .headerFilter {
    visibility: hidden;
    height: 32px;
}

теперь это css для всех вещей, которые вы собираетесь добавить. Я слишком ленив, чтобы отделить его, плюс я думаю, что вы можете понять это. gridContainer - это макетная панель, которая обертывает ваш datagrid, а gridData - ваша фактическая сетка данных.

Теперь, когда вы компилируете, вы должны увидеть пробел ниже текста заголовка столбца. Здесь вы можете поместить свои фильтры в css

Шаг 4. Создание контейнера фильтров

теперь нам нужно что-то, чтобы вставить наши входные данные фильтра. У этого контейнера также есть css, который будет перемещать его вниз в пространство, которое мы только что создали в заголовках. Да, правильно, что фильтры, которые находятся в заголовке, фактически и технически отсутствуют в заголовке. Это единственный способ избежать проблемы с сортировкой и потерять проблему с фокусом.

private HorizontalPanel filterContainer_ = new HorizontalPanel();

и инициализация фильтра

public void initFilters() {
        filterContainer_.setStylePrimaryName("filterContainer");

        for (GridStringColumn<T> column : getColumns()) {
            if (!column.isHidden()) {
                Filter filterInput = new Filter(column);
                filters_.add(filterInput);
                filterContainer_.add(filterInput);
                filterContainer_.setCellWidth(filterInput, "auto");
            }
        }
    }

вы можете видеть, что для сборки столбцов требуется сбор данных, чтобы правильно создавать входные данные фильтра, которые входят в ваш контейнер. Кроме того, класс фильтра также передается в столбце, чтобы связать столбец с конкретным фильтром. Это позволяет вам получить доступ к полям и таким

public class Filter extends TextBox {

        final private GridStringColumn<T> boundColumn_;

        public Filter(GridStringColumn<T> column) {
            super();
            boundColumn_ = column;
            addStyleName("filterInput " + boundColumn_.getDataStoreName());
            addKeyUpHandler(new KeyUpHandler() {

                @Override
                public void onKeyUp(KeyUpEvent event) {
                    filterTimer.cancel();
                    filterTimer.schedule(FILTER_DELAY);
                }
            });
        }

        public GridStringColumn<T> getBoundColumn() {
            return boundColumn_;
        }
    }

Шаг 5: добавьте свои компоненты в LayoutPanel

Теперь, когда вы создаете сетку для добавления пейджера и сетки в контейнер макета, мы не учитываем вертикальную высоту, которую обычно должен принимать фильтр. Поскольку он установлен в относительное положение с z-index greated, то, что имеет сетка и столбцы, он будет отображаться в самом заголовке. МАГИЯ!!!

public void setupDataGrid() {
        add(pagerContainer_);
        setWidgetTopHeight(pagerContainer_, 0, Unit.PX, PAGER_HEIGHT, Unit.PX);
        add(filterContainer_);
        setWidgetTopHeight(filterContainer_, PAGER_HEIGHT + FILTER_HEIGHT, Unit.PX, FILTER_HEIGHT, Unit.PX);
        add(dataGrid_);
        setWidgetTopHeight(dataGrid_, PAGER_HEIGHT, Unit.PX, ScreenManager.getScreenHeight() - PAGER_HEIGHT - BORDER_HEIGHT, Unit.PX);


        pager_.setVisible(true);
        dataGrid_.setVisible(true);
    }

и теперь для некоторых констант

final private static int PAGER_HEIGHT = 32;
final private static int FILTER_HEIGHT = 32;
final private static int BORDER_HEIGHT = 2;

Высота границы для конкретного стиля css, которое может иметь ваше приложение, технически это пусковое пространство, чтобы убедиться, что все плотно прилегает.

Шаг 6. Использование CSS Magic

конкретный css, который помещает фильтры в ваши столбцы сверху, это

.gridContainer .filterContainer {
    position: relative;
    z-index: 1000;
    top: 28px;
}

который переместит контейнер над столбцами и расположит там слой над вашими заголовками.

Далее нам нужно убедиться, что ячейки в filterContainer совпадают с ячейками в нашем datagrid

.gridContainer .filterContainer td {
    padding: 0 13px 0 5px;
    width: auto;
    text-align: center;
}

далее убедитесь, что наши входы масштабируются в соответствии с размером ячейки контейнера, в которой они живут,

.gridContainer .filterContainer .filterInput {
    width: 100%;
}

Наконец, мы хотим перенести наш индикатор сортировки изображения так, чтобы текстовые поля ввода фильтра не скрывали их

.gridData table.columnHeader div img {   позиция: относительная;   наверх: -18px; }

теперь, когда вы компилируете, вы должны увидеть фильтры над заголовками столбцов. вам может потребоваться настроить CSS, чтобы они точно выстроились в очередь. Это также предполагает, что у вас нет специальных ширины столбцов. если вы это сделаете, вам нужно будет создать дополнительную функциональность, чтобы вручную установить размеры ячеек и настроить ширину для синхронизации с столбцами. Я пропустил это для santity.


* теперь его время для разрыва, ваше почти там! ^ ________ ^ *


Шаг 7 и 8: Добавить обработчики событий

это легкая часть. если вы посмотрите на класс фильтра сверху, обратите внимание на этот метод body

addKeyUpHandler(new KeyUpHandler() {

                @Override
                public void onKeyUp(KeyUpEvent event) {
                    filterTimer.cancel();
                    filterTimer.schedule(FILTER_DELAY);
                }
            });

создать таймер фильтра

private FilterTimer filterTimer = new FilterTimer();

убедитесь, что вы указали поле в корне тела класса, а не в строке.

private class FilterTimer extends Timer {

        @Override
        public void run() {
            updateDataList();
        }
    }

требуется таймер, чтобы событие не запускалось каждый раз, когда пользователь вводит значение. Многие люди добавили onblur или других глупых обработчиков, но это бессмысленно. Пользователь может вводить данные только в одно поле за раз, поэтому его точка-заглушка. просто используйте обработчик onKeyUp.

Шаг 9: Обновите сетку

теперь, когда мы вызываем updateDataList (который также должен вызываться из вашего события onRangeChanged (для сортировки и загрузки данных), мы хотим итерации, если наша коллекция фильтров для наших примененных фильтров вошла пользователем. Лично я храню все запросите параметры в хэш-карту для легкого доступа и обновления. Затем просто передайте всю карту в мой механизм запросов, который делает ваши RPC или RequestFactory.

public void updateDataList() {
        initParameters();

        // required parameters controlled by datagrid
        parameters_.put("limit", limit_ + "");
        parameters_.put("offset", offset_ + "");

        // sort parameters
        if (sortField_.equals("") || sortOrder_.equals("")) {
            parameters_.remove("sortField");
            parameters_.remove("sortDir");
        } else {
            parameters_.put("sortField", sortField_);
            parameters_.put("sortDir", sortOrder_);
        }

        // filter parameters
        for (Filter filter : filters_) {
            if (!filter.getValue().equals("")) {
                CGlobal.LOG.info("filter: " + filter.getBoundColumn().getDataStoreName() + "=" + filter.getValue());
                parameters_.put(filter.getBoundColumn().getDataStoreName(), filter.getValue());
            }
        }

        RequestServiceAsync requestService = (RequestServiceAsync) GWT.create(RequestService.class);
        requestService.getRequest("RPC", getParameters(), new ProviderAsyncCallback());
    }

вы можете увидеть, как и почему нам нужно привязать фильтр к столбцу, поэтому, когда мы итерации, хотя фильтры, мы можем получить имя сохраненного поля. обычно я просто передаю имя поля и значение фильтра в качестве параметра запроса, а затем передаю все фильтры как один параметр запроса фильтра. Это более расширяемый способ, и редко есть edgecases, в которых ваши столбцы db должны == зарезервировать слова для ваших параметров запроса, таких как sortDir или sortField выше.

* Готово < _____ > *


Я надеюсь, что это поможет всем вам с некоторыми передовыми данными gwt datagrid. Я знаю, что это была боль, чтобы создать себя, поэтому, надеюсь, это спасет вас всю кучу времени в будущем. Гудлак!