Механизм "выделения" предназначен для универсального обозначения "выбранных" элементов, часто используется в интерфейсах.
Определение "выбранный" стоит воспринимать более широко. Такие состояния как активный (active), текущий (current) и другие, так же могут быть выражены через этот паттерн.
- Принцип работы
- Контекст выделения (Selection)
- Работа с Selection
- Единичный и множественный выбор
- Реактивные значения
- Примеры использования
Узел считается выбранным, если его свойство selected
имеет значение true
. Это значение можно изменить методами:
select(multiple)
– устанавливает значениеtrue
;unselect()
– устанавливает значениеfalse
;setSelected(selected, multiple)
- устанавливает значение в зависимости от переданного параметраselected
.
var Node = require('basis.ui').Node;
var view = new Node();
console.log(view.selected);
// > false
view.select();
console.log(view.selected);
// > true
view.unselect();
console.log(view.selected);
// > false
Значение
selected
должно меняться исключительно указанными выше методами. Параметрmultiple
описан в разделе Единичный и множественный выбор.
Когда свойство selected
становится равно true
, выбрасывается событие select
, при установке в false
– unselect
. Начальное значение можно задать при создании узла, но событие select
при этом выбрасываться не будет.
var Node = require('basis.ui').Node;
var view = new Node({
selected: true,
handler: {
select: function(){
console.log('node selected');
},
unselect: function(){
console.log('node unselected');
}
}
});
// событие select выброшено не будет
console.log(view.selected);
// > true
view.select();
// событие select выброшено не будет, так как
// значение selected не было изменено
view.unselect();
// > node unselected
view.select();
// > node selected
Изменение значения selected
без дополнительных настроек не влияет на состояние других узлов. Но нередко необходимо чтобы значение selected
учитывалось в рамках нескольких узлов. Например, в списках нужно учитывать все элементы списка. Для этой цели узлам назначается контекст выделения.
Контекст выделения задается через свойство contextSelection
. По умолчанию оно не задано (равно null
) и узел ведет себя независимо (поведение похоже на checkbox
).
Сам контекст выделения представляет собой набор, экземпляр специального класса basis.dom.wrapper.Selection
. Этот класс унаследован от basis.data.Dataset
и имеет тот же интерфейс. Его главной особенностью является синхронизация своего состава со значением свойства selected
его членов. Так, если добавить в такой набор узел, то свойство selected
узла будет выставлено в true
, а если удалить из набора, то у удаленного узла selected
станет равно false
. Справедливо и обратное, если узлу выставить значение selected
в true
, то он будет автоматически добавлен в набор, указанный в contextSelection
, а если в false
, то удален из него.
var Node = require('basis.ui').Node;
var Selection = require('basis.dom.wrapper').Selection;
var selection = new Selection();
var view = new Node({
contextSelection: selection
});
console.log(view.selected, selection.itemCount);
// > false 0
view.select();
console.log(view.selected, selection.itemCount);
// > true 1
selection.remove(view);
console.log(view.selected, selection.itemCount);
// > false 0
Во избежание конфликтов узлы нельзя добавлять в произвольный экземпляр Selection
. В экземпляр Selection
можно добавлять только те узлы, у которых свойство contextSelection
ссылается на этот набор. Так же узел должен быть экземпляром basis.dom.wrapper.Node
и его свойство selected
не должно быть реактивным.
Не смотря на то, что contextSelection
можно выставить при создании узла, его не предполагается задавать явно. Обычно контекст выделения узлы заимствуют у родителя, при его назначении.
Если у узла не задано значение contextSelection
(по умолчанию не задано) и ему назначается новый родитель, то в качестве значения contextSelection
выставляется значение свойства selection
родителя, а если оно не задано (равно null
), то значение contextSelection
родителя.
var Node = require('basis.ui').Node;
var Selection = require('basis.dom.wrapper').Selection;
var nodeWithSelection = new Node({
selection: new Selection()
});
var foo = new Node();
var bar = new Node();
nodeWithSelection.appendChild(foo);
console.log(foo.contextSelection === nodeWithSelection.selection);
// > true
foo.appendChild(bar);
console.log(bar.contextSelection === foo.contextSelection);
// > true
console.log(bar.selection);
// > null
Когда узел отвязывается от своего родителя, то значение contextSelection
выставляется в null
, при условии, что contextSelection
равен selection
родителя или, если значение selection
не задано (равно null
), совпадает с contextSelection
родителя. То есть ситуация обратная привязыванию родителю.
console.log(bar.contextSelection === foo.contextSelection);
// > true
foo.removeChild(bar);
console.log(bar.contextSelection);
// > null
console.log(foo.contextSelection === nodeWithSelection.selection);
// > true
nodeWithSelection.removeChild(foo);
console.log(foo.contextSelection);
// > null
Когда у узла (инициатор) меняется значение contextSelection
, то это же значение выставляется и всем узлам его поддерева, при условии что их значения совпадают. Если у узла в поддереве значение contextSelection
отличается от значения инициатора до изменения, то его значение не меняется и его поддерево игнорируется. Таким образом контекст выделения работает для деревьев "из коробки".
var Node = require('basis.ui').Node;
var Selection = require('basis.dom.wrapper').Selection;
var node = new Node({
selection: new Selection()
});
var foo = new Node();
var bar = new Node();
foo.appendChild(bar);
console.log(foo.contextSelection);
// > null
console.log(bar.contextSelection);
// > null
node.appendChild(foo);
console.log(foo.contextSelection === node.selection);
// > true
console.log(bar.contextSelection === node.selection);
// > true
node.removeChild(foo)
console.log(foo.contextSelection);
// > null
console.log(bar.contextSelection);
// > null
Сложность описания функционирования
contextSelection
, не приводит к сложностям использования. В основном работа осуществляется только сselection
, аcontextSelection
остается "за кадром". Главное, о чем нужно помнить, что выставление контекста выделения для узла влияет не только на его детей, но и на все его поддерево.
Как было показано в предыдущих примерах, selection
можно задать при создании узла. Это значение в дальнейшем можно изменить используя метод setSelection(selection)
. При изменении selection
срабатывает событие selectionChanged
, а так же обновляется контекст выделения поддерева узла.
var Node = require('basis.ui').Node;
var Selection = require('basis.dom.wrapper').Selection;
var node = new Node({
selection: new Selection(),
handler: {
selectionChanged: function(sender, oldSelection){
console.log('selection changed:', oldSelection, '->', this.selection);
}
},
childNodes: [
new Node()
]
});
console.log(node.firstChild.contextSelection === node.selection);
// > true
console.log(node.firstChild.contextSelection)
// > basis.dom.wrapper.Selection { .. }
node.setSelection();
// > selection changed: basis.dom.wrapper.Selection { .. } -> null
console.log(node.selection);
// > null
console.log(node.firstChild.contextSelection);
// > null
В качестве selection
можно задать только экземпляр basis.dom.wrapper.Selection
. Если задается иное значение, которое приводится к true
, то это значение считается конфигом и экземпляр Selection
создается неявно. По этой причине отпадает необходимость импортировать класс Selection
из модуля basis.dom.wrapper
.
var Node = require('basis.ui').Node;
var node = new Node({
selection: { /* настройки */ } // неявное создание new Selection({ .. })
});
// если настроек нет, можно просто передать true
var node = new Node({
selection: true
});
Для selection
так же поддерживается listen
.
var node = new Node({
selection: true,
listen: {
selection: {
itemsChanged: function(selection, delta){
// this ссылается на node
console.log('selected node count:', selection.itemCount);
}
}
},
childNodes: [
new Node()
]
});
node.firstChild.select();
// > selected node count: 1
node.selection.clear();
// > selected node count: 0
Экземпляры Selection
могут работать в двух режимах:
- единичный выбор - в один момент времени может быть выбран максимум один узел, поведение по умолчанию;
- множественный выбор – может быть выбрано произвольное количество узлов.
При единичном выборе, выбранным всегда становится последний "выбранный" узел, при этом предыдущий удаляется из набора, а его свойство selected
выставляется в false
. Если добавляется несколько узлов одновременно (например, используя метод add()
или set()
), то выделение сначала очищается, а в качестве единственного члена используется первый элемент переданного массива.
var Node = require('basis.ui').Node;
var list = new Node({
selection: true,
childNodes: [
{ name: 'foo', selected: true },
{ name: 'bar' },
{ name: 'baz' }
]
});
console.log(list.firstChild.selected);
// > true
console.log(list.selection.pick() === list.firstChild);
// > true
list.lastChild.select();
console.log(list.selection.itemCount);
// > 1
console.log(list.selection.pick() === list.lastChild);
// > true
console.log(list.firstChild.selected);
// > false
list.selection.set(list.childNodes);
console.log(list.selection.itemCount);
// > 1
console.log(list.selection.pick() === list.firstChild);
// > true
В случае множественного выбора, количество выбранных узлов не лимитируется. Чтобы включить множественный выбор нужно задать свойство multiple
равным true
при создании Selection
.
var Node = require('basis.ui').Node;
var list = new Node({
selection: { multiple: true }, // то же что new Selection({ multiple: true })
childNodes: [
{ name: 'foo' },
{ name: 'bar' },
{ name: 'baz', selected: true }
]
});
console.log(list.selection.itemCount);
// > 2
console.log(list.selection.getValues('name'));
// > ['baz']
list.selection.set(list.childNodes);
console.log(list.selection.getValues('name'));
// > ['foo', 'bar', 'baz']
Как видно из примера, выделить несколько узлов не сложно, имея доступ к узлу (в данном случае list
) и его selection
. Но обычно удобнее управлять выделенностью узла в рамках его собственной логики, то есть пользоваться его методами select()
и setSelected()
. Однако, если вызывать эти методы последовательно для нескольких узлов, выбранным окажется только последний. Это происходит по причине того, что эти методы по умолчанию используют режим одиночного выделения.
var Node = require('basis.ui').Node;
var list = new Node({
selection: { multiple: true },
childNodes: [
{ name: 'foo' },
{ name: 'bar' },
{ name: 'baz' }
]
});
console.log(list.selection.getValues('name'));
// > []
list.childNodes.forEach(function(child){
// такой вызов метода select использует режим одиночного выбора
// внутри используется child.contextSelection.set(child)
child.select();
});
console.log(list.selection.getValues('name'));
// > ['baz']
Чтобы использовать режим множественного выбора для методов select
и setSelected
, необходимо передавать дополнительный параметр multiple
равный true
. Однако такой режим не подразумевает, что узел будет только добавляться к выбранным. В этом режиме значение selected
меняется как переключатель (toggle
): если узел еще не выбран, то он становится выбранным, иначе выделение снимается. При этом значение selected
других узлов в контексте остается неизменным. Поэтому без дополнительных проверок получается инверсия выделения.
var Node = require('basis.ui').Node;
var list = new Node({
selection: { multiple: true },
childNodes: [
{ name: 'foo' },
{ name: 'bar', selected: true },
{ name: 'baz' }
]
});
console.log(list.selection.getValues('name'));
// > ['bar']
list.childNodes.forEach(function(child){
// если методу select передается true, то используется режим
// множественного выбора
child.select(true);
});
console.log(list.selection.getValues('name'));
// > ['foo', 'baz']
Чтобы гарантированно выделить все необходимые узлы, нужно использовать дополнительную проверку.
list.childNodes.forEach(function(child){
// вызываем метод select только если узел еще не выбран
if (!child.selected)
child.select(true);
});
Конечно, в простых случаях, если есть доступ к selection
, то проще использовать selection.add([node, node, ..])
.
Может показаться странной логика режима множественного выделения. Но все встает на свои места, если учесть, что механизм ориентирован на взаимодействие с пользователем. Режим множественного выделения используется если зажата клавиша ctrl
(Windows) или meta
(Mac). В результате получается простое и элегантное решение:
var Node = require('basis.ui').Node;
var list = new Node({
selection: { multiple: true },
childNodes: [
{ name: 'foo' },
{ name: 'bar' },
{ name: 'baz' }
],
childClass: {
template: '<li event-click="select">item</li>',
action: {
// данная функция будет вызываться при клике по элементу
select: function(event){
// используем режим выбора, в зависимости от зажатых клавиш
this.select(event.ctrlKey || event.metaKey);
}
}
}
});
Начиная с версии 1.4
в качестве значения selected
можно задать объект с интерфейсом binding bridge
(bb-value
). В этом случае selected
будет автоматически синхрозироваться с таким объектом (его значение приводится к boolean
). При этом selected
по прежнему хранит true
или false
, а при изменениях срабатывают события select
и unselect
. Связь с bb-value
"прячется" в приватном свойстве selectedRA_
, в котором хранится специальный адаптер-наблюдатель.
var Node = require('basis.ui').Node;
var someValue = new basis.Token(false);
var view = new Node({
selected: someValue
});
console.log(view.selected);
// > false
someValue.set(true);
console.log(view.selected);
// > true
Можно использовать эту возможность для более сложных сценариев. Например, меню, выбранный элемент которого зависит от внешнего значения.
var Node = require('basis.ui').Node;
var Value = require('basis.data').Value;
var currentName = new Value();
var menu = new Node({
childClass: {
selected: currentName.compute(function(child, name){
return child.name == name;
})
},
childNodes: [
{ name: 'foo' },
{ name: 'bar' }
]
});
console.log(menu.childNodes.map(function(child){
return child.selected;
}));
// > [false, false]
currentName.set('foo');
console.log(menu.childNodes.map(function(child){
return child.selected;
}));
// > [true, false]
currentName.set('bar');
console.log(menu.childNodes.map(function(child){
return child.selected;
}));
// > [false, true]
Особенности использования bb-value
в качестве значения selected
:
- узлы с привязанным
bb-value
не участвуют в контексте выделения и их нельзя добавить в экземплярSelection
; - вызов методов
select()
иunselect()
не меняет состояниеselected
(в режиме разработки выводится предупреждение), когда для него установленоbb-value
; - для привязки и сбрасывания
bb-value
необходимо использовать методsetSelected(newValue)
.
Примеры в демонстрациях:
- одно и то же "выделение" для нескольких компонент;
- несколько контекстов выделения в рамках одного дерева;
- использование набора выбранных узлов как источник данных для других компонент.
var Node = require('basis.ui').Node;
var list = new Node({
selection: { multiple: true }
});
list.selection.itemCount; // текущее кол-во выбранных узлов
list.selection.addHandler({
// случаем изменения
itemsChanged: function(selection){
selection.itemCount; // новое кол-во выбранных узлов
}
});
// используем Value.from
var Value = require('basis.data').Value;
var selectedCount = Value.from(list.selection, 'itemsChanged', 'itemCount');
// используем basis.data.index
var selectedCount = require('basis.data.index').count(list.selection);
var Node = require('basis.ui').Node;
var list = new Node({
selection: true,
listen: {
selection: {
// слушаем когда меняется состав выделения
itemsChanged: function(selection){
// this -> list
// если выделение не содержит узлов, делаем выбранным
// первого ребенка
if (!selection.itemCount && this.firstChild)
this.firstChild.select();
}
}
},
handler: {
// слушаем изменение состава детей
childNodesModified: function(){
if (!this.selection.itemCount && this.firstChild)
this.firstChild.select();
}
}
});
var Node = require('basis.ui').Node;
var Value = require('basis.data').Value;
var list = new Node({
selection: true
});
var view = new Node();
list.selection.addHandler({
itemsChanged: function(){
view.setDelegate(this.pick());
}
});
// альтернативное решение
// basis.js 1.3 и выше
var list = new Node({
selection: true
});
var view = new Node({
delegate: Value.from(list.selection, 'itemsChanged', 'pick()')
});
var Node = require('basis.ui').Node;
var Value = require('basis.data').Value;
var router = require('basis.router');
var menu = new Node({
selection: true,
childNodes: [
{ name: 'foo' },
{ name: 'bar' },
{ name: 'baz' }
]
});
router.add('/:name', function(name){
var child = basis.array.search(menu.childNodes, name, 'name');
menu.selection.set(child);
});
// альтернативное решение
// basis.js 1.4 и выше
var menu = new Node({
childClass: {
selected: Value
.from(router.route('/:name').param('name'))
.compute(function(child, name){
return child.name == name;
})
},
childNodes: [
{ name: 'foo' }, // будет выбран если путь '/foo'
{ name: 'bar' }, // будет выбран если путь '/bar'
{ name: 'baz' } // и т.д.
]
});
В модуле basis.ui
уже определены биндинги selected
и unselected
, в их описании нет необходимости. Здесь приведено их описание в качестве примера, как можно использовать состояние выделения в биндингах.
var Node = require('basis.ui').Node;
var view = new Node({
binding: {
selected: {
events: 'select unselect',
getter: function(node){
return node.selected;
}
},
unselected: {
events: 'select unselect',
getter: function(node){
return !node.selected;
}
}
}
});
[TODO] hasOwnSelection/nullSelection