Merge branch 'develop' of github.com:octobercms/october into develop

This commit is contained in:
alekseybobkov 2014-07-16 20:50:16 +11:00
commit 48160088f2
26 changed files with 621 additions and 432 deletions

View File

@ -1,5 +1,6 @@
<?php namespace Backend\FormWidgets;
use Backend\Widgets\Grid;
use Backend\Classes\FormWidgetBase;
use System\Classes\ApplicationException;
@ -7,15 +8,6 @@ use System\Classes\ApplicationException;
* Grid
* Renders a grid field.
*
* Supported options:
*
* - allowInsert
* - autoInsertRows
* - allowRemove
* - allowImport
* - allowExport
* - exportFileName
*
* @package october\backend
* @author Alexey Bobkov, Samuel Georges
*/
@ -26,23 +18,24 @@ class DataGrid extends FormWidgetBase
*/
public $defaultAlias = 'datagrid';
/**
* @var array Grid columns
*/
protected $columns = [];
/**
* @var string Grid size
*/
protected $size = 'large';
/**
* @var Backend\Widgets\Grid Grid widget
*/
protected $grid;
/**
* {@inheritDoc}
*/
public function init()
{
$this->columns = $this->getConfig('columns', []);
$this->size = $this->getConfig('size', $this->size);
$this->grid = $this->makeGridWidget();
$this->grid->bindToController();
}
/**
@ -59,125 +52,10 @@ class DataGrid extends FormWidgetBase
*/
public function prepareVars()
{
$this->vars['grid'] = $this->grid;
$this->vars['name'] = $this->formField->getName();
$this->vars['columnHeaders'] = $this->getColumnHeaders();
$this->vars['columnDefinitions'] = $this->getColumnDefinitions();
$this->vars['columnWidths'] = $this->getColumnWidths();
$this->vars['size'] = $this->size;
$this->vars['toolbarWidget'] = $this->makeToolbarWidget();
$this->vars['value'] = json_encode($this->model->{$this->columnName});
}
protected function makeToolbarWidget()
{
$toolbarConfig = $this->makeConfig([
'alias' => $this->alias . 'Toolbar',
'buttons' => $this->getViewPath('_toolbar.htm'),
]);
$toolbarWidget = $this->makeWidget('Backend\Widgets\Toolbar', $toolbarConfig);
return $toolbarWidget;
}
//
// Getters
//
protected function getColumnHeaders()
{
$headers = [];
foreach ($this->columns as $key => $column) {
$headers[] = isset($column['title']) ? $column['title'] : '???';
}
return $headers;
}
protected function getColumnWidths()
{
$widths = [];
foreach ($this->columns as $key => $column) {
$widths[] = isset($column['width']) ? $column['width'] : '0';
}
return $widths;
}
protected function getColumnDefinitions()
{
$definitions = [];
foreach ($this->columns as $key => $column) {
$item = [];
$item['data'] = $key;
if (isset($column['readOnly']))
$item['readOnly'] = $column['readOnly'];
$item = $this->evalColumnType($column, $item);
$definitions[] = $item;
}
return $definitions;
}
protected function evalColumnType($column, $item)
{
if (!isset($column['type']))
return $item;
switch ($column['type']) {
case 'number':
$item['type'] = 'numeric';
break;
case 'currency':
$item['type'] = 'numeric';
$item['format'] = '$0,0.00';
break;
case 'checkbox':
$item['type'] = 'checkbox';
break;
case 'autocomplete':
$item['type'] = 'autocomplete';
if (isset($column['options'])) $item['source'] = $column['options'];
if (isset($column['strict'])) $item['strict'] = $column['strict'];
break;
}
return $item;
}
//
// AJAX
//
public function onAutocomplete()
{
if (!$this->model->methodExists('getGridAutocompleteValues'))
throw new ApplicationException('Model :model does not contain a method getGridAutocompleteValues()');
$field = post('autocomplete_field');
$value = post('autocomplete_value');
$data = post('autocomplete_data', []);
$result = $this->model->getGridAutocompleteValues($field, $value, $data);
if (!is_array($result))
$result = [];
return ['result' => $result];
}
//
// Internals
//
/**
* {@inheritDoc}
*/
public function loadAssets()
{
$this->addCss('vendor/handsontable/jquery.handsontable.css', 'core');
$this->addCss('css/datagrid.css', 'core');
$this->addJs('vendor/handsontable/jquery.handsontable.js', 'core');
$this->addJs('js/datagrid.js', 'core');
$this->vars['value'] = json_encode($this->formField->value);
}
/**
@ -187,4 +65,28 @@ class DataGrid extends FormWidgetBase
{
return json_decode($value);
}
protected function makeGridWidget()
{
$config = $this->makeConfig((array) $this->config);
$config->dataLocker = '#'.$this->getId('dataLocker');
$grid = new Grid($this->controller, $config);
$grid->alias = $this->alias . 'Grid';
$grid->bindEvent('grid.autocomplete', [$this, 'getAutocompleteValues']);
return $grid;
}
public function getAutocompleteValues($field, $value, $data)
{
if (!$this->model->methodExists('getGridAutocompleteValues'))
throw new ApplicationException('Model :model does not contain a method getGridAutocompleteValues()');
$result = $this->model->getGridAutocompleteValues($field, $value, $data);
if (!is_array($result))
$result = [];
return $result;
}
}

View File

@ -2,16 +2,7 @@
id="<?= $this->getId() ?>"
class="field-datagrid size-<?= $size ?>">
<?= $toolbarWidget->render() ?>
<div
id="<?= $this->getId('grid') ?>"
style="width:100%"
class="control-datagrid"
data-control="datagrid"
data-data-locker="#<?= $this->getId('dataLocker') ?>"
data-autocomplete-handler="<?= $this->getEventHandler('onAutocomplete') ?>"
></div>
<?= $grid->render() ?>
<input
type="hidden"
@ -21,9 +12,3 @@
/>
</div>
<script>
$('#<?= $this->getId('grid') ?>')
.data('columns', <?= json_encode($columnDefinitions) ?>)
.data('columnHeaders', <?= json_encode($columnHeaders) ?>)
.data('columnWidths', <?= json_encode($columnWidths) ?>)
</script>

View File

@ -1,6 +0,0 @@
<div data-control="toolbar">
<a href="#" class="btn btn-sm btn-default oc-icon-plus-square" onclick="$(this).closest('.field-datagrid').find('[data-control=datagrid]').dataGrid('insertRow')">Insert Row</a>
<a href="#" class="btn btn-sm btn-default oc-icon-minus-square" onclick="$(this).closest('.field-datagrid').find('[data-control=datagrid]').dataGrid('removeRow')">Delete Row</a>
<!-- <a href="#" class="btn btn-sm btn-default oc-icon-floppy-o">Save as CSV</a> -->
<!-- <a href="#" class="btn btn-sm btn-default oc-icon-upload">Import CSV</a> -->
</div>

View File

@ -0,0 +1,226 @@
<?php namespace Backend\Widgets;
use Backend\Classes\WidgetBase;
/**
* Grid Widget
* Renders a search container used for viewing tabular data
*
* Supported options:
*
* - allowInsert
* - autoInsertRows
* - allowRemove
* - allowImport
* - allowExport
* - exportFileName
*
* @package october\backend
* @author Alexey Bobkov, Samuel Georges
*/
class Grid extends WidgetBase
{
/**
* {@inheritDoc}
*/
public $defaultAlias = 'grid';
/**
* @var array Grid columns
*/
protected $columns = [];
/**
* @var boolean Show data table header
*/
protected $showHeader = true;
/**
* @var boolean Insert row button
*/
protected $allowInsert = true;
/**
* @var boolean Delete row button
*/
protected $allowRemove = true;
/**
* @var boolean Disable the toolbar
*/
protected $disableToolbar = false;
/**
* @var mixed Array of data, or callable for data source.
*/
protected $dataSource;
/**
* @var string HTML element that can [re]store the grid data.
*/
protected $dataLocker;
/**
* Initialize the widget, called by the constructor and free from its parameters.
*/
public function init()
{
$this->columns = $this->getConfig('columns', []);
$this->showHeader = $this->getConfig('showHeader', $this->showHeader);
$this->allowInsert = $this->getConfig('allowInsert', $this->allowInsert);
$this->allowRemove = $this->getConfig('allowRemove', $this->allowRemove);
$this->disableToolbar = $this->getConfig('disableToolbar', $this->disableToolbar);
$this->dataLocker = $this->getConfig('dataLocker', $this->dataLocker);
$this->dataSource = $this->getConfig('dataSource', $this->dataSource);
}
/**
* Renders the widget.
*/
public function render()
{
$this->prepareVars();
return $this->makePartial('grid');
}
/**
* Prepares the view data
*/
public function prepareVars()
{
$this->vars['columnHeaders'] = $this->getColumnHeaders();
$this->vars['columnDefinitions'] = $this->getColumnDefinitions();
$this->vars['columnWidths'] = $this->getColumnWidths();
$this->vars['toolbarWidget'] = $this->makeToolbarWidget();
$this->vars['showHeader'] = $this->showHeader;
$this->vars['allowInsert'] = $this->allowInsert;
$this->vars['allowRemove'] = $this->allowRemove;
$this->vars['disableToolbar'] = $this->disableToolbar;
$this->vars['dataLocker'] = $this->dataLocker;
}
protected function makeToolbarWidget()
{
if ($this->disableToolbar)
return;
$toolbarConfig = $this->makeConfig([
'alias' => $this->alias . 'Toolbar',
'buttons' => $this->getViewPath('_toolbar.htm'),
]);
$toolbarWidget = $this->makeWidget('Backend\Widgets\Toolbar', $toolbarConfig);
$toolbarWidget->vars['allowInsert'] = $this->allowInsert;
$toolbarWidget->vars['allowRemove'] = $this->allowRemove;
return $toolbarWidget;
}
//
// AJAX
//
public function onAutocomplete()
{
$field = post('autocomplete_field');
$value = post('autocomplete_value');
$data = post('autocomplete_data', []);
$result = $this->fireEvent('grid.autocomplete', [$field, $value, $data], true);
return ['result' => $result];
}
public function onDataSource()
{
if ($this->dataLocker)
return;
$result = $this->dataSource;
return ['result' => $result];
}
//
// Getters
//
protected function getColumnHeaders()
{
if (!$this->showHeader)
return false;
$headers = [];
foreach ($this->columns as $key => $column) {
$headers[] = isset($column['title']) ? $column['title'] : '???';
}
return $headers;
}
protected function getColumnWidths()
{
$widths = [];
foreach ($this->columns as $key => $column) {
$widths[] = isset($column['width']) ? $column['width'] : '0';
}
return $widths;
}
protected function getColumnDefinitions()
{
$definitions = [];
foreach ($this->columns as $key => $column) {
$item = [];
$item['data'] = $key;
if (isset($column['readOnly']))
$item['readOnly'] = $column['readOnly'];
$item = $this->evalColumnType($column, $item);
$definitions[] = $item;
}
return $definitions;
}
protected function evalColumnType($column, $item)
{
if (!isset($column['type']))
return $item;
switch ($column['type']) {
case 'number':
$item['type'] = 'numeric';
break;
case 'currency':
$item['type'] = 'numeric';
$item['format'] = '$0,0.00';
break;
case 'checkbox':
$item['type'] = 'checkbox';
break;
case 'autocomplete':
$item['type'] = 'autocomplete';
if (isset($column['options'])) $item['source'] = $column['options'];
if (isset($column['strict'])) $item['strict'] = $column['strict'];
break;
}
return $item;
}
//
// Internals
//
/**
* {@inheritDoc}
*/
public function loadAssets()
{
$this->addCss('vendor/handsontable/jquery.handsontable.css', 'core');
$this->addCss('css/datagrid.css', 'core');
$this->addJs('vendor/handsontable/jquery.handsontable.js', 'core');
$this->addJs('js/datagrid.js', 'core');
}
}

