├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── PreparseField.php ├── fields └── PreparseFieldType.php ├── icon-mask.svg ├── icon.svg ├── migrations ├── m190226_225259_craft3.php └── m240711_230833_jalendport.php ├── services └── PreparseFieldService.php └── templates └── _components └── fields ├── _input.twig └── _settings.twig /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Preparse Field Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## 3.0.0-alpha.2 - 2024-07-15 8 | ### Fixed 9 | - Fixed reference to renamed method that was preventing preparse fields from rendering in the table view in certain cases ([#101](https://github.com/jalendport/craft-preparse/issues/101)) 10 | 11 | ## 3.0.0-alpha.1 - 2024-07-12 12 | ### Added 13 | - Initial Craft 5 release 14 | 15 | ## 2.1.2 - 2024-07-12 16 | ### Fixed 17 | - Added namespace aliasing to prevent integrations with other plugins/modules from breaking 18 | 19 | ## 2.1.1 - 2024-07-12 20 | ### Fixed 21 | - Fixed a bug where the `preparseFieldService` could not be found([#100](https://github.com/jalendport/craft-preparse/issues/100)) 22 | 23 | ## 2.1.0 - 2024-07-11 24 | ### Changed 25 | - Migrated to `jalendport/craft-preparse` 26 | 27 | ## 2.0.2 - 2022-12-05 28 | ### Fixed 29 | - Updated reference to Twigfield 30 | 31 | ## 2.0.1 - 2022-12-02 32 | ### Changed 33 | - Updated to use craft-code-editor instead of craft-twigfield ([#87](https://github.com/jalendport/craft-preparse/pull/87) - thanks @khalwat) 34 | 35 | ## 2.0.0 - 2022-08-08 36 | ### Added 37 | - Initial Craft 4 release 38 | 39 | ## 1.4.1 - 2022-12-02 40 | ### Changed 41 | - Updated to use craft-code-editor instead of craft-twigfield ([#86](https://github.com/jalendport/craft-preparse/pull/86) - thanks @khalwat) 42 | 43 | ## 1.4.0 - 2022-08-08 44 | ### Added 45 | - Added support for craft-twigfield ([#81](https://github.com/jalendport/craft-preparse/pull/81) - thanks @khalwat) 46 | 47 | ## 1.3.0 - 2022-08-06 48 | ### Added 49 | - Added datetime column type option ([#63](https://github.com/jalendport/craft-preparse/pull/63) - thanks @mmikkel) 50 | 51 | ## 1.2.5 - 2021-07-02 52 | ### Fixed 53 | - Reverted [#66](https://github.com/jalendport/craft-preparse/pull/66) due to bug where sometimes the element couldn't be re-fetched from the database ([#70](https://github.com/jalendport/craft-preparse/issues/70), [#71](https://github.com/jalendport/craft-preparse/issues/71), [#72](https://github.com/jalendport/craft-preparse/issues/72), [#73](https://github.com/jalendport/craft-preparse/issues/73)) 54 | - Fixed a bug causing missing Matrix blocks on elements in certain cases ([#69](https://github.com/jalendport/craft-preparse/issues/69)) 55 | 56 | ## 1.2.4 - 2021-02-24 57 | ### Fixed 58 | - Fixed a bug preventing elements from saving successfully in certain multisite setups ([#70](https://github.com/jalendport/craft-preparse/pull/70)) 59 | 60 | ## 1.2.3 - 2021-02-23 61 | ### Fixed 62 | - Fixed a bug causing missing Matrix blocks on new elements ([#66](https://github.com/jalendport/craft-preparse/pull/66) - thanks @monachilada) 63 | 64 | ## 1.2.2 - 2020-11-30 65 | ### Fixed 66 | - Fixed a bug causing missing Matrix blocks on revisions ([#65](https://github.com/jalendport/craft-preparse/pull/65) - thanks @brandonkelly) 67 | 68 | ## 1.2.1 - 2020-06-25 69 | ### Fixed 70 | - Fixed incorrect branch names in README and composer.json 71 | 72 | ## 1.2.0 - 2020-06-25 73 | Transfer of ownership... 74 | 75 | ### Added 76 | - Added a class alias so sites with Preparse currently installed will continue to function smoothly after the namespace change 77 | 78 | ## 1.1.0 - 2019-08-03 79 | ### Fixed 80 | - Fixes compability issues with Craft 3.2 (Thanks, @brandonkelly). 81 | 82 | ### Added 83 | - Added `SortableFieldInterface` to field type. 84 | 85 | ### Changed 86 | - Changed composer requirement for `craftcms/cms` to `^3.2.0`. 87 | 88 | ## 1.0.7 - 2019-08-03 89 | ### Changed 90 | - Replaced `unset()` on `$_FILES` with setting it to an empty array (fixes #52). 91 | 92 | ## 1.0.6 - 2019-03-21 93 | ### Fixed 94 | - Fixed a bug where warnings weren’t showing up when editing an existing preparse field’s column type. 95 | 96 | ## 1.0.5.1 - 2019-02-27 97 | ### Fixed 98 | - Fixed an error that occurred when updating to preparse 1.0.5 on Craft 3.0.x 99 | 100 | ## 1.0.5 - 2019-02-27 101 | ### Added 102 | - Adds Craft 3 migrations. (thanks @carlcs). 103 | 104 | ## 1.0.4 - 2018-12-16 105 | ### Added 106 | - Adds support for showing preparse fields in element indexes (#33) (thanks @benface). 107 | 108 | ## 1.0.3 - 2018-10-24 109 | ### Fixed 110 | - Fixed an issue (#45) that would occure when uploading files through a front-end form for elements with a preparse field (thanks @aaronwaldon and @ademers). 111 | 112 | ## 1.0.2 - 2018-08-01 113 | ### Fixed 114 | - Fixed a bug that would keep preparse fields on assets from parsing on first save/upload (#37). 115 | - Fixes a bug where preparse fields could not be hidden in asset element modals and matrixblocks. 116 | 117 | ## 1.0.1 - 2018-07-30 118 | ### Added 119 | - Added support for DECIMAL column types. 120 | 121 | ### Fixed 122 | - Fixed an issue that would result in a duplicate key exception in multisite installations. 123 | 124 | ## 1.0.0 - 2017-12-02 125 | ### Added 126 | - Initial Craft 3 release. 127 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Jalen Davenport 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Preparse Field for Craft 2 | 3 | A fieldtype that parses Twig when an element is saved and saves the result as plain text. 4 | 5 | ## Requirements 6 | 7 | This plugin requires Craft CMS 5.0.0 or later and PHP 8.2.0 or later. 8 | 9 | ## Installation 10 | 11 | To install the plugin, follow these instructions. 12 | 13 | 1. Open your terminal and go to your Craft project: 14 | 15 | cd /path/to/project 16 | 17 | 2. Then tell Composer to load the plugin: 18 | 19 | composer require jalendport/craft-preparse 20 | 21 | 3. In the Control Panel, go to Settings → Plugins and click the “Install” button for Preparse Field. 22 | 23 | ## Usage 24 | 25 | When creating a new Preparse field, you add the Twig that you want run to the field's settings. When an element with that Preparse field is saved, the code will be parsed and the resulting value saved as plain text. 26 | 27 | It's worth noting that the Preparse field is only updated when the element the field is on is saved. If you grab data from a related element (like in the category title example below), and then update the related element, the preparsed value will not automatically be updated. 28 | 29 | In the Twig, the element that the Preparse field is added to is available as a variable named `element`. It's best to use this variable (as opposed to something like `entry` or `asset`) because it's possible you add the same Preparse field to multiple element types. This also means that when a Preparse field is added to a Matrix, SuperTable, or Neo block, that block will be what is available as `element`, so if you want to access the element that the Matrix/SuperTable/Neo field belongs to, you will want to use `element.owner`. 30 | 31 | ### Examples 32 | 33 | If you have a category field on your element named `relatedCategory`, you can save the category title to the Preparse field by adding the following Twig to the field settings: 34 | 35 | {{ element.relatedCategory.one().title ?? '' }} 36 | 37 | This is useful for saving preparsed values to a field for use with sorting, searching, or similar things. 38 | 39 | You can also do more advanced stuff, for instance performance optimizing. Let's say you have three different asset fields that may or may not be populated. Having to check these in the template may require a bunch of queries since you can't check if a field has a relation in Craft without actually querying for it. You could do something like this to get the id of the asset to use: 40 | 41 | {% if element.smallListImage | length %} 42 | {{ element.smallListImage.one().id }} 43 | {% elseif element.largeListImage | length %} 44 | {{ element.largeListImage.one().id }} 45 | {% elseif element.mainImage | length %} 46 | {{ element.mainImage.one().id }} 47 | {% endif %} 48 | 49 | _You'd probably want to wrap that in `{% apply spaceless %} ... {% endapply %}` to make it more useful..._ 50 | 51 | Or you could just use it to do some bulk work when saving, like pre-generating a bunch of image transforms with [Imager X](https://plugins.craftcms.com/imager-x?craft4): 52 | 53 | {% if element.mainImage | length %} 54 | {% set transformedImages = craft.imager.transformImage(element.mainImage.one(), [ 55 | { width: 1000 }, 56 | { width: 900 }, 57 | { width: 800 }, 58 | { width: 700 }, 59 | { width: 600 }, 60 | { width: 500 }, 61 | { width: 400 }, 62 | { width: 300 }, 63 | { width: 200 }, 64 | { width: 100 } 65 | ]) %} 66 | {% endif %} 67 | 68 | Preparse also has access to your site's template root, so you can even include local templates if you want to do more advanced stuff and/or want to keep your field's Twig in version control: 69 | 70 | {% include '_partials/customPreparseFieldStuff' %} 71 | 72 | Make sure that you always write solid Twig, taking into account that fields may not be populated yet. If an error occurs in your Twig, the element will not be saved. [Code defensively!](https://nystudio107.com/blog/handling-errors-gracefully-in-craft-cms#defensive-coding-in-twig) 73 | 74 | ## Price, License, and Support 75 | 76 | The plugin is released under the MIT license, meaning you can do whatever you want with it as long as you don't blame us. **It's free**, which means there is absolutely no support included, but you might get it anyway. Just post an issue here on GitHub if you have one, and we'll see what we can do. :) 77 | 78 | ## Changelog 79 | 80 | See the [changelog file](https://github.com/jalendport/craft-preparse/blob/master/CHANGELOG.md). 81 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jalendport/craft-preparse", 3 | "description": "A fieldtype that parses Twig when an element is saved and saves the result as plain text.", 4 | "type": "craft-plugin", 5 | "version": "3.0.0-alpha.2", 6 | "keywords": [ 7 | "craft", 8 | "cms", 9 | "craftcms", 10 | "craft-plugin", 11 | "fieldtype", 12 | "preparse", 13 | "twig" 14 | ], 15 | "support": { 16 | "docs": "https://github.com/jalendport/craft-preparse/blob/master/README.md", 17 | "issues": "https://github.com/jalendport/craft-preparse/issues" 18 | }, 19 | "license": "MIT", 20 | "authors": [ 21 | { 22 | "name": "Jalen Davenport", 23 | "homepage": "https://jalendport.com/" 24 | }, 25 | { 26 | "name": "André Elvan", 27 | "homepage": "https://www.vaersaagod.no" 28 | } 29 | ], 30 | "require": { 31 | "craftcms/cms": "^5.0.0", 32 | "nystudio107/craft-code-editor": "^1.0.0", 33 | "php": "^8.2.0" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "jalendport\\preparse\\": "src/", 38 | "aelvan\\preparsefield\\": "src/", 39 | "besteadfast\\preparsefield\\": "src/" 40 | } 41 | }, 42 | "extra": { 43 | "name": "Preparse", 44 | "handle": "preparse-field", 45 | "schemaVersion": "1.1.0", 46 | "hasCpSettings": false, 47 | "hasCpSection": false, 48 | "changelogUrl": "https://github.com/jalendport/craft-preparse/blob/master/CHANGELOG.md", 49 | "components": { 50 | "preparseFieldService": "jalendport\\preparse\\services\\PreparseFieldService" 51 | }, 52 | "class": "jalendport\\preparse\\PreparseField" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/PreparseField.php: -------------------------------------------------------------------------------- 1 | preparsedElements = [ 58 | 'onBeforeSave' => [], 59 | 'onPropagate' => [], 60 | 'onMoveElement' => [], 61 | ]; 62 | 63 | // Register our fields 64 | Event::on( 65 | Fields::class, 66 | Fields::EVENT_REGISTER_FIELD_TYPES, 67 | static function (RegisterComponentTypesEvent $event) { 68 | $event->types[] = PreparseFieldType::class; 69 | } 70 | ); 71 | 72 | // Before save element event handler 73 | Event::on( 74 | Elements::class, 75 | Elements::EVENT_BEFORE_SAVE_ELEMENT, 76 | function (ElementEvent $event) { 77 | if ($event->element->getIsRevision()) { 78 | return; 79 | } 80 | 81 | /** @var Element $element */ 82 | $element = $event->element; 83 | $key = $element->id . '__' . $element->siteId; 84 | 85 | if (!isset($this->preparsedElements['onBeforeSave'][$key])) { 86 | $this->preparsedElements['onBeforeSave'][$key] = true; 87 | 88 | $content = self::$plugin->preparseFieldService->getPreparseFieldsContent($element, 'onBeforeSave'); 89 | 90 | if (!empty($content)) { 91 | $this->resetUploads(); 92 | $element->setFieldValues($content); 93 | } 94 | 95 | unset($this->preparsedElements['onBeforeSave'][$key]); 96 | } 97 | } 98 | ); 99 | 100 | // After propagate element event handler 101 | Event::on( 102 | Element::class, 103 | Element::EVENT_AFTER_PROPAGATE, 104 | function (ModelEvent $event) { 105 | /** @var Element $element */ 106 | $element = $event->sender; 107 | 108 | if ($element->getIsRevision()) { 109 | return; 110 | } 111 | 112 | $key = $element->id . '__' . $element->siteId; 113 | 114 | if (!isset($this->preparsedElements['onPropagate'][$key])) { 115 | $this->preparsedElements['onPropagate'][$key] = true; 116 | 117 | $content = self::$plugin->preparseFieldService->getPreparseFieldsContent($element, 'onPropagate'); 118 | 119 | if (!empty($content)) { 120 | $this->resetUploads(); 121 | 122 | if ($element instanceof Asset) { 123 | $element->setScenario(Element::SCENARIO_DEFAULT); 124 | } 125 | 126 | $element->setFieldValues($content); 127 | $success = Craft::$app->elements->saveElement($element, true, false); 128 | 129 | // if no success, log error 130 | if (!$success) { 131 | Craft::error('Couldn’t save element with id “' . $element->id . '”', __METHOD__); 132 | } 133 | } 134 | 135 | unset($this->preparsedElements['onPropagate'][$key]); 136 | } 137 | } 138 | ); 139 | 140 | // After move element event handler 141 | Event::on( 142 | Structures::class, 143 | Structures::EVENT_AFTER_MOVE_ELEMENT, 144 | function (MoveElementEvent $event) { 145 | /** @var Element $element */ 146 | $element = $event->element; 147 | $key = $element->id . '__' . $element->siteId; 148 | 149 | if (self::$plugin->preparseFieldService->shouldParseElementOnMove($element) && !isset($this->preparsedElements['onMoveElement'][$key])) { 150 | $this->preparsedElements['onMoveElement'][$key] = true; 151 | 152 | if ($element instanceof Asset) { 153 | $element->setScenario(Element::SCENARIO_DEFAULT); 154 | } 155 | 156 | $success = Craft::$app->getElements()->saveElement($element, true, false); 157 | 158 | // if no success, log error 159 | if (!$success) { 160 | Craft::error('Couldn’t move element with id “' . $element->id . '”', __METHOD__); 161 | } 162 | 163 | unset($this->preparsedElements['onMoveElement'][$key]); 164 | } 165 | } 166 | ); 167 | } 168 | 169 | /** 170 | * @param $msg 171 | * @param string $level 172 | * @param string $file 173 | */ 174 | public static function log($msg, string $level = 'notice', string $file = 'Preparse') 175 | { 176 | try 177 | { 178 | $file = Craft::getAlias('@storage/logs/' . $file . '.log'); 179 | $log = "\n" . date('Y-m-d H:i:s') . " [{$level}]" . "\n" . print_r($msg, true); 180 | FileHelper::writeToFile($file, $log, ['append' => true]); 181 | } 182 | catch(Exception $e) 183 | { 184 | Craft::error($e->getMessage()); 185 | } 186 | } 187 | 188 | /** 189 | * @param $msg 190 | * @param string $level 191 | * @param string $file 192 | */ 193 | public static function error($msg, string $level = 'error', string $file = 'Preparse') 194 | { 195 | static::log($msg, $level, $file); 196 | } 197 | 198 | /** 199 | * Fix file uploads being processed twice by craft, which causes an error. 200 | * 201 | * @see https://github.com/jalendport/craft-preparse/issues/23#issuecomment-284682292 202 | */ 203 | private function resetUploads() 204 | { 205 | $_FILES = []; 206 | UploadedFile::reset(); 207 | } 208 | } 209 | 210 | class_alias(PreparseField::class, \aelvan\preparsefield\PreparseField::class); 211 | class_alias(PreparseField::class, \besteadfast\preparsefield\PreparseField::class); 212 | -------------------------------------------------------------------------------- /src/fields/PreparseFieldType.php: -------------------------------------------------------------------------------- 1 | ''], 75 | ['columnType', 'string'], 76 | ['columnType', 'default', 'value' => ''], 77 | ['decimals', 'number'], 78 | ['decimals', 'default', 'value' => 0], 79 | ['textareaRows', 'number'], 80 | ['textareaRows', 'default', 'value' => 5], 81 | ['parseBeforeSave', 'boolean'], 82 | ['parseBeforeSave', 'default', 'value' => false], 83 | ['parseOnMove', 'boolean'], 84 | ['parseOnMove', 'default', 'value' => false], 85 | ['displayType', 'string'], 86 | ['displayType', 'default', 'value' => 'hidden'], 87 | ['allowSelect', 'boolean'], 88 | ['allowSelect', 'default', 'value' => false], 89 | ]); 90 | } 91 | 92 | /** 93 | * @return array|string 94 | * @throws Exception 95 | */ 96 | public function getContentColumnType(): array|string 97 | { 98 | if ($this->columnType === Schema::TYPE_DECIMAL) { 99 | return Db::getNumericalColumnType(null, null, $this->decimals); 100 | } 101 | 102 | return $this->columnType; 103 | } 104 | 105 | /** 106 | * @return null|string 107 | * @throws LoaderError 108 | * @throws RuntimeError 109 | * @throws SyntaxError|Exception 110 | */ 111 | public function getSettingsHtml(): ?string 112 | { 113 | $columns = [ 114 | Schema::TYPE_TEXT => Craft::t('preparse-field', 'Text (stores about 64K)'), 115 | Schema::TYPE_MEDIUMTEXT => Craft::t('preparse-field', 'Mediumtext (stores about 16MB)'), 116 | Schema::TYPE_INTEGER => Craft::t('preparse-field', 'Number (integer)'), 117 | Schema::TYPE_DECIMAL => Craft::t('preparse-field', 'Number (decimal)'), 118 | Schema::TYPE_FLOAT => Craft::t('preparse-field', 'Number (float)'), 119 | Schema::TYPE_DATETIME => Craft::t('preparse-field', 'Date (datetime)'), 120 | ]; 121 | 122 | $displayTypes = [ 123 | 'hidden' => 'Hidden', 124 | 'textinput' => 'Text input', 125 | 'textarea' => 'Textarea', 126 | ]; 127 | 128 | // Render the settings template 129 | return Craft::$app->getView()->renderTemplate( 130 | 'preparse-field/_components/fields/_settings', 131 | [ 132 | 'field' => $this, 133 | 'columns' => $columns, 134 | 'displayTypes' => $displayTypes, 135 | 'existing' => $this->id !== null, 136 | ] 137 | ); 138 | } 139 | 140 | /** 141 | * @param mixed $value 142 | * @param ElementInterface|null $element 143 | * 144 | * @return string 145 | * @throws LoaderError 146 | * @throws RuntimeError 147 | * @throws SyntaxError|Exception 148 | */ 149 | public function getInputHtml(mixed $value, ?ElementInterface $element = null): string 150 | { 151 | // Get our id and namespace 152 | $id = Craft::$app->getView()->formatInputId($this->handle); 153 | $namespacedId = Craft::$app->getView()->namespaceInputId($id); 154 | 155 | // Render the input template 156 | $displayType = $this->displayType; 157 | if ($displayType !== 'hidden' && $this->columnType === Schema::TYPE_DATETIME) { 158 | $displayType = 'date'; 159 | } 160 | return Craft::$app->getView()->renderTemplate( 161 | 'preparse-field/_components/fields/_input', 162 | [ 163 | 'name' => $this->handle, 164 | 'value' => $value, 165 | 'field' => $this, 166 | 'id' => $id, 167 | 'namespacedId' => $namespacedId, 168 | 'displayType' => $displayType, 169 | ] 170 | ); 171 | } 172 | 173 | /** 174 | * @inheritdoc 175 | */ 176 | public function getSearchKeywords(mixed $value, ElementInterface $element): string 177 | { 178 | if ($this->columnType === Schema::TYPE_DATETIME) { 179 | return ''; 180 | } 181 | return parent::getSearchKeywords($value, $element); 182 | } 183 | 184 | /** 185 | * @inheritdoc 186 | * @throws InvalidConfigException 187 | */ 188 | public function getPreviewHtml(mixed $value, ElementInterface $element): string 189 | { 190 | if (!$value) { 191 | return ''; 192 | } 193 | 194 | if ($this->columnType === Schema::TYPE_DATETIME) { 195 | return Craft::$app->getFormatter()->asDatetime($value, Locale::LENGTH_SHORT); 196 | } 197 | 198 | return parent::getPreviewHtml($value, $element); 199 | } 200 | 201 | /** 202 | * @inheritdoc 203 | * @throws \Exception 204 | */ 205 | public function normalizeValue(mixed $value, ?ElementInterface $element = null): mixed 206 | { 207 | if ($this->columnType === Schema::TYPE_DATETIME) { 208 | if ($value && ($date = DateTimeHelper::toDateTime($value)) !== false) { 209 | return $date; 210 | } 211 | return null; 212 | } 213 | return parent::normalizeValue($value, $element); 214 | } 215 | 216 | /** 217 | * @inheritdoc 218 | */ 219 | public function modifyElementsQuery(ElementQueryInterface $query, mixed $value): void 220 | { 221 | if ($this->columnType === Schema::TYPE_DATETIME) { 222 | if ($value !== null) { 223 | /** @var ElementQuery $query */ 224 | $query->subQuery->andWhere(Db::parseDateParam('content.' . Craft::$app->getContent()->fieldColumnPrefix . $this->handle, $value)); 225 | } 226 | } 227 | parent::modifyElementsQuery($query, $value); 228 | } 229 | 230 | /** 231 | * @inheritdoc 232 | */ 233 | public function getContentGqlType(): Type|array 234 | { 235 | if ($this->columnType === Schema::TYPE_DATETIME) { 236 | return DateTimeType::getType(); 237 | } 238 | return parent::getContentGqlType(); 239 | } 240 | } 241 | 242 | class_alias(PreparseFieldType::class, \aelvan\preparsefield\fields\PreparseFieldType::class); 243 | class_alias(PreparseFieldType::class, \besteadfast\preparsefield\fields\PreparseFieldType::class); 244 | -------------------------------------------------------------------------------- /src/icon-mask.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 9 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/icon.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 9 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/migrations/m190226_225259_craft3.php: -------------------------------------------------------------------------------- 1 | update('{{%fields}}', ['type' => PreparseFieldType::class], ['type' => 'PreparseField_Preparse']); 19 | } 20 | 21 | /** 22 | * @inheritdoc 23 | */ 24 | public function safeDown() 25 | { 26 | echo "m190226_225259_craft3 cannot be reverted.\n"; 27 | return false; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/migrations/m240711_230833_jalendport.php: -------------------------------------------------------------------------------- 1 | update('{{%fields}}', ['type'=> PreparseFieldType::class], ['type' => $oldType]); 24 | 25 | // Don’t make the same config changes twice 26 | $projectConfig = Craft::$app->getProjectConfig(); 27 | $schemaVersion = $projectConfig->get('plugins.preparse-field.schemaVersion', true); 28 | 29 | if (version_compare($schemaVersion, '1.1.0', '>=')) 30 | { 31 | return true; 32 | } 33 | 34 | $fields = $projectConfig->get('fields') ?? []; 35 | 36 | foreach ($fields as $fieldUid => $field) 37 | { 38 | if ($field['type'] === $oldType) 39 | { 40 | $field['type'] = PreparseFieldType::class; 41 | try { 42 | $projectConfig->set("fields.{$fieldUid}", $field); 43 | } catch (Exception|ErrorException $e) { 44 | return false; 45 | } 46 | } 47 | } 48 | 49 | return true; 50 | } 51 | 52 | /** 53 | * @inheritdoc 54 | */ 55 | public function safeDown(): bool 56 | { 57 | echo "m240711_230833_jalendport cannot be reverted.\n"; 58 | return false; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/services/PreparseFieldService.php: -------------------------------------------------------------------------------- 1 | getFieldLayout(); 42 | 43 | if ($fieldLayout) { 44 | foreach ($fieldLayout->getCustomFields() as $field) { 45 | if ($field instanceof PreparseFieldType) { 46 | /** @var PreparseFieldType $field */ 47 | 48 | // only get field content for the right event listener 49 | $isBeforeSave = ($eventHandle === 'onBeforeSave'); 50 | $parseBeforeSave = (bool)$field->parseBeforeSave; 51 | 52 | if ($isBeforeSave === $parseBeforeSave) { 53 | $fieldValue = $this->parseField($field, $element); 54 | 55 | if ($fieldValue !== null) { 56 | $content[$field->handle] = $fieldValue; 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | return $content; 64 | } 65 | 66 | /** 67 | * Parses field for a given element. 68 | * 69 | * @param PreparseFieldType $field 70 | * @param Element $element 71 | * 72 | * @return null|string|DateTime 73 | * @throws Exception 74 | */ 75 | public function parseField(PreparseFieldType $field, Element $element): DateTime|string|null 76 | { 77 | $fieldTwig = $field->fieldTwig; 78 | $columnType = $field->columnType; 79 | $decimals = $field->decimals; 80 | $fieldValue = null; 81 | 82 | $elementTemplateName = 'element'; 83 | 84 | if (method_exists($element, 'refHandle')) { 85 | $elementTemplateName = strtolower($element->refHandle()); 86 | } 87 | 88 | // Enable generateTransformsBeforePageLoad always 89 | $generateTransformsBeforePageLoad = Craft::$app->config->general->generateTransformsBeforePageLoad; 90 | Craft::$app->config->general->generateTransformsBeforePageLoad = true; 91 | 92 | // save cp template path and set to site templates 93 | $oldMode = Craft::$app->view->getTemplateMode(); 94 | Craft::$app->view->setTemplateMode(View::TEMPLATE_MODE_SITE); 95 | 96 | // render value from the field template 97 | try { 98 | $vars = array_merge(['element' => $element], [$elementTemplateName => $element]); 99 | $fieldValue = Craft::$app->view->renderString($fieldTwig, $vars); 100 | } catch (\Exception $e) { 101 | Craft::error('Couldn’t render value for element with id “'.$element->id.'” and preparse field “'. 102 | $field->handle.'” ('.$e->getMessage().').', __METHOD__); 103 | } 104 | 105 | // restore cp template paths 106 | Craft::$app->view->setTemplateMode($oldMode); 107 | 108 | // set generateTransformsBeforePageLoad back to whatever it was 109 | Craft::$app->config->general->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; 110 | 111 | if (null === $fieldValue) { 112 | return null; 113 | } 114 | 115 | if ($columnType === Schema::TYPE_FLOAT || $columnType === Schema::TYPE_INTEGER) { 116 | if ($decimals > 0) { 117 | return number_format(trim($fieldValue), $decimals, '.', ''); 118 | } 119 | 120 | return number_format(trim($fieldValue), 0, '.', ''); 121 | } else if ($columnType === Schema::TYPE_DATETIME) { 122 | $fieldValue = \trim($fieldValue); 123 | if (!$fieldValue || !$date = DateTimeHelper::toDateTime($fieldValue, true)) { 124 | // Return an empty string rather than null to clear out existing DateTime value (null would mean "no change") 125 | return ''; 126 | } 127 | return $date; 128 | } 129 | 130 | return $fieldValue; 131 | } 132 | 133 | /** 134 | * Checks to see if an element has a preparse field that should be saved on move 135 | * 136 | * @param Element $element 137 | * 138 | * @return bool 139 | */ 140 | public function shouldParseElementOnMove(Element $element): bool 141 | { 142 | $fieldLayout = $element->getFieldLayout(); 143 | 144 | if ($fieldLayout) { 145 | foreach ($fieldLayout->getCustomFields() as $field) { 146 | if ($field instanceof PreparseFieldType) { 147 | $parseOnMove = $field->parseOnMove; 148 | 149 | if ($parseOnMove) { 150 | return true; 151 | } 152 | } 153 | } 154 | } 155 | 156 | return false; 157 | } 158 | } 159 | 160 | class_alias(PreparseFieldService::class, \aelvan\preparsefield\services\PreparseFieldService::class); 161 | class_alias(PreparseFieldService::class, \besteadfast\preparsefield\services\PreparseFieldService::class); 162 | -------------------------------------------------------------------------------- /src/templates/_components/fields/_input.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * Preparse Field plugin for Craft CMS 4.x 4 | * 5 | * Field Input 6 | */ 7 | #} 8 | 9 | {% import "_includes/forms" as forms %} 10 | 11 | {% set displayType = displayType ?? 'hidden' %} 12 | 13 | {% if displayType == 'hidden' %} 14 | 19 | {% else %} 20 | {# Setup our field #} 21 | {% if displayType == 'date' %} 22 | {{ forms.dateTimeField({ 23 | value: value, 24 | disabled: true, 25 | }) }} 26 | 31 | {% elseif displayType == 'textarea' %} 32 | {{ forms.textarea({ 33 | value: value, 34 | disabled: not field['allowSelect'], 35 | readonly: field['allowSelect'], 36 | rows: field['textareaRows'] 37 | }) }} 38 | {% else %} 39 | {{ forms.text({ 40 | value: value, 41 | disabled: not field['allowSelect'], 42 | readonly: field['allowSelect'] 43 | }) }} 44 | {% endif %} 45 | {% endif %} 46 | -------------------------------------------------------------------------------- /src/templates/_components/fields/_settings.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * Preparse Field plugin for Craft CMS 4.x 4 | * 5 | * Field Settings 6 | */ 7 | #} 8 | 9 | {% import "_includes/forms" as forms %} 10 | {% import "codeeditor/codeEditor" as codeEditor %} 11 | 12 | {% set monacoOptions = { 13 | } %} 14 | {% set codeEditorOptions = { 15 | wrapperClass:"monaco-editor-background-frame" 16 | } %} 17 | {{ codeEditor.textareaField( { 18 | label: "Twig code to parse"|t, 19 | instructions: "Enter the twig code that you want to parse after the entry has been saved.\nIf the column type is set to Date (datetime), the parsed Twig should output a date formatted as `Y-m-d H:i:s`."|t, 20 | id: 'fieldTwig', 21 | name: 'fieldTwig', 22 | value: field['fieldTwig'], 23 | class: 'code', 24 | rows: 10, 25 | }, "CodeField", monacoOptions, codeEditorOptions) }} 26 | 27 | {% set columnType %} 28 | {{ forms.select({ 29 | id: 'columnType', 30 | name: 'columnType', 31 | options: columns, 32 | value: field['columnType'], 33 | }) }} 34 | {% endset %} 35 | 36 | {{ forms.field({ 37 | label: "Column Type"|t, 38 | instructions: "The underlying database column type to use when saving content."|t, 39 | id: 'columnType', 40 | warning: (existing ? "Changing this may result in data loss."|t), 41 | }, columnType) }} 42 | 43 | {{ forms.textField({ 44 | label: 'Decimals'|t, 45 | instructions: "Only relevant if the column type is Number (float)."|t, 46 | id: 'decimals', 47 | name: 'decimals', 48 | size: 2, 49 | maxlength: 2, 50 | value: field['decimals'] 51 | }) }} 52 | 53 | {{ forms.lightswitchField({ 54 | label: "Parse before save"|t, 55 | instructions: "If you turn this on, the field will be parsed before the element is saved."|t, 56 | id: 'parseBeforeSave', 57 | name: 'parseBeforeSave', 58 | on: field['parseBeforeSave'], 59 | onLabel: "Yes"|t, 60 | offLabel: "no"|t 61 | }) }} 62 | 63 | {{ forms.lightswitchField({ 64 | label: "Parse on move"|t, 65 | instructions: "If you turn this on, elements that contain this field will be resaved when an element is moved in a structure. Necessary if you use .parent() or similar structure-related code in your twig."|t, 66 | id: 'parseOnMove', 67 | name: 'parseOnMove', 68 | on: field['parseOnMove'], 69 | onLabel: "Yes"|t, 70 | offLabel: "no"|t 71 | }) }} 72 | 73 | {% set displayType %} 74 | {{ forms.select({ 75 | id: 'displayType', 76 | name: 'displayType', 77 | options: displayTypes, 78 | value: field['displayType'], 79 | }) }} 80 | {% endset %} 81 | 82 | {{ forms.field({ 83 | label: "Display Type"|t, 84 | instructions: "Select how this field is to be shown in the edit page.\nIf the column type is set to Date (datetime), any other option than \"Hidden\" will render the field value in a disabled datepicker."|t, 85 | id: 'displayType', 86 | }, displayType) }} 87 | 88 | {{ forms.textField({ 89 | label: 'Textarea rows'|t, 90 | instructions: "Only relevant if the display type is Textarea."|t, 91 | id: 'textareaRows', 92 | name: 'textareaRows', 93 | size: 2, 94 | maxlength: 2, 95 | value: field['textareaRows'] 96 | }) }} 97 | 98 | {{ forms.lightswitchField({ 99 | label: "Allow text selection"|t, 100 | instructions: "If you turn this on and the field is visible, the output of the field will be selectable by the user."|t, 101 | id: 'allowSelect', 102 | name: 'allowSelect', 103 | on: field['allowSelect'], 104 | onLabel: "Yes"|t, 105 | offLabel: "no"|t 106 | }) }} 107 | --------------------------------------------------------------------------------