@infinite-canvas-tutorial/webcomponents
Version:
WebComponents UI implementation
294 lines (287 loc) • 11.6 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { html, css, LitElement } from 'lit';
import { consume } from '@lit/context';
import { map } from 'lit/directives/map.js';
import { when } from 'lit/directives/when.js';
import { customElement } from 'lit/decorators.js';
import { Task, sortByFractionalIndex, UI, ZIndex, } from '@infinite-canvas-tutorial/ecs';
import { apiContext, appStateContext, nodesContext } from '../context';
import { Event } from '../event';
import { localized, msg, str } from '@lit/localize';
let LayersPanel = class LayersPanel extends LitElement {
connectedCallback() {
super.connectedCallback();
this.api.element.addEventListener(Event.SELECTED_NODES_CHANGED, (e) => {
const { selected } = e.detail;
// Scroll to the selected layer
if (selected.length > 0) {
const scrollToId = this.generateLayersPanelItemId(selected[0]);
const scrollToElement = this.shadowRoot.querySelector(`#${scrollToId}`);
const container = this.shadowRoot.querySelector('.container');
if (scrollToElement && container) {
// 计算元素相对于容器的位置
const elementTop = scrollToElement.offsetTop;
const elementHeight = scrollToElement.offsetHeight;
const containerHeight = container.clientHeight;
// 如果元素不在视口内,则滚动到合适位置
const currentScrollTop = container.scrollTop;
const elementBottom = elementTop + elementHeight;
const visibleTop = currentScrollTop;
const visibleBottom = currentScrollTop + containerHeight;
if (elementTop < visibleTop || elementBottom > visibleBottom) {
// 计算目标滚动位置,让元素在视口中央
const targetScrollTop = elementTop - (containerHeight - elementHeight) / 2;
container.scrollTo({
top: Math.max(0, targetScrollTop),
behavior: 'smooth',
});
}
}
}
});
}
generateLayersPanelItemId(node) {
return `layers-panel-item-${node.id}`;
}
handleClose() {
this.api.setAppState({
taskbarSelected: this.appState.taskbarSelected.filter((task) => task !== Task.SHOW_LAYERS_PANEL),
});
}
handleDelete() {
const { layersSelected } = this.api.getAppState();
this.api.deleteNodesById(layersSelected);
// Try to select the next layer
const nextLayer = this.nodes.find((node) => !layersSelected.includes(node.id));
if (nextLayer) {
this.api.selectNodes([nextLayer]);
}
this.api.record();
}
handleAdd() {
// TODO: add new layer
// this.api.addNodes(this.nodes);
}
handleSelect(e, id) {
const { layersSelected } = this.api.getAppState();
if (layersSelected.length === 1 && layersSelected.includes(id)) {
return;
}
const node = this.api.getNodeById(id);
if (!node.locked) {
this.api.selectNodes([node], e.shiftKey);
this.api.record();
}
}
handleBringToFront() {
const { layersSelected } = this.api.getAppState();
const node = this.api.getNodeById(layersSelected[0]);
this.api.bringToFront(node);
this.api.record();
}
handleBringForward() {
const { layersSelected } = this.api.getAppState();
const node = this.api.getNodeById(layersSelected[0]);
this.api.bringForward(node);
this.api.record();
}
handleSendBackward() {
const { layersSelected } = this.api.getAppState();
const node = this.api.getNodeById(layersSelected[0]);
this.api.sendBackward(node);
this.api.record();
}
handleSendToBack() {
const { layersSelected } = this.api.getAppState();
const node = this.api.getNodeById(layersSelected[0]);
this.api.sendToBack(node);
this.api.record();
}
render() {
const { layersSelected, taskbarSelected } = this.appState;
const sortedNodes = this.nodes
.filter((node) => node.parentId === undefined)
.map((node) => {
return this.api.getEntity(node);
})
.sort(sortByFractionalIndex)
.map((entity) => {
return this.api.getNodeByEntity(entity);
});
const isSelectedEmpty = layersSelected.length === 0;
let bringForwardDisabled = false;
let sendBackwardDisabled = false;
if (layersSelected.length === 1) {
const node = this.api.getNodeById(layersSelected[0]);
if (node) {
const children = this.api
.getSiblings(node)
.filter((child) => !child.has(UI));
const maxZIndex = Math.max(...children.map((child) => child.read(ZIndex).value));
const minZIndex = Math.min(...children.map((child) => child.read(ZIndex).value));
if (node.zIndex === maxZIndex) {
bringForwardDisabled = true;
}
if (node.zIndex === minZIndex) {
sendBackwardDisabled = true;
}
}
}
return taskbarSelected.includes(Task.SHOW_LAYERS_PANEL)
? html `<section>
<h4>
${msg(str `Layers`)}
<sp-action-button quiet size="s" =${this.handleClose}>
<sp-icon-close slot="icon"></sp-icon-close>
</sp-action-button>
</h4>
<sp-action-group class="actions">
<sp-action-button quiet size="s" disabled =${this.handleAdd}>
<sp-tooltip self-managed placement="bottom">
${msg(str `Add new layer`)}
</sp-tooltip>
<sp-icon-add slot="icon"></sp-icon-add>
</sp-action-button>
<sp-action-menu
label=${msg(str `Arrange layers`)}
quiet
size="s"
.disabled=${layersSelected.length === 0}
>
<sp-icon-show-all-layers slot="icon"></sp-icon-show-all-layers>
<sp-menu-group>
<span slot="header">${msg(str `Arrange layers`)}</span>
<sp-menu-item
?disabled=${isSelectedEmpty || bringForwardDisabled}
=${this.handleBringToFront}
>
<sp-icon-layers-bring-to-front
slot="icon"
></sp-icon-layers-bring-to-front>
${msg(str `Bring to front`)}
</sp-menu-item>
<sp-menu-item
?disabled=${isSelectedEmpty || bringForwardDisabled}
=${this.handleBringForward}
>
<sp-icon-layers-forward slot="icon"></sp-icon-layers-forward>
${msg(str `Bring forward`)}
</sp-menu-item>
<sp-menu-item
?disabled=${isSelectedEmpty || sendBackwardDisabled}
=${this.handleSendBackward}
>
<sp-icon-layers-backward
slot="icon"
></sp-icon-layers-backward>
${msg(str `Send backward`)}
</sp-menu-item>
<sp-menu-item
?disabled=${isSelectedEmpty || sendBackwardDisabled}
=${this.handleSendToBack}
>
<sp-icon-layers-send-to-back
slot="icon"
></sp-icon-layers-send-to-back>
${msg(str `Send to back`)}
</sp-menu-item>
</sp-menu-group>
</sp-action-menu>
<sp-action-button
quiet
size="s"
=${this.handleDelete}
.disabled=${layersSelected.length === 0}
>
<sp-tooltip self-managed placement="bottom">
${msg(str `Delete`)}
</sp-tooltip>
<sp-icon-delete slot="icon"></sp-icon-delete>
</sp-action-button>
</sp-action-group>
<div class="container">
${map(sortedNodes, (node) => {
return this.renderParentNode(node);
})}
</div>
</section>`
: null;
}
renderParentNode(node, depth = 0) {
const children = this.api.getChildren(node);
const sortedNodes = children.sort(sortByFractionalIndex).map((entity) => {
return this.api.getNodeByEntity(entity);
});
const { layersSelected, layersHighlighted, layersExpanded } = this.appState;
const hasChildren = sortedNodes.length > 0;
const isExpanded = layersExpanded.includes(node.id);
return html `<ic-spectrum-layers-panel-item
id=${this.generateLayersPanelItemId(node)}
.node=${node}
.depth=${depth}
.hasChildren=${hasChildren}
draggable
=${(e) => this.handleSelect(e, node.id)}
?selected=${layersSelected.includes(node.id)}
?highlighted=${layersHighlighted.includes(node.id)}
></ic-spectrum-layers-panel-item>
${when(hasChildren && isExpanded, () => html `
${map(sortedNodes, (node) => {
// TODO: virtual scroll for better performance
return this.renderParentNode(node, depth + 1);
})}
`)}`;
}
};
LayersPanel.styles = css `
section {
display: flex;
flex-direction: column;
background: var(--spectrum-gray-100);
border-radius: var(--spectrum-corner-radius-200);
margin: 4px;
filter: drop-shadow(
var(--spectrum-drop-shadow-color) 0px var(--spectrum-drop-shadow-y)
var(--spectrum-drop-shadow-blur)
);
}
h4 {
padding: var(--spectrum-global-dimension-size-100);
display: flex;
align-items: center;
justify-content: space-between;
margin: 0;
color: canvastext;
}
.actions {
padding: 4px;
padding-top: 0;
}
.container {
height: 300px;
overflow: hidden;
overflow-y: auto;
scroll-behavior: smooth;
scroll-padding: 8px;
}
`;
__decorate([
consume({ context: appStateContext, subscribe: true })
], LayersPanel.prototype, "appState", void 0);
__decorate([
consume({ context: nodesContext, subscribe: true })
], LayersPanel.prototype, "nodes", void 0);
__decorate([
consume({ context: apiContext, subscribe: true })
], LayersPanel.prototype, "api", void 0);
LayersPanel = __decorate([
customElement('ic-spectrum-layers-panel'),
localized()
], LayersPanel);
export { LayersPanel };
//# sourceMappingURL=layers-panel.js.map