View File

@ -83,6 +83,6 @@ class Toolbar extends WidgetBase
if (!isset($this->config->buttons))
return false;
return $this->controller->makePartial($this->config->buttons);
return $this->controller->makePartial($this->config->buttons, $this->vars);
}
}

View File

@ -32,17 +32,17 @@
colWidths: function(columnIndex) {
return self.staticWidths[columnIndex]
},
height: 400,
// height: 400,
columns: this.columns,
startRows: this.options.startRows,
minRows: this.options.minRows,
currentRowClassName: 'currentRow',
// rowHeaders: true,
// rowHeaders: false,
// manualColumnMove: true,
// manualRowMove: true,
fillHandle: false,
multiSelect: false,
removeRowPlugin: true
removeRowPlugin: this.options.allowRemove
}
if (this.options.autoInsertRows)
@ -68,6 +68,13 @@
delete handsontableOptions.data
}
}
else if (this.options.sourceHandler) {
$.request(self.options.sourceHandler, {
success: function(data, textStatus, jqXHR){
self.gridInstance.loadData(data.result)
}
})
}
this.$el.handsontable(handsontableOptions)
this.gridInstance = this.$el.handsontable('getInstance')
@ -138,6 +145,8 @@
columnWidths: null,
columns: null,
autocompleteHandler: null,
sourceHandler: null,
allowRemove: true,
confirmMessage: 'Are you sure?'
}

