UNPKG

oda-framework

Version:

It's an ES Progressive Framework based on the technology of Web Components and designed especially for creating custom UI/UX of any complexity for web and cross-platform PWA mobile applications.

1,228 lines (1,216 loc) 41.1 kB
ODA({ is: 'oda-layout-designer', imports: '@oda/icon', template: /*html*/` <style> :host{ @apply --vertical; overflow-y: auto; padding: 16px 32px; background-color: {{paper.background}} !important; } </style> <oda-tree ~if="layout && designMode" slot="left-panel" icon="odant:fields" title="fields" icon-checked="icons:visibility" icon-unchecked="icons:visibility-off" allow-check="single" :label="label || 'structure'" cell-template="oda-layout-designer-table-cell" allow-selection="level" :data-set="[layout]" show-grouping-panel="false" :selected-rows="selection" ></oda-tree> <div class="flex vertical" style="overflow-x: hidden; overflow-y: auto;"> <oda-layout-designer-structure ~is="structureTemplate" class="flex" ~class="{header: designMode}" :layout></oda-layout-designer-structure> <div class="flex"></div> </div> <oda-property-grid icon="icons:menu" ~if="designMode" slot="right-panel" group-expanding-mode="all" :inspected-object="selection"></oda-property-grid> `, async loadSettings(layout) { return this.settings[layout.path] ??= {}; }, async saveSettings(layout) { this.settings[layout.path] = layout.settings; }, resetDesign() { this.designInfo.selected = []; this.layout.reset(true); }, label: '', $pdp: { settings: {}, get designer() { return this; }, designInfo: { selected: [], dropTo: { target: null, align: '' } }, editorTemplate: "input", dataKeys: '', iconSize: 24, structureTemplate: 'oda-layout-designer-structure', containerTemplate: 'oda-layout-designer-container', focusedField: null, }, get selection() { return this.designInfo.selected; }, data: { $type: Object }, colors: { $public: true, $pdp: true, paper: { $def: { color: { $def: 'var(--content-color)', $editor: '@oda/color-picker[oda-color-picker]', }, background: { $def: 'var(--content-background)', $editor: '@oda/color-picker[oda-color-picker]', set(v) { console.log(v); } } }, $save: true, }, tools: { $def: { color: { $def: 'var(--dark-color)', $editor: '@oda/color-picker[oda-color-picker]', }, background: { $def: 'var(--dark-background)', $editor: '@oda/color-picker[oda-color-picker]', } }, $save: true, }, content: { $def: { color: { $def: 'var(--content-color)', $editor: '@oda/color-picker[oda-color-picker]', $save: true, }, background: { $def: 'var(--content-background)', $editor: '@oda/color-picker[oda-color-picker]', $save: true, } }, $save: true, } }, $public: { $pdp: true, get isDesignChanged() { const flatter = i => i.items && i.items.length ? i.items.flatMap(flatter) : [i]; const itemsToSave = this.designer.layout.items.flatMap(flatter); return !!itemsToSave.filter(i => i.layer.isChanged).length; }, designMode: { $def: false, async set(n) { if (n) { await ODA.import('@oda/tree'); await ODA.import('@tools/property-grid'); } this.designInfo = this.constructor.__rocks__.descrs.designInfo.$def(); }, } }, $observers: { setLayout(data, dataKeys) { if (dataKeys) { this.style.visibility = 'hidden'; this.layout = new LayoutItem(undefined, undefined, this.dataKeys, this.data); this.layout.hideExpander = true; this.layout.hideCheckbox = true; this.layout.__expanded__ = true; this.layout.designer = this; } }, // restoreDesign(layout, settings){ // layout.settings.groups = []; // layout.actions = settings; // this.async(()=>{ // this.style.visibility = 'visible'; // }, 100) // } }, layout: { $type: Object } }) ODA({ is: 'oda-layout-designer-structure', template: /*html*/` <style> :host { pointer-events: all !important; filter: none !important; opacity: 1 !important; position: relative; @apply --horizontal; @apply --no-flex; flex-wrap: wrap;/*{{isBlock? 'none' : 'wrap'}};*/ background-color: {{paper.background}} !important; @apply --header; } :host([is-tabs-container]){ flex-wrap: nowrap !important; border: none !important; overflow: hidden; } :host([align=top]){ flex-direction: column !important; } :host([is-block]){ margin: 1px !important; } :host([align=bottom]){ flex-direction: column !important; } </style> <div ~is="containerTemplate" ~for="items" :item="$for.item" :is-active-tab="isTabsContainer && layout?.focusedTab === $for.item" :denied="_denied($for.item)" :allow="_allow($for.item)" :outlined="_allowDrop($for.item)" :info="_allowDrop($for.item)"></div> `, _allowDrop(item) { return this.designMode && this.designInfo.selected.includes(item); }, _denied(item) { const layer = this.designMode && this.designInfo.selected?.[0]?.layer; return layer && layer !== item.layer; }, _allow(item) { return this.designMode && !!this.designInfo.selected.length && this.designInfo.selected?.[0]?.layer === item.layer; }, isTabsContainer: { get() { return this.layout?.itemType === 'tabs'; }, $attr: true }, get items() { return this.layout?.items; }, isBlock: { $type: Boolean, $attr: true, get() { return this.designMode && this.layout?.itemType === 'block'; } }, layout: { $type: Object, async set(v) { if (v?.isLayerRoot) { this.style.visibility = 'hidden'; v.settings ??= await this.loadSettings(v) || {}; this.designer.debounce('show-layer', () => { this.designer.style.visibility = 'visible'; }, 200); this.async(() => { this.style.visibility = 'inherit'; }); } } }, $pdp: { align: { $type: '', $attr: true, get() { return this.layout?.align; } } } }) ODA({ is: 'oda-layout-designer-container', template: /*html*/` <style> :host { position: relative; margin: {{isTab?'0px':'4px'}}; padding: {{isGroup?'4px 2px': '0px 0px 2px 0px'}}; max-height: {{(showExt || extendedMode || isGroup || isBlock || isTabs)?'initial':((iconSize * 2.2) +'px')}}; max-width: {{maxWidth? (maxWidth + 8) + 'px':'calc(100% - 8px)'}}; min-width: {{((extendedMode || isGroup) && !isBlock && !isTabs)?'calc(100% - 8px)':(item?.width?item?.width:'')}}; box-sizing: border-box; @apply --horizontal; overflow: hidden; @apply --flex; flex: {{maxWidth ? '0 0 auto':'1000000000000000000000000000000 1 auto'}} !important; display: {{hidden && !designMode?'none':'flex'}}; } :host([denied]) { background-color: rgba(255,0,0,.3); color: var(--dark-background); fill: var(--dark-background); } :host([allow]) { background-color: rgba(0, 255, 0,.3); color: var(--content-color); } :host([allow]) > fieldset, :host([denied]) > fieldset { background-color: transparent !important; } :host([is-group]) { @apply --dark; /*@apply --raised;*/ border: 1px solid {{paper.background}}; min-width: calc(100% - 8px) !important; border-radius: 6px; } :host([is-block]) { padding: 0px !important; } :host([info]) { @apply --info; } legend { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left; font-size: small; /*font-weight: bold;*/ /*opacity: .5;*/ margin-left: 4px; max-width: 90%; } :host([drop-to]) { @apply --outlined; } :host([drop-to=top])::before { content: ""; position: absolute; top: 0px; left: 0px; right: 0px; height: 3px; z-index: 1; background-color: var(--success-color); } :host([drop-to=bottom])::after { content: ""; position: absolute; bottom: 0px; left: 0px; right: 0px; height: 3px; z-index: 1; background-color: var(--success-color); } :host([drop-to=left])::before { content: ""; position: absolute; top: 0px; left: 0px; bottom: 0px; width: 3px; z-index: 1; background-color: var(--success-color); } :host([drop-to=right])::after { content: ""; position: absolute; bottom: 0px; top: 0px; right: 0px; width: 3px; z-index: 1; background-color: var(--success-color); } :host([is-tab]) { padding: 0px !important; visibility: hidden; min-width: calc(100%-8px); order: 1; } :host([is-tabs]) { padding: 0px !important; } :host([is-active-tab]) { visibility: inherit; order: 0 !important; } </style> <fieldset ~is="fieldset" class="vertical flex" :title :content="!isGroup" style="border-radius: 4px; min-width: 0px;" ~style="_fieldsetStyle" > <legend ~if="isField" ~html="title" ~style="{color: isGroup?'inherit':item.textColor}"></legend> <div ~if="!isBlock" class="horizontal" style="overflow: hidden; justify-content: end;" ~show="!isTabs && !isTab"> <oda-icon ~if="expandIcon" :icon="expandIcon" :icon-size style="cursor: pointer; opacity: .5" @pointerdown.stop @tap.stop="expand" ></oda-icon> <div ~is="item?.editorTemplate || editorTemplate" id="input" :disabled="designMode && !isGroup" class="flex" :layout="item" style="border: none; outline: none;" :focused="item === focusedField" @focusin="this.editorFocusIn($event, item)" ></div> </div> <div ~if="showExt" ~is="item?.structureTemplate || structureTemplate" :layout="item" style="position: relative;" :flex="isTab || isGroup" ></div> </fieldset> `, editorFocusIn(e, item) { this.focusedField = item; e.target.focus(); }, get showExt() { return this.item?.__expanded__ || this.isTab }, get _fieldsetStyle() { return { border: (this.isGroup || !this.isField) ? 'none' : '1px solid ' + this.item.textColor, padding: !this.isField ? '0px' : '0px 0px;' } }, get isField() { return !this.isTabs && !this.isBlock && !this.isTab; }, get fieldset() { if (!this.isField) return 'div'; return 'fieldset'; }, tabindex: { get() { if (this.designMode && this.designInfo.selected.includes(this.item)) { this.async(() => { this.focus(); }) return 0; } this.blur(); return ''; }, $attr: true, }, isGroup: { $type: Boolean, $attr: true, get() { return this.item?.itemType === 'group'; } }, isBlock: { $type: Boolean, $attr: true, get() { return this.item?.itemType === 'block'; } }, isTabs: { $type: Boolean, $attr: true, get() { return this.item?.itemType === 'tabs'; } }, isTab: { $type: Boolean, $attr: true, get() { return this.item?.itemType === 'tab'; } }, // $pdp: { get maxWidth() { return this.$('#input')?.maxWidth || undefined; }, // }, // $pdp:{ // get maxWidth(){ // return this.item?.maxWidth; // }, // }, dropTo: { $attr: true, get() { if (this.designMode) return this.designInfo.dropTo.target === this && this.designInfo.dropTo.align; return ''; } }, hidden: { $attr: true, get() { return this.item?.hidden; } }, get title() { return this.item?.title; }, get expandIcon() { return (this.extendedMode || this.isGroup) ? (this.item?.__expanded__ ? 'icons:chevron-right:90' : 'icons:chevron-right') : ''; }, get extendedMode() { return this.item?.items?.length; }, expand() { if (!this.expandIcon) return; this.item.__expanded__ = !this.item.__expanded__; }, $public: { label: { $def: { color: 'black', align: 'top', hidden: false }, }, }, draggable: { $attr: true, get() { return this.designMode ? 'true' : 'false'; } }, item: {}, $keyBindings: { escape(e) { if (!this.item?.owner?.name) return; e.stopPropagation(); this.designInfo.selected = [this.item.owner]; this.blur(); } }, $listeners: { dragover(e) { if (!this.designMode) return; e.stopPropagation(); if (this.designInfo.selected[0]?.layer !== this.item.layer) return; if (this.designInfo.selected.includes(this.item)) return; e.preventDefault(); const w = e.target.offsetWidth; const h = e.target.offsetHeight; const min = Math.min(w, h) / 2 const x = e.layerX; const y = e.layerY; this.designInfo.dropTo.target = e.target; let align = ''; this.designInfo.dropTo.align = ''; if (w > h) { if (x < min) align = 'left'; else if (x > w - min) align = 'right'; else if (y < min) align = 'top'; else align = 'bottom'; } else { if (y < min) align = 'top'; else if (y > h - min) align = 'bottom'; else if (x < min) align = 'left'; else align = 'right'; } this.designInfo.dropTo.align = align; }, dragleave(e) { this.designInfo.dropTo.align = ''; this.designInfo.dropTo.target = null; }, drop(e) { if (!this.designInfo.dropTo.target) return; if (this.designInfo.dropTo.target?.item.itemType === 'tab') this.designInfo.dropTo.target.item.addItems({ items: this.designInfo.selected, align: this.designInfo.dropTo.align }, true); else this.designInfo.selected = [this.designInfo.dropTo.target.item.addBlock({ items: this.designInfo.selected, align: this.designInfo.dropTo.align }, true)]; this.designInfo.dropTo.align = ''; this.designInfo.dropTo.target = null; }, dragstart(e) { if (!this.designMode) return; e.stopPropagation(); if (!this.designInfo.selected.includes(this.item)) this.$listeners.mousedown.call(this, e) }, mousedown(e) { if (this.designMode) { selectItem.call(this, e) window.dispatchEvent(new PointerEvent('mousedown', e)) this.designer.$render(); } }, contextmenu(e) { if (!this.designInfo.selected.includes(this.item)) return; contextMenu.call(this, e) } } }) function selectItem(e) { if (!this.designMode) return; e.stopPropagation(); if (e.button === 0) { if (!e.ctrlKey || !this.designInfo.selected.length) { if (!this.designInfo.selected.includes(this.item)) this.designInfo.selected = [this.item]; } else if (this.designInfo.selected[0].layer === this.item.layer) { if (this.designInfo.selected.includes(this.item)) this.designInfo.selected.remove(this.item); else this.designInfo.selected.add(this.item); } } else if (!this.designInfo.selected.includes(this.item)) { if (!e.ctrlKey) this.designInfo.selected = [this.item]; else this.designInfo.selected.add(this.item); } } async function contextMenu(e) { if (!this.designMode) return; e.preventDefault(); if (!this.designInfo.selected.includes(this.item)) this.$listeners.mousedown?.call(this, e) await ODA.import('@tools/containers'); await ODA.import('@oda/menu'); const menu = [ { label: `Grouping`, icon: 'fontawesome:s-object-group', execute: () => { this.item.group({ items: this.designInfo.selected, align: '' }, true); } } ]; if (this.item.itemType) { menu.push({ label: `Ungrouping`, icon: 'fontawesome:s-object-ungroup', execute: () => { this.item.ungroup({ items: this.designInfo.selected }, true); } }) } menu.push({ label: this.item.hidden ? `Show` : 'Hide', icon: this.item.hidden ? 'icons:visibility' : 'icons:visibility-off', execute: () => { const hidden = !this.item.hidden this.designInfo.selected.add(this.item); for (let item of this.designInfo.selected) { item.hidden = hidden; } } }) const res = await ODA.showDropdown('oda-menu', { iconSize: this.iconSize, items: menu }, { title: `Design menu [${this.designInfo.selected.length}]`, allowClose: true }); res?.focusedItem.execute(); } ODA({is:'oda-layout-designer-tabs', template:/*html*/` <style> :host { background-color: transparent !important; @apply --horizontal; overflow: hidden; } .tab { @apply --border; @apply --horizontal; @apply --raised; border-bottom: none; border-top-left-radius: 4px; border-top-right-radius: 4px; font-size: small; margin-top: 2px; cursor: pointer; padding: 0px 2px; @apply --dark; } .tab[focused]{ color: {{tools.background}} !important; fill: {{tools.background}} !important; background-color: {{tools.color}} !important; } .tab[focused] oda-button{ color: {{tools.background}} !important; fill: {{tools.background}} !important; } oda-button{ fill: {{tools.color}}; color: {{tools.color}}; } </style> <div class="flex horizontal" style="overflow: hidden; flex-wrap: wrap;" @pointerdown.stop> <div class="tab flex horizontal" :focused="getFocusedTab($for.item)" ~for="tabs" @tap.stop="selTab($for.item)" ~show="isShow($for.item)"> <div class="horizontal flex" style="justify-content: center; min-width: 100px; align-items: center; margin: 4px;"> <oda-icon :icon="$for.item.icon" :icon-size ~if="$for.item.icon"></oda-icon> <label style="text-align: center; margin: 4px; max-width: 90%;">{{$for.item.label}}</label> </div> <oda-button icon="icons:close" class="no-flex" ~if="designMode" :icon-size @tap.stop="removeTab($for.item)"></oda-button> </div> </div> <oda-button icon="icons:add" class="no-flex" ~if="designMode" @pointerdown.stop :icon-size @tap="addTab"></oda-button> `, isShow(item){ return !item.hidden; }, getFocusedTab(item){ return this.tabsItem?.focusedTab === item; }, removeTab(item){ item.remove(true); }, selTab(item){ this.designInfo.selected = [item]; this.tabsItem.focusedTab = item; this.domHost.$render(); this.$render(); }, get tabsItem(){ return this.layout.items.find(i=>i.itemType === 'tabs'); }, get tabs(){ return this.tabsItem?.items; }, set disabled(n){ this.disabled = false; }, addTab(e){ e.stopPropagation(); this.layout.addTab({}, true); }, layout: null, }) ODA({is:'oda-layout-designer-table-cell', extends: 'oda-icon, oda-table-cell', template: /*html*/` <style> .icon { padding: 2px; opacity: .5; transform: scale(.5); display: none; } :host([icon]) .icon { display: block !important; } label { padding: 4px; } :host([is-dark]) > label { @apply --dark; border-radius: 4px; } :host([drop-to]) { @apply --outlined; } :host([drop-to=top])::before { content: ""; position: absolute; top: 0px; left: 0px; right: 0px; height: 3px; z-index: 1; background-color: var(--success-color); } :host([drop-to=bottom])::after { content: ""; position: absolute; bottom: 0px; left: 0px; right: 0px; height: 3px; z-index: 1; background-color: var(--success-color); } :host([drop-to=left])::before { content: ""; position: absolute; top: 0px; left: 0px; bottom: 0px; width: 3px; z-index: 1; background-color: var(--success-color); } :host([drop-to=right])::after { content: ""; position: absolute; bottom: 0px; top: 0px; right: 0px; width: 3px; z-index: 1; background-color: var(--success-color); } </style> <oda-button ~if="isDesigned" @down.stop @up.stop icon="carbon:reset" @pointerdown.stop @pointerup.stop @tap.stop="item.reset()" ></oda-button> <oda-button :disabled="isLayerRoot && !item?.isChanged" :icon="buttonIcon" @down.stop @up.stop @pointerdown.stop @pointerup.stop @tap.stop="buttonTap" ></oda-button> `, get isDesigned() { return this.item?.settings?.actions?.length || Object.keys(this.item?.settings?.states || {}).length; }, dropTo: { $attr: true, get() { if (this.designMode) return this.designInfo.dropTo.target?.item === this.item && this.designInfo.dropTo.align; return ''; } }, async buttonTap(e) { switch (this.item.itemType) { case 'group': { this.item.addTab(true); } break; case 'tab': { this.item.remove(true); } break; default: { if (this.isLayerRoot && this.item.isChanged) { await this.saveSettings(this.item); this.item.isChanged = false; this.$render(); } } } }, disabled: { $attr: true, get() { return this.item?.hidden; } }, header: { $attr: true, get() { return this.isLayerRoot; } }, isDark: { $attr: true, get() { return this.item?.itemType && this.item?.itemType !== 'block'; } }, get isLayerRoot() { return this.item.isLayerRoot; }, get buttonIcon() { switch (this.item.itemType) { case 'group': return 'icons:add'; case 'tab': return 'icons:close'; default: { if (this.isLayerRoot) return 'icons:save'; } } }, icon: { $attr: true, get() { return this.item?.icon; } }, get value() { return this.item.label + ' [' + this.item.name + ']' }, $listeners: { contextmenu(e) { contextMenu.call(this, e) }, mousedown(e) { if (e.button === 2) { selectItem.call(this, e) } else if (this.item.itemType === 'tab') { this.item.owner.focusedTab = this.item; } window.dispatchEvent(new PointerEvent('mousedown', e)) } } }) const LayoutItem = class extends ROCKS({ settings: { $type: Object, async set(n) { for (let step of n?.actions || []) { let context = await this.findItem(step.context); if (!context) continue; const params = {}; for (let p in step.params) { let value = step.params[p]; switch (p) { case 'items': { value = await Promise.all(value.map(async i => { return await this.findItem(i); })) } break; case 'old': case 'item': { value = await this.findItem(value); } } params[p] = value; } context[step.action](params); } } }, get states() { const layer = this.layer || this; return layer.settings?.states?.[this.name]; }, groups: [], get path() { if (this.isLayerRoot) return !this.layer ? ('ROOT') : this.layer.path + '/' + this.name; }, async findItem(name) { const items = (this.items?.then ? await this.items : this.items) || [] for (let i of items) { if (i.layer === this || this.layer) { if (i.name == name) return i; const res = await i.findItem(name); if (res) return res; } } }, align: '', get checked() { return this.hidden ? 'unchecked' : 'checked'; }, set checked(v) { this.hidden = v === 'unchecked'; this.layer.isChanged = true; }, group(params = { items: [], align }, inLog) { let items = [...params.items]; let align = params.align; let idx = this.owner.items.indexOf(this); const layer = this.layer; const name = this.generateName(align ? 'block' : 'group'); const data = { itemType: (align ? 'block' : 'group'), name: name, [this.dataKeys]: [] } layer.groups.push(data); const grp = new LayoutItem(this.owner, this.layer, this.dataKeys, data) this.owner?.items.splice(idx, 0, grp); grp.align = align; if (!align) { grp.editorTemplate = 'oda-layout-designer-tabs'; } while (items.length) { grp.addItem({ item: items.shift() }); } grp.__expanded__ = true; if (inLog) this.addAction({ action: 'group', context: this.name, params: { items: params.items.map(i => i.name), align } }); return grp; }, addBlock(params = { items: [], align: '' }, inLog) { let toItem = this; let items = [...params.items]; const align = params.align; if (items.length > 1) { let grp = items[0].group({ items, align: items[0].owner.align || align }); items = [grp]; } switch (align) { case 'left': case 'top': { items.push(toItem); } break; default: { items.unshift(toItem); } } if (inLog) this.addAction({ action: 'addBlock', context: this.name, params: { items: params.items.map(i => i.name), align } }); return toItem.group({ items, align }); }, generateName(type) { return type + (this.layer.groups.filter(i => i.itemType === type).length + 1); }, addTab({ }, inLog) { if (this.itemType !== 'group') return; let tabs = this.items.find(i => { return i.itemType === 'tabs' }) let items = []; if (!tabs) { items = this.items; const name = this.generateName('tabs'); const data = { itemType: 'tabs', name, [this.dataKeys]: [], label: 'tabs' } this.layer.groups.push(data); tabs = new LayoutItem(this, this.layer, this.dataKeys, data); this.items = [tabs]; tabs.__expanded__ = true; tabs.hideExpander = true; } const name = this.generateName('tab'); const data = { itemType: 'tab', name, [this.dataKeys]: [] } this.layer.groups.push(data); const tab = new LayoutItem(tabs, this.layer, this.dataKeys, data); tab.addItems({ items }); tabs.addItem({ item: tab }); tab.__expanded__ = true; data.label = 'Tab ' + (tabs.items.length); if (inLog) { tabs.focusedTab = tab; this.addAction({ action: 'addTab', context: this.name }); } else if (tabs.items.length === 1) tabs.focusedTab = tab; }, ungroup(params = { items }, inLog) { let items = params.items; if (!this.itemType) return; this.owner.replaceItems({ item: this, items: this.items }); items?.forEach(i => { i.ungroup({}); }) if (inLog) this.addAction({ action: 'ungroup', context: this.name, params: { items: params.items.map(i => i.name) } }); }, removeItem(params = { item }, inLog) { let item = params.item; if (!this.items.includes(item)) return; const res = this.items.remove(item); switch (this.itemType) { case 'block': { if (this.items.length === 1) { this.owner.replaceItem({ old: this, item: this.items[0] }); } } break; case 'tabs': case 'group': { if (this.items.length === 0) { this.owner.removeItem({ item: this }); } } } if (inLog) this.addAction({ action: 'removeItem', context: this.name, params: { item: item.name } }); }, remove(inLog) { if (this.itemType === 'tab') { let nextFocus = this.owner.items.indexOf(this); const grp = this.owner.owner; const grp_idx = grp.owner.items.indexOf(grp) + 1; grp.owner.items.splice(grp_idx, 0, ...this.items) // this.layer.addItems({items: this.items}); this.owner.removeItem({ item: this }); while (nextFocus > -1) { const next = this.owner.items[nextFocus]; if (next?.itemType === 'tab') { this.owner.focusedTab = next; break; } nextFocus--; } } else { this.owner.replaceItems({ item: this, items: this.items }); this.owner.removeItem({ item: this }); } if (inLog) this.addAction({ action: 'remove', context: this.name }); }, addItems(params = { items }, inLog) { let items = params.items; for (let item of items) { this.addItem({ item }); } if (inLog) this.addAction({ action: 'addItems', context: this.name, params: { items: params.items.map(i => i.name) } }); }, addItem(params = { item }, inLog) { let item = params.item; item.owner?.removeItem({ item }); this.items.push(item); item.owner = this; if (inLog) this.addAction({ action: 'addItem', context: this.name, params: { item: item.name } }); }, replaceItems(params = { item, items }, inLog) { let items = params.items; let item = params.item; const idx = this.items.indexOf(item); if (idx < 0) return; this.items.splice(idx, 1, ...items); for (let i of items) i.owner = this; if (inLog) this.addAction({ action: 'replaceItems', context: this.name, params: { item, items: params.items.map(i => i.name) } }); }, replaceItem(params = { old, item }, inLog) { let old = params.old; let item = params.item; const idx = this.items.indexOf(old); this.items.splice(idx, 1, item); item.owner = this; if (inLog) this.addAction({ action: 'replaceItem', context: this.name, params: { old: old.name, item: item.name } }); }, items: { $def: [], get() { const items = this.data?.[this.dataKeys]; const layer = this.itemType === 'group' ? this.layer : this; if (items?.then) { return items.then(items => { return items.map(i => new LayoutItem(this, layer, this.dataKeys, i)) }) } return items?.map(item => new LayoutItem(this, layer, this.dataKeys, item)) } }, addAction(action) { const layer = this.layer || this; layer.settings.actions ??= []; layer.settings.actions.push(action); layer.isChanged = true; this.root.designer.$render(); }, get isLayerRoot() { return !!((this.items?.[0]?.layer === this) || this.items?.then); }, title: { $readOnly: true, get() { return this.label; } }, get root() { return this.owner?.root || this; }, $public: { label: { $type: String, get() { return this.states?.label || this.data.label || this.name || (!this.owner && 'ROOT'); }, set(n) { this.setPropValue('label', n) } }, hidden: { $type: Boolean, get() { return this.states?.hidden || this.data.hidden || false; }, set(n) { this.setPropValue('hidden', n) } }, textColor: { $type: String, $editor: '@oda/color-picker[oda-color-picker]', get() { return this.states?.textColor || 'var(--dark-background)'; }, set(n) { this.setPropValue('textColor', n) } } }, setPropValue(prop, val) { const layer = this.layer || this; layer.settings ??= {}; layer.settings.states ??= {}; layer.settings.states[this.name] ??= {}; this.states[prop] = val; layer.isChanged = true; this.root.designer.$render(); }, get name() { return this.data?.name; }, isChanged: false, get itemType() { return this.data?.itemType; }, data: Object, __expanded__: false, width: undefined, get icon() { return this.data?.icon; }, reset(deep) { if (deep) { for (let item of ((this.items?.length && this.items) || [])) { item.reset(deep); } } if (this.settings?.actions?.length) { const exp = this.__expanded__; this.items = undefined; this.settings.actions = []; this.isChanged = true; this.__expanded__ = exp; } } }) { constructor(owner, layer, dataKeys = 'items', data) { super(...arguments); this.data = data; this.owner = owner; this.dataKeys = layer?.dataKeys || dataKeys; this.layer = layer; } }