Let me preface this by saying the solution below is far more invasive than I initially thought it would have needed to be. Most notably the total overwrite of Magento_Swatches/js/product-attributes
. The code below was testing and functioned on Magento 2.2.4.
Model/Swatch.php
This class just holds the constant values for the new frontend input types
<?php
namespace Pmclain\MultiSelectSwatch\Model;
class Swatch
{
const SWATCH_MULTISELECT_TYPE_TEXTUAL_ATTRIBUTE_FRONTEND_INPUT = 'swatch_multiselect_text';
const SWATCH_MULTISELECT_TYPE_VISUAL_ATTRIBUTE_FRONTEND_INPUT = 'swatch_multiselect_visual';
}
Observer/AddMultiSelectSwatchType.php
The observer will add the new frontend input type to the product attribute edit form.
<?php
namespace Pmclain\MultiSelectSwatch\Observer;
use Magento\Framework\Module\Manager;
use Magento\Framework\Event\Observer as EventObserver;
use Magento\Framework\Event\ObserverInterface;
class AddMultiSelectSwatchType implements ObserverInterface
{
protected $moduleManager;
public function __construct(Manager $moduleManager)
{
$this->moduleManager = $moduleManager;
}
public function execute(EventObserver $observer)
{
if (!$this->moduleManager->isOutputEnabled('Pmclain_MultiSelectSwatch')) {
return;
}
$response = $observer->getEvent()->getResponse();
$types = $response->getTypes();
$types[] = [
'value' => \Pmclain\MultiSelectSwatch\Model\Swatch::SWATCH_MULTISELECT_TYPE_VISUAL_ATTRIBUTE_FRONTEND_INPUT,
'label' => __('Visual Swatch - Multiselect'),
'hide_fields' => [
'is_unique',
'is_required',
'frontend_class',
'_scope',
'_default_value',
],
];
$types[] = [
'value' => \Pmclain\MultiSelectSwatch\Model\Swatch::SWATCH_MULTISELECT_TYPE_TEXTUAL_ATTRIBUTE_FRONTEND_INPUT,
'label' => __('Text Swatch - Multiselect'),
'hide_fields' => [
'is_unique',
'is_required',
'frontend_class',
'_scope',
'_default_value',
],
];
$response->setTypes($types);
}
}
etc/adminhtml/events.xml
Register the observer created above.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
<event name="adminhtml_product_attribute_types">
<observer name="multiselect_swatch_attribute_type" instance="Pmclain\MultiSelectSwatch\Observer\AddMultiSelectSwatchType" />
</event>
</config>
view/adminhtml/web/js/product-attributes.js
Add the frontend input values to the product attribute javascript. I couldn't find a clean way for doing this via a mixin, so this is a full rewrite of the core javascript.
define([
'jquery',
'Magento_Ui/js/modal/alert',
'Magento_Ui/js/modal/prompt',
'uiRegistry',
'collapsable'
], function ($, alert, prompt, rg) {
'use strict';
return function (optionConfig) {
var swatchProductAttributes = {
frontendInput: $('#frontend_input'),
isFilterable: $('#is_filterable'),
isFilterableInSearch: $('#is_filterable_in_search'),
backendType: $('#backend_type'),
usedForSortBy: $('#used_for_sort_by'),
frontendClass: $('#frontend_class'),
isWysiwygEnabled: $('#is_wysiwyg_enabled'),
isHtmlAllowedOnFront: $('#is_html_allowed_on_front'),
isRequired: $('#is_required'),
isUnique: $('#is_unique'),
defaultValueText: $('#default_value_text'),
defaultValueTextarea: $('#default_value_textarea'),
defaultValueDate: $('#default_value_date'),
defaultValueYesno: $('#default_value_yesno'),
isGlobal: $('#is_global'),
useProductImageForSwatch: $('#use_product_image_for_swatch'),
updateProductPreviewImage: $('#update_product_preview_image'),
usedInProductListing: $('#used_in_product_listing'),
isVisibleOnFront: $('#is_visible_on_front'),
position: $('#position'),
attrTabsFront: $('#product_attribute_tabs_front'),
get tabsFront() {
return this.attrTabsFront.length ? this.attrTabsFront.closest('li') : $('#front_fieldset-wrapper');
},
selectFields: ['select', 'multiselect', 'price', 'swatch_text', 'swatch_visual', 'swatch_multiselect_text', 'swatch_multiselect_visual'],
toggleApplyVisibility: function (select) {
if ($(select).val() === 1) {
$(select).next('select').removeClass('no-display');
$(select).next('select').removeClass('ignore-validate');
} else {
$(select).next('select').addClass('no-display');
$(select).next('select').addClass('ignore-validate');
$(select).next('select option:selected').each(function () {
this.selected = false;
});
}
},
checkOptionsPanelVisibility: function () {
var selectOptionsPanel = $('#manage-options-panel'),
visualOptionsPanel = $('#swatch-visual-options-panel'),
textOptionsPanel = $('#swatch-text-options-panel');
this._hidePanel(selectOptionsPanel);
this._hidePanel(visualOptionsPanel);
this._hidePanel(textOptionsPanel);
switch (this.frontendInput.val()) {
case 'swatch_visual':
case 'swatch_multiselect_visual':
this._showPanel(visualOptionsPanel);
break;
case 'swatch_text':
case 'swatch_multiselect_text':
this._showPanel(textOptionsPanel);
break;
case 'select':
case 'multiselect':
this._showPanel(selectOptionsPanel);
break;
}
},
bindAttributeInputType: function () {
this.checkOptionsPanelVisibility();
this.switchDefaultValueField();
if (!~$.inArray(this.frontendInput.val(), this.selectFields)) {
// not in array
this.isFilterable.selectedIndex = 0;
this._disable(this.isFilterable);
this._disable(this.isFilterableInSearch);
} else {
// in array
this._enable(this.isFilterable);
this._enable(this.isFilterableInSearch);
this.backendType.val('int');
}
if (this.frontendInput.val() === 'multiselect' ||
this.frontendInput.val() === 'gallery' ||
this.frontendInput.val() === 'textarea'
) {
this._disable(this.usedForSortBy);
} else {
this._enable(this.usedForSortBy);
}
if (this.frontendInput.val() === 'swatch_text') {
$('.swatch-text-field-0').addClass('required-option');
} else {
$('.swatch-text-field-0').removeClass('required-option');
}
this.setRowVisibility(this.isWysiwygEnabled, false);
this.setRowVisibility(this.isHtmlAllowedOnFront, false);
switch (this.frontendInput.val()) {
case 'textarea':
this.setRowVisibility(this.isWysiwygEnabled, true);
if (this.isWysiwygEnabled.val() === '0') {
this._enable(this.isHtmlAllowedOnFront);
}
this.frontendClass.val('');
this._disable(this.frontendClass);
break;
case 'text':
this.setRowVisibility(this.isHtmlAllowedOnFront, true);
this._enable(this.frontendClass);
break;
case 'select':
case 'multiselect':
this.setRowVisibility(this.isHtmlAllowedOnFront, true);
this.frontendClass.val('');
this._disable(this.frontendClass);
break;
default:
this.frontendClass.val('');
this._disable(this.frontendClass);
}
this.switchIsFilterable();
},
switchIsFilterable: function () {
if (this.isFilterable.selectedIndex === 0) {
this._disable(this.position);
} else {
this._enable(this.position);
}
},
/**
* @this {swatchProductAttributes}
*/
switchDefaultValueField: function () {
var currentValue = this.frontendInput.val(),
defaultValueTextVisibility = false,
defaultValueTextareaVisibility = false,
defaultValueDateVisibility = false,
defaultValueYesnoVisibility = false,
scopeVisibility = true,
useProductImageForSwatch = false,
defaultValueUpdateImage = false,
optionDefaultInputType = '',
isFrontTabHidden = false,
thing = this;
if (!this.frontendInput.length) {
return;
}
switch (currentValue) {
case 'select':
optionDefaultInputType = 'radio';
break;
case 'multiselect':
optionDefaultInputType = 'checkbox';
break;
case 'date':
defaultValueDateVisibility = true;
break;
case 'boolean':
defaultValueYesnoVisibility = true;
break;
case 'textarea':
defaultValueTextareaVisibility = true;
break;
case 'media_image':
defaultValueTextVisibility = false;
break;
case 'price':
scopeVisibility = false;
break;
case 'swatch_visual':
case 'swatch_multiselect_visual':
useProductImageForSwatch = true;
defaultValueUpdateImage = true;
defaultValueTextVisibility = false;
break;
case 'swatch_text':
case 'swatch_multiselect_text':
useProductImageForSwatch = false;
defaultValueUpdateImage = true;
defaultValueTextVisibility = false;
break;
default:
defaultValueTextVisibility = true;
break;
}
delete optionConfig.hiddenFields['swatch_visual'];
delete optionConfig.hiddenFields['swatch_text'];
if (currentValue === 'media_image') {
this.tabsFront.hide();
this.setRowVisibility(this.isRequired, false);
this.setRowVisibility(this.isUnique, false);
this.setRowVisibility(this.frontendClass, false);
} else if (optionConfig.hiddenFields[currentValue]) {
$.each(optionConfig.hiddenFields[currentValue], function (key, option) {
switch (option) {
case '_front_fieldset':
thing.tabsFront.hide();
isFrontTabHidden = true;
break;
case '_default_value':
defaultValueTextVisibility = false;
defaultValueTextareaVisibility = false;
defaultValueDateVisibility = false;
defaultValueYesnoVisibility = false;
break;
case '_scope':
scopeVisibility = false;
break;
default:
thing.setRowVisibility($('#' + option), false);
}
});
if (!isFrontTabHidden) {
thing.tabsFront.show();
}
} else {
this.tabsFront.show();
this.showDefaultRows();
}
this.setRowVisibility(this.defaultValueText, defaultValueTextVisibility);
this.setRowVisibility(this.defaultValueTextarea, defaultValueTextareaVisibility);
this.setRowVisibility(this.defaultValueDate, defaultValueDateVisibility);
this.setRowVisibility(this.defaultValueYesno, defaultValueYesnoVisibility);
this.setRowVisibility(this.isGlobal, scopeVisibility);
/* swatch attributes */
this.setRowVisibility(this.useProductImageForSwatch, useProductImageForSwatch);
this.setRowVisibility(this.updateProductPreviewImage, defaultValueUpdateImage);
$('input[name=\'default[]\']').each(function () {
$(this).attr('type', optionDefaultInputType);
});
},
/**
* @this {swatchProductAttributes}
*/
showDefaultRows: function () {
this.setRowVisibility(this.isRequired, true);
this.setRowVisibility(this.isUnique, true);
this.setRowVisibility(this.frontendClass, true);
},
/**
* @param {Object} el
* @param {Boolean} isVisible
* @this {swatchProductAttributes}
*/
setRowVisibility: function (el, isVisible) {
if (isVisible) {
el.show();
el.closest('.field').show();
} else {
el.hide();
el.closest('.field').hide();
}
},
/**
* @param {Object} el
* @this {swatchProductAttributes}
*/
_disable: function (el) {
el.attr('disabled', 'disabled');
},
/**
* @param {Object} el
* @this {swatchProductAttributes}
*/
_enable: function (el) {
if (!el.attr('readonly')) {
el.removeAttr('disabled');
}
},
/**
* @param {Object} el
* @this {swatchProductAttributes}
*/
_showPanel: function (el) {
el.closest('.fieldset').show();
this._render(el.attr('id'));
},
/**
* @param {Object} el
* @this {swatchProductAttributes}
*/
_hidePanel: function (el) {
el.closest('.fieldset').hide();
},
/**
* @param {String} id
* @this {swatchProductAttributes}
*/
_render: function (id) {
rg.get(id, function () {
$('#' + id).trigger('render');
});
},
/**
* @param {String} promptMessage
* @this {swatchProductAttributes}
*/
saveAttributeInNewSet: function (promptMessage) {
prompt({
content: promptMessage,
actions: {
/**
* @param {String} val
* @this {actions}
*/
confirm: function (val) {
var rules = ['required-entry', 'validate-no-html-tags'],
newAttributeSetNameInputId = $('#new_attribute_set_name'),
editForm = $('#edit_form'),
newAttributeSetName = val,
i;
if (!newAttributeSetName) {
return;
}
for (i = 0; i < rules.length; i++) {
if (!$.validator.methods[rules[i]](newAttributeSetName)) {
alert({
content: $.validator.messages[rules[i]]
});
return;
}
}
if (newAttributeSetNameInputId.length) {
newAttributeSetNameInputId.val(newAttributeSetName);
} else {
editForm.append(new Element('input', {
type: 'hidden',
id: newAttributeSetNameInputId,
name: 'new_attribute_set_name',
value: newAttributeSetName
})
);
}
// Temporary solution will replaced after refactoring of attributes functionality
editForm.triggerHandler('save');
}
}
});
}
};
$(function () {
$('#frontend_input').bind('change', function () {
swatchProductAttributes.bindAttributeInputType();
});
$('#is_filterable').bind('change', function () {
swatchProductAttributes.switchIsFilterable();
});
swatchProductAttributes.bindAttributeInputType();
// @todo: refactor collapsable component
$('.attribute-popup .collapse, [data-role="advanced_fieldset-content"]')
.collapsable()
.collapse('hide');
});
window.saveAttributeInNewSet = swatchProductAttributes.saveAttributeInNewSet;
window.toggleApplyVisibility = swatchProductAttributes.toggleApplyVisibility;
};
});
view/adminhtml/web/js/type-change.js
Add the ability for switching between multiselect, visual swatch multiselect, and text swatch multiselect.
require([
'jquery',
'mage/translate'
], function ($) {
'use strict';
$(function () {
$('select#frontend_input:disabled').each(function () {
var select = $(this),
currentValue = select.find('option:selected').val(),
enabledTypes = ['multiselect', 'swatch_multiselect_visual', 'swatch_multiselect_text'],
warning = $('<label>')
.hide()
.text($.mage.__('These changes affect all related products.'))
.addClass('mage-error')
.attr({
generated: true, for: select.attr('id')
}),
toggleWarning = function () {
if (select.find('option:selected').val() === currentValue) {
warning.hide();
} else {
warning.show();
}
},
removeOption = function () {
if (!~enabledTypes.indexOf($(this).val())) {
$(this).remove();
}
};
if (!~enabledTypes.indexOf(currentValue)) {
return;
}
select.removeAttr('disabled').find('option').each(removeOption);
select.after(warning).on('change', toggleWarning);
});
});
});
view/adminhtml/requirejs-config.js
Register the rewrite of the core product attribute javascript above.
var config = {
map: {
'*': {
swatchesProductAttributes: 'Pmclain_MultiSelectSwatch/js/product-attributes'
}
}
};
view/adminhtml/templates/catalog/product/attribute/js.phtml
For adding all the javascript about on the attribute edit forms.
<script type="text/x-magento-init">
{
"*": {
"swatchesProductAttributes": {
"hiddenFields": <?= /* @escapeNotVerified */ json_encode($this->helper('Magento\Catalog\Helper\Data')->getAttributeHiddenFields()) ?>
},
"swatchesTypeChange": { },
"Pmclain_MultiSelectSwatch/js/type-change": { }
}
}
</script>
view/adminhtml/layout/catalog_product_attribute_edit_popup.xml
<?xml version="1.0"?>
<page layout="admin-1column" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceBlock name="attribute_edit_js">
<action method="setTemplate">
<argument name="template" xsi:type="string">Pmclain_MultiSelectSwatch::catalog/product/attribute/js.phtml</argument>
</action>
</referenceBlock>
</body>
</page>
view/adminhtml/layout/catalog_product_attribute_edit.xml
<?xml version="1.0"?>
<page layout="admin-1column" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceBlock name="attribute_edit_js">
<action method="setTemplate">
<argument name="template" xsi:type="string">Pmclain_MultiSelectSwatch::catalog/product/attribute/js.phtml</argument>
</action>
</referenceBlock>
</body>
</page>
Plugin/Controller/Adminhtml/Product/Attribute/Save.php
Handle the new frontend types when saving product attributes.
<?php
namespace Pmclain\MultiSelectSwatch\Plugin\Controller\Adminhtml\Product\Attribute;
use Magento\Catalog\Controller\Adminhtml\Product\Attribute;
use Magento\Framework\App\RequestInterface;
use Magento\Swatches\Model\Swatch;
use Pmclain\MultiSelectSwatch\Model\Swatch as MultiselectSwatch;
class Save
{
public function beforeDispatch(Attribute\Save $subject, RequestInterface $request)
{
$data = $request->getPostValue();
if (isset($data['frontend_input'])) {
switch ($data['frontend_input']) {
case MultiselectSwatch::SWATCH_MULTISELECT_TYPE_VISUAL_ATTRIBUTE_FRONTEND_INPUT:
$data[Swatch::SWATCH_INPUT_TYPE_KEY] = Swatch::SWATCH_INPUT_TYPE_VISUAL;
$data['frontend_input'] = 'multiselect';
$request->setPostValue($data);
break;
case MultiselectSwatch::SWATCH_MULTISELECT_TYPE_TEXTUAL_ATTRIBUTE_FRONTEND_INPUT:
$data[Swatch::SWATCH_INPUT_TYPE_KEY] = Swatch::SWATCH_INPUT_TYPE_TEXT;
$data['use_product_image_for_swatch'] = 0;
$data['frontend_input'] = 'multiselect';
$request->setPostValue($data);
break;
}
}
return [$request];
}
}
Block/Adminhtml/Product/Attribute/Edit/Form.php
Ensure the proper frontend type is pre-selected when loading an existing attribute with the multiselect swatch type.
<?php
namespace Pmclain\MultiSelectSwatch\Block\Adminhtml\Product\Attribute\Edit;
use Magento\Swatches\Model\Swatch;
class Form extends \Magento\Swatches\Block\Adminhtml\Product\Attribute\Edit\Form
{
public function addValues($values)
{
if (!is_array($values)) {
return $this;
}
$values = array_merge(
$values,
$this->getAdditionalData($values)
);
if (isset($values['frontend_input']) && 'multiselect' == $values['frontend_input']
&& isset($values[Swatch::SWATCH_INPUT_TYPE_KEY])
) {
$values['frontend_input'] = 'swatch_multiselect_' . $values[Swatch::SWATCH_INPUT_TYPE_KEY];
}
return parent::addValues($values);
}
}