import { ReactNode, useState } from "react" import React from "react" import ActionCancelModal from "./ActionCancelModalComponent" import ReorderableListComponent from "./reorderable/ReorderableListComponent" /** Generic editor for a list of items that shows the items within a Bootstrap 3 list-group. * Adding and editing are done via a popup if present */ export function ListEditorComponent(props: { items: T[] onItemsChange: (items: T[]) => void /** Render the item in the list. Already inside a list-group-item */ renderItem: (item: T, index: number, onItemChange: (item: T) => void) => ReactNode /** Render the editor in the popup modal */ renderEditor?: (item: Partial, onItemChange: (item: Partial) => void) => ReactNode /** Create a new item. Doesn't allow add if not present. If editor not present, must return valid item */ createNew?: () => Partial /** Validate an item. True for valid */ validateItem?: (item: Partial) => boolean /** Override label of add button */ addLabel?: string /** Prompt to confirm deletion */ deleteConfirmPrompt?: string | ((item: T) => string) /** Allows list to be re-ordered by dragging. Returns unique key for each item. * Can just return index for simplicity. */ getReorderableKey?: (item: T, index: number) => any /** Puts an edit on the right which must be clicked to edit */ editLink?: boolean /** Selected item is highlighted */ selectedIndex?: number /** Modal size. Default is "large" */ modalSize?: "large" | "full" | "normal" | "small" | "x-large" /** Title of the modal */ modalTitle?: React.ReactNode }) { const [adding, setAdding] = useState>() const [editing, setEditing] = useState>() const [editingIndex, setEditingIndex] = useState() const handleAdd = (ev: React.MouseEvent) => { ev.stopPropagation() if (props.renderEditor != null) { setAdding(props.createNew!()) } else { props.onItemsChange(props.items.concat(props.createNew!() as T)) } } const handleDelete = (index: number, ev: React.MouseEvent) => { ev.stopPropagation() ev.preventDefault() // Confirm deletion if (props.deleteConfirmPrompt) { const prompt = (typeof props.deleteConfirmPrompt === "string") ? props.deleteConfirmPrompt : props.deleteConfirmPrompt(props.items[index]) if (!confirm(prompt)) { return } } const items = props.items.slice() items.splice(index, 1) props.onItemsChange(items) } /** Render as an li element */ const renderListItem = (item: T, index: number) => { const handleChange = (value: T) => { const items = props.items.slice() items[index] = value props.onItemsChange(items) } const handleClick = () => { if (props.renderEditor != null) { setEditing(item) setEditingIndex(index) } } return (
  • {props.editLink && props.renderEditor != null ? ( ) : null} {props.renderItem(item, index, handleChange)}
  • ) } /** Render as an li element */ const renderDraggableListItem = ( item: T, index: number, connectDragSource: (node: ReactNode) => ReactNode, connectDragPreview: (node: ReactNode) => ReactNode, connectDropTarget: (node: ReactNode) => ReactNode ) => { let elem: ReactNode = renderListItem(item, index) elem = connectDragSource(elem) elem = connectDragPreview(elem) elem = connectDropTarget(elem) return elem } return (
    {adding && props.renderEditor != null ? ( setAdding(undefined)} onAction={() => { if (props.validateItem != null && !props.validateItem(adding)) { return } props.onItemsChange(props.items.concat([adding as T])) setAdding(undefined) }} > {props.renderEditor(adding, setAdding)} ) : null} {editing != null && props.renderEditor != null ? ( setEditing(undefined)} onAction={() => { if (props.validateItem != null && !props.validateItem(editing)) { return } const items = props.items.slice() items.splice(editingIndex!, 1, editing as T) props.onItemsChange(items) setEditing(undefined) }} > {props.renderEditor(editing, setEditing)} ) : null} {props.getReorderableKey ? ( } /> ) : (
      {props.items.map(renderListItem)}
    )} {props.createNew ? (
    ) : null}
    ) }