Card   B
last analyzed

Complexity

Total Complexity 48

Size/Duplication

Total Lines 344
Duplicated Lines 0 %

Importance

Changes 13
Bugs 0 Features 0
Metric Value
wmc 48
eloc 128
c 13
b 0
f 0
dl 0
loc 344
rs 8.5599

9 Methods

Rating   Name   Duplication   Size   Complexity  
A renderActionItem() 0 25 5
A setCardClass() 0 13 5
A renderActionButton() 0 15 3
B renderTitleContent() 0 18 7
B run() 0 34 7
B init() 0 39 8
A renderRevealContent() 0 20 5
A renderImageContent() 0 20 6
A renderTabsItem() 0 12 2

How to fix   Complexity   

Complex Class

Complex classes like Card often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Card, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @link https://github.com/DMGPage/yii2-materialize
4
 * @copyright Copyright (c) 2018 Dmitrijs Reinmanis
5
 * @license https://github.com/DMGPage/yii2-materialize/blob/master/LICENSE
6
 */
7
8
namespace dmgpage\yii2materialize\widgets;
9
10
use dmgpage\yii2materialize\helpers\Html;
11
use yii\helpers\ArrayHelper;
12
use yii\base\InvalidConfigException;
13
use dmgpage\yii2materialize\assets\MaterializeExtraAsset;
14
15
/**
16
 * Cards are a convenient means of displaying content composed of different types of objects.
17
 * They’re also well-suited for presenting similar objects whose size or supported actions can vary considerably,
18
 * like photos with captions of variable length.
19
 *
20
 * You can use Cards like this:
21
 *
22
 * ```php
23
 * echo Card::widget([
24
 *     'options' => ['class' => 'light-blue darken-4'],
25
 *     'image' => [
26
 *         'title' => 'Image Card Title',
27
 *         'url' => 'https://materializecss.com/images/sample-1.jpg',
28
 *         'fab' => [
29
 *             'type' => ButtonType::FLOATING,
30
 *             'size' => Size::MEDIUM,
31
 *             'waves' => Waves::LIGHT,
32
 *             'icon' => ['name' => 'add'],
33
 *             'options' => ['class' => 'light-blue accent-2']
34
 *         ]
35
 *     ],
36
 *     'content' => [
37
 *         'value' => '<p>I am a very simple card. I am good at containing small bits of information. '
38
 *             . 'I am convenient because I require little markup to use effectively.</p>',
39
 *         'options' => ['class' => 'white-text']
40
 *     ],
41
 *     'actions' => [
42
 *         ['label' => 'This is a link #1'],
43
 *         ['label' => 'This is a link #2']
44
 *     ],
45
 * ]);
46
 * ```
47
 * @see https://materializecss.com/cards.html
48
 * @package widgets
49
 */
