goodteditor-web-components v1.2.823
Общие сведения
Данный проект содержит набор компонентов (далее widget) для СУПа.
- component — инкапсулированный vue компонент, выполняющий какую-либо бизнес логику.
- panel — vue компонент(ы), который настраивает по средством ui свойства component, которые описаны в дескрипторе.
- widget — совокупность component + panel
DEV mode
Для упрощения разработки, в состав пакета входит приложение, эмулирующее среду редактора СУП.
Запуск dev режима — npm run serve
Для отправки событий через шину событий, используйте window.eventBus
Зависимости
- css (https://www.npmjs.com/package/goodt-framework-css)
- event bus (https://www.npmjs.com/package/goodteditor-event-bus)
- даты (https://www.npmjs.com/package/dayjs)
- http (https://www.npmjs.com/package/axios)
- ui элементы для компонентов виджетов (https://www.npmjs.com/package/goodteditor-ui)
- утилиты (https://www.npmjs.com/package/lodash)
- popup layer (https://www.npmjs.com/package/portal-vue)
- drag'n'drop (https://www.npmjs.com/package/vuedraggable)
Соглашения
в проекте используется спецификация es6 (так же nullish-coalescing, spread, object rest spread, optional-chaining)
названия component должны начинаться с префикса
Elem...
используя CamelCase (напримерElemExample
)названия panel должны иметь суффикс
...Panel
используя CamelCase (напримерSettingsPanel
) и распологаться в директории./panels
(относительно директории виджета)widget должны находится в директории
src/lib/
, каждый отдельный компонент должен находится в своей директории, имя которой должно совпадать с именем компонента. Напримерsrc/lib/ElemExample/ElemExample.vue
widget могут использовать свое пространство имен namespace, заданный директориями любой вложенности (например
src/lib/<namespace>/ElemExample/ElemExample.vue
)widget могут не использовать пространство имен только в случае, если данные виджет являются базовыми компонентами, не привязанными к какому-либо проекту (например 'кнопка')
src/utils/
директория, которая содержит общие зависимости, доступные всем widget (утилиты, классы, сервисы и тд). Зависимости в данной директории должны быть общего назначения, не привязанными к какому-либо проекту (например утилиты для работы с http, socket, датами и тд)Группировать widget в рамках одного проекта следует как минимум одним уровнем вложенности namespace (например
src/lib/<projectname>/<namespace>/ElemExample/...
)Утилиты, специфичные для конкретного проекта или widget должны храниться в соответствующей директории. Пример:
src/lib/MyProject/utils/...
— утилитыMyProject
src/lib/Myproject/ElemExample/utils/...
— утилиты widgetMyProject/ElemExample
Для форматирования кода используется
prettier
, который уже настроен в проекте. Вы можете установить плагинprettier
для вашей IDE https://prettier.io/docs/en/editors.htmlВ проекте необходимо стремиться к мин. кол-ву внешних зависимостей, добавление новых в проект обсуждается заранее.
У виджетов обязательно должен быть dom-node (должен рендериться в любом случае).
Резюме:
Widget component
Каждый component наследуется от базового компонента Elem src/lib/Elem.vue
Публичный интерфейс базового Elem.vue
template
Базовый шаблон component
<template>
<div :class="cssClass" :style="cssStyle"></div>
</template>
descriptor
descriptor — объект, который описывает свойства и переменные component. Используется как окружением, так и при написании панелей.
let descriptor = () => ({
// описание свойств
// повторяет интерфейс @see https://vuejs.org/v2/api/#props
props: {
"<название-свойства>": {
// @required
// json-compatible тип свойства: String, Number, Boolean, Array, Object.
// Так же можно указывать массивом несколько типов например [String,Array]
type: "<тип-свойства>"
// @required
// default значение свойства.
// фабричный метод, если значение Array, Object
default: "<значение>"
// @optional
// массив вариантов значений свойства (для удобства написания панелей)
options: [
{ value:"<значение>", label:"<название>" }
]
}
}
// описание переменных
// данное описание используется окружением для настройки props.varAliases
// используются для настройки работы с шиной событий при использовании методов: `eventBusWrapper.listenStateChange()`, `eventBusWrapper.triggerStateChange()`
vars: {
"<название-переменной>": {
// @required
// описание
description: "<описание>",
// @optional
// int, используется редактором в ui для сортировки списка переменных (asc)
sortIndex: "<индекс-сортировки>"
}
}
});
props
props: {
// уникальный идентификатор инстанса компонента, задается окружением
id: {
default: ''
}
// тип - полное имя компонента в рамках соглашений, задается окружением (например `Project/MyElem`)
type: {
type: String,
default: ''
}
// объект, содержащий свойства самого компонента, задается окружением
props: {
type: Object,
default() {
return getDescriptorDefaultProps(descriptor());
}
}
// режим работы окружения: true - компонент создан в плеере; false - компонент создан в редакторе; задается окружением
isEditorMode: {
type: Boolean,
default: false
}
}
data
data() {
return {
// css class рутового элемента шаблона компонента
cssClass: {}
// css style рутового элемента шаблона компонента
cssStyle: {}
// дескриптор с описанием свойств компонента
descriptor: descriptor()
// имя текущего активного <slot> элемента, который будет использоваться при drag'n'drop в редакторе
slotDefault: 'default'
// шина событий @see https://www.npmjs.com/package/goodteditor-event-bus
eventBusWrapper: null
}
}
methods
methods: {
/**
* Хелпер для вызова super методов при наследовании.
* Важно! Не забывайте менять контекст вызова методов родителя на свой
* @example this.super(Elem).genCssClass.call(this)
* @param {VueComponent} ParentComponent parent компонент, почти всегда будет Elem
* @return {object} объект с методами ParentComponent
*/
super(ParentComponent);
/**
* Генерирует css class в @see this.cssClass
* @invoked created()
*/
genCssClass();
/**
* Генерирует css style свойства в @see this.cssStyle
* @invoked created()
*/
genCssStyle();
/**
* Возвращает список имен слотов, доступных в шаблоне элемента
* @NOTE <slot></slot> без имени имеют имя 'default'
* @return {array<String>} @default ['default']
*/
getSlotNames();
/**
* Возвращает массив async import() промисов модулей компонентов-панелей
* @return {array<Promise>} модули компонентов-панелей @example [ import('...') ] @default []
*/
getPanels();
/**
* Возвращает true если разрешено создавать дочерние компоненты указанного типа
* @param {string} type тип дочернего элемента @see props.type
* @return {boolean} true разрешено; иначе false @default true
*/
isChildAllowed(type);
/**
* Вызывается окружением. Этап life-cycle.
* Метод гарантирует доступность @see data.eventBusWrapper
* Вызывается после mounted()
*/
subscribe();
/**
* Вызывается окружением. Сеттер для шины событий.
* Получается ссылку на шину событий, создает инстанс EventBusWrapper и вызывает метод @invoke subscribe()
* @param {object} eventBus
*/
setEventBus(eventBus);
/**
* Получение значения константы из инстанса менеджера констант ConstManager
* @param {string} key
* @return {*}
*/
$c(key);
}
Widget panel
Каждая panel наследуется от базового компонента Panel src/lib/Panel.vue
Публичный интерфейс базового Panel.vue
props
props: {
/** @type {VueComponent} ссылка на инстанс компонента виджета */
elementInstance: {
type: Object
},
},
data
data() {
return {
// мета данные панели для окружения редактора ( name:String ~ название; icon:String ~ mdi icon class)
$meta: { name: '', icon: '' },
// объект, который хранит тек. настройки виджета (мутабелен)
props: {},
// дескриптор компонента виджета (описывает атрибуты объекта props, переменные виджета)
descriptor
};
},
methods
methods: {
/**
* Триггерит 'change' евент, оповещая окружение редактора об изменеии 'props' объекта
* @param {string} propName название атрибута, который изменился в 'props' объекте или null для перезаписи всего объекта @default null
*/
propChanged((propName = null));
}
UI
Для описания ui панелей в проекте есть специальный пакет ui компонентов.
При наследовании от Panel.vue
ui компоненты автоматически импортируются и доступны с префиксом ui-
.
Для запуска документации по ui компонентам используйте команду
npm run docs:panel-ui
Пример использования ui в панели
// MyPanel.vue
<template>
<div>
<ui-input>label</ui-input>
<ui-input-browse>label</ui-input-browse>
<ui-switch>label</ui-switch>
...
</div>
</template>
<script>
import Panel from '<src-path>/lib/Panel';
export default {
extends: Panel,
...
};
</script>
Шина событий
Для взаимодействия между компонентами component и с окружением (плеер, редактор), используется событийная модель, реализованная через шину событий EventBus.
Для чего component нужна шина событий:
- для навигации в плеере
eventBusWrapper.listenNavigate()
,eventBusWrapper.triggerNavigate()
- для получения состояния в плеере (глобальный state)
eventBusWrapper.listenStateChange()
,eventBusWrapper.triggerStateChange()
- для отправки кастомных событий
eventBusWrapper.listen()
,eventBusWrapper.trigger()
В каждом инстансе component создается инстанс обертки для работы с шиной событий EventBusWrapper, доступный внутри компонента как this.eventBusWrapper
.
Для получения доступа к шине, необходимо в компоненте-наследнике переопределить метод life-cycle subscribe()
// ElemTest.vue
...
export default {
extends: Elem,
...
methods: {
/**
* Переопределяем метод Elem.subscribe()
*/
subscribe() {
// теперь шина событий доступна
this.eventBusWrapper;
}
}
}
интерфейс EventBusWrapper
{
/**
* Подписывается обработчик на событие "навигации" по страницам в окружении (плеере)
* @param {function} handler обработчик вида (e, { url:String, params:Object }) => {}
* @param {boolean} once true для разовой подписки @default false
* @return {function} dispose метод, для отписки
*/
listenNavigate(handler, once = false);
/**
* Отправка события "навигация" по страницам в окружении.
* Вызовет переход по роуту в окружении/переход по внешнему url.
* Если url относительный вида '/...' окружение попытается найти роут и перейти по нему
* Если url абсолютный вида '<protocol>://' будет вызван window.location
* @param {object} data объект вида { url:String, params:Object }
* params ~ query параметры запроса
* @return {boolean} true если был осуществлен переход по роуту
*/
triggerNavigate({ url, params = {} });
/**
* Подписывается обработчик на событие "изменение состояния" в окружении (плеере)
* @param {function} handler обработчик вида (e, state:Object) => {}
* @param {boolean} once true для разовой подписки @default false
* @return {function} decoratedHandler декорированный обработчик, для отписки @see unlistenStateChange()
*/
listenStateChange(handler, once = false);
/**
* Отправка события "изменение состояния" в окружение
* @param {object} stateChange объект "изменение-состояния"
*/
triggerStateChange(stateChange);
/**
* Подписывает обработчик на событие eventType
* @param {string} eventType тип события
* @param {function} handler обработчик вида (e, data) => {}
* @param {boolean} once true для разовой подписки @default false
* @return {function} dispose метод, для отписки
*/
listen(eventType, handler, once = false);
/**
* Отправка события eventType
* @param {string} eventType тип события
* @param {object} data объект для отправки вместе с событием
*/
trigger(eventType, data);
}
Так же в интерфейсе есть методы для явной отписки от событий. Их можно не использовать т.к. базовый компонент-родитель Elem сам производит отписку от всех евентов в хуке life-cycle beforeDestroy()
{
/**
* Отписывает от события "навигация" обработчик
* @param {function} handler обработчик, ранее подписанный @see listenNavigate()
*/
unlistenNavigate(handler);
/**
* Отписывает от события "изменение состояния" обработчик
* @param {function} decoratedHandler декорированный обработчик, ранее подписанный @see listenStateChange()
*/
unlistenStateChange(decoratedHandler);
/**
* Отписывает от события eventType обработчик
* @param {string} eventType тип события
* @param {function} handler обработчик
*/
unlisten(eventType, handler);
}
Кейсы
Частовозникающие задачи/проблемы при написании widget
Кейс: css/style
Кейс: widget компонент не реагирует на изменение props
стандартной панели StylePanel
Для корректной работы props
, унаследованных от Elem.vue
, необходимо объявить cssClass, cssStyle
в шаблоне своего widget компонента/
Пример:
ElemExample.vue
<template>
<div :class="cssClass" :style="cssStyle"></div>
</template>
Кейс: css переменные фреймворка
Кейс: хочу использовать свои css scoped
классы в шаблоне widget компонента, но при этом использовать css переменные фреймворка
Css фреймворк имеет массу компонентов, утилит. Однако он не способен удовлетворить все потребности. Если вам необходимо описать свой scoped
класс, вы можете использовать css переменные, используя var()
.
Список переменных: https://goodt-css.netlify.app/#about/about-cssvars
Пример:
ElemExample.vue
<template>
<div :class="cssClass" :style="cssStyle">...</div>
</template>
<style scoped>
.my-class {
background: var(--color-primary);
}
</style>
Кейс: state
Кейс: хотим работать с глобальным state шины событий (читать, писать) в widget компоненте.
Для работы с глобальным state окружения, доступного всем component используются методы: EventBusWrapper.listenStateChange()
и EventBusWrapper.triggerStateChange()
.
Но так же следует зарегистрировать переменные в vars дескрипторе, которые component собирается использовать при работе с state.
Пример:
ElemExample.vue
let descriptor = () => ({
props: {},
vars: {
// объявляем явно переменную, которую хотим получать/отправлять из/в state
myVariable: {
description: 'date description'
}
}
});
export default {
...
subscribe() {
// подписываемся на изменение 'state' окружения
this.eventBusWrapper.listenStateChange((e, state) => {
// ловим свою переменную
console.log(e, state.myVariable)
});
// отправляем свою переменную в 'state' окружения
this.eventBusWrapper.triggerStateChange({ myVariable: 'Hello world!' })
}
};
Кейс: навигация
Кейс: хотим вызывать навигацию по страницам из widget компонента.
Для навигации в окружении, доступного всем component используются методы: EventBusWrapper.listenNavigate()
и EventBusWrapper.triggerNavigate()
.
Пример:
ElemExample.vue
export default {
...
subscribe() {
// подписываемся на 'navigate' окружения
this.eventBusWrapper.listenNavigate((e, { url, params }) => {
// ловим
console.log(e, url, params)
});
// триггерим навигацию на новый route в окружении -> '/profile'
this.eventBusWrapper.triggerNavigate({ url:'/profile' })
}
};
Кейс: panel group
Кейс: при написании панели виджета, есть необходимость разбить настройки панели на группы
Пример:
ElemExamplePanel.vue
<template>
<div>
<panel-group :panels="panels">
<template #first-panel>
<div>First panel content</div>
</template>
<template #second-panel>
<div>Second panel content</div>
</template>
</panel-group>
</div>
</template>
<script>
import Panel from '<src-path>/lib/Panel';
import ElemExample from '<src-path>/lib/ElemExample';
import PanelGroup from '<src-path>/utils/components/PanelGroup';
let descriptor = ElemExample.data().descriptor;
export default {
extends: Panel,
components: {
PanelGroup
},
data() {
return {
descriptor,
panels: [
{ slot: 'first-panel', label: 'First panel', visible: true },
{ slot: 'second-panel', label: 'Second panel' }
]
};
}
};
</script>
Кейс: popup
Кейс: хотим использовать popup внутри своего widget компонента.
Для работы с popup в окружении, необходимо использовать компонент Portal
из portal-vue
.
Пример:
ElemExample.vue
<template>
<div :class="cssClass" :style="cssStyle">
<popup :visible.sync="isPopupActive">
<template #body>
<h3>Popup body</h3>
<div>Content goes here...</div>
</template>
<template #footer="{ close }">
<div class="text-right">
<div class="btn btn-primary" @click="close">close</div>
</div>
</template>
</popup>
</div>
</template>
<script>
import Elem, { getDescriptorDefaultProps } from '<src-path>/lib/Elem';
import Popup from '<src-path>/utils/components/Popup';
let descriptor = () => ({
props: {},
vars: {}
});
export default {
extends: Elem,
components: {
Popup
},
props: {
props: {
default() {
return getDescriptorDefaultProps(descriptor());
}
}
},
data() {
return {
// дескриптор
descriptor: descriptor(),
isPopupActive: true
}
},
};
</script>
Кейс: работа с константами окружения
Кейс: хотим использовать константы окружения в качестве значений props
своего widget компонента.
Пример:
ElemExample.vue
Для получения значения константы окружения в компоненте widget нужно использовать метод $c()
<template>
<div :class="cssClass" :style="cssStyle">
{{ $c(props.message) }}
</div>
</template>
<script>
import Elem, { getDescriptorDefaultProps } from '<src-path>/lib/Elem';
let descriptor = () => ({
props: {
message: {
type: String,
default: ''
}
},
vars: {}
});
export default {
extends: Elem,
props: {
props: {
default() {
return getDescriptorDefaultProps(descriptor());
}
}
},
data() {
return {
// дескриптор
descriptor: descriptor()
};
},
created() {
// для теста
this.$watch('props.message', (val) => console.log('props.message', this.$c(val)))
}
};
</script>
Пример:
ElemExamplePanel.vue
Для получения списка имен констант окружения в панели widget, следует использовать computed свойство envConstantsNames
<template>
<div>
<div class="form-label">message</div>
<div class="form-control w-100">
<input-autocomplete
class="w-100"
size="small"
v-model="props.message"
@change="propChanged()"
:options="envConstantsNames"
></input-autocomplete>
</div>
</div>
</template>
<script>
import Panel from '<src-path>/lib/Panel';
import ElemExample from '<src-path>/lib/ElemExample';
import { InputAutocomplete } from 'goodteditor-ui';
let descriptor = ElemExample.data().descriptor;
export default {
extends: Panel,
components: { InputAutocomplete },
data() {
return {
descriptor
};
}
};
</script>
Кейс: dremio
Кейс: хотим использовать dremio в качестве источника данных внутри своего widget компонента.
Для работы с dremio в окружении, необходимо подключить node-module goodt-dremio-sdk
, а так же панель lib/DremioPanel
.
Пример:
ElemExample.vue
<template>
<div :class="cssClass" :style="cssStyle">
<template v-if="queryHelper">{{ result }}</template>
<template v-else>Dremio не настроен</template>
</div>
</template>
<script>
import Elem, { getDescriptorDefaultProps } from '<src-path>/lib/Elem';
import { Query, SDKFactory } from '<src-path>/utils/dremio/SDK';
import cloneDeep from 'lodash/cloneDeep';
let descriptor = () => ({
props: {
dremio: {
type: Object,
default: null
}
},
vars: {}
});
export default {
extends: Elem,
props: {
props: {
default() {
return getDescriptorDefaultProps(descriptor());
}
}
},
data() {
return {
// дескриптор
descriptor: descriptor(),
// тут будут наши данные из сервиса
result: null,
// ошибка запроса
error: null,
// query помощник
queryHelper: null,
// sdk
dremioSdk: SDKFactory()
};
},
created() {
let handler = () => {
// источника пока нет
if (!this.props.dremio) {
this.result = null;
return;
}
// отдаем источник на управление
this.queryHelper = new Query(cloneDeep(this.props.dremio));
this.loadData();
};
if (this.isEditorMode) {
this.$watch('props.dremio', handler, { deep: true });
}
handler();
},
methods: {
// возвращаем промис с компонентом нашей панели
getPanels() {
return [import('<src-path>/lib/DremioPanel')];
},
// метод для загруки данных
loadData() {
let query = this.queryHelper.buildQuery();
dremioSdk.getData(query)
.then(result => (this.result = result))
.catch(e => (this.error = e));
}
}
};
</script>
Быстрый старт
Scaffolding
Для упрощения написания виджетов можно воспользоваться cli утилитой создания виджетов, вызвав команду:
npm run scaffold
Manual
Создаем файловую структуру:
src/lib/test/ ~
-- ElemTest/ ~
---- ElemTest.vue
Файл ElemTest.vue
:
<template>
<div :class="cssClass" :style="cssStyle">
{{props.result}}
</div>
</template>
<script>
import Elem, { getDescriptorDefaultProps } from './../../Elem';
let descriptor = () => ({
// props описывает свойства, которые задаются из СУП и настраиваются панелью
props: {
message: {
type: String,
default: 'Default message'
}
},
vars: {}
});
export default {
extends: Elem,
props: {
props: {
default() {
return getDescriptorDefaultProps(descriptor());
}
}
},
data() {
return {
// дескриптор
descriptor: descriptor()
}
},
methods: {
// наш компонент не предполагает наличия других дочерних компонентов
isChildAllowed(type) {
return false;
},
// наш компонент не имеет <slot>
getSlotNames() {
return [];
},
// возвращаем промис с компонентом нашей панели
getPanels() {
return [ import('./ElemTestPanel') ];
}
}
}
</script>
Файл ElemTestPanel.vue
:
<template>
<div>
<div class="p">
<div class="form-label">message</div>
<div class="form-control w-100">
<textarea class="textarea w-100" v-model="props.message" @change="propChanged"></textarea>
</div>
</div>
</div>
</template>
<script>
import Panel from './../../Panel';
import Component from './ElemTest';
// если нам обходим дескриптор с описанием параметров
let descriptor = Component.data().descriptor;
export default {
extends: Panel,
data() {
return {
descriptor
}
}
}
</script>
Всё готово для теста.
Открываем файл src/App.vue
меняем код:
{
// полный путь до нашего компонента, относительно `src/lib/` ~ `<namespace>/<elem-comp>`
elemTypes: ['test/ElemTest'];
}
Запускаем dev режим npm run serve
Результат:
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago