Completed
Push — master ( 6a7454...e111d0 )
by Dmitry
04:41
created

JsonEditor   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 243
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 30
lcom 1
cbo 9
dl 0
loc 243
ccs 94
cts 94
cp 1
rs 10
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A init() 0 20 5
B determineValue() 0 26 7
A issetValue() 0 4 2
A run() 0 11 2
A initClientOptions() 0 10 4
B registerClientScript() 0 56 10
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
        'onChange',
77
        'onChangeJSON',
78
        'onChangeText',
79
        'onClassName',
80
        'onEditable',
81
        'onError',
82
        'onModeChange',
83
        'onNodeName',
84
        'onValidate',
85
        'onCreateMenu',
86
        'schema',
87
        'schemaRefs',
88
        'templates',
89
        'autocomplete',
90
        'onTextSelectionChange',
91
        'onSelectionChange',
92
        'onEvent',
93
        'onColorPicker',
94
        'languages',
95
        'modalAnchor',
96
    ];
97
98
    /**
99
     * @var string default JSON editor mode
100
     */
101
    private $mode = 'tree';
102
103
    /**
104
     * @var string[] available JSON editor modes
105
     */
106
    private $modes = [];
107
108
    /**
109
     * {@inheritdoc}
110
     */
111 15
    public function init()
112
    {
113 15
        parent::init();
114 15
        if (!isset($this->containerOptions['id'])) {
115 15
            $this->containerOptions['id'] = $this->options['id'] . '-json-editor';
116 15
        }
117
118 15
        $this->determineValue();
119
120 15
        foreach (['mode', 'modes'] as $parameterName) {
121 15
            $this->$parameterName = ArrayHelper::getValue($this->clientOptions, $parameterName, $this->$parameterName);
122 15
        }
123
        // make sure that "mode" is specified, otherwise JavaScript error can occur in some situations
124 15
        $this->clientOptions['mode'] = $this->mode;
125
126
        // if property is not set then try to determine automatically whether full version is needed
127 15
        if ($this->minimalist === null) {
128 15
            $this->minimalist = $this->mode != 'code' && !in_array('code', $this->modes);
129 15
        }
130 15
    }
131
132
    /**
133
     * Analyses input data and determines what should be used as value.
134
     * This method must set `value` and `decodedValue` properties.
135
     */
136 15
    protected function determineValue()
137
    {
138
        // decodedValue property has first precedence
139 15
        if ($this->decodedValue !== null) {
140 1
            $this->value = Json::encode($this->decodedValue);
141 1
            return;
142
        }
143
144
        // value property has second precedence
145
        // options['value'] property has third precedence
146 14
        if (!$this->issetValue() && isset($this->options['value'])) {
147 1
            $this->value = $this->options['value'];
148 1
        }
149
150
        // model attribute has fourth precedence
151 14
        if (!$this->issetValue() && $this->hasModel()) {
152 4
            $this->value = Html::getAttributeValue($this->model, $this->attribute);
153 4
        }
154
155
        // value is not set anywhere, use default
156 14
        if (!$this->issetValue()) {
157 6
            $this->value = $this->defaultValue;
158 6
        }
159
160 14
        $this->decodedValue = Json::decode($this->value, false);
161 14
    }
162
163
    /**
164
     * Check whether `value` property is set. For JSON string the empty string is considered as equivalent of null.
165
     * @return bool whether `value` property is set.
166
     */
167 14
    protected function issetValue()
168
    {
169 14
        return $this->value !== null && $this->value !== '';
170
    }
171
172
    /**
173
     * {@inheritdoc}
174
     */
175 15
    public function run()
176
    {
177 15
        $this->registerClientScript();
178 15
        if ($this->hasModel()) {
179 7
            $this->options['value'] = $this->value; // model may contain decoded JSON, override value for rendering
180 7
            echo Html::activeHiddenInput($this->model, $this->attribute, $this->options);
181 7
        } else {
182 8
            echo Html::hiddenInput($this->name, $this->value, $this->options);
183
        }
184 15
        echo Html::tag('div', '', $this->containerOptions);
185 15
    }
