Skip to content

Commit

Permalink
[duoyun-ui] Improve table select
Browse files Browse the repository at this point in the history
[gem] Fixed slot/part static decorator
[gem] Remove `GemElement.closestElement`

Closed #165
  • Loading branch information
mantou132 committed Jun 23, 2024
1 parent 59b74d8 commit 417d5ef
Show file tree
Hide file tree
Showing 14 changed files with 184 additions and 112 deletions.
49 changes: 36 additions & 13 deletions packages/duoyun-ui/src/elements/selection-box.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { adoptedStyle, customElement, emitter, Emitter, state } from '@mantou/gem/lib/decorators';
import { adoptedStyle, customElement, emitter, Emitter, property } from '@mantou/gem/lib/decorators';
import { GemElement, html } from '@mantou/gem/lib/element';
import { createCSSSheet, css, styleMap } from '@mantou/gem/lib/utils';
import { addListener, createCSSSheet, css, styleMap } from '@mantou/gem/lib/utils';

import { theme } from '../lib/theme';
import { contentsContainer } from '../lib/styles';
import { isInputElement } from '../lib/element';

import './reflect';

Expand Down Expand Up @@ -36,8 +37,13 @@ export type SelectionChange = {
@customElement('dy-selection-box')
@adoptedStyle(contentsContainer)
export class DuoyunSelectionBoxElement extends GemElement<State> {
@property container?: HTMLElement;

@emitter change: Emitter<SelectionChange>;
@state selecting: boolean;

get #container() {
return this.container || ((this.getRootNode() as ShadowRoot).host as HTMLElement | undefined) || document.body;
}

state: State = {
rect: {
Expand All @@ -57,10 +63,29 @@ export class DuoyunSelectionBoxElement extends GemElement<State> {
return 'delete';
};

#getCursor = () => {
switch (this.state.mode) {
case 'append':
return 'copy';
case 'delete':
return 'crosshair';
default:
return 'cell';
}
};

#restoreContainerStyle: () => void;

#onPointerDown = (evt: PointerEvent) => {
if (evt.altKey) return;
const target = evt.composedPath()[0];
if (target instanceof HTMLElement && isInputElement(target)) return;
document.getSelection()?.removeAllRanges();
this.selecting = true;
const container = this.#container;
const containerStyle = container.getAttribute('style');
this.#restoreContainerStyle = () =>
containerStyle ? container.setAttribute('style', containerStyle) : container.removeAttribute('style');
container.style.userSelect = 'none';
this.setState({ start: [evt.x, evt.y], mode: this.#getMode(evt) });
addEventListener('pointermove', this.#onPointerMove);
addEventListener('pointerup', this.#onPointerUp);
Expand All @@ -69,7 +94,7 @@ export class DuoyunSelectionBoxElement extends GemElement<State> {
};

#onPointerUp = () => {
this.selecting = false;
this.#restoreContainerStyle();
this.setState({ start: undefined, stop: undefined });
removeEventListener('pointermove', this.#onPointerMove);
removeEventListener('pointerup', this.#onPointerUp);
Expand All @@ -78,8 +103,6 @@ export class DuoyunSelectionBoxElement extends GemElement<State> {
};

#onPointerMove = (evt: PointerEvent) => {
// disabled text select
evt.preventDefault();
const start = this.state.start!;
const x = evt.x === start[0] ? evt.x + 0.01 : evt.x;
const y = evt.y === start[1] ? evt.y + 0.01 : evt.y;
Expand All @@ -97,11 +120,10 @@ export class DuoyunSelectionBoxElement extends GemElement<State> {
};

mounted = () => {
const root = this.getRootNode();
root.addEventListener('pointerdown', this.#onPointerDown);
return () => {
root.removeEventListener('pointerdown', this.#onPointerDown);
};
this.effect(
() => addListener(this.#container, 'pointerdown', this.#onPointerDown),
() => [this.container],
);
};

render = () => {
Expand All @@ -118,14 +140,15 @@ export class DuoyunSelectionBoxElement extends GemElement<State> {
top: top + 'px',
width: width + 'px',
height: height + 'px',
cursor: this.#getCursor(),
})}
></dy-selection-box-mask>
</dy-reflect>
`;
};
}

const borderWidth = 10;
const borderWidth = 100;
const maskStyle = createCSSSheet(css`
:host {
position: fixed;
Expand Down
35 changes: 20 additions & 15 deletions packages/duoyun-ui/src/elements/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ const styles = createCSSSheet(css`
overflow: auto;
font-size: 0.875em;
font-variant-numeric: tabular-nums;
border-radius: ${theme.normalRound};
overscroll-behavior: auto;
container-type: inline-size;
}
Expand All @@ -52,12 +51,6 @@ const styles = createCSSSheet(css`
/* 为啥用户代理在 localhost 下是 normal,但在 StackBlitz 下没有设置? */
font-size: inherit;
}
.selection:where([data-selecting], :state(selecting)) ~ table {
user-select: none;
}
.selection ~ table {
cursor: crosshair;
}
thead {
text-align: -webkit-match-parent;
text-align: -moz-match-parent;
Expand All @@ -70,23 +63,27 @@ const styles = createCSSSheet(css`
border-block-end: 1px solid ${theme.borderColor};
transition: background 0.1s;
}
tbody tr:hover {
tbody tr:where(:hover, [data-state-active='?1']) {
background-color: ${theme.lightBackgroundColor};
}
td {
position: relative;
}
tr.selected td::after {
table.selection td::after {
position: absolute;
display: block;
content: '';
top: 0px;
left: 0px;
width: 100%;
height: calc(100% + 1px);
/* ellipsis 单元格覆盖不到边框 */
inset: 0;
opacity: 0.15;
cursor: copy;
}
tr.selected td::after {
background-color: ${theme.informativeColor};
}
tr.selected td::after {
cursor: crosshair;
}
td.ellipsis {
overflow: hidden;
white-space: nowrap;
Expand Down Expand Up @@ -170,6 +167,7 @@ export class DuoyunTableElement<T = any, K = any> extends DuoyunScrollBoxElement

@boolattribute selectable: boolean;
@property selection?: K[];
@property selectionContainer?: HTMLElement;

@emitter select: Emitter<K[]>;
@emitter itemclick: Emitter<T>;
Expand Down Expand Up @@ -399,7 +397,7 @@ export class DuoyunTableElement<T = any, K = any> extends DuoyunScrollBoxElement

const rowSpanMemo = columns.map(() => 0);
return html`
<table part=${DuoyunTableElement.table}>
<table part=${DuoyunTableElement.table} class=${classMap({ selection: this.#selectionSet.size })}>
${this.caption
? html`
<caption>
Expand All @@ -412,6 +410,7 @@ export class DuoyunTableElement<T = any, K = any> extends DuoyunScrollBoxElement
${this.data?.map(
(record, _rowIndex, _data, colSpanMemo = [0]) => html`
<tr
data-state-active
@click=${() => record && this.#onItemClick(record)}
@contextmenu=${(evt: MouseEvent) => record && this.#onItemContextMenu(evt, record)}
part=${DuoyunTableElement.tr}
Expand Down Expand Up @@ -476,7 +475,13 @@ export class DuoyunTableElement<T = any, K = any> extends DuoyunScrollBoxElement
</table>
${this.#sidePart}
${this.selectable
? html`<dy-selection-box class="selection" @change=${this.#onSelectionBoxChange}></dy-selection-box>`
? html`
<dy-selection-box
class="selection"
.container=${this.selectionContainer}
@change=${this.#onSelectionBoxChange}
></dy-selection-box>
`
: ''}
${!this.data
? html`<div class="side" part=${DuoyunTableElement.side}><dy-loading></dy-loading></div>`
Expand Down
3 changes: 2 additions & 1 deletion packages/duoyun-ui/src/elements/tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { createCSSSheet, css, partMap, classMap, styleMap } from '@mantou/gem/li
import { theme } from '../lib/theme';
import { commonHandle } from '../lib/hotkeys';
import { focusStyle } from '../lib/styles';
import { closestElement } from '../lib/element';

import { DuoyunScrollBaseElement } from './base/scroll';

Expand Down Expand Up @@ -230,7 +231,7 @@ export class DuoyunTabPanelElement extends DuoyunScrollBaseElement {
this.internals.role = 'tabpanel';
this.addEventListener('change', (e) => e.stopPropagation());
this.effect(() => {
this.vertical = this.closestElement(DuoyunTabsElement)?.orientation === 'vertical';
this.vertical = closestElement(this, DuoyunTabsElement)?.orientation === 'vertical';
});
}
}
36 changes: 29 additions & 7 deletions packages/duoyun-ui/src/lib/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ export function getBoundingClientRect(eleList: Element[]) {
}

export function toggleActiveState(ele: Element | undefined | null, active: boolean) {
if (!ele) return;
if (ele instanceof GemElement) {
if ((ele.constructor as typeof GemElement).definedCSSStates?.includes('active')) {
(ele as any).active = active;
}
// button/combobox
ele.internals.ariaExpanded = String(active);
if (['button', 'combobox'].includes(ele.role || ele.internals.role || '')) {
ele.internals.ariaExpanded = String(active);
}
}
const closestEle = closestElement(ele, '[data-state-active]');
if (closestEle instanceof HTMLElement) {
closestEle.dataset.stateActive = active ? '?1' : '';
}
}

Expand Down Expand Up @@ -81,12 +87,28 @@ export function isInputElement(originElement: HTMLElement) {
}
}

export function closestElement(ele: Element, selector: string) {
export function closestElement<K extends keyof HTMLElementTagNameMap>(
ele: Element,
tag: K,
): HTMLElementTagNameMap[K] | null;
export function closestElement<K extends abstract new (...args: any) => any>(
ele: Element,
constructor: K,
): InstanceType<K> | null;
export function closestElement<K extends Element>(ele: Element, tag: string): K | null;
export function closestElement<K extends abstract new (...args: any) => any>(ele: Element, selector: K | string) {
let node: Element | null = ele;
while (node) {
const e = node.closest(selector);
if (e) return e;
node = (node.getRootNode() as ShadowRoot).host;
if (typeof selector === 'function') {
while (node) {
if (node instanceof selector) return node;
node = node.parentElement || (node.getRootNode() as ShadowRoot).host;
}
} else {
while (node) {
const e = node.closest(selector);
if (e) return e;
node = (node.getRootNode() as ShadowRoot).host;
}
}
return null;
}
Expand Down
14 changes: 7 additions & 7 deletions packages/duoyun-ui/src/patterns/console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,14 @@ const rules = css`
flex-grow: 1;
min-width: 0;
}
dy-pat-console dy-light-route {
display: contents;
}
dy-pat-console .main {
margin: auto;
padding: calc(2 * ${theme.gridGutter});
max-width: 80em;
}
dy-pat-console dy-light-route {
display: contents;
}
dy-pat-console[responsive] {
@media ${mediaQuery.PHONE_LANDSCAPE} {
.sidebar {
Expand Down Expand Up @@ -250,17 +250,17 @@ export class DyPatConsoleElement extends GemElement {
`
: ''}
</div>
<div class="main-container">
<main class="main" aria-label="Content">
<main class="main-container">
<div class="main" aria-label="Content">
<dy-light-route
@loading=${this.#onLoading}
@routechange=${this.#onChange}
.routes=${this.routes}
.locationStore=${locationStore}
.scrollContainer=${document.body}
></dy-light-route>
</main>
</div>
</div>
</main>
${this.keyboardAccess ? html`<dy-keyboard-access></dy-keyboard-access>` : ''}
${this.screencastMode ? html`<dy-input-capture></dy-input-capture>` : ''}
`;
Expand Down
Loading

0 comments on commit 417d5ef

Please sign in to comment.