Skip to content

Commit

Permalink
[duoyun-ui] Closed #122, #133
Browse files Browse the repository at this point in the history
  • Loading branch information
mantou132 committed Oct 2, 2024
1 parent cd0e9a4 commit 4c101ac
Show file tree
Hide file tree
Showing 10 changed files with 254 additions and 51 deletions.
2 changes: 1 addition & 1 deletion packages/duoyun-ui/src/elements/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ export class DuoyunFormItemElement extends GemElement {
render = () => {
const { invalidMessage } = this.#state;
return html`
${this.#type === 'checkbox'
${this.#type === 'checkbox' || !this.label
? ''
: html`
<label class="label" part=${DuoyunFormItemElement.label} @click=${() => this.focus()}>
Expand Down
16 changes: 12 additions & 4 deletions packages/duoyun-ui/src/elements/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,14 @@ export class DuoyunModalElement extends GemElement {
this.dangerDefaultOkBtn = !!dangerDefaultOkBtn;
}

get #header() {
return this.header || this.headerSlot;
}

get #body() {
return this.body || this.bodySlot;
}

#maskRef = createRef<HTMLElement>();
#dialogRef = createRef<HTMLElement>();
#bodyRef = createRef<HTMLElement>();
Expand Down Expand Up @@ -319,7 +327,7 @@ export class DuoyunModalElement extends GemElement {
aria-modal="true"
class="dialog absolute"
>
${this.body || this.bodySlot || html`<slot></slot>`}
${this.#body || html`<slot></slot>`}
</div>
`
: html`
Expand All @@ -331,16 +339,16 @@ export class DuoyunModalElement extends GemElement {
aria-modal="true"
class="dialog main absolute"
>
${this.header
${this.#header
? html`
<div part=${DuoyunModalElement.header} role="heading" aria-level="1" class="header">
<slot name=${DuoyunModalElement.header}>${this.header || this.headerSlot}</slot>
<slot name=${DuoyunModalElement.header}>${this.#header}</slot>
</div>
<dy-divider part=${DuoyunModalElement.divider} class="header-divider" size="medium"></dy-divider>
`
: ''}
<dy-scroll-box class="body" part=${DuoyunModalElement.body}>
<slot ref=${this.#bodyRef.ref}>${this.body || this.bodySlot}</slot>
<slot ref=${this.#bodyRef.ref}>${this.#body}</slot>
</dy-scroll-box>
<div class="footer" part=${DuoyunModalElement.footer}>
<slot name=${DuoyunModalElement.footer}>
Expand Down
96 changes: 96 additions & 0 deletions packages/duoyun-ui/src/elements/sort-box.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { GemElement, createCSSSheet } from '@mantou/gem/lib/element';
import { adoptedStyle, boolattribute, customElement, emitter, mounted, type Emitter } from '@mantou/gem/lib/decorators';
import { addListener, css } from '@mantou/gem/lib/utils';

import { blockContainer } from '../lib/styles';
import { theme } from '../lib/theme';

import { DuoyunGestureElement } from './gesture';
import type { PanEventDetail } from './gesture';

const style = createCSSSheet(css`
:where(dy-sort-item[handle], dy-sort-handle):state(grabbing) {
cursor: grabbing;
* {
pointer-event: none;
}
}
dy-sort-item[handle]:state(grabbing),
dy-sort-item:has(:state(grabbing)) {
position: relative;
z-index: ${theme.popupZIndex};
}
`);

export type SortEventDetail = { new: number; old: number };

/**
* @customElement dy-sort-box
*/
@customElement('dy-sort-box')
@adoptedStyle(style)
@adoptedStyle(blockContainer)
export class DuoyunSortBoxElement extends GemElement {
@emitter sort: Emitter<SortEventDetail>;

#listeners: (undefined | (() => void)[])[] = [];

#removeListeners = () => {
this.#listeners.forEach((e) => e?.forEach((ee) => ee()));
};

#listen = () => {
this.#removeListeners();
const items = [...this.querySelectorAll<DuoyunSortItemElement>('dy-sort-item')];
const handles = items.map((item) =>
item.handle ? item : item.querySelector<DuoyunSortHandleElement>('dy-sort-handle'),
);
this.#listeners = handles.map((e, index) => {
if (!e) return;
const item = items[index];
let itemTranslate = [0, 0];
const removeEnd = addListener(e, 'end', ({ detail }: CustomEvent<PointerEvent>) => {
itemTranslate = [0, 0];
item.style.translate = 'none';
const itemsRect = items.map((i) => i.getBoundingClientRect());
const newIndex = itemsRect.findIndex(
(i) => i.left <= detail.x && i.right > detail.x && i.top <= detail.y && i.bottom > detail.y,
);
if (newIndex === -1) return;
this.sort({ new: newIndex, old: index });
});
const removePan = addListener(e, 'pan', ({ detail }: CustomEvent<PanEventDetail>) => {
itemTranslate[0] += detail.x;
itemTranslate[1] += detail.y;
item.style.translate = itemTranslate.map((p) => p + 'px').join(' ');
});
return [removeEnd, removePan];
});
};

@mounted()
#init = () => {
this.#listen();
const ob = new MutationObserver(this.#listen);
ob.observe(this, { subtree: true, childList: true });
return () => {
ob.disconnect();
this.#removeListeners();
};
};
}

/**
* @customElement dy-sort-item
*/
@customElement('dy-sort-item')
export class DuoyunSortItemElement extends DuoyunGestureElement {
@boolattribute handle: boolean;
}

/**
* @customElement dy-sort-handle
*/
@customElement('dy-sort-handle')
export class DuoyunSortHandleElement extends DuoyunGestureElement {}
36 changes: 16 additions & 20 deletions packages/duoyun-ui/src/lib/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,32 @@ export const focusStyle = createCSSSheet(css`
}
`);

export const blockContainer = createCSSSheet(css`
:host(:where(:not([hidden]))),
:where(:scope:not([hidden])) {
display: block;
}
`);
function createContainer(display: string) {
return createCSSSheet(css`
@layer {
:host(:where(:not([hidden]))),
:where(:scope:not([hidden])) {
display: ${display};
}
}
`);
}

export const flexContainer = createCSSSheet(css`
:host(:where(:not([hidden]))),
:where(:scope:not([hidden])) {
display: flex;
}
`);
export const blockContainer = createContainer('block');

export const contentsContainer = createCSSSheet(css`
:host(:where(:not([hidden]))),
:where(:scope:not([hidden])) {
display: contents;
}
`);
export const flexContainer = createContainer('flex');

export const contentsContainer = createContainer('contents');

/** render empty content */
export const noneTemplate = html`
<style>
:host {
display: none;
display: none !important;
}
@scope {
:scope {
display: none;
display: none !important;
}
}
</style>
Expand Down
1 change: 1 addition & 0 deletions packages/duoyun-ui/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default {
copySuccess: 'Copy success',
copyFail: 'Copy fail',
add: 'Add',
remove: 'Remove',
search: 'Search',
filter: 'Filter',
loading: 'Loading',
Expand Down
1 change: 1 addition & 0 deletions packages/duoyun-ui/src/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const lang: typeof en = {
copySuccess: '复制成功',
copyFail: '复制失败',
add: '新增',
remove: '移除',
search: '搜索',
filter: '过滤',
loading: '加载中',
Expand Down
114 changes: 99 additions & 15 deletions packages/duoyun-ui/src/patterns/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,19 @@ import { DuoyunDatePickerElement } from '../elements/date-picker';
import { DuoyunDateRangePickerElement } from '../elements/date-range-picker';
import { DuoyunPickerElement } from '../elements/picker';
import { locale } from '../lib/locale';
import type { SortEventDetail } from '../elements/sort-box';

import '../elements/form';

// ts 5.4
declare global {
interface MapConstructor {
/**
* Groups members of an iterable according to the return value of the passed callback.
* @param items An iterable.
* @param keySelector A callback which will be invoked for each item in items.
*/
groupBy<K, T>(items: Iterable<T>, keySelector: (item: T, index: number) => K): Map<K, T[]>;
}
}
import '../elements/button';
import '../elements/space';
import '../elements/sort-box';

type ListOptions = {
add?: boolean | string | TemplateResult;
remove?: boolean | string | TemplateResult;
initItem?: any;
sortable?: boolean;
};

type FormItemProps<T = unknown> = {
label: string;
Expand Down Expand Up @@ -69,6 +68,9 @@ type FormItemProps<T = unknown> = {

/**update field setting for any field change */
update?: (data: T) => Partial<FormItemProps<T>>;

// array field
list?: boolean | ListOptions;
};

export type FormItem<T = unknown> =
Expand All @@ -86,6 +88,14 @@ const style = createCSSSheet(css`
dy-form {
width: 100%;
}
dy-sort-item {
display: flex;
align-items: flex-start;
gap: 1em;
dy-form-item:first-of-type {
flex-grow: 1;
}
}
.template {
margin-block-end: 1em;
font-size: 0.875em;
Expand Down Expand Up @@ -160,23 +170,29 @@ export class DyPatFormElement<T = Record<string, unknown>> extends GemElement {
});
};

#onChange = ({ detail }: CustomEvent<any>) => {
#changeData = (detail: any) => {
const data = Object.keys(detail).reduce((prev, key) => {
const val = detail[key];
const path = key.split(',');
Reflect.set(readProp(prev, path.slice(0, -1), { fill: true }), path.at(-1)!, val);
const prop = path.at(-1)!;
const root = readProp(prev, path.slice(0, -1), { fill: true });
Reflect.set(root, prop, val);
return prev;
}, {} as any);
this.state({ data });

this.#forEachFormItems((props) => {
if (!props.update && props.type !== 'number') return;
if (!props.list && !props.update && props.type !== 'number') return;

const path = Array.isArray(props.field) ? props.field : [props.field];
const wrapObj = readProp(data, path.slice(0, -1) as string[]);
const lastKey = path.at(-1)!;
const val = wrapObj[lastKey];

if (props.list && val && !Array.isArray(val)) {
Reflect.set(wrapObj, lastKey, Array.from({ ...val, length: Object.keys(val).length }));
}

if (props.type === 'number') {
Reflect.set(wrapObj, lastKey, Number(val) || 0);
}
Expand All @@ -193,6 +209,8 @@ export class DyPatFormElement<T = Record<string, unknown>> extends GemElement {
});
};

