Библиотека SVGTree, о которой шла речь в предыдущем посте, позволяет рисовать красивые деревья, однако для удобства применения ей недостает интерактивности. Не помешала бы функциональность, позволяющая пользователям обращаться с созданными деревьями в соответствии со следующими сценарии использования:
- создание и удаление вершин дерева при помощи более удобных средств, чем кнопки Insert и Delete (в мобильных устройствах этих кнопок попросту нет);
- настройка внешнего вида дерева (например, формы вершин и ребер);
- просмотр «исходного кода» дерева в Newick-формате и, возможно, его непосредственное редактирование (по аналогии с тем, как HTML-редакторы вроде TinyMCE предоставляют возможность редактировать исходный код HTML);
- откат / повторение произведенных изменений (undo / redo).
Поскольку в неинтерактивных случаях использования базовой функциональности библиотеки SVGTree вполне достаточно, новую функциональность логично выделить в отдельный компонент — SVGTreeViewer, зависящий от базовой библиотеки. Компонент представляет собой JavaScript-файл и таблицу стилей, которые в сжатом виде занимают около 15 килобайт.
Базовые примеры использования компонента приведены на этой веб-странице, еще пара есть под катом.
Например, пример, приведенный в описании алгоритмов на деревьях будет выглядеть так:
((,A,)B,((C,D),E),(F,G))H;Виджеты создаются при помощи метода SVGTreeViewer.
// Контейнер, в котором размещается виджет
// обычно для этой цели можно использовать пустой DIV-элемент
var elem = document.querySelector('#tree-1');
// Newick-запись дерева
var notation = '((,A,)B,((C,D),E),(F,G))H;';
SVGTreeViewer(notation, elem, {
'interaction': ['collapse'],
'undo': false
});
В качестве параметров при создании виджета можно указывать все параметры, используемые для отображения деревьев (например, interaction), а также дополнительные опции, которые относятся непосредственно к виджету (например, undo — опция, определяющая, стоит ли включать функциональность undo / redo).
Вот еще один пример с возможностью редактирования:
Частные классы
В отличие от самой библиотеки SVGTree, виджеты создаются при помощи функций, а не конструктора. Этот шаблон проектирования соответствует случаю, когда нет нужды объявлять интерфейс для программного взаимодействия с объектом; в терминах объектно-ориентированного программирования, у объекта отсутствуют публичные методы и поля. Частные поля и функции реализуются в соответствии с шаблоном реализуются в виде внутренних по отношению к глобальной функции переменных:
function PrivateClass(...) {
// поля
var field1, field2, ...;
// методы
function foo(...) { ... };
function bar(...) { ... };
// инициализация
function init() { ... };
init();
}
// использование
PrivateClass(...);
Это же определение в виде традиционного класса:
function PrivateClass(...) {
// Поля
this.field1 = ...;
this.field2 = ...;
}
PrivateClass.prototype = {
constructor: PrivateClass,
// методы
foo: function(...) { ... },
bar: function(...) { ... }
};
// использование
var instance = new PrivateClass(...);
Шаблон проектирования в чем-то напоминает чистые функции из функциональных языков программирования: в отличие от традиционных классов, объекты, созданные при помощи частных функций, не обладают состоянием, видимым постороннему наблюдателю. При необходимости ничто не мешает функции PrivateClass из листинга выше возвращать какое-то значение, позволяющее ограниченно влиять на состояние класса.
В случае виджетов, все взаимодействие инициируется действиями пользователя, то есть реализация виджетов в виде функций более-менее обоснована. Необходимо оговориться, что конкретно виджеты SVGTreeViewer не совсем соответствуют парадигме функционального программирования: у виджетов есть общее состояние, а именно общее количество виджетов на странице nWidgets. Это состояние используется, чтобы при инициализации изменить идентификаторы для компонентов пользовательского интерфейса, сделав их уникальными в рамках веб-страницы. Если этого не делать и разместить на странице несколько виджетов, появятся несколько компонентов с одинаковым идентификатором, что нарушит обработку событий.
// из кода инициализации виджета
var labels = container.querySelectorAll('label[for]');
for (var i = 0; i < labels.length; i++) {
var id = labels[i].htmlFor;
container.querySelector('#' + id).id += '-' + nWidgets;
labels[i].htmlFor += '-' + nWidgets;
}
nWidgets++;
<!-- компоненты до инициализации --> <label for="some-id">...</label><input id="some-id" .../> <!-- после инициализации: в первом виджете --> <label for="some-id-0">...</label><input id="some-id-0" .../> <!-- после инициализации: во втором виджете --> <label for="some-id-1">...</label><input id="some-id-1" .../> <!-- и так далее -->
Undo / Redo
Функциональность отмены / повтора действий в JavaScript логично реализовать в виде шаблона проектирования «типаж», добавляя требуемую функциональность к каждому объекту, вызывая для него определенный метод.
var obj = ...; // патч объекта, добавляющий в него функциональность undo / redo UndoFixture(obj); obj.canUndo(); // проверяет, можно ли откатить состояние объекта obj.undo(); // откатывает состояние к предыдущему obj.canRedo(); // та же функциональность для возврата к состояниям obj.redo();
By convention предполагается, что объект реализует базовые методы для хранения состояния:
- метод
getStateвозвращает текущее состояние объекта (например, сериализованное в виде строки); - метод
setStateустанавливает состояние объекта на основе данных, возвращенных методом getState; - событие
onchangeвызывается каждый раз при изменении состояния объекта.
В таком случае, суть типажа заключается в добавлении к объекту вектора предыдущих сериализованных состояний и указателя на текущее состояние.
function UndoFixture(obj) {
// проверить наличие необходимых функций в объекте
if (!obj.getState || !obj.setState || !obj.onchange) {
throw 'Object does not have necessary methods';
}
// зарегистировать обработчик события, чтобы автоматически
// запоминать состояния
obj.onchange = function() { ... };
obj.undo = function() { ... };
obj.redo = function() { ... };
obj.canUndo = function() { ... };
obj.canRedo = function() { ... };
// предыдущие состояния
obj._pastStates = [ obj.getState() ];
// указатель на текущее состояние в массиве _pastStates
obj._pastStatePointer = 0;
}
Возникает вопрос: возможно ли изменить код функции UndoFixture, чтобы она могла патчить не отдельные объекты, а классы? Можно заметить, что вызов UndoFixture(Foo.prototype) приводит к «почти правильным» результатам. Единственная неувязка заключается в инициализации, так как создание свойств для прототипа — неправильное поведение (свойства должны создаваться в отдельности для каждого объекта). Поскольку конструкторы классов в JavaScript нельзя переопределить (т.е. вызов new Foo() в любом случае вызывает функцию Foo), то решение состоит в небольшом изменении семантики функции:
function Foo() { /* конструктор */ }
Foo.prototype = { /* методы */ };
Foo = UndoFixtureClass(Foo); // патч
// теперь любой объект класса Foo будет обладать функциональностью
var x = new Foo();
x.canUndo();
Новая функция будет создавать класс с добавленными методами:
function UndoFixtureClass(cls) {
// ...
var PatchedClass = function() {
cls.apply(this, arguments); // вызвать унаследованный конструктор
// добавить новые переменные
this._pastStates = [ this.getState() ];
this._pastStatePointer = 0;
this.oncreate = function() { ... };
};
// скопировать методы исходного класса
PatchedClass.prototype = Object.create(cls.prototype);
// создать новые методы
PatchedClass.prototype.undo = function() { ... };
// ...
return PatchedClass;
}
При этом оба определения можно объединить в одну функцию, действующую в зависимости от типа аргумента:
function UndoFixtureUniversal(obj) {
var patchedObj = obj;
if (typeof(obj) == 'function') {
// имеем дело с функцией
patchedObj = function() {
obj.apply(this, arguments);
this._initUndo();
};
obj = patchedObj.prototype = Object.create(obj.prototype);
}
obj._initUndo = function() { /* инициализация */ };
// другие определения из UndoFixture
if (patchedObj == obj) {
// аргумент функции — объект; инициализацию нужно провести сейчас
obj._initUndo();
}
return patchedObj;
}
// Примеры использования
var x = new Foo();
UndoFixtureUniversal(x); // патчит отдельный объект
var PatchedFoo = UndoFixtureUniversal(Foo); // патчит класс