///
namespace Leaf {
// Any element belongs to this element may implement this interface
export interface WidgetElement extends HTMLElement {
// If the element if the root node of any Widget
// In some cases, root node is shared, for example using a commonWidget as top node of a widget.
//
// When it happens, widget always point to the child widget, which is in the example.
// At time of writing, widget reference is only used for retreiving custom DOM event provided by the widget.
widget: WidgetAny
// All element have it's property rules
propertyConfigs: WidgetDynamicPropertyConfig[]
}
// A rule for generating the element's property
export interface WidgetDynamicPropertyConfig {
master: WidgetAny
rules: DynamicProperties
}
export abstract class WidgetCoreDesign {
constructor(protected widget: WidgetAny) { }
public subWidgetPlaceholder: {
[index: string]: {
holder: HTMLElement
dynamics: DynamicProperties
slots: Slot[]
}
} = {}
public slotsPlaceholder: {
[index: string]: {
el: HTMLElement
name: string
dynamics: DynamicProperties
index: number
}
} = {}
public slotIndexOffset = 0
abstract attachElement(el: WidgetElement, dynamics: DynamicProperties);
abstract detachElement(el: WidgetElement);
abstract attachText(text: Text, config: Leaf.DynamicStringConfig);
abstract detachText(text: Text);
// trim attribute with dynamic property string, and return the parsed dynamic properties.
abstract extractDynamicPropertyConfig(el: WidgetElement): DynamicProperties;
abstract extractDynamicStringConfig(content: string): Leaf.DynamicStringConfig
// 1. attach listeners
// 2. add dynamic rule to property configs of this element(and filter out the old one)
// 3. evaluate once for now.
abstract attachDynamicProperty(el: WidgetElement, dynamics: DynamicProperties);
// 1. remove listeners
// 2. remove dynamic rules
// 3. revaluate for now.
abstract detachDynamicProperty(el: WidgetElement);
abstract getExistDynamicProperty(el: WidgetElement): DynamicProperties;
// evaluate all dynamic properties and apply
abstract syncDynamicProperties(el: WidgetElement);
// evaluate only changed property
abstract syncDynamicProperty(el: WidgetElement, name: string);
abstract syncDynamicText(text: Text, rule: Leaf.DynamicStringConfig);
abstract attachDynamicText(text: Text, rule: Leaf.DynamicStringConfig);
abstract detachDynamicText(text: Text);
abstract bindUI(node: WidgetElement, id: string);
abstract unbindUI(id: string);
abstract checkAndBindUI(el: WidgetElement);
abstract checkAndUnbindUI(el: WidgetElement);
protected abstract explain(config: DynamicValueConfig);
// slot related
abstract defineSlot(name: string, el: Element, dynamics: DynamicProperties);
abstract fillSlots(slots: Slot[]);
// widget
abstract defineSubWidget(name: string, el: WidgetElement, dynamics: DynamicProperties, slots: Slot[]);
abstract setSubWidget(name: string, widget);
abstract mergeProps(from: HTMLElement, to: HTMLElement, props: string[])
// list
abstract defineList(name: string, el: HTMLElement)
// helper
abstract extractTemplates(node: HTMLElement): { [index: string]: string }
abstract transferProperties(to: WidgetElement, dynamics: DynamicProperties)
}
export abstract class WidgetInitializerDesign {
constructor(protected widget: WidgetAny) { }
abstract initTemplate(template?: WidgetTemplate);
abstract traverseAndInitialize(node: WidgetElement | HTMLElement | Text);
abstract createElement(tagName: string, properties: ElementConfigProperties, children: WidgetElement[]): WidgetElement;
abstract createText(rule: Leaf.DynamicStringConfig);
}
export class WidgetCore extends WidgetCoreDesign {
attachElement(el: WidgetElement, dynamics: DynamicProperties): WidgetElement {
// 1. widget
// 2. common widget
// 3. slot
// 4. normal
// generally todos
// 0. create real element if needed
// 1. bind and sync props
// 2. bindUI by data id
let tagName = el.tagName.toLowerCase()
if (tagName.toLowerCase() === "widget") {
// widget
let name = el.getAttribute("data-name")
let path = el.getAttribute("data-widget")
if (!name && path) {
let last = path.split("/").pop()
last = last.split(".")[0]
name = last
}
if (!name) {
console.error(el, "widget missing name")
throw new Error(" missing data-name or data-widget")
}
this.defineSubWidget(name, el, dynamics, this.getChildrenAsSlot(el))
} else if (this.widget.namespace.has(tagName) && !this.widget.namespace.isTagWidgetName(tagName, this.widget)) {
// common widget
let props = {}
// none dynamic props
for (let i = 0; i < el.attributes.length; i++) {
let item = el.attributes[i]
props[item.name] = item.value
}
// slots
let customWidget = this.widget.namespace.createWidgetByName(tagName, props)
// inherit properties
for (let name in props) {
if (!customWidget.node.getAttribute(name)) {
// resolve data-id-conflict
if (name === "data-id") name = "data-id-of-parent"
customWidget.node.setAttribute(name, props[name])
}
}
customWidget.core.fillSlots(this.getChildrenAsSlot(el))
// apply properties latter on this custom widget than this place holder
el = customWidget.node as WidgetElement
// use the data-id of placeholder to bindUI
this.checkAndBindUI(el, props["data-id"])
// sync props after end of this if statement...
} else if (tagName === "slot") {
// slot
let name = el.getAttribute("data-slot")
this.defineSlot(name, el, dynamics)
// We also bind UI temperarily until filled
this.checkAndBindUI(el)
} else {
// normal
// Id-for-parent priorier to data-id.
this.checkAndBindUI(el)
// Only for element that is not delegated
if (!el.widget && el.getAttribute("data-list")) {
let name = el.getAttribute("data-list")
this.defineList(name, el)
}
}
this.attachDynamicProperty(el, dynamics)
this.syncDynamicProperties(el)
// Whatever it is, we initialize and sync the properties on it
return el
}
detachElement(el: WidgetElement) {
if (!el) return
this.detachDynamicProperty(el)
};
attachText(text: Text, config: Leaf.DynamicStringConfig) {
for (let item of config.templates) {
if (item.type !== "value") continue
let root = CoreLogic.getPropertyRoot(item.content)
this.widget.ViewModel.events.listenBy(text, [`change/${root}`, `depChange/${root}`], this.syncDynamicText.bind(this, text, config))
}
this.syncDynamicText(text, config)
}
detachText(text: Text) {
this.widget.ViewModel.events.stopListenBy(text)
}
// Should ignore `data-` properties
extractDynamicPropertyConfig(el: WidgetElement): DynamicProperties {
let properties: DynamicProperties = {}
for (let i = 0; i < el.attributes.length; i++) {
let attr = el.attributes[i]
let configs = Leaf.CoreLogic.createDynamicValueConfigs(attr.value)
if (configs && configs.length > 0) {
el.removeAttribute(attr.name)
properties[attr.name] = {
name: attr.name,
templates: configs
}
}
}
return properties
}
getExistDynamicProperty(el: WidgetElement): DynamicProperties {
for (let props of el.propertyConfigs || []) {
if (props.master === this.widget) return props.rules
}
return null
}
syncDynamicProperties(el: WidgetElement) {
let props: { [index: string]: string[] } = {}
for (let config of el.propertyConfigs || []) {
let vm = config.master.ViewModel
for (let name in config.rules) {
let rule = config.rules[name]
let value = Leaf.CoreLogic.explainDynamicString(vm.tree, rule)
if (!props[name]) props[name] = []
props[name].push(value)
}
}
let result: { [index: string]: string } = {}
for (let name in props) {
if (props[name].length > 1 && Leaf.CoreLogic.isPropertyMergable(name)) {
result[name] = Leaf.CoreLogic.mergeProperty(name, props[name])
} else {
result[name] = props[name].pop()
}
}
for (let name in result) {
let value = result[name]
if (value !== el.getAttribute(name)) {
el.setAttribute(name, value)
}
}
}
syncDynamicProperty(el: WidgetElement, propertyName: string) {
let values: string[] = []
for (let config of el.propertyConfigs) {
for (let name in config.rules) {
if (name === propertyName) {
values.push(Leaf.CoreLogic.explainDynamicString(config.master.ViewModel.tree, config.rules[name]))
}
}
}
let value = Leaf.CoreLogic.mergeProperty(propertyName, values)
if (value.toString() != el.getAttribute(propertyName).toString()) {
el.setAttribute(propertyName, value)
}
}
syncDynamicText(text: Text, rule: Leaf.DynamicStringConfig) {
// Should add pollyfill for IE8/7
// https://developer.mozilla.org/en/docs/Web/API/Node/textContent
let value = text.textContent
let strs = rule.templates.map((t) => {
return this.explain(t)
})
let result = strs.join("")
if (result !== value) {
text.textContent = result
}
}
explain(config: DynamicValueConfig) {
return Leaf.CoreLogic.explainDynamicValue(this.widget.ViewModel.tree, config)
}
checkAndUnbindUI(el: WidgetElement) {
let id = Leaf.Util.slugToCamel(el.getAttribute("data-id"))
if (id && this.widget.UI[id] === el) {
this.unbindUI(id)
}
}
checkAndBindUI(el: WidgetElement, name?: string) {
let id = Leaf.Util.slugToCamel(name || el.getAttribute("data-id"))
if (id) {
this.bindUI(el, id)
}
}
bindUI(node: WidgetElement, id: string) {
if (this.widget.UI[id] === node) return
this.unbindUI(id);
this.widget.UI[id] = node
this.widget.delegates.applyUIDelegateOwner(id)
}
unbindUI(id: string) {
this.widget.delegates.clearUIDelegateOwner(id)
delete this.widget.UI[id]
}
defineSubWidget(name: string, el: WidgetElement, dynamics: DynamicProperties, slots: Slot[]) {
// I've considered using widget.subWidget[name], but after
// consider twice, I think directly assign subWidget to widget
// is a better practice and name collision should be minimum.
// First set this element to it
this.subWidgetPlaceholder[name] = {
holder: el
, dynamics: dynamics
, slots
}
Leaf.Property.define(this.widget, name, {
set: (value: WidgetAny, cache: PropertyCache) => {
if ((value instanceof Leaf.Widget || value && value["isWidget"]) || !value) {
// First assignment do nothing, it's likely the placeholder
this.setSubWidget(name, value)
cache.value = value
}
}
})
}
setSubWidget(name: string, widget: WidgetAny) {
// 0. detach old
// 1. sync properties
// 2. bindUI
// 3. replace element
// replace
let oldWidget = this.widget[name] as WidgetAny
let replaceTarget: HTMLElement
let replaceSrc: HTMLElement
let isReplaceRoot = false
if (oldWidget && oldWidget.node && oldWidget.node.parentElement) {
replaceTarget = oldWidget.node
} else {
replaceTarget = this.subWidgetPlaceholder[name].holder
if (replaceTarget === this.widget.node) {
isReplaceRoot = true
}
}
if (widget && widget.node) {
replaceSrc = widget.node
} else {
replaceSrc = this.subWidgetPlaceholder[name].holder
}
// is case replace root, the replaceTarget.parentElement can be null
if (replaceSrc !== replaceTarget && replaceTarget.parentElement) {
replaceTarget.parentElement.replaceChild(replaceSrc, replaceTarget)
}
// detach old
if (oldWidget) {
this.detachElement(oldWidget.node as WidgetElement)
this.checkAndUnbindUI(oldWidget.node)
if (isReplaceRoot) {
this.widget.core.unbindUI("node")
}
}
if (widget) {
// transfer static property to it
// transfer dynamic property to it and bind UI
let holder = this.subWidgetPlaceholder[name]
this.transferProperties(widget.node, holder.dynamics)
this.attachElement(widget.node, null)
widget.core.fillSlots(this.subWidgetPlaceholder[name].slots)
}
if (isReplaceRoot) {
this.widget.node = widget.node
this.widget.core.bindUI(widget.node, "node")
}
}
attachDynamicProperty(el: WidgetElement, dynamics?: DynamicProperties) {
if (!el.propertyConfigs) el.propertyConfigs = []
//console.log("attach", dynamics, el.propertyConfigs)
let todo: DynamicProperties[] = []
this.widget.ViewModel.events.stopListenBy(el)
if (dynamics) {
el.propertyConfigs.push({ master: this.widget, rules: dynamics });
}
for (let config of el.propertyConfigs) {
// already attached
if (config.master === this.widget) {
todo.push(config.rules)
}
}
if (todo.length > 1) {
console.warn("Multiple dynamic attaches")
console.trace()
}
for (let dynamics of todo) {
for (let name in dynamics) {
let config = dynamics[name]
for (let part of config.templates) {
if (part.type === "raw") continue
let root = CoreLogic.getPropertyRoot(part.content)
this.widget.ViewModel.events.listenBy(
el,
[`change/${root}`, `depChange/${root}`],
this.syncDynamicProperty.bind(this, el, name)
)
}
}
}
}
detachDynamicProperty(el: WidgetElement) {
// Detach and remove properties
if (!el) return
if (!el.propertyConfigs) el.propertyConfigs = []
el.propertyConfigs = el.propertyConfigs.filter(item => item.master !== this.widget)
this.widget.ViewModel.events.stopListenBy(el)
}
attachDynamicText(text: Text, rule: Leaf.DynamicStringConfig) {
for (let part of rule.templates) {
if (part.type === "raw") continue
let root = CoreLogic.getPropertyRoot(part.content)
this.widget.ViewModel.events.listenBy(
text,
[`change/${root}`, `depChange/${root}`],
this.syncDynamicText.bind(this, text, rule)
)
}
}
detachDynamicText(text: Text) {
this.widget.ViewModel.events.stopListenBy(text)
}
extractTemplates(node: HTMLElement) {
let result = {}
let templates = node.querySelectorAll("template")
for (let i = 0; i < templates.length; i++) {
let item = templates[i]
if (node.contains(item)) {
item.parentElement.removeChild(item)
let name = item.getAttribute("data-name")
if (name) {
result[name] = item.innerHTML
}
}
}
return result
}
defineSlot(name: string, el: Element, dynamics: DynamicProperties) {
// refactor slot implementation and abstract it to the same
// way as subwidget.
this.slotsPlaceholder[name] = {
el: el as HTMLElement,
name: name,
index: this.slotIndexOffset++,
dynamics: dynamics
}
}
fillSlots(slots: Slot[]) {
for (let i = 0; i < slots.length; i++) {
let slot = slots[i]
let holder = this.getSlotHolder(slot.name, i)
if (holder) {
holder.el.parentElement.replaceChild(slot.el, holder.el)
holder.el = slot.el as HTMLElement
this.transferProperties(slot.el as WidgetElement, holder.dynamics)
this.bindUI(slot.el as WidgetElement, holder.name)
}
}
}
extractDynamicStringConfig(content: string): Leaf.DynamicStringConfig {
let config = Leaf.CoreLogic.createDynamicValueConfigs(content)
return {
templates: config
}
}
mergeProps(from: HTMLElement, to: HTMLElement, props: string[]) {
for (let prop of props) {
let toPropValue = to.getAttribute(prop)
let fromPropValue = from.getAttribute(prop)
if (fromPropValue !== toPropValue) {
if (Leaf.CoreLogic.isPropertyMergable(prop)) {
to.setAttribute(prop, Leaf.CoreLogic.mergeProperty(prop, [fromPropValue, toPropValue]))
} else {
to.setAttribute(prop, fromPropValue)
}
}
}
}
transferProperties(to: WidgetElement, dynamics: DynamicProperties) {
if (!to.propertyConfigs) to.propertyConfigs = []
to.propertyConfigs = to.propertyConfigs.filter(item => item.master !== this.widget)
dynamics = Leaf.Util.clone(dynamics)
this.attachDynamicProperty(to, dynamics)
this.syncDynamicProperties(to)
}
defineList(name: string, el: HTMLElement) {
let list = this.widget[name] = Leaf.Widget.makeList(el)
let events = ["child/add", "child/remove"]
for (let event of events) {
let camelEventName = event.replace(/\/[a-z]/ig, (match) => {
return match[1].toUpperCase()
})
let handlerName = `on` + Leaf.Util.capitalize(camelEventName) + Leaf.Util.capitalize(name)
Leaf.Property.define(this.widget, handlerName, {
set: (value: Function, cache) => {
if (cache.value) {
this.widget[name].events.removeEventListener(event, cache.value)
}
if (value) {
cache.value = (...args) => {
value.apply(this.widget, args)
}
this.widget[name].events.on(event, cache.value)
} else {
cache.value = null
}
}
})
if (this.widget[handlerName]) {
this.widget[handlerName] = this.widget[handlerName]
}
}
list.events.listenBy(this, "child/remove", (child) => {
child.events.stopListenBy(this)
})
}
protected getPropsFromElement(el: HTMLElement) {
let result = {}
let attrs = el.attributes
for (let i = 0; i < attrs.length; i++) {
let item = attrs[i]
result[item.name] = item.value
}
return result
}
protected getChildrenAsSlot(el: HTMLElement): Slot[] {
let slots: Slot[] = []
if (el.childNodes.length === 1 && el.childNodes[0] instanceof Text) {
let result = document.createElement("span")
result.appendChild(el.childNodes[0])
return [{
el: result,
name: "slot0"
}]
}
for (let i = 0; i < el.children.length; i++) {
let item = el.children[i]
let name = item.getAttribute("data-slot") || `slot${i}`
slots.push({ name, el: item })
}
return slots
}
protected getSlotHolder(name: string, index: number) {
for (let s in this.slotsPlaceholder) {
let item = this.slotsPlaceholder[s]
if (item.name === name) return item
}
for (let s in this.slotsPlaceholder) {
let item = this.slotsPlaceholder[s]
if (item.index === index) return item
}
return null
}
}
export class WidgetInitializer extends WidgetInitializerDesign {
initTemplate(template?: WidgetTemplate) {
if (!template) {
template = document.createElement("div")
}
// All kind of template
if (typeof template === "string") {
let tempNode = document.createElement("div");
tempNode.innerHTML = template.trim()
this.widget.node = tempNode.children[0] as WidgetElement
tempNode.removeChild(this.widget.node)
} else if (template instanceof Element) {
this.widget.node = template as WidgetElement
} else if (typeof template === "function") {
return this.initTemplate(template())
}
if (!this.widget.node) {
console.error("Invalid Widget.node constructed with template", template)
throw new Error("BrokenWidgetTemplate")
}
//if (this.widget.node.getAttribute("data-id")) {
// console.error("Shouldn't bind data-id on root of the any widget you can always refer to them as `node`. `data-id` may latter be assigned by parent widget.");
// throw new Error("Invalid data id assignment")
//}
if (this.widget.node.nodeType === this.widget.node.TEXT_NODE) return
this.widget.templates = this.widget.core.extractTemplates(this.widget.node)
this.traverseAndInitialize(this.widget.node)
this.widget.core.bindUI(this.widget.node, "node")
this.widget.node.widget = this.widget
}
traverseAndInitialize(node: WidgetElement | Text) {
if (node instanceof HTMLElement) {
for (let i = 0; i < node.childNodes.length; i++) {
let item = node.childNodes[i]
this.traverseAndInitialize(item as WidgetElement | Text)
}
let config = this.widget.core.extractDynamicPropertyConfig(node)
this.widget.core.attachElement(node, config)
} else if (node instanceof Text) {
let config = this.widget.core.extractDynamicStringConfig(node.textContent)
this.widget.core.attachText(node, config)
}
}
createElement(tagName: string, properties: ElementConfigProperties, children: WidgetElement[]): WidgetElement {
let el = document.createElement(tagName)
let frag = document.createDocumentFragment()
for (let child of children) {
frag.appendChild(child)
}
let dynamics: DynamicProperties = {}
for (let name in properties) {
let v = properties[name]
if (typeof v === "string") {
el.setAttribute(name, v)
el[name] = v
} else {
dynamics[name] = v
el.setAttribute(name, Leaf.CoreLogic.explainDynamicString(this.widget.ViewModel.tree, v))
}
}
el.appendChild(frag)
el = this.widget.core.attachElement(el as WidgetElement, dynamics)
return el as WidgetElement
}
createText(rule: Leaf.DynamicStringConfig): Text {
let text = document.createTextNode("")
this.widget.core.attachText(text, rule)
return text
}
}
export class WidgetDOMEventDelegator {
constructor(public readonly widget) {
}
private lazyHandlers: {
[index: string]: {
[eventName: string]: {
handler?: WidgetEventHandler
}
}
} = {}
private prepareUIDelegates(uiName: string) {
if (this.lazyHandlers[uiName]) return
this.lazyHandlers[uiName] = {}
let events = Leaf.CoreLogic.AllEvents.slice(0)
let ui = this.widget.UI[uiName] as WidgetElement
if (ui.widget && ui.widget.providingEvents && ui.widget.providingEvents.length > 0) {
events = events.concat(ui.widget.providingEvents)
}
for (let event of events) {
this.prepareUIDelegate(uiName, event.name)
}
}
private prepareUIDelegate(uiName: string, eventName: string) {
if (this.lazyHandlers[uiName][eventName]) return
this.lazyHandlers[uiName][eventName] = {}
let handlerName = `on${Leaf.Util.capitalize(eventName)}${Leaf.Util.capitalize(uiName)}`
Leaf.Property.define(this.widget, handlerName, {
set: (v, cache) => {
if (typeof v != "function") return
cache.value = v
this.unbindLazyHandler(uiName, eventName)
this.provideLazyHandler(uiName, eventName, v)
this.bindLazyHandler(uiName, eventName)
}
})
// only set handler, dont' unbind or bind it.
// apply/clear delegate will do it.
if (this.widget[handlerName]) {
this.provideLazyHandler(uiName, eventName, this.widget[handlerName])
}
// for onClick => onClickNode
if (uiName === "node") {
let handlerName = `on${Leaf.Util.capitalize(eventName)}`
Leaf.Property.define(this, handlerName, {
set: (v, cache) => {
if (typeof v != "function") return
cache.value = v
this.unbindLazyHandler(uiName, eventName)
this.provideLazyHandler(uiName, eventName, v)
this.bindLazyHandler(uiName, eventName)
}
})
if (this.widget[handlerName]) {
this.provideLazyHandler(uiName, eventName, this.widget[handlerName])
}
}
}
// call unbind/bind/provide carefully, or just don't call them.
private unbindLazyHandler(uiName: string, eventName: string) {
let owner = this.widget.UI[uiName] as HTMLElement
let info = this.lazyHandlers[uiName][eventName] || {}
if (info.handler) {
owner.removeEventListener(eventName, info.handler)
}
}
private bindLazyHandler(uiName: string, eventName: string) {
let owner = this.widget.UI[uiName] as HTMLElement
let info = this.lazyHandlers[uiName][eventName] || {}
if (info.handler) {
owner.addEventListener(eventName, info.handler)
}
}
private provideLazyHandler(uiName: string, eventName: string, v: Function) {
let handler = (e: WidgetEnhancedEvent) => {
e.capture = () => {
e.stopImmediatePropagation()
e.preventDefault()
}
v.call(this.widget, e)
}
this.lazyHandlers[uiName][eventName].handler = handler
}
public clearUIDelegateOwner(id: string) {
if (!this.widget.UI[id]) return
let owner = this.widget.UI[id] as HTMLElement
let infos = this.lazyHandlers[id] || {}
for (let eventName in infos) {
let info = infos[eventName]
if (info.handler) {
owner.removeEventListener(eventName, info.handler)
}
}
}
// Carefull call apply UI delegate owner
// 1. call clear
// 2. set UI[uiName]
// 3. call apply
// Always call them like above.
// But feel free to call `clear` at any time.
public applyUIDelegateOwner(id: string) {
if (!this.widget.UI[id]) return
let owner = this.widget.UI[id] as HTMLElement
this.prepareUIDelegates(id)
let infos = this.lazyHandlers[id]
// bind any existing events
for (let eventName in infos) {
let info = infos[eventName]
if (info.handler) {
owner.addEventListener(eventName, info.handler)
}
}
}
private delegateTo(name: string, event: Event) {
let fnName = `on${Util.capitalize(event.type)}${Util.capitalize(name)}`
let fn = this.widget[fnName] as WidgetEventHandler
if (!fn) {
return true
}
return fn.call(this.widget, event)
}
}
export class WidgetSlotBehavior {
constructor(public readonly widget: Leaf.Widget) {
}
fill(...widgets: Leaf.Widget[])
fill(widgets: T)
fill(...infos: any[]) {
if (infos[0] instanceof Leaf.Widget) {
this.fillSequentially(...infos)
} else {
this.fillNamingly(infos[0])
}
}
private fillSequentially(...widgets: Leaf.Widget[]) {
let slots = widgets.map(widget => {
return {
name: "#",
el: widget.node
}
})
this.widget.core.fillSlots(slots)
}
private fillNamingly(infos: T) {
let slots = Object.keys(infos).map((name) => {
return {
name, el: infos[name].node
}
})
this.widget.core.fillSlots(slots)
}
}
}