From 5114e0eaf5b90084b793fc7b9180db3c9073c135 Mon Sep 17 00:00:00 2001 From: Bogdanova Olga Date: Sun, 28 Feb 2021 14:27:13 +0300 Subject: [PATCH 1/2] Issues-419: adding non-link menu item in dropdown --- .../library/class.nestedcollection.php | 764 ++++++++++++++++++ .../dashboard/views/modules/dropdown.php | 69 ++ vanilla/js/flyouts.js | 331 ++++++++ 3 files changed, 1164 insertions(+) create mode 100644 vanilla/applications/dashboard/library/class.nestedcollection.php create mode 100644 vanilla/applications/dashboard/views/modules/dropdown.php create mode 100644 vanilla/js/flyouts.js diff --git a/vanilla/applications/dashboard/library/class.nestedcollection.php b/vanilla/applications/dashboard/library/class.nestedcollection.php new file mode 100644 index 0000000..5980805 --- /dev/null +++ b/vanilla/applications/dashboard/library/class.nestedcollection.php @@ -0,0 +1,764 @@ + + * @copyright 2015 Vanilla Forums, Inc + * @license http://www.opensource.org/licenses/gpl-2.0.php GPL + * @since 2.3 + */ + +trait NestedCollection { + + /** + * @var string The css class to add to active items and groups. + */ + private $activeCssClass = 'active'; + + /** + * @var array List of items to sort. + */ + private $items = []; + + /** + * @var int Index number to start the item* key-generation with. + */ + private $keyNumber = 1; + + /** + * @var bool Whether to use CSS prefixes on the generated CSS classes for the items. + */ + private $useCssPrefix = false; + + /** + * @var string CSS prefix for a header item. + */ + private $headerCssClassPrefix = 'header'; + + /** + * @var string CSS prefix for a link item. + */ + private $linkCssClassPrefix = 'link'; + + /** + * @var string CSS prefix for a divider item. + */ + private $dividerCssClassPrefix = 'divider'; + + /** + * @var bool Whether to flatten the list (as with a dropdown menu) or allow nesting (as with a nav). + */ + private $flatten = false; + + /** + * @var bool Whether to separate groups with a hr element. Only supported for flattened lists. + */ + private $forceDivider = true; + + /** + * @var array The allowed keys in the $modifiers array parameter in the 'addItem' methods. + */ + private $isPrepared = false; + + /** + * @var string The url to the display as active. + */ + private $highlightRoute = ''; + + /** + * @var array The item modifiers allowed to be passed in the modifiers array. + */ + private $allowedItemModifiers = ['popinRel', 'icon', 'badge', 'rel', 'description', 'attributes', 'listItemCssClasses']; + + /** + * @param boolean $forceDivider Whether to separate groups with a
element. Only supported for flattened lists. + * @return $this + */ + public function setForceDivider($forceDivider) { + $this->forceDivider = $forceDivider; + return $this; + } + + /** + * @return string + */ + public function getActiveCssClass() { + return $this->activeCssClass; + } + + /** + * @param string $activeCssClass + * @return $this + */ + public function setActiveCssClass($activeCssClass) { + $this->activeCssClass = $activeCssClass; + return $this; + } + + /** + * @param boolean $useCssPrefix + * @return $this + */ + public function useCssPrefix($useCssPrefix) { + $this->useCssPrefix = $useCssPrefix; + return $this; + } + + /** + * @return string + */ + public function getHeaderCssClassPrefix() { + return $this->headerCssClassPrefix; + } + + /** + * @param string $headerCssClassPrefix + * @return $this + */ + public function setHeaderCssClassPrefix($headerCssClassPrefix) { + $this->headerCssClassPrefix = $headerCssClassPrefix; + return $this; + } + + /** + * @return string + */ + public function getLinkCssClassPrefix() { + return $this->linkCssClassPrefix; + } + + /** + * @param string $linkCssClassPrefix + * @return $this + */ + public function setLinkCssClassPrefix($linkCssClassPrefix) { + $this->linkCssClassPrefix = $linkCssClassPrefix; + return $this; + } + + /** + * @return string + */ + public function getDividerCssClassPrefix() { + return $this->dividerCssClassPrefix; + } + + /** + * @param string $dividerCssClassPrefix + * @return $this + */ + public function setDividerCssClassPrefix($dividerCssClassPrefix) { + $this->dividerCssClassPrefix = $dividerCssClassPrefix; + return $this; + } + + /** + * @param boolean $flatten + * @return $this + */ + public function setFlatten($flatten) { + $this->flatten = $flatten; + return $this; + } + + /** + * @return string + */ + public function getHighlightRoute() { + return $this->highlightRoute; + } + + /** + * @param string $highlightRoute + * @return $this + */ + public function setHighlightRoute($highlightRoute) { + $this->highlightRoute = $highlightRoute; + return $this; + } + + /** + * @return array + */ + public function getItems() { + return $this->items; + } + + /** + * @param array $items + * @return $this + */ + public function setItems($items) { + $this->items = $items; + return $this; + } + + /** + * Add a divider to the items array if it satisfies the $isAllowed condition. + * + * @param bool|string|array $isAllowed Either a boolean to indicate whether to actually add the item + * or a permission string or array of permission strings (full match) to check. + * @param string $key The item's key (for sorting and CSS targeting). + * @param array|int $sort Either a numeric sort position or and array in the style: array('before|after', 'key'). + * @param string $cssClass The divider's CSS class. + * @return object $this The calling object. + * @throws Exception + */ + public function addDividerIf($isAllowed = true, $key = '', $cssClass = '', $sort = []) { + if (!$this->isAllowed($isAllowed)) { + return $this; + } else { + return $this->addDivider($key, $cssClass, $sort); + } + } + + /** + * Add a divider to the items array. + * + * @param string $key The item's key (for sorting and CSS targeting). + * @param array|int $sort Either a numeric sort position or and array in the style: array('before|after', 'key'). + * @param string $cssClass The divider's CSS class. + * @return object $this The calling object. + * @throws Exception + */ + public function addDivider($key = '', $cssClass = '', $sort = []) { + $divider = ['key' => $key]; + if ($sort) { + $divider['sort'] = $sort; + } + + $this->touchKey($divider); + $divider['cssClass'] = $cssClass.' '.$this->buildCssClass($this->dividerCssClassPrefix, $divider); + + $this->addItem('divider', $divider); + return $this; + } + + /** + * Add a group to the items array if it satisfies the $isAllowed condition. + * + * @param bool|string|array $isAllowed Either a boolean to indicate whether to actually add the item + * or a permission string or array of permission strings (full match) to check. + * @param string $text The display text for the group header. + * @param string $key The item's key (for sorting and CSS targeting). + * @param string $cssClass The group header's CSS class. + * @param array|int $sort Either a numeric sort position or and array in the style: array('before|after', 'key'). + * @param array $modifiers List of attribute => value, where the attribute is in $this->allowedItemModifiers. + * - **popinRel**: string - Endpoint for a popin. + * - **badge**: string - Info to put into a badge, usually a number. + * - **icon**: string - Name of the icon for the item, excluding the 'icon-' prefix. + * @return object $this The calling object. + * @throws Exception + */ + public function addGroupIf($isAllowed = true, $text = '', $key = '', $cssClass = '', $sort = [], $modifiers = []) { + if (!$this->isAllowed($isAllowed)) { + return $this; + } else { + return $this->addGroup($text, $key, $cssClass, $sort, $modifiers); + } + } + + + /** + * Checks whether an item can be added to the items list by returning it if it is already a boolean, + * or checking the permission if it is a string or array. + * + * @param bool|string|array $isAllowed Either a boolean to indicate whether to actually add the item + * or a permission string or array of permission strings (full match) to check. + * @return bool Whether the item has permission to be added to the items list. + */ + protected function isAllowed($isAllowed) { + if (is_bool($isAllowed)) { + return $isAllowed; + } + if (is_string($isAllowed) || is_array($isAllowed)) { + return Gdn::session()->checkPermission($isAllowed); + } + return false; + } + + /** + * Add a group to the items array. + * + * @param string $text The display text for the group header. + * @param string $key The item's key (for sorting and CSS targeting). + * @param string $cssClass The group header's CSS class. + * @param array|int $sort Either a numeric sort position or and array in the style: array('before|after', 'key'). + * @param array $modifiers List of attribute => value, where the attribute is in $this->allowedItemModifiers. + * - **popinRel**: string - Endpoint for a popin. + * - **badge**: string - Info to put into a badge, usually a number. + * - **icon**: string - Name of the icon for the item, excluding the 'icon-' prefix. + * @return SortableModule $this The calling object. + * @throws Exception + */ + public function addGroup($text = '', $key = '', $cssClass = '', $sort = [], $modifiers = []) { + $group = [ + 'text' => $text, + 'key' => $key, + 'cssClass' => $cssClass + ]; + + if ($sort) { + $group['sort'] = $sort; + } + + if (!empty($modifiers)) { + $this->addItemModifiers($group, $modifiers); + } + + $this->touchKey($group); + + if ($text) { + $group['headerCssClass'] = $cssClass.' '.$this->buildCssClass($this->headerCssClassPrefix, $group); + } + $this->addItem('group', $group); + return $this; + } + + /** + * Add a link to the items array if it satisfies the $isAllowed condition. + * + * @param bool|string|array $isAllowed Either a boolean to indicate whether to actually add the item + * or a permission string or array of permission strings (full match) to check. + * @param string $text The display text for the link. + * @param string $url The destination url for the link. + * @param string $key The item's key (for sorting and CSS targeting). + * @param string $cssClass The link's CSS class. + * @param array|int $sort Either a numeric sort position or and array in the style: array('before|after', 'key'). + * @param array $modifiers List of attribute => value, where the attribute is in $this->allowedItemModifiers. + * - **popinRel**: string - Endpoint for a popin. + * - **badge**: string - Info to put into a badge, usually a number. + * - **icon**: string - Name of the icon for the item, excluding the 'icon-' prefix. + * @param bool $disabled Whether to disable the link. + * @return object $this The calling object. + */ + public function addLinkIf($isAllowed = true, $text, $url, $key = '', $cssClass = '', $sort = [], $modifiers = [], $disabled = false) { + if (!$this->isAllowed($isAllowed)) { + return $this; + } else { + return $this->addLink($text, $url, $key, $cssClass, $sort, $modifiers, $disabled); + } + } + + /** + * Add a link to the items array. + * + * @param string $text The display text for the link. + * @param string $url The destination url for the link. + * @param string $key The item's key (for sorting and CSS targeting). + * @param string $cssClass The link's CSS class. + * @param array|int $sort Either a numeric sort position or and array in the style: array('before|after', 'key'). + * @param array $modifiers List of attribute => value, where the attribute is in $this->allowedItemModifiers. + * - **popinRel**: string - Endpoint for a popin. + * - **badge**: string - Info to put into a badge, usually a number. + * - **icon**: string - Name of the icon for the item, excluding the 'icon-' prefix. + * - **listItemCssClasses**: array - Array of class names to be applied to the list item. + * @param bool $disabled Whether to disable the link. + * @return $this The calling object. + * @throws Exception + */ + public function addLink($text, $url, $key = '', $cssClass = '', $sort = [], $modifiers = [], $disabled = false) { + $link = [ + 'text' => $text, + 'url' => $url, + 'key' => $key, + ]; + + if ($sort) { + $link['sort'] = $sort; + } + + if (!empty($modifiers)) { + $this->addItemModifiers($link, $modifiers); + } + + $this->touchKey($link); + $link['cssClass'] = $cssClass.' '.$this->buildCssClass($this->linkCssClassPrefix, $link); + + $listItemCssClasses = $modifiers['listItemCssClasses'] ?? []; + if ($disabled) { + $listItemCssClasses[] = 'disabled'; + } + if ($this->isActive($link)) { + $link['isActive'] = true; + $listItemCssClasses[] = $this->activeCssClass; + } else { + $link['isActive'] = false; + } + + $link['listItemCssClass'] = implode(' ', $listItemCssClasses); + $this->addItem('link', $link); + return $this; + } + + public function addText($text, $key = '', $cssClass = '', $sort = [], $modifiers = []) { + $link = [ + 'text' => $text, + 'key' => $key, + ]; + + if ($sort) { + $link['sort'] = $sort; + } + + if (!empty($modifiers)) { + $this->addItemModifiers($link, $modifiers); + } + + $this->touchKey($link); + $link['cssClass'] = $cssClass.' '.$this->buildCssClass($this->linkCssClassPrefix, $link); + + $listItemCssClasses = $modifiers['listItemCssClasses'] ?? []; + + $link['listItemCssClass'] = implode(' ', $listItemCssClasses); + $this->addItem('link', $link); + return $this; + } + + /** + * Adds the attributes in the modifiers array to the item. + * Constrains the modifier to those defined in $this->allowedItemModifiers. + * + * @param array $item The item to modify. + * @param array $modifiers The modifiers to add to the item. + */ + private function addItemModifiers(&$item, $modifiers) { + $modifiers = array_intersect_key($modifiers, array_flip($this->allowedItemModifiers)); + foreach ($modifiers as $attribute => $value) { + $item[$attribute] = $value; + } + } + + /** + * Generate a key for an item if one does not exist, and add the property to the item. + * + * @param array $item The item to generate and add a key for. + */ + private function touchKey(&$item) { + if (!val('key', $item)) { + $item['key'] = 'item'.$this->keyNumber; + $this->keyNumber = $this->keyNumber + 1; + } + } + + /** + * Add an item to the items array. + * + * @param string $type The type of the item: link, group or divider. + * @param array $item The item to add to the array. + * @throws Exception + */ + private function addItem($type, array $item) { + $this->touchKey($item); + $key = $item['key'] ?? false; + if (!is_array($key)) { + $item['key'] = explode('.', $key); + } else { + $item['key'] = array_values($key); + } + + $item = (array)$item; + + // Make sure the link has its type. + $item['type'] = $type; + + // Walk into the items list to set the item. + $items =& $this->items; + foreach ($item['key'] as $i => $key_part) { + + if ($i === count($item['key'] ?? false) - 1) { + // Add the item here. + if (array_key_exists($key_part, $items)) { + // The item is already here so merge this one on top of it. + if ($items[$key_part]['type'] !== $type) + throw new \Exception(($item['key'] ?? '')." of type $type does not match existing type {$items[$key_part]['type']}.", 500); + + $items[$key_part] = array_merge($items[$key_part], $item); + } else { + // The item is new so just add it here. + touchValue('_sort', $item, count($items)); + $items[$key_part] = $item; + } + } else { + // This is a group. + if (!array_key_exists($key_part, $items)) { + // The group doesn't exist so lazy-create it. + $items[$key_part] = ['type' => 'group', 'text' => '', 'items' => [], '_sort' => count($items)]; + } elseif ($items[$key_part]['type'] !== 'group') { + throw new \Exception("$key_part is not a group", 500); + } elseif (!array_key_exists('items', $items[$key_part])) { + // Lazy create the items array. + $items[$key_part]['items'] = []; + } + $items =& $items[$key_part]['items']; + } + } + } + + /** + * Remove an item from the nested set. + * + * @param string $key The key of the item to remove, separated by dots. + */ + public function removeItem($key) { + $parts = explode('.', $key); + + $arr = &$this->items; + foreach ($parts as $i => $part) { + if (array_key_exists($part, $arr)) { + if ($i + 1 === count($parts)) { + unset($arr[$part]); + } else { + $arr = &$arr[$part]; + } + } else { + // The key wasn't found so short circuit. + return; + } + } + } + + /** + * Builds a CSS class for an item, based on the 'key' property of the item. + * Optionally prepends a prefix to generated class names. + * + * @param string $prefix The optional prefix to add to class name. + * @param array $item The item to generate CSS class for. + * @return string The generated CSS class. + */ + private function buildCssClass($prefix, array $item = []) { + $result = ''; + if ($prefix) { + $prefix .= '-'; + } + if (!$this->useCssPrefix) { + $prefix = ''; + } + if ($cssClass = ($item['key'] ?? false)) { + if (is_array($cssClass)) { + $result .= $prefix.implode('-', $cssClass); + } + else { + $result .= $prefix.str_replace('.', '-', $cssClass); + } + } + return trim($result); + } + + /** + * Checks whether the current request url matches an item's link url. + * + * @param array $item The item to check. + * @return bool Whether the current request url matches an item's link url. + */ + private function isActive($item) { + if (empty($this->highlightRoute)) { + $highlightRoute = Gdn_Url::request(true); + } else { + $highlightRoute = url($this->highlightRoute); + } + $url = $item['url'] ?? false; + if ($url) { + $result = trim(url($url), '/') == trim($highlightRoute, '/'); + } else { + $result = false; + } + return $result; + } + + /** + * Recursive function to sort the items in a given array. + * + * @param array &$items The items to sort. + */ + protected function sortItems(&$items) { + $i = 0; + foreach ($items as &$item) { + $item += ['_sort' => $i++]; + if ($item['items'] ?? false) { + $this->sortItems($item['items']); + } + } + + uasort($items, function ($a, $b) use ($items) { + $sort_a = $this->sortItemsOrder($a, $items); + $sort_b = $this->sortItemsOrder($b, $items); + + if ($sort_a > $sort_b) { + return 1; + } elseif ($sort_a < $sort_b) { + return -1; + } else { + return 0; + } + }); + } + + /** + * Get the sort order of an item in the items array. + * + * This function looks at the following keys: + * - **sort (numeric)**: A specific numeric sort was provided. + * - **sort array('before|after', 'key')**: You can specify that the item is before or after another item. + * - **_sort**: The order the item was added is used. + * + * @param array $item The item to get the sort order from. + * @param array $items The entire list of items. + * @param int $depth The current recursive depth used to prevent infinite recursion. + * @return number + */ + private function sortItemsOrder($item, $items, $depth = 0) { + $default_sort = val('_sort', $item, 100); + + // Check to see if a custom sort has been specified. + if (isset($item['sort'])) { + if (is_numeric($item['sort'])) { + // This is a numeric sort + return $item['sort'] * 10000 + $default_sort; + } elseif (is_array($item['sort']) && !empty($item['sort']) && $depth < 10) { + // This sort is before or after another depth. + $op = array_keys($item['sort'])[0]; + $key = $item['sort'][$op]; + + if (array_key_exists($key, $items)) { + switch ($op) { + case 'after': + return $this->sortItemsOrder($items[$key], $items, $depth + 1) + 1000; + case 'before': + default: + return $this->sortItemsOrder($items[$key], $items, $depth + 1) - 1000; + } + } + } + } + return $default_sort * 10000 + $default_sort; + } + + /** + * Prepares the items array for output by sorting and optionally flattening. + * + * @return bool Whether to render the module. + */ + public function prepare() { + if ($this->isPrepared) { + return !empty($this->items); + } + $this->isPrepared = true; + $this->sortItems($this->items); + $this->prepareData($this->items); + if ($this->flatten) { + $this->items = $this->flattenArray($this->items); + } + return !empty($this->items); + } + + /** + * Performs post-sort operations to the items array. + * Removes empty groups, removes the '_sort' and 'key' attributes and bubbles up the active css class. + * + * @param array $items The item list to parse. + */ + private function prepareData(&$items) { + foreach($items as $key => &$item) { + unset($item['_sort'], $item['key']); + $subItems = false; + + // Group item + if (val('type', $item) === 'group') { + // ensure groups have items + if (val('items', $item)) { + $subItems = true; + } else { + unset($items[$key]); + } + } + + if ($subItems) { + $this->prepareData($item['items']); + // Set active state on parents if child has it + if (!$this->flatten) { + foreach ($item['items'] as $subItem) { + if (val('isActive', $subItem)) { + $item['isActive'] = true; + $item['cssClass'] .= ' '.$this->activeCssClass; + } + } + } + } + } + } + + /** + * Recursive utility function to support returning this object as an array. + * + * @param $obj The object to transform. + * @param array $blackList Blacklisted property names. + * @param array $whiteList Whitelisted property names. If set, only whitelisted properties will appear in the result. + * @return array An array transformation of this object. + */ + private function objectToArray($obj, array $blackList = [], array $whiteList = []) { + if (is_array($obj) || is_object($obj)) { + $result = []; + foreach ($obj as $key => $value) { + if (!in_array($key, $blackList) && (empty($whiteList) || in_array($key, $whiteList))) { + $result[$key] = $this->objectToArray($value); + } + } + return $result; + } + return $obj; + } + + /** + * Copies the object to an array. A simple (array) typecast won't work, + * since the properties are private and as such, add unwanted information to the array keys. + * + * @param array $blackList Blacklisted property names. + * @param array $whiteList Whitelisted property names. If set, only whitelisted properties will appear in the result. + * @return array Copy of this object in an array format. + */ + public function toArray(array $blackList = [], array $whiteList = []) { + $blackList[] = '_Sender'; + return $this->objectToArray($this, $blackList, $whiteList); + } + + /** + * Creates a flattened array of menu items. + * Useful for lists like dropdown menu, where nesting lists is not necessary. + * + * @param array $items The item list to flatten. + * @return array The flattened items list. + */ + private function flattenArray(array $items) { + $newItems = []; + $itemslength = sizeof($items); + $index = 0; + foreach($items as $key => $item) { + $subItems = []; + + // Group item + if (($item['type'] ?? '') == 'group') { + if (($item['items'] ?? false)) { + $subItems = $item['items']; + unset($item['items']); + if (($item['text'] ?? false)) { + $newItems[] = $item; + } + } + } + if (($item['type'] ?? '') != 'group') { + $newItems[] = $item; + } + if ($subItems) { + $newItems = array_merge($newItems, $this->flattenArray($subItems)); + if ($this->forceDivider && $index + 1 < $itemslength) { + // Add hr after group but not the last one + $newItems[] = ['type' => 'divider']; + } + } + $index++; + } + return $newItems; + } +} diff --git a/vanilla/applications/dashboard/views/modules/dropdown.php b/vanilla/applications/dashboard/views/modules/dropdown.php new file mode 100644 index 0000000..7b90816 --- /dev/null +++ b/vanilla/applications/dashboard/views/modules/dropdown.php @@ -0,0 +1,69 @@ +getTrigger(); +?> + + + + + + data('DashboardCount', '')) ? wrap($dropdown->data('DashboardCount', ''), 'span', ['class' => 'Alert']) : ''; + echo anchor($icon.$text.$alert, $url, $cssClass, $attributes); + endif; ?> + + diff --git a/vanilla/js/flyouts.js b/vanilla/js/flyouts.js new file mode 100644 index 0000000..8fd72fc --- /dev/null +++ b/vanilla/js/flyouts.js @@ -0,0 +1,331 @@ +/** + * Legacy flyout code extracted from global.js + * + * @copyright 2009-2018 Vanilla Forums Inc. + * @license GPL-2.0-only + */ + +/** + * IFFE for flyout code. + * + * @param {Window} window + * @param {jQuery} $ + */ +(function(window, $) { + var USE_NEW_FLYOUTS = gdn.getMeta("useNewFlyouts", false); + var OPEN_CLASS = "Open"; + + /** + * Content load handler, which is fired on first load, and when additional content is loaded in. + */ + $(document).on("contentLoad", function(e) { + kludgeFlyoutHTML(); + }); + + /** + * Document ready handler. Runs only the first time the page is loaded. + */ + $(function() { + $(document).delegate(".Hijack, .js-hijack", "click", handleHijackClick); + $(document).delegate(".ButtonGroup > .Handle", "click", handleButtonHandleClick); + $(document).delegate(".ToggleFlyout", "click", handleToggleFlyoutClick); + $(document).delegate(".ToggleFlyout a, .Dropdown a", "mouseup", handleToggleFlyoutMouseUp); + $(document).delegate(".mobileFlyoutOverlay", "click", function (e) { + e.preventDefault(); + e.stopPropagation(); + closeAllFlyouts(); + }); + $(document).delegate(".Flyout, .Dropdown", "click", function (e) { + e.stopPropagation(); + }); + $(document).on("click", function (e) { + closeAllFlyouts(); + }) + }); + + /** + * Workarounds for limitations of flyout's HTML structure. + */ + function kludgeFlyoutHTML() { + var $handles = $(".ToggleFlyout, .editor-dropdown, .ButtonGroup"); + + $handles.each(function() { + $handles + .find(".FlyoutButton, .Button-Options, .Handle, .editor-action:not(.editor-action-separator)") + .each(function() { + $(this) + .attr("tabindex", "0") + .attr("role", "button") + .attr("aria-haspopup", "true"); + + $(this).accessibleFlyoutHandle(false); + }); + + $handles.find(".Flyout, .Dropdown").each(function() { + $(this).accessibleFlyout(false); + + $(this) + .find("a") + .each(function() { + $(this).attr("tabindex", "0"); + }); + }); + }); + + if (USE_NEW_FLYOUTS) { + var $contents = $(".Flyout, .ButtonGroup .Dropdown"); + var wrap = document.createElement("span"); + wrap.classList.add("mobileFlyoutOverlay"); + + $contents.each(function() { + var $item = $(this); + if (!this.parentElement.classList.contains("mobileFlyoutOverlay")) { + $item.wrap(wrap); + } + + // Some flyouts had conflicting inline display: none directly in the view. + // We don't change that on open/close with the new style anymore so let's clean it up here. + $item.removeAttr("style"); + }); + } + + // Button accessibility + $(document).delegate("[role=button]", "keydown", function(event) { + var $button = $(this); + var ENTER_KEY = 13; + var SPACE_KEY = 32; + var isActiveElement = document.activeElement === $button[0]; + var isSpaceOrEnter = event.keyCode === ENTER_KEY || event.keyCode === SPACE_KEY; + if (isActiveElement && isSpaceOrEnter) { + event.preventDefault(); + $button.click(); + } + }); + } + + var BODY_CLASS = "flyoutIsOpen"; + + /** + * Close all flyouts and open the specified one. + * + * @param {JQuery} $toggleFlyout The flyout handle + * @param {JQuery} $flyout The flyout body. + */ + function openFlyout($toggleFlyout, $flyout) { + closeAllFlyouts(); + + $toggleFlyout + .addClass(OPEN_CLASS) + .closest(".Item") + .addClass(OPEN_CLASS); + + if (!USE_NEW_FLYOUTS) { + $flyout.show(); + } + $toggleFlyout.setFlyoutAttributes(); + document.body.classList.add(BODY_CLASS); + $toggleFlyout.trigger('OpenFlyout', [$toggleFlyout]); + } + + /** + * Close the specified flyout. + * + * @param {JQuery} $toggleFlyout The flyout handle + * @param {JQuery} $flyout The flyout body. + */ + function closeFlyout($toggleFlyout, $flyout) { + if (!USE_NEW_FLYOUTS) { + $flyout.hide(); + } + $toggleFlyout + .removeClass(OPEN_CLASS) + .closest(".Item") + .removeClass(OPEN_CLASS); + $toggleFlyout.setFlyoutAttributes(); + document.body.classList.remove(BODY_CLASS); + $toggleFlyout.trigger('CloseFlyout', [ $toggleFlyout]); + } + + /** + * Close all flyouts, including ButtonGroups. + */ + function closeAllFlyouts(e) { + closeFlyout($(".ToggleFlyout"), $(".Flyout")); + // Clear the button groups that are open as well. + $(".ButtonGroup") + .removeClass(OPEN_CLASS) + .setFlyoutAttributes(); + + // Kludge for legacy editor. + $(".editor-dropdown-open") + .removeClass("editor-dropdown-open") + .setFlyoutAttributes(); + document.body.classList.remove(BODY_CLASS); + } + + window.closeAllFlyouts = closeAllFlyouts; + + /** + * Take over the clicking of an element in order to make a post request. + * + * @param {MouseEvent} e The click event. + */ + function handleHijackClick(e) { + var $elem = $(this); + var $parent = $(this).closest(".Item"); + var $toggleFlyout = $elem.closest(".ToggleFlyout"); + var href = $elem.attr("href"); + var progressClass = $elem.hasClass("Bookmark") ? "Bookmarking" : "InProgress"; + + // If empty, or starts with a fragment identifier, do not send + // an async request. + if (!href || href.trim().indexOf("#") === 0) return; + gdn.disable(this, progressClass); + e.stopPropagation(); + + $.ajax({ + type: "POST", + url: href, + data: { DeliveryType: "VIEW", DeliveryMethod: "JSON", TransientKey: gdn.definition("TransientKey") }, + dataType: "json", + complete: function() { + gdn.enable($elem.get(0)); + $elem.removeClass(progressClass); + $elem.attr("href", href); + $flyout = $toggleFlyout.find(".Flyout"); + closeFlyout($toggleFlyout, $flyout); + }, + error: function(xhr) { + gdn.informError(xhr); + }, + success: function(json) { + if (json === null) json = {}; + + var informed = gdn.inform(json); + gdn.processTargets(json.Targets, $elem, $parent); + // If there is a redirect url, go to it. + if (json.RedirectTo) { + setTimeout(function() { + window.location.replace(json.RedirectTo); + }, informed ? 3000 : 0); + } + }, + }); + + return false; + } + + /** + * Close existing flyouts and dropdowns and open the dropdown for a particular button handle. + */ + function handleButtonHandleClick() { + var $buttonGroup = $(this).closest(".ButtonGroup"); + var $isOpen = $buttonGroup.hasClass(OPEN_CLASS); + closeAllFlyouts(); + if (!$isOpen) { + // Open this one + $buttonGroup.addClass(OPEN_CLASS).setFlyoutAttributes(); + } + return false; + } + + /** + * Handle clicks on the flyout. + * + * @param {MouseEvent} e The click event to handle. + */ + function handleToggleFlyoutClick(e) { + var $toggleFlyout = $(this); + var $flyout = $(".Flyout", this); + var isHandle = false; + + if ($(e.target).closest(".Flyout").length === 0) { + isHandle = true; + e.stopPropagation(); + } else if ( + $(e.target).hasClass("Hijack") || + $(e.target) + .closest("a") + .hasClass("Hijack") + ) { + return; + } + e.stopPropagation(); + $toggleFlyout.fillFlyoutDynamically(); + + // The old check. + var isFlyoutClosed = $flyout.css("display") == "none"; + if (USE_NEW_FLYOUTS) { + // The new check. + isFlyoutClosed = !$toggleFlyout.hasClass(OPEN_CLASS); + } + + // Toggling. + if (isFlyoutClosed) { + openFlyout($toggleFlyout, $flyout); + } else { + closeFlyout($toggleFlyout, $flyout); + } + + if (isHandle) return false; + } + + /** + * Close all of the flyouts unless we are clicking on a button inside of a flyout. + */ + function handleToggleFlyoutMouseUp() { + if ($(this).hasClass("FlyoutButton")) return; + closeAllFlyouts(); + } + + /** + * jQuery function extensions + */ + $.fn.extend({ + fillFlyoutDynamically: function() { + var rel = $(this).attr("rel"); + if (rel) { + $flyout = $(this).find(".Flyout"); + + // Clear the rel and set a progress indicator. + $(this).attr("rel", ""); + $flyout.html('
'); + + // Fetch the contents dynamically and fill on contents of the flyout. + $.ajax({ + url: gdn.url(rel), + data: { DeliveryType: "VIEW" }, + success: function(data) { + $flyout.html(data); + }, + error: function(xhr) { + $flyout.html(""); + gdn.informError(xhr, true); + }, + }); + } + }, + accessibleFlyoutHandle: function(isOpen) { + $(this).attr("aria-expanded", isOpen.toString()); + }, + + accessibleFlyout: function(isOpen) { + $(this).attr("aria-hidden", (!isOpen).toString()); + }, + + setFlyoutAttributes: function() { + $toggleFlyouts = $(this); + $toggleFlyouts.each(function() { + $toggle = $(this); + var $handle = $(this).find( + ".FlyoutButton, .Button-Options, .Handle, .editor-action:not(.editor-action-separator)" + ); + var $flyout = $(this).find(".Flyout, .Dropdown"); + var isOpen = $toggle.hasClass(OPEN_CLASS); + + $handle.accessibleFlyoutHandle(isOpen); + $flyout.accessibleFlyout(isOpen); + }); + }, + }); +})(window, jQuery); From d25656c91f3570b6708f30255822791f4a928b31 Mon Sep 17 00:00:00 2001 From: Bogdanova Olga Date: Sun, 28 Feb 2021 23:09:10 +0300 Subject: [PATCH 2/2] Issues-409: Removed 'in recent discussions' option from Announce popup --- .../vanilla/views/discussion/announce.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 vanilla/applications/vanilla/views/discussion/announce.php diff --git a/vanilla/applications/vanilla/views/discussion/announce.php b/vanilla/applications/vanilla/views/discussion/announce.php new file mode 100644 index 0000000..fe481b6 --- /dev/null +++ b/vanilla/applications/vanilla/views/discussion/announce.php @@ -0,0 +1,21 @@ + + +

data('Title'); ?>

+ +Form->open(); +echo $this->Form->errors(); + +echo '
'.t('Where do you want to announce this discussion?').'
'; + +echo '
', $this->Form->radio('Announce', '@'.sprintf(t('In %s.'), $this->data('Category.Name')), ['Value' => '2']), '
'; +// FIX: https://github.com/topcoder-platform/forums/issues/409 +//echo '
', $this->Form->radio('Announce', '@'.sprintf(t('In %s and recent discussions.'), $this->data('Category.Name')), ['Value' => '1']), '
'; +echo '
', $this->Form->radio('Announce', '@'.t("Don't announce."), ['Value' => '0']), '
'; + +echo '
'; +echo $this->Form->button('OK'); +echo $this->Form->button('Cancel', ['type' => 'button', 'class' => 'Button Close']); +echo '
'; +echo $this->Form->close(); +?>