View File

@ -14,6 +14,7 @@
* - Removed contextMenu plugin
* - Use autocomplete plugin instead of typeahead
* - Custom checkboxes
* - Removed native scrollbars
*
*/
@ -2797,9 +2798,7 @@ DefaultSettings.prototype = {
allowInvalid: true,
invalidCellClassName: 'htInvalid',
fragmentSelection: false,
readOnly: false,
scrollbarModelV: 'dragdealer',
scrollbarModelH: 'dragdealer'
readOnly: false
};
$.fn.handsontable = function (action) {
@ -3021,8 +3020,6 @@ Handsontable.TableView = function (instance) {
data: instance.getDataAtCell,
totalRows: instance.countRows,
totalColumns: instance.countCols,
scrollbarModelV: this.settings.scrollbarModelV,
scrollbarModelH: this.settings.scrollbarModelH,
offsetRow: 0,
offsetColumn: 0,
width: this.getWidth(),
@ -3317,9 +3314,6 @@ Handsontable.TableView.prototype.maximumVisibleElementWidth = function (left) {
*/
Handsontable.TableView.prototype.maximumVisibleElementHeight = function (top) {
var rootHeight = this.wt.wtViewport.getWorkspaceHeight();
if(this.wt.isNativeScroll) {
return rootHeight;
}
return rootHeight - top;
};
@ -8004,25 +7998,6 @@ WalkontableScroll.prototype.scrollViewport = function (coords) {
, fixedRowsTop = this.instance.getSetting('fixedRowsTop')
, fixedColumnsLeft = this.instance.getSetting('fixedColumnsLeft');
if (this.instance.isNativeScroll) {
var TD = this.instance.wtTable.getCell(coords);
if (typeof TD === 'object') {
var offset = WalkontableDom.prototype.offset(TD);
var outerHeight = WalkontableDom.prototype.outerHeight(TD);
var scrollY = window.scrollY;
var clientHeight = document.documentElement.clientHeight;
if (outerHeight < clientHeight) {
if (offset.top < scrollY) {
TD.scrollIntoView(true);
}
else if (offset.top + outerHeight > scrollY + clientHeight) {
TD.scrollIntoView(false);
}
}
return;
}
}
if (coords[0] < 0 || coords[0] > totalRows - 1) {
throw new Error('row ' + coords[0] + ' does not exist');
}
@ -8437,129 +8412,10 @@ WalkontableScrollbarNative.prototype.destroy = function () {
this.$scrollHandler.off('scroll.walkontable');
};
///
var WalkontableVerticalScrollbarNative = function (instance) {
this.instance = instance;
this.type = 'vertical';
this.cellSize = 23;
this.init();
var that = this;
WalkontableCellStrategy.prototype.isLastIncomplete = function () { //monkey patch needed. In future get rid of it to improve performance
/*
* this.remainingSize = window viewport reduced by sum of all rendered cells (also those before the visible part)
* that.sumCellSizes(...) = sum of the sizes of cells that are before the visible part + 1 cell that is partially visible on top of the screen
*/
return this.remainingSize > that.sumCellSizes(that.offset, that.offset + that.curOuts + 1);
};
};
WalkontableVerticalScrollbarNative.prototype = new WalkontableScrollbarNative();
WalkontableVerticalScrollbarNative.prototype.getLastCell = function () {
return this.instance.getSetting('offsetRow') + this.instance.wtTable.tbodyChildrenLength - 1;
};
WalkontableVerticalScrollbarNative.prototype.getTableSize = function () {
return this.instance.wtDom.outerHeight(this.TABLE);
};
var partialOffset = 0;
WalkontableVerticalScrollbarNative.prototype.sumCellSizes = function (from, length) {
var sum = 0;
while (from < length) {
sum += this.instance.getSetting('rowHeight', from);
from++;
}
return sum;
};
WalkontableVerticalScrollbarNative.prototype.applyToDOM = function () {
var headerSize = this.instance.wtViewport.getColumnHeaderHeight();
this.fixedContainer.style.height = headerSize + this.sumCellSizes(0, this.total) + 'px';
this.fixed.style.top = this.measureBefore + 'px';
this.fixed.style.bottom = '';
};
WalkontableVerticalScrollbarNative.prototype.scrollTo = function (cell) {
var newY = this.tableParentOffset + cell * this.cellSize;
this.$scrollHandler.scrollTop(newY);
this.onScroll(newY);
};
WalkontableVerticalScrollbarNative.prototype.readSettings = function () {
var offset = this.instance.wtDom.offset(this.fixedContainer);
this.tableParentOffset = offset.top;
this.tableParentOtherOffset = offset.left;
this.windowSize = this.$scrollHandler.height();
this.windowScrollPosition = this.$scrollHandler.scrollTop();
this.offset = this.instance.getSetting('offsetRow');
this.total = this.instance.getSetting('totalRows');
};
///
var WalkontableHorizontalScrollbarNative = function (instance) {
this.instance = instance;
this.type = 'horizontal';
this.cellSize = 50;
this.init();
};
WalkontableHorizontalScrollbarNative.prototype = new WalkontableScrollbarNative();
WalkontableHorizontalScrollbarNative.prototype.getLastCell = function () {
return this.instance.wtTable.getLastVisibleColumn();
};
WalkontableHorizontalScrollbarNative.prototype.getTableSize = function () {
return this.instance.wtDom.outerWidth(this.TABLE);
};
WalkontableHorizontalScrollbarNative.prototype.applyToDOM = function () {
this.fixedContainer.style.paddingLeft = this.measureBefore + 'px';
this.fixedContainer.style.paddingRight = this.measureAfter + 'px';
};
WalkontableHorizontalScrollbarNative.prototype.scrollTo = function (cell) {
this.$scrollHandler.scrollLeft(this.tableParentOffset + cell * this.cellSize);
};
WalkontableHorizontalScrollbarNative.prototype.readSettings = function () {
var offset = this.instance.wtDom.offset(this.fixedContainer);
this.tableParentOffset = offset.left;
this.tableParentOtherOffset = offset.top;
this.windowSize = this.$scrollHandler.width();
this.windowScrollPosition = this.$scrollHandler.scrollLeft();
this.offset = this.instance.getSetting('offsetColumn');
this.total = this.instance.getSetting('totalColumns');
};
function WalkontableScrollbars(instance) {
if(instance.getSetting('scrollbarModelV') === 'native') {
instance.update('scrollbarModelH', 'none');
}
switch (instance.getSetting('scrollbarModelV')) {
case 'dragdealer':
this.vertical = new WalkontableVerticalScrollbar(instance);
break;
case 'native':
this.vertical = new WalkontableVerticalScrollbarNative(instance);
break;
}
switch (instance.getSetting('scrollbarModelH')) {
case 'dragdealer':
this.horizontal = new WalkontableHorizontalScrollbar(instance);
break;
case 'native':
this.horizontal = new WalkontableHorizontalScrollbarNative(instance);
break;
}
this.vertical = new WalkontableVerticalScrollbar(instance);
this.horizontal = new WalkontableHorizontalScrollbar(instance);
}
WalkontableScrollbars.prototype.destroy = function () {
@ -8677,8 +8533,6 @@ function WalkontableSettings(instance, settings) {
//presentation mode
scrollH: 'auto', //values: scroll (always show scrollbar), auto (show scrollbar if table does not fit in the container), none (never show scrollbar)
scrollV: 'auto', //values: see above
scrollbarModelH: 'dragdealer', //values: dragdealer, native
scrollbarModelV: 'dragdealer', //values: dragdealer, native
stretchH: 'hybrid', //values: hybrid, all, last, none
currentRowClassName: null,
currentColumnClassName: null,
@ -8882,10 +8736,6 @@ function WalkontableTable(instance) {
this.columnFilter = new WalkontableColumnFilter();
this.verticalRenderReverse = false;
if (this.instance.getSetting('scrollbarModelV') === 'native' || this.instance.getSetting('scrollbarModelH') === 'native') {
this.instance.isNativeScroll = true;
}
}
WalkontableTable.prototype.refreshHiderDimensions = function () {
@ -8894,7 +8744,7 @@ WalkontableTable.prototype.refreshHiderDimensions = function () {
var spreaderStyle = this.spreader.style;
if ((height !== Infinity || width !== Infinity) && !this.instance.isNativeScroll) {
if (height !== Infinity || width !== Infinity) {
if (height === Infinity) {
height = this.instance.wtViewport.getWorkspaceActualHeight();
}
@ -8908,13 +8758,9 @@ WalkontableTable.prototype.refreshHiderDimensions = function () {
spreaderStyle.top = '0';
spreaderStyle.left = '0';
if (this.instance.getSetting('scrollbarModelV') === 'dragdealer') {
spreaderStyle.height = '4000px';
}
if (this.instance.getSetting('scrollbarModelH') === 'dragdealer') {
spreaderStyle.width = '4000px';
}
// For dragdealer
spreaderStyle.height = '4000px';
spreaderStyle.width = '4000px';
if (height < 0) { //this happens with WalkontableScrollbarNative and causes "Invalid argument" error in IE8
height = 0;
@ -8923,11 +8769,6 @@ WalkontableTable.prototype.refreshHiderDimensions = function () {
this.hiderStyle.height = height + 'px';
this.hiderStyle.width = width + 'px';
}
else {
spreaderStyle.position = 'relative';
spreaderStyle.width = 'auto';
spreaderStyle.height = 'auto';
}
};
WalkontableTable.prototype.refreshStretching = function () {
@ -8960,9 +8801,6 @@ WalkontableTable.prototype.refreshStretching = function () {
}
var containerHeightFn = function (cacheHeight) {
if (that.instance.isNativeScroll) {
return 2 * that.instance.wtViewport.getViewportHeight(cacheHeight);
}
return that.instance.wtViewport.getViewportHeight(cacheHeight);
};
@ -9075,10 +8913,6 @@ WalkontableTable.prototype.adjustColumns = function (TR, desiredCount) {
};
WalkontableTable.prototype.draw = function (selectionsOnly) {
if (this.instance.isNativeScroll) {
this.verticalRenderReverse = false; //this is only supported in dragdealer mode, not in native
}
this.rowFilter.readSettings(this.instance);
this.columnFilter.readSettings(this.instance);
@ -9180,7 +9014,6 @@ WalkontableTable.prototype._doDraw = function () {
}
if (first) {
// if (r === 0) {
first = false;
this.adjustAvailableNodes();
@ -9248,9 +9081,7 @@ WalkontableTable.prototype._doDraw = function () {
res = this.rowStrategy.add(r, TD, this.verticalRenderReverse);
if (res === false) {
if (!this.instance.isNativeScroll) {
this.rowStrategy.removeOutstanding();
}
this.rowStrategy.removeOutstanding();
}
if (this.rowStrategy.isLastIncomplete()) {
@ -9368,10 +9199,6 @@ WalkontableTable.prototype.refreshSelections = function (selectionsOnly) {
*
*/
WalkontableTable.prototype.getCell = function (coords) {
if (this.instance.isNativeScroll) {
return this.instance.wtTable.TBODY.querySelectorAll('[data-row="' + coords[0] + '"][data-column="' + coords[1] + '"]')[0];
}
if (this.isRowBeforeViewport(coords[0])) {
return -1; //row before viewport
}
@ -9425,21 +9252,11 @@ WalkontableTable.prototype.isColumnAfterViewport = function (c) {
};
WalkontableTable.prototype.isRowInViewport = function (r) {
if (this.instance.isNativeScroll) {
return !!this.instance.wtTable.TBODY.querySelectorAll('[data-row="' + r + '"]')[0];
}
else {
return (!this.isRowBeforeViewport(r) && !this.isRowAfterViewport(r));
}
return (!this.isRowBeforeViewport(r) && !this.isRowAfterViewport(r));
};
WalkontableTable.prototype.isColumnInViewport = function (c) {
if (this.instance.isNativeScroll) {
return !!this.instance.wtTable.TBODY.querySelectorAll('[data-column="' + c + '"]')[0];
}
else {
return (!this.isColumnBeforeViewport(c) && !this.isColumnAfterViewport(c));
}
return (!this.isColumnBeforeViewport(c) && !this.isColumnAfterViewport(c));
};
WalkontableTable.prototype.isLastRowFullyVisible = function () {
@ -9453,22 +9270,10 @@ WalkontableTable.prototype.isLastColumnFullyVisible = function () {
function WalkontableViewport(instance) {
this.instance = instance;
this.resetSettings();
if (this.instance.isNativeScroll) {
var that = this;
that.clientHeight = document.documentElement.clientHeight; //browser viewport height
$(window).on('resize', function () {
that.clientHeight = document.documentElement.clientHeight;
});
}
}
// Used by scrollbar
WalkontableViewport.prototype.getWorkspaceHeight = function (proposedHeight) {
if (this.instance.isNativeScroll) {
return this.clientHeight;
}
var height = this.instance.getSetting('height');
if (height === Infinity || height === void 0 || height === null || height < 1) {
@ -9584,10 +9389,6 @@ WalkontableViewport.prototype.resetSettings = function () {
this.columnHeaderHeight = NaN;
};
function WalkontableWheel(instance) {
if (instance.isNativeScroll) {
return;
}
//spreader === instance.wtTable.TABLE.parentNode
$(instance.wtTable.spreader).on('mousewheel', function (event, delta, deltaX, deltaY) {
if (!deltaX && !deltaY && delta) { //we are in IE8, see https://github.com/brandonaaron/jquery-mousewheel/issues/53

View File

@ -0,0 +1,23 @@
<div class="datagrid-widget">
<?php if (!$disableToolbar): ?>
<?= $toolbarWidget->render() ?>
<?php endif ?>
<div
id="<?= $this->getId('grid') ?>"
style="width:100%"
class="control-datagrid"
data-control="datagrid"
data-allow-remove="<?= $allowRemove ? 'true' : 'false' ?>"
<?php if ($dataLocker): ?>data-data-locker="<?= $dataLocker ?>"<?php endif ?>
data-autocomplete-handler="<?= $this->getEventHandler('onAutocomplete') ?>"
data-source-handler="<?= $this->getEventHandler('onDataSource') ?>"
></div>
</div>
<script>
$('#<?= $this->getId('grid') ?>')
.data('columns', <?= json_encode($columnDefinitions) ?>)
.data('columnHeaders', <?= json_encode($columnHeaders) ?>)
.data('columnWidths', <?= json_encode($columnWidths) ?>)
</script>

View File

@ -0,0 +1,20 @@
<div data-control="toolbar">
<?php if ($allowInsert): ?>
<a
href="javascript:;"
class="btn btn-sm btn-default oc-icon-plus-square"
onclick="$(this).closest('.datagrid-widget').find('[data-control=datagrid]').dataGrid('insertRow')">
Insert Row
</a>
<?php endif ?>
<?php if ($allowRemove): ?>
<a
href="javascript:;"
class="btn btn-sm btn-default oc-icon-minus-square"
onclick="$(this).closest('.datagrid-widget').find('[data-control=datagrid]').dataGrid('removeRow')">
Delete Row
</a>
<?php endif ?>
<!-- <a href="#" class="btn btn-sm btn-default oc-icon-floppy-o">Save as CSV</a> -->
<!-- <a href="#" class="btn btn-sm btn-default oc-icon-upload">Import CSV</a> -->
</div>

View File

@ -4,7 +4,6 @@ use Lang;
use Backend;
use BackendMenu;
use BackendAuth;
use System\Classes\MarkupManager;
use Backend\Classes\WidgetManager;
use October\Rain\Support\ModuleServiceProvider;
@ -93,48 +92,6 @@ class ServiceProvider extends ModuleServiceProvider
]);
});
/*
* Register markup tags
*/
MarkupManager::instance()->registerCallback(function($manager){
$manager->registerFunctions([
// Global helpers
'post' => 'post',
// Form helpers
'form_ajax' => ['Form', 'ajax'],
'form_open' => ['Form', 'open'],
'form_close' => ['Form', 'close'],
'form_token' => ['Form', 'token'],
'form_session_key' => ['Form', 'sessionKey'],
'form_token' => ['Form', 'token'],
'form_model' => ['Form', 'model'],
'form_label' => ['Form', 'label'],
'form_text' => ['Form', 'text'],
'form_password' => ['Form', 'password'],
'form_checkbox' => ['Form', 'checkbox'],
'form_radio' => ['Form', 'radio'],
'form_file' => ['Form', 'file'],
'form_select' => ['Form', 'select'],
'form_select_range' => ['Form', 'selectRange'],
'form_select_month' => ['Form', 'selectMonth'],
'form_submit' => ['Form', 'submit'],
'form_macro' => ['Form', '__call'],
'form_value' => ['Form', 'value'],
]);
$manager->registerFilters([
// String helpers
'slug' => ['Str', 'slug'],
'plural' => ['Str', 'plural'],
'singular' => ['Str', 'singular'],
'finish' => ['Str', 'finish'],
'snake' => ['Str', 'snake'],
'camel' => ['Str', 'camel'],
'studly' => ['Str', 'studly'],
]);
});
/*
* Register widgets
*/

View File

@ -4,6 +4,7 @@ use URL;
use File;
use Lang;
use Cache;
use Route;
use Config;
use Request;
use Response;
@ -193,7 +194,13 @@ class CombineAssets
*/
protected function getCombinedUrl($outputFilename = 'undefined.css')
{
return URL::action('Cms\Classes\Controller@combine', [$outputFilename], false);
$combineAction = 'Cms\Classes\Controller@combine';
$actionExists = Route::getRoutes()->getByAction($combineAction) !== null;
if ($actionExists)
return URL::action($combineAction, [$outputFilename], false);
else
return Request::getBasePath().'/combine/'.$outputFilename;
}
/**

View File

@ -10,6 +10,7 @@ use BackendAuth;
use Twig_Environment;
use Twig_Loader_String;
use System\Classes\ErrorHandler;
use System\Classes\MarkupManager;
use System\Classes\PluginManager;
use System\Classes\SettingsManager;
use System\Twig\Engine as TwigEngine;
@ -179,6 +180,44 @@ class ServiceProvider extends ModuleServiceProvider
]);
});
/*
* Register markup tags
*/
MarkupManager::instance()->registerCallback(function($manager){
$manager->registerFunctions([
// Functions
'post' => 'post',
'link_to' => 'link_to',
'link_to_asset' => 'link_to_asset',
'link_to_route' => 'link_to_route',
'link_to_action' => 'link_to_action',
'asset' => 'asset',
'action' => 'action',
'url' => 'url',
'route' => 'route',
'secure_url' => 'secure_url',
'secure_asset' => 'secure_asset',
// Classes
'str_*' => ['Str', '*'],
'url_*' => ['URL', '*'],
'html_*' => ['HTML', '*'],
'form_*' => ['Form', '*'],
'form_macro' => ['Form', '__call'],
]);
$manager->registerFilters([
// Classes
'slug' => ['Str', 'slug'],
'plural' => ['Str', 'plural'],
'singular' => ['Str', 'singular'],
'finish' => ['Str', 'finish'],
'snake' => ['Str', 'snake'],
'camel' => ['Str', 'camel'],
'studly' => ['Str', 'studly'],
]);
});
/*
* Register settings
*/

View File

@ -1,5 +1,10 @@
<?php namespace System\Classes;
use Str;
use Twig_TokenParser;
use Twig_SimpleFilter;
use Twig_SimpleFunction;
use System\Classes\ApplicationException;
use System\Classes\PluginManager;
/**
@ -189,4 +194,126 @@ class MarkupManager
return $this->listExtensions(self::EXTENSION_TOKEN_PARSER);
}
/**
* Makes a set of Twig functions for use in a twig extension.
* @param array $functions Current collection
* @return array
*/
public function makeTwigFunctions($functions = [])
{
if (!is_array($functions))
$functions = [];
foreach ($this->listFunctions() as $name => $callable) {
/*
* Handle a wildcard function
*/
if (strpos($name, '*') !== false && $this->isWildCallable($callable)) {
$callable = function($name) use ($callable) {
$arguments = array_slice(func_get_args(), 1);
$method = $this->isWildCallable($callable, Str::camel($name));
return call_user_func_array($method, $arguments);
};
}
if (!is_callable($callable))
throw new ApplicationException(sprintf('The markup function for %s is not callable.', $name));
$functions[] = new Twig_SimpleFunction($name, $callable, ['is_safe' => ['html']]);
}
return $functions;
}
/**
* Makes a set of Twig filters for use in a twig extension.
* @param array $filters Current collection
* @return array
*/
public function makeTwigFilters($filters = [])
{
if (!is_array($filters))
$filters = [];
foreach ($this->listFilters() as $name => $callable) {
/*
* Handle a wildcard function
*/
if (strpos($name, '*') !== false && $this->isWildCallable($callable)) {
$callable = function($name) use ($callable) {
$arguments = array_slice(func_get_args(), 1);
$method = $this->isWildCallable($callable, Str::camel($name));
return call_user_func_array($method, $arguments);
};
}
if (!is_callable($callable))
throw new ApplicationException(sprintf('The markup filter for %s is not callable.', $name));
$filters[] = new Twig_SimpleFilter($name, $callable, ['is_safe' => ['html']]);
}
return $filters;
}
/**
* Makes a set of Twig token parsers for use in a twig extension.
* @param array $parsers Current collection
* @return array
*/
public function makeTwigTokenParsers($parsers = [])
{
if (!is_array($parsers))
$parsers = [];
$extraParsers = $this->listTokenParsers();
foreach ($extraParsers as $obj) {
if (!$obj instanceof Twig_TokenParser)
continue;
$parsers[] = $obj;
}
return $parsers;
}
/**
* Tests if a callable type contains a wildcard, also acts as a
* utility to replace the wildcard with a string.
* @param callable $callable
* @param string $replaceWith
* @return mixed
*/
protected function isWildCallable($callable, $replaceWith = false)
{
$isWild = false;
if (is_string($callable) && strpos($callable, '*') !== false)
$isWild = $replaceWith ? str_replace('*', $replaceWith, $callable) : true;
if (is_array($callable)) {
if (is_string($callable[0]) && strpos($callable[0], '*') !== false) {
if ($replaceWith) {
$isWild = $callable;
$isWild[0] = str_replace('*', $replaceWith, $callable[0]);
}
else
$isWild = true;
}
if (!empty($callable[1]) && strpos($callable[1], '*') !== false) {
if ($replaceWith) {
$isWild = $isWild ?: $callable;
$isWild[1] = str_replace('*', $replaceWith, $callable[1]);
}
else
$isWild = true;
}
}
return $isWild;
}
}

View File

@ -52,12 +52,7 @@ class Extension extends Twig_Extension
/*
* Include extensions provided by plugins
*/
foreach ($this->markupManager->listFunctions() as $name => $callable) {
if (!is_callable($callable))
throw new ApplicationException(sprintf('The markup function for %s is not callable.', $name));
$functions[] = new Twig_SimpleFunction($name, $callable, ['is_safe' => ['html']]);
}
$functions = $this->markupManager->makeTwigFunctions($functions);
return $functions;
}
@ -76,12 +71,7 @@ class Extension extends Twig_Extension
/*
* Include extensions provided by plugins
*/
foreach ($this->markupManager->listFilters() as $name => $callable) {
if (!is_callable($callable))
throw new ApplicationException(sprintf('The markup filter for %s is not callable.', $name));
$filters[] = new Twig_SimpleFilter($name, $callable, ['is_safe' => ['html']]);
}
$filters = $this->markupManager->makeTwigFilters($filters);
return $filters;
}
@ -95,13 +85,10 @@ class Extension extends Twig_Extension
{
$parsers = [];
$extraParsers = $this->markupManager->listTokenParsers();
foreach ($extraParsers as $obj) {
if (!$obj instanceof Twig_TokenParser)
continue;
$parsers[] = $obj;
}
/*
* Include extensions provided by plugins
*/
$parsers = $this->markupManager->makeTwigTokenParsers($parsers);
return $parsers;
}

View File

@ -5,7 +5,8 @@ use Cms\Classes\Theme;
class ControllerTest extends TestCase
{
public function tearDown() {
public function tearDown()
{
Mockery::close();
}

View File

@ -0,0 +1,111 @@
<?php
use System\Classes\MarkupManager;
class MarkupManagerTest extends TestCase
{
public function setUp()
{
include_once base_path().'/tests/fixtures/system/plugins/october/test/Plugin.php';
}
//
// Helpers
//
protected static function callProtectedMethod($object, $name, $params = [])
{
$className = get_class($object);
$class = new ReflectionClass($className);
$method = $class->getMethod($name);
$method->setAccessible(true);
return $method->invokeArgs($object, $params);
}
public static function getProtectedProperty($object, $name)
{
$className = get_class($object);
$class = new ReflectionClass($className);
$property = $class->getProperty($name);
$property->setAccessible(true);
return $property->getValue($object);
}
public static function setProtectedProperty($object, $name, $value)
{
$className = get_class($object);
$class = new ReflectionClass($className);
$property = $class->getProperty($name);
$property->setAccessible(true);
return $property->setValue($object, $value);
}
//
// Tests
//
public function testIsWildCallable()
{
$manager = MarkupManager::instance();
/*
* Negatives
*/
$callable = 'something';
$result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]);
$this->assertFalse($result);
$callable = ['Form', 'open'];
$result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]);
$this->assertFalse($result);
$callable = function() { return 'O, Hai!'; };
$result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]);
$this->assertFalse($result);
/*
* String
*/
$callable = 'something_*';
$result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]);
$this->assertTrue($result);
$result = self::callProtectedMethod($manager, 'isWildCallable', [$callable, 'delicious']);
$this->assertEquals('something_delicious', $result);
/*
* Array
*/
$callable = ['Class', 'foo_*'];
$result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]);
$this->assertTrue($result);
$result = self::callProtectedMethod($manager, 'isWildCallable', [$callable, 'bar']);
$this->assertTrue(isset($result[0]));
$this->assertTrue(isset($result[1]));
$this->assertEquals('Class', $result[0]);
$this->assertEquals('foo_bar', $result[1]);
$callable = ['My*', 'method'];
$result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]);
$this->assertTrue($result);
$result = self::callProtectedMethod($manager, 'isWildCallable', [$callable, 'Class']);
$this->assertTrue(isset($result[0]));
$this->assertTrue(isset($result[1]));
$this->assertEquals('MyClass', $result[0]);
$this->assertEquals('method', $result[1]);
$callable = ['My*', 'my*'];
$result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]);
$this->assertTrue($result);
$result = self::callProtectedMethod($manager, 'isWildCallable', [$callable, 'Food']);
$this->assertTrue(isset($result[0]));
$this->assertTrue(isset($result[1]));
$this->assertEquals('MyFood', $result[0]);
$this->assertEquals('myFood', $result[1]);
}
}