diff --git a/README.md b/README.md index e20b971..765d4f6 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,9 @@ JSWindowで囲むだけで、そこが仮想ウインドウ化します ## 3.links +- WebSite +[https://ttis.croud.jp/?uuid=b292d429-dbad-49b5-8fed-6d268f4feaf0](https://ttis.croud.jp/?uuid=b292d429-dbad-49b5-8fed-6d268f4feaf0) + - Source code [https://github.com/JavaScript-WindowFramework/jswf-react](https://github.com/JavaScript-WindowFramework/jswf-react) @@ -113,13 +116,12 @@ ReactDOM.render(, document.getElementById("root") as HTMLElement); - 重ね合わせ - 親子ウインドウ - 画面分割 +- リストビュー ## 6.コンポーネント ### 6.1 **JSWindow** -
- #### Propsパラメータ | Name | Type | Info | @@ -152,12 +154,8 @@ WindowState.MAX WindowState.MIN WindowState.HIDE -
- ### 6.2 **SplitView** -
- #### Propsパラメータ | Name | Type | Info | @@ -169,8 +167,6 @@ WindowState.HIDE | bold | number | Bar thickness | | style | React.CSSProperties | CSS | -
- ## 7.ライセンス MIT diff --git a/images/file.png b/images/file.png new file mode 100644 index 0000000..1325bf3 Binary files /dev/null and b/images/file.png differ diff --git a/images/talone.svg b/images/talone.svg new file mode 100644 index 0000000..cc99ea6 --- /dev/null +++ b/images/talone.svg @@ -0,0 +1,15 @@ + + + + +talone + + + + + diff --git a/images/tclose.svg b/images/tclose.svg new file mode 100644 index 0000000..7ca11e8 --- /dev/null +++ b/images/tclose.svg @@ -0,0 +1,16 @@ + + + + +tclose + + + + + + diff --git a/images/topen.svg b/images/topen.svg new file mode 100644 index 0000000..db7eeff --- /dev/null +++ b/images/topen.svg @@ -0,0 +1,15 @@ + + + + +topen + + + + + diff --git a/package.json b/package.json index f8dbaeb..33bc717 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@jswf/react", - "version": "0.1.1", + "version": "0.2.1", "description": "Virtual Window for React", "main": "dist/index.js", "scripts": { diff --git a/src/JSWindow/index.tsx b/src/JSWindow/index.tsx index f90f540..f8f16ab 100644 --- a/src/JSWindow/index.tsx +++ b/src/JSWindow/index.tsx @@ -29,7 +29,7 @@ export interface WindowProps { windowStyle?: number; windowState?: WindowState; onUpdate?: ((status: WindowInfo) => void) | null; - clientStyle?:React.CSSProperties; + clientStyle?: React.CSSProperties; } type NonNullableType = { [P in K]-?: T[P]; @@ -93,7 +93,7 @@ export class JSWindow extends Component { overlapped: true, windowStyle: 0xff, windowState: WindowState.NORMAL, - clientStyle:{}, + clientStyle: {}, onUpdate: null }; @@ -119,10 +119,9 @@ export class JSWindow extends Component { state = { active: props.active!, overlapped: props.overlapped!, - titlePrmisson:props.windowStyle!, - titleSize:(props.windowStyle! & WindowStyle.TITLE) === 0 - ? 0 - : props.titleSize!, + titlePrmisson: props.windowStyle!, + titleSize: + (props.windowStyle! & WindowStyle.TITLE) === 0 ? 0 : props.titleSize!, borderSize: props.borderSize!, x: props.x!, y: props.y!, @@ -154,7 +153,7 @@ export class JSWindow extends Component { onUpdate: props.onUpdate!, clientWidth: 0, clientHeight: 0, - clientStyle:props.clientStyle!, + clientStyle: props.clientStyle!, realX: 0, realY: 0, realWidth: 0, @@ -411,6 +410,7 @@ export class JSWindow extends Component { /> ))} { ) { if (Manager.moveNode == null) { this.foreground(); - Manager.moveNode = this.rootRef.current; - let p = Manager.getPos((e as unknown) as MouseEvent | TouchEvent); - Manager.baseX = p.x; - Manager.baseY = p.y; - Manager.nodeX = this.windowInfo.realX; - Manager.nodeY = this.windowInfo.realY; - Manager.nodeWidth = this.windowInfo.realWidth; - Manager.nodeHeight = this.windowInfo.realHeight; - e.stopPropagation(); + if (this.props.moveable || Manager.frame) { + Manager.moveNode = this.rootRef.current; + let p = Manager.getPos((e as unknown) as MouseEvent | TouchEvent); + Manager.baseX = p.x; + Manager.baseY = p.y; + Manager.nodeX = this.windowInfo.realX; + Manager.nodeY = this.windowInfo.realY; + Manager.nodeWidth = this.windowInfo.realWidth; + Manager.nodeHeight = this.windowInfo.realHeight; + e.stopPropagation(); + } } else { - e.preventDefault(); + // e.preventDefault(); } } //フレームクリックイベントの処理 @@ -626,8 +628,8 @@ export class JSWindow extends Component { .call(parent.childNodes, 0) .filter(node => { return ( - (node as typeof node & { _symbol?: JSWindow }) - ._symbol instanceof JSWindow + (node as typeof node & { _symbol?: JSWindow })._symbol instanceof + JSWindow ); }) .sort((a, b) => { diff --git a/src/ListView/index.tsx b/src/ListView/index.tsx index 7450751..7bc3086 100644 --- a/src/ListView/index.tsx +++ b/src/ListView/index.tsx @@ -1,3 +1,4 @@ +import ResizeObserver from "resize-observer-polyfill"; import React, { Component, ReactNode, ReactElement, createRef } from "react"; import { Root } from "./parts/Root"; import { Headers } from "./parts/Header/Headers"; @@ -7,6 +8,12 @@ interface Props { children?: ReactNode; onItemClick?: (row: number, col: number) => void; onItemDoubleClick?: (row: number, col: number) => void; + onItemDragStart?: (e: React.DragEvent, row: number, col: number) => void; + onItemDragEnter?: (e: React.DragEvent, row: number, col: number) => void; + onItemDragLeave?: (e: React.DragEvent, row: number, col: number) => void; + onItemDragOver?: (e: React.DragEvent, row: number, col: number) => void; + onItemDrop?: (e: React.DragEvent, row: number, col: number) => void; + onDrop?: (e: React.DragEvent) => void; } interface State { xScroll: number; @@ -15,7 +22,15 @@ interface State { sortOrder?: boolean; sortType?: string; selectItems: Set; + clientWidth?: number; } +/** + *WindowsライクなListView + * + * @export + * @class ListView + * @extends {Component} + */ export class ListView extends Component { static defaultProps = { children: [] }; state: State = { @@ -24,11 +39,12 @@ export class ListView extends Component { sortIndex: -1, selectItems: new Set() }; - rootRef = createRef(); - itemsRef = createRef(); - headersRef = createRef(); + private resizeObserver?: ResizeObserver; + private rootRef = createRef(); + private itemsRef = createRef(); + private headersRef = createRef(); - render() { + public render(): JSX.Element { const children = React.Children.toArray( this.props.children ) as React.ReactElement[]; @@ -41,11 +57,13 @@ export class ListView extends Component { return ( { this.setState({ xScroll: this.rootRef.current!.scrollLeft }); }} > this.setState({ headerSizes })} @@ -54,7 +72,9 @@ export class ListView extends Component { { headerSizes={this.state.headerSizes} onClick={this.onItemClick.bind(this)} onDoubleClick={this.onItemDoubleClick.bind(this)} + onItemDragStart={this.props.onItemDragStart} + onItemDragEnter={this.props.onItemDragEnter} + onItemDragLeave={this.props.onItemDragLeave} + onItemDragOver={this.props.onItemDragOver} + onItemDrop={this.props.onItemDrop} > {items} ); } - onHeaderClick(sortIndex: number) { + public componentDidMount(): void { + this.resizeObserver = new ResizeObserver(() => { + this.layout(); + }); + this.resizeObserver.observe(this.rootRef.current! as Element); + this.layout(); + } + public componentWillUnmount(): void { + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + this.resizeObserver = undefined; + } + } + /** + *レイアウト処理 + * + * @protected + * @memberof SplitView + */ + protected layout(): void { + this.setState({ clientWidth: this.rootRef.current!.clientWidth }); + if (this.rootRef.current) { + this.rootRef.current.scrollLeft = 0; + } + } + protected onHeaderClick(sortIndex: number): void { let sortOrder; if (this.state.sortIndex === sortIndex) { sortOrder = !this.state.sortOrder; @@ -81,7 +131,7 @@ export class ListView extends Component { .current!.getType(); this.setState({ sortOrder, sortIndex, sortType }); } - onItemClick(e: React.MouseEvent, row: number, col: number) { + protected onItemClick(e: React.MouseEvent, row: number, col: number): void { const selectItems = this.state.selectItems; if (e.ctrlKey) { if (!selectItems.has(row)) selectItems.add(row); @@ -104,34 +154,95 @@ export class ListView extends Component { this.props.onItemClick(row, col); } } - onItemDoubleClick(e: React.MouseEvent, row: number, col: number) { + protected onItemDoubleClick( + e: React.MouseEvent, + row: number, + col: number + ): void { if (this.props.onItemDoubleClick) { this.props.onItemDoubleClick(row, col); } } - getSelectItem() { + /** + *選択中の最初のアイテムを返す + * + * @returns 0<=:アイテム番号 -1:選択無し + * @memberof ListView + */ + public getSelectItem(): number { const selectItems = this.state.selectItems; - if (selectItems.size) return selectItems.values().next(); + if (selectItems.size) return selectItems.values().next().value; return -1; } - getSelectItems() { + /** + *選択中のアイテムを配列で返す + * + * @returns 選択中のアイテム番号の配列 + * @memberof ListView + */ + public getSelectItems(): number[] { return Array.from(this.state.selectItems.values()); } - getItem(row: number, col: number): React.ReactNode | undefined { + /** + *アイテムの内容を返す + * + * @param {number} row + * @param {number} col + * @returns {(React.ReactNode | undefined)} 内容 + * @memberof ListView + */ + public getItem(row: number, col: number): React.ReactNode | undefined { const itemValues = this.itemsRef.current!.getItemValues(); if (row >= itemValues.length) return undefined; return itemValues[row][col]; } - getRows() { + /** + *アイテムの内容を変更する + * + * @param {number} row + * @param {number} col + * @param {ReactNode} value + * @memberof ListView + */ + public setItem(row: number, col: number, value: ReactNode): void { + const itemValues = this.itemsRef.current!.getItemValues(); + if (row < itemValues.length) itemValues[row][col] = value; + this.forceUpdate(); + } + /** + *アイテム数を返す + * + * @returns + * @memberof ListView + */ + public getRows(): number { return this.itemsRef.current!.getItemValues().length; } - getCols() { + /** + *アイテムのカラム数を返す + * + * @returns + * @memberof ListView + */ + public getCols(): number { return this.state.headerSizes.length; } - addItem(item: ReactNode[]) { + /** + *アイテムの追加 + * + * @param {ReactNode[]} item 追加するアイテム + * @memberof ListView + */ + public addItem(item: ReactNode[]): void { this.itemsRef.current!.addItem(item); } - removeItem(row: number) { + /** + *アイテムの削除 + * + * @param {number} row 削除するレコード番号 + * @memberof ListView + */ + public removeItem(row: number): void { this.itemsRef.current!.removeItem(row); this.state.selectItems.clear(); } diff --git a/src/ListView/parts/Header/Header.tsx b/src/ListView/parts/Header/Header.tsx index 0b1d6b3..d945f42 100644 --- a/src/ListView/parts/Header/Header.tsx +++ b/src/ListView/parts/Header/Header.tsx @@ -6,24 +6,32 @@ interface HeaderProps { onSize: () => void; onClick: () => void; } -interface HeaderStatus { +interface HeaderState { width: number; + tempWidth: number; } -export class Header extends Component { +/** + *ListViewヘッダークラス + * + * @export + * @class Header + * @extends {Component} + */ +export class Header extends Component { static defaultProps = { minWidth: 60 }; - state = { width: -1 }; - type: string = "string"; - labelRef = createRef(); - sliderRef = createRef(); - render() { + state: HeaderState = { width: -1,tempWidth:0 }; + private type: string = "string"; + private labelRef = createRef(); + private sliderRef = createRef(); + public render() { const child = this.props.children as ReactElement; const label = child.props.children; return (
@@ -63,15 +71,15 @@ export class Header extends Component { this.props.onSize(); }); } - public componentDitUnmount() { + public componentWillUnmount() { const node = this.sliderRef.current!; node.removeEventListener("move", this.onMove.bind(this)); } public getWidth() { - return this.state.width; + return Math.max(this.state.width,this.state.tempWidth); } - public setWidth(width:number) { - return this.setState({width}); + public getTempWidth() { + return this.state.tempWidth; } public getType() { return this.type; diff --git a/src/ListView/parts/Header/Headers.tsx b/src/ListView/parts/Header/Headers.tsx index 791fe26..0e78179 100644 --- a/src/ListView/parts/Header/Headers.tsx +++ b/src/ListView/parts/Header/Headers.tsx @@ -6,67 +6,80 @@ interface HeadersProps { onSize: (headers: number[]) => void; onClick: (index: number) => void; children: ReactNode; + clientWidth?: number; } +/** + *ListViewヘッダー管理クラス + * + * @export + * @class Headers + * @extends {Component} + */ export class Headers extends Component { - headers: RefObject
[] = []; - values:ReactNode[] = []; - componentDidMount() { + private headers: RefObject
[] = []; + private values: ReactNode[] = []; + private rootRef = createRef(); + public componentDidMount() { this.values = React.Children.toArray(this.props.children); this.onSize(); } - componentDidUpdate(){ - const headerWidth = this.rootRef.current!.offsetWidth; - const width = this.headers.reduce((a,b)=>{ - return a-b.current!.getWidth(); - },this.rootRef.current!.offsetWidth); - console.log(headerWidth); - const index = this.headers.length -1; - if(index >= 0 && width){ - const header = this.headers[index].current!; - header.setState({width:header.getWidth()+width},()=>{ this.onSize();}); + public componentDidUpdate() { + const clientWidth = this.props.clientWidth; + if (this.props.clientWidth !== undefined) { + const width = this.headers.reduce((a, b) => { + const w = b.current!.getWidth(); + if (w < 0) a = -9999999; + return a - b.current!.getWidth(); + }, clientWidth!); + const index = this.headers.length - 1; + if (index >= 0) { + const header = this.headers[index].current!; + let tempWidth = header.getTempWidth() + width; + if (tempWidth < 0) tempWidth = 0; + if (header.state.tempWidth !== tempWidth) { + header.setState({ tempWidth }); + } + } } } - rootRef = createRef(); - render() { + public render() { this.headers = []; return ( - {this.values.map( - (element: ReactNode, index) => { - const refHeader = createRef
(); - this.headers.push(refHeader); - return ( -
{ - this.onClick(index); - }} - onSize={() => this.onSize()} - > - {element} -
- ); - } - )} + {this.values.map((element: ReactNode, index) => { + const refHeader = createRef
(); + this.headers.push(refHeader); + return ( +
{ + this.onClick(index); + }} + onSize={() => this.onSize()} + > + {element} +
+ ); + })} ); } - onSize() { + protected onSize() { const headerSizes = this.headers.map(headerRef => { if (headerRef.current) return headerRef.current.getWidth(); return -1; }); if (headerSizes.indexOf(-1) === -1) this.props.onSize(headerSizes); } - onClick(index: number) { + protected onClick(index: number) { this.props.onClick(index); } - getHeader(index: number) { + public getHeader(index: number) { return this.headers[index]; } - getTypes() { + public getTypes() { return this.headers.map(header => { return header.current!.getType(); }); diff --git a/src/ListView/parts/Header/Root.ts b/src/ListView/parts/Header/Root.ts index 1a407e6..2af583b 100644 --- a/src/ListView/parts/Header/Root.ts +++ b/src/ListView/parts/Header/Root.ts @@ -13,28 +13,28 @@ export const Root = styled.div.attrs(p => ({ min-width: 100%; background-image: linear-gradient( 180deg, - rgba(144, 197, 240, 0.9) 0%, - rgba(63, 164, 201, 0.9) 50%, - rgba(100, 122, 221, 0.9) 100% + rgb(144, 197, 240) 0%, + rgb(63, 164, 201) 50%, + rgb(100, 122, 221) 100% ); > div { #back { - display:flex; + display: flex; justify-content: center; align-items: center; height: 100%; background-image: linear-gradient( 180deg, - rgba(144, 197, 240, 0.9) 0%, - rgba(63, 164, 201, 0.9) 50%, - rgba(100, 122, 221, 0.9) 100% + rgb(144, 197, 240) 0%, + rgb(63, 164, 201) 50%, + rgb(100, 122, 221) 100% ); &:hover { background-image: linear-gradient( 0deg, - rgba(144, 197, 240, 0.9) 0%, - rgba(63, 164, 201, 0.9) 50%, - rgba(100, 122, 221, 0.9) 100% + rgb(144, 197, 240) 0%, + rgb(63, 164, 201) 50%, + rgb(100, 122, 221) 100% ); } } @@ -53,10 +53,16 @@ export const Root = styled.div.attrs(p => ({ cursor: ew-resize; position: absolute; top: 0px; - right: -12px; + right: -16px; width: 32px; height: 100%; // background-color: rgba(255, 255, 255, 0.4); } + &:last-child { + > #slider { + right: 0; + width: 16px; + } + } } `; diff --git a/src/ListView/parts/Item/Items.tsx b/src/ListView/parts/Item/Items.tsx index 26cf320..7e60552 100644 --- a/src/ListView/parts/Item/Items.tsx +++ b/src/ListView/parts/Item/Items.tsx @@ -8,6 +8,7 @@ import React, { } from "react"; import { Root } from "./Root"; import { Item } from "./Item"; +import imgFile from "../../../../images/file.png"; interface ItemColumnProps { width: number; @@ -27,9 +28,14 @@ interface ItemsProps { sortOrder?: boolean; sortIndex?: number; sortType?: string; - selectItems: Set; + selectItems: Set; onClick: (e: React.MouseEvent, row: number, col: number) => void; onDoubleClick: (e: React.MouseEvent, row: number, col: number) => void; + onItemDragStart?: (e: React.DragEvent, row: number, col: number) => void; + onItemDragEnter?: (e: React.DragEvent, row: number, col: number) => void; + onItemDragLeave?: (e: React.DragEvent, row: number, col: number) => void; + onItemDragOver?: (e: React.DragEvent, row: number, col: number) => void; + onItemDrop?: (e: React.DragEvent, row: number, col: number) => void; } interface State { values: ReactNode[][]; @@ -37,15 +43,34 @@ interface State { export class Items extends Component { state: State = { values: [] }; - items: RefObject[][] = []; - sortOrder?: boolean; - sortIndex?: number; - values: ReactNode[][] = []; + private rootRef: RefObject = createRef(); + private columnsRef: RefObject[] = []; + private itemsRef: RefObject[][] = []; + private sortOrder?: boolean; + private sortIndex?: number; + private values: ReactNode[][] = []; + private fileImage?: HTMLImageElement; - componentDidUpdate() { - this.items.forEach((row, index) => { + public componentDidUpdate() { + //カラムのスクロールバー幅修正 + if (this.columnsRef.length) { + const rootWidth = this.rootRef.current!.clientWidth + this.props.xScroll; + const columnWidth = this.columnsRef.reduce((a, b) => { + return a - b.current!.offsetWidth; + }, rootWidth); + if (columnWidth) { + const node = this.columnsRef[this.columnsRef.length - 1].current!; + node.style.width = node.offsetWidth + columnWidth + "px"; + } + } + + //アイテム選択と高さ設定 + this.itemsRef.forEach((row, index) => { let height = row.reduce((a, b) => { - return Math.max(a, b.current!.offsetHeight); + return Math.max( + a, + (b.current!.childNodes[0] as HTMLDivElement).offsetHeight + ); }, 0); const select = this.props.selectItems.has(index); row.forEach(item => { @@ -55,7 +80,11 @@ export class Items extends Component { }); }); } - componentDidMount() { + public componentDidMount() { + this.fileImage = document.createElement("img"); + this.fileImage.src = imgFile; + this.fileImage.style.height = "64px"; + const values: ReactNode[][] = []; React.Children.forEach(this.props.children, children => { const value: ReactNode[] = []; @@ -73,7 +102,7 @@ export class Items extends Component { this.sort(); } - render() { + public render() { if ( this.sortOrder !== this.props.sortOrder || this.sortIndex !== this.props.sortIndex @@ -85,25 +114,28 @@ export class Items extends Component { }); } - this.items = []; + this.itemsRef = []; + this.columnsRef = []; return ( - + e.preventDefault()}>
{this.props.headerSizes.map((size, cols) => { + this.columnsRef[cols] = createRef(); return ( - + {this.state.values.map((items, rows) => { const ref = createRef(); - if (this.items[rows]) this.items[rows][cols] = ref; - else this.items[rows] = [ref]; + if (this.itemsRef[rows]) this.itemsRef[rows][cols] = ref; + else this.itemsRef[rows] = [ref]; let type = "flex-start"; - if(this.props.headerTypes[cols] === "number") - type="flex-end"; + if (this.props.headerTypes[cols] === "number") + type = "flex-end"; return ( { this.onOver(rows, true); }} @@ -116,8 +148,23 @@ export class Items extends Component { onDoubleClick={e => { this.props.onDoubleClick(e, rows, cols); }} + onDragStart={e => { + this.onDragStart(e, rows, cols); + }} + onDragLeave={e => { + this.onDragLeave(e, rows, cols); + }} + onDragEnter={e => { + this.onDragEnter(e, rows, cols); + }} + onDragOver={e => { + this.onDragOver(e, rows, cols); + }} + onDrop={e => { + this.onDrop(e, rows, cols); + }} > - {items[cols]} +
{items[cols]}
); })} @@ -128,14 +175,14 @@ export class Items extends Component { ); } - onOver(index: number, flag: boolean) { - const rows = this.items[index]; + protected onOver(index: number, flag: boolean) { + const rows = this.itemsRef[index]; for (const item of rows) { if (flag) item.current!.classList.add("Hover"); else item.current!.classList.remove("Hover"); } } - sort() { + protected sort() { const sortIndex = this.props.sortIndex; if (sortIndex === undefined || sortIndex < 0) return; const sortOrder = this.props.sortOrder; @@ -160,14 +207,45 @@ export class Items extends Component { } this.setState({ values: this.values }); } - getItemValues() { + + protected onDragStart(e: React.DragEvent, row: number, col: number) { + if (!this.props.selectItems.has(row)) { + e.preventDefault(); + return; + } + if (this.props.onItemDragStart) this.props.onItemDragStart(e, row, col); + e.dataTransfer.effectAllowed = "move"; + e.dataTransfer.setDragImage(this.fileImage!, 10, 10); + const rows = Array.from(this.props.selectItems.values()); + const items = rows.map((item)=>{ + return this.values[item]; + }) + e.dataTransfer.setData("text/plain",JSON.stringify(items)); + } + protected onDragLeave(e: React.DragEvent, row: number, col: number) { + if (this.props.onItemDragLeave) this.props.onItemDragLeave(e, row, col); + } + protected onDragEnter(e: React.DragEvent, row: number, col: number) { + if (this.props.onItemDragEnter) this.props.onItemDragEnter(e, row, col); + e.preventDefault(); + } + protected onDragOver(e: React.DragEvent, row: number, col: number) { + if (this.props.onItemDragOver) this.props.onItemDragOver(e, row, col); + e.preventDefault(); + } + protected onDrop(e: React.DragEvent, row: number, col: number) { + if (this.props.onItemDrop) this.props.onItemDrop(e, row, col); + e.preventDefault(); + } + + public getItemValues() { return this.state.values; } - addItem(item: ReactNode[]) { + public addItem(item: ReactNode[]) { this.values.push(item); this.setState({ values: this.values }); } - removeItem(row: number) { + public removeItem(row: number) { this.values.splice(row, 1); this.setState({ values: this.values }); } diff --git a/src/ListView/parts/Root.tsx b/src/ListView/parts/Root.tsx index 2054249..fb0052e 100644 --- a/src/ListView/parts/Root.tsx +++ b/src/ListView/parts/Root.tsx @@ -1,16 +1,14 @@ import styled from "styled-components"; -interface StyleProps{ - -} +interface StyleProps {} export const Root = styled.div.attrs(p => ({ style: {} }))` -position:absolute; -overflow-x:auto; -overflow-y:hidden; -width:100%; -height:100%; -display:flex; -flex-direction:column; -`; \ No newline at end of file + position: absolute; + overflow-x: auto; + overflow-y: hidden; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +`; diff --git a/src/TreeView/Item/TreeItem.style.ts b/src/TreeView/Item/TreeItem.style.ts new file mode 100644 index 0000000..3461aa6 --- /dev/null +++ b/src/TreeView/Item/TreeItem.style.ts @@ -0,0 +1,90 @@ +import styled from "styled-components"; + +interface StyleProps {} + +const lineSize = 1.6; + +export const Root = styled.div.attrs(p => ({ + style: {} +}))` + position: relative; + + #icon { + cursor: pointer; + box-sizing: border-box; + margin: ${lineSize*0.2}em; + width: ${lineSize*0.8}em; + } + #item { + border-radius: 4px; + cursor: default; + flex: 1; + display: flex; + flex-wrap: nowrap; + align-items: center; + &:hover { + background-color: rgba(200, 230, 250, 0.4); + } + &.select { + background-color: rgba(100, 150, 250, 0.4); + &:hover { + background-color: rgba(100, 150, 250, 0.5); + } + } + } + #label { + flex: 1; + flex-wrap: nowrap; + word-break: break-all; + margin: 0.1em; + } + #child { + position: relative; + > div { + display: flex; + } + } + #children{ + flex:1; + } + #line { + width:${lineSize/2}em; + margin-right:${lineSize/2}em; + bottom: 0; + flex-grow: 0; + flex-shrink: 0; + border-right:solid 1px; + } + + .close { + > div { + animation: treeClose 0.5s ease 0s forwards; + } + } + .open { + > div { + animation: treeOpen 0.1s ease 0s normal; + } + } + + @keyframes treeOpen { + 0% { + margin-top: -100%; + } + + 100% { + margin-top: 0%; + } + } + + @keyframes treeClose { + 0% { + height: auto; + margin-top: 0; + } + + 100% { + margin-top: -100%; + } + } +`; diff --git a/src/TreeView/Item/TreeItem.tsx b/src/TreeView/Item/TreeItem.tsx new file mode 100644 index 0000000..e425e3e --- /dev/null +++ b/src/TreeView/Item/TreeItem.tsx @@ -0,0 +1,226 @@ +import React, { + Component, + ReactNode, + ReactElement, + createRef, + RefObject +} from "react"; +import imgAlone from "../../../images/talone.svg"; +import imgClose from "../../../images/tclose.svg"; +import imgOpen from "../../../images/topen.svg"; +import { Root } from "./TreeItem.style"; +import { TreeView } from ".."; +interface Props { + children?: ReactNode; + label?: ReactNode; + expand?: boolean; + value?: unknown; + treeView?: TreeView; + parent?: TreeItem; + onItemClick?: (item: TreeItem) => void; + onDoubleClick?: (item: TreeItem) => void; +} +interface State { + expand: boolean; + label: ReactNode; + value: unknown; + select: boolean; + checked: boolean; + items: TreeItem[]; +} + +export class TreeItem extends Component { + static defaultProps = { label: "", expand: true }; + + childRef: RefObject = createRef(); + itemsRef: RefObject[] = []; + keepProps: Props; + constructor(props: Props) { + super(props); + this.keepProps = props; + //子アイテムの抽出 + const items = React.Children.toArray(this.props.children).filter(item => { + return (item as ReactElement).type === TreeItem; + }) as TreeItem[]; + + this.state = { + items: items, + select: false, + checked: false, + expand: this.props.expand!, + label: this.props.label, + value: this.props.value + }; + } + componentDidUpdate() { + if (this.keepProps.children !== this.props.children) { + const items = React.Children.toArray(this.props.children).filter(item => { + return (item as ReactElement).type === TreeItem; + }) as TreeItem[]; + this.setState({items}); + this.keepProps = this.props; + } + } + render() { + this.itemsRef = this.state.items.map(() => { + return createRef(); + }); + for (const item of this.state.items) console.log(item.props.label); + return ( + +
{ + this.props.onItemClick && this.props.onItemClick(this); + if (this.props.treeView) { + this.props.treeView.selectItem(this); + this.props.treeView.props.onItemClick && + this.props.treeView.props.onItemClick(this); + } + }} + onDoubleClick={() => { + this.props.onDoubleClick && this.props.onDoubleClick(this); + this.props.treeView && + this.props.treeView.props.onItemDoubleClick && + this.props.treeView.props.onItemDoubleClick(this); + }} + > + { + const expand = !this.state.expand; + this.setState({ expand }); + e.stopPropagation(); + //e.preventDefault(); + if (expand) this.childRef.current!.style.display = "block"; + }} + id="icon" + src={ + React.Children.count(this.props.children) === 0 + ? imgAlone + : this.state.expand + ? imgOpen + : imgClose + } + /> + { + e.stopPropagation(); + }} + value="" + onChange={() => this.setChecked(!this.state.checked)} + /> +
{this.state.label}
+
+
{ + this.childRef.current!.style.overflow = "hidden"; + }} + onAnimationEnd={() => { + this.childRef.current!.style.overflow = "visible"; + if (!this.state.expand) + this.childRef.current!.style.display = "none"; + }} + > +
+
+
+ {this.state.items.map((item, index) => ( + + ))} +
+
+
+
+ ); + } + getLabel() { + return this.state.label; + } + setLabel(label: ReactNode) { + this.setState({ label: label }); + } + findItem(value: unknown): TreeItem | null { + if (this.state.value === value) return this; + for (const item of this.state.items) { + const target = item.findItem(value); + if (target) return target; + } + return null; + } + findItems(value: unknown): TreeItem[] { + const items: TreeItem[] = []; + if (this.state.value === value) items.push(this); + for (const item of this.state.items) { + items.push(...item.findItems(value)); + } + return items; + } + addItem(props: Props) { + this.state.items.push(new TreeItem(props)); + this.forceUpdate(); + } + delItem(item: TreeItem) { + const index = this.itemsRef.findIndex(itemRef => { + return itemRef.current === item; + }); + if (index >= 0) { + if ( + this.props.treeView && + this.props.treeView.getSelectItem() === this.itemsRef[index].current + ) + this.props.treeView.selectItem(null); + this.itemsRef.splice(index, 1); + const items = this.state.items.slice(); + items.splice(index, 1); + this.setState({ items }); + this.forceUpdate(); + return true; + } else { + for (const itemRef of this.itemsRef) { + if (itemRef.current!.delItem(item)) return true; + } + } + return false; + } + remove() { + if (this.props.parent) this.props.parent.delItem(this); + this.forceUpdate(); + } + clear() { + this.setState({ items: [] }); + this.forceUpdate(); + } + getChildren() { + return this.state.items; + } + onSelect(select: boolean) { + this.setState({ select }); + } + setChecked(checked: boolean) { + this.setState({ checked }); + for (const item of this.itemsRef) { + item.current!.setChecked(checked); + } + } + getCheckItems() { + const checks: TreeItem[] = []; + if (this.state.checked) checks.push(this); + for (const item of this.itemsRef) { + checks.push(...item.current!.getCheckItems()); + } + return checks; + } +} diff --git a/src/TreeView/Root.ts b/src/TreeView/Root.ts new file mode 100644 index 0000000..b1ce779 --- /dev/null +++ b/src/TreeView/Root.ts @@ -0,0 +1,14 @@ +import styled from "styled-components"; + +interface StyleProps {} + +export const Root = styled.div.attrs(p => ({ + style: {} +}))` + user-select:none; + position: absolute; + width: 100%; + height: 100%; + overflow:auto; + +`; diff --git a/src/TreeView/index.tsx b/src/TreeView/index.tsx new file mode 100644 index 0000000..68c98ca --- /dev/null +++ b/src/TreeView/index.tsx @@ -0,0 +1,63 @@ +import React, { Component, createRef, ReactElement } from "react"; +import { TreeItem } from "./Item/TreeItem"; +import { Root } from "./Root"; + +interface Props { + onItemClick?: (item: TreeItem) => void; + onItemDoubleClick?: (item: TreeItem) => void; +} +interface State {} + +export class TreeView extends Component { + rootItemRef = createRef(); + select: TreeItem | null = null; + render() { + const rootItem = this.props.children as ReactElement; + if (rootItem && rootItem.type === TreeItem) { + //TreeItemを再定義 + return ( + + + + ); + } else { + //データが存在しなかった場合は、デフォルトでrootアイテムを用意 + return ( + + + + ); + } + } + getItem(): TreeItem { + return this.rootItemRef.current!; + } + findItem(value: unknown) { + return this.rootItemRef.current!.findItem(value); + } + findItems(value: unknown) { + return this.rootItemRef.current!.findItems(value); + } + delItem(item: TreeItem) { + return this.rootItemRef.current!.delItem(item); + } + getSelectItem(): TreeItem | null { + return this.select; + } + selectItem(item: TreeItem | null) { + if (this.select) this.select.onSelect(false); + if (item) item.onSelect(true); + this.select = item; + } + getCheckItems(){ + return this.rootItemRef.current!.getCheckItems(); + } +} diff --git a/src/index.ts b/src/index.ts index 7f1c12c..f6d03f1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,5 @@ export * from "./JSWindow"; export * from "./SplitView"; -export * from "./ListView"; \ No newline at end of file +export * from "./ListView"; +export * from "./TreeView"; +export * from "./TreeView/Item/TreeItem"; \ No newline at end of file diff --git a/src/splitView/index.tsx b/src/splitView/index.tsx index fb89d46..f47b602 100644 --- a/src/splitView/index.tsx +++ b/src/splitView/index.tsx @@ -43,18 +43,17 @@ export class SplitView extends Component { private resizeObserver?: ResizeObserver; private rootRef = createRef(); private childRef = [createRef(), createRef()]; - private children: (ReactNode | undefined)[] = [undefined, undefined]; public constructor(props: SplitProps) { super(props); - this.state = { pos: props.pos!, activeMode: false, barOpen: true }; + this.state = { + pos: props.pos!, + activeMode: false, + barOpen: true, + }; this.type = props.type!; - if (props.children) { - if (props.children instanceof Array) { - this.children = props.children; - } - } } public render() { + const children = React.Children.toArray(this.props.children); return ( { this.closeBar(); }} > - {this.children[1]} + {children[1]} (this.activeStop = true)}> - {this.children[0]} + {children[0]} { } } - onOpen(open: boolean) { + protected onOpen(open: boolean) { const children = [this.childRef[0].current!, this.childRef[1].current!]; if (open) { children[0].style.animation = @@ -251,7 +250,7 @@ export class SplitView extends Component { } this.setState({ barOpen: open }); } - onMove(pos: number) { + protected onMove(pos: number) { this.setState({ pos }); this.closeBar(); } diff --git a/src/svg.d.ts b/src/svg.d.ts index e1e62b7..d9f3b7c 100644 --- a/src/svg.d.ts +++ b/src/svg.d.ts @@ -1,4 +1,8 @@ declare module '*.svg' { const value: string; export = value; +} +declare module '*.png' { + const value: string; + export = value; } \ No newline at end of file