diff --git a/package.json b/package.json index 8c35698c..a2854daa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "boot-cell", - "version": "2.0.0-beta.7", + "version": "2.0.0-beta.9", "license": "LGPL-3.0", "author": "shiy2008@gmail.com", "description": "Web Components UI library based on WebCell v3, BootStrap v5, BootStrap Icon v1 & FontAwesome v6", @@ -29,7 +29,7 @@ "dom-renderer": "^2.0.6", "mobx": "^6.12.0", "regenerator-runtime": "^0.14.1", - "web-cell": "^3.0.0-rc.7", + "web-cell": "^3.0.0-rc.8", "web-utility": "^4.1.3" }, "peerDependencies": { @@ -70,7 +70,7 @@ "ts-jest": "^29.1.1", "ts-node": "^10.9.2", "typedoc": "^0.25.7", - "typedoc-plugin-mdn-links": "^3.1.12", + "typedoc-plugin-mdn-links": "^3.1.13", "typescript": "~5.3.3" }, "scripts": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4bfbdc8b..0432a45a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,8 +21,8 @@ dependencies: specifier: ^0.14.1 version: 0.14.1 web-cell: - specifier: ^3.0.0-rc.7 - version: 3.0.0-rc.7(element-internals-polyfill@1.3.10)(typescript@5.3.3) + specifier: ^3.0.0-rc.8 + version: 3.0.0-rc.8(element-internals-polyfill@1.3.10)(typescript@5.3.3) web-utility: specifier: ^4.1.3 version: 4.1.3(typescript@5.3.3) @@ -113,8 +113,8 @@ devDependencies: specifier: ^0.25.7 version: 0.25.7(typescript@5.3.3) typedoc-plugin-mdn-links: - specifier: ^3.1.12 - version: 3.1.12(typedoc@0.25.7) + specifier: ^3.1.13 + version: 3.1.13(typedoc@0.25.7) typescript: specifier: ~5.3.3 version: 5.3.3 @@ -4865,8 +4865,8 @@ packages: engines: {node: '>=14.16'} dev: true - /typedoc-plugin-mdn-links@3.1.12(typedoc@0.25.7): - resolution: {integrity: sha512-B6GLXAq2kL7crem0uJYAN7uMmbBZdf+znUanwk/u6gQQFKveUSzCZrtO9pb0ZIe2uCv1T60XDfcO+bTm7R18aw==} + /typedoc-plugin-mdn-links@3.1.13(typedoc@0.25.7): + resolution: {integrity: sha512-GDS6wz63kj9JcR4ewGgLzhAlhg3GVxh5d6IRuhmRcLO+LziYyDySa6QFWqrVkWkNoVsfw0IABIhkAj6GdrlwwA==} peerDependencies: typedoc: '>= 0.23.14 || 0.24.x || 0.25.x' dependencies: @@ -4968,8 +4968,8 @@ packages: - typescript dev: true - /web-cell@3.0.0-rc.7(element-internals-polyfill@1.3.10)(typescript@5.3.3): - resolution: {integrity: sha512-c4+nelL24NYDEtQMsr5r1ftSO6Jg9KCQJAxHYttBSdiZwrAybhVV/qLEsrI+ax3OLmHKxy7WjBWJVYopcaG54A==} + /web-cell@3.0.0-rc.8(element-internals-polyfill@1.3.10)(typescript@5.3.3): + resolution: {integrity: sha512-oxgBKP9nv7LFKRQmUBoltxVTlcBPatCvSlsUEaV31GA4tG5OT1e718HDZRrk3bSUfFL8KA6WR/i7wSxZohE20g==} peerDependencies: '@webcomponents/webcomponentsjs': ^2.8 core-js: ^3 diff --git a/source/Accordion.tsx b/source/Accordion.tsx new file mode 100644 index 00000000..03caf665 --- /dev/null +++ b/source/Accordion.tsx @@ -0,0 +1,102 @@ +import { observable } from 'mobx'; +import { + FC, + WebCell, + WebCellProps, + attribute, + component, + observer, + on, + reaction +} from 'web-cell'; + +import { CollapseProps, Collapse } from './Collapse'; + +export const AccordionItem: FC> = ({ + className = '', + children, + ...props +}) => ( +
+ {children} +
+); + +export const AccordionHeader: FC> = ({ + className = '', + children, + onClick, + ...props +}) => ( +

+ +

+); + +export const AccordionBody: FC = ({ + className = '', + children, + ...props +}) => ( + +
{children}
+
+); + +export interface AccordionProps { + flush?: boolean; + alwaysOpen?: boolean; +} + +export interface Accordion extends WebCell {} + +@component({ + tagName: 'accordion-box', + mode: 'open' +}) +@observer +export class Accordion extends HTMLElement implements WebCell { + @attribute + @observable + accessor flush = false; + + @attribute + @observable + accessor alwaysOpen = false; + + connectedCallback() { + this.classList.add('accordion'); + } + + @reaction(({ flush }) => flush) + handleFlush(flush: boolean) { + this.classList.toggle('accordion-flush', flush); + } + + @on('click', '.accordion-header') + handleClick( + _, + { nextElementSibling: currentCollapse }: HTMLHeadingElement + ) { + if (!this.alwaysOpen) + for (const collapse of this.querySelectorAll( + '.accordion-collapse' + )) + if (collapse !== currentCollapse) { + collapse.classList.remove('show'); + collapse.previousElementSibling.querySelector( + 'button' + ).ariaExpanded = 'false'; + } + currentCollapse.classList.toggle('show'); + currentCollapse.previousElementSibling.querySelector( + 'button' + ).ariaExpanded = 'false'; + } + + render() { + return ; + } +} diff --git a/source/Button.tsx b/source/Button.tsx index 6bfaa363..1e3d3317 100644 --- a/source/Button.tsx +++ b/source/Button.tsx @@ -18,12 +18,18 @@ export const Button: FC = ({ className, href, variant, + size, active, children, ...props }) => { const { disabled, tabIndex } = props, - Class = classNames('btn', variant && `btn-${variant}`, className); + Class = classNames( + 'btn', + variant && `btn-${variant}`, + size && `btn-${size}`, + className + ); return href ? ( { + dimension?: 'width' | 'height'; + in?: boolean; +} + +export const Collapse: FC = ({ + className, + dimension = 'width', + in: show, + children, + ...props +}) => ( +
+ {children} +
+); diff --git a/source/Form.tsx b/source/Form.tsx index 2bbcdb0a..0bee4ecb 100644 --- a/source/Form.tsx +++ b/source/Form.tsx @@ -38,6 +38,38 @@ export const FloatingLabel: FC = ({ ); +export interface InputGroupProps extends WebCellProps { + size?: 'sm' | 'lg'; +} + +export const InputGroup: FC = ({ + className = '', + size, + children, + ...props +}) => ( +
+ {children} +
+); + +export const InputGroupText: FC> = ({ + className = '', + children, + ...props +}) => ( + + {children} + +); + export type FormControlTag = 'input' | 'textarea' | 'select'; export type FormControlProps = WebCellProps & diff --git a/source/Icon.tsx b/source/Icon.tsx index 2b4ba9ea..67216b6e 100644 --- a/source/Icon.tsx +++ b/source/Icon.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import { WebCellProps } from 'web-cell'; +import { FC, WebCellProps } from 'web-cell'; import { Color } from './type'; @@ -9,7 +9,7 @@ export interface IconProps extends WebCellProps { size?: number; } -export function Icon({ +export const Icon: FC = ({ className, style, color, @@ -17,19 +17,42 @@ export function Icon({ size, children, ...rest -}: IconProps) { - return ( - - ); +}) => ( + +); + +export interface BGIconProps extends IconProps { + type?: 'square' | 'circle'; } + +export const BGIcon: FC = ({ + className = '', + type = 'square', + color = 'primary', + children, + ...props +}) => ( + + + +); diff --git a/source/MonthCalendar.tsx b/source/MonthCalendar.tsx new file mode 100644 index 00000000..8f54379c --- /dev/null +++ b/source/MonthCalendar.tsx @@ -0,0 +1,162 @@ +import classNames from 'classnames'; +import { JsxChildren } from 'dom-renderer'; +import { computed, observable } from 'mobx'; +import { WebCell, attribute, component, observer } from 'web-cell'; +import { + Day, + TimeData, + changeMonth, + formatDate, + splitArray +} from 'web-utility'; + +import { Badge } from './Badge'; +import { Button, ButtonProps } from './Button'; +import { Table, TableProps } from './Table'; + +export interface DateData { + date: TimeData; + content: JsxChildren; + link?: string; +} + +export interface MonthCalendarProps + extends Omit, + Pick { + locale?: Navigator['language']; + value?: DateData[]; + onSelect?: (event: CustomEvent) => any; + onChange?: (event: CustomEvent) => any; +} + +export interface MonthCalendar extends WebCell {} + +@component({ tagName: 'month-calendar' }) +@observer +export class MonthCalendar + extends HTMLElement + implements WebCell +{ + @attribute + @observable + accessor variant: MonthCalendarProps['variant'] = 'primary'; + + @attribute + @observable + accessor locale: Navigator['language']; + + @observable + accessor value: DateData[] = []; + + @computed + get weekFormatter() { + const { locale = globalThis.navigator?.language } = this; + + return new Intl.DateTimeFormat(locale, { weekday: 'long' }); + } + + @observable + accessor currentDate = new Date(); + + @computed + get dateGrid() { + let startDate = new Date(this.currentDate); + startDate.setDate(1); + startDate = new Date(+startDate - startDate.getDay() * Day); + + const dateList = Array.from( + new Array(42), + (_, index) => new Date(+startDate + index * Day) + ); + return splitArray(dateList, 7); + } + + changeMonth(delta: number) { + this.currentDate = changeMonth(this.currentDate, delta); + + this.emit('change', this.currentDate); + } + + renderDate = (date: Date) => { + const { value } = this, + dateText = formatDate(date, 'YYYY-MM-DD'); + const list = value?.filter( + ({ date }) => formatDate(date, 'YYYY-MM-DD') === dateText + ); + + return ( + + + + {list?.map(item => + typeof item.content === 'object' ? ( + item.content + ) : ( + this.emit('select', item)} + > + {item.content} + + ) + )} + + ); + }; + + render() { + const { style, variant, weekFormatter, currentDate, dateGrid } = this; + + return ( + + + + + {dateGrid[0].map((date, index, { length }) => ( + + ))} + + + + {dateGrid.map(days => ( + {days.map(this.renderDate)} + ))} + +
+
+ + + {formatDate(currentDate, 'YYYY-MM')} + + +
+
+ {weekFormatter.format(date)} +
+ ); + } +} diff --git a/source/Navbar.tsx b/source/Navbar.tsx index 4287d3c6..7bd91b1d 100644 --- a/source/Navbar.tsx +++ b/source/Navbar.tsx @@ -1,4 +1,4 @@ -import { JsxProps, VNode } from 'dom-renderer'; +import { JsxChildren, JsxProps } from 'dom-renderer'; import { observable } from 'mobx'; import { FC, @@ -77,7 +77,7 @@ export interface OffcanvasNavbarProps extends OffcanvasBoxProps, NavbarProps, ContainerProps { - brand?: VNode; + brand?: JsxChildren; } export interface OffcanvasNavbar extends WebCell {} @@ -121,7 +121,7 @@ export class OffcanvasNavbar extends HTMLElement implements WebCell { titleId = uniqueID(); @observable - accessor brand: VNode; + accessor brand: OffcanvasNavbarProps['brand']; offcanvasId = uniqueID(); diff --git a/source/index.ts b/source/index.ts index 94c5ae92..bbbf3fc9 100644 --- a/source/index.ts +++ b/source/index.ts @@ -13,6 +13,9 @@ export * from './Icon'; export * from './Image'; export * from './Tooltip'; export * from './Dropdown'; +export * from './Collapse'; +export * from './Accordion'; export * from './Nav'; export * from './Navbar'; export * from './Offcanvas'; +export * from './MonthCalendar'; diff --git a/v1/Calendar/MonthCalendar.tsx b/v1/Calendar/MonthCalendar.tsx deleted file mode 100644 index 66479198..00000000 --- a/v1/Calendar/MonthCalendar.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { - WebCellProps, - VNodeChildElement, - component, - mixin, - attribute, - watch, - createCell, - Fragment -} from 'web-cell'; -import { - TimeData, - Day, - formatDate, - changeMonth -} from 'web-utility/source/date'; -import classNames from 'classnames'; - -import { IconButton } from '../Form/Button'; -import { CalendarTableProps, WeekDays, CalendarTable } from './CalendarTable'; - -export interface MonthCalendarProps extends WebCellProps { - date?: TimeData; - dateTemplate?: string; - weekDays?: CalendarTableProps['weekDays']; - renderCell?: (date: Date) => VNodeChildElement; -} - -interface MonthCalendarState { - dayGrid: number[][]; -} - -@component({ - tagName: 'month-calendar', - renderTarget: 'children' -}) -export class MonthCalendar extends mixin< - MonthCalendarProps, - MonthCalendarState ->() { - state = { - dayGrid: [] - }; - - @attribute - @watch - set date(date: TimeData) { - if (!(date instanceof Date)) date = new Date(date); - - this.setProps({ date }); - this.setState({ dayGrid: MonthCalendar.createDayGrid(date) }); - } - - @attribute - @watch - dateTemplate = 'YYYY-MM'; - - @watch - weekDays = WeekDays; - - @watch - renderCell: MonthCalendarProps['renderCell']; - - connectedCallback() { - if (!this.date) this.date = new Date(); - } - - static createDayGrid(date: Date) { - var start = new Date(date.getFullYear(), date.getMonth(), 1); - - var offset = start.getDay() - 1; - - if (offset < 0) offset = 7 + offset; - - if (offset) start = new Date(+start - Day * offset); - - return Array(42) - .fill(0) - .reduce((list: number[][], _, index) => { - if (!(index % 7)) list.push([]); - - const row = Math.floor(index / 7); - - list[row].push(new Date(+start + Day * index).getDate()); - - return list; - }, []); - } - - renderRow(row: number[], index: number) { - const { date, renderCell } = this.props; - - return row.map(day => { - const prev = !index && day > 14, - next = index > 3 && day < 14; - - const outer = prev || next, - today = new Date( - (date as Date).getFullYear(), - (date as Date).getMonth() + (prev ? -1 : next ? 1 : 0), - day - ); - const sameDay = - formatDate(date, 'YYYY-MM-DD') === - formatDate(today, 'YYYY-MM-DD'); - - return ( - - {!renderCell ? day : renderCell(today)} - - ); - }); - } - - render( - { date, dateTemplate, weekDays }: MonthCalendarProps, - { dayGrid }: MonthCalendarState - ) { - return ( - <> -
- (this.date = changeMonth(date, -1))} - /> - - (this.date = changeMonth(date, 1))} - /> -
- - {dayGrid.map((row, index) => ( - {this.renderRow(row, index)} - ))} - - - ); - } -} diff --git a/v1/Content/Accordion.tsx b/v1/Content/Accordion.tsx deleted file mode 100644 index 2c3cfdd9..00000000 --- a/v1/Content/Accordion.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { WebCellProps, createCell, delegate } from 'web-cell'; -import { uniqueID } from 'web-utility/source/data'; -import classNames from 'classnames'; - -import { Button } from '../Form/Button'; -import { CollapseBox } from './Collapse'; - -export interface AccordionPanelProps extends WebCellProps { - active?: boolean; -} - -export function AccordionPanel({ - id = uniqueID(), - active, - title, - defaultSlot -}: AccordionPanelProps) { - const hID = `accordion_h_${id}`, - bID = `accordion_b_${id}`; - - return ( -
-
-

- -

-
- -
{defaultSlot}
-
-
- ); -} - -export interface AccordionProps extends WebCellProps {} - -const AllHeaders = '.card-header .btn'; - -function switchAccordion( - { currentTarget }: FocusEvent, - target: HTMLButtonElement -) { - for (const header of (currentTarget as HTMLDivElement).querySelectorAll( - AllHeaders - )) { - const active = header === target; - - if (active) header.classList.remove('collapsed'); - else header.classList.add('collapsed'); - - header.setAttribute('aria-expanded', active + ''); - (header.closest('.card-header') - .nextElementSibling as CollapseBox).open = active; - } -} - -export function Accordion({ className, defaultSlot, ...rest }: AccordionProps) { - return ( -
- {defaultSlot} -
- ); -}