diff --git a/config/install/xls_serialization.configuration.yml b/config/install/xls_serialization.configuration.yml new file mode 100644 index 0000000000000000000000000000000000000000..a1eb1148749935027a6d06c06492f1fef0a4cfbc --- /dev/null +++ b/config/install/xls_serialization.configuration.yml @@ -0,0 +1 @@ +xls_serialization_autosize: false diff --git a/config/schema/xls_serialization.schema.yml b/config/schema/xls_serialization.schema.yml new file mode 100644 index 0000000000000000000000000000000000000000..38a411cf32b410287a36a95aceefcc4a0bf4f7d5 --- /dev/null +++ b/config/schema/xls_serialization.schema.yml @@ -0,0 +1,10 @@ +# Schema for the configuration files of the Xls Serialization module. + +xls_serialization.configuration: + type: config_object + label: 'Xls Serialization settings' + mapping: + xls_serialization_autosize: + type: boolean + label: 'Disable AutoSize of columns' + diff --git a/src/Encoder/Xls.php b/src/Encoder/Xls.php index 8a538b7c7499fa9e9dd48a72df28abc1e12821fd..1645c6368ef7429210236423019aea51c1818494 100644 --- a/src/Encoder/Xls.php +++ b/src/Encoder/Xls.php @@ -71,6 +71,9 @@ class Xls implements EncoderInterface { $xls->setActiveSheetIndex(0); $sheet = $xls->getActiveSheet(); + // Load module config. + $config = \Drupal::config('xls_serialization.configuration'); + // Set headers. $this->setHeaders($sheet, $data, $context); @@ -78,7 +81,9 @@ class Xls implements EncoderInterface { $this->setData($sheet, $data); // Set the width of every column with data in it to AutoSize. - $this->setColumnsAutoSize($sheet); + if (empty($config->get('xls_serialization_autosize'))) { + $this->setColumnsAutoSize($sheet); + } // Set rows to auto-height. $this->setRowsAutoHeight($sheet); diff --git a/src/Encoder/Xls.php.orig b/src/Encoder/Xls.php.orig new file mode 100644 index 0000000000000000000000000000000000000000..8a538b7c7499fa9e9dd48a72df28abc1e12821fd --- /dev/null +++ b/src/Encoder/Xls.php.orig @@ -0,0 +1,522 @@ +<?php + +namespace Drupal\xls_serialization\Encoder; + +use Drupal\views\ViewExecutable; +use Drupal\Component\Serialization\Exception\InvalidDataTypeException; +use Drupal\Component\Utility\Html; +use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\Document\Properties; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\Conditional; +use PhpOffice\PhpSpreadsheet\Style\Fill; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; +use PhpOffice\PhpSpreadsheet\IOFactory; +use Symfony\Component\Serializer\Encoder\EncoderInterface; + +/** + * Adds XLS encoder support for the Serialization API. + */ +class Xls implements EncoderInterface { + + /** + * The format that this encoder supports. + * + * @var string + */ + protected static $format = 'xls'; + + /** + * Format to write XLS files as. + * + * @var string + */ + protected $xlsFormat = 'Xlsx'; + + /** + * Constructs an XLS encoder. + * + * @param string $xls_format + * The XLS format to use. + */ + public function __construct($xls_format = 'Xlsx') { + // Temporary fix until it wold be fixed at views_data_export. + if ($xls_format == 'Excel2007') { + $xls_format = 'Xlsx'; + } + $this->xlsFormat = $xls_format; + } + + /** + * {@inheritdoc} + */ + public function encode($data, $format, array $context = []):string { + switch (gettype($data)) { + case 'array': + // Nothing to do. + break; + + case 'object': + $data = (array) $data; + break; + + default: + $data = [$data]; + break; + } + + try { + // Instantiate a new excel object. + $xls = new Spreadsheet(); + $xls->setActiveSheetIndex(0); + $sheet = $xls->getActiveSheet(); + + // Set headers. + $this->setHeaders($sheet, $data, $context); + + // Set the data. + $this->setData($sheet, $data); + + // Set the width of every column with data in it to AutoSize. + $this->setColumnsAutoSize($sheet); + + // Set rows to auto-height. + $this->setRowsAutoHeight($sheet); + + if (isset($context['views_style_plugin'])) { + if (isset($context['views_style_plugin']->options['xls_settings'])) { + $this->setSettings($context['views_style_plugin']->options['xls_settings']); + // Set any metadata passed in via the context. + if (isset($context['views_style_plugin']->options['xls_settings']['metadata'])) { + $this->setMetaData($xls->getProperties(), $context['views_style_plugin']->options['xls_settings']['metadata']); + } + } + + if (!empty($context['views_style_plugin']->view)) { + /** @var \Drupal\views\ViewExecutable $view */ + $view = $context['views_style_plugin']->view; + // Set the worksheet title based on the view title within the context. + if (!empty($view->getTitle())) { + $sheet->setTitle($this->validateWorksheetTitle($view->getTitle())); + } + + // Set the header row of the worksheet to bold. + if ($view->getDisplay()->getOption('header_bold') == 1) { + $this->setHeaderRowBold($sheet); + } + + // Set the header row of the worksheet to italic. + if ($view->getDisplay()->getOption('header_italic') == 1) { + $this->setHeaderRowItalic($sheet); + } + + // Set the background color of the header row of the worksheet. + if ($view->getDisplay()->getOption('header_background_color') != '') { + $this->setHeaderRowBackgroundColor($sheet, $view->getDisplay()->getOption('header_background_color')); + } + + // Conditional formatting. + for ($i = 0; $i <= 4; $i++) { + $current_conditional_formatting_base_field = $view->getDisplay()->getOption('conditional_formatting_base_field_' . $i); + if ($current_conditional_formatting_base_field !== NULL && $current_conditional_formatting_base_field !== 'Select a field') { + $headers = $this->extractHeaders($data, $context); + $conditional_formatting_base_field[$i] = $current_conditional_formatting_base_field; + $field_label_or_name[$i] = $this->getViewFieldLabel($view, $conditional_formatting_base_field[$i]); + $base_field_column_letter[$i] = $this->getColumnLetterFromFieldName($headers, $field_label_or_name[$i]); + $operator[$i] = $this->getOperatorFromSelectIndex($view->getDisplay()->getOption('conditional_formatting_operator_' . $i)); + $compare_to[$i] = $view->getDisplay()->getOption('conditional_formatting_compare_to_' . $i); + $rgb_background_color[$i] = $view->getDisplay()->getOption('conditional_formatting_background_color_' . $i); + + $conditional_styles[] = $this->setConditionalFormat($base_field_column_letter[$i], $operator[$i], $compare_to[$i], $rgb_background_color[$i]); + } + } + if (isset($conditional_styles)) { + $this->setConditionalFormating($sheet, $conditional_styles); + } + } + } + + $writer = IOFactory::createWriter($xls, $this->xlsFormat); + + // @todo utilize a temporary file perhaps? + // @todo This should also support batch processing. + // @see http://stackoverflow.com/questions/9469779/how-do-i-write-my-excel-spreadsheet-into-a-variable-using-phpexcel + ob_start(); + $writer->save('php://output'); + return ob_get_clean(); + } + catch (\Exception $e) { + throw new InvalidDataTypeException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * {@inheritdoc} + */ + public function supportsEncoding($format):bool { + return $format === static::$format; + } + + /** + * Set sheet headers. + * + * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet + * The worksheet to set headers for. + * @param array $data + * The data array. + * @param array $context + * The context options array. + */ + protected function setHeaders(Worksheet $sheet, array $data, array $context) { + // Extract headers from the data. + $headers = $this->extractHeaders($data, $context); + foreach ($headers as $column => $header) { + $sheet->setCellValue(++$column, 1, $this->formatValue($header)); + } + } + + /** + * Set any available metadata. + * + * @param \PhpOffice\PhpSpreadsheet\Document\Properties $document_properties + * The document properties object. + * @param array $metadata + * An associative array of metadata to set on the document. The array can + * contain any of the following keys (with corresponding values). + * - 'creator': The document creator. + * - 'last_modified_by': The name of the person to last modify. + * - 'created': The time the document was created. + * - 'modified': The time the document was modified. + * - 'title': The document title. + * - 'description': The document description. + * - 'subject': The document subject. + * - 'keywords': Any keywords for the document. + * - 'category': The document category. + * - 'manager': The document manager. + * - 'company': The company that created the document. + * - 'custom_properties': An associative array of property name mapping to + * property value. If the value is an array, the first item should be the + * value, and the second item the property type: + * - 'i': integer + * - 'f': floating point + * - 's': string (default) + * - 'd': 'date/time' + * - 'b': boolean. + */ + protected function setMetaData(Properties $document_properties, array $metadata) { + if (isset($metadata['creator'])) { + $document_properties->setCreator($metadata['creator']); + } + if (isset($metadata['last_modified_by'])) { + $document_properties->setLastModifiedBy($metadata['last_modified_by']); + } + if (isset($metadata['created'])) { + $document_properties->setCreated($metadata['created']); + } + if (isset($metadata['modified'])) { + $document_properties->setModified($metadata['modified']); + } + if (isset($metadata['title'])) { + $document_properties->setTitle($metadata['title']); + } + if (isset($metadata['description'])) { + $document_properties->setDescription($metadata['description']); + } + if (isset($metadata['subject'])) { + $document_properties->setSubject($metadata['subject']); + } + if (isset($metadata['keywords'])) { + $document_properties->setKeywords($metadata['keywords']); + } + if (isset($metadata['category'])) { + $document_properties->setCategory($metadata['category']); + } + if (isset($metadata['manager'])) { + $document_properties->setManager($metadata['manager']); + } + if (isset($metadata['company'])) { + $document_properties->setCompany($metadata['company']); + } + + if (isset($metadata['custom_properties'])) { + foreach ($metadata['custom_properties'] as $name => $value) { + $type = 's'; + if (is_array($value)) { + $type = array_pop($value); + $value = reset($value); + } + $document_properties->setCustomProperty($name, $value, $type); + } + } + } + + /** + * Set sheet data. + * + * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet + * The worksheet to put the data in. + * @param array $data + * The data to be put in the worksheet. + */ + protected function setData(Worksheet $sheet, array $data) { + foreach ($data as $i => $row) { + $column = 1; + foreach ($row as $value) { + // Since headers have been added, rows are offset here by 2. + $sheet->setCellValue($column, $i + 2, $this->formatValue($value)); + $column++; + } + } + } + + /** + * Formats a single value for a given XLS cell. + * + * @param string $value + * The raw value to be formatted. + * + * @return string + * The formatted value. + */ + protected function formatValue($value) { + // @todo Make these filters configurable. + $value = Html::decodeEntities($value); + $value = strip_tags($value); + $value = trim($value); + + return $value; + } + + /** + * Extract the headers from the data array. + * + * @param array $data + * The data array. + * @param array $context + * The context options array. + * + * @return string[] + * An array of headers to be used. + */ + protected function extractHeaders(array $data, array $context) { + $headers = []; + if ($first_row = reset($data)) { + if (isset($context['header'])) { + $headers = $context['header']; + } + elseif (isset($context['views_style_plugin'])) { + /** @var \Drupal\views\ViewExecutable $view */ + $view = $context['views_style_plugin']->view; + $fields = $view->field; + foreach ($first_row as $key => $value) { + $headers[] = !empty($fields[$key]->options['label']) ? $fields[$key]->options['label'] : $key; + } + } + else { + $headers = array_keys($first_row); + } + } + + return $headers; + } + + /** + * Set XLS settings from the Views settings array. + * + * @param array $settings + * An array of XLS settings. + */ + protected function setSettings(array $settings) { + // Temporary fix this until it would be fixed at the views_data_export. + if ($settings['xls_format'] == 'Excel2007') { + $settings['xls_format'] = 'Xlsx'; + } + if ($settings['xls_format'] == 'Excel5') { + $settings['xls_format'] = 'Xls'; + } + $this->xlsFormat = $settings['xls_format']; + } + + /** + * Set width of all columns with data in them in sheet to AutoSize. + * + * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet + * The worksheet to set the column width to AutoSize for. + */ + protected function setColumnsAutoSize(Worksheet $sheet) { + foreach ($sheet->getColumnIterator() as $column) { + $column_index = $column->getColumnIndex(); + $sheet->getColumnDimension($column_index)->setAutoSize(TRUE); + } + } + + /** + * Sets height of all rows to automatic and enables text wrapping. + * + * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet + * The worksheet to set the column width to AutoSize for. + */ + protected function setRowsAutoHeight(Worksheet $sheet) { + foreach ($sheet->getRowDimensions() as $row_id) { + $row_id->setRowHeight(-1); + } + $sheet->getStyle('A1:' . $sheet->getHighestColumn() . $sheet->getHighestRow())->getAlignment()->setWrapText(TRUE); + } + + /** + * Set font of the header (first) row to bold. + * + * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet + * The worksheet to set the font of the header row to bold. + */ + protected function setHeaderRowBold(Worksheet $sheet) { + $sheet->getStyle('A1:' . $sheet->getHighestDataColumn() . '1')->getFont()->setBold(TRUE); + } + + /** + * Set font of the header (first) row to italic. + * + * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet + * The worksheet to set the font of the header row to italic. + */ + protected function setHeaderRowItalic(Worksheet $sheet) { + $sheet->getStyle('A1:' . $sheet->getHighestDataColumn() . '1')->getFont()->setItalic(TRUE); + } + + /** + * Set background color of the header (first) row. + * + * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet + * The worksheet to set the background color of the header row. + * @param string $rgb + * The worksheet to set the background color of the header row. + * + * @throws \PhpOffice\PhpSpreadsheet\Exception + */ + protected function setHeaderRowBackgroundColor(Worksheet $sheet, $rgb) { + $style = [ + 'fill' => [ + 'fillType' => Fill::FILL_SOLID, + 'startColor' => [ + 'argb' => 'FF' . $rgb, + ], + 'endColor' => [ + 'argb' => 'FF' . $rgb, + ], + ], + ]; + + $sheet->getStyle('A1:' . $sheet->getHighestDataColumn() . '1')->applyFromArray($style); + } + + /** + * Gets conditional formatting for whole row if the comparison is TRUE. + * + * @param string $column_letter + * The column letter that contains the cells to compare against. + * @param string $operator + * The operator to use in the comparison. + * @param string $compare_to + * The value to compare to. + * @param string $rgb_background_color + * The RGB value of the background color to set on the whole row, + * if the comparison is TRUE. + * + * @return \PhpOffice\PhpSpreadsheet\Style\Conditional + * The conditional formatting. + */ + protected function setConditionalFormat($column_letter, $operator, $compare_to, $rgb_background_color) { + $conditional_format = new Conditional(); + $conditional_format->setConditionType(Conditional::CONDITION_EXPRESSION); + $conditional_format->addCondition('=$' . $column_letter . '2' . $operator . '"' . $compare_to . '"'); + $conditional_format->getStyle()->getFill()->setFillType(Fill::FILL_SOLID)->getEndColor()->setRGB($rgb_background_color); + + return $conditional_format; + } + + /** + * Sets conditional formats on worksheet. + * + * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet + * The sheet to set the conditional formats on. + * @param array $conditional_styles + * The conditional formats to set. + * + * @throws \PhpOffice\PhpSpreadsheet\Exception + */ + protected function setConditionalFormating(Worksheet $sheet, array $conditional_styles) { + $highest_data_column = $sheet->getHighestDataColumn(); + $highest_data_row = $sheet->getHighestDataRow(); + $current_conditional_styles = $sheet->getStyle('A2')->getConditionalStyles(); + $conditional_styles = array_merge($current_conditional_styles, $conditional_styles); + + $sheet->getStyle('$A$2:$' . $highest_data_column . '$' . $highest_data_row)->setConditionalStyles($conditional_styles); + } + + /** + * Get the column letter for a field name from the headers. + * + * @param array $headers + * The header of the worksheet. + * @param string $field_label_or_name + * The field label or field name to get the column letter for. + * + * @return string + * The column letter + */ + protected function getColumnLetterFromFieldName(array $headers, $field_label_or_name) { + $base_field_header_index_num = array_search($field_label_or_name, $headers) + 1; + + return Coordinate::stringFromColumnIndex($base_field_header_index_num); + } + + /** + * Get the label in the view for a field name (if available) + * + * @param \Drupal\views\ViewExecutable $view + * The view to get the field labels from. + * @param string $field_name + * The field name to get the view label for. + * + * @return string + * The label in the view if available, field name otherwise. + */ + protected function getViewFieldLabel(ViewExecutable $view, $field_name) { + return $view->field[$field_name]->options['label'] ?? $field_name; + } + + /** + * Returns the operator for conditional formatting from the HTML-select index. + * + * @param string $operator_select_index + * The index from the HTML-select for operator. + * + * @return string + * The operator as a string. + */ + protected function getOperatorFromSelectIndex($operator_select_index) { + $operator_options = [0 => '=', 1 => '<>']; + + return $operator_options[$operator_select_index]; + } + + /** + * Validates the title of the Worksheet to ensure it's valid. + * + * Worksheet titles must not exceed 31 characters, + * contain:- ":" "\" "/" "?" "*" "[" "]" + * or be blank. + * + * @param string $title + * The orginal worksheet value. + * + * @return string + * The validated worksheet title + */ + protected function validateWorksheetTitle($title) { + $title = preg_replace('/[:\*\/\\\[\]\?]+/', '', $title); + + return trim(substr($title, 0, 30)); + } + +} diff --git a/src/Form/XlsSerializationConfigurationForm.php b/src/Form/XlsSerializationConfigurationForm.php new file mode 100644 index 0000000000000000000000000000000000000000..04872f200ecf1c483847a49bb5b4b6dd72965e99 --- /dev/null +++ b/src/Form/XlsSerializationConfigurationForm.php @@ -0,0 +1,74 @@ +<?php + +namespace Drupal\xls_serialization\Form; + +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\system\Entity\Menu; + +/** + * Configure Xls Serialization for this site. + */ +class XlsSerializationConfigurationForm extends ConfigFormBase { + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'xls_serialization_configuration_form'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return [ + 'xls_serialization.configuration', + ]; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $config = $this->config('xls_serialization.configuration'); + + $form['xls_serialization_autosize'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Disable AutoSize of columns'), + '#description' => $this->t('Checking this box, the width of the column won\'t have a flexible width but a fixed one.'), + '#default_value' => $config->get('xls_serialization_autosize'), + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + parent::submitForm($form, $form_state); + $this->config('xls_serialization.configuration') + ->set('xls_serialization_autosize', (bool) $form_state->getValue('xls_serialization_autosize')) + ->save(); + + } + + /** + * Return an associative array of menus names. + * + * @return array + * An array with the machine-readable names as the keys, and human-readable + * titles as the values. + */ + protected function getMenuLabels() { + $menus = []; + foreach (Menu::loadMultiple() as $menu_name => $menu) { + $menus[$menu_name] = $menu->label(); + } + asort($menus); + + return $menus; + } + +} diff --git a/xls_serialization.links.menu.yml b/xls_serialization.links.menu.yml new file mode 100644 index 0000000000000000000000000000000000000000..1118b67a023f1825294010945d589142012bb26a --- /dev/null +++ b/xls_serialization.links.menu.yml @@ -0,0 +1,9 @@ +xls_serialization.configuration: + title: Xls Serialization + description: 'Select if autosize column should be enabled or disabled.' + route_name: xls_serialization.configuration + parent: system.admin_config_ui + requirements: + _permission: 'administer xls serialization configuration' + options: + _admin_route: TRUE diff --git a/xls_serialization.permissions.yml b/xls_serialization.permissions.yml new file mode 100644 index 0000000000000000000000000000000000000000..c2587fb492d317597b7ebd42de19ca51cdb7d075 --- /dev/null +++ b/xls_serialization.permissions.yml @@ -0,0 +1,4 @@ +administer xls serialization configuration: + title: 'Administer xls serialization configuration' + description: 'Access to the Xls Serialization configurations' + restrict access: true diff --git a/xls_serialization.routing.yml b/xls_serialization.routing.yml new file mode 100644 index 0000000000000000000000000000000000000000..204e4e1331bdd04bde047b8a8066c6e4c83c56bd --- /dev/null +++ b/xls_serialization.routing.yml @@ -0,0 +1,9 @@ +xls_serialization.configuration: + path: '/admin/config/user-interface/xls_serialization' + defaults: + _form: 'Drupal\xls_serialization\Form\XlsSerializationConfigurationForm' + _title: 'Xls Serialization configuration' + requirements: + _permission: 'administer xls serialization configuration' + options: + _admin_route: TRUE