Реализация перемещающихся строк

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

Для начала создадим шаблон с именем moving-containers и директорию templates/blocks, в которой разместим шаблон.

$ cd [PROJECT_DIRECTORY]/templates $ mkdir blocks $ cd blocks $ fcfmngr create template moving-containers

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

Файл :templates/pages/main-page.tmpl:

... //~TEMPLATE body @{{ render.template(":templates/blocks/moving-containers.tmpl"); }}@

Теперь подключим CSS файл с подготовленными стилями к шаблону :templates/blocks/moving-containers.tmpl. Скачайте CSS файл moving-containers.css и разместите его в директории :templates/blocks и добавьте запись в шаблон.

Файл moving-containers.tmpl:

//~OPTIONS { .. include: ["moving-containers.css"], .. } ..

И так, в шаблоне moving-containers мы разместим хранение массива строк в аргументе _strings, которое будем считывать на стороне сервера из системной переменной. Соответственно, шаблон moving-containers должен будет рендерится на стороне сервера и опция шаблона clientRendering должна быть равна false. Начиная с этого примера аргументы шаблона, которые относятся к внутренним переменным и не должны изменяться внешним кодом, мы будем именовать с нижнего подчеркивания.

Объявим программный аргумент _strings в файле :templates/blocks/moving-containers.tmpl и запишем его заполнение в хуке шаблона hooksProgrammableArgument.

Файл :templates/blocks/moving-containers.tmpl

... //~ARGUMENTS { _strings: fcf.argProg(), } ...

Файл :templates/blocks/moving-containers.hooks.js

... // // Object of hooks for programmatically populated arguments. The function can be asynchronous // hooksProgrammableArgument: { // // Hook of the assembly of a programmatically populated argument created by the fcf.argProg() method with the name ARG_NAME // The function can be asynchronous // @result Returns the value of an argument or a Promise object // _strings: (a_taskInfo) => { return fcf.application.getSystemVariable("application:strings"); }, }, ...

Выведем в шаблон контейнер с нашими строками. Каждая строка будет отдельным подшаблоном view-item со своим таймером, который будет смещать их положение, а при смещении за границы вложенного контейнера менять вектор движения.

Файл :templates/blocks/moving-containers.tmpl

... //~TEMPLATE <div class="moving-containers-view"> %{{ for(let i = 0; i < args._strings.length; ++i) { }}% @{{ render.template( "+view-item", { string: fcf.argRef(`_strings[${i}]`), })}}@ %{{ } }}% </div> //~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~ SUB TEMPLATE view-item //~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~OPTIONS view-item { autoUpdate: true, clientRendering: true, } //~ARGUMENTS view-item { fcfClassInner: "moving-containers-view-item", string: "", } //~TEMPLATE view-item @{{args.string}}@

И так, разберем, что мы записали в шаблоне. Мы имеем основной шаблон, в котором происходит вывод подшаблонов view-item, содержащих сами строки.

В коде основного шаблона мы выводим все строки, используя ссылочный аргумент. Как видно из кода, используется 2-х уровневый адрес ссылки на аргумент. Путь к ссылочному аргументу должен быть указан в нотации Javascript, используя точки, как разделители полей или квадратные скобки []. Мы использовали передачу значения через ссылочный аргумент, для того чтобы при редактировании строки автоматически было обновление в подшаблоне view-item.

Так же отметим настройки подшаблона view-item. Опция autoUpdate установлена в значение true, для автоматического обновления шаблона при редактировании. Опция clientRendering равна true, так как основной шаблон выполняет свой рендеринг на стороне сервера и все вложенные шаблоны, так же наследуют этот параметр. Но для данного шаблона в этом нет необходимости.

Так как шаблон у нас выводит только одну строку мы устанавливаем CSS класс оберточного DOM элемента, используя системный аргумент fcfClassInner.

Теперь давайте запишем код реализации враппера для подшаблона view-item. Для начала создадим файл враппера.

$ cd [PROJECT_DIRECTORY]/templates/blocks $ fcfmngr create wrapper moving-containers+view-item

И запишем в созданный файл логику перемещения строк внутри родительского DOM элемента. Шаблон view-item перемещается в направлении определенной радианой, при пересечении границ внешнего контейнера меняет направление движения. И еще, для красоты будем раскрашивать наши строки в разные цвета, за это будет отвечать функция setColor().

Файл :templates/blocks/moving-containers+view-item.wrapper.js

fcf.module({ name: "templates/blocks/moving-containers+view-item.wrapper.js", dependencies: ["fcf:NClient/Wrapper.js"], module: function(Wrapper){ return class WrapperImpl extends Wrapper{ constructor(a_initializeOptions){ super(a_initializeOptions); this._radians = Math.PI * Math.random() * 2; this._x = 0; this._y = 0; } initialize() { this.timer(); return super.initialize(); } attach(){ this.setPosition(); this.setColor(); return super.attach(); } timer() { let self = this; if (this._destroy) return; this.setPosition(); setTimeout(()=>{ self.timer(); }, 30); } setPosition(){ let selfRect = fcf.getRect(this.getDomElement()); let ownerRect = fcf.getRect(this.getParent().select(".moving-containers-view")[0]); let dx = Math.cos(this._radians) / 200; let dy = Math.sin(this._radians) / 200; if ((dx > 0 && (this._x + (selfRect.width / ownerRect.width) + dx > 1)) || (dx < 0 && (this._x + dx < 0))) { this._radians += ((Math.PI / 2) - this._radians) * 2; dx = -dx; } else if (((dy > 0) && (this._y + (selfRect.height / ownerRect.height) + dy > 1)) || ((dy < 0) && (this._y + dy < 0))){ this._radians += (Math.PI - this._radians) * 2; dy = -dy; } this._x += dx; this._y += dy; let rect = fcf.getRect(this.getParent().select(".moving-containers-view")[0]); this.getDomElement().style.left = (this._x * rect.width + rect.left) + "px"; this.getDomElement().style.top = (this._y * rect.height + rect.top) + "px"; } setColor(){ let string = this.getArg("string"); let r = 0; let g = 0; let b = 0; for(let i = 0; i < string.length; ++i) { r += string.charCodeAt(i)*9; g += string.charCodeAt(i)*99; b += string.charCodeAt(i)*999; } r = Math.floor(r % 255); g = Math.floor(g % 255); b = Math.floor(b % 255); let color = "#" + fcf.padStart(r.toString(16), 2, "0") + fcf.padStart(g.toString(16), 2, "0") + fcf.padStart(b.toString(16), 2, "0"); this.getDomElement().style.color = color; } }; } });

Код враппера выполняет перемещение, которое выполняется методом setPosition(), вызываемое таймером (метод timer()) каждые 30 миллисекунд. Таймер запускается при инициализации враппера и завершает свое выполнение после вызова метода fcf.NClient.Wrapper.destroy(), анализируя состояние внутренней переменной класса _destroy.

Так как мы выполняем перемещение подшаблона и изменение цвета, изменяя свойство style для DOM элемента, нам необходимо выполнить его позиционирование при повторном рендеринге. Это выполняется перегрузкой метода fcf.NClient.Wrapper.attach() и вызовом методов setPosition() и setColor().

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

Теперь можно запустить приложение и посмотреть результат