Skip to content

Commit

Permalink
Add the post select control
Browse files Browse the repository at this point in the history
  • Loading branch information
Luehrsen committed Mar 29, 2023
1 parent 693b54d commit 19a73a0
Show file tree
Hide file tree
Showing 8 changed files with 747 additions and 31 deletions.
374 changes: 347 additions & 27 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@csstools/postcss-global-data": "^1.0.3",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/modifiers": "^6.0.1",
"@dnd-kit/sortable": "^7.0.2",
"@dnd-kit/utilities": "^3.2.1",
"@svgr/webpack": "^7.0.0",
"@wordpress/babel-plugin-import-jsx-pragma": "^4.12.0",
"@wordpress/babel-preset-default": "^7.13.0",
"@wordpress/dependency-extraction-webpack-plugin": "^4.12.0",
Expand Down Expand Up @@ -77,8 +82,5 @@
"webpack-cli": "^5.0.1",
"webpack-livereload-plugin": "^3.0.2",
"webpack-remove-empty-scripts": "^1.0.1"
},
"dependencies": {
"@svgr/webpack": "^7.0.0"
}
}
1 change: 1 addition & 0 deletions plugin/admin/src/css/components.css
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
@import "./components/icon-select-control.css";
@import "./components/react-select.css";
33 changes: 33 additions & 0 deletions plugin/admin/src/css/components/react-select.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.react-select {

& .react-select__input {
min-height: auto;

&:focus {
box-shadow: none;
}
}

& .react-select__multi-value {
display: grid;
grid-template-columns: 1fr auto;
}
}

.react-select__control:not(.react-select__control--is-disabled),
.react-select__option:not(.react-select__option--is-disabled) {
cursor: pointer;
}

.post-type-select__row,
.term-select__row {
flex-flow: row wrap;

& > * {
flex: 1 1 100%;
}
}

:root .react-select__menu {
z-index: 10;
}
268 changes: 268 additions & 0 deletions plugin/admin/src/js/components/post-select-control/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
/**
* A control to select a set of posts.
*/

/**
* WordPress dependencies.
*/
import { useEffect, useState } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';
import { __ } from '@wordpress/i18n';
import { PanelRow } from '@wordpress/components';

/**
* External dependencies.
*/
import AsyncSelect from 'react-select/async';
import { DndContext } from '@dnd-kit/core';
import { restrictToParentElement } from '@dnd-kit/modifiers';
import {
arrayMove,
horizontalListSortingStrategy,
SortableContext,
} from '@dnd-kit/sortable';

import {
MultiValue,
MultiValueRemove,
MultiValueContainer,
} from './multi-value';

