SVGTreeViewer — плагин для SVGTree

Библиотека SVGTree, о которой шла речь в предыдущем посте, позволяет рисовать красивые деревья, однако для удобства применения ей недостает интерактивности. Не помешала бы функциональность, позволяющая пользователям обращаться с созданными деревьями в соответствии со следующими сценарии использования:

  • создание и удаление вершин дерева при помощи более удобных средств, чем кнопки Insert и Delete (в мобильных устройствах этих кнопок попросту нет);
  • настройка внешнего вида дерева (например, формы вершин и ребер);
  • просмотр «исходного кода» дерева в Newick-формате и, возможно, его непосредственное редактирование (по аналогии с тем, как HTML-редакторы вроде TinyMCE предоставляют возможность редактировать исходный код HTML);
  • откат / повторение произведенных изменений (undo / redo).

Поскольку в неинтерактивных случаях использования базовой функциональности библиотеки SVGTree вполне достаточно, новую функциональность логично выделить в отдельный компонент — SVGTreeViewer, зависящий от базовой библиотеки. Компонент представляет собой JavaScript-файл и таблицу стилей, которые в сжатом виде занимают около 15 килобайт.

Базовые примеры использования компонента приведены на этой веб-странице, еще пара есть под катом.


Например, пример, приведенный в описании алгоритмов на деревьях будет выглядеть так:

Виджет SVGTreeViewer для дерева ((,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).

Вот еще один пример с возможностью редактирования:

Виджет SVGTreeViewer с возможностью редактирования

Частные классы

В отличие от самой библиотеки 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); // патчит класс

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *