So I made it happen, this is my first beta of a fully customizable input type, which is mostly a copy and past aproach from the repeater, but it works
The Class:
<?php
class CustomInput extends cbBaseInput {
public $defaultIcon = 'chunk_A';
public $defaultTpl = '<div class="custominput">
</div>';
/**
* @return string
*/
public function getName() {
return 'Custom Input';
}
public function getDescription() {
return 'Create your own cbField with any inputs that you need.';
}
/**
* @return array
*/
public function getJavaScripts() {
$assetsUrl = $this->modx->getOption('gpceeassets.assets_url', null, MODX_ASSETS_URL . 'gpceeassets/');
return array(
$assetsUrl . 'cbinputs/js/custominput.js',
);
}
/**
* @return array
*/
public function getTemplates()
{
$tpls = array();
// Grab the template from a .tpl file
$corePath = $this->modx->getOption('gpceeassets.core_path', null, MODX_ASSETS_PATH . 'gpceeassets/');
$template = file_get_contents($corePath . 'cbinputs/templates/custominput.tpl');
// Wrap the template, giving the input a reference of "my_awesome_input", and
// add it to the returned array.
$tpls[] = $this->contentBlocks->wrapInputTpl('custominput', $template);
// Grab the template from a .tpl file
$corePath = $this->modx->getOption('gpceeassets.core_path', null, MODX_ASSETS_PATH . 'gpceeassets/');
$template = file_get_contents($corePath . 'cbinputs/templates/custominput-row.tpl');
// Wrap the template, giving the input a reference of "my_awesome_input", and
// add it to the returned array.
$tpls[] = '<script type="text/x-tmpl" id="contentblocks-field-custominput-row">'.$template.'</script>';
return $tpls;
}
public function getFieldProperties()
{
return array(
array(
'key' => 'group',
'fieldLabel' => $this->modx->lexicon('contentblocks.repeater.group'),
'xtype' => 'fieldgroup', // special type which creates a grid of fields under the current field
'description' => $this->modx->lexicon('contentblocks.repeater.group.description')
),
);
}
/**
* Similar to {@see self::getFieldProperties}, except this is used when creating subfields to modify the edit panel.
*
* @return array
*/
public function getParentProperties()
{
return array(
array(
'key' => 'key',
'fieldLabel' => $this->modx->lexicon('contentblocks.repeater.key'),
'xtype' => 'textfield',
'default' => '',
'description' => $this->modx->lexicon('contentblocks.repeater.key.description'),
'allowBlank' => false
),
array(
'key' => 'width',
'fieldLabel' => $this->modx->lexicon('contentblocks.width'),
'xtype' => 'numberfield',
'description' => $this->modx->lexicon('contentblocks.width.description'),
'allowBlank' => false
),
);
}
/**
* Generate the HTML for the repeater
*
* @param cbField $field
* @param array $data
* @return mixed
*/
public function process(cbField $field, array $data = array())
{
// Ensure inputs are loaded
//$this->contentBlocks->loadInputs();
// Grab the group fields and template
$group = $this->getGroup($field);
$tpl = $field->get('template');
// Array to store the output in
$rowsOutput = array();
// Loop over each row
$idx = 0;
foreach ($data['rows'] as $row) {
$idx++;
$data['idx'] = $idx;
$rowsOutput[] = $this->processRow($row, $group, $data, $tpl);
}
$data['total'] = count($data['rows']);
// Glue individual rows together
$separator = $field->get('row_separator');
if (empty($separator)) $separator = "\n\n";
$rowsOutput = implode($separator, $rowsOutput);
// Throw it in a wrapper template with [[+rows]]
$wrapperTpl = $field->get('wrapper_template');
if (empty($wrapperTpl)) $wrapperTpl = '[[+rows]]';
$data['rows'] = $rowsOutput;
// Return the final output. Whew.
return $this->contentBlocks->parse($wrapperTpl, $data);
}
/**
* Processes a single row of the repeater
*
* @param array $row
* @param cbField[] $group
* @param array $data
* @param string $tpl
* @return mixed
*/
public function processRow($row, $group, $data, $tpl = '') {
// For each row, we store placeholders in the $rowFields array
$rowFields = array();
// Loop over each key in the row and its value (array)
foreach ($row as $key => $value) {
$field = $group[$key];
if ($field instanceof cbField) {
$inputType = $field->get('input');
// If it's a known input, we try to parse it
if (isset($this->contentBlocks->inputs[$inputType])) {
/** @var cbBaseInput $input */
$input = $this->contentBlocks->inputs[$inputType];
// Attempt to parse the data through that input type
try {
$parseData = array_merge($data, $field->toArray(), $value);
$value = $input->process($field, $parseData);
} catch (Exception $e) {
$value = 'Error parsing ' . $inputType . ': ' . $e->getMessage();
}
} else {
$value = 'Input ' . htmlentities($inputType, ENT_QUOTES, 'UTF-8') . ' not found.';
}
}
else {
$value = 'Could not find subfield with key "' . $key . '" in the group"';
}
// Set the value as placeholder in $rowFields
$rowFields[$key] = $value;
}
// Grab the $data and the $rowFields together so we have settings and everything
$phs = array_merge($data, $rowFields);
// Parse this row of fields
return $this->contentBlocks->parse($tpl, $phs);
}
/**
* Gets the repeater sub fields as key => cbField array
*
* @param cbField $field
* @return cbField[]
*/
public function getGroup(cbField $field)
{
$group = array();
$fields = $field->getSubfields();
foreach ($fields as $fld) {
$key = $fld->getParentProperty('key');
if (!empty($key)) {
$group[$key] = $fld;
}
}
return $group;
}
}
The Templates:
custominput
<div class="contentblocks-field contentblocks-field-custominput">
<div class="contentblocks-field-actions"></div>
<label><a class="contentblocks-collapser contentblocks-custominput-collapser contentblocks-custominput-expanded" href="javascript:void(0)">-</a> {%=o.name%}</label>
<ul class="contentblocks-custominput-wrapper"></ul>
</div>
custominput-row
<li class="contentblocks-repeater-row contentblocks-custominput-row">
<div class="contentblocks-repeater-item-actions">
</div>
<ul class="contentblocks-repeater-item-wrapper"></ul>
</li>
The Javascript:
// Wrap your stuff in this module pattern for dependency injection
(function ($, ContentBlocks) {
// Add your custom input to the fieldTypes object as a function
// The dom variable contains the injected field (from the template)
// and the data variable contains field information, properties etc.
ContentBlocks.fieldTypes.custominput = function(dom, data) {
var wrapper = dom.find('.contentblocks-custominput-wrapper'),
emptyRowTmpl = tmpl('contentblocks-field-custominput-row');
// emptyRowTmpl = tmpl('contentblocks-repeater-item');
var input = {
// Some optional variables can be defined here
};
// Do something when the input is being loaded
input.init = function() {
console.log('init', dom, data);
if (data.rows && $.isArray(data.rows) && data.rows.length > 0) {
$.each(data.rows, function(i, rowData) {
input.addRow(rowData);
console.log('rowData',rowData);
});
} else {
input.addRow(data.subfields);
}
dom.on('click', '.contentblocks-custominput-expanded', function() {
$(this).removeClass('contentblocks-custominput-expanded').addClass('contentblocks-custominput-collapsed').text('+').closest('.contentblocks-field-custominput').children('.contentblocks-custominput-wrapper').slideUp(300, function() {
ContentBlocks.fixColumnHeights();
});
}).on('click', '.contentblocks-custominput-collapsed', function() {
$(this).removeClass('contentblocks-custominput-collapsed').addClass('contentblocks-custominput-expanded').text('-').closest('.contentblocks-field-custominput').children('.contentblocks-custominput-wrapper').slideDown(300, function() {
ContentBlocks.fixColumnHeights();
});
});
};
input.addRow = function(rowData, target) {
rowData = rowData || {};
// Generate the empty row wrapper, and inject it into the page
var newRow = $(emptyRowTmpl({}));
if (!target || target == 'bottom') {
wrapper.append(newRow);
}
else {
wrapper.prepend(newRow);
}
// Loop over each of the subfields to generate them individually, added tp the wrapper.
$.each(data.subfields, function(idx, fld) {
// First make sure we combine whatever values we have available
var values = $.extend(true, {}, fld);
if (rowData[fld.parent_properties.key]) {
values = $.extend(true, {}, values, rowData[fld.parent_properties.key]);
}
// Call the ContentBlocks.addField API to create the subfield and have it injected into the canvas
var generatedField = input.addField(newRow.children('ul'), fld.id, values, 'bottom');
// Add a class for the width
if (values.parent_properties.width > 0) {
generatedField.css('width', values.parent_properties.width + '%');
}
// Keep track of the repeater-key value so we can link it back together later
generatedField.data('custominput-key', values.parent_properties.key);
});
// Ensure the various columns are kept nicely aligned
ContentBlocks.fixColumnHeights();
};
// Get the data from this input, it has to be a simple object.
input.getData = function() {
var rows = [];
wrapper.children('.contentblocks-custominput-row').each(function(idx, row){
var rowFields = {};
row = $(row);
row.children('ul').children('li').each(function(fldIdx, field){
field = $(field);
var fldId = field.attr('id'),
input = ContentBlocks.generatedContentFields[fldId],
repeaterKey = field.data('custominput-key');
if (input) {
var value = input.getData();
value.fieldId = fldId;
rowFields[repeaterKey] = value;
}
else {
if (console) console.error('input not found with id', fldId);
}
});
rows.push(rowFields);
});
console.log('rows',rows);
return {
rows: rows
}
};
input.addField = function (container, fldId, placeholders, position) {
var fieldType = (ContentBlocksFields['_'+fldId]) ? $.extend(true, {}, ContentBlocksFields['_'+fldId]) : {
input: (window.Ext && Ext.getCmp && Ext.getCmp('modx-resource-richtext') && Ext.getCmp('modx-resource-richtext').getValue()) ? 'richtext' : 'textarea',
name: 'Content'
};
position = (position || position === 0) ? position : 'bottom';
var settings = placeholders.settings;
vcJquery.extend(placeholders, fieldType);
// Add a unique ID
ContentBlocks.fldId++;
placeholders.generated_id = 'contentblocks-field-' + ContentBlocks.fldId;
placeholders.field = fldId;
// Build the field from its input template
try {
var generatedField = tmpl('contentblocks-field-' + fieldType.input, placeholders);
} catch (e) {
ContentBlocks.alert(_('contentblocks.error.input_not_found.message', {input: fieldType.input}) , _('contentblocks.error.input_not_found'));
if (window.console) {
console.error('Error initialising input type "' + fieldType.input + '": ' + e.message);
console.log(e.stack);
}
return;
}
// Inject it top of bottom of the stack
if (position == 'top') container.prepend(generatedField);
else if (position == 'bottom') container.append(generatedField);
else container.children('li').eq(position).after(generatedField);
// Get the generated DOM as jQuery object
var dom = $('#' + placeholders.generated_id);
// Add the "delete field" button
var allFieldSettings = ContentBlocks.getSettingFields(fieldType.settings);
if(allFieldSettings.modalFields.length) {
var modalFieldSettingsHTML = tmpl('contentblocks-button-field-settings', {});
}
if (settings) {
dom.data('settings', Ext.encode(settings));
}
// Create a new instance of the input js
if (typeof ContentBlocks.fieldTypes[fieldType.input] !== 'function') {
ContentBlocks.alert('Uh oh, could not load the input type ' + fieldType.input);
return;
}
var input = ContentBlocks.fieldTypes[fieldType.input](dom, placeholders);
input.id = placeholders.generated_id;
if (input.init) input.init();
input.fieldId = fldId;
// Store the input
ContentBlocks.generatedContentFields[placeholders.generated_id] = input;
if (allFieldSettings.exposedFields.length || allFieldSettings.exposedSettingFields.length) {
ContentBlocks.addExposedFieldSettings(dom);
}
container.removeClass('contentblocks-column-is-empty');
ContentBlocks.fixColumnHeights();
return dom;
};
// Always return the input variable.
return input;
}
})(vcJquery, ContentBlocks);
The Plugin:
<?php
/**
* @var modX $modx
* @var ContentBlocks $contentBlocks
* @var array $scriptProperties
*/
if ($modx->event->name == 'ContentBlocks_RegisterInputs') {
// Load your own class. No need to require cbBaseInput, that's already loaded.
$path = $modx->getOption('gpceeassets.core_path', null, MODX_ASSETS_PATH . 'gpceeassets/');
require_once($path . 'cbinputs/custominput.class.php');
// Create an instance of your input type, passing the $contentBlocks var
$instance = new CustomInput($contentBlocks);
// Pass back your input reference as key, and the instance as value
$modx->event->output(array(
'custominput' => $instance
));
}
You will need to modify some stuff, but you should get it to work easily.
I hope it helps someone