Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace core\output;
18
 
19
use core\exception\coding_exception;
20
use core\lang_string;
21
use core\output\local\action_menu\subpanel;
22
use core\output\action_menu\link as action_menu_link;
23
use core\output\action_menu\filler as action_menu_filler;
24
use moodle_page;
25
use stdClass;
26
 
27
/**
28
 * An action menu.
29
 *
30
 * This action menu component takes a series of primary and secondary actions.
31
 * The primary actions are displayed permanently and the secondary attributes are displayed within a drop
32
 * down menu.
33
 *
34
 * @package core
35
 * @category output
36
 * @copyright 2013 Sam Hemelryk
37
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38
 */
39
class action_menu implements renderable, templatable {
40
    /**
41
     * Top right alignment.
42
     */
43
    const TL = 1;
44
 
45
    /**
46
     * Top right alignment.
47
     */
48
    const TR = 2;
49
 
50
    /**
51
     * Top right alignment.
52
     */
53
    const BL = 3;
54
 
55
    /**
56
     * Top right alignment.
57
     */
58
    const BR = 4;
59
 
60
    /**
61
     * The instance number. This is unique to this instance of the action menu.
62
     * @var int
63
     */
64
    protected $instance = 0;
65
 
66
    /**
67
     * An array of primary actions. Please use {@see action_menu::add_primary_action()} to add actions.
68
     * @var array
69
     */
70
    protected $primaryactions = [];
71
 
72
    /**
73
     * An array of secondary actions. Please use {@see action_menu::add_secondary_action()} to add actions.
74
     * @var array
75
     */
76
    protected $secondaryactions = [];
77
 
78
    /**
79
     * An array of attributes added to the container of the action menu.
80
     * Initialised with defaults during construction.
81
     * @var array
82
     */
83
    public $attributes = [];
84
    /**
85
     * An array of attributes added to the container of the primary actions.
86
     * Initialised with defaults during construction.
87
     * @var array
88
     */
89
    public $attributesprimary = [];
90
    /**
91
     * An array of attributes added to the container of the secondary actions.
92
     * Initialised with defaults during construction.
93
     * @var array
94
     */
95
    public $attributessecondary = [];
96
 
97
    /**
98
     * The string to use next to the icon for the action icon relating to the secondary (dropdown) menu.
99
     * @var array
100
     */
101
    public $actiontext = null;
102
 
103
    /**
104
     * The string to use for the accessible label for the menu.
105
     * @var array
106
     */
107
    public $actionlabel = null;
108
 
109
    /**
110
     * An icon to use for the toggling the secondary menu (dropdown).
111
     * @var pix_icon
112
     */
113
    public $actionicon;
114
 
115
    /**
116
     * Any text to use for the toggling the secondary menu (dropdown).
117
     * @var string
118
     */
119
    public $menutrigger = '';
120
 
121
    /**
122
     * An array of attributes added to the trigger element of the secondary menu.
123
     * @var array
124
     */
125
    public $triggerattributes = [];
126
 
127
    /**
128
     * Any extra classes for toggling to the secondary menu.
129
     * @var string
130
     */
131
    public $triggerextraclasses = '';
132
 
133
    /**
134
     * Place the action menu before all other actions.
135
     * @var bool
136
     */
137
    public $prioritise = false;
138
 
139
    /**
140
     * Dropdown menu alignment class.
141
     * @var string
142
     */
143
    public $dropdownalignment = '';
144
 
145
    /**
146
     * Constructs the action menu with the given items.
147
     *
148
     * @param array $actions An array of actions (action_menu_link|pix_icon|string).
149
     */
150
    public function __construct(array $actions = []) {
151
        static $initialised = 0;
152
        $this->instance = $initialised;
153
        $initialised++;
154
 
155
        $this->attributes = [
156
            'id' => 'action-menu-' . $this->instance,
157
            'class' => 'moodle-actionmenu',
158
            'data-enhance' => 'moodle-core-actionmenu',
159
        ];
160
        $this->attributesprimary = [
161
            'id' => 'action-menu-' . $this->instance . '-menubar',
162
            'class' => 'menubar',
163
        ];
164
        $this->attributessecondary = [
165
            'id' => 'action-menu-' . $this->instance . '-menu',
166
            'class' => 'menu',
167
            'data-rel' => 'menu-content',
168
            'aria-labelledby' => 'action-menu-toggle-' . $this->instance,
169
            'role' => 'menu',
170
        ];
171
        $this->dropdownalignment = 'dropdown-menu-end';
172
        foreach ($actions as $action) {
173
            $this->add($action);
174
        }
175
    }
176
 
177
    /**
178
     * Sets the label for the menu trigger.
179
     *
180
     * @param string $label The text
181
     */
182
    public function set_action_label($label) {
183
        $this->actionlabel = $label;
184
    }
185
 
186
    /**
187
     * Sets the menu trigger text.
188
     *
189
     * @param string $trigger The text
190
     * @param string $extraclasses Extra classes to style the secondary menu toggle.
191
     */
192
    public function set_menu_trigger($trigger, $extraclasses = '') {
193
        $this->menutrigger = $trigger;
194
        $this->triggerextraclasses = $extraclasses;
195
    }
196
 
197
    /**
198
     * Classes for the trigger menu
199
     */
200
    const DEFAULT_KEBAB_TRIGGER_CLASSES = 'btn btn-icon d-flex no-caret';
201
 
202
    /**
203
     * Setup trigger as in the kebab menu.
204
     *
205
     * @param string|null $triggername
206
     * @param core_renderer|null $output
207
     * @param string|null $extraclasses extra classes for the trigger {@see self::set_menu_trigger()}
208
     * @throws coding_exception
209
     */
210
    public function set_kebab_trigger(
211
        ?string $triggername = null,
212
        ?core_renderer $output = null,
213
        ?string $extraclasses = ''
214
    ) {
215
        global $OUTPUT;
216
        if (empty($output)) {
217
            $output = $OUTPUT;
218
        }
219
        $label = $triggername ?? get_string('actions');
220
        $triggerclasses = self::DEFAULT_KEBAB_TRIGGER_CLASSES . ' ' . $extraclasses;
221
        $icon = $output->pix_icon('i/menu', $label);
222
        $this->set_menu_trigger($icon, $triggerclasses);
223
    }
224
 
225
    /**
226
     * Return true if there is at least one visible link in the menu.
227
     *
228
     * @return bool
229
     */
230
    public function is_empty() {
231
        return !count($this->primaryactions) && !count($this->secondaryactions);
232
    }
233
 
234
    /**
235
     * Initialises JS required fore the action menu.
236
     * The JS is only required once as it manages all action menu's on the page.
237
     *
238
     * @param moodle_page $page
239
     */
240
    public function initialise_js(moodle_page $page) {
241
        static $initialised = false;
242
        if (!$initialised) {
243
            $page->requires->yui_module('moodle-core-actionmenu', 'M.core.actionmenu.init');
244
            $initialised = true;
245
        }
246
    }
247
 
248
    /**
249
     * Adds an action to this action menu.
250
     *
251
     * @param action_link|pix_icon|subpanel|string $action
252
     */
253
    public function add($action) {
254
 
255
        if ($action instanceof subpanel) {
256
            $this->add_secondary_subpanel($action);
257
        } else if ($action instanceof action_link) {
258
            if ($action->primary) {
259
                $this->add_primary_action($action);
260
            } else {
261
                $this->add_secondary_action($action);
262
            }
263
        } else if ($action instanceof pix_icon) {
264
            $this->add_primary_action($action);
265
        } else {
266
            $this->add_secondary_action($action);
267
        }
268
    }
269
 
270
    /**
271
     * Adds a secondary subpanel.
272
     * @param subpanel $subpanel
273
     */
274
    public function add_secondary_subpanel(subpanel $subpanel) {
275
        $this->secondaryactions[] = $subpanel;
276
    }
277
 
278
    /**
279
     * Adds a primary action to the action menu.
280
     *
281
     * @param action_menu_link|action_link|pix_icon|string $action
282
     */
283
    public function add_primary_action($action) {
284
        if ($action instanceof action_link || $action instanceof pix_icon) {
285
            $action->attributes['role'] = 'menuitem';
286
            $action->attributes['tabindex'] = '-1';
287
            if ($action instanceof action_menu_link) {
288
                $action->actionmenu = $this;
289
            }
290
        }
291
        $this->primaryactions[] = $action;
292
    }
293
 
294
    /**
295
     * Adds a secondary action to the action menu.
296
     *
297
     * @param action_link|pix_icon|string $action
298
     */
299
    public function add_secondary_action($action) {
300
        if ($action instanceof action_link || $action instanceof pix_icon) {
301
            $action->attributes['role'] = 'menuitem';
302
            $action->attributes['tabindex'] = '-1';
303
            if ($action instanceof action_menu_link) {
304
                $action->actionmenu = $this;
305
            }
306
        }
307
        $this->secondaryactions[] = $action;
308
    }
309
 
310
    /**
311
     * Returns the primary actions ready to be rendered.
312
     *
313
     * @param null|core_renderer $output The renderer to use for getting icons.
314
     * @return array
315
     */
316
    public function get_primary_actions(?core_renderer $output = null) {
317
        global $OUTPUT;
318
        if ($output === null) {
319
            $output = $OUTPUT;
320
        }
321
        $pixicon = $this->actionicon;
322
        $linkclasses = ['toggle-display'];
323
 
324
        $title = '';
325
        if (!empty($this->menutrigger)) {
326
            $pixicon = '<b class="caret"></b>';
327
            $linkclasses[] = 'textmenu';
328
        } else {
329
            $title = new lang_string('actionsmenu', 'moodle');
330
            $this->actionicon = new pix_icon(
331
                't/edit_menu',
332
                '',
333
                'moodle',
334
                ['class' => 'iconsmall actionmenu', 'title' => '']
335
            );
336
            $pixicon = $this->actionicon;
337
        }
338
        if ($pixicon instanceof renderable) {
339
            $pixicon = $output->render($pixicon);
340
            if ($pixicon instanceof pix_icon && isset($pixicon->attributes['alt'])) {
341
                $title = $pixicon->attributes['alt'];
342
            }
343
        }
344
        $string = '';
345
        if ($this->actiontext) {
346
            $string = $this->actiontext;
347
        }
348
        $label = '';
349
        if ($this->actionlabel) {
350
            $label = $this->actionlabel;
351
        } else {
352
            $label = $title;
353
        }
354
        $actions = $this->primaryactions;
355
        $attributes = [
356
            'class' => implode(' ', $linkclasses),
357
            'title' => $title,
358
            'aria-label' => $label,
359
            'id' => 'action-menu-toggle-' . $this->instance,
360
            'role' => 'menuitem',
361
            'tabindex' => '-1',
362
        ];
363
        $link = html_writer::link('#', $string . $this->menutrigger . $pixicon, $attributes);
364
        if ($this->prioritise) {
365
            array_unshift($actions, $link);
366
        } else {
367
            $actions[] = $link;
368
        }
369
        return $actions;
370
    }
371
 
372
    /**
373
     * Returns the secondary actions ready to be rendered.
374
     * @return array
375
     */
376
    public function get_secondary_actions() {
377
        return $this->secondaryactions;
378
    }
379
 
380
    /**
381
     * Sets the selector that should be used to find the owning node of this menu.
382
     * @param string $selector A CSS/YUI selector to identify the owner of the menu.
383
     */
384
    public function set_owner_selector($selector) {
385
        $this->attributes['data-owner'] = $selector;
386
    }
387
 
388
    /**
389
     * @deprecated since Moodle 4.0, use action_menu::set_menu_left().
390
     */
391
    #[\core\attribute\deprecated('action_menu::set_menu_left', since: '4.0', mdl: 'MDL-72466', final: true)]
392
    public function set_alignment(): void {
393
        \core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
394
    }
395
 
396
    /**
397
     * Returns a string to describe the alignment.
398
     *
399
     * @param int $align One of action_menu::TL, action_menu::TR, action_menu::BL, action_menu::BR.
400
     * @return string
401
     */
402
    protected function get_align_string($align) {
403
        switch ($align) {
404
            case self::TL:
405
                return 'tl';
406
            case self::TR:
407
                return 'tr';
408
            case self::BL:
409
                return 'bl';
410
            case self::BR:
411
                return 'br';
412
            default:
413
                return 'tl';
414
        }
415
    }
416
 
417
    /**
418
     * Aligns the left corner of the dropdown.
419
     *
420
     */
421
    public function set_menu_left() {
422
        $this->dropdownalignment = 'dropdown-menu-start';
423
    }
424
 
425
    /**
426
     * @deprecated since Moodle 4.3, use set_boundary() method instead.
427
     */
428
    #[\core\attribute\deprecated('action_menu::set_boundary', since: '4.3', mdl: 'MDL-77375', final: true)]
429
    public function set_constraint(): void {
430
        \core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
431
    }
432
 
433
    /**
434
     * Set the overflow constraint boundary of the dropdown menu.
435
     * @see https://getbootstrap.com/docs/4.6/components/dropdowns/#options The 'boundary' option in the Bootstrap documentation
436
     *
437
     * @param string $boundary Accepts the values of 'viewport', 'window', or 'scrollParent'.
438
     * @throws coding_exception
439
     */
440
    public function set_boundary(string $boundary) {
441
        if (!in_array($boundary, ['viewport', 'window', 'scrollParent'])) {
442
            throw new coding_exception("HTMLElement reference boundaries are not supported." .
443
                "Accepted boundaries are 'viewport', 'window', or 'scrollParent'.", DEBUG_DEVELOPER);
444
        }
445
 
446
        $this->triggerattributes['data-boundary'] = $boundary;
447
    }
448
 
449
    /**
450
     * @deprecated since Moodle 3.2, use a list of action_icon instead.
451
     */
452
    #[\core\attribute\deprecated('Use a list of action_icons instead', since: '3.2', mdl: 'MDL-55904', final: true)]
453
    public function do_not_enhance() {
454
        \core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
455
    }
456
 
457
    /**
458
     * Returns true if this action menu will be enhanced.
459
     *
460
     * @return bool
461
     */
462
    public function will_be_enhanced() {
463
        return isset($this->attributes['data-enhance']);
464
    }
465
 
466
    /**
467
     * Sets nowrap on items. If true menu items should not wrap lines if they are longer than the available space.
468
     *
469
     * This property can be useful when the action menu is displayed within a parent element that is either floated
470
     * or relatively positioned.
471
     * In that situation the width of the menu is determined by the width of the parent element which may not be large
472
     * enough for the menu items without them wrapping.
473
     * This disables the wrapping so that the menu takes on the width of the longest item.
474
     *
475
     * @param bool $value If true nowrap gets set, if false it gets removed. Defaults to true.
476
     */
477
    public function set_nowrap_on_items($value = true) {
478
        $class = 'nowrap-items';
479
        if (!empty($this->attributes['class'])) {
480
            $pos = strpos($this->attributes['class'], $class);
481
            if ($value === true && $pos === false) {
482
                // The value is true and the class has not been set yet. Add it.
483
                $this->attributes['class'] .= ' ' . $class;
484
            } else if ($value === false && $pos !== false) {
485
                // The value is false and the class has been set. Remove it.
486
                $this->attributes['class'] = substr($this->attributes['class'], $pos, strlen($class));
487
            }
488
        } else if ($value) {
489
            // The value is true and the class has not been set yet. Add it.
490
            $this->attributes['class'] = $class;
491
        }
492
    }
493
 
494
    /**
495
     * Add classes to the action menu for an easier styling.
496
     *
497
     * @param string $class The class to add to attributes.
498
     */
499
    public function set_additional_classes(string $class = '') {
500
        if (!empty($this->attributes['class'])) {
501
            $this->attributes['class'] .= " " . $class;
502
        } else {
503
            $this->attributes['class'] = $class;
504
        }
505
    }
506
 
507
    /**
508
     * Export for template.
509
     *
510
     * @param renderer_base $output The renderer.
511
     * @return stdClass
512
     */
513
    public function export_for_template(renderer_base $output) {
514
        $data = new stdClass();
515
        // Assign a role of menubar to this action menu when:
516
        // - it contains 2 or more primary actions; or
517
        // - if it contains a primary action and secondary actions.
518
        if (count($this->primaryactions) > 1 || (!empty($this->primaryactions) && !empty($this->secondaryactions))) {
519
            $this->attributes['role'] = 'menubar';
520
        }
521
        $attributes = $this->attributes;
522
 
523
        $data->instance = $this->instance;
524
 
525
        $data->classes = isset($attributes['class']) ? $attributes['class'] : '';
526
        unset($attributes['class']);
527
 
528
        $data->attributes = array_map(function ($key, $value) {
529
            return [ 'name' => $key, 'value' => $value ];
530
        }, array_keys($attributes), $attributes);
531
 
532
        $data->primary = $this->export_primary_actions_for_template($output);
533
        $data->secondary = $this->export_secondary_actions_for_template($output);
534
        $data->dropdownalignment = $this->dropdownalignment;
535
 
536
        return $data;
537
    }
538
 
539
    /**
540
     * Export the primary actions for the template.
541
     * @param renderer_base $output
542
     * @return stdClass
543
     */
544
    protected function export_primary_actions_for_template(renderer_base $output): stdClass {
545
        $attributes = $this->attributes;
546
        $attributesprimary = $this->attributesprimary;
547
 
548
        $primary = new stdClass();
549
        $primary->title = '';
550
        $primary->prioritise = $this->prioritise;
551
 
552
        $primary->classes = isset($attributesprimary['class']) ? $attributesprimary['class'] : '';
553
        unset($attributesprimary['class']);
554
 
555
        $primary->attributes = array_map(function ($key, $value) {
556
            return ['name' => $key, 'value' => $value];
557
        }, array_keys($attributesprimary), $attributesprimary);
558
        $primary->triggerattributes = array_map(function ($key, $value) {
559
            return ['name' => $key, 'value' => $value];
560
        }, array_keys($this->triggerattributes), $this->triggerattributes);
561
 
562
        $actionicon = $this->actionicon;
563
        if (!empty($this->menutrigger)) {
564
            $primary->menutrigger = $this->menutrigger;
565
            $primary->triggerextraclasses = $this->triggerextraclasses;
566
            if ($this->actionlabel) {
567
                $primary->title = $this->actionlabel;
568
            } else if ($this->actiontext) {
569
                $primary->title = $this->actiontext;
570
            } else {
571
                $primary->title = strip_tags($this->menutrigger);
572
            }
573
        } else {
574
            $primary->title = get_string('actionsmenu');
575
            $iconattributes = ['class' => 'iconsmall actionmenu', 'title' => $primary->title];
576
            $actionicon = new pix_icon('t/edit_menu', '', 'moodle', $iconattributes);
577
        }
578
 
579
        // If the menu trigger is within the menubar, assign a role of menuitem. Otherwise, assign as a button.
580
        $primary->triggerrole = 'button';
581
        if (isset($attributes['role']) && $attributes['role'] === 'menubar') {
582
            $primary->triggerrole = 'menuitem';
583
        }
584
 
585
        if ($actionicon instanceof pix_icon) {
586
            $primary->icon = $actionicon->export_for_pix();
587
            if (!empty($actionicon->attributes['alt'])) {
588
                $primary->title = $actionicon->attributes['alt'];
589
            }
590
        } else {
591
            $primary->iconraw = $actionicon ? $output->render($actionicon) : '';
592
        }
593
 
594
        $primary->actiontext = $this->actiontext ? (string) $this->actiontext : '';
595
        $primary->items = array_map(function ($item) use ($output) {
596
            $data = (object) [];
597
            if ($item instanceof action_menu_link) {
598
                $data->actionmenulink = $item->export_for_template($output);
599
            } else if ($item instanceof action_menu_filler) {
600
                $data->actionmenufiller = $item->export_for_template($output);
601
            } else if ($item instanceof action_link) {
602
                $data->actionlink = $item->export_for_template($output);
603
            } else if ($item instanceof pix_icon) {
604
                $data->pixicon = $item->export_for_template($output);
605
            } else {
606
                $data->rawhtml = ($item instanceof renderable) ? $output->render($item) : $item;
607
            }
608
            return $data;
609
        }, $this->primaryactions);
610
        return $primary;
611
    }
612
 
613
    /**
614
     * Export the secondary actions for the template.
615
     * @param renderer_base $output
616
     * @return stdClass
617
     */
618
    protected function export_secondary_actions_for_template(renderer_base $output): stdClass {
619
        $attributessecondary = $this->attributessecondary;
620
        $secondary = new stdClass();
621
        $secondary->classes = isset($attributessecondary['class']) ? $attributessecondary['class'] : '';
622
        unset($attributessecondary['class']);
623
 
624
        $secondary->attributes = array_map(function ($key, $value) {
625
            return ['name' => $key, 'value' => $value];
626
        }, array_keys($attributessecondary), $attributessecondary);
627
        $secondary->items = array_map(function ($item) use ($output) {
628
            $data = (object) [
629
                'simpleitem' => true,
630
            ];
631
            if ($item instanceof action_menu_link) {
632
                $data->actionmenulink = $item->export_for_template($output);
633
                $data->simpleitem = false;
634
            } else if ($item instanceof action_menu_filler) {
635
                $data->actionmenufiller = $item->export_for_template($output);
636
                $data->simpleitem = false;
637
            } else if ($item instanceof subpanel) {
638
                $data->subpanel = $item->export_for_template($output);
639
                $data->simpleitem = false;
640
            } else if ($item instanceof action_link) {
641
                $data->actionlink = $item->export_for_template($output);
642
            } else if ($item instanceof pix_icon) {
643
                $data->pixicon = $item->export_for_template($output);
644
            } else {
645
                $data->rawhtml = ($item instanceof renderable) ? $output->render($item) : $item;
646
            }
647
            return $data;
648
        }, $this->secondaryactions);
649
        return $secondary;
650
    }
651
}
652
 
653
// Alias this class to the old name.
654
// This file will be autoloaded by the legacyclasses autoload system.
655
// In future all uses of this class will be corrected and the legacy references will be removed.
656
class_alias(action_menu::class, \action_menu::class);