#onChange = ({ detail }: CustomEvent<any>) => this.#changeData(detail);

#onOptionsChange = async (props: FormItemProps<T>, input: string) => {
if (!props.getOptions) return;
const { optionsRecord, data } = this.state;
Expand Down Expand Up @@ -401,6 +419,69 @@ export class DyPatFormElement<T = Record<string, unknown>> extends GemElement {
`;
};

#renderItemList = (item: FormItemProps<T>) => {
const { add = true, remove = true, initItem, sortable } = typeof item.list === 'boolean' ? {} : item.list || {};
const addTemplate = typeof add === 'boolean' ? locale.add : add;
const removeTemplate = typeof remove === 'boolean' ? '' : remove;
const path = [item.field].flat() as string[];
const prop = path.at(-1)!;
const value = readProp(this.state.data!, path) as any[] | undefined;
const onSort = ({ detail }: CustomEvent<SortEventDetail>) => {
[value![detail.new], value![detail.old]] = [value![detail.old], value![detail.new]];
this.state();
};
const addEle = () => {
const root = readProp(this.state.data!, path.slice(0, -1), { fill: true });
if (!root[prop]) root[prop] = [];
root[prop].push(initItem);
this.state();
};
const removeEle = (index: number) => {
value!.splice(index, 1);
this.state();
};
return html`
<dy-form-item style="margin-bottom: 0" label=${item.label}></dy-form-item>
<dy-sort-box @sort=${onSort}>
${value?.map(
(e, index) => html`
<dy-sort-item>
${sortable && !item.disabled
? html`
<dy-sort-handle>
<dy-button .icon=${icons.menu} square color="cancel"></dy-button>
</dy-sort-handle>
`
: ''}
${this.#renderItem({ ...item, field: [...path, index] as string[], label: '' })}
<dy-form-item ?hidden=${item.disabled}>
<dy-space>
${removeTemplate
? html`${removeTemplate}`
: html`
<dy-button
@click=${() => removeEle(index)}
.icon=${icons.delete}
square
round
color="cancel"
title=${locale.remove}
></dy-button>
`}
</dy-space>
</dy-form-item>
</dy-sort-item>
`,
)}
</dy-sort-box>
<dy-form-item>
<dy-button type="reverse" .disabled=${item.disabled} .icon=${icons.add} @click=${addEle}>
${addTemplate}
</dy-button>
</dy-form-item>
`;
};

#renderInlineGroup = (items: FormItemProps<T>[]) => {
return html`<dy-form-item-inline-group>${this.#renderItems(items)}</dy-form-item-inline-group>`;
};
Expand All @@ -423,6 +504,9 @@ export class DyPatFormElement<T = Record<string, unknown>> extends GemElement {
</details>
`;
}
if (item.list) {
return this.#renderItemList(item);
}
const inputs = inputGroup.get(item.label);
if (inputs) {
switch (inputs.length) {
Expand Down
Loading

0 comments on commit 4c101ac

Please sign in to comment.