Настройка конструктора фреймов

Конструктор фреймов - компонент, который строит указанный ему фрейм в режиме редактирования(designtime). В данном режиме можно выполнять различные операции над виджетами: перетаскивать, добавлять/удалять, менять их свойства. Результатом работы компонента является json конфиг - фрейм(результат конструирования).

Конфигурация конструктора

Чтобы добавить конструктор на страницу, необходимо:

  1. Добавить контрол SiteEditorBase/constructor:View в шаблон своего контрола.
  2. Определить опции:
    • widgets — фрейм(набор отображаемых виджетов).
    • decorators — набор декораторов, применяемых к каждому виджету.
    • widgetsData — набор данных для виджетов.
    • onWidgetsChanged — функция-коллбек, которая будет вызвана при изменении фрейма(при добавлении/удалении/перемещении/редактировании виджетов).

Настройка опций widgets, decorators и widgetsData аналогична настройке для плеера, пример можно найти в статье.

Платформенный декоратор

В большинстве случаев, в разных конструкторах присутствуют одни и те же операции (удаление/редактирование свойств), которые отображаются одинаковыми управляющими элементами. Для этого разработан платформенный декоратор виджетов SiteEditorBase/widgets:BaseDecorator, который позволяет удалять виджеты и редактировать их свойства через PropertyGrid.

Декоратор имеет следующие опции:

  1. allowDeleteCallback — функция, которая должна вернуть признак возможности удаления виджета; Если опция не задана, то удалять можно все виджеты.
  2. allowEditCallback — функция, которая должна вернуть признак возможности редактирования свойств виджета. Если опция не задана, то редактировать можно все виджеты.

В функции будут переданы следующие аргументы:

  1. widgetId — идентификатор виджета.
  2. widgetType — тип виджета.
  3. widgetProps — текущие свойства виджета.

Для подключения декоратора необходимо передать его в опцию decorators конструктора.

Пример:

<!-- WML -->
<SiteEditorBase.constructor:View
    decorators="{{_decorators}}"
/>
// TypeScript

protected _decorators = [{
    templateName: 'SiteEditorBase/widgets:BaseDecorator',
    props: {
        allowEditCallback(widgetId, widgetType, widgetProps): {
           return widgetType === 'baseWidget';
        }
    }
}];

При нажатии на кнопку редактирования декоратор открывает окно с PropertyGrid, в котором отображаются все поля, указанные в propTypes у supportedWidgets, у которых есть PGEditorConfig. Подробнее о настройке propTypes можно найти в статье.

При нажатии на кнопку удаления/редактирования будет вызван метод onWidgetsChanged, который был передан в конструктор, и в него будет передан актуальный фрейм(результат конструирования).

Настройка onWidgetsChanged

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

<!-- WML -->
<SiteEditorBase.constructor:View
    widgets="{{_widgets}}"
    onWidgetsChanged="{{_onWidgetsChanged}}"
/>
// TypeScript

protected _widgets = [...];
protected _onWidgetsChanged = (widgets) => {
    this._widgets = widgets;
}

Изменение фрейма из виджетов/декораторов

Конструктор позволяет изменять отображаемый фрейм. Для этого он в качестве опций во все декораторы передает следующие поля и методы:

  1. id — идентификатор виджета.
  2. type — тип виджета.
  3. props — свойства виджета, которые были указаны в поле props виджета. Если в фрейме у виджета не было указано значение свойства, то будет взято дефолтные значения из описания виджета в supportedWidgets.
  4. deleteWidget — метод для удаления виджета. В метод должны передаваться следующие параметры:
    • widgetId — идентификатор удаляемого виджета.
  5. addWidget — метод для добавления виджета. В метод должны передаваться следующие параметры:
    • widgetType — тип добавляемого виджета.
    • targetId — идентификатор виджета, относительного которого нужно добавить новый виджет.
    • position: 'after' | 'before' — позиция, в которую нужно добавить виджет.
  6. onPropsChange - функция обратного вызова принимающая функцию которая вызывается когда декоратор изменяет опции контрола. В метод должны передаваться параметры:
    • newProps — новые свойства виджета.

В виджет передаются свойства в соответствии с их значениями описанными во фрейме. Так же для каждого свойства передается функция обратного вызова, вызвав которую виджет в режиме редактирования может оповестить конструктор, что свойство поменялось. Опции, через которые передаются эти функции обратного вызова именуются стандартным образом. Если опция называется myOption, то опция функция обратного вызова должна называться onMyOptionChange.

Также в виджет передаются опции deleteWidget и addWidget.

С помощью этих методов виджеты и декораторы могут изменять фрейм, в результате вызова этих методов будет вызван коллбэк, который был передан в опцию onWidgetsChanged.

Drag'n'drop виджетов

Конструктор предоставляет инструменты для реализации drag’n’drop виджетов. В корне конструктора находится платформенный контрол работы с drag'n'drop. За запуск и окончание drag'n'drop должны отвечать виджеты и декораторы.

Запуск перемещения

