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 a47b28f
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 47 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
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
112 changes: 97 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,67 @@ 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 .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 +502,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
2 changes: 1 addition & 1 deletion packages/gem-examples/src/console/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const EXAMPLE = {
updated: 0,
};

export type Item = typeof EXAMPLE;
export type Item = typeof EXAMPLE & { social?: string[] };

export async function fetchItemsWithArgs(args: FetchEventDetail): Promise<PaginationRes<Item>> {
console.log('args:', args);
Expand Down
Loading

0 comments on commit a47b28f

Please sign in to comment.