diff options
Diffstat (limited to 'library/WT/Theme/BaseTheme.php')
| -rw-r--r-- | library/WT/Theme/BaseTheme.php | 1692 |
1 files changed, 1692 insertions, 0 deletions
diff --git a/library/WT/Theme/BaseTheme.php b/library/WT/Theme/BaseTheme.php new file mode 100644 index 0000000000..404f15e3a8 --- /dev/null +++ b/library/WT/Theme/BaseTheme.php @@ -0,0 +1,1692 @@ +<?php +// webtrees: Web based Family History software +// Copyright (C) 2015 webtrees development team. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +namespace WT\Theme; + +use gedcom_favorites_WT_Module; +use user_favorites_WT_Module; +use WT\Auth; +use WT\Theme; +use WT\User; +use WT_Controller_Page; +use WT_DB; +use WT_Fact; +use WT_FlashMessages; +use WT_Gedcom_Tag; +use WT_GedcomRecord; +use WT_I18N; +use WT_Filter; +use WT_Individual; +use WT_Menu; +use WT_Module; +use WT_Site; +use WT_Tree; +use Zend_Session_Namespace; + +/** + * Class Base - Common functions and interfaces for all themes. + */ +abstract class BaseTheme { + /** @var Zend_Session_Namespace */ + protected $session; + + /** @var WT_Tree The current tree */ + protected $tree; + + /** @var string An escaped version of the "ged=XXX" URL parameter */ + protected $tree_url; + + /** @var boolean Are we showing a page to a search engine? */ + protected $search_engine; + + /** + * Custom themes should place their initialization code in the function hookAfterInit(), not in + * the constructor, as all themes get constructed - whether they are used or not. + */ + final public function __construct() { + } + + /** + * Create the top of the <body>. + * + * @return string + */ + public function bodyHeader() { + return + '<body class="container">' . + '<header>' . + $this->headerContent() . + $this->primaryMenuContainer($this->primaryMenu()) . + '</header>' . + '<main id="content" role="main">' . + $this->flashMessagesContainer(WT_FlashMessages::getMessages()); + } + + /** + * Create the top of the <body> (for popup windows). + * + * @return string + */ + public function bodyHeaderPopupWindow() { + return + '<body>' . + '<main id="content" role="main">' . + $this->flashMessagesContainer(WT_FlashMessages::getMessages()); + } + + /** + * Create a contact link for a user. + * + * @param User $user + * + * @return string + */ + public function contactLink(User $user) { + $method = $user->getPreference('contactmethod'); + + switch ($method) { + case 'none': + return ''; + case 'mailto': + return '<a href="mailto:' . WT_Filter::escapeHtml($user->getEmail()) . '">' . WT_Filter::escapeHtml($user->getRealName()) . '</a>'; + default: + return "<a href='#' onclick='message(\"" . WT_Filter::escapeJs($user->getUserName()) . "\", \"" . $method . "\", \"" . WT_SERVER_NAME . WT_SCRIPT_PATH . WT_Filter::escapeJs(get_query_url()) . "\", \"\");return false;'>" . WT_Filter::escapeHtml($user->getRealName()) . '</a>'; + } + } + + /** + * Create contact link for both technical and genealogy support. + * + * @param User $user + * + * @return string + */ + protected function contactLinkEverything(User $user) { + return WT_I18N::translate('For technical support or genealogy questions, please contact') . ' ' . $this->contactLink($user); + } + + /** + * Create contact link for genealogy support. + * + * @param User $user + * + * @return string + */ + protected function contactLinkGenealogy(User $user) { + return WT_I18N::translate('For help with genealogy questions contact') . ' ' . $this->contactLink($user); + } + + /** + * Create contact link for technical support. + * + * @param User $user + * + * @return string + */ + protected function contactLinkTechnical(User $user) { + return WT_I18N::translate('For technical support and information contact') . ' ' . $this->contactLink($user); + } + + /** + * Create contact links for the page footer. + * + * @return string + */ + protected function contactLinks() { + $contact_user = User::find($this->tree->getPreference('CONTACT_USER_ID')); + $webmaster_user = User::find($this->tree->getPreference('WEBMASTER_USER_ID')); + + if ($contact_user && $contact_user === $webmaster_user) { + return $this->contactLinkEverything($contact_user); + } elseif ($contact_user && $webmaster_user) { + return $this->contactLinkGenealogy($contact_user) . '<br>' . $this->contactLinkTechnical($webmaster_user); + } elseif ($contact_user) { + return $this->contactLinkGenealogy($contact_user); + } elseif ($webmaster_user) { + return $this->contactLinkTechnical($webmaster_user); + } else { + return ''; + } + } + + /** + * Where are our CSS assets? + * + * @return string A relative path, such as "themes/foo/" + */ + public function cssUrl() { + return ''; + } + + /** + * Create the <DOCTYPE> tag. + * + * @return string + */ + public function doctype() { + return '<!DOCTYPE html>'; + } + + /** + * HTML link to a "favorites icon". + * + * @return string + */ + protected function favicon() { + return ''; + } + + /** + * Add markup to a flash message. + * + * @param \stdClass $message + * + * @return string + */ + protected function flashMessageContainer(\stdClass $message) { + return + '<div class="alert alert-' . $message->status . ' alert-dismissible" role="alert">' . + '<button type="button" class="close" data-dismiss="alert" aria-label="' . /* I18N: button label */ WT_I18N::translate('close') . '">' . + '<span aria-hidden="true">×</span>' . + '</button>' . + $message->text . + '</div>'; + } + + /** + * Create a container for messages that are "flashed" to the session + * on one request, and displayed on another. If there are many messages, + * the container may need a max-height and scroll-bar. + * + * @param \stdClass[] $messages + * + * @return string + */ + protected function flashMessagesContainer(array $messages) { + $html = ''; + foreach ($messages as $message) { + $html .= $this->flashMessageContainer($message); + } + + if ($html) { + return '<div class="flash-messages">' . $html . '</div>'; + } else { + return ''; + } + } + + /** + * Create the <footer> tag. + * + * @return string + */ + public function footerContainer() { + return + '</main>' . + '<footer>' . $this->footerContent() . '</footer>'; + } + + /** + * Create the contents of the <footer> tag. + * + * @return string + */ + protected function footerContent() { + return + $this->formatContactLinks() . + $this->logoPoweredBy(); + } + + /** + * Format the contents of a variable-height home-page block. + * + * @param string $id + * @param string $title + * @param string $class + * @param string $content + * + * @return string + */ + public function formatBlock($id, $title, $class, $content) { + return + '<div id="' . $id . '" class="block" >' . + '<div class="blockheader">' . $title . '</div>' . + '<div class="blockcontent ' . $class . '">' . $content . '</div>' . + '</div>'; + } + + /** + * Add markup to the contact links. + * + * @return string + */ + protected function formatContactLinks() { + if ($this->tree) { + return '<div class="contact_links">' . $this->contactLinks() . '</div>'; + } else { + return ''; + } + } + + /** + * Create a pending changes link for the page footer. + * + * @return string + */ + protected function formatPendingChangesLink() { + if ($this->pendingChangesExist()) { + return '<div class="pending-changes-link">' . $this->pendingChangesLink() . '</div>'; + } else { + return ''; + } + } + + /** + * Add markup to the tree title. + * + * @return string + */ + protected function formatTreeTitle() { + if ($this->tree) { + return '<h1 class="header-title">' . $this->tree->tree_title_html . '</h1>'; + } else { + return ''; + } + } + + /** + * Add markup to the user menu. + * + * @return string + */ + protected function formatSecondaryMenu() { + return + '<ul class="secondary-menu">' . + implode('', $this->secondaryMenu()) . + '</ul>'; + } + + /** + * Add markup to an item in the user menu. + * + * @param WT_Menu $menu + * + * @return string + */ + protected function formatUserMenuItem(WT_Menu $menu) { + return $menu->getMenuAsList(); + } + + /** + * Create a quick search form for the header. + * + * @return string + */ + protected function formQuickSearch() { + if ($this->tree) { + return + '<form action="search.php" class="header-search" method="post" role="search">' . + '<input type="hidden" name="action" value="general">' . + '<input type="hidden" name="ged" value="' . $this->tree->tree_name_html . '">' . + '<input type="hidden" name="topsearch" value="yes">' . + $this->formQuickSearchFields() . + '</form>'; + } else { + return ''; + } + } + + /** + * Create a search field and submit button for the quick search form in the header. + * + * @return string + */ + protected function formQuickSearchFields() { + return + '<input type="search" name="query" size="15" placeholder="' . WT_I18N::translate('Search') . '">' . + '<input type="image" src="' . Theme::theme()->parameter('image-search') . '" alt="' . WT_I18N::translate('Search') . '">'; + } + + /** + * Create the <head> tag. + * + * @param WT_Controller_Page $controller The current controller + * + * @return string + */ + public function head(WT_Controller_Page $controller) { + return + '<head>' . + $this->headContents($controller) . + $this->hookHeaderExtraContent() . + '</head>'; + } + + /** + * Create the contents of the <head> tag. + * + * @param WT_Controller_Page $controller The current controller + * + * @return string + */ + protected function headContents(WT_Controller_Page $controller) { + // The title often includes the names of records, which may include HTML markup. + $title = WT_Filter::unescapeHtml($controller->getPageTitle()); + + // If an extra (site) title is specified, append it. + if ($this->tree && $this->tree->getPreference('META_TITLE')) { + $title .= ' - ' . WT_Filter::escapeHtml($this->tree->getPreference('META_TITLE')); + } + + $html = + // Modernizr needs to be loaded before the <body> to avoid FOUC in IE8 + '<!--[if IE 8]><script src="https://cdnjs.cloudflare.com/ajax/libs/modernizr/2.8.3/modernizr.min.js"></script><![endif]-->' . + $this->metaCharset() . + $this->metaRobots($controller->getMetaRobots()) . + $this->title($title) . + $this->metaGenerator(WT_WEBTREES . ' ' . WT_VERSION . ' - ' . WT_WEBTREES_URL) . + $this->metaCanonicalUrl($controller->getCanonicalUrl()); + + if ($this->tree) { + $html .= $this->metaDescription($this->tree->getPreference('META_DESCRIPTION', html_entity_decode(strip_tags($this->tree->tree_title_html), ENT_QUOTES))); + } + + // CSS files + foreach ($this->stylesheets() as $css) { + $html .= '<link rel="stylesheet" type="text/css" href="' . $css . '">'; + } + + return $html; + } + + /** + * Create the contents of the <header> tag. + * + * @return string + */ + protected function headerContent() { + return + $this->logoHeader() . + $this->secondaryMenuContainer($this->secondaryMenu()) . + $this->formatTreeTitle() . + $this->formQuickSearch(); + } + + /** + * Create the <header> tag for a popup window. + * + * @return string + */ + protected function headerSimple() { + return + WT_FlashMessages::getHtmlMessages() . // Feedback from asynchronous actions + '<div id="content">'; + } + + /** + * Allow themes to do things after initialization (since they cannot use + * the constructor). + * + * @return void + */ + public function hookAfterInit() { + } + + /** + * Allow themes to add extra scripts to the page footer. + * + * @return string + */ + public function hookFooterExtraJavascript() { + return ''; + } + + /** + * Allow themes to add extra content to the page header. + * Typically this will be additional CSS. + * + * @return string + */ + public function hookHeaderExtraContent() { + return ''; + } + + /** + * Create the <html> tag. + * + * @return string + */ + public function html() { + return '<html ' . WT_I18N::html_markup() . '>'; + } + + /** + * Display an icon for this fact. + * + * @param WT_Fact $fact + * + * @return string + */ + public function icon(WT_Fact $fact) { + $icon = 'images/facts/' . $fact->getTag() . '.png'; + $dir = substr($this->cssUrl(), strlen(WT_STATIC_URL)); + if (file_exists($dir . $icon)) { + return '<img src="' . $this->cssUrl() . $icon . '" title="' . WT_Gedcom_Tag::getLabel($fact->getTag()) . '">'; + } elseif (file_exists($dir . 'images/facts/NULL.png')) { + // Spacer image - for alignment - until we move to a sprite. + return '<img src="' . Theme::theme()->cssUrl() . 'images/facts/NULL.png">'; + } else { + return ''; + } + } + + /** + * Display an individual in a box - for charts, etc. + * + * @param WT_Individual $individual + * + * @return string + */ + public function individualBox(WT_Individual $individual) { + $personBoxClass = array_search($individual->getSex(), array('person_box' => 'M', 'person_boxF' => 'F', 'person_boxNN' => 'U')); + if ($this->tree->getPreference('SHOW_HIGHLIGHT_IMAGES')) { + $thumbnail = $individual->displayImage(); + } else { + $thumbnail = ''; + } + + return + '<div data-pid="' . $individual->getXref() . '" class="person_box_template ' . $personBoxClass . ' box-style1" style="width: ' . $this->parameter('chart-box-x') . 'px; min-height: ' . $this->parameter('chart-box-y') . 'px">' . + '<div class="noprint icons">' . + '<span class="iconz icon-zoomin" title="' . WT_I18N::translate('Zoom in/out on this box.') . '"></span>' . + '<div class="itr"><i class="icon-pedigree"></i><div class="popup">' . + '<ul class="' . $personBoxClass . '">' . implode('', $this->individualBoxMenu($individual)) . '</ul>' . + '</div>' . + '</div>' . + '</div>' . + '<div class="chart_textbox" style="max-height:' . $this->parameter('chart-box-y') . 'px;">' . + $thumbnail . + '<a href="' . $individual->getHtmlUrl() . '">'. + '<span class="namedef name1">' . $individual->getFullName() . '</span>' . + '</a>' . + '<div class="namedef name1">' . $individual->getAddName() . '</div>' . + '<div class="inout2 details1">' . $this->individualBoxFacts($individual) .'</div>' . + '</div>' . + '<div class="inout"></div>' . + '</div>'; + } + + /** + * Display an empty box - for a missing individual in a chart. + * + * @return string + */ + public function individualBoxEmpty() { + return '<div class="person_box_template person_boxNN box-style1" style="width: ' . $this->parameter('chart-box-x') . 'px; min-height: ' . $this->parameter('chart-box-y') . 'px"></div>'; + } + + /** + * Display an individual in a box - for charts, etc. + * + * @param WT_Individual $individual + * + * @return string + */ + public function individualBoxLarge(WT_Individual $individual) { + $personBoxClass = array_search($individual->getSex(), array('person_box' => 'M', 'person_boxF' => 'F', 'person_boxNN' => 'U')); + if ($this->tree->getPreference('SHOW_HIGHLIGHT_IMAGES')) { + $thumbnail = $individual->displayImage(); + } else { + $thumbnail = ''; + } + + return + '<div data-pid="' . $individual->getXref() . '" class="person_box_template ' . $personBoxClass . ' box-style2">' . + '<div class="noprint icons">' . + '<span class="iconz icon-zoomin" title="' . WT_I18N::translate('Zoom in/out on this box.') . '"></span>' . + '<div class="itr"><i class="icon-pedigree"></i><div class="popup">' . + '<ul class="' . $personBoxClass . '">' . implode('', $this->individualBoxMenu($individual)) . '</ul>' . + '</div>' . + '</div>' . + '</div>' . + '<div class="chart_textbox" style="max-height:' . $this->parameter('chart-box-y') . 'px;">' . + $thumbnail . + '<a href="' . $individual->getHtmlUrl() . '">'. + '<span class="namedef name2">' . $individual->getFullName() . '</span>' . + '</a>' . + '<div class="namedef name2">' . $individual->getAddName() . '</div>' . + '<div class="inout2 details2">' . $this->individualBoxFacts($individual) .'</div>' . + '</div>' . + '<div class="inout"></div>' . + '</div>'; + } + + /** + * Display an individual in a box - for charts, etc. + * + * @param WT_Individual $individual + * + * @return string + */ + public function individualBoxSmall(WT_Individual $individual) { + $personBoxClass = array_search($individual->getSex(), array('person_box' => 'M', 'person_boxF' => 'F', 'person_boxNN' => 'U')); + if ($this->tree->getPreference('SHOW_HIGHLIGHT_IMAGES')) { + $thumbnail = $individual->displayImage(); + } else { + $thumbnail = ''; + } + + + return + '<div data-pid="' . $individual->getXref() . '" class="person_box_template ' . $personBoxClass . ' box-style0" style="width: ' . $this->parameter('compact-chart-box-x') . 'px; min-height: ' . $this->parameter('compact-chart-box-y') . 'px">' . + '<div class="compact_view">' . + $thumbnail . + '<a href="' . $individual->getHtmlUrl() . '">'. + '<span class="namedef name0">' . $individual->getFullName() . '</span>' . + '</a>' . + '<div class="inout2 details0">' . $individual->getLifeSpan() .'</div>' . + '</div>' . + '<div class="inout"></div>' . + '</div>'; + } + + /** + * Display an individual in a box - for charts, etc. + * + * @return string + */ + public function individualBoxSmallEmpty() { + return '<div class="person_box_template person_boxNN box-style1" style="width: ' . $this->parameter('compact-chart-box-x') . 'px; min-height: ' . $this->parameter('compact-chart-box-y') . 'px"></div>'; + } + + /** + * Generate the facts, for display in charts. + * + * @param WT_Individual $individual + * + * @return string + */ + protected function individualBoxFacts(WT_Individual $individual) { + $html = ''; + + $opt_tags = preg_split('/\W/', $this->tree->getPreference('CHART_BOX_TAGS'), 0, PREG_SPLIT_NO_EMPTY); + // Show BIRT or equivalent event + foreach (explode('|', WT_EVENTS_BIRT) as $birttag) { + if (!in_array($birttag, $opt_tags)) { + $event = $individual->getFirstFact($birttag); + if ($event) { + $html .= $event->summary(); + break; + } + } + } + // Show optional events (before death) + foreach ($opt_tags as $key => $tag) { + if (!preg_match('/^(' . WT_EVENTS_DEAT . ')$/', $tag)) { + $event = $individual->getFirstFact($tag); + if (!is_null($event)) { + $html .= $event->summary(); + unset ($opt_tags[$key]); + } + } + } + // Show DEAT or equivalent event + foreach (explode('|', WT_EVENTS_DEAT) as $deattag) { + $event = $individual->getFirstFact($deattag); + if ($event) { + $html .= $event->summary(); + if (in_array($deattag, $opt_tags)) { + unset ($opt_tags[array_search($deattag, $opt_tags)]); + } + break; + } + } + // Show remaining optional events (after death) + foreach ($opt_tags as $tag) { + $event = $individual->getFirstFact($tag); + if ($event) { + $html .= $event->summary(); + } + } + + return $html; + } + + /** + * Generate the LDS summary, for display in charts. + * + * @param WT_Individual $individual + * + * @return string + */ + protected function individualBoxLdsSummary(WT_Individual $individual) { + if ($this->tree->getPreference('SHOW_LDS_AT_GLANCE')) { + $BAPL = $individual->getFacts('BAPL') ? 'B' : '_'; + $ENDL = $individual->getFacts('ENDL') ? 'E' : '_'; + $SLGC = $individual->getFacts('SLGC') ? 'C' : '_'; + $SLGS = '_'; + + foreach ($individual->getSpouseFamilies() as $family) { + if ($family->getFacts('SLGS')) { + $SLGS = ''; + } + } + + return $BAPL . $ENDL . $SLGS . $SLGC; + } else { + return ''; + } + } + + /** + * Links, to show in chart boxes; + * + * @param WT_Individual $individual + * + * @return WT_Menu[] + */ + protected function individualBoxMenu(WT_Individual $individual) { + $menus = array_merge( + $this->individualBoxMenuCharts($individual), + $this->individualBoxMenuFamilyLinks($individual) + ); + + return $menus; + } + + /** + * Chart links, to show in chart boxes; + * + * @param WT_Individual $individual + * + * @return WT_Menu[] + */ + protected function individualBoxMenuCharts(WT_Individual $individual) { + $menus = array_filter(array( + $this->menuChartAncestors($individual), + $this->menuChartCompact($individual), + $this->menuChartDescendants($individual), + $this->menuChartFanChart($individual), + $this->menuChartHourglass($individual), + $this->menuChartInteractiveTree($individual), + $this->menuChartPedigree($individual), + $this->menuChartPedigreeMap($individual), + $this->menuChartRelationship($individual), + $this->menuChartTimeline($individual), + )); + + usort($menus, function (WT_Menu $x, WT_Menu $y) { + return WT_I18N::strcasecmp($x->getLabel(), $y->getLabel()); + }); + + return $menus; + } + + /** + * Family links, to show in chart boxes. + * + * @param WT_Individual $individual + * + * @return WT_Menu[] + */ + protected function individualBoxMenuFamilyLinks(WT_Individual $individual) { + $menus = array(); + + foreach ($individual->getSpouseFamilies() as $family) { + $menus[] = new WT_Menu('<strong>' . WT_I18N::translate('Family with spouse') . '</strong>', $family->getHtmlUrl()); + $spouse = $family->getSpouse($individual); + if ($spouse && $spouse->canShowName()) { + $menus[] = new WT_Menu($spouse->getFullName(), $spouse->getHtmlUrl()); + } + foreach ($family->getChildren() as $child) { + if ($child->canShowName()) { + $menus[] = new WT_Menu($child->getFullName(), $child->getHtmlUrl()); + } + } + } + + return $menus; + } + + /** + * Create part of an individual box + * + * @param WT_Individual $individual + * + * @return string + */ + protected function individualBoxSexSymbol(WT_Individual $individual) { + if ($this->tree->getPreference('PEDIGREE_SHOW_GENDER')) { + return $individual->sexImage('large'); + } else { + return ''; + } + } + + /** + * Initialise the theme. We cannot pass these in a constructor, as the construction + * happens in a theme file, and we need to be able to change it. + * + * @param Zend_Session_Namespace $session + * @param bool $search_engine + * @param WT_Tree|null $tree The current tree (if there is one). + * + * @return void + */ + final public function init(Zend_Session_Namespace $session, $search_engine, WT_Tree $tree = null) { + $this->tree = $tree; + $this->tree_url = $tree ? 'ged=' . WT_Filter::escapeUrl($tree->tree_name) : null; + $this->session = $session; + $this->search_engine = $search_engine; + + $this->hookAfterInit(); + } + + /** + * Are we generating a page for a robot (instead of a human being). + * + * @return boolean + */ + protected function isSearchEngine() { + return $this->search_engine; + } + + /** + * A large webtrees logo, for the footer. + * + * @return string + */ + protected function logoHeader() { + return '<div class="header-logo"></div>'; + } + + /** + * A small "powered by webtrees" logo for the footer. + * + * @return string + */ + protected function logoPoweredBy() { + return '<a href="' . WT_WEBTREES_URL . '"><div class="footer-logo"></div></a>'; + } + + /** + * @return WT_Menu + */ + protected function menuCalendar() { + if ($this->isSearchEngine()) { + return new WT_Menu(WT_I18N::translate('Calendar'), '#', 'menu-calendar'); + } + + // Default action is the day view. + $menu = new WT_Menu(WT_I18N::translate('Calendar'), 'calendar.php?' . $this->tree_url, 'menu-calendar'); + + // Day view + $submenu = new WT_Menu(WT_I18N::translate('Day'), 'calendar.php?' . $this->tree_url, 'menu-calendar-day'); + $menu->addSubmenu($submenu); + + // Month view + $submenu = new WT_Menu(WT_I18N::translate('Month'), 'calendar.php?' . $this->tree_url . '&action=calendar', 'menu-calendar-month'); + $menu->addSubmenu($submenu); + + //Year view + $submenu = new WT_Menu(WT_I18N::translate('Year'), 'calendar.php?' . $this->tree_url . '&action=year', 'menu-calendar-year'); + $menu->addSubmenu($submenu); + + return $menu; + } + + /** + * Generate a menu for each of the different charts. + * + * @param WT_Individual $individual + * + * @return WT_Menu + */ + protected function menuChart(WT_Individual $individual) { + if ($this->tree && !$this->isSearchEngine()) { + // The top level menu is the pedigree chart + $menu = $this->menuChartPedigree($individual); + $menu->setLabel(WT_I18N::translate('Charts')); + $menu->setId('menu-chart'); + + $submenus = array_filter(array( + $this->menuChartAncestors($individual), + $this->menuChartCompact($individual), + $this->menuChartDescendants($individual), + $this->menuChartFamilyBook($individual), + $this->menuChartFanChart($individual), + $this->menuChartHourglass($individual), + $this->menuChartInteractiveTree($individual), + $this->menuChartLifespan($individual), + $this->menuChartPedigree($individual), + $this->menuChartPedigreeMap($individual), + $this->menuChartRelationship($individual), + $this->menuChartStatistics(), + $this->menuChartTimeline($individual), + )); + + usort($submenus, function (WT_Menu $x, WT_Menu $y) { + return WT_I18N::strcasecmp($x->getLabel(), $y->getLabel()); + }); + + $menu->setSubmenus($submenus); + + return $menu; + } else { + return new WT_Menu(WT_I18N::translate('Charts'), '#', 'menu-chart'); + } + } + + /** + * Generate a menu item for the ancestors chart (ancestry.php). + * + * @param WT_Individual $individual + * + * @return WT_Menu + */ + protected function menuChartAncestors(WT_Individual $individual) { + return new WT_Menu(WT_I18N::translate('Ancestors'), 'ancestry.php?rootid=' . $individual->getXref() . '&' . $this->tree_url, 'menu-chart-pedigree'); + } + + /** + * Generate a menu item for the compact tree (compact.php). + * + * @param WT_Individual $individual + * + * @return WT_Menu + */ + protected function menuChartCompact(WT_Individual $individual) { + return new WT_Menu(WT_I18N::translate('Compact tree'), 'compact.php?rootid=' . $individual->getXref() . '&' . $this->tree_url, 'menu-chart-compact'); + } + + /** + * Generate a menu item for the descendants chart (descendancy.php). + * + * @param WT_Individual $individual + * + * @return WT_Menu + */ + protected function menuChartDescendants(WT_Individual $individual) { + return new WT_Menu(WT_I18N::translate('Descendants'), 'descendancy.php?rootid=' . $individual->getXref() . '&' . $this->tree_url, 'menu-chart-descendants'); + } + + /** + * Generate a menu item for the family-book chart (familybook.php). + * + * @param WT_Individual $individual + * + * @return WT_Menu + */ + protected function menuChartFamilyBook(WT_Individual $individual) { + return new WT_Menu(WT_I18N::translate('Family book'), 'familybook.php?rootid=' . $individual->getXref() . '&' . $this->tree_url, 'menu-chart-familybook'); + } + + /** + * Generate a menu item for the fan chart (fanchart.php). + * + * We can only do this if the GD2 library is installed with TrueType support. + * + * @param WT_Individual $individual + * + * @return WT_Menu|null + */ + protected function menuChartFanChart(WT_Individual $individual) { + if (function_exists('imagettftext')) { + return new WT_Menu(WT_I18N::translate('Fan chart'), 'fanchart.php?rootid=' . $individual->getXref() . '&' . $this->tree_url, 'menu-chart-fanchart'); + } else { + return null; + } + } + + /** + * Generate a menu item for the pedigree map (googlemap module). + * + * @param WT_Individual $individual + * + * @return WT_Menu|null + */ + protected function menuChartInteractiveTree(WT_Individual $individual) { + if (array_key_exists('tree', WT_Module::getActiveModules())) { + return new WT_Menu(WT_I18N::translate('Interactive tree'), 'module.php?mod=tree&mod_action=treeview&' . $this->tree_url . '&rootid=' . $individual->getXref(), 'menu-chart-tree'); + } else { + return null; + } + } + + /** + * Generate a menu item for the hourglass chart (hourglass.php). + * + * @param WT_Individual $individual + * + * @return WT_Menu + */ + protected function menuChartHourglass(WT_Individual $individual) { + return new WT_Menu(WT_I18N::translate('Hourglass chart'), 'hourglass.php?rootid=' . $individual->getXref() . '&' . $this->tree_url, 'menu-chart-hourglass'); + } + + /** + * Generate a menu item for the lifepsan chart (lifespan.php). + * + * @param WT_Individual $individual + * + * @return WT_Menu + */ + protected function menuChartLifespan(WT_Individual $individual) { + return new WT_Menu(WT_I18N::translate('Lifespans'), 'lifespan.php?pids%5B%5D=' . $individual->getXref() . '&addFamily=1&' . $this->tree_url, 'menu-chart-lifespan'); + } + + /** + * Generate a menu item for the pedigree chart (pedigree.php). + * + * @param WT_Individual $individual + * + * @return WT_Menu + */ + protected function menuChartPedigree(WT_Individual $individual) { + return new WT_Menu(WT_I18N::translate('Pedigree'), 'pedigree.php?rootid=' . $individual->getXref() . '&' . $this->tree_url, 'menu-chart-pedigree'); + } + + /** + * Generate a menu item for the pedigree map (googlemap module). + * + * @param WT_Individual $individual + * + * @return WT_Menu|null + */ + protected function menuChartPedigreeMap(WT_Individual $individual) { + if (array_key_exists('googlemap', WT_Module::getActiveModules())) { + return new WT_Menu(WT_I18N::translate('Pedigree map'), 'module.php?' . $this->tree_url . '&mod=googlemap&mod_action=pedigree_map&rootid=' . $individual->getXref(), 'menu-chart-pedigree_map'); + } else { + return null; + } + } + + /** + * Generate a menu item for the relationship chart (relationship.php). + * + * @param WT_Individual $individual + * + * @return WT_Menu + */ + protected function menuChartRelationship(WT_Individual $individual) { + if (WT_USER_GEDCOM_ID && $individual->getXref()) { + return new WT_Menu(WT_I18N::translate('Relationship to me'), 'relationship.php?pid1=' . WT_USER_GEDCOM_ID . '&pid2=' . $individual->getXref() . '&ged=' . $this->tree_url, 'menu-chart-relationship'); + } else { + return new WT_Menu(WT_I18N::translate('Relationships'), 'relationship.php?pid1=' . $individual->getXref() . '&ged=' . $this->tree_url, 'menu-chart-relationship'); + } + } + + /** + * Generate a menu item for the statistics charts (statistics.php). + * + * @return WT_Menu + */ + protected function menuChartStatistics() { + return new WT_Menu(WT_I18N::translate('Statistics'), 'statistics.php?' . $this->tree_url, 'menu-chart-statistics'); + } + + /** + * Generate a menu item for the timeline chart (timeline.php). + * + * @param WT_Individual $individual + * + * @return WT_Menu + */ + protected function menuChartTimeline(WT_Individual $individual) { + return new WT_Menu(WT_I18N::translate('Timeline'), 'timeline.php?pids%5B%5D=' . $individual->getXref() . '&' . $this->tree_url, 'menu-chart-timeline'); + } + + /** + * Favorites menu. + * + * @return WT_Menu|null + */ + protected function menuFavorites() { + global $controller; + + $show_user_favs = Auth::check() && array_key_exists('user_favorites', WT_Module::getActiveModules()); + $show_gedc_favs = !$this->tree->getPreference('REQUIRE_AUTHENTICATION') && array_key_exists('gedcom_favorites', WT_Module::getActiveModules()); + + if ($show_user_favs && Auth::check()) { + if ($show_gedc_favs && !$this->isSearchEngine()) { + $favorites = array_merge( + gedcom_favorites_WT_Module::getFavorites(WT_GED_ID), + user_favorites_WT_Module::getFavorites(Auth::id()) + ); + } else { + $favorites = user_favorites_WT_Module::getFavorites(Auth::id()); + } + } else { + if ($show_gedc_favs && !$this->isSearchEngine()) { + $favorites = gedcom_favorites_WT_Module::getFavorites(WT_GED_ID); + } else { + return null; + } + } + + $menu = new WT_Menu(WT_I18N::translate('Favorites'), '#', 'menu-favorites'); + + foreach ($favorites as $favorite) { + switch ($favorite['type']) { + case 'URL': + $submenu = new WT_Menu($favorite['title'], $favorite['url']); + $menu->addSubmenu($submenu); + break; + case 'INDI': + case 'FAM': + case 'SOUR': + case 'OBJE': + case 'NOTE': + $obj = WT_GedcomRecord::getInstance($favorite['gid']); + if ($obj && $obj->canShowName()) { + $submenu = new WT_Menu($obj->getFullName(), $obj->getHtmlUrl()); + $menu->addSubmenu($submenu); + } + break; + } + } + + if ($show_user_favs) { + if (isset($controller->record) && $controller->record instanceof WT_GedcomRecord) { + $submenu = new WT_Menu(WT_I18N::translate('Add to favorites'), '#'); + $submenu->setOnclick("jQuery.post('module.php?mod=user_favorites&mod_action=menu-add-favorite',{xref:'" . $controller->record->getXref() . "'},function(){location.reload();})"); + $menu->addSubmenu($submenu); + } + } + + return $menu; + } + + /** + * @return WT_Menu + */ + protected function menuHomePage() { + $menu = new WT_Menu(WT_I18N::translate('Home page'), 'index.php?ctype=gedcom&' . $this->tree_url, 'menu-tree'); + $ALLOW_CHANGE_GEDCOM = WT_Site::getPreference('ALLOW_CHANGE_GEDCOM') && count(WT_Tree::getAll()) > 1; + foreach (WT_Tree::getAll() as $tree) { + if ($tree->tree_id === WT_GED_ID || $ALLOW_CHANGE_GEDCOM) { + $submenu = new WT_Menu( + $tree->tree_title_html, + 'index.php?ctype=gedcom&ged=' . $tree->tree_name_url, + 'menu-tree-' . $tree->tree_id // Cannot use name - it must be a CSS identifier + ); + $menu->addSubmenu($submenu); + } + } + + return $menu; + } + + /** + * A menu to show a list of available languages. + * + * @return WT_Menu|null + */ + protected function menuLanguages() { + $menu = new WT_Menu(WT_I18N::translate('Language'), '#', 'menu-language'); + + foreach (WT_I18N::installed_languages() as $lang => $name) { + $submenu = new WT_Menu($name, get_query_url(array('lang' => $lang), '&'), 'menu-language-' . $lang); + if (WT_LOCALE === $lang) { + $submenu->addClass('', '', 'active'); + } + $menu->addSubmenu($submenu); + } + + if (count($menu->getSubmenus()) > 1 && !$this->isSearchEngine()) { + return $menu; + } else { + return null; + } + } + + /** + * Create a menu to show lists of individuals, families, sources, etc. + * + * @return WT_Menu|null + */ + protected function menuLists() { + global $controller; + + // The top level menu shows the individual list + $menu = new WT_Menu(WT_I18N::translate('Lists'), 'indilist.php?' . $this->tree_url, 'menu-list'); + + // Do not show empty lists + $row = WT_DB::prepare( + "SELECT SQL_CACHE" . + " EXISTS(SELECT 1 FROM `##sources` WHERE s_file=? ) AS sour," . + " EXISTS(SELECT 1 FROM `##other` WHERE o_file=? AND o_type='REPO') AS repo," . + " EXISTS(SELECT 1 FROM `##other` WHERE o_file=? AND o_type='NOTE') AS note," . + " EXISTS(SELECT 1 FROM `##media` WHERE m_file=? ) AS obje" + )->execute(array(WT_GED_ID, WT_GED_ID, WT_GED_ID, WT_GED_ID))->fetchOneRow(); + + // Build a list of submenu items and then sort it in localized name order + $surname_url = '&surname=' . rawurlencode($controller->getSignificantSurname()) . '&' . $this->tree_url; + + $menulist = array( + new WT_Menu(WT_I18N::translate('Individuals'), 'indilist.php?' . $this->tree_url . $surname_url, 'menu-list-indi'), + ); + + if (!$this->isSearchEngine()) { + $menulist[] = new WT_Menu(WT_I18N::translate('Families'), 'famlist.php?' . $this->tree_url . $surname_url, 'menu-list-fam'); + $menulist[] = new WT_Menu(WT_I18N::translate('Branches'), 'branches.php?' . $this->tree_url . $surname_url, 'menu-branches'); + $menulist[] = new WT_Menu(WT_I18N::translate('Place hierarchy'), 'placelist.php?' . $this->tree_url, 'menu-list-plac'); + if ($row->obje) { + $menulist[] = new WT_Menu(WT_I18N::translate('Media objects'), 'medialist.php?' . $this->tree_url, 'menu-list-obje'); + } + if ($row->repo) { + $menulist[] = new WT_Menu(WT_I18N::translate('Repositories'), 'repolist.php?' . $this->tree_url, 'menu-list-repo'); + } + if ($row->sour) { + $menulist[] = new WT_Menu(WT_I18N::translate('Sources'), 'sourcelist.php?' . $this->tree_url, 'menu-list-sour'); + } + if ($row->note) { + $menulist[] = new WT_Menu(WT_I18N::translate('Shared notes'), 'notelist.php?' . $this->tree_url, 'menu-list-note'); + } + } + uasort($menulist, function (WT_Menu $x, WT_Menu $y) { + return WT_I18N::strcasecmp($x->getLabel(), $y->getLabel()); + }); + + $menu->setSubmenus($menulist); + + return $menu; + } + + /** + * A login menu option (or null if we are already logged in). + * + * @return WT_Menu|null + */ + protected function menuLogin() { + if (Auth::check() || $this->isSearchEngine()) { + return null; + } else { + return new WT_Menu(WT_I18N::translate('Login'), WT_LOGIN_URL . '?url=' . rawurlencode(get_query_url())); + } + } + + /** + * A logout menu option (or null if we are already logged out). + * + * @return WT_Menu|null + */ + protected function menuLogout() { + if (Auth::check()) { + return new WT_Menu(WT_I18N::translate('Logout'), 'logout.php'); + } else { + return null; + } + } + + /** + * Get the additional menus created by each of the modules + * + * @return WT_Menu[] + */ + protected function menuModules() { + $menus = array(); + foreach (WT_Module::getActiveMenus() as $module) { + $menu = $module->getMenu(); + if ($menu) { + $menus[] = $menu; + } + } + + return $menus; + } + + /** + * A link to allow users to edit their account settings. + * + * @return WT_Menu|null + */ + protected function menuMyAccount() { + if (Auth::check()) { + return new WT_Menu(WT_Filter::escapeHtml(Auth::user()->getRealName()), 'edituser.php'); + } else { + return null; + } + } + + /** + * @return WT_Menu|null + */ + protected function menuMyMenu() { + $showFull = $this->tree->getPreference('PEDIGREE_FULL_DETAILS') ? 1 : 0; + $showLayout = $this->tree->getPreference('PEDIGREE_LAYOUT') ? 1 : 0; + + if (!Auth::id()) { + return null; + } + + $menu = new WT_Menu(WT_I18N::translate('My page'), 'index.php?ctype=user&' . $this->tree_url, 'menu-mymenu'); + + $menu->addSubmenu($this->menuMyPage()); + + if (Auth::user()->getPreference('editaccount')) { + $menu->addSubmenu(new WT_Menu(WT_I18N::translate('My account'), 'edituser.php', 'menu-myaccount')); + } + + if (WT_USER_GEDCOM_ID) { + $menu->addSubmenu(new WT_Menu( + WT_I18N::translate('My pedigree'), + 'pedigree.php?' . $this->tree_url . '&rootid=' . WT_USER_GEDCOM_ID . "&show_full={$showFull}&talloffset={$showLayout}", + 'menu-mypedigree' + )); + + $menu->addSubmenu(new WT_Menu( + WT_I18N::translate('My individual record'), 'individual.php?pid=' . WT_USER_GEDCOM_ID . '&' . $this->tree_url, 'menu-myrecord' + )); + } + + if (WT_USER_GEDCOM_ADMIN) { + $menu->addSubmenu(new WT_Menu(WT_I18N::translate('Administration'), 'admin.php', 'menu-admin')); + } + + return $menu; + } + + /** + * A link to the user's personal home page. + * + * @return WT_Menu|null + */ + protected function menuMyPage() + { + return new WT_Menu(WT_I18N::translate('My page'), 'index.php?ctype=user&' . $this->tree_url, 'menu-mypage'); + } + + /** + * Create a pending changes menu. + * + * @return WT_Menu|null + */ + protected function menuPendingChanges() { + if ($this->pendingChangesExist()) { + $menu = new WT_Menu(WT_I18N::translate('Pending changes'), '#', 'menu-pending'); + $menu->setOnclick('window.open(\'edit_changes.php\', \'_blank\', chan_window_specs); return false;'); + + return $menu; + } else { + return null; + } + } + + /** + * @return WT_Menu|null + */ + protected function menuReports() { + $active_reports = WT_Module::getActiveReports(); + + if ($this->isSearchEngine() || !$active_reports) { + return new WT_Menu(WT_I18N::translate('Reports'), '#', 'menu-report'); + } + + $menu = new WT_Menu(WT_I18N::translate('Reports'), 'reportengine.php?' . $this->tree_url, 'menu-report'); + + $sub_menu = false; + foreach ($active_reports as $report) { + foreach ($report->getReportMenus() as $submenu) { + $menu->addSubmenu($submenu); + $sub_menu = true; + } + } + + if ($sub_menu && !$this->isSearchEngine()) { + return $menu; + } else { + return null; + } + } + + /** + * Create the search menu + * + * @return WT_Menu + */ + protected function menuSearch() { + if ($this->isSearchEngine()) { + return new WT_Menu(WT_I18N::translate('Search'), '#', 'menu-search'); + } + //-- main search menu item + $menu = new WT_Menu(WT_I18N::translate('Search'), 'search.php?' . $this->tree_url, 'menu-search'); + //-- search_general sub menu + $submenu = new WT_Menu(WT_I18N::translate('General search'), 'search.php?' . $this->tree_url, 'menu-search-general'); + $menu->addSubmenu($submenu); + //-- search_soundex sub menu + $submenu = new WT_Menu(/* I18N: search using “sounds like”, rather than exact spelling */ + WT_I18N::translate('Phonetic search'), 'search.php?' . $this->tree_url . '&action=soundex', 'menu-search-soundex'); + $menu->addSubmenu($submenu); + //-- advanced search + $submenu = new WT_Menu(WT_I18N::translate('Advanced search'), 'search_advanced.php?' . $this->tree_url, 'menu-search-advanced'); + $menu->addSubmenu($submenu); + //-- search_replace sub menu + if (WT_USER_CAN_EDIT) { + $submenu = new WT_Menu(WT_I18N::translate('Search and replace'), 'search.php?' . $this->tree_url . '&action=replace', 'menu-search-replace'); + $menu->addSubmenu($submenu); + } + + return $menu; + } + + /** + * Themes menu. + * + * @return WT_Menu|null + */ + public function menuThemes() { + if ($this->tree && !$this->isSearchEngine() && WT_Site::getPreference('ALLOW_USER_THEMES') && $this->tree->getPreference('ALLOW_THEME_DROPDOWN')) { + $menu = new WT_Menu(WT_I18N::translate('Theme'), '#', 'menu-theme'); + foreach (Theme::installedThemes() as $theme) { + $submenu = new WT_Menu($theme->themeName(), get_query_url(array('theme' => $theme->themeId()), '&'), 'menu-theme-' . $theme->themeId()); + if ($theme === $this) { + $submenu->addClass('', '', 'active'); + } + $menu->addSubmenu($submenu); + } + + return $menu; + } else { + return null; + } + } + + /** + * Create the <link rel="canonical"> tag. + * + * @param string $url + * + * @return string + */ + protected function metaCanonicalUrl($url) { + if ($url) { + return '<link rel="canonical" href="' . $url . '">'; + } else { + return ''; + } + } + + /** + * Create the <meta charset=""> tag. + * + * @return string + */ + protected function metaCharset() { + return '<meta charset="UTF-8">'; + } + + /** + * Create the <meta name="description"> tag. + * + * @param string $description + * + * @return string + */ + protected function metaDescription($description) { + if ($description) { + return '<meta name="description" content="' . $description . '">'; + } else { + return ''; + } + } + + /** + * Create the <meta name="generator"> tag. + * + * @param string $generator + * + * @return string + */ + protected function metaGenerator($generator) { + if ($generator) { + return '<meta name="generator" content="' . $generator . '">'; + } else { + return ''; + } + } + + /** + * Create the <meta name="robots"> tag. + * + * @param string $robots + * + * @return string + */ + protected function metaRobots($robots) { + if ($robots) { + return '<meta name="robots" content="' . $robots . '">'; + } else { + return ''; + } + } + + /** + * Create the <meta http-equiv="X-UA-Compatible"> tag. + * + * @return string + */ + protected function metaUaCompatible() { + return '<meta http-equiv="X-UA-Compatible" content="IE=edge">'; + } + + /** + * Misecellaneous dimensions, fonts, styles, etc. + * + * @param string $parameter_name + * + * @return string|integer|float + */ + public function parameter($parameter_name) { + $parameters = array( + 'chart-background-f' => 'dddddd', + 'chart-background-m' => 'cccccc', + 'chart-background-u' => 'eeeeee', + 'chart-box-x' => 250, + 'chart-box-y' => 80, + 'chart-descendancy-box-x' => 260, + 'chart-descendancy-box-y' => 80, + 'chart-descendancy-indent' => 15, + 'chart-font-color' => '000000', + 'chart-font-name' => WT_ROOT . 'includes/fonts/DejaVuSans.ttf', + 'chart-font-size' => 7, + 'chart-offset-x' => 10, + 'chart-offset-y' => 10, + 'chart-spacing-x' => 1, + 'chart-spacing-y' => 5, + 'compact-chart-box-x' => 240, + 'compact-chart-box-y' => 50, + 'distribution-chart-high-values' => '555555', + 'distribution-chart-low-values' => 'cccccc', + 'distribution-chart-no-values' => 'ffffff', + 'distribution-chart-x' => 440, + 'distribution-chart-y' => 220, + 'line-width' => 1.5, + 'shadow-blur' => 0, + 'shadow-color' => '', + 'shadow-offset-x' => 0, + 'shadow-offset-y' => 0, + 'stats-small-chart-x' => 440, + 'stats-small-chart-y' => 125, + 'stats-large-chart-x' => 900, + 'image-dline' => $this->cssUrl() . 'images/dline.png', + 'image-dline2' => $this->cssUrl() . 'images/dline2.png', + 'image-hline' => $this->cssUrl() . 'images/hline.png', + 'image-spacer' => $this->cssUrl() . 'images/spacer.png', + 'image-vline' => $this->cssUrl() . 'images/vline.png', + 'image-add' => $this->cssUrl() . 'images/add.png', + 'image-button_family' => $this->cssUrl() . 'images/buttons/family.png', + 'image-minus' => $this->cssUrl() . 'images/minus.png', + 'image-plus' => $this->cssUrl() . 'images/plus.png', + 'image-remove' => $this->cssUrl() . 'images/delete.png', + 'image-search' => $this->cssUrl() . 'images/go.png', + 'image-default_image_F' => $this->cssUrl() . 'images/silhouette_female.png', + 'image-default_image_M' => $this->cssUrl() . 'images/silhouette_male.png', + 'image-default_image_U' => $this->cssUrl() . 'images/silhouette_unknown.png', + ); + + if (array_key_exists($parameter_name, $parameters)) { + return $parameters[$parameter_name]; + } else { + throw new \InvalidArgumentException($parameter_name); + } + } + + /** + * Are there any pending changes for us to approve? + * + * @return bool + */ + protected function pendingChangesExist() { + return exists_pending_change(Auth::user(), $this->tree); + } + + /** + * Create a pending changes link. + * + * @return string + */ + protected function pendingChangesLink() { + return + '<a href="#" onclick="window.open(\'edit_changes.php\', \'_blank\', chan_window_specs); return false;">' . + $this->pendingChangesLinkText() . + '</a>'; + } + + /** + * Text to use in the pending changes link. + * + * @return string + */ + protected function pendingChangesLinkText() { + return WT_I18N::translate('There are pending changes for you to moderate.'); + } + + /** + * Generate a list of items for the main menu. + * + * @return WT_Menu[] + */ + protected function primaryMenu() { + global $controller; + + $individual = $controller->getSignificantIndividual(); + + return array_filter(array_merge(array( + $this->menuHomePage(), + $this->menuMyMenu(), + $this->menuChart($individual), + $this->menuLists(), + $this->menuCalendar(), + $this->menuReports(), + $this->menuSearch(), + ), $this->menuModules())); + } + + /** + * Add markup to the primary menu. + * + * @param WT_Menu[] $menus + * + * @return string + */ + protected function primaryMenuContainer(array $menus) { + $html = ''; + foreach ($menus as $menu) { + $html .= $menu->getMenuAsList(); + } + + return '<nav><ul class="primary-menu">' . $html . '</ul></nav>'; + } + + /** + * Generate a list of items for the user menu. + * + * @return WT_Menu[] + */ + protected function secondaryMenu() { + return array_filter(array( + $this->menuPendingChanges(), + $this->menuLogin(), + $this->menuMyAccount(), + $this->menuLogout(), + $this->menuFavorites(), + $this->menuLanguages(), + $this->menuThemes(), + )); + } + + /** + * Add markup to the secondary menu. + * + * @param WT_Menu[] $menus + * + * @return string + */ + protected function secondaryMenuContainer(array $menus) { + $html = ''; + foreach ($menus as $menu) { + $html .= $menu->getMenuAsList(); + } + + return '<ul class="nav nav-pills secondary-menu">' . $html . '</ul>'; + } + + /** + * Send any HTTP headers. + * + * @return void + */ + public function sendHeaders() { + header('Content-Type: text/html; charset=UTF-8'); + } + + /** + * A list of CSS files to include for this page. + * + * @return string[] + */ + protected function stylesheets() { + return array(); + } + + /** + * A fixed string to identify this theme, in settings, etc. + * + * @return string + */ + abstract public function themeId(); + + /** + * What is this theme called? + * + * @return string + */ + abstract public function themeName(); + + /** + * Create the <title> tag. + * + * @param string $title + * + * @return string + */ + protected function title($title) { + return '<title>' . WT_Filter::escapeHtml($title) . '</title>'; + } +} |