186
187
    /**
188
     * Initializes client options.
189
     */
190 15
    protected function initClientOptions()
191
    {
192 15
        $options = $this->clientOptions;
193 15
        foreach ($options as $key => $value) {
194 15
            if (!$value instanceof JsExpression && in_array($key, $this->jsExpressionClientOptions)) {
195 1
                $options[$key] = new JsExpression($value);
196 1
            }
197 15
        }
198 15
        $this->clientOptions = $options;
199 15
    }
200
201
    /**
202
     * Registers the needed client script.
203
     */
204 15
    public function registerClientScript()
205
    {
206 15
        $this->initClientOptions();
207 15
        $view = $this->getView();
208
209 15
        if ($this->minimalist) {
210 14
            JsonEditorMinimalistAsset::register($view);
211 14
        } else {
212 3
            JsonEditorFullAsset::register($view);
213
        }
214
215 15
        $hiddenInputId = $this->options['id'];
216 15
        $editorName = Inflector::variablize($hiddenInputId) . 'JsonEditor_' . hash('crc32', $hiddenInputId);
217 15
        $this->options['data-json-editor-name'] = $editorName;
218
219 15
        $jsUpdateHiddenField = "jQuery('#$hiddenInputId').val($editorName.getText());";
220
221 15
        if (isset($this->clientOptions['onChange'])) {
222 1
            $userFunction = " var userFunction = {$this->clientOptions['onChange']}; userFunction.call(this);";
223 1
        } else {
224 14
            $userFunction = '';
225
        }
226 15
        $this->clientOptions['onChange'] = new JsExpression("function() {{$jsUpdateHiddenField}$userFunction}");
227
228 15
        if (!empty($this->collapseAll) || !empty($this->expandAll)) {
229 2
            if (isset($this->clientOptions['onModeChange'])) {
230 1
                $userFunction = " var userFunction = {$this->clientOptions['onModeChange']}; " .
231 1
                    "userFunction.call(this, newMode, oldMode);";
232 1
            } else {
233 1
                $userFunction = '';
234
            }
235 2
            $jsOnModeChange = "function(newMode, oldMode) {";
236 2
            foreach (['collapseAll', 'expandAll'] as $property) {
237 2
                if (!empty($this->$property)) {
238 2
                    $jsOnModeChange .= "if (" . Json::htmlEncode($this->$property) . ".indexOf(newMode) !== -1) " .
239 2
                        "{{$editorName}.$property();}";
240 2
                }
241 2
            }
242 2
            $jsOnModeChange .= "$userFunction}";
243 2
            $this->clientOptions['onModeChange'] = new JsExpression($jsOnModeChange);
244 2
        }
245
246 15
        $htmlEncodedValue = Json::htmlEncode($this->decodedValue); // Json::htmlEncode is needed to prevent XSS
247 15
        $jsCode = "$editorName = new JSONEditor(document.getElementById('{$this->containerOptions['id']}'), " .
248 15
            Json::htmlEncode($this->clientOptions) . ");\n" .
249 15
            "$editorName.set($htmlEncodedValue);\n" . // have to use set method,
250
            // because constructor works wrong for '0', 'null', '""'; constructor turns them to {}, which may be wrong
251 15
            "jQuery('#$hiddenInputId').parents('form').submit(function() {{$jsUpdateHiddenField}});";
252 15
        if (in_array($this->mode, $this->collapseAll)) {
253 1
            $jsCode .= "\n$editorName.collapseAll();";
254 1
        }
255 15
        if (in_array($this->mode, $this->expandAll)) {
256 1
            $jsCode .= "\n$editorName.expandAll();";
257 1
        }
258 15
        $view->registerJs($jsCode);
259 15
    }
260
}
261