name; } public function getContents() { return $this->contents; } public function getSettings() { return $this->settings; } public function getTitle() { return isset($this->settings['title']) ? $this->translateString($this->settings['title'], 'title') : $this->name; } /** * Instantiate, populate and return a QuickSet object wrapped in a renderer. * * @param $name * The unique name (machine name) of the QuickSet instance. * * @param $contents * The array of content items, each one itself an array with at least a 'type' * key, a 'title' key, and the other info necessary for that type. * * @param $renderer * The plugin key for this renderer plugin * * @param $settings * An array of settings determining the behaviour of this QuickSet instance. * */ public static function QuickSetRendererFactory($name, $contents, $renderer, $settings) { ctools_include('plugins'); if ($class = ctools_plugin_load_class('quicktabs', 'renderers', $renderer, 'handler')) { try { $qs = new self($name, $contents, $settings); } catch (InvalidQuickSetException $e) { watchdog('Quicktabs', $e->getMessage()); return NULL; } return new $class($qs); } } /** * Returns a reference to an object that implements the QuickContentRenderable * interface. */ public static function getContentRenderer($tab) { if ($tab['type'] == 'prerendered') { return new QuickPreRenderedContent($tab); } if ($content = QuickContent::factory($tab['type'], $tab)) { return $content; } return NULL; } /** * Static method to retrieve content from an ajax call. This is called by the * quicktabs_ajax() callback in quicktabs.module. */ public static function ajaxRenderContent($type, $args) { if ($renderer = self::getContentRenderer(array('type' => $type))) { $output = $renderer->render(FALSE, $args); return !empty($output) ? drupal_render($output) : ''; } return ''; } /** * Ensure sensible default settings for each QuickSet object. */ private static function getDefaultSettings() { return array( 'title' => '', 'style' => 'nostyle', 'hide_empty_tabs' => 0, 'ajax' => 0, 'default_tab' => 0, 'options' => array(), ); } /** * Constructor */ public function __construct($name, $contents, $settings) { $this->name = $name; $this->contents = array(); foreach ($contents as $key => $item) { // Instantiate a content renderer object and add it to the contents array. if ($renderer = self::getContentRenderer($item)) { $this->contents[$key] = $renderer; } } $default_settings = self::getDefaultSettings(); $this->settings = array_merge($default_settings, $settings); $this->prepareContents(); // Set the default style if necessary. if ($this->settings['style'] == 'default') { $this->settings['style'] = variable_get('quicktabs_tabstyle', 'nostyle'); } } /** * Returns an ajax path to be used on ajax-enabled tab links. * * @param $index The index of the tab, i.e where it fits into the QuickSet * instance. * * @param $type The type of content we are providing an ajax path for. */ public function getAjaxPath($index, $type) { return 'quicktabs/ajax/'. $this->name .'/'. $index . '/'. $type; } /** * Translates Quicktabs user-defined strings if the i18n module is * enabled. */ public function translateString($string, $type = 'tab', $index = 0) { switch ($type) { case 'tab': $name = "tab:{$this->name}-{$index}:title"; break; case 'title': $name = "title:{$this->name}"; break; } return quicktabs_translate($name, $string); } /** * This method does some initial set-up of the tab contents, such as hiding * tabs with no content if the hide_empty_tabs option is set. It also makes sure * that prerendered contents are never attempted to be loaded via ajax. * * @throws InvalidQuickSetException if there are no contents to render. */ protected function prepareContents() { if (!count($this->contents)) { throw new InvalidQuickSetException('There are no contents to render.'); } if ($this->settings['hide_empty_tabs'] && !$this->settings['ajax']) { // Check if any tabs need to be hidden because of empty content. $renderable_contents = 0; foreach ($this->contents as $key => $tab) { $contents = $tab->render(TRUE); if (empty($contents)) { // Rather than removing the item, we set it to NULL. This way we retain // the same indices across tabs, so that permanent links to particular // tabs can be relied upon. $this->contents[$key] = NULL; // The default tab must not be a hidden tab. if ($this->settings['default_tab'] == $key) { $this->settings['default_tab'] = ($key + 1) % count($this->contents); } } else { $renderable_contents++; } } if (!$renderable_contents) { throw new InvalidQuickSetException('There are no contents to render.'); } } elseif ($this->settings['ajax']) { // Make sure that there is at most 1 prerendered tab and it is the default tab. // Prerendered content cannot be rendered via ajax. $has_prerendered = FALSE; // keep track of whether we have found a prerendered tab. foreach ($this->contents as $key => $tab) { $type = $tab->getType(); if ($type == 'prerendered') { if (!$has_prerendered) { $has_prerendered = TRUE; $this->settings['default_tab'] = $key; // In the case of a direct link to a different tab, the 'default_tab' // will be overridden, so we need to make sure it does not attempt // to load a pre-rendered tab via ajax. Turn ajax option off. if ($this->getActiveTab() !== $key) { $this->settings['ajax'] = 0; } } else { // We are on a second custom tab and the ajax option is set, we cannot // render custom tabs via ajax, so we skip out of the loop, set the // ajax option to off, and call the method again. $this->settings['ajax'] = 0; $this->prepareContents(); return; } } } } } /** * Returns the active tab for a given Quicktabs instance. This could be coming * from the URL or just from the settings for this instance. If neither, it * defaults to 0. */ public function getActiveTab() { $active_tab = isset($this->settings['default_tab']) ? $this->settings['default_tab'] : key($this->contents); $active_tab = isset($_GET['qt-' . $this->name]) ? $_GET['qt-' . $this->name] : $active_tab; $active_tab = (isset($active_tab) && isset($this->contents[$active_tab])) ? $active_tab : 0; return $active_tab; } } /** * Abstract base class for QuickSet Renderers. * * A renderer object contains a reference to a QuickSet object, which it can * then render. */ abstract class QuickRenderer { /** * @var QuickSet */ protected $quickset; /** * Constructor */ public function __construct($quickset) { $this->quickset = $quickset; } /** * Accessor method for the title. */ public function getTitle() { return $this->quickset->getTitle(); } /** * The only method that renderer plugins must implement. * * @return A render array to be passed to drupal_render(). */ abstract public function render(); /** * Method for returning the form elements to display for this renderer type on * the admin form. * @param $qt An object representing the Quicktabs instance that the tabs are * being built for. */ public static function optionsForm($qt) { return array(); } } /******************************************************* * The classes below relate to individual tab content * *******************************************************/ /** * Each QuickSet object has a "contents" property which is an array of objects * that implement the QuickContentRenderable interface. */ interface QuickContentRenderable { /** * Returns the short type name of the content plugin, e.g. 'block', 'node', * 'prerendered'. */ public static function getType(); /** * Returns the tab title. */ public function getTitle(); /** * Returns an array of settings specific to the type of content. */ public function getSettings(); /** * Renders the content. * * @param $hide_emtpy If set to true, then the renderer should return an empty * array if there is no content to display, for example if the user does not * have access to the requested content. * * @param $args Used during an ajax call to pass in the settings necessary to * render this type of content. */ public function render($hide_empty = FALSE, $args = array()); /** * Returns an array of keys to use for constructing the correct arguments for * an ajax callback to retrieve content of this type. The order of the keys * returned affects the order of the args passed in to the render method when * called via ajax (see the render() method above). */ public function getAjaxKeys(); } /** * Abstract base class for content plugins. */ abstract class QuickContent implements QuickContentRenderable { /** * Used as the title of the tab. * @var string */ protected $title; /** * An array containing the information that defines the tab content, specific * to its type. * @var array */ protected $settings; /** * A render array of the contents. * @var array */ protected $rendered_content; /** * Constructor */ public function __construct($item) { $this->title = isset($item['title']) ? $item['title'] : ''; // We do not need to store title, type or weight in the settings array, which // is for type-specific settings. unset($item['title'], $item['type'], $item['weight']); $this->settings = $item; } /** * Accessor for the tab title. */ public function getTitle() { return $this->title; } /** * Accessor for the tab settings. */ public function getSettings() { return $this->settings; } /** * Instantiate a content type object. * * @param $name * The type name of the plugin. * * @param $item * An array containing the item definition * */ public static function factory($name, $item) { ctools_include('plugins'); if ($class = ctools_plugin_load_class('quicktabs', 'contents', $name, 'handler')) { // We now need to check the plugin's dependencies, to make sure they're installed. // This info has already been statically cached at this point so there's no // harm in making a call to ctools_get_plugins(). $plugin = ctools_get_plugins('quicktabs', 'contents', $name); if (isset($plugin['dependencies'])) { foreach ($plugin['dependencies'] as $dep) { // If any dependency is missing we cannot instantiate our class. if (!module_exists($dep)) return NULL; } } return new $class($item); } return NULL; } /** * Method for returning the form elements to display for this tab type on * the admin form. * * @param $delta Integer representing this tab's position in the tabs array. * * @param $qt An object representing the Quicktabs instance that the tabs are * being built for. */ abstract public function optionsForm($delta, $qt); } /** * This class implements the same interface that content plugins do but it is not * a content plugin. It is a special class for pre-rendered content which is used * when "custom" tabs are added to existing Quicktabs instances in a call to * quicktabs_build_quicktabs(). */ class QuickPreRenderedContent implements QuickContentRenderable { public static function getType() { return 'prerendered'; } /** * Used as the title of the tab. * @var title */ protected $title; /** * A render array of the contents. * @var array */ protected $rendered_content; /** * Constructor */ public function __construct($item) { $contents = isset($item['contents']) ? $item['contents'] : array(); if (!is_array($contents)) { $contents = array('#markup' => $contents); } $this->rendered_content = $contents; $this->title = isset($item['title']) ? $item['title'] : ''; } /** * Accessor for the tab title. */ public function getTitle() { return $this->title; } /** * Prerendered content doesn't need any extra settings. */ public function getSettings() { return array(); } /** * The render method simply returns the contents that were passed in and * stored during construction. */ public function render($hide_empty = FALSE, $args = array()) { return $this->rendered_content; } /** * This content cannot be rendered via ajax so we don't return any ajax keys. */ public function getAjaxKeys() { return array(); } } /** * Create our own exception class. */ class InvalidQuickSetException extends Exception { }