From b7198b8350ed72966d623dd891a1063e78cb0334 Mon Sep 17 00:00:00 2001 From: Bogdanova Olga Date: Thu, 11 Feb 2021 20:12:03 +0300 Subject: [PATCH] Issues-386: design tweaks --- .../controllers/class.searchcontroller.php | 4 +- .../dashboard/modules/class.guestmodule.php | 58 + .../dashboard/views/modules/guest.php | 25 + .../dashboard/views/search/index.php | 17 + .../class.categoriescontroller.php | 26 +- .../class.discussioncontroller.php | 6 +- .../class.discussionscontroller.php | 8 +- .../controllers/class.draftscontroller.php | 124 ++ .../controllers/class.postcontroller.php | 3 +- .../vanilla/views/categories/all.php | 10 +- .../views/categories/helper_functions.php | 43 +- .../views/discussions/helper_functions.php | 44 +- .../vanilla/views/drafts/drafts.php | 42 + .../views/modules/discussionfilter.php | 13 +- .../vanilla/views/post/comment.php | 89 + .../vanilla/views/post/editcomment.php | 26 + .../SmartyPlugins/function.breadcrumbs.php | 28 + vanilla/library/core/class.theme.php | 564 +++++ vanilla/library/core/functions.render.php | 1928 +++++++++++++++++ 19 files changed, 2992 insertions(+), 66 deletions(-) create mode 100644 vanilla/applications/dashboard/modules/class.guestmodule.php create mode 100644 vanilla/applications/dashboard/views/modules/guest.php create mode 100644 vanilla/applications/dashboard/views/search/index.php create mode 100644 vanilla/applications/vanilla/controllers/class.draftscontroller.php create mode 100644 vanilla/applications/vanilla/views/drafts/drafts.php create mode 100644 vanilla/applications/vanilla/views/post/comment.php create mode 100644 vanilla/applications/vanilla/views/post/editcomment.php create mode 100644 vanilla/library/SmartyPlugins/function.breadcrumbs.php create mode 100644 vanilla/library/core/class.theme.php create mode 100644 vanilla/library/core/functions.render.php diff --git a/vanilla/applications/dashboard/controllers/class.searchcontroller.php b/vanilla/applications/dashboard/controllers/class.searchcontroller.php index e348f43..89cf525 100644 --- a/vanilla/applications/dashboard/controllers/class.searchcontroller.php +++ b/vanilla/applications/dashboard/controllers/class.searchcontroller.php @@ -58,9 +58,9 @@ public function initialize() { $this->addCssFile('style.css'); $this->addCssFile('vanillicon.css', 'static'); $this->addModule('GuestModule'); - $this->addModule('NewDiscussionModule'); + //$this->addModule('NewDiscussionModule'); $this->addModule('DiscussionFilterModule'); - $this->addModule('CategoriesModule'); + //$this->addModule('CategoriesModule'); $this->addModule('BookmarkedModule'); parent::initialize(); $this->setData('Breadcrumbs', [['Name' => t('Search'), 'Url' => '/search']]); diff --git a/vanilla/applications/dashboard/modules/class.guestmodule.php b/vanilla/applications/dashboard/modules/class.guestmodule.php new file mode 100644 index 0000000..85d4a1f --- /dev/null +++ b/vanilla/applications/dashboard/modules/class.guestmodule.php @@ -0,0 +1,58 @@ +Visible = c('Garden.Modules.ShowGuestModule'); + } + + /** + * + * + * @return string + */ + public function assetTarget() { + return 'Panel'; + } + + /** + * Render. + * + * @return string + */ + public function toString() { + if (!Gdn::session()->isValid()) { + return parent::toString(); + } + + return ''; + } +} diff --git a/vanilla/applications/dashboard/views/modules/guest.php b/vanilla/applications/dashboard/views/modules/guest.php new file mode 100644 index 0000000..f845557 --- /dev/null +++ b/vanilla/applications/dashboard/views/modules/guest.php @@ -0,0 +1,25 @@ + +
+

+ +

MessageCode, $this->MessageDefault); ?>

+ +

fireEvent('BeforeSignInButton'); ?>

