├── Csv.php ├── LICENSE.md ├── composer.json ├── i18n ├── de.json ├── en.json └── index.php ├── index.js └── index.php /Csv.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/distantnative/kirby-csv-field 11 | * @copyright Nico Hoffmann 12 | * @license https://opensource.org/licenses/MIT 13 | */ 14 | class Csv extends Collection 15 | { 16 | public function columns(): array 17 | { 18 | if ($first = $this->first()) { 19 | return array_keys($first); 20 | } 21 | 22 | return []; 23 | } 24 | 25 | public static function for(string $file, string $delimiter = ','): static 26 | { 27 | $lines = file($file); 28 | $lines[0] = str_replace("\xEF\xBB\xBF", '', $lines[0]); 29 | $csv = array_map(fn ($d) => str_getcsv(string: $d, separator: $delimiter, escape: '\\'), $lines); 30 | 31 | array_walk($csv, fn (&$a) => $a = array_combine($csv[0], $a)); 32 | array_shift($csv); 33 | 34 | return new static($csv); 35 | } 36 | 37 | public function rows(): array 38 | { 39 | return $this->toArray(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nico Hoffmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "distantnative/kirby-csv-field", 3 | "description": "Kirby CMS Panel field to upload and display a single CSV file", 4 | "license": "MIT", 5 | "type": "kirby-plugin", 6 | "keywords": [ 7 | "kirby-plugin", 8 | "csv", 9 | "spreadsheet", 10 | "field" 11 | ], 12 | "version": "1.1.0", 13 | "homepage": "https://distantnative.com/kirby-csv-field/", 14 | "authors": [ 15 | { 16 | "name": "Nico Hoffmann", 17 | "email": "nico@getkirby.com" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=8.1.0 <8.5.0", 22 | "getkirby/cms": "^4.0 || ^5.0", 23 | "getkirby/composer-installer": "^1.2" 24 | }, 25 | "extra": { 26 | "installer-name": "csv-field", 27 | "kirby-cms-path": false 28 | }, 29 | "config": { 30 | "allow-plugins": { 31 | "getkirby/composer-installer": true 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /i18n/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "field.csv.empty": "Wähle die CSV-Datei aus oder lade sie hoch", 3 | "field.csv.loading": "Lade Tabelle…" 4 | } 5 | -------------------------------------------------------------------------------- /i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "field.csv.empty": "Select or upload CSV file", 3 | "field.csv.loading": "Loading table…" 4 | } 5 | -------------------------------------------------------------------------------- /i18n/index.php: -------------------------------------------------------------------------------- 1 | this.clear()}]},file(){var e;return(e=this.selected[0])==null?void 0:e.id},index(){var e;if((e=this.csv)!=null&&e.pagination)return(this.csv.pagination.page-1)*this.csv.pagination.limit+1},pagination(){var e;return{...((e=this.csv)==null?void 0:e.pagination)??{},details:!0}},table(){return this.csv?{...this.csv,index:this.index,pagination:this.pagination}:{}}},watch:{file:{handler(){this.preview()},immediate:!0}},methods:{clear(){this.selected=[],this.csv=null,this.onInput()},onInput(){this.$emit("input",this.selected),this.preview()},async preview(e=1){this.file&&(this.csv=await this.$api.get(this.endpoints.field+"/preview",{file:this.file,page:e}))}}};var l=function(){var t=this,i=t._self._c;return i("k-field",t._b({staticClass:"k-models-field k-csv-field",scopedSlots:t._u([t.disabled?null:{key:"options",fn:function(){return[i("k-button-group",{ref:"buttons",staticClass:"k-field-options",attrs:{buttons:t.file?t.button:t.buttons,layout:"collapsed",size:"xs",variant:"filled"}})]},proxy:!0}],null,!0)},"k-field",t.$props,!1),[t.csv?i("k-table",t._b({on:{paginate:function(s){return t.preview(s.page)}}},"k-table",t.table,!1)):t.file?i("k-empty",{attrs:{text:t.$t("field.csv.loading"),icon:"loader",layout:"table"}}):i("k-dropzone",{attrs:{disabled:!t.hasDropzone},on:{drop:t.drop}},[i("k-empty",{attrs:{text:t.$t("field.csv.empty"),icon:"table",layout:"table"},on:{click:t.open}})],1)],1)},r=[],c=a(o,l,r);const d=c.exports;panel.plugin("distantnative/kirby-csv-field",{fields:{csv:d}})})(); 2 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | __DIR__ . '/Csv.php' 10 | ]); 11 | 12 | App::plugin('distantnative/kirby-csv-field', [ 13 | 'fields' => [ 14 | 'csv' => [ 15 | 'extends' => 'files', 16 | 'props' => [ 17 | /** 18 | * Unset inherited props 19 | */ 20 | 'default' => null, 21 | 'empty' => null, 22 | 'image' => null, 23 | 'info' => null, 24 | 'layout' => null, 25 | 'link' => null, 26 | 'max' => null, 27 | 'query' => null, 28 | 'search' => null, 29 | 'size' => null, 30 | 'text' => null, 31 | 32 | 'columns' => function (array|null $columns = null) { 33 | return $columns; 34 | }, 35 | 'delimiter' => function (string $delimiter = ',') { 36 | return $delimiter; 37 | }, 38 | 'limit' => function (int|null $limit = null) { 39 | return $limit; 40 | }, 41 | 'multiple' => function (bool $multiple = false) { 42 | return false; 43 | }, 44 | 'uploads' => function (array|bool $uploads = []) { 45 | if ($uploads === false) { 46 | return false; 47 | } 48 | 49 | if (is_string($uploads) === true) { 50 | $uploads = ['template' => $uploads]; 51 | } 52 | 53 | if (is_array($uploads) === false) { 54 | $uploads = []; 55 | } 56 | 57 | $uploads['accept'] = '.csv'; 58 | 59 | return $uploads; 60 | }, 61 | ], 62 | 'api' => fn () => [ 63 | ...(require $this->kirby()->core()->fields()['files'])['api'](), 64 | [ 65 | 'pattern' => 'preview', 66 | 'method' => 'GET', 67 | 'action' => function () { 68 | $field = $this->field(); 69 | $file = $this->requestQuery('file'); 70 | 71 | // read file and turn into Csv collection 72 | $csv = Csv::for( 73 | $field->model()->file($file)->root(), 74 | $field->delimiter() 75 | ); 76 | 77 | // gather columns or use 78 | // manually configured columns 79 | $columns = []; 80 | 81 | foreach ($field->columns() ?? $csv->columns() as $key => $column) { 82 | $key = match (true) { 83 | is_array($column) => $column['key'] ?? $key, 84 | is_string($key) => $key, 85 | default => $column 86 | }; 87 | 88 | $columns[$key] = match (true) { 89 | is_array($column) => $column, 90 | default => ['label' => $column] 91 | }; 92 | } 93 | 94 | // paginate rows 95 | if ($limit = $field->limit()) { 96 | $csv = $csv->paginate([ 97 | 'page' => $this->requestQuery('page', 1), 98 | 'limit' => $limit 99 | ]); 100 | } 101 | 102 | return [ 103 | 'columns' => $columns, 104 | 'rows' => $csv->rows(), 105 | 'pagination' => $csv->pagination()?->toArray() ?? false 106 | ]; 107 | } 108 | ] 109 | ] 110 | ] 111 | ], 112 | 'fieldMethods' => [ 113 | 'toCsv' => function (Field $field, string $delimiter = ','): Csv|null { 114 | if ($file = $field->toFiles()->first()) { 115 | return Csv::for($file->root(), $delimiter); 116 | } 117 | 118 | return null; 119 | } 120 | ], 121 | 'translations' => require_once __DIR__ . '/i18n/index.php' 122 | ]); 123 | --------------------------------------------------------------------------------