diff --git a/src/scss/page/common.scss b/src/scss/page/common.scss index 5b821e3a40..e722a81dad 100644 --- a/src/scss/page/common.scss +++ b/src/scss/page/common.scss @@ -17,4 +17,4 @@ @import "./main/tag"; @import "./main/date"; @import "./main/settings"; -@import "./main/relation"; \ No newline at end of file +@import "./main/settings"; \ No newline at end of file diff --git a/src/ts/component/block/dataview/cell/select.tsx b/src/ts/component/block/dataview/cell/select.tsx new file mode 100644 index 0000000000..eb769bab01 --- /dev/null +++ b/src/ts/component/block/dataview/cell/select.tsx @@ -0,0 +1,408 @@ +import * as React from 'react'; +import $ from 'jquery'; +import arrayMove from 'array-move'; +import { observer } from 'mobx-react'; +import { getRange, setRange } from 'selection-ranges'; +import { Tag, Icon, DragBox } from 'Component'; +import { I, S, U, Relation, translate, keyboard } from 'Lib'; + +interface State { + isEditing: boolean; +}; + +const MAX_LENGTH = 320; + +const CellSelect = observer(class CellSelect extends React.Component { + + _isMounted = false; + node = null; + state = { + isEditing: false, + }; + + constructor (props: I.Cell) { + super(props); + + this.onClear = this.onClear.bind(this); + this.onKeyPress = this.onKeyPress.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + this.onKeyUp = this.onKeyUp.bind(this); + this.onInput = this.onInput.bind(this); + this.onFocus = this.onFocus.bind(this); + this.onBlur = this.onBlur.bind(this); + this.onDragEnd = this.onDragEnd.bind(this); + this.focus = this.focus.bind(this); + }; + + render () { + const { relation, recordId, getRecord, elementMapper, arrayLimit, canEdit, placeholder } = this.props; + const { isEditing } = this.state; + const record = getRecord(recordId); + const isSelect = relation.format == I.RelationType.Select; + const cn = [ 'wrap' ]; + + if (!relation || !record) { + return null; + }; + + let value = this.getItems(); + let content = null; + + const length = value.length; + + if (elementMapper) { + value = value.map(it => elementMapper(relation, it)); + }; + + if (arrayLimit) { + value = value.slice(0, arrayLimit); + if (length > arrayLimit) { + cn.push('overLimit'); + }; + }; + + if (isEditing) { + const cni = [ 'itemWrap' ]; + + if (!isSelect) { + cni.push('isDraggable'); + }; + + content = ( +
+
{placeholder}
+ + + + {value.map((item: any, i: number) => ( + this.onContextMenu(e, item)} + {...U.Common.dataProps({ id: item.id, index: i })} + > + this.onClick(e, item.id)} + onRemove={() => this.onValueRemove(item.id)} + /> + + ))} + + + + {canEdit ? ( + keyboard.setComposition(true)} + onCompositionEnd={() => keyboard.setComposition(false)} + onClick={e => e.stopPropagation()} + > + {'\n'} + + ) : ''} + + {isSelect ? : ''} +
+ ); + } else { + if (!value.length) { + content =
{placeholder}
; + } else { + content = ( + + {value.map((item: any, i: number) => ( + this.onClick(e, item.id)} + onContextMenu={e => this.onContextMenu(e, item)} + /> + ))} + {arrayLimit && (length > arrayLimit) ?
+{length - arrayLimit}
: ''} +
+ ); + }; + }; + + return ( +
this.node = node} + className={cn.join(' ')} + > + {content} +
+ ); + }; + + componentDidMount () { + this._isMounted = true; + }; + + componentDidUpdate () { + const { isEditing } = this.state; + const { id } = this.props; + const cell = $(`#${id}`); + + if (isEditing) { + cell.addClass('isEditing'); + + this.placeholderCheck(); + this.focus(); + this.resize(); + } else { + cell.removeClass('isEditing'); + }; + }; + + componentWillUnmount () { + this._isMounted = false; + }; + + setEditing (v: boolean) { + const { canEdit } = this.props; + const { isEditing } = this.state; + + if (canEdit && (v != isEditing)) { + this.setState({ isEditing: v }); + + if (v) { + window.setTimeout(() => this.focus(), 15); + }; + }; + }; + + onKeyPress (e: any) { + if (!this._isMounted || keyboard.isComposition) { + return; + }; + + const node = $(this.node); + const entry = node.find('#entry'); + + if (entry.length && (entry.text().length >= MAX_LENGTH)) { + e.preventDefault(); + }; + }; + + onKeyDown (e: any) { + if (!this._isMounted || keyboard.isComposition) { + return; + }; + + const node = $(this.node); + const entry = node.find('#entry'); + + keyboard.shortcut('backspace', e, () => { + e.stopPropagation(); + + const range = getRange(entry.get(0)); + if (range.start || range.end) { + return; + }; + + e.preventDefault(); + + const value = this.getValue(); + value.existing.pop(); + this.setValue(value.existing); + }); + + this.placeholderCheck(); + this.resize(); + this.scrollToBottom(); + }; + + onKeyUp (e: any) { + if (!this._isMounted || keyboard.isComposition) { + return; + }; + + S.Menu.updateData('dataviewOptionList', { filter: this.getValue().new }); + + this.placeholderCheck(); + this.resize(); + this.scrollToBottom(); + }; + + onInput () { + this.placeholderCheck(); + }; + + onClick (e: any, id: string) { + }; + + placeholderCheck () { + if (!this._isMounted) { + return; + }; + + const node = $(this.node); + const value = this.getValue(); + const list = node.find('#list'); + const placeholder = node.find('#placeholder'); + + value.existing.length ? list.show() : list.hide(); + value.new || value.existing.length ? placeholder.hide() : placeholder.show(); + }; + + clear () { + if (!this._isMounted) { + return; + }; + + const node = $(this.node); + node.find('#entry').text(' '); + + this.focus(); + }; + + onValueRemove (id: string) { + this.setValue(this.getItemIds().filter(it => it != id)); + }; + + onDragEnd (oldIndex: number, newIndex: number) { + this.setValue(arrayMove(this.getItemIds(), oldIndex, newIndex)); + }; + + onFocus () { + keyboard.setFocus(true); + }; + + onBlur () { + keyboard.setFocus(false); + }; + + focus () { + if (!this._isMounted) { + return; + }; + + const node = $(this.node); + const entry = node.find('#entry'); + + if (entry.length) { + window.setTimeout(() => { + entry.focus(); + setRange(entry.get(0), { start: 0, end: 0 }); + + this.scrollToBottom(); + }); + }; + }; + + scrollToBottom () { + const { id } = this.props; + const cell = $(`#${id}`); + const content = cell.hasClass('.cellContent') ? cell : cell.find('.cellContent'); + + if (content.length) { + content.scrollTop(content.get(0).scrollHeight + parseInt(content.css('paddingBottom'))); + }; + }; + + onClear (e: any) { + e.preventDefault(); + e.stopPropagation(); + + this.setValue([]); + }; + + onContextMenu (e: React.MouseEvent, item: any) { + const { id, canEdit, menuClassName, menuClassNameWrap } = this.props; + + if (!canEdit) { + return; + }; + + e.preventDefault(); + e.stopPropagation(); + + S.Menu.open('dataviewOptionEdit', { + element: `#${id} #item-${item.id}`, + className: menuClassName, + classNameWrap: menuClassNameWrap, + offsetY: 4, + data: { + option: item, + } + }); + }; + + getItems (): any[] { + const { relation, recordId, getRecord } = this.props; + const record = getRecord(recordId); + + return relation && record ? Relation.getOptions(record[relation.relationKey]).filter(it => !it.isArchived && !it.isDeleted) : []; + }; + + getItemIds (): string[] { + return this.getItems().map(it => it.id); + }; + + getValue () { + if (!this._isMounted) { + return; + }; + + const node = $(this.node); + const list = node.find('#list'); + const items = list.find('.itemWrap'); + const entry = node.find('#entry'); + const existing = []; + + items.each((i: number, item: any) => { + item = $(item); + existing.push(item.data('id')); + }); + + return { + existing, + new: (entry.length ? String(entry.text() || '').trim() : ''), + }; + }; + + setValue (value: string[]) { + const { onChange, relation } = this.props; + const { maxCount } = relation; + + value = U.Common.arrayUnique(value); + + const length = value.length; + if (maxCount && (length > maxCount)) { + value = value.slice(length - maxCount, length); + }; + + if (onChange) { + onChange(value, () => { + this.clear(); + + S.Menu.updateData('dataviewOptionList', { value }); + }); + }; + }; + + resize () { + $(window).trigger('resize.menuDataviewOptionList'); + }; + +}); + +export default CellSelect; diff --git a/src/ts/component/list/object.tsx b/src/ts/component/list/object.tsx index 11484c45f3..8b23ce6235 100644 --- a/src/ts/component/list/object.tsx +++ b/src/ts/component/list/object.tsx @@ -20,7 +20,7 @@ interface Props { sources?: string[]; filters?: I.Filter[]; relationKeys?: string[]; - route: string; + route?: string; }; interface ListObjectRefProps { diff --git a/src/ts/component/menu/dataview/filter/values.tsx b/src/ts/component/menu/dataview/filter/values.tsx index f83cf14b12..48deb898e4 100644 --- a/src/ts/component/menu/dataview/filter/values.tsx +++ b/src/ts/component/menu/dataview/filter/values.tsx @@ -109,7 +109,7 @@ const MenuDataviewFilterValues = observer(class MenuDataviewFilterValues extends
diff --git a/src/ts/component/menu/dataview/option/list.tsx b/src/ts/component/menu/dataview/option/list.tsx index c46f2b2925..c25d3787cc 100644 --- a/src/ts/component/menu/dataview/option/list.tsx +++ b/src/ts/component/menu/dataview/option/list.tsx @@ -87,7 +87,7 @@ const MenuOptionList = observer(class MenuOptionList extends React.Component this.onOver(e, item)} >
this.onClick(e, item)}> - +
{active ? : ''} diff --git a/src/ts/component/menu/item/filter.tsx b/src/ts/component/menu/item/filter.tsx index 939c1d2ce4..eacd2795d5 100644 --- a/src/ts/component/menu/item/filter.tsx +++ b/src/ts/component/menu/item/filter.tsx @@ -94,7 +94,7 @@ const MenuItemFilter = observer(class MenuItemFilter extends React.Component ))} diff --git a/src/ts/lib/relation.ts b/src/ts/lib/relation.ts index f6527e5945..9f50c7cdea 100644 --- a/src/ts/lib/relation.ts +++ b/src/ts/lib/relation.ts @@ -10,15 +10,7 @@ class Relation { }; public className (v: I.RelationType): string { - v = Number(v); - - let c = ''; - if ([ I.RelationType.Select, I.RelationType.MultiSelect ].includes(v)) { - c = `select ${this.selectClassName(v)}`; - } else { - c = this.typeName(v); - }; - return `c-${c}`; + return `c-${this.typeName(v)}`; }; public iconName (key: string, v: I.RelationType): string {