├── phpstan.neon
├── src
├── assetbundles
│ ├── units
│ │ ├── dist
│ │ │ ├── css
│ │ │ │ └── Units.css
│ │ │ ├── js
│ │ │ │ └── Units.js
│ │ │ └── img
│ │ │ │ └── Units-icon.svg
│ │ └── UnitsAsset.php
│ └── unitsfield
│ │ ├── dist
│ │ ├── css
│ │ │ └── Units.css
│ │ ├── js
│ │ │ └── Units.js
│ │ └── img
│ │ │ └── Units-icon.svg
│ │ └── UnitsFieldAsset.php
├── gql
│ └── types
│ │ ├── UnitsDataType.php
│ │ └── generators
│ │ └── UnitsDataGenerator.php
├── templates
│ ├── _components
│ │ └── fields
│ │ │ ├── Units_input.twig
│ │ │ └── Units_settings.twig
│ └── settings.twig
├── translations
│ └── en
│ │ └── units.php
├── config.php
├── helpers
│ ├── ClassHelper.php
│ └── ClassMapGenerator.php
├── controllers
│ └── UnitsController.php
├── models
│ ├── Settings.php
│ └── UnitsData.php
├── icon.svg
├── validators
│ └── EmbeddedUnitsDataValidator.php
├── Units.php
├── variables
│ └── UnitsVariable.php
└── fields
│ └── Units.php
├── ecs.php
├── CHANGELOG.md
├── Makefile
├── LICENSE.md
├── composer.json
└── README.md
/phpstan.neon:
--------------------------------------------------------------------------------
1 | includes:
2 | - %currentWorkingDirectory%/vendor/craftcms/phpstan/phpstan.neon
3 |
4 | parameters:
5 | level: 5
6 | paths:
7 | - src
8 |
--------------------------------------------------------------------------------
/src/assetbundles/units/dist/css/Units.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Units plugin for Craft CMS
3 | *
4 | * Units CSS
5 | *
6 | * @author nystudio107
7 | * @copyright Copyright (c) nystudio107
8 | * @link https://nystudio107.com/
9 | * @package Units
10 | * @since 1.0.0
11 | */
12 |
--------------------------------------------------------------------------------
/ecs.php:
--------------------------------------------------------------------------------
1 | paths([
8 | __DIR__ . '/src',
9 | __FILE__,
10 | ]);
11 | $ecsConfig->parallel();
12 | $ecsConfig->sets([SetList::CRAFT_CMS_4]);
13 | };
14 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Units Changelog
2 |
3 | ## 5.0.1 - 2025.06.13
4 | ### Fixed
5 | * Pinned the `php-units-of-measure` to version `~2.1.0` to avoid breaking changes in non-major releases
6 |
7 | ## 5.0.0 - 2025.01.19
8 | ### Added
9 | * Initial Craft CMS 5 release
10 | * Add a GraphQL interface for Units fields, closes ([#5](https://github.com/nystudio107/craft-units/issues/5))
11 |
--------------------------------------------------------------------------------
/src/assetbundles/unitsfield/dist/css/Units.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Units plugin for Craft CMS
3 | *
4 | * Units Field CSS
5 | *
6 | * @author nystudio107
7 | * @copyright Copyright (c) nystudio107
8 | * @link https://nystudio107.com/
9 | * @package Units
10 | * @since 1.0.0
11 | */
12 |
13 | .units-field-units {
14 | margin: -20px 9px;
15 | }
16 |
17 | .units-field-units-select {
18 | margin: -16px 0px;
19 | }
20 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | MAJOR_VERSION?=5
2 | PLUGINDEV_PROJECT_DIR?=/Users/andrew/webdev/sites/plugindev/cms_v${MAJOR_VERSION}/
3 | VENDOR?=nystudio107
4 | PROJECT_PATH?=${VENDOR}/$(shell basename $(CURDIR))
5 |
6 | .PHONY: dev docs release
7 |
8 | # Start up the buildchain dev server
9 | dev:
10 | # Start up the docs dev server
11 | docs:
12 | ${MAKE} -C docs/ dev
13 | # Run code quality tools, tests, and build the buildchain & docs in preparation for a release
14 | release: --code-quality --code-tests --buildchain-clean-build --docs-clean-build
15 | # The internal targets used by the dev & release targets
16 | --buildchain-clean-build:
17 | --code-quality:
18 | ${MAKE} -C ${PLUGINDEV_PROJECT_DIR} -- ecs check vendor/${PROJECT_PATH}/src --fix
19 | ${MAKE} -C ${PLUGINDEV_PROJECT_DIR} -- phpstan analyze -c vendor/${PROJECT_PATH}/phpstan.neon
20 | --code-tests:
21 | --docs-clean-build:
22 | ${MAKE} -C docs/ clean
23 | ${MAKE} -C docs/ image-build
24 | ${MAKE} -C docs/ fix
25 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 nystudio107
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.
--------------------------------------------------------------------------------
/src/assetbundles/units/UnitsAsset.php:
--------------------------------------------------------------------------------
1 | sourcePath = "@nystudio107/units/assetbundles/units/dist";
32 |
33 | $this->depends = [
34 | CpAsset::class,
35 | ];
36 |
37 | $this->js = [
38 | 'js/Units.js',
39 | ];
40 |
41 | $this->css = [
42 | 'css/Units.css',
43 | ];
44 |
45 | parent::init();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/assetbundles/unitsfield/UnitsFieldAsset.php:
--------------------------------------------------------------------------------
1 | sourcePath = "@nystudio107/units/assetbundles/unitsfield/dist";
32 |
33 | $this->depends = [
34 | CpAsset::class,
35 | ];
36 |
37 | $this->js = [
38 | 'js/Units.js',
39 | ];
40 |
41 | $this->css = [
42 | 'css/Units.css',
43 | ];
44 |
45 | parent::init();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/gql/types/UnitsDataType.php:
--------------------------------------------------------------------------------
1 | fieldName;
31 | // Special-case to `toString` since __ prefixes are reserved in GQL
32 | if ($fieldName === 'toString') {
33 | $fieldName = '__toString';
34 | }
35 | // If the method exists, call it with the passed in args. Otherwise try to retur a property
36 | if (method_exists($source, $fieldName)) {
37 | return $source->$fieldName(...$arguments);
38 | } else {
39 | return $source->$fieldName;
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/assetbundles/unitsfield/dist/js/Units.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Units plugin for Craft CMS
3 | *
4 | * Units Field JS
5 | *
6 | * @author nystudio107
7 | * @copyright Copyright (c) nystudio107
8 | * @link https://nystudio107.com/
9 | * @package Units
10 | * @since 1.0.0UnitsUnits
11 | */
12 |
13 | ;(function ($, window, document, undefined) {
14 |
15 | var pluginName = "UnitsUnits",
16 | defaults = {};
17 |
18 | // Plugin constructor
19 | function Plugin(element, options) {
20 | this.element = element;
21 |
22 | this.options = $.extend({}, defaults, options);
23 |
24 | this._defaults = defaults;
25 | this._name = pluginName;
26 |
27 | this.init();
28 | }
29 |
30 | Plugin.prototype = {
31 |
32 | init: function (id) {
33 | var _this = this;
34 |
35 | $(function () {
36 |
37 | /* -- _this.options gives us access to the $jsonVars that our FieldType passed down to us */
38 |
39 | });
40 | }
41 | };
42 |
43 | // A really lightweight plugin wrapper around the constructor,
44 | // preventing against multiple instantiations
45 | $.fn[pluginName] = function (options) {
46 | return this.each(function () {
47 | if (!$.data(this, "plugin_" + pluginName)) {
48 | $.data(this, "plugin_" + pluginName,
49 | new Plugin(this, options));
50 | }
51 | });
52 | };
53 |
54 | })(jQuery, window, document);
55 |
--------------------------------------------------------------------------------
/src/assetbundles/units/dist/js/Units.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Units plugin for Craft CMS
3 | *
4 | * Units JS
5 | *
6 | * @author nystudio107
7 | * @copyright Copyright (c) nystudio107
8 | * @link https://nystudio107.com/
9 | * @package Units
10 | * @since 1.0.0
11 | */
12 |
13 | /**
14 | * Fill a dynamic schema.org type menu with the units data
15 | *
16 | * @param menuId
17 | * @param menuValue
18 | * @param unitsClass
19 | * @param callback
20 | */
21 | function fillDynamicUnitsMenu(menuId, menuValue, unitsClass, callback) {
22 | var menu = $('#' + menuId);
23 |
24 | if (menu.length) {
25 | menu.empty();
26 | $.ajax({
27 | url: Craft.getActionUrl('units/units/available-units?unitsClass=' + unitsClass)
28 | })
29 | .done(function (data) {
30 | var newValue = Object.keys(data)[0];
31 | for (var k in data) {
32 | if (data.hasOwnProperty(k)) {
33 | if (k === menuValue) {
34 | newValue = menuValue;
35 | }
36 | $('')
37 | .attr('value', k)
38 | .html(data[k])
39 | .appendTo(menu);
40 | }
41 | }
42 | menu.val(newValue);
43 | if (callback !== undefined) {
44 | callback();
45 | }
46 | })
47 | .fail(function (data) {
48 | console.log('Error loading units data');
49 | })
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/templates/_components/fields/Units_input.twig:
--------------------------------------------------------------------------------
1 | {# @var craft \craft\web\twig\variables\CraftVariable #}
2 | {# @var model \nystudio107\units\models\UnitsData #}
3 | {# @var field \nystudio107\units\fields\Units #}
4 | {#
5 | /**
6 | * Units plugin for Craft CMS
7 | *
8 | * Units Field Input
9 | *
10 | * @author nystudio107
11 | * @copyright Copyright (c) nystudio107
12 | * @link https://nystudio107.com/
13 | * @package Units
14 | * @since 1.0.0
15 | */
16 | #}
17 |
18 | {% import "_includes/forms" as forms %}
19 |
20 | {% do view.registerAssetBundle("nystudio107\\units\\assetbundles\\units\\UnitsAsset") %}
21 | {% do view.registerAssetBundle("nystudio107\\units\\assetbundles\\unitsfield\\UnitsFieldAsset") %}
22 |
23 | {{ forms.textField({
24 | id: id ~ "value",
25 | name: name ~ "[value]",
26 | value: value,
27 | size: field.size,
28 | }) }}
29 |
30 | {% if field.changeableUnits %}
31 |
32 | {{ forms.select({
33 | id: id ~ "units",
34 | name: name ~ "[units]",
35 | options: craft.units.availableUnits(model.unitsClass),
36 | value: model.units,
37 | }) }}
38 |
39 | {% else %}
40 |
41 |
42 |
{{ field.defaultUnits }}
43 |
44 |
45 | {% endif %}
46 |
--------------------------------------------------------------------------------
/src/translations/en/units.php:
--------------------------------------------------------------------------------
1 | 'Min Value',
18 | 'Max Value' => 'Max Value',
19 | 'Default Size' => 'Default Size',
20 | 'Default Min Value' => 'Default Min Value',
21 | 'Is not a Model object.' => 'Is not a Model object.',
22 | 'Default Measure Type' => 'Default Measure Type',
23 | 'Size' => 'Size',
24 | 'Default Units Changeable' => 'Default Units Changeable',
25 | 'Default Value' => 'Default Value',
26 | '{name} plugin loaded' => '{name} plugin loaded',
27 | 'blah.' => 'blah.',
28 | 'Default Max Value' => 'Default Max Value',
29 | 'Units Changeable' => 'Units Changeable',
30 | 'Units' => 'Units',
31 | 'Measure Type' => 'Measure Type',
32 | 'Object failed to validate' => 'Object failed to validate',
33 | 'Decimal Points' => 'Decimal Points',
34 | 'UnitsData failed validation: ' => 'UnitsData failed validation: ',
35 | 'Default Decimal Points' => 'Default Decimal Points',
36 | 'Default Units' => 'Default Units',
37 | 'Default settings for newly created Units fields' => 'Default settings for newly created Units fields',
38 | ];
39 |
--------------------------------------------------------------------------------
/src/config.php:
--------------------------------------------------------------------------------
1 | Length::class,
29 |
30 | // The default value of the unit of measure
31 | 'defaultValue' => 0.0,
32 |
33 | // The default units that the unit of measure is in
34 | 'defaultUnits' => 'ft',
35 |
36 | // Whether the units the field can be changed
37 | 'defaultChangeableUnits' => true,
38 |
39 | // The default minimum allowed number
40 | 'defaultMin' => 0,
41 |
42 | // The default maximum allowed number
43 | 'defaultMax' => null,
44 |
45 | // The default number of digits allowed after the decimal point
46 | 'defaultDecimals' => 3,
47 |
48 | // The default size of the field
49 | 'defaultSize' => 6,
50 | ];
51 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nystudio107/craft-units",
3 | "description": "Units is a plugin that can convert between any units of measure, and comes with a Field for content authors to use",
4 | "type": "craft-plugin",
5 | "version": "5.0.1",
6 | "keywords": [
7 | "craft",
8 | "cms",
9 | "craftcms",
10 | "craft-plugin",
11 | "measurement",
12 | "measures",
13 | "units",
14 | "conversion"
15 | ],
16 | "support": {
17 | "docs": "https://nystudio107.com/docs/units/",
18 | "issues": "https://nystudio107.com/plugins/units/support",
19 | "source": "https://github.com/nystudio107/craft-units"
20 | },
21 | "license": "MIT",
22 | "authors": [
23 | {
24 | "name": "nystudio107",
25 | "homepage": "https://nystudio107.com/"
26 | }
27 | ],
28 | "require": {
29 | "craftcms/cms": "^5.0.0",
30 | "php-units-of-measure/php-units-of-measure": "~2.1.0"
31 | },
32 | "require-dev": {
33 | "craftcms/ecs": "dev-main",
34 | "craftcms/phpstan": "dev-main",
35 | "craftcms/rector": "dev-main"
36 | },
37 | "scripts": {
38 | "phpstan": "phpstan --ansi --memory-limit=1G",
39 | "check-cs": "ecs check --ansi",
40 | "fix-cs": "ecs check --fix --ansi"
41 | },
42 | "config": {
43 | "allow-plugins": {
44 | "craftcms/plugin-installer": true,
45 | "yiisoft/yii2-composer": true
46 | },
47 | "optimize-autoloader": true,
48 | "platform": {
49 | "php": "8.2"
50 | },
51 | "platform-check": false,
52 | "sort-packages": true
53 | },
54 | "autoload": {
55 | "psr-4": {
56 | "nystudio107\\units\\": "src/"
57 | }
58 | },
59 | "extra": {
60 | "class": "nystudio107\\units\\Units",
61 | "handle": "units",
62 | "name": "Units"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/helpers/ClassHelper.php:
--------------------------------------------------------------------------------
1 | fullyQualifiedName
31 | *
32 | * @param string $className
33 | *
34 | * @return array
35 | */
36 | public static function getClassesInNamespace(string $className): array
37 | {
38 | $result = [];
39 | $loader = include Craft::getAlias('@vendor/autoload.php');
40 | $filePath = $loader->findFile($className);
41 | if ($filePath) {
42 | $dir = realpath(dirname($filePath));
43 | $classesMap = ClassMapGenerator::createMap($dir);
44 | foreach ($classesMap as $class => $path) {
45 | try {
46 | $reflect = new ReflectionClass($class);
47 | $shortName = $reflect->getShortName();
48 | $result[$shortName] = $class;
49 | } catch (ReflectionException $e) {
50 | Craft::error($e->getMessage(), __METHOD__);
51 | }
52 | }
53 | }
54 | ksort($result);
55 |
56 | return $result;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/controllers/UnitsController.php:
--------------------------------------------------------------------------------
1 | asJson(Units::$variable->allAvailableUnits($includeAliases));
48 | }
49 |
50 | /**
51 | * Return the available units for a given AbstractPhysicalQuantity as JSON
52 | *
53 | * @param string $unitsClass
54 | * @param bool $includeAliases whether to include aliases or not
55 | *
56 | * @return Response
57 | */
58 | public function actionAvailableUnits(string $unitsClass, bool $includeAliases = false): Response
59 | {
60 | return $this->asJson(Units::$variable->availableUnits($unitsClass, $includeAliases));
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://scrutinizer-ci.com/g/nystudio107/craft-units/?branch=v5) [](https://scrutinizer-ci.com/g/nystudio107/craft-units/?branch=v5) [](https://scrutinizer-ci.com/g/nystudio107/craft-units/build-status/v5) [](https://scrutinizer-ci.com/code-intelligence)
2 |
3 | # Units plugin for Craft CMS 5.x
4 |
5 | Units is a plugin that can convert between any units of measure, and comes with a Field for content authors to use.
6 |
7 | 
8 |
9 | ## Requirements
10 |
11 | This plugin requires Craft CMS 5.0.0 or later.
12 |
13 | ## Installation
14 |
15 | To install the plugin, follow these instructions.
16 |
17 | 1. Open your terminal and go to your Craft project:
18 |
19 | cd /path/to/project
20 |
21 | 2. Then tell Composer to load the plugin:
22 |
23 | composer require nystudio107/craft-units
24 |
25 | 3. Install the plugin via `./craft install/plugin units` via the CLI, or in the Control Panel, go to Settings → Plugins and click the “Install” button for Units.
26 |
27 | You can also install Units via the **Plugin Store** in the Craft Control Panel.
28 |
29 | ## Documentation
30 |
31 | Click here -> [Units Documentation](https://nystudio107.com/plugins/units/documentation)
32 |
33 | ## Units Roadmap
34 |
35 | Some things to do, and ideas for potential features:
36 |
37 | * Add the ability to control what units appear in the list (because who uses _yottameters_?)
38 |
39 | Brought to you by [nystudio107](https://nystudio107.com/)
40 |
--------------------------------------------------------------------------------
/src/assetbundles/unitsfield/dist/img/Units-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
50 |
--------------------------------------------------------------------------------
/src/assetbundles/units/dist/img/Units-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
53 |
--------------------------------------------------------------------------------
/src/templates/_components/fields/Units_settings.twig:
--------------------------------------------------------------------------------
1 | {# @var craft \craft\web\twig\variables\CraftVariable #}
2 | {# @var field \nystudio107\units\fields\Units #}
3 | {#
4 | /**
5 | * Units plugin for Craft CMS
6 | *
7 | * Units Field Settings
8 | *
9 | * @author nystudio107
10 | * @copyright Copyright (c) nystudio107
11 | * @link https://nystudio107.com/
12 | * @package Units
13 | * @since 1.0.0
14 | */
15 | #}
16 |
17 | {% import "_includes/forms" as forms %}
18 |
19 | {% do view.registerAssetBundle("nystudio107\\units\\assetbundles\\units\\UnitsAsset") %}
20 | {% do view.registerAssetBundle("nystudio107\\units\\assetbundles\\unitsfield\\UnitsFieldAsset") %}
21 |
22 | {{ forms.selectField({
23 | label: "Measure Type"|t("units"),
24 | id: "defaultUnitsClass",
25 | name: "defaultUnitsClass",
26 | options: unitsClassMap,
27 | value: field.defaultUnitsClass,
28 | errors: field.getErrors("defaultUnitsClass"),
29 | }) }}
30 |
31 | {{ forms.textField({
32 | label: "Default Value"|t("units"),
33 | id: 'defaultValue',
34 | name: 'defaultValue',
35 | value: field.defaultValue,
36 | size: 5,
37 | errors: field.getErrors("defaultValue"),
38 | }) }}
39 |
40 | {{ forms.selectField({
41 | label: "Default Units"|t("units"),
42 | id: "defaultUnits",
43 | name: "defaultUnits",
44 | options: craft.units.availableUnits(field.defaultUnitsClass),
45 | value: field.defaultUnits,
46 | errors: field.getErrors("defaultUnits"),
47 | }) }}
48 |
49 | {{ forms.lightswitchField({
50 | label: "Units Changeable"|t("units"),
51 | id: "changeableUnits",
52 | name: "changeableUnits",
53 | on: field.changeableUnits,
54 | errors: field.getErrors("changeableUnits"),
55 | }) }}
56 |
57 | {{ forms.textField({
58 | label: "Min Value"|t('units'),
59 | id: 'min',
60 | name: 'min',
61 | value: field.min,
62 | size: 5,
63 | errors: field.getErrors('min')
64 | }) }}
65 |
66 | {{ forms.textField({
67 | label: "Max Value"|t('units'),
68 | id: 'max',
69 | name: 'max',
70 | value: field.max,
71 | size: 5,
72 | errors: field.getErrors('max')
73 | }) }}
74 |
75 | {{ forms.textField({
76 | label: "Decimal Points"|t('units'),
77 | id: 'decimals',
78 | name: 'decimals',
79 | value: field.decimals,
80 | size: 1,
81 | errors: field.getErrors('decimals')
82 | }) }}
83 |
84 | {{ forms.textField({
85 | label: "Size"|t('units'),
86 | id: 'size',
87 | name: 'size',
88 | value: field.size,
89 | size: 2,
90 | errors: field.getErrors('size')
91 | }) }}
92 |
93 |
94 | {% js %}
95 | // Fill in the dynamic unit menu
96 | var defaultUnitsId = '{{ "defaultUnits" |namespaceInputId }}';
97 | var defaultUnitsClassId = '{{ "defaultUnitsClass" |namespaceInputId }}';
98 |
99 | $('#'+defaultUnitsClassId).on('change', function(e) {
100 | var value = $('#'+defaultUnitsClassId).val();
101 | fillDynamicUnitsMenu(defaultUnitsId, '{{ field.defaultUnits }}', value);
102 | });
103 | {% endjs %}
104 |
--------------------------------------------------------------------------------
/src/models/Settings.php:
--------------------------------------------------------------------------------
1 | Length::class],
78 | ['defaultValue', 'number'],
79 | ['defaultValue', 'default', 'value' => 0.0],
80 | ['defaultUnits', 'string'],
81 | ['defaultUnits', 'default', 'value' => 'ft'],
82 | ['defaultChangeableUnits', 'boolean'],
83 | ['defaultChangeableUnits', 'default', 'value' => true],
84 | [['defaultMin', 'defaultMax'], 'number'],
85 | [
86 | ['defaultMax'],
87 | 'compare',
88 | 'compareAttribute' => 'defaultMin',
89 | 'operator' => '>=',
90 | ],
91 | ['defaultMin', 'default', 'value' => 0],
92 | ['defaultMax', 'default', 'value' => null],
93 | [['defaultDecimals', 'defaultSize'], 'integer'],
94 | ['defaultDecimals', 'default', 'value' => 3],
95 | ['defaultSize', 'default', 'value' => 6],
96 | ]);
97 |
98 | if (!$this->defaultDecimals) {
99 | $rules[] = [['defaultMin', 'defaultMax'], 'integer'];
100 | }
101 |
102 | return $rules;
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/templates/settings.twig:
--------------------------------------------------------------------------------
1 | {# @var craft \craft\web\twig\variables\CraftVariable #}
2 | {#
3 | /**
4 | * Units plugin for Craft CMS
5 | *
6 | * Units Settings.twig
7 | *
8 | * @author nystudio107
9 | * @copyright Copyright (c) nystudio107
10 | * @link https://nystudio107.com/
11 | * @package Units
12 | * @since 1.0.0
13 | */
14 | #}
15 |
16 | {% import "_includes/forms" as forms %}
17 |
18 | {% do view.registerAssetBundle("nystudio107\\units\\assetbundles\\units\\UnitsAsset") %}
19 |
20 |
21 |
22 |
23 |
24 |
25 | {{ "Default settings for newly created Units fields"|t("units") }}
26 |
27 |
28 |
29 |
30 | {{ forms.selectField({
31 | label: "Default Measure Type"|t("units"),
32 | id: "defaultUnitsClass",
33 | name: "defaultUnitsClass",
34 | options: unitsClassMap,
35 | value: settings.defaultUnitsClass,
36 | errors: settings.getErrors("defaultUnitsClass"),
37 | }) }}
38 |
39 | {{ forms.textField({
40 | label: "Default Value"|t("units"),
41 | id: 'defaultValue',
42 | name: 'defaultValue',
43 | value: settings.defaultValue,
44 | size: 5,
45 | errors: settings.getErrors("defaultValue"),
46 | }) }}
47 |
48 | {{ forms.selectField({
49 | label: "Default Units"|t("units"),
50 | id: "defaultUnits",
51 | name: "defaultUnits",
52 | options: craft.units.availableUnits(settings.defaultUnitsClass),
53 | value: settings.defaultUnits,
54 | errors: settings.getErrors("defaultUnits"),
55 | }) }}
56 |
57 | {{ forms.lightswitchField({
58 | label: "Default Units Changeable"|t("units"),
59 | id: "defaultChangeableUnits",
60 | name: "defaultChangeableUnits",
61 | on: settings.defaultChangeableUnits,
62 | errors: settings.getErrors("defaultChangeableUnits"),
63 | }) }}
64 |
65 | {{ forms.textField({
66 | label: "Default Min Value"|t('units'),
67 | id: 'defaultMin',
68 | name: 'defaultMin',
69 | value: settings.defaultMin,
70 | size: 5,
71 | errors: settings.getErrors('defaultMin')
72 | }) }}
73 |
74 | {{ forms.textField({
75 | label: "Default Max Value"|t('units'),
76 | id: 'defaultMax',
77 | name: 'defaultMax',
78 | value: settings.defaultMax,
79 | size: 5,
80 | errors: settings.getErrors('defaultMax')
81 | }) }}
82 |
83 | {{ forms.textField({
84 | label: "Default Decimal Points"|t('units'),
85 | id: 'defaultDecimals',
86 | name: 'defaultDecimals',
87 | value: settings.defaultDecimals,
88 | size: 1,
89 | errors: settings.getErrors('defaultDecimals')
90 | }) }}
91 |
92 | {{ forms.textField({
93 | label: "Default Size"|t('units'),
94 | id: 'defaultSize',
95 | name: 'defaultSize',
96 | value: settings.defaultSize,
97 | size: 2,
98 | errors: settings.getErrors('defaultSize')
99 | }) }}
100 |
101 |
102 |
103 | {% js %}
104 | // Fill in the dynamic unit menu
105 | var defaultUnitsId = '{{ "defaultUnits" |namespaceInputId }}';
106 | var defaultUnitsClassId = '{{ "defaultUnitsClass" |namespaceInputId }}';
107 |
108 | $('#'+defaultUnitsClassId).on('change', function(e) {
109 | var value = $('#'+defaultUnitsClassId).val();
110 | fillDynamicUnitsMenu(defaultUnitsId, '{{ settings.defaultUnits }}', value);
111 | });
112 | {% endjs %}
113 |
--------------------------------------------------------------------------------
/src/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
40 |
--------------------------------------------------------------------------------
/src/validators/EmbeddedUnitsDataValidator.php:
--------------------------------------------------------------------------------
1 | $attribute;
65 |
66 | if ($value !== null && is_object($value) && $value instanceof UnitsData) {
67 | // Validate the model
68 | $value->validate();
69 | // Normalize the min/max value
70 | $this->normalizeMinMax($value);
71 | // Do a min/max validation, too
72 | $config = [
73 | 'integerOnly' => $this->integerOnly,
74 | 'min' => $this->min,
75 | 'max' => $this->max,
76 | ];
77 | $numberValidator = new NumberValidator($config);
78 | $numberValidator->validateAttribute($value, 'value');
79 | // Add any errors to the parent model
80 | $errors = $value->getErrors();
81 | foreach ($errors as $attributeError => $valueErrors) {
82 | /** @var array $valueErrors */
83 | foreach ($valueErrors as $valueError) {
84 | $model->addError(
85 | $attribute,
86 | $valueError
87 | );
88 | }
89 | }
90 | } else {
91 | $model->addError($attribute, Craft::t('units', 'Is not a Model object.'));
92 | }
93 | }
94 |
95 | // Protected Methods
96 | // =========================================================================
97 |
98 | /**
99 | * Normalize the min/max values using the base units
100 | *
101 | * @param UnitsData $unitsData
102 | */
103 | protected function normalizeMinMax(UnitsData $unitsData)
104 | {
105 | $config = [
106 | 'unitsClass' => $unitsData->unitsClass,
107 | 'units' => $this->units,
108 | ];
109 | // Normalize the min
110 | if (!empty($this->min)) {
111 | $config['value'] = (float)$this->min;
112 | $baseUnit = new UnitsData($config);
113 | $this->min = $baseUnit->toUnit($unitsData->units);
114 | } else {
115 | $this->min = null;
116 | }
117 | // Normalize the max
118 | if (!empty($this->max)) {
119 | $config['value'] = (float)$this->max;
120 | $baseUnit = new UnitsData($config);
121 | $this->max = $baseUnit->toUnit($unitsData->units);
122 | } else {
123 | $this->max = null;
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/Units.php:
--------------------------------------------------------------------------------
1 | types[] = UnitsField::class;
81 | }
82 | );
83 |
84 | self::$variable = new UnitsVariable();
85 | Event::on(
86 | CraftVariable::class,
87 | CraftVariable::EVENT_INIT,
88 | static function(Event $event) {
89 | /** @var CraftVariable $variable */
90 | $variable = $event->sender;
91 | $variable->set('units', self::$variable);
92 | }
93 | );
94 |
95 | Event::on(
96 | UnitsField::class,
97 | 'craftQlGetFieldSchema',
98 | static function($event) {
99 | $field = $event->sender;
100 |
101 | if (!$field instanceof UnitsField) {
102 | return;
103 | }
104 |
105 | $object = $event->schema->createObjectType(ucfirst($field->handle) . 'Units');
106 | $object->addFloatField('value');
107 | $object->addStringField('units');
108 |
109 | $event->schema->addField($field)->type($object);
110 | $event->handled = true;
111 | }
112 | );
113 |
114 | Craft::info(
115 | Craft::t(
116 | 'units',
117 | '{name} plugin loaded',
118 | ['name' => $this->name]
119 | ),
120 | __METHOD__
121 | );
122 | }
123 |
124 | // Protected Methods
125 | // =========================================================================
126 |
127 | /**
128 | * @inheritdoc
129 | */
130 | protected function createSettingsModel(): ?Model
131 | {
132 | return new Settings();
133 | }
134 |
135 | /**
136 | * @inheritdoc
137 | */
138 | protected function settingsHtml(): ?string
139 | {
140 | $unitsClassMap = array_flip(ClassHelper::getClassesInNamespace(Length::class));
141 | return Craft::$app->view->renderTemplate(
142 | 'units/settings',
143 | [
144 | 'settings' => $this->getSettings(),
145 | 'unitsClassMap' => $unitsClassMap,
146 | ]
147 | );
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/gql/types/generators/UnitsDataGenerator.php:
--------------------------------------------------------------------------------
1 | [
41 | 'name' => 'toString',
42 | 'description' => 'The unit value & label',
43 | 'type' => Type::string(),
44 | ],
45 | 'units' => [
46 | 'name' => 'units',
47 | 'description' => 'The units of measurement',
48 | 'type' => Type::string(),
49 | ],
50 | 'value' => [
51 | 'name' => 'value',
52 | 'description' => 'The unit value',
53 | 'type' => Type::string(),
54 | ],
55 | 'toUnit' => [
56 | 'name' => 'toUnit',
57 | 'description' => 'Convert to another unit of measurement',
58 | 'type' => Type::string(),
59 | 'args' => [
60 | 'unit' => [
61 | 'name' => 'unit',
62 | 'description' => 'The unit to convert to',
63 | 'type' => Type::nonNull(Type::string()),
64 | ],
65 | ],
66 | ],
67 | 'toNativeUnit' => [
68 | 'name' => 'toNativeUnit',
69 | 'description' => 'The unit value as a fraction',
70 | 'type' => Type::string(),
71 | ],
72 | 'toFraction' => [
73 | 'name' => 'toFraction',
74 | 'description' => 'The unit value as a fraction',
75 | 'type' => Type::string(),
76 | ],
77 | 'toUnitFraction' => [
78 | 'name' => 'toUnitFraction',
79 | 'description' => 'Convert to another unit of measurement as a fraction',
80 | 'type' => Type::string(),
81 | 'args' => [
82 | 'unit' => [
83 | 'name' => 'unit',
84 | 'description' => 'The unit to convert to',
85 | 'type' => Type::nonNull(Type::string()),
86 | ],
87 | ],
88 | ],
89 | 'getValueFraction' => [
90 | 'name' => 'getValueFraction',
91 | 'description' => 'Return the value as a fraction',
92 | 'type' => Type::string(),
93 | ],
94 | ];
95 | $unitsDataType = GqlEntityRegistry::getEntity($typeName)
96 | ?: GqlEntityRegistry::createEntity($typeName, new UnitsDataType([
97 | 'name' => $typeName,
98 | 'description' => 'This entity has all the UnitsData properties',
99 | 'fields' => function() use ($unitsDataFields) {
100 | return $unitsDataFields;
101 | },
102 | ]));
103 |
104 | TypeLoader::registerType($typeName, static function() use ($unitsDataType) {
105 | return $unitsDataType;
106 | });
107 |
108 | return [$unitsDataType];
109 | }
110 |
111 | /**
112 | * @inheritdoc
113 | */
114 | public static function getName($context = null): string
115 | {
116 | /** @var Units $context */
117 | return $context->handle . '_UnitsData';
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/helpers/ClassMapGenerator.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace nystudio107\units\helpers;
12 |
13 | /**
14 | * ClassMapGenerator.
15 | *
16 | * @author Gyula Sallai
17 | */
18 | class ClassMapGenerator
19 | {
20 | /**
21 | * Generate a class map file.
22 | *
23 | * @param array|string $dirs Directories or a single path to search in
24 | * @param string $file The name of the class map file
25 | */
26 | public static function dump($dirs, $file)
27 | {
28 | $dirs = (array)$dirs;
29 | $maps = [];
30 | foreach ($dirs as $dir) {
31 | $maps = array_merge($maps, static::createMap($dir));
32 | }
33 | file_put_contents($file, sprintf('isFile()) {
52 | continue;
53 | }
54 | $path = $file->getRealPath() ?: $file->getPathname();
55 | if ('php' !== pathinfo($path, PATHINFO_EXTENSION)) {
56 | continue;
57 | }
58 | $classes = self::findClasses($path);
59 | if (\PHP_VERSION_ID >= 70000) {
60 | // PHP 7 memory manager will not release after token_get_all(), see https://bugs.php.net/70098
61 | gc_mem_caches();
62 | }
63 | foreach ($classes as $class) {
64 | $map[$class] = $path;
65 | }
66 | }
67 |
68 | return $map;
69 | }
70 |
71 | /**
72 | * Extract the classes in the given file.
73 | *
74 | * @param string $path The file to check
75 | *
76 | * @return array The found classes
77 | */
78 | private static function findClasses($path): array
79 | {
80 | $contents = file_get_contents($path);
81 | $tokens = token_get_all($contents);
82 | $classes = [];
83 | $namespace = '';
84 | for ($i = 0; isset($tokens[$i]); ++$i) {
85 | $token = $tokens[$i];
86 | if (!isset($token[1])) {
87 | continue;
88 | }
89 | $class = '';
90 | switch ($token[0]) {
91 | case T_NAMESPACE:
92 | $namespace = '';
93 | // If there is a namespace, extract it
94 | while (isset($tokens[++$i][1])) {
95 | if (PHP_MAJOR_VERSION >= 8) {
96 | if (\in_array($tokens[$i][0], [T_NAME_QUALIFIED], false)) {
97 | $namespace .= $tokens[$i][1];
98 | }
99 | } else {
100 | if (\in_array($tokens[$i][0], [T_STRING, T_NS_SEPARATOR], false)) {
101 | $namespace .= $tokens[$i][1];
102 | }
103 | }
104 | }
105 | $namespace .= '\\';
106 | break;
107 | case T_CLASS:
108 | case T_INTERFACE:
109 | case T_TRAIT:
110 | // Skip usage of ::class constant
111 | $isClassConstant = false;
112 | for ($j = $i - 1; $j > 0; --$j) {
113 | if (!isset($tokens[$j][1])) {
114 | break;
115 | }
116 | if (T_DOUBLE_COLON === $tokens[$j][0]) {
117 | $isClassConstant = true;
118 | break;
119 | } elseif (!\in_array($tokens[$j][0], [T_WHITESPACE, T_DOC_COMMENT, T_COMMENT], false)) {
120 | break;
121 | }
122 | }
123 | if ($isClassConstant) {
124 | break;
125 | }
126 | // Find the classname
127 | while (isset($tokens[++$i][1])) {
128 | $t = $tokens[$i];
129 | if (T_STRING === $t[0]) {
130 | $class .= $t[1];
131 | } elseif ('' !== $class && T_WHITESPACE === $t[0]) {
132 | break;
133 | }
134 | }
135 | $classes[] = ltrim($namespace . $class, '\\');
136 | break;
137 | default:
138 | break;
139 | }
140 | }
141 |
142 | return $classes;
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/variables/UnitsVariable.php:
--------------------------------------------------------------------------------
1 | unitsClassMap)) {
81 | $this->unitsClassMap = ClassHelper::getClassesInNamespace(Length::class);
82 | }
83 | $unitsClassKey = ucfirst($method);
84 | if (isset($this->unitsClassMap[$unitsClassKey])) {
85 | [$value, $units] = $args;
86 | $config = [
87 | 'unitsClass' => $this->unitsClassMap[$unitsClassKey],
88 | 'value' => $value,
89 | 'units' => $units,
90 | ];
91 |
92 | return new UnitsData($config);
93 | }
94 |
95 | throw new InvalidArgumentException("Method {$method} doesn't exist");
96 | }
97 |
98 | /**
99 | * Outputs a floating point number as a fraction
100 | *
101 | * @param float $value
102 | *
103 | * @return string
104 | */
105 | public function fraction(float $value): string
106 | {
107 | [$whole, $decimal] = $this->float2parts($value);
108 |
109 | return $whole . ' ' . $this->float2ratio($decimal);
110 | }
111 |
112 | /**
113 | * Convert a floating point number to the whole and the decimal
114 | *
115 | * @param float $number
116 | * @param bool $returnUnsigned
117 | *
118 | * @return array
119 | */
120 | public function float2parts(float $number, bool $returnUnsigned = false): array
121 | {
122 | $negative = 1;
123 | if ($number < 0) {
124 | $negative = -1;
125 | $number *= -1;
126 | }
127 |
128 | if ($returnUnsigned) {
129 | return [
130 | floor($number),
131 | $number - floor($number),
132 | ];
133 | }
134 |
135 | return [
136 | floor($number) * $negative,
137 | ($number - floor($number)) * $negative,
138 | ];
139 | }
140 |
141 | /**
142 | * Convert a floating point number to a ratio
143 | *
144 | * @param float $n
145 | * @param float $tolerance
146 | *
147 | * @return string
148 | */
149 | public function float2ratio(float $n, float $tolerance = 1.e-6): string
150 | {
151 | if ($n === 0.0) {
152 | return '';
153 | }
154 | $h1 = 1;
155 | $h2 = 0;
156 | $k1 = 0;
157 | $k2 = 1;
158 | $b = 1 / $n;
159 | do {
160 | $b = 1 / $b;
161 | $a = floor($b);
162 | $aux = $h1;
163 | $h1 = $a * $h1 + $h2;
164 | $h2 = $aux;
165 | $aux = $k1;
166 | $k1 = $a * $k1 + $k2;
167 | $k2 = $aux;
168 | $b -= $a;
169 | } while (abs($n - $h1 / $k1) > $n * $tolerance);
170 |
171 | return "$h1/$k1";
172 | }
173 |
174 | /**
175 | * Return all of the available units
176 | *
177 | * @param bool $includeAliases whether to include aliases or not
178 | *
179 | * @return array
180 | */
181 | public function allAvailableUnits(bool $includeAliases = false): array
182 | {
183 | $unitsList = [];
184 | $units = ClassHelper::getClassesInNamespace(Length::class);
185 | foreach ($units as $key => $value) {
186 | /** @var AbstractPhysicalQuantity $value */
187 | $unitsList[$key] = $this->availableUnits($value, $includeAliases);
188 | }
189 | ksort($unitsList);
190 |
191 | return $unitsList;
192 | }
193 |
194 | /**
195 | * Return the available units for a given AbstractPhysicalQuantity
196 | *
197 | * @param string $unitsClass
198 | * @param bool $includeAliases whether to include aliases or not
199 | *
200 | * @return array
201 | */
202 | public function availableUnits(string $unitsClass, bool $includeAliases = false): array
203 | {
204 | $availableUnits = [];
205 | if (is_subclass_of($unitsClass, AbstractPhysicalQuantity::class)) {
206 | /** @var array $units */
207 | /** @var AbstractPhysicalQuantity $unitsClass */
208 | $units = $unitsClass::getUnitDefinitions();
209 | /** @var UnitOfMeasure $unit */
210 | foreach ($units as $unit) {
211 | $name = $unit->getName();
212 | $aliases = $unit->getAliases();
213 | $availableUnits[$name] = $includeAliases ? $aliases : $aliases[0] ?? $name;
214 | }
215 | }
216 |
217 | return $availableUnits;
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/src/models/UnitsData.php:
--------------------------------------------------------------------------------
1 | unitsInstance;
78 | if (method_exists($unitsInstance, $method)) {
79 | return call_user_func_array([$unitsInstance, $method], $args);
80 | }
81 |
82 | throw new InvalidArgumentException("Method {$method} doesn't exist");
83 | }
84 |
85 | /**
86 | * @inheritdoc
87 | */
88 | public function init(): void
89 | {
90 | parent::init();
91 | /** @var Settings $settings */
92 | $settings = Units::$plugin->getSettings();
93 | $this->unitsClass = $this->unitsClass ?? $settings->defaultUnitsClass;
94 | $this->value = $this->value ?? $settings->defaultValue;
95 | $this->units = $this->units ?? $settings->defaultUnits;
96 |
97 | if ($this->unitsClass !== null) {
98 | $this->unitsInstance = new $this->unitsClass($this->value, $this->units);
99 | }
100 | }
101 |
102 | /**
103 | * @inheritdoc
104 | */
105 | public function rules(): array
106 | {
107 | $rules = parent::rules();
108 | $rules = array_merge($rules, [
109 | ['unitsClass', 'string'],
110 | ['value', 'number'],
111 | ['units', 'string'],
112 | ]);
113 |
114 | return $rules;
115 | }
116 |
117 | /**
118 | * @inheritdoc
119 | */
120 | public function fields(): array
121 | {
122 | $fields = parent::fields();
123 | $fields = array_diff_key(
124 | $fields,
125 | array_flip([
126 | 'unitsInstance',
127 | ])
128 | );
129 |
130 | return $fields;
131 | }
132 |
133 | /**
134 | * @inheritdoc
135 | */
136 | public function toUnit($unit)
137 | {
138 | return $this->unitsInstance->toUnit($unit);
139 | }
140 |
141 | /**
142 | * @inheritdoc
143 | */
144 | public function toNativeUnit()
145 | {
146 | return $this->unitsInstance->toNativeUnit();
147 | }
148 |
149 | /**
150 | * @inheritdoc
151 | */
152 | public function __toString(): string
153 | {
154 | return $this->unitsInstance->__toString();
155 | }
156 |
157 | /**
158 | * @inheritdoc
159 | */
160 | public function add(PhysicalQuantityInterface $quantity)
161 | {
162 | /** @var UnitsData $quantity */
163 | return $this->physicalQuantityToUnitsData($this->unitsInstance->add($quantity->unitsInstance));
164 | }
165 |
166 | /**
167 | * @inheritdoc
168 | */
169 | public function subtract(PhysicalQuantityInterface $quantity)
170 | {
171 | /** @var UnitsData $quantity */
172 | return $this->physicalQuantityToUnitsData($this->unitsInstance->subtract($quantity->unitsInstance));
173 | }
174 |
175 | /**
176 | * @inheritdoc
177 | */
178 | public function isEquivalentQuantity(PhysicalQuantityInterface $testQuantity)
179 | {
180 | /** @var UnitsData $testQuantity */
181 | return $this->unitsInstance->isEquivalentQuantity($testQuantity->unitsInstance);
182 | }
183 |
184 | /**
185 | * @inheritdoc
186 | */
187 | public function availableUnits(bool $includeAliases = true)
188 | {
189 | $availableUnits = [];
190 | $units = $this->unitsInstance::getUnitDefinitions();
191 | /** @var UnitOfMeasure $unit */
192 | foreach ($units as $unit) {
193 | $name = $unit->getName();
194 | $aliases = $unit->getAliases();
195 | $availableUnits[$name] = $includeAliases ? $aliases : $aliases[0] ?? $name;
196 | }
197 |
198 | return $availableUnits;
199 | }
200 |
201 | /**
202 | * Return the measurement as a fraction, with the units appended
203 | *
204 | * @return string
205 | */
206 | public function toFraction(): string
207 | {
208 | return trim(Units::$variable->fraction($this->value) . ' ' . $this->units);
209 | }
210 |
211 | /**
212 | * Return the measurement as a fraction, in the given unit of measure
213 | *
214 | * @param UnitOfMeasureInterface|string $unit The desired unit of measure,
215 | * or a string name of one
216 | *
217 | * @return string The measurement cast in the requested units, as a
218 | * fraction
219 | */
220 | public function toUnitFraction($unit): string
221 | {
222 | $value = $this->toUnit($unit);
223 |
224 | return Units::$variable->fraction($value);
225 | }
226 |
227 | /**
228 | * Return the value as a fraction
229 | *
230 | * @return string
231 | */
232 | public function getValueFraction(): string
233 | {
234 | return Units::$variable->fraction($this->value);
235 | }
236 |
237 | /**
238 | * Return an array of the whole number and decimal number ports of the value
239 | * [0] has the whole number part, and [1] has the decimal part
240 | *
241 | * @return float[]
242 | */
243 | public function getValueParts(): array
244 | {
245 | return Units::$variable->float2parts($this->value);
246 | }
247 |
248 | /**
249 | * Return an array of the whole number and decimal number ports of the
250 | * value with the decimal part converted to a fraction. [0] has the whole
251 | * number part, and [1] has the fractional part
252 | *
253 | * @return string[]
254 | */
255 | public function getValuePartsFraction(): array
256 | {
257 | $parts = Units::$variable->float2parts($this->value);
258 | $parts[0] = (string)$parts[0];
259 | $parts[1] = Units::$variable->float2ratio($parts[1]);
260 |
261 | return $parts;
262 | }
263 |
264 | // Protected Methods
265 | // =========================================================================
266 |
267 | /**
268 | * Convert a PhysicalQuantity object into a UnitsData object
269 | *
270 | * @param PhysicalQuantityInterface $quantity
271 | *
272 | * @return UnitsData
273 | */
274 | protected function physicalQuantityToUnitsData(PhysicalQuantityInterface $quantity): UnitsData
275 | {
276 | $unitsClass = get_class($quantity);
277 | list($value, $units) = explode(' ', (string)$quantity);
278 | $config = [
279 | 'unitsClass' => $unitsClass,
280 | 'value' => $value,
281 | 'units' => $units,
282 | ];
283 |
284 | return new UnitsData($config);
285 | }
286 | }
287 |
--------------------------------------------------------------------------------
/src/fields/Units.php:
--------------------------------------------------------------------------------
1 | getSettings();
106 | if ($settings !== null) {
107 | $this->defaultUnitsClass = $this->defaultUnitsClass ?? $settings->defaultUnitsClass;
108 | $this->defaultValue = $this->defaultValue ?? $settings->defaultValue;
109 | $this->defaultUnits = $this->defaultUnits ?? $settings->defaultUnits;
110 | $this->changeableUnits = $this->changeableUnits ?? $settings->defaultChangeableUnits;
111 | $this->min = $this->min ?? $settings->defaultMin;
112 | $this->max = $this->max ?? $settings->defaultMax;
113 | $this->decimals = $this->decimals ?? $settings->defaultDecimals;
114 | $this->size = $this->size ?? $settings->defaultSize;
115 | }
116 | }
117 | }
118 |
119 | /**
120 | * @inheritdoc
121 | */
122 | public function rules(): array
123 | {
124 | $rules = parent::rules();
125 | $rules = array_merge($rules, [
126 | ['defaultUnitsClass', 'string'],
127 | ['defaultValue', 'number'],
128 | ['defaultUnits', 'string'],
129 | ['changeableUnits', 'boolean'],
130 | [['min', 'max'], 'number'],
131 | [['decimals', 'size'], 'integer'],
132 | [
133 | ['max'],
134 | 'compare',
135 | 'compareAttribute' => 'min',
136 | 'operator' => '>=',
137 | ],
138 | ]);
139 |
140 | if (!$this->decimals) {
141 | $rules[] = [['min', 'max'], 'integer'];
142 | }
143 |
144 | return $rules;
145 | }
146 |
147 | /**
148 | * @inheritdoc
149 | */
150 | public function normalizeValue(mixed $value, ?ElementInterface $element = null): mixed
151 | {
152 | if ($value instanceof UnitsData) {
153 | return $value;
154 | }
155 | // Default config
156 | $config = [
157 | 'unitsClass' => $this->defaultUnitsClass,
158 | 'value' => $this->defaultValue,
159 | 'units' => $this->defaultUnits,
160 | ];
161 | // Handle incoming values potentially being JSON or an array
162 | if (!empty($value)) {
163 | // Handle a numeric value coming in (perhaps from a Number field)
164 | if (is_numeric($value)) {
165 | $config['value'] = (float)$value;
166 | } elseif (is_string($value)) {
167 | $config = Json::decodeIfJson($value);
168 | }
169 | if (is_array($value)) {
170 | $config = array_merge($config, array_filter($value));
171 | }
172 | }
173 | // Typecast it to a float
174 | $config['value'] = (float)$config['value'];
175 | // Create and validate the model
176 | $unitsData = new UnitsData($config);
177 | if (!$unitsData->validate()) {
178 | Craft::error(
179 | Craft::t('units', 'UnitsData failed validation: ')
180 | . print_r($unitsData->getErrors(), true),
181 | __METHOD__
182 | );
183 | }
184 |
185 | return $unitsData;
186 | }
187 |
188 | /**
189 | * @inheritdoc
190 | */
191 | public function getSettingsHtml(): ?string
192 | {
193 | $unitsClassMap = array_flip(ClassHelper::getClassesInNamespace(Length::class));
194 |
195 | // Render the settings template
196 | return Craft::$app->getView()->renderTemplate(
197 | 'units/_components/fields/Units_settings',
198 | [
199 | 'field' => $this,
200 | 'unitsClassMap' => $unitsClassMap,
201 | ]
202 | );
203 | }
204 |
205 | /**
206 | * @inheritdoc
207 | */
208 | public function getInputHtml(mixed $value, ?ElementInterface $element = null): string
209 | {
210 | if ($value instanceof UnitsData) {
211 | // Register our asset bundle
212 | try {
213 | Craft::$app->getView()->registerAssetBundle(UnitsFieldAsset::class);
214 | } catch (InvalidConfigException $e) {
215 | Craft::error($e->getMessage(), __METHOD__);
216 | }
217 | $model = $value;
218 | $value = $model->value;
219 | $decimals = $this->decimals;
220 | // If decimals is 0 (or null, empty for whatever reason), don't run this
221 | if ($decimals) {
222 | $decimalSeparator = Craft::$app->getLocale()->getNumberSymbol(Locale::SYMBOL_DECIMAL_SEPARATOR);
223 | $value = number_format($value, $decimals, $decimalSeparator, '');
224 | }
225 | // Get our id and namespace
226 | $id = Html::id($this->handle);
227 | $namespacedId = Craft::$app->getView()->namespaceInputId($id);
228 |
229 | // Variables to pass down to our field JavaScript to let it namespace properly
230 | $jsonVars = [
231 | 'id' => $id,
232 | 'name' => $this->handle,
233 | 'namespace' => $namespacedId,
234 | 'prefix' => Craft::$app->getView()->namespaceInputId(''),
235 | ];
236 | $jsonVars = Json::encode($jsonVars);
237 | Craft::$app->getView()->registerJs("$('#{$namespacedId}-field').UnitsUnits(" . $jsonVars . ");");
238 |
239 | // Render the input template
240 | return Craft::$app->getView()->renderTemplate(
241 | 'units/_components/fields/Units_input',
242 | [
243 | 'name' => $this->handle,
244 | 'field' => $this,
245 | 'id' => $id,
246 | 'namespacedId' => $namespacedId,
247 | 'value' => $value,
248 | 'model' => $model,
249 | ]
250 | );
251 | }
252 |
253 | return '';
254 | }
255 |
256 | /**
257 | * @inheritdoc
258 | */
259 | public function getContentGqlType(): Type|array
260 | {
261 | $typeArray = UnitsDataGenerator::generateTypes($this);
262 |
263 | return [
264 | 'name' => $this->handle,
265 | 'description' => 'Units field',
266 | 'type' => array_shift($typeArray),
267 | ];
268 | }
269 |
270 | /**
271 | * @inheritdoc
272 | */
273 | public function getElementValidationRules(): array
274 | {
275 | return [
276 | [
277 | EmbeddedUnitsDataValidator::class,
278 | 'units' => $this->defaultUnits,
279 | 'integerOnly' => !$this->decimals,
280 | 'min' => $this->min,
281 | 'max' => $this->max,
282 | ],
283 | ];
284 | }
285 | }
286 |
--------------------------------------------------------------------------------