import './style.scss';
import selectStyles from 'gutenberg-react-select-styles';
import $ from 'jquery';
import rafSchd from 'raf-schd';
import Select, { components } from 'react-select';
import AsyncSelect from 'react-select/async';
import CreatableSelect from 'react-select/creatable';
import {
SortableContainer,
SortableElement,
sortableHandle,
} from 'react-sortable-hoc';
import { debounce } from 'throttle-debounce';
import { Component } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
const { Option } = components;
const { ajaxurl, VPGutenbergVariables } = window;
const cachedOptions = {};
const SortableSelect = SortableContainer(Select);
const SortableCreatableSelect = SortableContainer(CreatableSelect);
const SortableAsyncSelect = SortableContainer(AsyncSelect);
const SortableMultiValueLabel = sortableHandle((props) => (
));
const SortableMultiValue = SortableElement((props) => {
// this prevents the menu from being opened/closed when the user clicks
// on a value to begin dragging it. ideally, detecting a click (instead of
// a drag) would still focus the control and toggle the menu, but that
// requires some magic with refs that are out of scope for this example
const onMouseDown = (e) => {
e.preventDefault();
e.stopPropagation();
};
const innerProps = { ...props.innerProps, onMouseDown };
return ;
});
function arrayMove(array, from, to) {
array = array.slice();
array.splice(to < 0 ? array.length + to : to, 0, array.splice(from, 1)[0]);
return array;
}
/**
* Component Class
*/
export default class VpfSelectControl extends Component {
constructor(...args) {
super(...args);
const { callback } = this.props;
this.state = {
options: {},
ajaxStatus: !!callback,
};
this.getOptions = this.getOptions.bind(this);
this.getDefaultValue = this.getDefaultValue.bind(this);
this.findValueData = this.findValueData.bind(this);
this.requestAjax = this.requestAjax.bind(this);
this.requestAjaxDebounce = debounce(300, rafSchd(this.requestAjax));
}
componentDidMount() {
const { callback } = this.props;
if (callback) {
this.requestAjax({}, (result) => {
if (result.options) {
this.setState({
options: result.options,
});
}
});
}
}
/**
* Get options list.
*
* @return {Object} - options list for React Select.
*/
getOptions() {
const { controlName } = this.props;
if (cachedOptions[controlName]) {
return cachedOptions[controlName];
}
return Object.keys(this.state.options).length
? this.state.options
: this.props.options;
}
/**
* Get default value in to support React Select attribute.
*
* @return {Object} - value object for React Select.
*/
getDefaultValue() {
const { value, isMultiple } = this.props;
let result = null;
if (isMultiple) {
if ((!value && typeof value !== 'string') || !value.length) {
return result;
}
result = [];
value.forEach((innerVal) => {
result.push(this.findValueData(innerVal));
});
} else {
if (!value && typeof value !== 'string') {
return result;
}
result = this.findValueData(value);
}
return result;
}
/**
* Find option data by value.
*
* @param {string} findVal - value.
*
* @return {Object | boolean} - value object.
*/
findValueData(findVal) {
let result = {
value: findVal,
label: findVal,
};
const options = this.getOptions();
// Find value in options.
if (options) {
Object.keys(options).forEach((val) => {
const data = options[val];
if (val === findVal) {
if (typeof data === 'string') {
result.label = data;
} else {
result = data;
}
}
});
}
return result;
}
/**
* Request AJAX dynamic data.
*
* @param {Object} additionalData - additional data for AJAX call.
* @param {Function} callback - callback.
* @param {boolean} useStateLoading - use state change when loading.
*/
requestAjax(
additionalData = {},
callback = () => {},
useStateLoading = true
) {
const { controlName, attributes } = this.props;
if (this.isAJAXinProgress) {
return;
}
this.isAJAXinProgress = true;
if (useStateLoading) {
this.setState({
ajaxStatus: 'progress',
});
}
const ajaxData = {
action: 'vp_dynamic_control_callback',
nonce: VPGutenbergVariables.nonce,
vp_control_name: controlName,
vp_attributes: attributes,
...additionalData,
};
$.ajax({
url: ajaxurl,
method: 'POST',
dataType: 'json',
data: ajaxData,
complete: (data) => {
const json = data.responseJSON;
if (callback && json.response) {
if (json.response.options) {
cachedOptions[controlName] = {
...cachedOptions[controlName],
...json.response.options,
};
}
callback(json.response);
}
if (useStateLoading) {
this.setState({
ajaxStatus: true,
});
}
this.isAJAXinProgress = false;
},
});
}
/**
* Prepare options for React Select structure.
*
* @param {Object} options - options object.
*
* @return {Object} - prepared options.
*/
prepareOptions(options) {
return Object.keys(options || {}).map((val) => {
const option = options[val];
let result = {
value: val,
label: options[val],
};
if (typeof option === 'object') {
result = { ...option };
}
return result;
});
}
render() {
const { onChange, isMultiple, isSearchable, isCreatable, callback } =
this.props;
const { ajaxStatus } = this.state;
const isAsync = !!callback && isSearchable;
const isLoading = ajaxStatus && ajaxStatus === 'progress';
const selectProps = {
// Test opened menu items:
// menuIsOpen: true,
className: 'vpf-component-select',
styles: {
...selectStyles,
menuPortal: (styles) => {
return {
...styles,
zIndex: 1000000,
};
},
},
menuPortalTarget: document.body,
components: {
Option(optionProps) {
const { data } = optionProps;
return (
);
},
},
value: this.getDefaultValue(),
options: this.prepareOptions(this.getOptions()),
onChange(val) {
if (isMultiple) {
if (Array.isArray(val)) {
const result = [];
val.forEach((innerVal) => {
result.push(innerVal ? innerVal.value : '');
});
onChange(result);
} else {
onChange([]);
}
} else {
onChange(val ? val.value : '');
}
},
isMulti: isMultiple,
isSearchable,
isLoading,
isClearable: false,
placeholder: isSearchable
? __('Type to search…', 'visual-portfolio')
: __('Select…', 'visual-portfolio'),
};
// Multiple select.
if (isMultiple) {
selectProps.useDragHandle = true;
selectProps.axis = 'xy';
selectProps.onSortEnd = ({ oldIndex, newIndex }) => {
const newValue = arrayMove(
this.getDefaultValue(),
oldIndex,
newIndex
);
selectProps.onChange(newValue);
};
selectProps.distance = 4;
// small fix for https://github.com/clauderic/react-sortable-hoc/pull/352:
selectProps.getHelperDimensions = ({ node }) =>
node.getBoundingClientRect();
selectProps.components.MultiValue = SortableMultiValue;
selectProps.components.MultiValueLabel = SortableMultiValueLabel;
// prevent closing options dropdown after select.
selectProps.closeMenuOnSelect = false;
}
// Creatable select.
if (isCreatable) {
selectProps.placeholder = __(
'Type and press Enter…',
'visual-portfolio'
);
selectProps.isSearchable = true;
if (isMultiple) {
return ;
}
return ;
}
// Async select.
if (isAsync) {
selectProps.loadOptions = (inputValue, cb) => {
this.requestAjaxDebounce(
{ q: inputValue },
(result) => {
const newOptions = [];
if (result && result.options) {
Object.keys(result.options).forEach((k) => {
newOptions.push(result.options[k]);
});
}
cb(newOptions.length ? newOptions : null);
},
false
);
};
selectProps.cacheOptions = true;
selectProps.defaultOptions = selectProps.options;
delete selectProps.options;
delete selectProps.isLoading;
if (isMultiple) {
return ;
}
return ;
}
// Default select.
if (isMultiple) {
return ;
}
return ;
}
}