__('ProFields: Page Table', __FILE__), // Module Title 'summary' => __('Inputfield to accompany FieldtypePageTable', __FILE__), // Module Summary 'version' => 14, 'requires' => 'FieldtypePageTable' ); } /** * Labels for native fields, indexed by native field name * * @var array * */ protected $nativeLabels = array(); /** * Array of Template objects used for each row * * @var array * */ protected $rowTemplates = array(); /** * Possible orphan pages that may be added (set by the Fieldtype) * * @var PageArray|null * */ protected $orphans = null; /** * Whether or not a separate "edit" column is needed * * @var bool * */ protected $needsEditColumn = false; /** * True when in renderValue mode * * @var bool * */ protected $renderValueMode = false; /** * Initialize and establish default values * */ public function init() { // fieldtype and inputfield config settings $this->set('parent_id', 0); $this->set('template_id', 0); // placeholder only $this->set('columns', ''); $this->set('nameFormat', ''); $this->set('noclose', 0); $this->set('blankLabel', $this->_('[blank]')); // local settings $this->nativeLabels = array( 'id' => $this->_x('ID', 'th'), 'name' => $this->_x('Name', 'th'), 'created' => $this->_x('Created', 'th'), 'modified' => $this->_x('Modified', 'th'), 'published' => $this->_x('Published', 'th'), 'modifiedUser' => $this->_x('Modified By', 'th'), 'createdUser' => $this->_x('Created By', 'th'), 'url' => $this->_x('URL', 'th'), 'path' => $this->_x('Path', 'th'), 'template' => $this->_x('Template', 'th'), 'parent' => $this->_x('Parent', 'th'), 'numChildren' => $this->_x('Children', 'th'), 'status' => $this->_x('Status', 'th'), ); parent::init(); } public function renderReady(Inputfield $parent = null, $renderValueMode = false) { $this->addClass('InputfieldNoFocus', 'wrapClass'); $jQueryUI = $this->wire()->modules->get('JqueryUI'); /** @var JqueryUI $jQueryUI */ $jQueryUI->use('modal'); return parent::renderReady($parent, $renderValueMode); } /** * Render the PageTable Inputfield * * @return string * */ public function ___render() { $sanitizer = $this->wire()->sanitizer; $modules = $this->wire()->modules; $process = $this->wire()->process; $config = $this->wire()->config; $input = $this->wire()->input; // make sure we've got enough info to generate a table $errors = array(); if(!count($this->rowTemplates)) $errors[] = $this->_('Please configure this field with a template selection before using it.'); if(!$this->columns) $errors[] = $this->_('Please enter one or more columns in your field settings before using this field.'); if(count($errors)) return "

" . implode('
', $errors) . "

"; // determine what columns we'll show in the table $columnsString = $this->columns ? $this->columns : $this->getConfigDefaultColumns(); $columns = array(); foreach(explode("\n", $columnsString) as $column) { $width = 0; if(strpos($column, '=') !== false) list($column, $width) = explode('=', $column); $column = trim($column); $width = (int) $width; $columns[$column] = $width; } // render the table $out = $this->renderTable($columns); if($this->renderValueMode) return $out; $editID = (int) $input->get('id'); if(!$editID && $process instanceof WirePageEditor) $editID = $process->getPage()->id; $parentID = $this->parent_id ? $this->parent_id : $editID; // render the 'Add New' buttons for each template $btn = ''; foreach($this->rowTemplates as $template) { /** @var Template $template */ /** @var InputfieldButton $button */ $button = $modules->get('InputfieldButton'); $button->icon = 'plus-circle'; $button->value = count($this->rowTemplates) == 1 ? $this->_x('Add New', 'button') : $template->getLabel(); $url = $config->urls->admin . "page/add/?modal=1&template_id=$template->id&parent_id=$parentID&context=PageTable"; if($this->nameFormat) $url .= "&name_format=" . $sanitizer->entities($this->nameFormat); $btn .= "" . $button->render() . ""; } if(count($this->rowTemplates) > 1) $btn = "$btn"; $out .= "
$btn
"; if(!$input->get('InputfieldPageTableField')) { $url = "./?id=$editID&InputfieldPageTableField=$this->name"; $out = "
$out
"; // input for sorting purposes $value = $sanitizer->entities($this->attr('value')); $name = $sanitizer->entities($this->attr('name')); $out .= ""; $out .= ""; if($this->orphans && count($this->orphans)) { $out .= "

