Create initial version of repeater form widget
This commit is contained in:
parent
24cbeee959
commit
022eb4d673
|
|
@ -66,6 +66,10 @@ class ServiceProvider extends ModuleServiceProvider
|
|||
'label' => 'Record Finder',
|
||||
'code' => 'recordfinder'
|
||||
]);
|
||||
$manager->registerFormWidget('Backend\FormWidgets\Repeater', [
|
||||
'label' => 'Repeater',
|
||||
'code' => 'repeater'
|
||||
]);
|
||||
});
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
<?php namespace Backend\FormWidgets;
|
||||
|
||||
use Backend\Classes\FormField;
|
||||
use Backend\Classes\FormWidgetBase;
|
||||
|
||||
/**
|
||||
* Repeater Form Widget
|
||||
*/
|
||||
class Repeater extends FormWidgetBase
|
||||
{
|
||||
const INDEX_PREFIX = '___index_';
|
||||
|
||||
//
|
||||
// Configurable properties
|
||||
//
|
||||
|
||||
/**
|
||||
* @var array Form field configuration
|
||||
*/
|
||||
public $form;
|
||||
|
||||
//
|
||||
// Object properties
|
||||
//
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected $defaultAlias = 'repeater';
|
||||
|
||||
protected $indexCount = 0;
|
||||
|
||||
protected $formWidgets = [];
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
$this->fillFromConfig([
|
||||
'form',
|
||||
]);
|
||||
|
||||
$this->processExistingItems();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$this->prepareVars();
|
||||
return $this->makePartial('repeater');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the form widget view data
|
||||
*/
|
||||
public function prepareVars()
|
||||
{
|
||||
$this->vars['indexName'] = self::INDEX_PREFIX.$this->formField->getName(false).'[]';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function loadAssets()
|
||||
{
|
||||
$this->addCss('css/repeater.css', 'core');
|
||||
$this->addJs('js/repeater.js', 'core');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getSaveValue($value)
|
||||
{
|
||||
return array_values($value);
|
||||
}
|
||||
|
||||
protected function processExistingItems()
|
||||
{
|
||||
$itemIndexes = post(self::INDEX_PREFIX.$this->formField->getName(false), $this->getLoadValue());
|
||||
|
||||
if (!is_array($itemIndexes)) return;
|
||||
|
||||
foreach ($itemIndexes as $itemIndex) {
|
||||
$this->makeFormWidget($itemIndex);
|
||||
$this->indexCount = max((int) $itemIndex, $this->indexCount);
|
||||
}
|
||||
}
|
||||
|
||||
protected function makeFormWidget($index = 0)
|
||||
{
|
||||
$config = $this->makeConfig($this->form);
|
||||
$config->model = $this->model;
|
||||
$config->alias = $this->alias . 'Form'.$index;
|
||||
$config->arrayName = $this->formField->getName().'['.$index.']';
|
||||
|
||||
$widget = $this->makeWidget('Backend\Widgets\Form', $config);
|
||||
$widget->bindToController();
|
||||
|
||||
return $this->formWidgets[$index] = $widget;
|
||||
}
|
||||
|
||||
public function onAddItem()
|
||||
{
|
||||
$this->indexCount++;
|
||||
|
||||
$this->prepareVars();
|
||||
$this->vars['widget'] = $this->makeFormWidget($this->indexCount);
|
||||
$this->vars['indexValue'] = $this->indexCount;
|
||||
|
||||
$itemContainer = '@#'.$this->getId('items');
|
||||
return [$itemContainer => $this->makePartial('repeater_item')];
|
||||
}
|
||||
|
||||
public function onRemoveItem()
|
||||
{
|
||||
// Useful for deleting relations
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
.field-repeater {
|
||||
padding-top: 5px;
|
||||
}
|
||||
.field-repeater .field-repeater-item {
|
||||
position: relative;
|
||||
margin-left: 5px;
|
||||
padding-left: 15px;
|
||||
border-left: 1px solid #dbdee0;
|
||||
}
|
||||
.field-repeater .field-repeater-item:before {
|
||||
color: #bdc3c7;
|
||||
font-family: FontAwesome;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
text-decoration: inherit;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
*margin-right: .3em;
|
||||
content: "\f111";
|
||||
font-size: 8px;
|
||||
position: absolute;
|
||||
left: -4px;
|
||||
top: -2px;
|
||||
}
|
||||
.field-repeater .field-repeater-item .field-repeater-form {
|
||||
position: relative;
|
||||
top: -7px;
|
||||
}
|
||||
.field-repeater .field-repeater-item .repeater-item-handle {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: -10px;
|
||||
color: #bdc3c7;
|
||||
background: #f9f9f9;
|
||||
cursor: move;
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.5s;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.field-repeater .field-repeater-item .repeater-item-handle:hover {
|
||||
color: #999;
|
||||
}
|
||||
.field-repeater .field-repeater-item .repeater-item-remove {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: 0;
|
||||
z-index: 90;
|
||||
}
|
||||
.field-repeater .field-repeater-item .repeater-item-handle,
|
||||
.field-repeater .field-repeater-item .repeater-item-remove {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.field-repeater .field-repeater-item:hover .repeater-item-handle,
|
||||
.field-repeater .field-repeater-item:active .repeater-item-handle,
|
||||
.field-repeater .field-repeater-item:hover .repeater-item-remove,
|
||||
.field-repeater .field-repeater-item:active .repeater-item-remove {
|
||||
opacity: 1;
|
||||
}
|
||||
.field-repeater .field-repeater-add-item {
|
||||
position: relative;
|
||||
margin-top: 10px;
|
||||
margin-left: 20px;
|
||||
border: 2px dotted #e0e0e0;
|
||||
}
|
||||
.field-repeater .field-repeater-add-item:before {
|
||||
color: #bdc3c7;
|
||||
font-family: FontAwesome;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
text-decoration: inherit;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
*margin-right: .3em;
|
||||
content: "\f067";
|
||||
font-size: 16px;
|
||||
position: absolute;
|
||||
left: -23px;
|
||||
top: -11px;
|
||||
}
|
||||
.field-repeater .field-repeater-add-item > a {
|
||||
color: #bdc3c7;
|
||||
text-align: center;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
padding: 20px 10px;
|
||||
}
|
||||
.field-repeater .field-repeater-add-item:hover {
|
||||
border: 2px dotted rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.field-repeater .field-repeater-add-item:hover:before {
|
||||
color: #999;
|
||||
}
|
||||
.field-repeater .field-repeater-add-item:hover > a {
|
||||
color: #bdc3c7;
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Field Repeater plugin
|
||||
*
|
||||
* Data attributes:
|
||||
* - data-control="fieldrepeater" - enables the plugin on an element
|
||||
* - data-option="value" - an option with a value
|
||||
*
|
||||
* JavaScript API:
|
||||
* $('a#someElement').fieldRepeater({ option: 'value' })
|
||||
*
|
||||
* Dependences:
|
||||
* - Some other plugin (filename.js)
|
||||
*/
|
||||
|
||||
+function ($) { "use strict";
|
||||
|
||||
// FIELD REPEATER CLASS DEFINITION
|
||||
// ============================
|
||||
|
||||
var Repeater = function(element, options) {
|
||||
this.options = options
|
||||
this.$el = $(element)
|
||||
|
||||
// Init
|
||||
this.init()
|
||||
}
|
||||
|
||||
Repeater.DEFAULTS = {
|
||||
option: 'default'
|
||||
}
|
||||
|
||||
Repeater.prototype.init = function() {
|
||||
// Public properties
|
||||
this.something = false
|
||||
|
||||
// Init with no arguments
|
||||
}
|
||||
|
||||
Repeater.prototype.someFunction = function() {
|
||||
// Do stuff
|
||||
}
|
||||
|
||||
// FIELD REPEATER PLUGIN DEFINITION
|
||||
// ============================
|
||||
|
||||
var old = $.fn.fieldRepeater
|
||||
|
||||
$.fn.fieldRepeater = function (option) {
|
||||
var args = Array.prototype.slice.call(arguments, 1), result
|
||||
this.each(function () {
|
||||
var $this = $(this)
|
||||
var data = $this.data('oc.repeater')
|
||||
var options = $.extend({}, Repeater.DEFAULTS, $this.data(), typeof option == 'object' && option)
|
||||
if (!data) $this.data('oc.repeater', (data = new Repeater(this, options)))
|
||||
if (typeof option == 'string') result = data[option].apply(data, args)
|
||||
if (typeof result != 'undefined') return false
|
||||
})
|
||||
|
||||
return result ? result : this
|
||||
}
|
||||
|
||||
$.fn.fieldRepeater.Constructor = Repeater
|
||||
|
||||
// FIELD REPEATER NO CONFLICT
|
||||
// =================
|
||||
|
||||
$.fn.fieldRepeater.noConflict = function () {
|
||||
$.fn.fieldRepeater = old
|
||||
return this
|
||||
}
|
||||
|
||||
// FIELD REPEATER DATA-API
|
||||
// ===============
|
||||
|
||||
$(document).on('click.oc.myplugin', '[data-control="fieldrepeater"]', function() {
|
||||
$(this).fieldRepeater()
|
||||
});
|
||||
|
||||
}(window.jQuery);
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
@import "../../../../assets/less/core/boot.less";
|
||||
|
||||
.field-repeater {
|
||||
|
||||
padding-top: 5px;
|
||||
|
||||
.field-repeater-item {
|
||||
position: relative;
|
||||
margin-left: 5px;
|
||||
padding-left: 15px;
|
||||
border-left: 1px solid #dbdee0;
|
||||
|
||||
&:before {
|
||||
color: #bdc3c7;
|
||||
.icon(@circle);
|
||||
font-size: 8px;
|
||||
position: absolute;
|
||||
left: -4px;
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
.field-repeater-form {
|
||||
position: relative;
|
||||
top: -7px;
|
||||
}
|
||||
|
||||
.repeater-item-handle {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: -10px;
|
||||
color: #bdc3c7;
|
||||
background: @color-body-bg;
|
||||
cursor: move;
|
||||
opacity: 0;
|
||||
.transition(~'opacity 0.5s');
|
||||
&:hover {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.repeater-item-remove {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: 0;
|
||||
z-index: 90;
|
||||
}
|
||||
|
||||
.repeater-item-handle,
|
||||
.repeater-item-remove {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&:hover, &:active {
|
||||
.repeater-item-handle,
|
||||
.repeater-item-remove {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.field-repeater-add-item {
|
||||
position: relative;
|
||||
margin-top: 10px;
|
||||
margin-left: 20px;
|
||||
border: 2px dotted #e0e0e0;
|
||||
|
||||
&:before {
|
||||
color: #bdc3c7;
|
||||
.icon(@plus);
|
||||
font-size: 16px;
|
||||
position: absolute;
|
||||
left: -23px;
|
||||
top: -11px;
|
||||
}
|
||||
|
||||
> a {
|
||||
color: #bdc3c7;
|
||||
text-align: center;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
padding: 20px 10px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: 2px dotted rgba(0,0,0,.1);
|
||||
&:before {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
> a { color: #bdc3c7;}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<div class="field-repeater">
|
||||
|
||||
<div id="<?= $this->getId('items') ?>" class="field-repeater-items">
|
||||
</div>
|
||||
|
||||
<div class="field-repeater-add-item loading-indicator-container indicator-center">
|
||||
<a
|
||||
href="javascript:;"
|
||||
data-load-indicator
|
||||
data-request="<?= $this->getEventHandler('onAddItem') ?>">
|
||||
Add new item
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<div class="field-repeater-item">
|
||||
|
||||
<div class="repeater-item-handle">
|
||||
<i class="icon-bars"></i>
|
||||
</div>
|
||||
|
||||
<div class="repeater-item-remove">
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
aria-label="Remove"
|
||||
data-request="<?= $this->getEventHandler('onRemoveItem') ?>"
|
||||
data-request-data="'index': '<?= $indexValue ?>'"
|
||||
data-request-success="$el.closest('.field-repeater-item').remove()"
|
||||
data-request-confirm="Are you sure?">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="field-repeater-form">
|
||||
<?= $widget->render([
|
||||
'useContainer' => false
|
||||
]) ?>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="<?= $indexName ?>" value="<?= $indexValue ?>" />
|
||||
|
||||
</div>
|
||||
Loading…
Reference in New Issue