Для запуска drag'n'drop виджет/декоратор должен вызвать метод startDrag, который был передан конструктором в опции виджета/декоратора. В метод необходимо передать:

  1. Объект, на основе которого будет создана сущность перемещения. Объект должен содержать следующие поля:
    • id — идентификатор виджета.
    • widgetType — тип перемещаемого виджета.
    • props — установленные свойства перемещаемого виджета.
  2. dragEndCallback — функция-колбек, которая будет вызвана при завершении drag'n'drop(подробнее в разделе "Окончание перемещения").
  3. draggingTemplate — путь до компонента, который будет использоваться в качестве шаблона перемещаемого элемента.
  4. draggingTemplateOptions — опции, которые будут переданы в компонент, указанный в параметре draggingTemplate.

Метод создаст сущность перемещения и запустит механизм drag'n'drop. Если виджет или декоратор должны обрабатывать перемещение, им необходимо подписаться на событие начала перемещения documentDragStart у Controls/dragnDrop:Container, с помощью которого можно получить сущность перемещения. Чтобы проверить, что сущность перемещения является виджетом, необходимо проверить, что она принадлежит классу WidgetDragEntity. Чтобы понять, что перемещение на странице завершилось, необходимо подписаться на событие documentDragEnd у Controls/dragnDrop:Container.

Пример:

// WML
<Controls.dragnDrop:Container
    on:documentDragStart="_documentDragStartHandler()"
    on:documentDragEnd="_documentDragEndHandler()">
    <div class="myWidget" on:mousedown="_startDrag()">
        ...
    </div>
</Controls.dragnDrop:Container>
// TypeScript
import {WidgetDragEntity} from 'SiteEditorBase/constructor';

protected _dragEntity;

protected _documentDragStartHandler(event, dragEntity): void {
    if (dragEntity.entity instanceof WidgetDragEntity) {
        this._dragEntity = dragEntity.entity;
    }
}

protected _documentDragEndHandler(): void {
    this._dragEntity = null;
}

protected _startDrag(event, widgetId, widgetType, widgetProps): void {
    this._options.startDrag(event, {
        id: this._options.widgetId,
        widgetType: this._options.widgetType,
        props: this._options.widgetProps
    },  this.dragEndCallback);
}

dragEndCallback = () => {
  ...
}

Перемещение

В процессе перемещения виджеты и декораторы должны самостоятельно обрабатывать наведение курсора мыши на их контейнеры. Если пользователь навел на часть виджета/декоратора, в которую можно переместить виджет, виджет/декоратор может визуально выделить эту область, а также он должен вызвать опцию-коллбек canProcessDragEnd — метод для указания, что виджет или декоратор могут обработать окончание перемещения. В метод нужно передать сущность, которая обработает окончание перемещения. У этой сущности должен быть реализован метод dragEnd, который будет вызываться при окончании перемещения (подробнее в разделе "Окончание перемещения").

// WML
<div>
    <div class="dragLine" on:mousemove="_mouseMoveHandler('before', 0)"></div>
    <Widget1/>
    <div class="dragLine" on:mousemove="_mouseMoveHandler('after', 0)"></div>
    <Widget2/>
    <div class="dragLine" on:mousemove="_mouseMoveHandler('after', 1)"></div>
</div>
// TypeScript

protected _dragState;
protected _mouseMoveHandler = (event, position, widgetIndex) => {
    if (this._options.dragEntity) {
        this._options.canProcessDragEnd(this);
        this._dragState = {
            position,
            widgetIndex
        };
    }
}
dragEnd = () => {
  ...
}

Окончание перемещения

Когда пользователь завершит drag'n'drop, все конструкторы будут оповещены об этом платформенным механизмом. Конструктор, у которого есть установленная сущность, которая должна обработать окончание перемещения, вызывает метод dragEnd у этой сущности, в метод передается сущность перемещения. Внутри этого метода необходимо:

  1. произвести добавление виджета в нужное место.
  2. вызвать метод editProperty, который был передан в виджет конструтором, и передать в него новый массив виджетов.
  3. вернуть delete, если перемещение завершилось успешно и перемещаемый виджет необходимо удалить из места перетаскивания, иначе ничего не возвращать.

Пример:

// TypeScript

dragEnd = (dragEntity) => {
    const draggedWidgetPosition = this._widgets.findIndex((widget) => widget.id === dragEntity.id);
    let newWidgets;
    // если перемещаемый виджет из текущего компонента
    if (draggedWidgetPosition !== -1) {
        newWidgets = ...;
    // если перемещаемый виджет из другого компонента
    } else {
        newWidgets = ...;
    }
    this._options.editProperty(this._options.id, 'widgets', newWidgets);

    return draggedWidgetPosition === -1 ? 'delete' : null;
}

После этого будет вызван dragEndCallback, который был установлен при запуске перемещения. В метод будет передана сущность перемещения, а также результат, который вернул метод dragEnd. Если dragEnd вернул delete, внутри dragEndCallback необходимо:

  1. удалить перемещаемый виджет.
  2. вызвать метод editProperty, который был передан в виджет конструтором, и передать в него новый массив виджетов.

Пример:

// TypeScript

dragEndCallback = (dragEntity, dragEndResult) => {
    if (dragEndResult === 'delete') {
        const newWidgets = this._options.widgets.filter((widget) => widget.id !== dragEntity.id);
        this._options.editProperty(this._options.id, 'widgets', newWidgets);
    }
}