50
class Card extends Widget
51
{
52
    /**
53
     * @var bool whether to HTML-encode the link labels and card title
54
     */
55
    public $encodeLabels = true;
56
57
    /**
58
     * @var array a list of attributes to be displayed in the card content tag. Item should be an array
59
     * of the following structure:
60
     * - title: string, title for the card image. Value will be HTML-encoded.
61
     *   You can change this, by setting extra attribute ["encode" => false] in "titleOptions" attribute
62
     * - value: string, the HTML content for the card body. It will NOT be HTML-encoded.
63
     *   Therefore you can pass in HTML code. If this is coming from end users,
64
     *   you should consider encode() it to prevent XSS attacks.
65
     * - options: array, the HTML attributes for the card content tag.
66
     * - titleOptions: array the HTML attributes for the title tag. You can specify 'icon' attribute
67
     *   (name for the icon), if you want to add icon to the right side of the title.
68
     */
69
    public $content = [];
70
71
    /**
72
     * @var array a list of attributes to be displayed in the card image tag. Item should be an array
73
     * of the following structure:
74
     * - title: string, title for the card image. Value will be HTML-encoded.
75
     *   You can change this, by setting extra attribute ["encode" => false] in "titleOptions" attribute
76
     * - url: string the image URL. This parameter will be processed by [[Url::to()]].
77
     * - fab: array list of attributes for floating action button. Value will be passed to [[Button]] widget.
78
     *   You can set extra param 'url' to render it as link.
79
     * - options: array, the HTML attributes for the card image tag.
80
     * - titleOptions: array the HTML attributes for the title tag. You can specify 'icon' attribute
81
     *   (name for the icon), if you want to add icon to the right side of the title.
82
     * - imageOptions: array the HTML attributes for the image tag.
83
     */
84
    public $image = [];
85
86
    /**
87
     * @var array list of card action items. Each action item should be an array of the following structure:
88
     * - label: string, specifies the action item label. When [[encodeLabels]] is true, the label
89
     *   will be HTML-encoded.
90
     * - encode: boolean, optional, whether this item`s label should be HTML-encoded. This param will override
91
     *   global [[encodeLabels]] param.
92
     * - url: string or array, optional, specifies the URL of the action item. It will be processed by [[Url::to]].
93
     * - icon: string or array, optional, icon name or array with 'name' and 'options'.
94
     * - options: array, optional, the HTML attributes for the action container tag.
95
     */
96
    public $actions = [];
97
98
    /**
99
     * @var array the HTML attributes for the action wrapper tag of the card view.
100
     * You can use attribute "sticky" to change, whether card actions must be always visible.
101
     * Default value is false.
102
     * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
103
     */
104
    public $actionOptions = [];
105
106
    /**
107
     * @var array a list of attributes to be displayed in the card reveal tag. Item should be an array
108
     * of the following structure:
109
     * - title: string, title for the card image. Value will be HTML-encoded.
110
     *   You can change this, by setting extra attribute ["encode" => false] in "titleOptions" attribute
111
     * - value: string, the HTML content for the card body. It will NOT be HTML-encoded.
112
     *   Therefore you can pass in HTML code. If this is coming from end users,
113
     *   you should consider encode() it to prevent XSS attacks.
114
     * - options: array, the HTML attributes for the card content tag.
115
     * - titleOptions: array the HTML attributes for the title tag. You can specify 'icon' attribute
116
     *   (name for the icon), if you want to add icon to the right side of the title.
117
     */
118
    public $reveal = [];
119
120
    /**
121
     * @var bool whether image should be on left side. Default value is false
122
     */
123
    public $horizontal = false;
124
125
    /**
126
     * @var bool whether card should be spaced as panel. Default value is false.
127
     * Use this for a simpler card with less markup, because it has only padding and a shadow effect.
128
     */
129
    public $panel = false;
130
131
    /**
132
     * @var Tabs the class for tab content creation
133
     * @see \dmgpage\yii2materialize\widgets\Tabs for details on how content are being rendered.
134
     */
135
    public $tabs;
136
137
    /**
138
     * Initializes the widget.
139
     * @return void
140
     */
141
    public function init()
142
    {
143
        parent::init();
144
145
        $contentData = $this->content;
146
        $cardContentOptions = isset($this->content['options']) ? $this->content['options'] : [];
147
        Html::addCssClass($cardContentOptions, ['class' => 'card-content']);
148
        $this->setCardClass(); // set main css classes to card tag
149
150
        $html = Html::beginTag('div', $this->options);
151
        $html .= $this->renderImageContent();
152
153
        // Create stacked content, if horizontal attribute is true
154
        if ($this->horizontal) {
155
            $html .= Html::beginTag('div', ['class' => 'card-stacked']);
156
        }
157
158
        // Add reviel button to content title, if reveal attribute is not empty
159
        if (!empty($this->reveal)) {
160
            Html::addCssClass($contentData['titleOptions'], 'activator');
161
            $addIcon = !isset($contentData['titleOptions']['icon']);
162
            $contentData['titleOptions']['icon'] = $addIcon
163
                ? 'more_vert'
164
                : $contentData['titleOptions']['icon'];
165
        }
166
167
        // Create card content, if panel not true
168
        if (!$this->panel) {
169
            $html .= Html::beginTag('div', $cardContentOptions);
170
        }
171
172
        // Validate tab attribute
173
        if ($this->tabs && !$this->tabs instanceof Tabs) {
0 ignored issues
show
introduced by
$this->tabs is always a sub-type of dmgpage\yii2materialize\widgets\Tabs.
Loading history...
174
            throw new InvalidConfigException('"' . get_class($this) . '::$tabs" should be an instance of "\dmgpage\yii2materialize\widgets\Tabs".');
175
        }
176
177
        $html .= $this->renderTitleContent($contentData);
178
179
        echo $html;
180
    }
181
182
    /**
183
     * Renders the widget.
184
     * @return void
185
     */
186
    public function run()
187
    {
188
        $this->registerPlugin('card');
189
190
        $html = isset($this->content['value']) ? $this->content['value'] : '';
191
192
        if (!$this->panel) {
193
            $html .= Html::endTag('div'); // ends card-content tag
194
        }
195
196
        if (!empty($this->actions)) {
197
            Html::addCssClass($this->actionOptions, ['class' => 'card-action']);
198
            $html .= Html::beginTag('div', $this->actionOptions);
199
200
            foreach ($this->actions as $action) {
201
                $html .= $this->renderActionItem($action);
202
            }
203
204
            $html .= Html::endTag('div');
205
        }
206
207
        if ($this->horizontal) {
208
            $html .= Html::endTag('div'); //ends card-stacked tag
209
        }
210
211
        if ($this->tabs) {
212
             $html .= $this->renderTabsItem();
213
        }
214
215
        // Add card reveal tag
216
        $html .= $this->renderRevealContent();
217
        $html .= Html::endTag('div'); //ends card tag
218
219
        echo $html;
220
    }
221
222
    /**
223
     * Sets card CSS class (or several classes) to the card options.
224
     * @return void
225
     */
226
    protected function setCardClass()
227
    {
228
        $useStickyActions = isset($this->actionOptions['sticky']) ? $this->actionOptions['sticky'] : false;
229
230
        if ($this->panel) {
231
            Html::addCssClass($this->options, ['card-panel']);
232
        } elseif ($this->horizontal) {
233
            Html::addCssClass($this->options, ['card', 'horizontal']);
234
        } elseif ($useStickyActions) {
235
            Html::addCssClass($this->options, ['card', 'sticky-action']);
236
            unset($this->actionOptions['sticky']);
237
        } else {
238
            Html::addCssClass($this->options, ['card']);
239
        }
240
    }
241
242
    /**
243
     * Renders card content title tag content.
244
     *
245
     * @param array $source data source for title and options
246
     * @return string the rendering result
247
     */
248
    protected function renderTitleContent($source)
249
    {
250
        $html = '';
251
252
        if (isset($source['title'])) {
253
            $titleValue = $source['title'];
254
            $titleOptions = isset($source['titleOptions']) ? $source['titleOptions'] : [];
255
            $encode = isset($titleOptions['encode']) ? $titleOptions['encode'] : $this->encodeLabels;
256
            $icon = isset($titleOptions['icon']) ? $titleOptions['icon'] : null;
257
            unset($titleOptions['encode']);
258
            Html::addCssClass($titleOptions, 'card-title');
259
            $title = $encode ? Html::encode($titleValue) : $titleValue;
260
            $title .= !empty($icon) ? Html::icon($icon, ['class' => 'material-icons right']) : '';
261
            unset($titleOptions['icon']);
262
            $html .= Html::tag('span', $title, $titleOptions);
263
        }
264
265
        return $html;
266
    }
267
268
    /**
269
     * Renders a single card action item.
270
     *
271
     * @param array $link the link to be rendered. It must contain the "label" element. The "url" and "icon" element is optional.
272
     * @return string the rendering result
273
     * @throws InvalidConfigException if `$link` does not have "label" element.
274
     */
275
    protected function renderActionItem($link)
276
    {
277
        $encodeLabel = ArrayHelper::remove($link, 'encode', $this->encodeLabels);
278
279
        if (array_key_exists('label', $link)) {
280
            $label = $encodeLabel ? Html::encode($link['label']) : $link['label'];
281
        } else {
282
            throw new InvalidConfigException('The "label" element is required for each link.');
283
        }
284
285
        // Add icon to label text
286
        if (isset($link['icon'])) {
287
            $view = $this->getView();
288
            MaterializeExtraAsset::register($view);
289
290
            // Has issues on positioning: https://github.com/google/material-design-icons/issues/206
291
            $label = $this->renderIcon($link['icon']) . $label;
292
        }
293
294
        $options = $link['options'];
295
296
        if (isset($link['url'])) {
297
            return Html::a($label, $link['url'], $options);
298
        } else {
299
            return Html::a($label, '#', $options);
300
        }
301
    }
302
303
    /**
304
     * Renders card-image tag content.
305
     * @return string the rendering result
306
     */
307
    protected function renderImageContent()
308
    {
309
        $html = '';
310
311
        if (!empty($this->image)) {
312
            $contentData = $this->image;
313
            $fabData = isset($contentData['fab']) ? $contentData['fab'] : [];
314
            $options = isset($contentData['options']) ? $contentData['options'] : [];
315
            $imageOptions = isset($contentData['imageOptions']) ? $contentData['imageOptions'] : [];
316
            $imageUrl = isset($contentData['url']) ? $contentData['url'] : [];
317
            Html::addCssClass($options, ['class' => 'card-image']);
318
319
            $html .= Html::beginTag('div', $options);
320
            $html .= Html::img($imageUrl, $imageOptions);
321
            $html .= $this->renderTitleContent($contentData);
322
            $html .= $this->renderActionButton($fabData);
323
            $html .= Html::endTag('div');
324
        }
325
326
        return $html;
327
    }
328
329
    /**
330
     * Renders floating action button tag.
331
     *
332
     * @param array $config attribute values and options for Button widget.
333
     * @return string the rendering result
334
     */
335
    protected function renderActionButton($config)
336
    {
337
        $html = '';
338
339
        if (!empty($config)) {
340
            if (!isset($config['options'])) {
341
                $config['options'] = ['class' => 'halfway-fab'];
342
            } else {
343
                Html::addCssClass($config['options'], 'halfway-fab');
344
            }
345
346
            $html .= Button::widget($config);
347
        }
348
349
        return $html;
350
    }
351
352
    /**
353
     * Renders card-reveal tag content.
354
     * @return string the rendering result
355
     */
356
    protected function renderRevealContent()
357
    {
358
        $html = '';
359
360
        if (!empty($this->reveal)) {
361
            $contentData = $this->reveal;
362
            $options = isset($contentData['options']) ? $contentData['options'] : [];
363
            $addIcon = !isset($contentData['titleOptions']['icon']);
364
            $contentData['titleOptions']['icon'] = $addIcon
365
                ? 'close'
366
                : $contentData['titleOptions']['icon'];
367
            Html::addCssClass($options, ['class' => 'card-reveal']);
368
369
            $html .= Html::beginTag('div', $options);
370
            $html .= $this->renderTitleContent($contentData);
371
            $html .= isset($contentData['value']) ? $contentData['value'] : '';
372
            $html .= Html::endTag('div');
373
        }
374
375
        return $html;
376
    }
377
378
    /**
379
     * Renders card-tabs tag content.
380
     * @return string the rendering result
381
     */
382
    protected function renderTabsItem()
383
    {
384
        $html = '';
385
        $widget = $this->tabs;
386
387
        if ($widget->beforeRun()) {
388
            $widget->insideCard = true;
389
            $result = $widget->run();
390
            $html .= $widget->afterRun($result);
391
        }
392
393
        return $html;
394
    }
395
}
396