JsonEditor   A
last analyzed

Complexity

Total Complexity 30

Size/Duplication

Total Lines 251
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 30
eloc 112
c 5
b 0
f 0
dl 0
loc 251
ccs 93
cts 93
cp 1
rs 10

6 Methods

Rating   Name   Duplication   Size   Complexity  
B registerClientScript() 0 55 10
A initClientOptions() 0 9 4
A init() 0 18 5
A run() 0 10 2
A issetValue() 0 3 2
B determineValue() 0 26 7
1
<?php
2
3
namespace kdn\yii2;
4
5
use kdn\yii2\assets\JsonEditorFullAsset;
6
use kdn\yii2\assets\JsonEditorMinimalistAsset;
7
use yii\helpers\ArrayHelper;
8
use yii\helpers\Html;
9
use yii\helpers\Inflector;
10
use yii\helpers\Json;
11
use yii\web\JsExpression;
12
use yii\widgets\InputWidget;
13
14
/**
15
 * Class JsonEditor.
16
 * @package kdn\yii2
17
 */
18
class JsonEditor extends InputWidget
19
{
20
    /**
21
     * @var array options which will be passed to JSON editor
22
     * @see https://github.com/josdejong/jsoneditor/blob/master/docs/api.md#configuration-options
23
     */
24
    public $clientOptions = [];
25
26
    /**
27
     * @var string[] list of JSON editor modes for which all fields should be collapsed automatically;
28
     * allowed modes 'tree', 'view', and 'form'
29
     * @see https://github.com/josdejong/jsoneditor/blob/master/docs/api.md#jsoneditorcollapseall
30
     */
31
    public $collapseAll = [];
32
33
    /**
34
     * @var array HTML attributes to be applied to the JSON editor container tag
35
     * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered
36
     */
37
    public $containerOptions = [];
38
39
    /**
40
     * @var mixed this property can be used instead of `value`;
41
     * while `value` must be JSON string, `decodedValue` accepts decoded JSON, i.e. arrays, floats, booleans, etc.;
42
     * `decodedValue` has precedence over `value`: if `decodedValue` is set then `value` will be ignored
43
     * @see value
44
     */
45
    public $decodedValue;
46
47
    /**
48
     * @var string default value
49
     */
50
    public $defaultValue = '{}';
51
52
    /**
53
     * @var string[] list of JSON editor modes for which all fields should be expanded automatically;
54
     * allowed modes 'tree', 'view', and 'form'
55
     * @see https://github.com/josdejong/jsoneditor/blob/master/docs/api.md#jsoneditorexpandall
56
     */
57
    public $expandAll = [];
58
59
    /**
60
     * @var null|bool whether to use minimalist version of JSON editor;
61
     * note that "minimalist" is not the same as "minimized";
62
     * if property is not set then extension will try to determine automatically whether full version is needed,
63
     * if full version is not required then minimalist version will be used;
64
     * you can explicitly set this property to true or false if automatic detection does not fit for you application
65
     * @see https://github.com/josdejong/jsoneditor/blob/master/src/docs/which%20files%20do%20I%20need.md
66
     */
67
    public $minimalist;
68
69
    /**
70
     * @var string[] list of client options which should be automatically converted to `JsExpression`
71
     * @see clientOptions
72
     */
73
    protected $jsExpressionClientOptions = [
74
        'ace',
75
        'ajv',
76
        'autocomplete',
77
        'createQuery',
78
        'executeQuery',
79
        'languages',
80
        'modalAnchor',
81
        'onBlur',
82
        'onChange',
83
        'onChangeJSON',
84
        'onChangeText',
85
        'onClassName',
86
        'onColorPicker',
87
        'onCreateMenu',
88
        'onExpand',
89
        'onEditable',
90
        'onError',
91
        'onEvent',
92
        'onFocus',
93
        'onModeChange',
94
        'onNodeName',
95
        'onSelectionChange',
96
        'onTextSelectionChange',
97
        'onValidate',
98
        'onValidationError',
99
        'popupAnchor',
100
        'schema',
101
        'schemaRefs',
102
        'templates',
103
        'timestampFormat',
104
        'timestampTag',
105
    ];
106
107
    /**
108
     * @var string default JSON editor mode
109
     */
110
    private $mode = 'tree';
111
112
    /**
113
     * @var string[] available JSON editor modes
114
     */
115
    private $modes = [];
116
117
    /**
118
     * {@inheritdoc}
119
     */
120 15
    public function init()
121
    {
122 15
        parent::init();
123 15
        if (!isset($this->containerOptions['id'])) {
124 15
            $this->containerOptions['id'] = $this->options['id'] . '-json-editor';
125 15
        }
126
127 15
        $this->determineValue();
128
129 15
        foreach (['mode', 'modes'] as $parameterName) {
130 15
            $this->$parameterName = ArrayHelper::getValue($this->clientOptions, $parameterName, $this->$parameterName);
131 15
        }
132
        // make sure that "mode" is specified, otherwise JavaScript error can occur in some situations
133 15
        $this->clientOptions['mode'] = $this->mode;
134
135
        // if property is not set then try to determine automatically whether full version is needed
136 15
        if ($this->minimalist === null) {
137 15
            $this->minimalist = $this->mode != 'code' && !in_array('code', $this->modes);
138 15
        }
139 15
    }
140
141
    /**
142
     * Analyses input data and determines what should be used as value.
143
     * This method must set `value` and `decodedValue` properties.
144
     */
145 15
    protected function determineValue()
146
    {
147
        // decodedValue property has first precedence
148 15
        if ($this->decodedValue !== null) {
149 1
            $this->value = Json::encode($this->decodedValue);
150
151 1
            return;
152
        }
153
154
        // value property has second precedence
155
        // options['value'] property has third precedence
156 14
        if (!$this->issetValue() && isset($this->options['value'])) {
157 1
            $this->value = $this->options['value'];
158 1
        }
159
160
        // model attribute has fourth precedence
161 14
        if (!$this->issetValue() && $this->hasModel()) {
162 4
            $this->value = Html::getAttributeValue($this->model, $this->attribute);
163 4
        }
164
165
        // value is not set anywhere, use default
166 14
        if (!$this->issetValue()) {
167 6
            $this->value = $this->defaultValue;
168 6
        }
169
170 14
        $this->decodedValue = Json::decode($this->value, false);
171 14
    }
172
173
    /**
174
     * Check whether `value` property is set. For JSON string the empty string is considered as equivalent of null.
175
     * @return bool whether `value` property is set.
176
     */
177 14
    protected function issetValue()
178
    {
179 14
        return $this->value !== null && $this->value !== '';
180
    }
181
182
    /**
183
     * {@inheritdoc}
184
     */
185 15
    public function run()
186
    {
187 15
        $this->registerClientScript();
188 15
        if ($this->hasModel()) {
189 7
            $this->options['value'] = $this->value; // model may contain decoded JSON, override value for rendering
190 7
            echo Html::activeHiddenInput($this->model, $this->attribute, $this->options);
191 7
        } else {
192 8
            echo Html::hiddenInput($this->name, $this->value, $this->options);
193
        }
194 15
        echo Html::tag('div', '', $this->containerOptions);
195 15
    }
196
197
    /**
198
     * Initializes client options.
199
     */
200 15
    protected function initClientOptions()
201
    {
202 15
        $options = $this->clientOptions;
203 15
        foreach ($options as $key => $value) {
204 15
            if (!$value instanceof JsExpression && in_array($key, $this->jsExpressionClientOptions)) {
205 1
                $options[$key] = new JsExpression($value);
206 1
            }
207 15
        }
208 15
        $this->clientOptions = $options;
209 15
    }
210
211
    /**
212
     * Registers the needed client script.
213
     */
214 15
    public function registerClientScript()
215
    {
216 15
        $this->initClientOptions();
217 15
        $view = $this->getView();
218
219 15
        if ($this->minimalist) {
220 14
            JsonEditorMinimalistAsset::register($view);
221 14
        } else {
222 3
            JsonEditorFullAsset::register($view);
223
        }
224
225 15
        $hiddenInputId = $this->options['id'];
226 15
        $editorName = Inflector::variablize($hiddenInputId) . 'JsonEditor_' . hash('crc32', $hiddenInputId);
227 15
        $this->options['data-json-editor-name'] = $editorName;
228
229 15
        $jsUpdateHiddenField = "jQuery('#$hiddenInputId').val($editorName.getText());";
230
231 15
        if (isset($this->clientOptions['onChange'])) {
232 1
            $userFunction = " var userFunction = {$this->clientOptions['onChange']}; userFunction.call(this);";
233 1
        } else {
234 14
            $userFunction = '';
235
        }
236 15
        $this->clientOptions['onChange'] = new JsExpression("function() {{$jsUpdateHiddenField}$userFunction}");
237
238 15
        if (!empty($this->collapseAll) || !empty($this->expandAll)) {
239 2
            if (isset($this->clientOptions['onModeChange'])) {
240 1
                $userFunction = " var userFunction = {$this->clientOptions['onModeChange']}; " .
241 1
                    "userFunction.call(this, newMode, oldMode);";
242 1
            } else {
243 1
                $userFunction = '';
244
            }
245 2
            $jsOnModeChange = "function(newMode, oldMode) {";
246 2
            foreach (['collapseAll', 'expandAll'] as $property) {
247 2
                if (!empty($this->$property)) {
248 2
                    $jsOnModeChange .= "if (" . Json::htmlEncode($this->$property) . ".indexOf(newMode) !== -1) " .
249 2
                        "{{$editorName}.$property();}";
250 2
                }
251 2
            }
252 2
            $jsOnModeChange .= "$userFunction}";
253 2
            $this->clientOptions['onModeChange'] = new JsExpression($jsOnModeChange);
254 2
        }
255
256 15
        $htmlEncodedValue = Json::htmlEncode($this->decodedValue); // Json::htmlEncode is needed to prevent XSS
257 15
        $jsCode = "$editorName = new JSONEditor(document.getElementById('{$this->containerOptions['id']}'), " .
258 15
            Json::htmlEncode($this->clientOptions) . ");\n" .
259 15
            "$editorName.set($htmlEncodedValue);\n" . // have to use set method,
260
            // because constructor works wrong for '0', 'null', '""'; constructor turns them to {}, which may be wrong
261 15
            "jQuery('#$hiddenInputId').parents('form').submit(function() {{$jsUpdateHiddenField}});";
262 15
        if (in_array($this->mode, $this->collapseAll)) {
263 1
            $jsCode .= "\n$editorName.collapseAll();";
264 1
        }
265 15
        if (in_array($this->mode, $this->expandAll)) {
266 1
            $jsCode .= "\n$editorName.expandAll();";
267 1
        }
268 15
        $view->registerJs($jsCode);
269 15
    }
270
}
271