+ + _Sender->SelfUrl); + + if ($signInUrl) { + echo '
'; + + echo anchor(t('Login'), signInUrl($this->_Sender->SelfUrl), 'Button Primary SignIn BigButton'.(signInPopup() ? ' SignInPopup' : ''), ['rel' => 'nofollow']); + // $Url = registerUrl($this->_Sender->SelfUrl); + // if (!empty($Url)) { + // echo ' '.anchor(t('Register', t('Apply for Membership', 'Register')), $Url, 'Button ApplyButton', ['rel' => 'nofollow']); + // } + + echo '
'; + } + ?> + fireEvent('AfterSignInButton'); ?> +
diff --git a/vanilla/applications/dashboard/views/search/index.php b/vanilla/applications/dashboard/views/search/index.php new file mode 100644 index 0000000..ce2123b --- /dev/null +++ b/vanilla/applications/dashboard/views/search/index.php @@ -0,0 +1,17 @@ + +

Search

+
+ Form; + echo $Form->open(['action' => url('/search'), 'method' => 'get']), + '
', + $Form->textBox('Search', ['aria-label' => t('Enter your search term.'), 'title' => t('Enter your search term.') ]), + $Form->button('Search', ['aria-label' => t('Search'), 'Name' => '']), + '
', + $Form->errors(), + $Form->close(); + ?> +
+fetchViewLocation('results'); +include($ViewLocation); diff --git a/vanilla/applications/vanilla/controllers/class.categoriescontroller.php b/vanilla/applications/vanilla/controllers/class.categoriescontroller.php index 288035f..19b3be1 100644 --- a/vanilla/applications/vanilla/controllers/class.categoriescontroller.php +++ b/vanilla/applications/vanilla/controllers/class.categoriescontroller.php @@ -34,6 +34,7 @@ class CategoriesController extends VanillaController { const SORT_LAST_POST = 'new'; const SORT_OLDEST_POST = 'old'; + const ROOT_CATEGORY = ['Name' => 'Roundtables', 'Url'=>'/']; /** * @var \Closure $categoriesCompatibilityCallback A backwards-compatible callback to get `$this->data('Categories')`. */ @@ -332,7 +333,11 @@ public function index($categoryIdentifier = '', $page = '0') { } // Load the breadcrumbs. - $this->setData('Breadcrumbs', CategoryModel::getAncestors(val('CategoryID', $category))); + + $ancestors = CategoryModel::getAncestors(val('CategoryID', $category)); + array_unshift ( $ancestors , self::ROOT_CATEGORY); + $this->setData('Breadcrumbs', $ancestors); + $this->setData('Category', $category, true); // Set CategoryID @@ -406,7 +411,7 @@ public function index($categoryIdentifier = '', $page = '0') { // Add modules $this->addModule('NewDiscussionModule'); $this->addModule('DiscussionFilterModule'); - $this->addModule('CategoriesModule'); + // $this->addModule('CategoriesModule'); $this->addModule('BookmarkedModule'); $this->addModule('TagModule'); @@ -535,7 +540,7 @@ public function all($Category = '', $displayAs = '') { if ($Title) { $this->title($Title, ''); } else { - $this->title(t('All Categories')); + $this->title(t('Roundtables')); } } Gdn_Theme::section('CategoryList'); @@ -544,7 +549,10 @@ public function all($Category = '', $displayAs = '') { $this->description(c('Garden.Description', null)); } - $this->setData('Breadcrumbs', CategoryModel::getAncestors(val('CategoryID', $this->data('Category')))); + $ancestors = CategoryModel::getAncestors(val('CategoryID', $this->data('Category'))); + array_unshift ( $ancestors , self::ROOT_CATEGORY); + $this->setData('Breadcrumbs', $ancestors); + // Set the category follow toggle before we load category data so that it affects the category query appropriately. $CategoryFollowToggleModule = new CategoryFollowToggleModule($this); @@ -630,10 +638,12 @@ public function all($Category = '', $displayAs = '') { $this->setData('CategoryTree', $categoryTree); // Add modules - $this->addModule('NewDiscussionModule'); + if($Category) { + $this->addModule('NewDiscussionModule'); + } $this->addModule('DiscussionFilterModule'); $this->addModule('BookmarkedModule'); - $this->addModule('CategoriesModule'); + // $this->addModule('CategoriesModule'); $this->addModule($CategoryFollowToggleModule); $this->addModule('TagModule'); @@ -669,7 +679,7 @@ public function discussions($Category = '') { if ($Title) { $this->title($Title, ''); } else { - $this->title(t('All Categories')); + $this->title(t('Roundtables')); } } @@ -725,7 +735,7 @@ public function discussions($Category = '') { // Add modules $this->addModule('NewDiscussionModule'); $this->addModule('DiscussionFilterModule'); - $this->addModule('CategoriesModule'); + // $this->addModule('CategoriesModule'); $this->addModule('BookmarkedModule'); $this->addModule($CategoryFollowToggleModule); diff --git a/vanilla/applications/vanilla/controllers/class.discussioncontroller.php b/vanilla/applications/vanilla/controllers/class.discussioncontroller.php index f11785f..e5cfc55 100644 --- a/vanilla/applications/vanilla/controllers/class.discussioncontroller.php +++ b/vanilla/applications/vanilla/controllers/class.discussioncontroller.php @@ -125,7 +125,9 @@ public function index($DiscussionID = '', $DiscussionStub = '', $Page = '') { Gdn_Theme::section($CategoryCssClass); } - $this->setData('Breadcrumbs', CategoryModel::getAncestors($this->CategoryID)); + $ancestors = CategoryModel::getAncestors($this->CategoryID); + array_unshift ( $ancestors , CategoriesController::ROOT_CATEGORY); + $this->setData('Breadcrumbs', $ancestors); // Setup $this->title($this->Discussion->Name); @@ -294,7 +296,7 @@ public function index($DiscussionID = '', $DiscussionStub = '', $Page = '') { // Add modules $this->addModule('DiscussionFilterModule'); $this->addModule('NewDiscussionModule'); - $this->addModule('CategoriesModule'); + // $this->addModule('CategoriesModule'); $this->addModule('BookmarkedModule'); $this->CanEditComments = Gdn::session()->checkPermission('Vanilla.Comments.Edit', true, 'Category', 'any') && c('Vanilla.AdminCheckboxes.Use'); diff --git a/vanilla/applications/vanilla/controllers/class.discussionscontroller.php b/vanilla/applications/vanilla/controllers/class.discussionscontroller.php index e38f38e..557163d 100644 --- a/vanilla/applications/vanilla/controllers/class.discussionscontroller.php +++ b/vanilla/applications/vanilla/controllers/class.discussionscontroller.php @@ -116,7 +116,7 @@ public function index($Page = false) { // Add modules $this->addModule('DiscussionFilterModule'); $this->addModule('NewDiscussionModule'); - $this->addModule('CategoriesModule'); + // $this->addModule('CategoriesModule'); $this->addModule('BookmarkedModule'); $this->addModule('TagModule'); @@ -282,7 +282,7 @@ public function unread($page = '0') { // Add modules $this->addModule('DiscussionFilterModule'); $this->addModule('NewDiscussionModule'); - $this->addModule('CategoriesModule'); + // $this->addModule('CategoriesModule'); $this->addModule('BookmarkedModule'); $this->addModule('TagModule'); @@ -459,7 +459,7 @@ public function bookmarked($page = '0') { // Add modules $this->addModule('DiscussionFilterModule'); $this->addModule('NewDiscussionModule'); - $this->addModule('CategoriesModule'); + // $this->addModule('CategoriesModule'); $this->addModule('TagModule'); // Render default view (discussions/bookmarked.php) @@ -560,7 +560,7 @@ public function mine($page = 'p1') { // Add modules $this->addModule('DiscussionFilterModule'); $this->addModule('NewDiscussionModule'); - $this->addModule('CategoriesModule'); + // $this->addModule('CategoriesModule'); $this->addModule('BookmarkedModule'); $this->addModule('TagModule'); diff --git a/vanilla/applications/vanilla/controllers/class.draftscontroller.php b/vanilla/applications/vanilla/controllers/class.draftscontroller.php new file mode 100644 index 0000000..108b02f --- /dev/null +++ b/vanilla/applications/vanilla/controllers/class.draftscontroller.php @@ -0,0 +1,124 @@ +permission('Garden.SignIn.Allow'); + $this->addJsFile('jquery.gardenmorepager.js'); + $this->addJsFile('discussions.js'); + $this->title(t('My Drafts')); + + // Validate $Offset + if (!is_numeric($offset) || $offset < 0) { + $offset = 0; + } + + // Set criteria & get drafts data + $limit = Gdn::config('Vanilla.Discussions.PerPage', 30); + $session = Gdn::session(); + $wheres = ['d.InsertUserID' => $session->UserID]; + $this->DraftData = $this->DraftModel->getByUser($session->UserID, $offset, $limit); + $countDrafts = $this->DraftModel->getCountByUser($session->UserID); + + // Build a pager + $pagerFactory = new Gdn_PagerFactory(); + $this->Pager = $pagerFactory->getPager('MorePager', $this); + $this->Pager->MoreCode = 'More drafts'; + $this->Pager->LessCode = 'Newer drafts'; + $this->Pager->ClientID = 'Pager'; + $this->Pager->configure( + $offset, + $limit, + $countDrafts, + 'drafts/%1$s' + ); + + // Deliver JSON data if necessary + if ($this->_DeliveryType != DELIVERY_TYPE_ALL) { + $this->setJson('LessRow', $this->Pager->toString('less')); + $this->setJson('MoreRow', $this->Pager->toString('more')); + $this->View = 'drafts'; + } + + // Add modules + $this->addModule('DiscussionFilterModule'); + //$this->addModule('NewDiscussionModule'); + //$this->addModule('CategoriesModule'); + $this->addModule('BookmarkedModule'); + + // Render default view (drafts/index.php) + $this->render(); + } + + /** + * Delete a single draft. + * + * Redirects user back to Index unless DeliveryType is set. + * + * @since 2.0.0 + * @access public + * + * @param int $draftID Unique ID of draft to be deleted. + * @param string $transientKey Single-use hash to prove intent. + */ + public function delete($draftID = '', $transientKey = '') { + $form = Gdn::factory('Form'); + $session = Gdn::session(); + if (is_numeric($draftID) && $draftID > 0) { + $draft = $this->DraftModel->getID($draftID); + } + if ($draft) { + if ($session->validateTransientKey($transientKey) + && ((val('InsertUserID', $draft) == $session->UserID) || checkPermission('Garden.Community.Manage')) + ) { + // Delete the draft + if (!$this->DraftModel->deleteID($draftID)) { + $form->addError('Failed to delete draft'); + } + } else { + throw permissionException('Garden.Community.Manage'); + } + } else { + throw notFoundException('Draft'); + } + + // Redirect + if ($this->_DeliveryType === DELIVERY_TYPE_ALL) { + $target = getIncomingValue('Target', '/drafts'); + redirectTo($target); + } + + // Return any errors + if ($form->errorCount() > 0) { + $this->setJson('ErrorMessage', $form->errors()); + } + + // Render default view + $this->render(); + } +} diff --git a/vanilla/applications/vanilla/controllers/class.postcontroller.php b/vanilla/applications/vanilla/controllers/class.postcontroller.php index a3ffdbb..ed33d7f 100644 --- a/vanilla/applications/vanilla/controllers/class.postcontroller.php +++ b/vanilla/applications/vanilla/controllers/class.postcontroller.php @@ -419,6 +419,7 @@ public function discussion($categoryUrlCode = '') { 'Url' => val('AddUrl', val($this->data('Type'), DiscussionModel::discussionTypes()), '/post/discussion') ]; + array_unshift ( $breadcrumbs , CategoriesController::ROOT_CATEGORY); $this->setData('Breadcrumbs', $breadcrumbs); // FIX: Hide Announce options @@ -1022,7 +1023,7 @@ public function initialize() { // Add modules $this->addModule('NewDiscussionModule'); $this->addModule('DiscussionFilterModule'); - $this->addModule('CategoriesModule'); + // $this->addModule('CategoriesModule'); $this->addModule('BookmarkedModule'); parent::initialize(); diff --git a/vanilla/applications/vanilla/views/categories/all.php b/vanilla/applications/vanilla/views/categories/all.php index 8c5e430..5541414 100644 --- a/vanilla/applications/vanilla/views/categories/all.php +++ b/vanilla/applications/vanilla/views/categories/all.php @@ -6,15 +6,17 @@ $title .= watchButton($this->Category->CategoryID); } echo '

'.$title.'

'; -if ($description = $this->description()) { - echo wrap($description, 'div', ['class' => 'P PageDescription']); -} +// if ($description = $this->description()) { + //echo wrap($description, 'div', ['class' => 'P PageDescription']); +// } $this->fireEvent('AfterPageTitle'); echo '
'; if ($this->data('EnableFollowingFilter')) { echo categoryFilters(); } -echo categorySorts(); +if (!is_null($this->Category) && $this->Category->DisplayAs == 'Discussions') { + echo categorySorts(); +} echo '
'; $categories = $this->data('CategoryTree'); writeCategoryList($categories, 1); diff --git a/vanilla/applications/vanilla/views/categories/helper_functions.php b/vanilla/applications/vanilla/views/categories/helper_functions.php index bef05ae..1313bac 100644 --- a/vanilla/applications/vanilla/views/categories/helper_functions.php +++ b/vanilla/applications/vanilla/views/categories/helper_functions.php @@ -104,11 +104,11 @@ function mostRecentString($row) { $r = ''; $r .= ''; - $r .= ''.t('Most recent:').' '; - $r .= anchor( - sliceString(Gdn_Format::text($row['LastTitle']), 150), - $row['LastUrl'], - 'LatestPostTitle'); + $r .= ''.t('Most recent').' '; + // $r .= anchor( + // sliceString(Gdn_Format::text($row['LastTitle']), 150), + // $row['LastUrl'], + // 'LatestPostTitle'); if ($last) { $r .= ' '; @@ -123,9 +123,10 @@ function mostRecentString($row) { $r .= ''; $r .= t('on').' '; - $dateFormatted = Gdn::getContainer()->get(DateTimeFormatter::class)->formatDate($lastDate, false, DateTimeFormatter::FORCE_FULL_FORMAT); + $dateFormatted = Gdn::getContainer()->get(DateTimeFormatter::class)->formatDate($lastDate, false, '%a, %b %e %Y'); + $timeFormatted = Gdn::getContainer()->get(DateTimeFormatter::class)->formatDate($lastDate, false, '%I:%M %p'); $r .= anchor( - $dateFormatted, + $dateFormatted.' at '.$timeFormatted, $row['LastUrl'], 'CommentDate'); $r .= ''; @@ -191,7 +192,20 @@ function writeListItem($category, $depth) { ?>
- + + + + + + + +
+ + +
+ + + · - - - - - - -
- - -
-
diff --git a/vanilla/applications/vanilla/views/discussions/helper_functions.php b/vanilla/applications/vanilla/views/discussions/helper_functions.php index ed238a0..b547c40 100644 --- a/vanilla/applications/vanilla/views/discussions/helper_functions.php +++ b/vanilla/applications/vanilla/views/discussions/helper_functions.php @@ -183,32 +183,15 @@ function writeDiscussion($discussion, $sender, $session) { - CountViews, - '%s view html', '%s views html', t('%s view'), t('%s views')), - bigPlural($discussion->CountViews, '%s view')); - ?> - CountComments, - '%s comment html', '%s comments html', t('%s comment'), t('%s comments')), - bigPlural($discussion->CountComments, '%s comment')); - ?> - fireEvent('AfterCountMeta'); if ($discussion->LastDiscussionCommentsUserID != '') { - $dateFormatted = Gdn::getContainer()->get(DateTimeFormatter::class)->formatDate($lastDate, false, DateTimeFormatter::FORCE_FULL_FORMAT); - echo ' '.sprintf(t('Most recent by %1$s'), userAnchor($last)).' '; - echo ' '.$dateFormatted.''; + $dateFormatted = Gdn::getContainer()->get(DateTimeFormatter::class)->formatDate($lastDate, false, '%a, %b %e %Y'); + $timeFormatted = Gdn::getContainer()->get(DateTimeFormatter::class)->formatDate($lastDate, false, '%I:%M %p'); + echo ''.sprintf(t('Most recent by %1$s on %2$s at %3$s'), userAnchor($last),$dateFormatted, $timeFormatted).''; } else { $dateFormatted = Gdn::getContainer()->get(DateTimeFormatter::class)->formatDate($discussion->FirstDate, false, DateTimeFormatter::FORCE_FULL_FORMAT); echo ' '.sprintf(t('Started by %1$s'), userAnchor($first)).' '; @@ -228,9 +211,28 @@ function writeDiscussion($discussion, $sender, $session) { 'span', ['class' => 'MItem Category '.$category['CssClass']] ); - } + } ?> + + CountComments, + '%s comment html', '%s comments html', t('%s comment'), t('%s comments')), + bigPlural($discussion->CountComments, '%s comment')); + ?> + · + CountViews, + '%s view html', '%s views html', t('%s view'), t('%s views')), + bigPlural($discussion->CountViews, '%s view')); + ?> + + fireEvent('DiscussionMeta'); ?> + fireEvent('AfterDiscussionContent'); ?> diff --git a/vanilla/applications/vanilla/views/drafts/drafts.php b/vanilla/applications/vanilla/views/drafts/drafts.php new file mode 100644 index 0000000..90c2fe4 --- /dev/null +++ b/vanilla/applications/vanilla/views/drafts/drafts.php @@ -0,0 +1,42 @@ +DraftData->resultArray() as $Draft) { + $Offset = val('CountComments', $Draft, 0); + if ($Offset > c('Vanilla.Comments.PerPage', 30)) { + $Offset -= c('Vanilla.Comments.PerPage', 30); + } else { + $Offset = 0; + } + + $draftID = val('DraftID', $Draft); + $discussionID = val('DiscussionID', $Draft); + $excerpt = sliceString(Gdn_Format::plainText(val('Body', $Draft), val('Format', $Draft)), 200); + + $isDiscussion = (!is_numeric($discussionID) || $discussionID <= 0); + $orphaned = !val('DiscussionExists', $Draft); + + $editUrl = ($isDiscussion || $orphaned) ? '/post/editdiscussion/0/'.$draftID : '/discussion/'.$discussionID.'/'.$Offset.'/#Form_Comment'; + $deleteUrl = 'vanilla/drafts/delete/'.$draftID.'/'.Gdn::session()->transientKey().'?Target='.urlencode($this->SelfUrl); + ?> +
  • +
    +
    + 'Draft-Title', 'role' => 'heading', 'aria-level' => '2']), $editUrl, 'Title DraftLink'); + } else { + echo anchor(wrap(t('No Title'), "h2"), $editUrl, 'sr-only'); + } + ?> + +
    + +
    + +
    +
  • +Request->path()) , 'categories') === 0) { $CssClass .= ' Active'; } - - echo '
  • '.anchor(sprite('SpAllCategories').' '.t('All Categories', 'Categories'), '/categories').'
  • '; + echo '
  • '.anchor('Roundtables', '/categories').'
  • '; } + /* +
  • +
  • + */ + $Controller->fireEvent('BeforeUserLinksDiscussionFilters'); ?> -
  • - + + 0 || $Controller->RequestMethod == 'mine') && c('Vanilla.Discussions.ShowMineTab', true)) { ?>
  • fireEvent('AfterDiscussionFilters'); ?> diff --git a/vanilla/applications/vanilla/views/post/comment.php b/vanilla/applications/vanilla/views/post/comment.php new file mode 100644 index 0000000..5e3869b --- /dev/null +++ b/vanilla/applications/vanilla/views/post/comment.php @@ -0,0 +1,89 @@ +Comment) || property_exists($this->Comment, 'DraftID') ? true : false; +$Editing = isset($this->Comment); +$formCssClass = 'MessageForm CommentForm FormTitleWrapper'; +$this->EventArguments['FormCssClass'] = &$formCssClass; +$this->fireEvent('BeforeCommentForm'); +?> +
    +

    + +
    + isValid()) : ?> +
    +
    + + + +
    +
    + +
    +
    +
    + Form->open(['id' => 'Form_Comment']); + echo $this->Form->errors(); + $this->fireEvent('BeforeBodyField'); + + echo $this->Form->bodyBox('Body', ['Table' => 'Comment', 'FileUpload' => true, 'placeholder' => t('Comment ...'), 'title' => t('Comment ...')]); + + echo '
    '; + $this->fireEvent('AfterBodyField'); + echo '
    '; + + echo "
    \n"; + $this->fireEvent('BeforeFormButtons'); + + $CancelText = t('Home'); + $CancelClass = 'Back'; + if (!$NewOrDraft || $Editing) { + $CancelText = t('Cancel'); + $CancelClass = 'Cancel'; + } + + echo ''; + echo anchor($CancelText, '/'); + if ($this->data('Editor.BackLink')) { + echo ' '.$this->data('Editor.BackLink') ; + } + echo ''; + + $ButtonOptions = ['class' => 'Button Primary CommentButton']; + $ButtonOptions['tabindex'] = 1; + + if (!$Editing && $Session->isValid()) { + echo ' '.anchor(t('Preview'), '#', 'Button PreviewButton')."\n"; + echo ' '.anchor(t('Edit'), '#', 'Button WriteButton Hidden')."\n"; + if ($NewOrDraft) { + echo ' '.anchor(t('Save Draft'), '#', 'Button DraftButton')."\n"; + } + } + + if ($Session->isValid()) { + echo $this->Form->button($Editing ? 'Save Comment' : 'Post Comment', $ButtonOptions); + } else { + $AllowSigninPopup = c('Garden.SignIn.Popup'); + $Attributes = ['tabindex' => '-1']; + if (!$AllowSigninPopup) { + $Attributes['target'] = '_parent'; + } + $AuthenticationUrl = signInUrl($this->SelfUrl); + $CssClass = 'Button Primary Stash'; + if ($AllowSigninPopup) { + $CssClass .= ' SignInPopup'; + } + echo anchor(t('Comment As ...'), $AuthenticationUrl, $CssClass, $Attributes); + } + + $this->fireEvent('AfterFormButtons'); + echo "
    \n"; + echo $this->Form->close(); + ?> +
    +
    +
    +
    +
    diff --git a/vanilla/applications/vanilla/views/post/editcomment.php b/vanilla/applications/vanilla/views/post/editcomment.php new file mode 100644 index 0000000..fd8ba95 --- /dev/null +++ b/vanilla/applications/vanilla/views/post/editcomment.php @@ -0,0 +1,26 @@ +fireEvent('BeforeCommentForm'); +?> +
    +
    +
    +
    + Form->open(); + echo $this->Form->errors(); + $this->fireEvent('BeforeBodyField'); + echo $this->Form->bodyBox('Body', ['Table' => 'Comment', 'FileUpload' => true, 'placeholder' => t('Comment ...'), 'title' => t('Comment ...')]); + $this->fireEvent('AfterBodyField'); + echo "
    \n"; + $this->fireEvent('BeforeFormButtons'); + echo anchor(t('Cancel'), '/', 'Button Cancel').' '; + echo $this->Form->button('Save Comment', ['class' => 'Button Primary CommentButton']); + $this->fireEvent('AfterFormButtons'); + echo "
    \n"; + echo $this->Form->close(); + ?> +
    +
    +
    +
    diff --git a/vanilla/library/SmartyPlugins/function.breadcrumbs.php b/vanilla/library/SmartyPlugins/function.breadcrumbs.php new file mode 100644 index 0000000..7583389 --- /dev/null +++ b/vanilla/library/SmartyPlugins/function.breadcrumbs.php @@ -0,0 +1,28 @@ +data('Breadcrumbs'); + + if (!is_array($breadcrumbs)) { + $breadcrumbs = []; + } + + $options = arrayTranslate($params, ['homeurl' => 'HomeUrl', 'hidelast' => 'HideLast']); + + // Don't show a home link by default + return Gdn_Theme::breadcrumbs($breadcrumbs, val('homelink', $params, false), $options); +} diff --git a/vanilla/library/core/class.theme.php b/vanilla/library/core/class.theme.php new file mode 100644 index 0000000..ae95016 --- /dev/null +++ b/vanilla/library/core/class.theme.php @@ -0,0 +1,564 @@ + + * @copyright 2009-2019 Vanilla Forums Inc. + * @license GPL-2.0-only + * @package Core + * @since 2.0 + */ + +/** + * Allows access to theme controls from within views, to give themers a unified + * toolset for interacting with Vanilla from within views. + */ +class Gdn_Theme { + + /** @var array */ + protected static $_AssetInfo = []; + + protected static $_BulletSep = false; + + protected static $_BulletSection = false; + + /** @var array */ + protected static $_Section = []; + + /** + * + * + * @param string $assetContainer + */ + public static function assetBegin($assetContainer = 'Panel') { + self::$_AssetInfo[] = ['AssetContainer' => $assetContainer]; + ob_start(); + } + + /** + * + */ + public static function assetEnd() { + if (count(self::$_AssetInfo) == 0) { + return; + } + + $asset = ob_get_clean(); + $assetInfo = array_pop(self::$_AssetInfo); + + Gdn::controller()->addAsset($assetInfo['AssetContainer'], $asset); + } + + /** + * + * + * @param $data + * @param bool $homeLink + * @param array $options + * @return string + */ + public static function breadcrumbs($data, $homeLink = false, $options = []) { + $format = ''; + + $result = ''; + + if (!is_array($data)) { + $data = []; + } + + + if ($homeLink) { + $homeUrl = val('HomeUrl', $options); + if (!$homeUrl) { + $homeUrl = url('/', true); + } + + $row = ['Name' => $homeLink, 'Url' => $homeUrl, 'CssClass' => 'CrumbLabel HomeCrumb']; + if (!is_string($homeLink)) { + $row['Name'] = t('Home'); + } + + array_unshift($data, $row); + } + if (val('HideLast', $options)) { + // Remove the last item off the list. + array_pop($data); + } + + $defaultRoute = ltrim(val('Destination', Gdn::router()->getRoute('DefaultController'), ''), '/'); + + // FIX: Don't show current page + if(count($data) <= 1) { + return ''; + } + + $count = 0; + $dataCount = 0; + $homeLinkFound = false; + + foreach ($data as $row) { + $dataCount++; + + if ($homeLinkFound && Gdn::request()->urlCompare($row['Url'], $defaultRoute) === 0) { + continue; // don't show default route twice. + } else { + $homeLinkFound = true; + } + + // Add the breadcrumb wrapper. + if ($count > 0) { + $result .= ''; + } + + $row['Url'] = $row['Url'] ? url($row['Url']) : '#'; + $cssClass = 'CrumbLabel '.val('CssClass', $row); + if ($dataCount == count($data)) { + $cssClass .= ' Last'; + } + + $label = ''.formatString($format, $row).' '; + $result = concatSep(''.t('Breadcrumbs Crumb', '›').' ', $result, $label); + + $count++; + } + + + + // Close the stack. + for ($count--; $count > 0; $count--) { + $result .= ''; + } + + $result = ''.$result.''; + return $result; + } + + /** + * Call before writing an item and it will optionally write a bullet seperator. + * + * @param string $section The name of the section. + * @param bool $return whether or not to return the result or echo it. + * @return string + * @since 2.1 + */ + public static function bulletItem($section, $return = true) { + $result = ''; + + if (self::$_BulletSection === false) { + self::$_BulletSection = $section; + } elseif (self::$_BulletSection != $section) { + $result = "".self::$_BulletSep; + self::$_BulletSection = $section; + } + + if ($return) { + return $result; + } else { + echo $result; + } + } + + /** + * Call before starting a row of bullet-seperated items. + * + * @param strng|bool $sep The seperator used to seperate each section. + * @since 2.1 + */ + public static function bulletRow($sep = false) { + if (!$sep) { + if (!self::$_BulletSep) { + self::$_BulletSep = ' '.bullet().' '; + } + } else { + self::$_BulletSep = $sep; + } + self::$_BulletSection = false; + } + + + /** + * Returns whether or not the page is in the current section. + * + * @param string|array $section + */ + public static function inSection($section) { + $section = (array)$section; + foreach ($section as $name) { + if (isset(self::$_Section[$name])) { + return true; + } + } + return false; + } + + /** + * + * + * @param $path + * @param bool $text + * @param null $format + * @param array $options + * @return mixed|null|string + */ + public static function link($path, $text = false, $format = null, $options = []) { + $session = Gdn::session(); + $class = val('class', $options, ''); + $withDomain = val('WithDomain', $options); + $target = val('Target', $options, ''); + if ($target == 'current') { + $target = trim(url('', true), '/ '); + } + + if (is_null($format)) { + $format = '%text'; + } + + switch ($path) { + case 'activity': + touchValue('Permissions', $options, 'Garden.Activity.View'); + break; + case 'category': + $breadcrumbs = Gdn::controller()->data('Breadcrumbs'); + if (is_array($breadcrumbs) && count($breadcrumbs) > 0) { + $last = array_pop($breadcrumbs); + $path = val('Url', $last); + $defaultText = htmlspecialchars(val('Name', $last, t('Back'))); + } else { + $path = '/'; + $defaultText = c('Garden.Title', t('Back')); + } + if (!$text) { + $text = $defaultText; + } + break; + case 'dashboard': + $path = 'dashboard/settings'; + touchValue('Permissions', $options, ['Garden.Settings.Manage', 'Garden.Settings.View']); + if (!$text) { + $text = t('Dashboard'); + } + break; + case 'home': + $path = '/'; + if (!$text) { + $text = t('Home'); + } + break; + case 'inbox': + $path = 'messages/inbox'; + touchValue('Permissions', $options, 'Garden.SignIn.Allow'); + if (!$text) { + $text = t('Inbox'); + } + if ($session->isValid() && $session->User->CountUnreadConversations) { + $class = trim($class.' HasCount'); + $text .= ' '.htmlspecialchars($session->User->CountUnreadConversations).''; + } + if (!$session->isValid() || !Gdn::addonManager()->lookupAddon('conversations')) { + $text = false; + } + break; + case 'forumroot': + $route = Gdn::router()->getDestination('DefaultForumRoot'); + if (is_null($route)) { + $path = '/'; + } else { + $path = combinePaths(['/', $route]); + } + break; + case 'profile': + touchValue('Permissions', $options, 'Garden.SignIn.Allow'); + if (!$text && $session->isValid()) { + $text = htmlspecialchars($session->User->Name); + } + if ($session->isValid() && $session->User->CountNotifications) { + $class = trim($class.' HasCount'); + $text .= ' '.htmlspecialchars($session->User->CountNotifications).''; + } + break; + case 'user': + $path = 'profile'; + touchValue('Permissions', $options, 'Garden.SignIn.Allow'); + if (!$text && $session->isValid()) { + $text = htmlspecialchars($session->User->Name); + } + + break; + case 'photo': + $path = 'profile'; + touchValue('Permissions', $options, 'Garden.SignIn.Allow'); + if (!$text && $session->isValid()) { + $isFullPath = strtolower(substr($session->User->Photo, 0, 7)) == 'http://' || strtolower(substr($session->User->Photo, 0, 8)) == 'https://'; + $photoUrl = ($isFullPath) ? $session->User->Photo : Gdn_Upload::url(changeBasename($session->User->Photo, 'n%s')); + $text = img($photoUrl, ['alt' => $session->User->Name]); + } + + break; + case 'drafts': + touchValue('Permissions', $options, 'Garden.SignIn.Allow'); + if (!$text) { + $text = t('My Drafts'); + } + if ($session->isValid() && $session->User->CountDrafts) { + $class = trim($class.' HasCount'); + $text .= ' '.htmlspecialchars($session->User->CountDrafts).''; + } + break; + case 'discussions/bookmarked': + touchValue('Permissions', $options, 'Garden.SignIn.Allow'); + if (!$text) { + $text = t('My Bookmarks'); + } + if ($session->isValid() && $session->User->CountBookmarks) { + $class = trim($class.' HasCount'); + $text .= ' '.htmlspecialchars($session->User->CountBookmarks).''; + } + break; + case 'discussions/mine': + touchValue('Permissions', $options, 'Garden.SignIn.Allow'); + if (!$text) { + $text = t('My Discussions'); + } + if ($session->isValid() && $session->User->CountDiscussions) { + $class = trim($class.' HasCount'); + $text .= ' '.htmlspecialchars($session->User->CountDiscussions).''; + } + break; + case 'register': + if (!$text) { + $text = t('Register'); + } + $path = registerUrl($target); + break; + case 'signin': + case 'signinout': + // The destination is the signin/signout toggle link. + if ($session->isValid()) { + if (!$text) { + $text = t('Sign Out'); + } + $path = signOutUrl($target); + $class = concatSep(' ', $class, 'SignOut'); + } else { + if (!$text) { + $text = t('Sign In'); + } + + $path = signInUrl($target); + if (signInPopup() && strpos(Gdn::request()->url(), 'entry') === false) { + $class = concatSep(' ', $class, 'SignInPopup'); + } + } + break; + } + + if ($text == false && strpos($format, '%text') !== false) { + return ''; + } + + if (val('Permissions', $options) && !$session->checkPermission($options['Permissions'], false)) { + return ''; + } + + $url = Gdn::request()->url($path, $withDomain); + + if ($tK = val('TK', $options)) { + if (in_array($tK, [1, 'true'])) { + $tK = 'TransientKey'; + } + $url .= (strpos($url, '?') === false ? '?' : '&').$tK.'='.urlencode(Gdn::session()->transientKey()); + } + + if (strcasecmp(trim($path, '/'), Gdn::request()->path()) == 0) { + $class = concatSep(' ', $class, 'Selected'); + } + + // Build the final result. + $result = $format; + $result = str_replace('%url', $url, $result); + $result = str_replace('%text', $text, $result); + $result = str_replace('%class', $class, $result); + + return $result; + } + + /** + * Renders the banner logo, or just the banner title if the logo is not defined. + * + * @param array $properties + */ + public static function logo($properties = []) { + $logo = c('Garden.Logo'); + $title = c('Garden.Title', 'Title'); + + if ($logo) { + $properties += ['alt' => $title]; + + // Only trim slash from relative paths. + if (!stringBeginsWith($logo, '//')) { + $logo = ltrim($logo, '/'); + } + + // Fix the logo path. + if (stringBeginsWith($logo, 'uploads/')) { + $logo = substr($logo, strlen('uploads/')); + } + + // Set optional title text. + if (empty($properties['title']) && c('Garden.LogoTitle')) { + $properties['title'] = c('Garden.LogoTitle'); + } + + echo img(Gdn_Upload::url($logo), $properties); + } else { + echo htmlEsc($title); + } + } + + /** + * Returns the mobile banner logo. If there is no mobile logo defined then this will just return + * the regular logo or the mobile title. + * + * @return string + */ + public static function mobileLogo() { + $logo = c('Garden.MobileLogo', c('Garden.Logo')); + $title = c('Garden.MobileTitle', c('Garden.Title', 'Title')); + + if ($logo) { + return img(Gdn_Upload::url($logo), ['alt' => $title]); + } else { + return $title; + } + } + + /** + * + * + * @param $name + * @param array $properties + * @return mixed|string + */ + public static function module($name, $properties = []) { + if (isset($properties['cache'])) { + $key = isset($properties['cachekey']) ? $properties['cachekey'] : 'module.'.$name; + + $result = Gdn::cache()->get($key); + if ($result !== Gdn_Cache::CACHEOP_FAILURE) { +// trace('Module: '.$Result, $Key); + return $result; + } + } + + try { + if (!class_exists($name)) { + if (debug()) { + $result = "Error: $name doesn't exist"; + } else { + $result = ""; + } + } else { + $module = new $name(Gdn::controller(), ''); + $module->Visible = true; + + // Add properties passed in from the controller. + $controllerProperties = Gdn::controller()->data('_properties.'.strtolower($name), []); + $properties = array_merge($controllerProperties, $properties); + + foreach ($properties as $name => $value) { + // Check for a setter method + if (method_exists($module, $method = 'set'.ucfirst($name))) { + $module->$method($value); + } else { + $module->$name = $value; + } + } + + $result = $module->toString(); + } + } catch (Exception $ex) { + if (debug()) { + $result = '
    '.htmlspecialchars($ex->getMessage()."\n".$ex->getTraceAsString()).'
    '; + } else { + $result = $ex->getMessage(); + } + } + + if (isset($key)) { +// trace($Result, "Store $Key"); + Gdn::cache()->store($key, $result, [Gdn_Cache::FEATURE_EXPIRY => $properties['cache']]); + } + + return $result; + } + + /** + * + * + * @return string + */ + public static function pagename() { + $application = Gdn::dispatcher()->application(); + $controller = Gdn::dispatcher()->controller(); + switch ($controller) { + case 'discussions': + case 'discussion': + case 'post': + return 'discussions'; + + case 'inbox': + return 'inbox'; + + case 'activity': + return 'activity'; + + case 'profile': + $args = Gdn::dispatcher()->controllerArguments(); + if (!sizeof($args) || (sizeof($args) && $args[0] == Gdn::session()->UserID)) { + return 'profile'; + } + break; + } + + return 'unknown'; + } + + /** + * The current section the site is in. This can be one or more values. Think of it like a server-side css-class. + * + * @since 2.1 + * + * @param string $section The name of the section. + * @param string $method One of: add, remove, set, get. + */ + public static function section($section, $method = 'add') { + $section = array_fill_keys((array)$section, true); + + + switch (strtolower($method)) { + case 'add': + self::$_Section = array_merge(self::$_Section, $section); + break; + case 'remove': + self::$_Section = array_diff_key(self::$_Section, $section); + break; + case 'set': + self::$_Section = $section; + break; + case 'get': + default: + return array_keys(self::$_Section); + } + } + + /** + * + * + * @param $code + * @param $default + * @return mixed + */ + public static function text($code, $default) { + return c("ThemeOption.{$code}", t('Theme_'.$code, $default)); + } +} diff --git a/vanilla/library/core/functions.render.php b/vanilla/library/core/functions.render.php new file mode 100644 index 0000000..c488c05 --- /dev/null +++ b/vanilla/library/core/functions.render.php @@ -0,0 +1,1928 @@ + [ + 'name' => $name, + 'alt' => $alt, + 'class' => $providedClass, + 'dangerousAttributeString' => new \Twig\Markup(attribute($attr), 'utf-8'), + ] + ]); + } +} + +if (!function_exists('bigPlural')) { + /** + * English "plural" formatting for numbers that can get really big. + * + * @param $number + * @param $singular + * @param bool $plural + * @return string + */ + function bigPlural($number, $singular, $plural = false) { + if (!$plural) { + $plural = $singular.'s'; + } + $title = sprintf(t($number == 1 ? $singular : $plural), number_format($number)); + + return ''.Gdn_Format::bigNumber($number).''; + } +} + +if (!function_exists('helpAsset')) { + /** + * Formats a help element and adds it to the help asset. + * + * @param $title + * @param $description + */ + function helpAsset($title, $description) { + Gdn_Theme::assetBegin('Help'); + echo ''; + Gdn_Theme::assetEnd(); + } +} + +if (!function_exists('heading')) { + /** + * Formats a h1 header block for the dashboard. Only to be used once on a page as the h1 header. + * Handles url-ifying. Adds an optional button or return link. + * + * @param string $title The page title. + * @param string|array $buttonText The text appearing on the button or an array of button definitions. + * @param string $buttonUrl The url for the button. + * @param string|array $buttonAttributes Can be string CSS class or an array of attributes. CSS class defaults to `btn btn-primary`. + * @param string $returnUrl The url for the return chrevron button. + * @return string The structured heading string. + * + * @deprecated 3.3 Use @dashboard/components/dashboardHeading.twig or the dashboardHeading mixin. + */ + function heading($title, $buttonText = '', $buttonUrl = '', $buttonAttributes = [], $returnUrl = '') { + if (is_array($buttonText)) { + $buttons = $buttonText; + } elseif (!empty($buttonText)) { + $buttons = [[ + 'text' => $buttonText, + 'url' => $buttonUrl, + 'attributes' => $buttonAttributes + ]]; + } else { + $buttons = []; + } + + $buttonsString = ''; + foreach ($buttons as $button) { + $buttonText = $button['text'] ?? ''; + $buttonUrl = $button['url'] ?? ''; + $buttonAttributes = $button['attributes'] ?? []; + if (is_string($buttonAttributes)) { + $buttonAttributes = ['class' => $buttonAttributes]; + } + + if ($buttonText !== '') { + if (val('class', $buttonAttributes, false) === false) { + $buttonAttributes['class'] = 'btn btn-primary'; + } + } + + if ($buttonUrl === '') { + $buttonsString .= ' '; + } else { + $buttonsString .= ' '.$buttonText.''; + } + } + + return TwigStaticRenderer::renderTwigStatic('@dashboard/components/dashboardHeading.twig', [ + 'params' => [ + 'title' => new \Twig\Markup($title, 'utf-8'), + 'returnUrl' => $returnUrl ?: null, + 'buttons' => $buttonsString ? new \Twig\Markup($buttonsString, 'utf-8') : null, + ] + ]); + } +} + + +if (!function_exists('subheading')) { + /** + * Renders a h2 subheading for the dashboard. + * + * @param string $title The subheading title. + * @param string $description The optional description for the subheading. + * @return string The structured subheading string. + */ + function subheading($title, $description = '') { + if ($description === '') { + return '

    '.$title.'

    '; + } else { + return '
    +

    '.$title.'

    +
    '.$description.'
    +
    '; + } + } +} + +if (!function_exists('badge')) { + /** + * Outputs standardized HTML for a badge. + * + * A badge generally designates a count, and displays with a contrasting background. + * + * @param string|int $badge Info to put into a badge, usually a number. + * @return string Badge HTML string. + */ + function badge($badge) { + return ' '.$badge.' '; + } +} + +if (!function_exists('popin')) { + /** + * Outputs standardized HTML for a popin badge. + * + * A popin contains data that is injected after the page loads. + * A badge generally designates a count, and displays with a contrasting background. + * + * @param string $rel Endpoint for a popin. + * @return string Popin HTML string. + */ + function popin($rel) { + return ' '; + } +} + +if (!function_exists('icon')) { + /** + * Outputs standardized HTML for an icon. + * + * Uses the same css class naming conventions as font-vanillicon. + * + * @param string $icon Name of the icon you want to use, excluding the 'icon-' prefix. + * @return string Icon HTML string. + */ + function icon($icon) { + if (substr(trim($icon), 0, 1) === '<') { + return $icon; + } else { + $icon = strtolower($icon); + return ' '; +} + } +} + +if (!function_exists('bullet')) { + /** + * Return a bullet character in html. + * + * @param string $pad A string used to pad either side of the bullet. + * @return string + * + * @changes + * 2.2 Added the $pad parameter. + */ + function bullet($pad = '') { + //· + return $pad.'·'.$pad; + } +} + +if (!function_exists('buttonDropDown')) { + /** + * Write a button drop down control. + * + * @param array $links An array of arrays with the following keys: + * - Text: The text of the link. + * - Url: The url of the link. + * @param string|array $cssClass The css class of the link. This can be a two-item array where the second element will be added to the buttons. + * @param string $label The text of the button. + * @since 2.1 + */ + function buttonDropDown($links, $cssClass = 'Button', $label = false) { + if (!is_array($links) || count($links) < 1) { + return; + } + + $buttonClass = ''; + if (is_array($cssClass)) { + list($cssClass, $buttonClass) = $cssClass; + } + + if (count($links) < 2) { + $link = array_pop($links); + + if (strpos(val('CssClass', $link, ''), 'Popup') !== false) { + $cssClass .= ' Popup'; + } + + echo anchor($link['Text'], $link['Url'], val('ButtonCssClass', $link, $cssClass)); + } else { + // NavButton or Button? + $buttonClass = concatSep(' ', $buttonClass, strpos($cssClass, 'NavButton') !== false ? 'NavButton' : 'Button'); + if (strpos($cssClass, 'Primary') !== false) { + $buttonClass .= ' Primary'; + } + + // Strip "Button" or "NavButton" off the group class. + echo '
    '; + + echo ''; + + echo anchor($label.' '.sprite('SpDropdownHandle'), '#', $buttonClass.' Handle'); + echo '
    '; + } + } +} + +if (!function_exists('buttonGroup')) { + /** + * Write a button group control. + * + * @param array $links An array of arrays with the following keys: + * - Text: The text of the link. + * - Url: The url of the link. + * @param string|array $cssClass The css class of the link. This can be a two-item array where the second element will be added to the buttons. + * @param string|false $default The url of the default link. + * @since 2.1 + */ + function buttonGroup($links, $cssClass = 'Button', $default = false) { + if (!is_array($links) || count($links) < 1) { + return; + } + + $text = $links[0]['Text']; + $url = $links[0]['Url']; + + $buttonClass = ''; + if (is_array($cssClass)) { + list($cssClass, $buttonClass) = $cssClass; + } + + if ($default && count($links) > 1) { + if (is_array($default)) { + $defaultText = $default['Text']; + $default = $default['Url']; + } + + // Find the default button. + $default = ltrim($default, '/'); + foreach ($links as $link) { + if (stringBeginsWith(ltrim($link['Url'], '/'), $default)) { + $text = $link['Text']; + $url = $link['Url']; + break; + } + } + + if (isset($defaultText)) { + $text = $defaultText; + } + } + + if (count($links) < 2) { + echo anchor($text, $url, $cssClass); + } else { + // NavButton or Button? + $buttonClass = concatSep(' ', $buttonClass, strpos($cssClass, 'NavButton') !== false ? 'NavButton' : 'Button'); + if (strpos($cssClass, 'Primary') !== false) { + $buttonClass .= ' Primary'; + } + // Strip "Button" or "NavButton" off the group class. + echo '
    '; + echo anchor($text, $url, $buttonClass); + + echo ''; + echo anchor(sprite('SpDropdownHandle', 'Sprite', t('Expand for more options.')), '#', $buttonClass.' Handle'); + + echo '
    '; + } + } +} + +if (!function_exists('category')) { + /** + * Get the current category on the page. + * + * @param int $depth The level you want to look at. + * @param array $category + * @return array + */ + function category($depth = null, $category = null) { + if (!$category) { + $category = Gdn::controller()->data('Category'); + } elseif (!is_array($category)) { + $category = CategoryModel::categories($category); + } + + if (!$category) { + $category = Gdn::controller()->data('CategoryID'); + if ($category) { + $category = CategoryModel::categories($category); + } + } + if (!$category) { + return null; + } + + $category = (array)$category; + + if ($depth !== null) { + // Get the category at the correct level. + while ($category['Depth'] > $depth) { + $category = CategoryModel::categories($category['ParentCategoryID']); + if (!$category) { + return null; + } + } + } + + return $category; + } +} + +if (!function_exists('categoryFilters')) { + /** + * Returns category filtering. + * + * @param string $extraClasses any extra classes you add to the drop down + * @return string + */ + function categoryFilters($extraClasses = '') { + if (!Gdn::session()->isValid()) { + return; + } + + $baseUrl = 'categories'; + $transientKey = Gdn::session()->transientKey(); + $filters = [ + [ + 'name' => t('Following'), + 'param' => 'followed', + 'extra' => ['save' => 1, 'TransientKey' => $transientKey] + ] + ]; + + $defaultParams = ['save' => 1, 'TransientKey' => $transientKey]; + if (Gdn::request()->get('followed')) { + $defaultParams['followed'] = 0; + } + + if (!empty($defaultParams)) { + $defaultUrl = $baseUrl.'?'.http_build_query($defaultParams); + } else { + $defaultUrl = $baseUrl; + } + + return filtersDropDown( + $baseUrl, + $filters, + $extraClasses, + t('All'), + $defaultUrl, + 'View' + ); + } +} + +if (!function_exists('categoryUrl')) { + /** + * Return a url for a category. This function is in here and not functions.general so that plugins can override. + * + * @param string|array $category + * @param string|int $page The page number. + * @param bool $withDomain Whether to add the domain to the URL + * @return string The url to a category. + */ + function categoryUrl($category, $page = '', $withDomain = true) { + if (is_string($category)) { + $category = CategoryModel::categories($category); + } + $category = (array)$category; + + $result = '/categories/'.rawurlencode($category['UrlCode']); + if ($page && $page > 1) { + $result .= '/p'.$page; + } + return url($result, $withDomain); + } +} + +if (!function_exists('condense')) { + /** + * + * + * @param string $html + * @return mixed + */ + function condense($html) { + $html = preg_replace('`(?:\s*)+`', "
    ", $html); + $html = preg_replace('`/>\s*
    \s* $number
    "; + } elseif ($number === null && $url) { + $cssClass = trim($cssClass.' Popin TinyProgress', ' '); + $url = htmlspecialchars($url); + return ""; + } else { + return ''; + } + } +} + +if (!function_exists('cssClass')) { + /** + * Add CSS class names to a row depending on other elements/values in that row. + * + * Used by category, discussion, and comment lists. + * + * @param array|object $row + * @param bool $inList Whether or not we are in a discussion list. + * @return string The CSS classes to be inserted into the row. + */ + function cssClass($row, $inList = true) { + static $alt = false; + $row = (array)$row; + $cssClass = 'Item'; + $session = Gdn::session(); + + // Alt rows + if ($alt) { + $cssClass .= ' Alt'; + } + $alt = !$alt; + + // Category list classes + if (array_key_exists('UrlCode', $row)) { + $cssClass .= ' Category-'.Gdn_Format::alphaNumeric($row['UrlCode']); + } + if ($row['CssClass'] ?? false) { + $cssClass .= ' Item-'.$row['CssClass']; + } + + if (array_key_exists('Depth', $row)) { + $cssClass .= " Depth{$row['Depth']} Depth-{$row['Depth']}"; + } + + if (array_key_exists('Archive', $row)) { + $cssClass .= ' Archived'; + } + + // Discussion list classes. + if ($inList) { + if (array_key_exists('Bookmarked', $row)) { + $cssClass .= ($row['Bookmarked'] ?? '') == '1' ? ' Bookmarked' : ''; + + $announce = $row['Announce']; + if ($announce == 2) { + $cssClass .= ' Announcement Announcement-Category'; + } elseif ($announce) { + $cssClass .= ' Announcement Announcement-Everywhere'; + } + + $cssClass .= ($row['Closed'] ?? '') == '1' ? ' Closed' : ''; + $cssClass .= ($row['Participated'] ?? '') == '1' ? ' Participated' : ''; + } + + $cssClass .= ($row['InsertUserID'] ?? false ) == $session->UserID ? ' Mine' : ''; + + if (array_key_exists('CountUnreadComments', $row) && $session->isValid()) { + $countUnreadComments = $row['CountUnreadComments']; + if ($countUnreadComments === true) { + $cssClass .= ' New'; + } elseif ($countUnreadComments == 0) { + $cssClass .= ' Read'; + } else { + $cssClass .= ' Unread'; + } + } elseif (($isRead = ($row['Read'] ?? null)) !== null) { + // Category list + $cssClass .= $isRead ? ' Read' : ' Unread'; + } + } + + // Comment list classes + if (array_key_exists('CommentID', $row)) { + $cssClass .= ' ItemComment'; + } elseif (array_key_exists('DiscussionID', $row)) { + $cssClass .= ' ItemDiscussion'; + } + + if (function_exists('IsMeAction')) { + $cssClass .= isMeAction($row) ? ' MeAction' : ''; + } + + if ($_CssClss = ($row['_CssClass'] ?? false)) { + $cssClass .= ' '.$_CssClss; + } + + // Insert User classes. + if ($userID = ($row['InsertUserID'] ?? false)) { + $user = Gdn::userModel()->getID($userID, DATASET_TYPE_ARRAY); + if ($_CssClss = ($user['_CssClass'] ?? false)) { + $cssClass .= ' '.$_CssClss; + } + } + + return trim($cssClass); + } +} + +if (!function_exists('dateUpdated')) { + /** + * + * + * @param $row + * @param null $wrap + * @return string + */ + function dateUpdated($row, $wrap = null) { + $result = ''; + $dateUpdated = val('DateUpdated', $row); + $updateUserID = val('UpdateUserID', $row); + + if ($dateUpdated) { + $updateUser = Gdn::userModel()->getID($updateUserID); + if ($updateUser) { + $title = sprintf(t('Edited %s by %s.'), Gdn_Format::dateFull($dateUpdated), val('Name', $updateUser)); + } else { + $title = sprintf(t('Edited %s.'), Gdn_Format::dateFull($dateUpdated)); + } + + $result = ' '. + sprintf(t('edited %s'), Gdn_Format::date($dateUpdated)). + ' '; + + if ($wrap) { + $result = $wrap[0].$result.$wrap[1]; + } + } + + return $result; + } +} + +if (!function_exists('anchor')) { + /** + * Builds and returns an anchor tag. + * + * @param $text + * @param string $destination + * @param string $cssClass + * @param array $attributes + * @param bool $forceAnchor + * @return string + */ + function anchor($text, $destination = '', $cssClass = '', $attributes = [], $forceAnchor = false) { + if (!is_array($cssClass) && $cssClass != '') { + $cssClass = ['class' => $cssClass]; + } + + if ($destination == '' && $forceAnchor === false) { + return $text; + } + + if (!is_array($attributes)) { + $attributes = []; + } + + $sSL = null; + if (isset($attributes['SSL'])) { + $sSL = $attributes['SSL']; + unset($attributes['SSL']); + } + + $withDomain = false; + if (isset($attributes['WithDomain'])) { + $withDomain = $attributes['WithDomain']; + unset($attributes['WithDomain']); + } + + $prefix = substr($destination, 0, 7); + if (!in_array($prefix, ['https:/', 'http://', 'mailto:']) && ($destination != '' || $forceAnchor === false)) { + $destination = Gdn::request()->url($destination, $withDomain, $sSL); + } + + return ''.$text.''; + } +} + +if (!function_exists('commentUrl')) { + /** + * Return a URL for a comment. This function is in here and not functions.general so that plugins can override. + * + * @param object|array $comment + * @param bool $withDomain + * @return string + */ + function commentUrl($comment, $withDomain = true) { + $comment = (object)$comment; + $result = "/discussion/comment/{$comment->CommentID}#Comment_{$comment->CommentID}"; + return url($result, $withDomain); + } +} + +if (!function_exists('discussionFilters')) { + /** + * Returns discussions filtering. + * + * @param string $extraClasses any extra classes you add to the drop down + * @return string + */ + function discussionFilters($extraClasses = '') { + if (!Gdn::session()->isValid()) { + return; + } + + $baseUrl = 'discussions'; + $transientKey = Gdn::session()->transientKey(); + $filters = [ + [ + 'name' => t('Following'), + 'param' => 'followed', + 'extra' => ['save' => 1, 'TransientKey' => $transientKey] + ] + ]; + + $defaultParams = ['save' => 1, 'TransientKey' => $transientKey]; + if (Gdn::request()->get('followed')) { + $defaultParams['followed'] = 0; + } + + if (!empty($defaultParams)) { + $defaultUrl = $baseUrl.'?'.http_build_query($defaultParams); + } else { + $defaultUrl = $baseUrl; + } + + return filtersDropDown( + $baseUrl, + $filters, + $extraClasses, + t('All'), + $defaultUrl, + 'View' + ); + } +} + +if (!function_exists('discussionUrl')) { + /** + * Return a URL for a discussion. This function is in here and not functions.general so that plugins can override. + * + * @param object|array $discussion + * @param int|string $page + * @param bool $withDomain + * @return string + */ + function discussionUrl($discussion, $page = '', $withDomain = true) { + $discussion = (object)$discussion; + $name = Gdn_Format::url($discussion->Name); + + // Disallow an empty name slug in discussion URLs. + if (empty($name)) { + $name = 'x'; + } + + $result = '/discussion/'.$discussion->DiscussionID.'/'.$name; + + if ($page) { + if ($page > 1 || Gdn::session()->UserID) { + $result .= '/p'.$page; + } + } + + return url($result, $withDomain); + } +} + +if (!function_exists('exportCSV')) { + /** + * Create a CSV given a list of column names & rows. + * + * @param array $columnNames + * @param array $data + */ + function exportCSV($columnNames, $data = []) { + $output = fopen("php://output",'w'); + header("Content-Type:application/csv"); + header("Content-Disposition:attachment;filename=profiles_export.csv"); + fputcsv($output, $columnNames); + foreach($data as $row) { + fputcsv($output, $row); + } + fclose($output); + } +} + +if (!function_exists('filtersDropDown')) { + /** + * Returns a filtering drop-down menu. + * + * @param string $baseUrl Target URL with no query string applied. + * @param array $filters A multidimensional array of rows with the following properties: + * ** 'name': Friendly name for the filter. + * ** 'param': URL parameter associated with the filter. + * ** 'value': A value for the URL parameter. + * @param string $extraClasses any extra classes you add to the drop down + * @param string|null $default The default label for when no filter is active. If `null`, the default label is "All". + * @param string|null $defaultURL URL override to return to the default, unfiltered state. + * @param string $label Text for the label to attach to the cont + * @return string + */ + function filtersDropDown($baseUrl, array $filters = [], $extraClasses = '', $default = null, $defaultUrl = null, $label = 'View') { + if ($default === null) { + $default = t('All'); + } + $output = ''; + + if (c('Vanilla.EnableCategoryFollowing')) { + $links = []; + $active = null; + + // Translate filters into links. + foreach ($filters as $filter) { + // Make sure we have the bare minimum: a label and a URL parameter. + if (!array_key_exists('name', $filter)) { + throw new InvalidArgumentException('Filter does not have a name field.'); + } + if (!array_key_exists('param', $filter)) { + throw new InvalidArgumentException('Filter does not have a param field.'); + } + + // Prepare for consumption by linkDropDown. + $value = val('value', $filter, 1); + $query = [$filter['param'] => $value]; + if (array_key_exists('extra', $filter) && is_array($filter['extra'])) { + $query += $filter['extra']; + } + $url = url($baseUrl.'?'.http_build_query($query)); + $link = [ + 'name' => $filter['name'], + 'url' => $url + ]; + + // If we don't already have an active link, and this parameter and value match, this is the active link. + if ($active === null && Gdn::request()->get($filter['param']) == $value) { + $active = $filter['name']; + $link['active'] = true; + } + + // Queue up another filter link. + $links[] = $link; + } + + // Add the default link to the top of the list. + array_unshift($links, [ + 'active' => $active === null, + 'name' => $default, + 'url' => $defaultUrl ?: $baseUrl + ]); + + // Generate the markup for the drop down menu. + $output = linkDropDown($links, 'selectBox-following '.trim($extraClasses), t($label).': '); + } + + return $output; + } +} + +if (!function_exists('fixnl2br')) { + /** + * Removes the break above and below tags that have a natural margin. + * + * @param string $text The text to fix. + * @return string + * @since 2.1 + * + * @deprecated 3.2 - Use \Vanilla\Formatting\Html\HtmlFormat::cleanupLineBreaks + */ + function fixnl2br($text) { + deprecated(__FUNCTION__, '\Vanilla\Formatting\Formats\HtmlFormat::cleanupLineBreaks'); + /** @var Formats\HtmlFormat $htmlFormat */ + $htmlFormat = Gdn::getContainer()->get(Formats\HtmlFormat::class); + return $htmlFormat->cleanupLineBreaks((string) $text); + } +} + +if (!function_exists('formatIP')) { + /** + * Format an IP address for display. + * + * @param string $iP An IP address to be formatted. + * @param bool $html Format as HTML. + * @return string Returns the formatted IP address. + */ + function formatIP($iP, $html = true) { + $result = ''; + + // Is this a packed IP address? + if (!filter_var($iP, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4|FILTER_FLAG_IPV6) && $unpackedIP = @inet_ntop($iP)) { + $iP = $unpackedIP; + } + + if (filter_var($iP, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + $result = $html ? htmlspecialchars($iP) : $iP; + } elseif (filter_var($iP, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + $result = $html ? wrap(t('IPv6'), 'span', ['title' => $iP]) : $iP; + } + + return $result; + } +} + +if (!function_exists('formatPossessive')) { + /** + * Format a word using English "possessive" formatting. + * + * This can be overridden in language definition files like: + * + * ``` + * /applications/garden/locale/en-US.php. + * ``` + */ + function formatPossessive($word) { + if (function_exists('formatPossessiveCustom')) { + return formatPossesiveCustom($word); + } + + return substr($word, -1) == 's' ? $word."'" : $word."'s"; + } +} + +if (!function_exists('formatRssHtmlCustom')) { + /** + * @param string $html + * @return string Returns the filtered RSS. + */ + function formatRssHtmlCustom($html) { + return Htmlawed::filterRSS($html); + } +} + +if (!function_exists('formatUsername')) { + /** + * + * + * @param $user + * @param $format + * @param bool $viewingUserID + * @return mixed|string + */ + function formatUsername($user, $format, $viewingUserID = false) { + if ($viewingUserID === false) { + $viewingUserID = Gdn::session()->UserID; + } + $userID = val('UserID', $user); + $name = val('Name', $user); + $gender = strtolower(val('Gender', $user)); + + $uCFirst = substr($format, 0, 1) == strtoupper(substr($format, 0, 1)); + + switch (strtolower($format)) { + case 'you': + if ($viewingUserID == $userID) { + return t("Format $format", $format); + } + return $name; + case 'his': + case 'her': + case 'your': + if ($viewingUserID == $userID) { + return t("Format Your", 'Your'); + } else { + switch ($gender) { + case 'm': + $format = 'his'; + break; + case 'f': + $format = 'her'; + break; + default: + $format = 'their'; + break; + } + if ($uCFirst) { + $format = ucfirst($format); + } + return t("Format $format", $format); + } + break; + default: + return $name; + } + } +} + +if (!function_exists('hasEditProfile')) { + /** + * Determine whether or not a given user has the edit profile link. + * + * @param int $userID The user ID to check. + * @return bool Return true if the user should have the edit profile link or false otherwise. + */ + function hasEditProfile($userID) { + if (checkPermission(['Garden.Users.Edit', 'Moderation.Profiles.Edit'])) { + return true; + } + if ($userID != Gdn::session()->UserID) { + return false; + } + + $result = checkPermission('Garden.Profiles.Edit') && c('Garden.UserAccount.AllowEdit'); + + $result = $result && ( + c('Garden.Profile.Titles') || + c('Garden.Profile.Locations', false) || + c('Garden.Registration.Method') != 'Connect' + ); + + return $result; + } +} + +if (!function_exists('hasViewProfile')) { + /** + * Determine whether or not a given user has the view profile link. + * + * @param int $userID The user ID to check. + * @return bool Return true if the user should have the view profile link or false otherwise. + */ + function hasViewProfile($userID) { + if ($userID != Gdn::session()->UserID) { + return false; + } + + $result = checkPermission('Garden.Profiles.View'); + + $result = $result && ( + c('Garden.Profile.Titles') || + c('Garden.Profile.Locations', false) || + c('Garden.Registration.Method') != 'Connect' + ); + + return $result; + } +} + +if (!function_exists('hoverHelp')) { + /** + * Add span with hover text to a string. + * + * @param string $string + * @param string $help + * @return string + */ + function hoverHelp($string, $help) { + return wrap($string.wrap($help, 'span', ['class' => 'Help']), 'span', ['class' => 'HoverHelp']); + } +} + +if (!function_exists('img')) { + /** + * Returns an img tag. + * + * @param string $image + * @param string $attributes + * @param bool|false $withDomain + * @return string + */ + function img($image, $attributes = '', $withDomain = false) { + if ($attributes != '') { + $attributes = attribute($attributes); + } + + if (!isUrl($image)) { + $image = smartAsset($image, $withDomain); + } + + return ''; + } +} + +if (!function_exists('inCategory')) { + /** + * Return whether or not the page is in a given category. + * + * @param string $category The url code of the category. + * @return boolean + * @since 2.1 + */ + function inCategory($category) { + $breadcrumbs = (array)Gdn::controller()->data('Breadcrumbs', []); + + foreach ($breadcrumbs as $breadcrumb) { + if (isset($breadcrumb['CategoryID']) && strcasecmp($breadcrumb['UrlCode'], $category) == 0) { + return true; + } + } + + return false; + } +} + +if (!function_exists('inSection')) { + /** + * Returns whether or not the page is in one of the given section(s). + * + * @param string|array $section + * @return bool + * @since 2.1 + */ + function inSection($section) { + return Gdn_Theme::inSection($section); + } +} + +if (!function_exists('ipAnchor')) { + /** + * Returns an IP address with a link to the user search. + * + * @param string $iP + * @param string $cssClass + * @return string + */ + function ipAnchor($iP, $cssClass = '') { + if ($iP) { + return anchor(formatIP($iP), '/user/browse?keywords='.urlencode(ipDecode($iP)), $cssClass); + } else { + return $iP; + } + } +} + +if (!function_exists('linkDropDown')) { + /** + * Write a link drop down control. + * + * @param array $links + * Has the following properties: + * ** 'url': string: The url for the link + * ** 'name': string: The text for the link + * ** 'active': boolean: is it the current page + * @param string $extraClasses any extra classes you add to the drop down + * @param string $label the label of the drop down + * + */ + function linkDropDown($links, $extraClasses = '', $label) { + $output = ''; + $selectedKey = 0; + foreach($links as $i => $link) { + if (val('active', $link)) { + $selectedKey = $i; + break; + } + } + $selectedLink = val($selectedKey, $links); + $extraClasses = trim($extraClasses); + $linkName = val('name', $selectedLink); + + $output .= << + {$label} + + + {$linkName} + + +