Merge pull request #622 from jitendra-webkul/jitendra

Option swatches feature added
This commit is contained in:
JItendra Singh 2019-02-26 11:55:42 +05:30 committed by GitHub
commit c1d890ee56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 431 additions and 99 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
{
"/js/admin.js": "/js/admin.js?id=7683c8f127f6ad2ac3ce",
"/js/admin.js": "/js/admin.js?id=da5ebef9c25a064e7ed6",
"/css/admin.css": "/css/admin.css?id=ccc39419fee98ed8ac7b"
}
}

View File

@ -416,7 +416,13 @@ return [
'is_filterable' => 'Use in Layered Navigation',
'is_configurable' => 'Use To Create Configurable Product',
'admin_name' => 'Admin Name',
'is_visible_on_front' => 'Visible on Product View Page on Front-end'
'is_visible_on_front' => 'Visible on Product View Page on Front-end',
'swatch_type' => 'Swatch Type',
'dropdown' => 'Dropdown',
'color-swatch' => 'Color Swatch',
'image-swatch' => 'Image Swatch',
'text-swatch' => 'Text Swatch',
'swatch' => 'Swatch'
],
'families' => [
'title' => 'Families',

View File

@ -219,10 +219,34 @@
@push('scripts')
<script type="text/x-template" id="options-template">
<div>
<div class="control-group">
<label for="swatch_type">{{ __('admin::app.catalog.attributes.swatch_type') }}</label>
<select class="control" id="swatch_type" name="swatch_type" v-model="swatch_type">
<option value="dropdown">
{{ __('admin::app.catalog.attributes.dropdown') }}
</option>
<option value="color">
{{ __('admin::app.catalog.attributes.color-swatch') }}
</option>
<option value="image">
{{ __('admin::app.catalog.attributes.image-swatch') }}
</option>
<option value="text">
{{ __('admin::app.catalog.attributes.text-swatch') }}
</option>
</select>
</div>
<div class="table">
<table>
<thead>
<tr>
<th v-if="swatch_type == 'color' || swatch_type == 'image'">{{ __('admin::app.catalog.attributes.swatch') }}</th>
<th>{{ __('admin::app.catalog.attributes.admin_name') }}</th>
@foreach (Webkul\Core\Models\Locale::all() as $locale)
@ -239,6 +263,14 @@
<tbody>
<tr v-for="row in optionRows">
<td v-if="swatch_type == 'color'">
<swatch-picker :input-name="'options[' + row.id + '][swatch_value]'" :color="row.swatch_value" colors="text-advanced" show-fallback />
</td>
<td v-if="swatch_type == 'image'">
<input type="file" accept="image/*" :name="'options[' + row.id + '][swatch_value]'"/>
</td>
<td>
<div class="control-group" :class="[errors.has(adminName(row)) ? 'has-error' : '']">
<input type="text" v-validate="'required'" v-model="row['admin_name']" :name="adminName(row)" class="control" data-vv-as="&quot;{{ __('admin::app.catalog.attributes.admin_name') }}&quot;"/>
@ -292,7 +324,8 @@
data: () => ({
optionRowCount: 0,
optionRows: []
optionRows: [],
swatch_type: ''
}),
methods: {
@ -301,7 +334,7 @@
var row = {'id': 'option_' + rowCount};
@foreach (Webkul\Core\Models\Locale::all() as $locale)
row['{{ $locale->code }}'] = '';
row['{{ $locale->code }}'] = '';
@endforeach
this.optionRows.push(row);

View File

@ -6,7 +6,7 @@
@section('content')
<div class="content">
<form method="POST" action="{{ route('admin.catalog.attributes.update', $attribute->id) }}" @submit.prevent="onSubmit">
<form method="POST" action="{{ route('admin.catalog.attributes.update', $attribute->id) }}" @submit.prevent="onSubmit" enctype="multipart/form-data">
<div class="page-header">
<div class="page-title">
@ -273,10 +273,34 @@
@push('scripts')
<script type="text/x-template" id="options-template">
<div>
<div class="control-group">
<label for="swatch_type">{{ __('admin::app.catalog.attributes.swatch_type') }}</label>
<select class="control" id="swatch_type" name="swatch_type" v-model="swatch_type">
<option value="dropdown">
{{ __('admin::app.catalog.attributes.dropdown') }}
</option>
<option value="color">
{{ __('admin::app.catalog.attributes.color-swatch') }}
</option>
<option value="image">
{{ __('admin::app.catalog.attributes.image-swatch') }}
</option>
<option value="text">
{{ __('admin::app.catalog.attributes.text-swatch') }}
</option>
</select>
</div>
<div class="table">
<table>
<thead>
<tr>
<th v-if="swatch_type == 'color' || swatch_type == 'image'">{{ __('admin::app.catalog.attributes.swatch') }}</th>
<th>{{ __('admin::app.catalog.attributes.admin_name') }}</th>
@foreach (Webkul\Core\Models\Locale::all() as $locale)
@ -292,7 +316,16 @@
</thead>
<tbody>
<tr v-for="row in optionRows">
<tr v-for="(row, index) in optionRows">
<td v-if="swatch_type == 'color'">
<swatch-picker :input-name="'options[' + row.id + '][swatch_value]'" :color="row.swatch_value" colors="text-advanced" show-fallback />
</td>
<td v-if="swatch_type == 'image'">
<img style="width: 36px;height: 36px;vertical-align: middle;background: #F2F2F2;border-radius: 2px;margin-right: 10px;" v-if="row.swatch_value_url" :src="row.swatch_value_url"/>
<input type="file" accept="image/*" :name="'options[' + row.id + '][swatch_value]'"/>
</td>
<td>
<div class="control-group" :class="[errors.has(adminName(row)) ? 'has-error' : '']">
<input type="text" v-validate="'required'" v-model="row['admin_name']" :name="adminName(row)" class="control" data-vv-as="&quot;{{ __('admin::app.catalog.attributes.admin_name') }}&quot;"/>
@ -331,75 +364,73 @@
</script>
<script>
$(document).ready(function () {
$('#type').on('change', function (e) {
if (['select', 'multiselect', 'checkbox'].indexOf($(e.target).val()) === -1) {
$('#options').parent().addClass('hide')
} else {
$('#options').parent().removeClass('hide')
}
})
$('#type').on('change', function (e) {
if (['select', 'multiselect', 'checkbox'].indexOf($(e.target).val()) === -1) {
$('#options').parent().addClass('hide')
} else {
$('#options').parent().removeClass('hide')
}
})
var optionWrapper = Vue.component('option-wrapper', {
Vue.component('option-wrapper', {
template: '#options-template',
template: '#options-template',
created () {
@foreach ($attribute->options as $option)
this.optionRowCount++;
var row = {'id': '{{ $option->id }}', 'admin_name': '{{ $option->admin_name }}', 'sort_order': '{{ $option->sort_order }}'};
data: () => ({
optionRowCount: 0,
optionRows: [],
swatch_type: "{{ $attribute->swatch_type }}"
}),
@foreach (Webkul\Core\Models\Locale::all() as $locale)
row['{{ $locale->code }}'] = "{{ $option->translate($locale->code)['label'] }}";
@endforeach
created () {
@foreach ($attribute->options as $option)
this.optionRowCount++;
var row = {
'id': '{{ $option->id }}',
'admin_name': '{{ $option->admin_name }}',
'sort_order': '{{ $option->sort_order }}',
'sort_order': '{{ $option->sort_order }}',
'swatch_value': '{{ $option->swatch_value }}',
'swatch_value_url': '{{ $option->swatch_value_url }}'
};
this.optionRows.push(row);
@foreach (Webkul\Core\Models\Locale::all() as $locale)
row['{{ $locale->code }}'] = "{{ $option->translate($locale->code)['label'] }}";
@endforeach
this.optionRows.push(row);
@endforeach
},
methods: {
addOptionRow () {
var rowCount = this.optionRowCount++;
var row = {'id': 'option_' + rowCount};
@foreach (Webkul\Core\Models\Locale::all() as $locale)
row['{{ $locale->code }}'] = '';
@endforeach
this.optionRows.push(row);
},
data: () => ({
optionRowCount: 0,
optionRows: []
}),
removeRow (row) {
var index = this.optionRows.indexOf(row)
Vue.delete(this.optionRows, index);
},
methods: {
addOptionRow () {
var rowCount = this.optionRowCount++;
var row = {'id': 'option_' + rowCount};
adminName (row) {
return 'options[' + row.id + '][admin_name]';
},
@foreach (Webkul\Core\Models\Locale::all() as $locale)
row['{{ $locale->code }}'] = '';
@endforeach
localeInputName (row, locale) {
return 'options[' + row.id + '][' + locale + '][label]';
},
this.optionRows.push(row);
},
removeRow (row) {
var index = this.optionRows.indexOf(row)
Vue.delete(this.optionRows, index);
},
adminName (row) {
return 'options[' + row.id + '][admin_name]';
},
localeInputName (row, locale) {
return 'options[' + row.id + '][' + locale + '][label]';
},
sortOrderName (row) {
return 'options[' + row.id + '][sort_order]';
}
sortOrderName (row) {
return 'options[' + row.id + '][sort_order]';
}
})
new Vue({
el: '#options',
components: {
optionWrapper: optionWrapper
},
})
}
});
</script>
@endpush

View File

@ -61,7 +61,7 @@
<accordian :title="'{{ __('admin::app.catalog.families.groups') }}'" :active="true">
<div slot="body">
<button type="button" class="btn btn-md btn-primary" @click="showModal('addGroup')">
<button type="button" style="margin-bottom : 20px" class="btn btn-md btn-primary" @click="showModal('addGroup')">
{{ __('admin::app.catalog.families.add-group-title') }}
</button>

View File

@ -64,7 +64,7 @@
{!! view_render_event('bagisto.admin.catalog.family.edit_form_accordian.groups.controls.before', ['attributeFamily' => $attributeFamily]) !!}
<button type="button" class="btn btn-md btn-primary" @click="showModal('addGroup')">
<button type="button" style="margin-bottom : 20px" class="btn btn-md btn-primary" @click="showModal('addGroup')">
{{ __('admin::app.catalog.families.add-group-title') }}
</button>

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddSwatchTypeColumnInAttributesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('attributes', function (Blueprint $table) {
$table->string('swatch_type')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('attributes', function (Blueprint $table) {
//
});
}
}

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddSwatchValueColumnsInAttributeOptionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('attribute_options', function (Blueprint $table) {
$table->string('swatch_value')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('attribute_options', function (Blueprint $table) {
//
});
}
}

View File

@ -3,15 +3,13 @@
namespace Webkul\Attribute\Models;
use Webkul\Core\Eloquent\TranslatableModel;
use Webkul\Attribute\Models\AttributeOption;
use Webkul\Attribute\Models\AttributeGroup;
use Webkul\Attribute\Contracts\Attribute as AttributeContract;
class Attribute extends TranslatableModel implements AttributeContract
{
public $translatedAttributes = ['name'];
protected $fillable = ['code', 'admin_name', 'type', 'position', 'is_required', 'is_unique', 'validation', 'value_per_locale', 'value_per_channel', 'is_filterable', 'is_configurable', 'is_visible_on_front', 'is_user_defined'];
protected $fillable = ['code', 'admin_name', 'type', 'position', 'is_required', 'is_unique', 'validation', 'value_per_locale', 'value_per_channel', 'is_filterable', 'is_configurable', 'is_visible_on_front', 'is_user_defined', 'swatch_type'];
protected $with = ['options'];

View File

@ -3,7 +3,6 @@
namespace Webkul\Attribute\Models;
use Illuminate\Database\Eloquent\Model;
use Webkul\Attribute\Models\Attribute;
use Webkul\Attribute\Contracts\AttributeGroup as AttributeGroupContract;
class AttributeGroup extends Model implements AttributeGroupContract

View File

@ -2,9 +2,8 @@
namespace Webkul\Attribute\Models;
use Illuminate\Support\Facades\Storage;
use Webkul\Core\Eloquent\TranslatableModel;
use Dimsav\Translatable\Translatable;
use Webkul\Attribute\Models\Attribute;
use Webkul\Attribute\Contracts\AttributeOption as AttributeOptionContract;
class AttributeOption extends TranslatableModel implements AttributeOptionContract
@ -13,13 +12,32 @@ class AttributeOption extends TranslatableModel implements AttributeOptionContra
public $translatedAttributes = ['label'];
protected $fillable = ['admin_name', 'sort_order'];
protected $fillable = ['admin_name', 'swatch_value', 'sort_order', 'attribute_id'];
/**
* Get the attribute that owns the attribute option.
*/
public function attribute()
{
return $this->model()->getCustomAttributesAttribute();
return $this->belongsTo(AttributeProxy::modelClass());
}
/**
* Get image url for the swatch value url.
*/
public function swatch_value_url()
{
if ($this->swatch_value && $this->attribute->swatch_type == 'image')
return Storage::url($this->swatch_value);
return;
}
/**
* Get image url for the product image.
*/
public function getSwatchValueUrlAttribute()
{
return $this->swatch_value_url();
}
}

View File

@ -22,4 +22,49 @@ class AttributeOptionRepository extends Repository
{
return 'Webkul\Attribute\Contracts\AttributeOption';
}
/**
* @param array $data
* @return mixed
*/
public function create(array $data)
{
$option = parent::create($data);
$this->uploadSwatchImage($data, $option->id);
return $option;
}
/**
* @param array $data
* @param $id
* @param string $attribute
* @return mixed
*/
public function update(array $data, $id, $attribute = "id")
{
$option = parent::update($data, $id);
$this->uploadSwatchImage($data, $id);
return $option;
}
/**
* @param array $data
* @param mixed $optionId
* @return mixed
*/
public function uploadSwatchImage($data, $optionId)
{
if (! isset($data['swatch_value']) || ! $data['swatch_value'])
return;
if ($data['swatch_value'] instanceof \Illuminate\Http\UploadedFile) {
parent::update([
'swatch_value' => $data['swatch_value']->store('attribute_option')
], $optionId);
}
}
}

View File

@ -60,8 +60,10 @@ class AttributeRepository extends Repository
$attribute = $this->model->create($data);
if (in_array($attribute->type, ['select', 'multiselect', 'checkbox']) && count($options)) {
foreach ($options as $option) {
$attribute->options()->create($option);
foreach ($options as $optionInputs) {
$this->attributeOption->create(array_merge([
'attribute_id' => $attribute->id
], $optionInputs));
}
}
@ -92,7 +94,9 @@ class AttributeRepository extends Repository
if (isset($data['options'])) {
foreach ($data['options'] as $optionId => $optionInputs) {
if (str_contains($optionId, 'option_')) {
$attribute->options()->create($optionInputs);
$this->attributeOption->create(array_merge([
'attribute_id' => $attribute->id,
], $optionInputs));
} else {
if (is_numeric($index = $previousOptionIds->search($optionId))) {
$previousOptionIds->forget($index);

View File

@ -165,6 +165,7 @@ class ConfigurableOption extends AbstractProduct
'id' => $attributeId,
'code' => $attribute->code,
'label' => $attribute->name,
'swatch_type' => $attribute->swatch_type,
'options' => $attributeOptionsData
];
}
@ -190,6 +191,7 @@ class ConfigurableOption extends AbstractProduct
$attributeOptionsData[] = [
'id' => $optionId,
'label' => $attributeOption->label,
'swatch_value' => $attribute->swatch_type == 'image' ? $attributeOption->swatch_value_url : $attributeOption->swatch_value,
'products' => $options[$attribute->id][$optionId]
];
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
{
"/js/shop.js": "/js/shop.js?id=e019a3a0b0cbcc981fd8",
"/css/shop.css": "/css/shop.css?id=f55c895c4bc57f6cd61b"
}
"/js/shop.js": "/js/shop.js?id=391400bc9a994e60b97f",
"/css/shop.css": "/css/shop.css?id=d3700e4d8b82bb01dbc2"
}

View File

@ -1891,6 +1891,55 @@ section.product-detail {
display: block;
width: 100%;
border-bottom: solid 1px rgba(162, 162, 162, 0.2);
.attribute.control-group {
margin-bottom: 20px;
.swatch-container {
margin-top: 10px;
display: inline-block;
.swatch {
display: inline-block;
margin-right: 5px;
min-width: 40px;
height: 40px;
span {
min-width: 38px;
height: 38px;
float: left;
border: 1px solid #C7C7C7;
border-radius: 3px;
line-height: 36px;
text-align: center;
cursor: pointer;
padding: 0 10px;
}
img {
width: 38px;
height: 38px;
border: 1px solid #C7C7C7;
border-radius: 3px;
cursor: pointer;
background: rgb(242, 242, 242);
}
input:checked + span, input:checked + img {
border: 1px solid #242424;
}
input {
display: none;
}
}
.no-options {
color: $danger-color;
}
}
}
}
}
}

View File

@ -323,7 +323,8 @@ return [
'quantity' => 'Quantity',
'in-stock' => 'In Stock',
'out-of-stock' => 'Out Of Stock',
'view-all' => 'View All'
'view-all' => 'View All',
'select-above-options' => 'Please select above options first.'
],
'wishlist' => [

View File

@ -72,6 +72,7 @@
</div>
</div>
<script type="text/javascript">
window.flashMessages = [];

View File

@ -18,11 +18,48 @@
<div v-for='(attribute, index) in childAttributes' class="attribute control-group" :class="[errors.has('super_attribute[' + attribute.id + ']') ? 'has-error' : '']">
<label class="required">@{{ attribute.label }}</label>
<select v-validate="'required'" class="control" :name="['super_attribute[' + attribute.id + ']']" :disabled="attribute.disabled" @change="configure(attribute, $event.target.value)" :id="['attribute_' + attribute.id]" :data-vv-as="'&quot;' + attribute.label + '&quot;'">
<span v-if="! attribute.swatch_type || attribute.swatch_type == '' || attribute.swatch_type == 'dropdown'">
<select
class="control"
v-validate="'required'"
:name="['super_attribute[' + attribute.id + ']']"
:disabled="attribute.disabled"
@change="configure(attribute, $event.target.value)"
:id="['attribute_' + attribute.id]"
:data-vv-as="'&quot;' + attribute.label + '&quot;'">
<option v-for='(option, index) in attribute.options' :value="option.id">@{{ option.label }}</option>
<option v-for='(option, index) in attribute.options' :value="option.id">@{{ option.label }}</option>
</select>
</select>
</span>
<span class="swatch-container" v-else>
<label class="swatch"
v-for='(option, index) in attribute.options'
v-if="option.id"
:data-id="option.id"
:for="['attribute_' + attribute.id + '_option_' + option.id]">
<input type="radio"
v-validate="'required'"
:name="['super_attribute[' + attribute.id + ']']"
:id="['attribute_' + attribute.id + '_option_' + option.id]"
:value="option.id"
:data-vv-as="'&quot;' + attribute.label + '&quot;'"
@change="configure(attribute, $event.target.value)"/>
<span v-if="attribute.swatch_type == 'color'" :style="{ background: option.swatch_value }"></span>
<img v-if="attribute.swatch_type == 'image'" :src="option.swatch_value" />
<span v-if="attribute.swatch_type == 'text'">
@{{ option.label }}
</span>
</label>
<span v-if="! attribute.options.length" class="no-options">{{ __('shop::app.products.select-above-options') }}</span>
</span>
<span class="control-error" v-if="errors.has('super_attribute[' + attribute.id + ']')">
@{{ errors.first('super_attribute[' + attribute.id + ']') }}
@ -96,6 +133,7 @@
attribute.nextAttribute.disabled = false;
this.fillSelect(attribute.nextAttribute);
this.resetChildren(attribute.nextAttribute);
} else {
this.selectedProductId = attribute.options[attribute.selectedIndex].allowedProducts[0];
@ -137,6 +175,7 @@
selectedIndex = index;
}
})
return selectedIndex;
},
@ -165,8 +204,7 @@
this.clearSelect(attribute)
attribute.options = [];
attribute.options[0] = {'id': '', 'label': this.config.chooseText, 'products': []};
attribute.options = [{'id': '', 'label': this.config.chooseText, 'products': []}];
if (attribute.prevAttribute) {
prevOption = attribute.prevAttribute.options[attribute.prevAttribute.selectedIndex];
@ -207,13 +245,23 @@
},
clearSelect: function (attribute) {
if (!attribute)
if (! attribute)
return;
var element = document.getElementById("attribute_" + attribute.id);
if (! attribute.swatch_type || attribute.swatch_type == '' || attribute.swatch_type == 'dropdown') {
var element = document.getElementById("attribute_" + attribute.id);
if (element) {
element.selectedIndex = "0";
if (element) {
element.selectedIndex = "0";
}
} else {
var elements = document.getElementsByName('super_attribute[' + attribute.id + ']');
var this_this = this;
elements.forEach(function(element) {
element.checked = false;
})
}
},

View File

@ -17,5 +17,8 @@
"jquery": "^3.2",
"vue": "^2.1.10",
"flatpickr": "^4.4.6"
},
"dependencies": {
"vue-swatches": "^1.0.3"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
{
"/js/ui.js": "/js/ui.js?id=d984bc35663c0fd9cf3f",
"/css/ui.css": "/css/ui.css?id=2ac413cc34214026f3ce"
}
"/js/ui.js": "/js/ui.js?id=2a928588409cb82a9e36",
"/css/ui.css": "/css/ui.css?id=1b4c4dfa67b7cfac4822"
}

View File

@ -16,5 +16,6 @@ Vue.directive("code", require("./directives/code"));
Vue.directive("alert", require("./directives/alert"));
Vue.component("datetime", require("./components/datetime"));
Vue.component("date", require("./components/date"));
Vue.component("swatch-picker", require("./components/swatch-picker"));
require('flatpickr/dist/flatpickr.css');
require('flatpickr/dist/flatpickr.css');

View File

@ -0,0 +1,25 @@
<template>
<span>
<swatches v-model="colorModel" v-bind="$attrs"></swatches>
<input type="hidden" :name="inputName" :value="colorModel"/>
</span>
</template>
<script>
import "vue-swatches/dist/vue-swatches.min.css"
import Swatches from 'vue-swatches'
export default {
components: { Swatches },
props: ['inputName', 'color'],
data () {
return {
colorModel: this.color
}
},
};
</script>

View File

@ -876,4 +876,8 @@ modal {
background-image: none;
}
}
}
.vue-swatches__trigger {
border: 1px solid #d3d3d3;
}