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/**
18 * This file contains classes used to manage the navigation structures in Moodle
19 * and was introduced as part of the changes occuring in Moodle 2.0
20 *
21 * @since     Moodle 2.0
22 * @package   block_navigation
23 * @copyright 2009 Sam Hemelryk
24 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 */
26
27/**
28 * The global navigation tree block class
29 *
30 * Used to produce the global navigation block new to Moodle 2.0
31 *
32 * @package   block_navigation
33 * @category  navigation
34 * @copyright 2009 Sam Hemelryk
35 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36 */
37class block_navigation extends block_base {
38
39    /** @var int This allows for multiple navigation trees */
40    public static $navcount;
41    /** @var string The name of the block */
42    public $blockname = null;
43    /** @var bool A switch to indicate whether content has been generated or not. */
44    protected $contentgenerated = false;
45    /** @var bool|null variable for checking if the block is docked*/
46    protected $docked = null;
47
48    /** @var int Trim characters from the right */
49    const TRIM_RIGHT = 1;
50    /** @var int Trim characters from the left */
51    const TRIM_LEFT = 2;
52    /** @var int Trim characters from the center */
53    const TRIM_CENTER = 3;
54
55    /**
56     * Set the initial properties for the block
57     */
58    function init() {
59        $this->blockname = get_class($this);
60        $this->title = get_string('pluginname', $this->blockname);
61    }
62
63    /**
64     * All multiple instances of this block
65     * @return bool Returns false
66     */
67    function instance_allow_multiple() {
68        return false;
69    }
70
71    /**
72     * Set the applicable formats for this block to all
73     * @return array
74     */
75    function applicable_formats() {
76        return array('all' => true);
77    }
78
79    /**
80     * Allow the user to configure a block instance
81     * @return bool Returns true
82     */
83    function instance_allow_config() {
84        return true;
85    }
86
87    /**
88     * The navigation block cannot be hidden by default as it is integral to
89     * the navigation of Moodle.
90     *
91     * @return false
92     */
93    function  instance_can_be_hidden() {
94        return false;
95    }
96
97    /**
98     * Find out if an instance can be docked.
99     *
100     * @return bool true or false depending on whether the instance can be docked or not.
101     */
102    function instance_can_be_docked() {
103        return (parent::instance_can_be_docked() && (empty($this->config->enabledock) || $this->config->enabledock=='yes'));
104    }
105
106    /**
107     * Gets Javascript that may be required for navigation
108     */
109    function get_required_javascript() {
110        parent::get_required_javascript();
111        $arguments = array(
112            'instanceid' => $this->instance->id
113        );
114        $this->page->requires->string_for_js('viewallcourses', 'moodle');
115        $this->page->requires->js_call_amd('block_navigation/navblock', 'init', $arguments);
116    }
117
118    /**
119     * Gets the content for this block by grabbing it from $this->page
120     *
121     * @return object $this->content
122     */
123    function get_content() {
124        global $CFG;
125        // First check if we have already generated, don't waste cycles
126        if ($this->contentgenerated === true) {
127            return $this->content;
128        }
129        // JS for navigation moved to the standard theme, the code will probably have to depend on the actual page structure
130        // $this->page->requires->js('/lib/javascript-navigation.js');
131        // Navcount is used to allow us to have multiple trees although I dont' know why
132        // you would want two trees the same
133
134        block_navigation::$navcount++;
135
136        // Check if this block has been docked
137        if ($this->docked === null) {
138            $this->docked = get_user_preferences('nav_in_tab_panel_globalnav'.block_navigation::$navcount, 0);
139        }
140
141        // Check if there is a param to change the docked state
142        if ($this->docked && optional_param('undock', null, PARAM_INT)==$this->instance->id) {
143            unset_user_preference('nav_in_tab_panel_globalnav'.block_navigation::$navcount);
144            $url = $this->page->url;
145            $url->remove_params(array('undock'));
146            redirect($url);
147        } else if (!$this->docked && optional_param('dock', null, PARAM_INT)==$this->instance->id) {
148            set_user_preferences(array('nav_in_tab_panel_globalnav'.block_navigation::$navcount=>1));
149            $url = $this->page->url;
150            $url->remove_params(array('dock'));
151            redirect($url);
152        }
153
154        $trimmode = self::TRIM_RIGHT;
155        $trimlength = 50;
156
157        if (!empty($this->config->trimmode)) {
158            $trimmode = (int)$this->config->trimmode;
159        }
160
161        if (!empty($this->config->trimlength)) {
162            $trimlength = (int)$this->config->trimlength;
163        }
164
165        // Get the navigation object or don't display the block if none provided.
166        if (!$navigation = $this->get_navigation()) {
167            return null;
168        }
169        $expansionlimit = null;
170        if (!empty($this->config->expansionlimit)) {
171            $expansionlimit = $this->config->expansionlimit;
172            $navigation->set_expansion_limit($this->config->expansionlimit);
173        }
174        $this->trim($navigation, $trimmode, $trimlength, ceil($trimlength/2));
175
176        // Get the expandable items so we can pass them to JS
177        $expandable = array();
178        $navigation->find_expandable($expandable);
179        if ($expansionlimit) {
180            foreach ($expandable as $key=>$node) {
181                if ($node['type'] > $expansionlimit && !($expansionlimit == navigation_node::TYPE_COURSE && $node['type'] == $expansionlimit && $node['branchid'] == SITEID)) {
182                    unset($expandable[$key]);
183                }
184            }
185        }
186
187        $limit = 20;
188        if (!empty($CFG->navcourselimit)) {
189            $limit = $CFG->navcourselimit;
190        }
191        $expansionlimit = 0;
192        if (!empty($this->config->expansionlimit)) {
193            $expansionlimit = $this->config->expansionlimit;
194        }
195
196        $options = array();
197        $options['linkcategories'] = (!empty($this->config->linkcategories) && $this->config->linkcategories == 'yes');
198
199        // Grab the items to display
200        $renderer = $this->page->get_renderer($this->blockname);
201        $this->content = new stdClass();
202        $this->content->text = $renderer->navigation_tree($navigation, $expansionlimit, $options);
203
204        // Set content generated to true so that we know it has been done
205        $this->contentgenerated = true;
206
207        return $this->content;
208    }
209
210    /**
211     * Returns the navigation
212     *
213     * @return navigation_node The navigation object to display
214     */
215    protected function get_navigation() {
216        // Initialise (only actually happens if it hasn't already been done yet)
217        $this->page->navigation->initialise();
218        return clone($this->page->navigation);
219    }
220
221    /**
222     * Returns the attributes to set for this block
223     *
224     * This function returns an array of HTML attributes for this block including
225     * the defaults.
226     * {@link block_tree::html_attributes()} is used to get the default arguments
227     * and then we check whether the user has enabled hover expansion and add the
228     * appropriate hover class if it has.
229     *
230     * @return array An array of HTML attributes
231     */
232    public function html_attributes() {
233        $attributes = parent::html_attributes();
234        if (!empty($this->config->enablehoverexpansion) && $this->config->enablehoverexpansion == 'yes') {
235            $attributes['class'] .= ' block_js_expansion';
236        }
237        return $attributes;
238    }
239
240    /**
241     * Trims the text and shorttext properties of this node and optionally
242     * all of its children.
243     *
244     * @param navigation_node $node
245     * @param int $mode One of navigation_node::TRIM_*
246     * @param int $long The length to trim text to
247     * @param int $short The length to trim shorttext to
248     * @param bool $recurse Recurse all children
249     */
250    public function trim(navigation_node $node, $mode=1, $long=50, $short=25, $recurse=true) {
251        switch ($mode) {
252            case self::TRIM_RIGHT :
253                if (core_text::strlen($node->text)>($long+3)) {
254                    // Truncate the text to $long characters
255                    $node->text = $this->trim_right($node->text, $long);
256                }
257                if (is_string($node->shorttext) && core_text::strlen($node->shorttext)>($short+3)) {
258                    // Truncate the shorttext
259                    $node->shorttext = $this->trim_right($node->shorttext, $short);
260                }
261                break;
262            case self::TRIM_LEFT :
263                if (core_text::strlen($node->text)>($long+3)) {
264                    // Truncate the text to $long characters
265                    $node->text = $this->trim_left($node->text, $long);
266                }
267                if (is_string($node->shorttext) && core_text::strlen($node->shorttext)>($short+3)) {
268                    // Truncate the shorttext
269                    $node->shorttext = $this->trim_left($node->shorttext, $short);
270                }
271                break;
272            case self::TRIM_CENTER :
273                if (core_text::strlen($node->text)>($long+3)) {
274                    // Truncate the text to $long characters
275                    $node->text = $this->trim_center($node->text, $long);
276                }
277                if (is_string($node->shorttext) && core_text::strlen($node->shorttext)>($short+3)) {
278                    // Truncate the shorttext
279                    $node->shorttext = $this->trim_center($node->shorttext, $short);
280                }
281                break;
282        }
283        if ($recurse && $node->children->count()) {
284            foreach ($node->children as &$child) {
285                $this->trim($child, $mode, $long, $short, true);
286            }
287        }
288    }
289    /**
290     * Truncate a string from the left
291     * @param string $string The string to truncate
292     * @param int $length The length to truncate to
293     * @return string The truncated string
294     */
295    protected function trim_left($string, $length) {
296        return '...'.core_text::substr($string, core_text::strlen($string)-$length, $length);
297    }
298    /**
299     * Truncate a string from the right
300     * @param string $string The string to truncate
301     * @param int $length The length to truncate to
302     * @return string The truncated string
303     */
304    protected function trim_right($string, $length) {
305        return core_text::substr($string, 0, $length).'...';
306    }
307    /**
308     * Truncate a string in the center
309     * @param string $string The string to truncate
310     * @param int $length The length to truncate to
311     * @return string The truncated string
312     */
313    protected function trim_center($string, $length) {
314        $trimlength = ceil($length/2);
315        $start = core_text::substr($string, 0, $trimlength);
316        $end = core_text::substr($string, core_text::strlen($string)-$trimlength);
317        $string = $start.'...'.$end;
318        return $string;
319    }
320
321    /**
322     * Returns the role that best describes the navigation block... 'navigation'
323     *
324     * @return string 'navigation'
325     */
326    public function get_aria_role() {
327        return 'navigation';
328    }
329
330    /**
331     * Return the plugin config settings for external functions.
332     *
333     * @return stdClass the configs for both the block instance and plugin
334     * @since Moodle 3.8
335     */
336    public function get_config_for_external() {
337        // Return all settings for all users since it is safe (no private keys, etc..).
338        $configs = !empty($this->config) ? $this->config : new stdClass();
339
340        return (object) [
341            'instance' => $configs,
342            'plugin' => new stdClass(),
343        ];
344    }
345}
346