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 string default value |
41
|
|
|
*/ |
42
|
|
|
public $defaultValue = '{}'; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* @var string[] list of JSON editor modes for which all fields should be expanded automatically; |
46
|
|
|
* allowed modes 'tree', 'view', and 'form' |
47
|
|
|
* @see https://github.com/josdejong/jsoneditor/blob/master/docs/api.md#jsoneditorexpandall |
48
|
|
|
*/ |
49
|
|
|
public $expandAll = []; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* @var null|boolean whether to use minimalist version of JSON editor; |
53
|
|
|
* note that "minimalist" is not the same as "minimized"; |
54
|
|
|
* if property is not set then extension will try to determine automatically whether full version is needed, |
55
|
|
|
* if full version is not required then minimalist version will be used; |
56
|
|
|
* you can explicitly set this property to true or false if automatic detection does not fit for you application |
57
|
|
|
* @see https://github.com/josdejong/jsoneditor/blob/master/dist/which%20files%20do%20I%20need.md |
58
|
|
|
*/ |
59
|
|
|
public $minimalist; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* @var string default JSON editor mode |
63
|
|
|
*/ |
64
|
|
|
private $mode = 'tree'; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* @var string[] available JSON editor modes |
68
|
|
|
*/ |
69
|
|
|
private $modes = []; |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* @inheritdoc |
73
|
|
|
*/ |
74
|
6 |
|
public function init() |
75
|
|
|
{ |
76
|
6 |
|
parent::init(); |
77
|
6 |
|
if (!isset($this->containerOptions['id'])) { |
78
|
6 |
|
$this->containerOptions['id'] = $this->options['id'] . '-json-editor'; |
79
|
6 |
|
} |
80
|
6 |
|
if (!array_key_exists('style', $this->containerOptions)) { |
81
|
6 |
|
$this->containerOptions['style'] = 'height: 250px;'; |
82
|
6 |
|
} |
83
|
6 |
|
if ($this->hasModel()) { |
84
|
2 |
|
$this->value = Html::getAttributeValue($this->model, $this->attribute); |
|
|
|
|
85
|
2 |
|
} |
86
|
6 |
|
if (empty($this->value)) { |
87
|
4 |
|
$this->value = $this->defaultValue; |
88
|
4 |
|
} |
89
|
6 |
|
foreach (['mode', 'modes'] as $parameterName) { |
90
|
6 |
|
$this->$parameterName = ArrayHelper::getValue($this->clientOptions, $parameterName, $this->$parameterName); |
91
|
6 |
|
} |
92
|
|
|
// make sure that "mode" is specified, otherwise JavaScript error can occur in some situations |
93
|
6 |
|
$this->clientOptions['mode'] = $this->mode; |
94
|
6 |
|
if (!isset($this->minimalist)) { |
95
|
6 |
|
$this->minimalist = $this->mode != 'code' && !in_array('code', $this->modes); |
96
|
6 |
|
} |
97
|
6 |
|
} |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* @inheritdoc |
101
|
|
|
*/ |
102
|
6 |
|
public function run() |
103
|
|
|
{ |
104
|
6 |
|
$this->registerClientScript(); |
105
|
6 |
|
if ($this->hasModel()) { |
106
|
2 |
|
echo Html::activeHiddenInput($this->model, $this->attribute, $this->options); |
107
|
2 |
|
} else { |
108
|
4 |
|
echo Html::hiddenInput($this->name, $this->value, $this->options); |
109
|
|
|
} |
110
|
6 |
|
echo Html::tag('div', '', $this->containerOptions); |
111
|
6 |
|
} |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* Initializes client options. |
115
|
|
|
*/ |
116
|
6 |
|
protected function initClientOptions() |
117
|
|
|
{ |
118
|
6 |
|
$options = $this->clientOptions; |
119
|
6 |
|
$jsExpressionOptions = ['ace', 'ajv', 'onChange', 'onEditable', 'onError', 'onModeChange', 'schema']; |
120
|
6 |
|
foreach ($options as $key => $value) { |
121
|
6 |
|
if (!$value instanceof JsExpression && in_array($key, $jsExpressionOptions)) { |
122
|
1 |
|
$options[$key] = new JsExpression($value); |
123
|
1 |
|
} |
124
|
6 |
|
} |
125
|
6 |
|
$this->clientOptions = $options; |
126
|
6 |
|
} |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* Registers the needed client script. |
130
|
|
|
*/ |
131
|
6 |
|
public function registerClientScript() |
132
|
|
|
{ |
133
|
6 |
|
$this->initClientOptions(); |
134
|
6 |
|
$view = $this->getView(); |
135
|
|
|
|
136
|
6 |
|
if ($this->minimalist) { |
137
|
5 |
|
JsonEditorMinimalistAsset::register($view); |
138
|
5 |
|
} else { |
139
|
3 |
|
JsonEditorFullAsset::register($view); |
140
|
|
|
} |
141
|
|
|
|
142
|
6 |
|
$hiddenInputId = $this->options['id']; |
143
|
6 |
|
$editorName = Inflector::variablize($hiddenInputId) . 'JsonEditor_' . hash('crc32', $hiddenInputId); |
144
|
6 |
|
$this->options['data-json-editor-name'] = $editorName; |
145
|
|
|
|
146
|
6 |
|
$jsUpdateHiddenField = "jQuery('#$hiddenInputId').val($editorName.getText());"; |
147
|
|
|
|
148
|
6 |
|
if (isset($this->clientOptions['onChange'])) { |
149
|
1 |
|
$userFunction = " var userFunction = {$this->clientOptions['onChange']}; userFunction.call(this);"; |
150
|
1 |
|
} else { |
151
|
5 |
|
$userFunction = ''; |
152
|
|
|
} |
153
|
6 |
|
$this->clientOptions['onChange'] = new JsExpression("function() {{$jsUpdateHiddenField}$userFunction}"); |
154
|
|
|
|
155
|
6 |
|
if (!empty($this->collapseAll) || !empty($this->expandAll)) { |
156
|
2 |
|
if (isset($this->clientOptions['onModeChange'])) { |
157
|
1 |
|
$userFunction = " var userFunction = {$this->clientOptions['onModeChange']}; " . |
158
|
1 |
|
"userFunction.call(this, newMode, oldMode);"; |
159
|
1 |
|
} else { |
160
|
1 |
|
$userFunction = ''; |
161
|
|
|
} |
162
|
2 |
|
$jsOnModeChange = "function(newMode, oldMode) {"; |
163
|
2 |
|
foreach (['collapseAll', 'expandAll'] as $property) { |
164
|
2 |
|
if (!empty($this->$property)) { |
165
|
2 |
|
$jsOnModeChange .= "if (" . Json::htmlEncode($this->$property) . ".indexOf(newMode) !== -1) " . |
166
|
2 |
|
"{{$editorName}.$property();}"; |
167
|
2 |
|
} |
168
|
2 |
|
} |
169
|
2 |
|
$jsOnModeChange .= "$userFunction}"; |
170
|
2 |
|
$this->clientOptions['onModeChange'] = new JsExpression($jsOnModeChange); |
171
|
2 |
|
} |
172
|
|
|
|
173
|
6 |
|
$encodedValue = Json::htmlEncode(Json::decode($this->value, false)); |
174
|
6 |
|
$jsCode = "$editorName = new JSONEditor(document.getElementById('{$this->containerOptions['id']}'), " . |
175
|
6 |
|
Json::htmlEncode($this->clientOptions) . ", $encodedValue);\n" . |
176
|
6 |
|
"jQuery('#$hiddenInputId').parents('form').submit(function() {{$jsUpdateHiddenField}});"; |
177
|
6 |
|
if (in_array($this->mode, $this->collapseAll)) { |
178
|
1 |
|
$jsCode .= "\n$editorName.collapseAll();"; |
179
|
1 |
|
} |
180
|
6 |
|
if (in_array($this->mode, $this->expandAll)) { |
181
|
1 |
|
$jsCode .= "\n$editorName.expandAll();"; |
182
|
1 |
|
} |
183
|
6 |
|
$view->registerJs($jsCode); |
184
|
6 |
|
} |
185
|
|
|
} |
186
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.