"; $out .= "" . $this->_('Children were found that may be added to this table. Check the box next to any you would like to add.') . " "; if(count($this->orphans) > 1) { $out .= "
" . $this->_('Select all') . " "; } foreach($this->orphans as $item) { $label = $item->title; if(!strlen($label)) $label = $item->name; $out .= ""; } $out .= "

"; } } return $out; } /** * Render non-editable value * * @return string * */ public function ___renderValue() { $this->renderValueMode = true; $out = $this->render(); $this->renderValueMode = false; return $out; } /** * Render the outputted PageTable * * @param array $columns Array of column name => width percent * @return string * */ protected function ___renderTable(array $columns) { $fields = $this->wire()->fields; $this->needsEditColumn = false; /** @var PageArray $value */ $value = $this->attr('value'); $this->wire()->modules->get('MarkupAdminDataTable'); // for styles if(!count($value)) return ''; // if nothing in the value, just return blank // $template = $this->template_id ? $this->wire('templates')->get((int) $this->template_id) : null; $template = count($this->rowTemplates) > 0 ? reset($this->rowTemplates) : null; $fieldsByCol = array(); $labelsByCol = array(); // populate $fieldsByCol and $labelsByCol foreach($columns as $column => $width) { $field = null; $fieldName = $column; $label = ''; // check if field contains field.subfield if(strpos($column, '.') !== false) { $parentField = null; list($parentFieldName, $fieldName) = explode('.', $column); if($template) $parentField = $template->fieldgroup->getFieldContext($parentFieldName); if(!$parentField) $parentField = $fields->get($parentFieldName); if($parentField) { $label = $parentField->getLabel(); } else if(isset($this->nativeLabels[$parentFieldName])) { $label = $this->nativeLabels[$parentFieldName]; } else { $label = $parentFieldName; } $label .= " > "; } if($template) $field = $template->fieldgroup->getFieldContext($fieldName); if(!$field) $field = $fields->get($fieldName); if($field) { $label .= $field->getLabel(); $fieldsByCol[$column] = $field; } else if(isset($this->nativeLabels[$fieldName])) { $label .= $this->nativeLabels[$fieldName]; } else { $label .= $column; } $labelsByCol[$column] = $label; } $out = $this->renderTableBody($value, $columns, $fieldsByCol); // render order intentional $out = $this->renderTableHead($columns, $labelsByCol) . $out; return $out; } /** * Render the table head, from
to * * @param array $columns * @param array $labels * @return string * */ protected function renderTableHead(array $columns, array $labels) { /** @var MarkupAdminDataTable $module */ $module = $this->wire()->modules->get('MarkupAdminDataTable'); $sanitizer = $this->wire()->sanitizer; $classes = array(); foreach(array('class', 'addClass', 'responsiveClass', 'responsiveAltClass') as $key) { $value = $module->settings($key); if(!empty($value)) $classes[] = $value; } $tableClass = implode(' ', $classes); $out = "
"; if($this->needsEditColumn) $out .= ""; foreach($columns as $column => $width) { $attr = $width ? " style='width: $width%'" : ''; $label = $labels[$column]; $out .= "" . $sanitizer->entities($label) . ""; } if(!$this->renderValueMode) $out .= ""; $out .= ""; return $out; } /** * Render the table body, from to
  
* * @param PageArray $items * @param array $columns * @param array $fields * @return string * */ protected function renderTableBody(PageArray $items, array $columns, array $fields) { $rows = array(); foreach($items as $key => $item) { /** @var Page $item */ $of = $item->of(); $item->of(true); $rows[$key] = $this->renderTableRow($item, $columns, $fields); $item->of($of); } if($this->needsEditColumn) { foreach($rows as $key => $row) { list($s1, $s2) = explode('>', $row, 2); $item = $items[$key]; $rows[$key] = "$s1>" . $this->renderItemLink($item, wireIconMarkup('edit')) . "$s2"; } } return "" . implode("", $rows) . ""; } /** * Render an individual table row for a given PageTable item * * @param Page $item * @param array $columns * @param array $fields * @return string * */ protected function renderTableRow(Page $item, array $columns, array $fields) { $out = ''; $n = 0; foreach($columns as $column => $width) { $linkURL = ($n++ && !$this->renderValueMode ? '' : $this->getItemEditURL($item)); $out .= $this->renderTableCol($item, $fields, $column, $width, $linkURL); } // append a delete column/link if(!$this->renderValueMode) { $a = "" . wireIconMarkup('trash-o') . ""; if(!$item->deletable()) $a = " "; $out .= "$a"; } // wrap the row in a $class = ''; if($item->hasStatus(Page::statusUnpublished)) $class .= 'PageListStatusUnpublished '; if($item->hasStatus(Page::statusHidden)) $class .= 'PageListStatusHidden '; if($item->isTrash()) $class .= 'PageListStatusTrash'; if($class) $class = " class='" . trim($class) . "'"; return "$out"; } /** * Render an individual for a table row * * @param Page $item * @param array $fields * @param $column * @param $width * @param string $linkURL * @return string * */ protected function renderTableCol(Page $item, array $fields, $column, $width, $linkURL = '') { $out = $this->getItemValue($item, $fields, $column); if($linkURL && !$this->renderValueMode) { if(stripos($out, 'needsEditColumn = true; if(!strlen($out)) $out = "$this->blankLabel"; } else { $out = $this->renderItemLink($item, $out, $linkURL); } } $attr = $width ? " style='width: $width%'" : ''; return "$out"; } /** * Get the value for the given Page field identified by $column * * @param Page $item * @param array $fields * @param $column * @return mixed|object|string * */ protected function getItemValue(Page $item, array $fields, $column) { $fieldName = $column; $subfieldName = ''; if(strpos($column, '.') !== false) { list($fieldName, $subfieldName) = explode('.', $column); } if(isset($fields[$column])) { // custom $field = $fields[$column]; /** @var Field $field */ $v = $item->getFormatted($fieldName); $value = (string) $field->type->markupValue($item, $field, $v, $subfieldName); } else { // native $value = $item->get($fieldName); if(is_object($value) && $subfieldName) { $value = $this->objectToString($value, $subfieldName); $fieldName = $subfieldName; } if($fieldName == 'modified' || $fieldName == 'created' || $fieldName == 'published') { $value = wireDate($this->_('Y-m-d H:i'), (int) $value); // Date format for created/modified/published } } return $value; } /** * Render an item edit link surrounded by the given output * * @param Page $item * @param string $out * @param string $url Optional * @return string * */ protected function renderItemLink(Page $item, $out, $url = '') { if(!$url) $url = $this->getItemEditURL($item); return "$out"; } /** * Get an item edit URL * * @param Page $item * @return string * */ protected function getItemEditURL(Page $item) { return $this->wire()->config->urls->admin . "page/edit/?id=$item->id&modal=1&context=PageTable"; } /** * Convert an object to a string for rendering in a table * * @param $object * @param string $property Property to display from the object (default=title|name) * @return string * */ protected function objectToString($object, $property = '') { if($object instanceof WireArray) { if(!$property) $property = 'title|name'; if($property == 'count') { $value = $object->count(); } else { $value = $object->implode("\n", $property); } } else if($property) { $value = $object->$property; if(is_object($value)) $value = $this->objectToString($value); } else { $value = (string) $object; } $value = $this->wire()->sanitizer->entities(strip_tags($value)); $value = nl2br($value); return $value; } /** * Process input submitted to a PageTable Inputfield * * @param WireInputData $input * @return $this * */ public function ___processInput(WireInputData $input) { $pages = $this->wire()->pages; $name = $this->attr('name'); $deleteName = $name . '__delete'; $deleteIDs = explode('|', $input->$deleteName); $ids = explode('|', $input->$name); $value = $this->attr('value'); /** @var PageArray $value */ $sorted = $pages->newPageArray(); $changed = false; // trash items that have been deleted foreach($deleteIDs as $id) { foreach($value as $item) { /** @var Page $item */ if($id != $item->id) continue; if(!$item->deleteable()) continue; $value->remove($item); $pages->trash($item); $changed = true; } } foreach($ids as $id) { if(in_array($id, $deleteIDs)) continue; foreach($value as $item) { if($id == $item->id) $sorted->add($item); } } // add in new items that may have been added after a sort foreach($value as $item) { if(!in_array($item->id, $ids)) $sorted->add($item); } // check for orphans that may have been added $orphanInputName = $name . '__add_orphan'; $orphanIDs = $input->$orphanInputName; if(is_array($orphanIDs) && count($orphanIDs) && $this->orphans) { $numOrphansAdded = 0; foreach($orphanIDs as $orphanID) { foreach($this->orphans as $orphan) { if($orphan->id == $orphanID) { $sorted->add($orphan); $numOrphansAdded++; } } } if($numOrphansAdded) { $this->message(sprintf($this->_('Added %d existing page(s) to table'), $numOrphansAdded) . " ($this->name)"); } } if("$value" != "$sorted") $changed = true; if($changed) { $this->setAttribute('value', $sorted); $this->trackChange('value'); } // check if we need to setup a name format for any pages foreach($value as $n => $item) { $name = $pages->setupPageName($item, array('format' => $this->nameFormat)); if($name) { $this->message("Auto assigned name '$name' to item #" . ($n+1), Notice::debug); $item->save(); } } return $this; } /** * Set a property to this Inputfield * * @param string $key * @param mixed $value * @return $this * */ public function set($key, $value) { if($key === 'template_id' && $value) { // convert template_id to $this->rowTemplates array $templates = $this->wire()->templates; if(!is_array($value)) $value = array($value); foreach($value as $id) { $template = $templates->get($id); if($template) $this->rowTemplates[$id] = $template; } return $this; } else { return parent::set($key, $value); } } /** * Set an attribute to the Inputfield * * In this case we capture set to the 'value' attribute to make sure it can only be a PageArray * * @param array|string $key * @param int|string $value * @return $this * @throws WireException * */ public function setAttribute($key, $value) { if($key == 'value') { if($value === null) $value = $this->wire()->pages->newPageArray(); if(!$value instanceof PageArray) { throw new WireException('This Inputfield only accepts a PageArray for its value attribute.'); } } return parent::setAttribute($key, $value); } public function setOrphans(PageArray $orphans) { $this->orphans = $orphans; } /** * Determine a default set of columns for the PageTable based on the fields defined in the defined template * * @return string of newline separated field names * */ protected function getConfigDefaultColumns() { $out = ''; if(!count($this->rowTemplates)) return $out; $fieldCounts = array(); foreach($this->rowTemplates as $template) { foreach($template->fieldgroup as $field) { if(!isset($fieldCounts[$field->name])) { $fieldCounts[$field->name] = 1; } else { $fieldCounts[$field->name]++; } } } // sort most used to least used if(count($this->rowTemplates) > 1) arsort($fieldCounts); $n = 0; foreach(array_keys($fieldCounts) as $fieldName) { $out .= $fieldName . "\n"; if(++$n >= 5) break; } return trim($out); } /** * Get field configuration for input tab * * @return InputfieldWrapper * */ public function ___getConfigInputfields() { $inputfields = parent::___getConfigInputfields(); $f = $inputfields->InputfieldTextarea; $f->attr('name', 'columns'); $f->label = $this->_('Table fields to display in admin'); $f->description = $this->_('Enter the names of the fields (1 per line) that you want to display as columns in the table.') . ' ' . $this->_('To specify a column width for the field, specify "field_name=30" where "30" is the width (in percent) of the column.') . ' ' . $this->_('When specifying widths, make the total of all columns add up to 100.'); $f->notes = $this->_('You may specify any native or custom field.') . ' ' . $this->_('You may also use subfields (field.subfield) with fields that contain multiple properties, like page references.') . ' '; $columns = $this->columns ? $this->columns : $this->getConfigDefaultColumns(); $f->attr('value', $columns); if(count($this->rowTemplates)) { $options = array(); foreach($this->rowTemplates as $template) { foreach($template->fieldgroup as $item) $options[$item->name] = $item->name; } $f->notes .= $this->_('Custom fields assigned to your selected templates include the following:') . ' **'; $f->notes .= implode(', ', $options) . '**'; } else { $f->notes .= $this->_('To see a list of possible custom fields here, select a template on the Details tab, Save, and come back here.'); } $inputfields->add($f); $f = $inputfields->InputfieldText; $f->attr('name', 'nameFormat'); $f->attr('value', $this->nameFormat); $f->label = $this->_('Automatic Page Name Format'); $f->description = $this->_('When populated, pages will be created automatically using this name format whenever a user clicks the "Add New" button.') . ' ' . // page name format description 1 $this->_('If left blank, the user will be asked to enter a name for the page before it is created.'); // page name format description 2 $f->notes = sprintf( $this->_('If the name format contains any non-alphanumeric characters, it is considered to be a [PHP date](%s) format.'), 'https://www.php.net/manual/en/datetime.format.php' ). ' ' . $this->_('If it contains only alphanumeric characters then it will be used directly, with a number appended to the end (when necessary) to ensure uniqueness.'); // page name format notes $f->notes .= ' ' . $this->_('Example: **Ymd:His** is a good name format for date/time based page names.'); $f->collapsed = Inputfield::collapsedBlank; $inputfields->add($f); $f = $inputfields->InputfieldRadios; $f->attr('name', 'noclose'); $f->label = $this->_('Modal edit window behavior'); $f->addOption(0, $this->_('Automatically close on save (default)')); $f->addOption(1, $this->_('Keep window open, close manually')); $f->attr('value', (int) $this->noclose); if(!$this->noclose) $f->collapsed = Inputfield::collapsedYes; $inputfields->add($f); return $inputfields; } }