const PostSelectControl = ({
value,
onChange,
endpoint = 'posts',
query = {},
label = '',
help = '',
multiple = true,
max,
}) => {
const [selectedOptions, setSelectedOptions] = useState([]);

if (typeof label === 'undefined') {
label = __('Select a post', 'jitmp');
}

/**
* Endpoint path.
*
* @type {string}
*/
const endpointPath = `/wp/v2/${endpoint}`;

/**
* Handle a change in the selected option.
*
* @param {*} option The selected option
*/
const onSelectPost = (option) => {
setSelectedOptions(option);

if (!multiple) {
onChange(option?.value);
} else {
onChange(option.map((o) => o.value));
}
};

let defaultSelectedOptionValues = [];

/**
* Iterate over the value and compose an array of selected values.
*/
if (multiple) {
defaultSelectedOptionValues = value;
} else {
defaultSelectedOptionValues = value;
}

/**
* A set of options that are available by default.
*
* @type {Array}
*/
const defaultOptions = [];

/**
* Load the default options. Must include the options that are already
* selected.
*/
const loadDefaultOptions = () => {
let newSelectedOptions = [];

// Check if selectedOptions is an array.
if (Array.isArray(selectedOptions)) {
newSelectedOptions = selectedOptions;
} else {
newSelectedOptions.push(selectedOptions);
}

return apiFetch({
path: addQueryArgs(endpointPath, {
per_page: 10,
include: defaultSelectedOptionValues,
...query,
}),
}).then((response) => {
/**
* Iterate over the response and add the options to the default
* options.
*/
const responseOptions = response.map((post) => {
const option = {
value: post.id,
// First: Post, Sec: search, Fallback: Taxonomy/Term.
label: post?.title?.rendered || post?.title || post.name,
};

/**
* If this post is in the defaultSelectedOption, add it to the state.
*/
if (
multiple &&
defaultSelectedOptionValues?.includes(post.id)
) {
newSelectedOptions.push(option);
} else if (post.id === defaultSelectedOptionValues) {
newSelectedOptions.push(option);
}

defaultOptions.push(option);

return option;
});

/**
* Update the state with the new options after we've completed
* iterating over the response.
*/
setSelectedOptions(newSelectedOptions);

return responseOptions;
});
};

const onSortEnd = (event) => {
const { active, over } = event;

if (!active || !over) return;

// const sortItems = (items) => {
// console.log({items});
// const oldIndex = items.findIndex(
// (item) => item.value === active.id
// );
// const newIndex = items.findIndex((item) => item.value === over.id);

// return arrayMove(items, oldIndex, newIndex);
// };

const oldIndex = selectedOptions.findIndex(
(item) => item.value === active.id
);
const newIndex = selectedOptions.findIndex(
(item) => item.value === over.id
);

const sortedItems = arrayMove(selectedOptions, oldIndex, newIndex);

setSelectedOptions(sortedItems);
onChange(sortedItems);
};

useEffect(() => {
if (!selectedOptions?.length) {
loadDefaultOptions();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedOptions]);

/**
* When the endpoint changes, we need to reset the selected options.
*/
useEffect(() => {
setSelectedOptions([]);
defaultOptions.length = 0;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [endpoint]);

/**
* Load the options for the select control from the API.
*
* @param {string} inputValue The search term.
* @return {Promise} The options to display.
*/
const loadOptions = (inputValue = null) => {
return new Promise((resolve) => {
return apiFetch({
path: addQueryArgs(endpointPath, {
per_page: 10,
search: inputValue,
...query,
}),
}).then((response) => {
resolve(
response.map((post) => ({
value: post.id,
// First: Post, Sec: search, Fallback: Taxonomy/Term.
label:
post?.title?.rendered || post?.title || post.name,
post,
}))
);
});
});
};

return (
<PanelRow className="post-type-select__row">
<span className="post-type-select__label">{label}</span>
{multiple && (
<DndContext
modifiers={[restrictToParentElement]}
onDragEnd={onSortEnd}
>
<SortableContext
items={selectedOptions.map((o) => o.value)}
strategy={horizontalListSortingStrategy}
>
<AsyncSelect
value={selectedOptions}
onChange={onSelectPost}
loadOptions={loadOptions}
defaultOptions={true}
className={'react-select'}
classNamePrefix={'react-select'}
isClearable
isMulti
isOptionDisabled={() => max && value?.length >= max}
closeMenuOnSelect={false}
components={{
MultiValue,
MultiValueContainer,
MultiValueRemove,
}}
/>
</SortableContext>
</DndContext>
)}
{!multiple && (
<AsyncSelect
value={selectedOptions}
onChange={onSelectPost}
loadOptions={loadOptions}
defaultOptions={true}
className={'react-select'}
classNamePrefix={'react-select'}
isClearable
isOptionDisabled={() => max && value?.length >= max}
/>
)}
{help && <p className="post-type-select__help">{help}</p>}
</PanelRow>
);
};

export default PostSelectControl;
59 changes: 59 additions & 0 deletions plugin/admin/src/js/components/post-select-control/multi-value.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { components } from 'react-select';
import { useDroppable } from '@dnd-kit/core';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';

const MultiValue = (props) => {
const onMouseDown = (e) => {
e.preventDefault();
e.stopPropagation();
};
const innerProps = { ...props.innerProps, onMouseDown };
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({
id: props.data.value,
});

const style = {
transform: CSS.Transform.toString(transform),
transition,
};

return (
<div style={style} ref={setNodeRef} {...attributes} {...listeners}>
<components.MultiValue {...props} innerProps={innerProps} />
</div>
);
};

const MultiValueContainer = (props) => {
const { isOver, setNodeRef } = useDroppable({
id: 'droppable',
});

const style = {
color: isOver ? 'green' : undefined,
};

return (
<div content={'Customise your multi-value container!'}>
<div style={style} ref={setNodeRef}>
<components.MultiValueContainer {...props} />
</div>
</div>
);
};

const MultiValueRemove = (props) => {
return (
<components.MultiValueRemove
{...props}
innerProps={{
onPointerDown: (e) => e.stopPropagation(),
...props.innerProps,
}}
/>
);
};

export { MultiValue, MultiValueContainer, MultiValueRemove };
Loading

0 comments on commit 19a73a0

Please sign in to comment.